aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-02-16 17:55:23 +0100
committerGitHub <noreply@github.com>2025-02-16 17:55:23 +0100
commitb04cddc35243115410e73c470d55bb4e406818a5 (patch)
tree31f6f54d61475e3be81dad3e5081ae12a721a68f
parentdocs(changelog): tips on startup (diff)
downloadsl0ppij-b04cddc35243115410e73c470d55bb4e406818a5.tar.gz
sl0ppij-b04cddc35243115410e73c470d55bb4e406818a5.tar.xz
sl0ppij-b04cddc35243115410e73c470d55bb4e406818a5.zip
feat(mouse): double-click to mark word boundaries, triple-click to mark paragraph (#3996)
* double-triple click on word/line boundaries * adjust snapshots * style(fmt): rustfmt
-rw-r--r--zellij-server/src/os_input_output.rs1
-rw-r--r--zellij-server/src/panes/grid.rs244
-rw-r--r--zellij-server/src/panes/selection.rs5
-rw-r--r--zellij-server/src/panes/terminal_pane.rs1
-rw-r--r--zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest1_3.snap12
-rw-r--r--zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_0.snap4
-rw-r--r--zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_2.snap4
-rw-r--r--zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_4.snap4
-rw-r--r--zellij-server/src/tab/mod.rs1
9 files changed, 259 insertions, 17 deletions
diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs
index e4263857..1ac4d64e 100644
--- a/zellij-server/src/os_input_output.rs
+++ b/zellij-server/src/os_input_output.rs
@@ -388,6 +388,7 @@ impl ClientSender {
sender.send(msg).with_context(err_context).non_fatal();
}
// If we're here, the message buffer is broken for some reason
+ log::error!("Client buffer overflow!");
let _ = sender.send(ServerToClientMsg::Exit(ExitReason::Disconnect));
});
ClientSender {
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index 34f68986..4e0ba9b8 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -365,7 +365,47 @@ pub struct Grid {
styled_underlines: bool,
pub supports_kitty_keyboard_protocol: bool, // has the app requested kitty keyboard support?
explicitly_disable_kitty_keyboard_protocol: bool, // has kitty keyboard support been explicitly
- // disabled by user config?
+ // disabled by user config?
+ click: Click,
+}
+
+const CLICK_TIME_THRESHOLD: u128 = 400; // Doherty Threshold
+
+#[derive(Clone, Debug, Default)]
+struct Click {
+ position_and_time: Option<(Position, std::time::Instant)>,
+ count: usize,
+}
+
+impl Click {
+ pub fn record_click(&mut self, position: Position) {
+ let click_is_same_position_as_last_click = self
+ .position_and_time
+ .map(|(p, _t)| p == position)
+ .unwrap_or(false);
+ let click_is_within_time_threshold = self
+ .position_and_time
+ .map(|(_p, t)| t.elapsed().as_millis() <= CLICK_TIME_THRESHOLD)
+ .unwrap_or(false);
+ if click_is_same_position_as_last_click && click_is_within_time_threshold {
+ self.count += 1;
+ } else {
+ self.count = 1;
+ }
+ self.position_and_time = Some((position, std::time::Instant::now()));
+ if self.count == 4 {
+ self.reset();
+ }
+ }
+ pub fn is_double_click(&self) -> bool {
+ self.count == 2
+ }
+ pub fn is_triple_click(&self) -> bool {
+ self.count == 3
+ }
+ pub fn reset(&mut self) {
+ self.count = 0;
+ }
}
#[derive(Clone, Debug)]
@@ -513,6 +553,7 @@ impl Grid {
lock_renders: false,
supports_kitty_keyboard_protocol: false,
explicitly_disable_kitty_keyboard_protocol,
+ click: Click::default(),
}
}
pub fn render_full_viewport(&mut self) {
@@ -1478,6 +1519,8 @@ impl Grid {
let line_wrapped_row = Row::new();
self.viewport.push(line_wrapped_row);
self.output_buffer.update_line(self.cursor.y);
+ } else if let Some(current_line) = self.viewport.get_mut(self.cursor.y) {
+ current_line.is_canonical = false;
}
}
}
@@ -1750,6 +1793,39 @@ impl Grid {
}
pub fn start_selection(&mut self, start: &Position) {
let old_selection = self.selection;
+ self.click.record_click(*start);
+
+ if self.click.is_double_click() {
+ let Some((start_position, end_position)) = self.word_around_position(&start) else {
+ // no-op
+ return;
+ };
+ self.selection
+ .set_start_and_end_positions(start_position, end_position);
+ for i in std::cmp::min(start_position.line.0, end_position.line.0)
+ ..=std::cmp::max(start_position.line.0, end_position.line.0)
+ {
+ self.output_buffer.update_line(i as usize);
+ }
+ self.mark_for_rerender();
+ return;
+ } else if self.click.is_triple_click() {
+ let Some((start_position, end_position)) = self.canonical_line_around_position(&start)
+ else {
+ // no-op
+ return;
+ };
+ self.selection
+ .set_start_and_end_positions(start_position, end_position);
+ for i in std::cmp::min(start_position.line.0, end_position.line.0)
+ ..=std::cmp::max(start_position.line.0, end_position.line.0)
+ {
+ self.output_buffer.update_line(i as usize);
+ }
+ self.mark_for_rerender();
+ return;
+ }
+
self.selection.start(*start);
self.update_selected_lines(&old_selection, &self.selection.clone());
self.mark_for_rerender();
@@ -1764,9 +1840,11 @@ impl Grid {
}
pub fn end_selection(&mut self, end: &Position) {
- let old_selection = self.selection;
- self.selection.end(*end);
- self.update_selected_lines(&old_selection, &self.selection.clone());
+ if !self.click.is_double_click() && !self.click.is_triple_click() {
+ let old_selection = self.selection;
+ self.selection.end(*end);
+ self.update_selected_lines(&old_selection, &self.selection.clone());
+ }
self.mark_for_rerender();
}
@@ -1860,6 +1938,87 @@ impl Grid {
pub fn absolute_position_in_scrollback(&self) -> usize {
self.lines_above.len() + self.cursor.y
}
+ pub fn word_around_position(&self, position: &Position) -> Option<(Position, Position)> {
+ let position_row = self.viewport.get(position.line.0 as usize)?;
+ let (index_start, index_end) =
+ position_row.word_indices_around_character_index(position.column.0)?;
+
+ let mut position_start = Position::new(position.line.0 as i32, index_start as u16);
+ let mut position_end = Position::new(position.line.0 as i32, index_end as u16);
+ let mut position_row_is_canonical = position_row.is_canonical;
+
+ while !position_row_is_canonical && position_start.column.0 == 0 {
+ if let Some(position_row_above) = self
+ .viewport
+ .get(position_start.line.0.saturating_sub(1) as usize)
+ {
+ let new_start_index = position_row_above.word_start_index_of_last_character();
+ position_start = Position::new(
+ position_start.line.0.saturating_sub(1) as i32,
+ new_start_index as u16,
+ );
+ position_row_is_canonical = position_row_above.is_canonical;
+ } else {
+ break;
+ }
+ }
+
+ let mut column_count_in_row = position_row.columns.len();
+ while position_end.column.0 == column_count_in_row {
+ if let Some(position_row_below) = self.viewport.get(position_end.line.0 as usize + 1) {
+ if position_row_below.is_canonical {
+ break;
+ }
+ let new_end_index = position_row_below.word_end_index_of_first_character();
+ position_end = Position::new(position_end.line.0 as i32 + 1, new_end_index as u16);
+ column_count_in_row = position_row_below.columns.len();
+ } else {
+ break;
+ }
+ }
+
+ Some((position_start, position_end))
+ }
+ pub fn canonical_line_around_position(
+ &self,
+ position: &Position,
+ ) -> Option<(Position, Position)> {
+ let position_row = self.viewport.get(position.line.0 as usize)?;
+
+ let mut position_start = Position::new(position.line.0 as i32, 0);
+ let mut position_end =
+ Position::new(position.line.0 as i32, position_row.columns.len() as u16);
+
+ let mut found_canonical_row_start = position_row.is_canonical;
+ while !found_canonical_row_start {
+ if let Some(row_above) = self
+ .viewport
+ .get(position_start.line.0.saturating_sub(1) as usize)
+ {
+ position_start.line.0 = position_start.line.0.saturating_sub(1);
+ found_canonical_row_start = row_above.is_canonical;
+ } else {
+ break;
+ }
+ }
+
+ let mut found_canonical_row_end = false;
+ while !found_canonical_row_end {
+ if let Some(row_below) = self.viewport.get(position_end.line.0 as usize + 1) {
+ if row_below.is_canonical {
+ found_canonical_row_end = true;
+ } else {
+ position_end = Position::new(
+ position_end.line.0 as i32 + 1,
+ row_below.columns.len() as u16,
+ );
+ }
+ } else {
+ break;
+ }
+ }
+ Some((position_start, position_end))
+ }
fn update_selected_lines(&mut self, old_selection: &Selection, new_selection: &Selection) {
for l in old_selection.diff(new_selection, self.height) {
@@ -3644,6 +3803,83 @@ impl Row {
self.width = None;
parts
}
+ pub fn word_indices_around_character_index(&self, index: usize) -> Option<(usize, usize)> {
+ let character_at_index = self.columns.get(index)?;
+ if is_selection_boundary_character(character_at_index.character) {
+ return Some((index, index + 1));
+ }
+ let mut end_position = self
+ .columns
+ .iter()
+ .enumerate()
+ .skip(index)
+ .find_map(|(i, t_c)| {
+ if is_selection_boundary_character(t_c.character) {
+ Some(i)
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| self.columns.len());
+ let start_position = self
+ .columns
+ .iter()
+ .enumerate()
+ .take(index)
+ .rev()
+ .find_map(|(i, t_c)| {
+ if is_selection_boundary_character(t_c.character) {
+ Some(i + 1)
+ } else {
+ None
+ }
+ })
+ .unwrap_or(0);
+ if start_position == end_position {
+ // so that if this is only one character, it'll still be marked
+ end_position += 1;
+ }
+ Some((start_position, end_position))
+ }
+ pub fn word_start_index_of_last_character(&self) -> usize {
+ self.columns
+ .iter()
+ .enumerate()
+ .rev()
+ .find_map(|(i, t_c)| {
+ if is_selection_boundary_character(t_c.character) {
+ Some(i + 1)
+ } else {
+ None
+ }
+ })
+ .unwrap_or(0)
+ }
+ pub fn word_end_index_of_first_character(&self) -> usize {
+ self.columns
+ .iter()
+ .enumerate()
+ .find_map(|(i, t_c)| {
+ if is_selection_boundary_character(t_c.character) {
+ Some(i)
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| self.columns.len())
+ }
+}
+
+fn is_selection_boundary_character(character: char) -> bool {
+ character.is_ascii_whitespace()
+ || character == '['
+ || character == ']'
+ || character == '{'
+ || character == '}'
+ || character == '<'
+ || character == '>'
+ || character == '('
+ || character == ')'
}
#[cfg(test)]
diff --git a/zellij-server/src/panes/selection.rs b/zellij-server/src/panes/selection.rs
index cfe10c5c..cc0fa675 100644
--- a/zellij-server/src/panes/selection.rs
+++ b/zellij-server/src/panes/selection.rs
@@ -37,6 +37,11 @@ impl Selection {
self.end = end;
}
+ pub fn set_start_and_end_positions(&mut self, start: Position, end: Position) {
+ self.start = start;
+ self.end = end;
+ }
+
pub fn contains(&self, row: usize, col: usize) -> bool {
let row = row as isize;
let (start, end) = if self.start <= self.end {
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index 3ad21ddc..186b8ac5 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -220,6 +220,7 @@ impl Pane for TerminalPane {
// here we match against those cases - if need be, we adjust the input and if not
// we send back the original input
+ self.reset_selection();
if !self.grid.bracketed_paste_mode {
// Zellij itself operates in bracketed paste mode, so the terminal sends these
// instructions (bracketed paste start and bracketed paste end respectively)
diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest1_3.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest1_3.snap
index f33ce414..1052e377 100644
--- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest1_3.snap
+++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest1_3.snap
@@ -1,28 +1,28 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
+assertion_line: 148
expression: "format!(\"{:?}\", grid)"
-
---
00 (C): Test of autowrap, mixing control and print characters.
01 (C): The left/right margins should have letters in order:
02 (C): I i
-03 (C): J j
+03 (W): J j
04 (C): K k
05 (C): L l
06 (C): M m
-07 (C): N n
+07 (W): N n
08 (C): O o
09 (C): P p
10 (C): Q q
-11 (C): R r
+11 (W): R r
12 (C): S s
13 (C): T t
14 (C): U u
-15 (C): V v
+15 (W): V v
16 (C): W w
17 (C): X x
18 (C): Y y
-19 (C): Z z
+19 (W): Z z
20 (C):
21 (C): Push <RETURN>
22 (C):
diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_0.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_0.snap
index a8250e29..408dbb32 100644
--- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_0.snap
+++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_0.snap
@@ -1,10 +1,10 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
+assertion_line: 241
expression: "format!(\"{:?}\", grid)"
-
---
00 (C): **************************************************************************************************************
-01 (C): **************************************************
+01 (W): **************************************************
02 (C): **************************************************************************************************************
03 (C):
04 (C): This should be three identical lines of *'s completely filling
diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_2.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_2.snap
index d751b7b5..10c900a8 100644
--- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_2.snap
+++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_2.snap
@@ -1,10 +1,10 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
+assertion_line: 303
expression: "format!(\"{:?}\", grid)"
-
---
00 (C): 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
-01 (C): 123456789012345678901
+01 (W): 123456789012345678901
02 (C): This is 132 column mode, light background.
03 (C): This is 132 column mode, light background.
04 (C): This is 132 column mode, light background.
diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_4.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_4.snap
index 7c819279..8d9822c2 100644
--- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_4.snap
+++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__vttest2_4.snap
@@ -1,10 +1,10 @@
---
source: zellij-server/src/panes/./unit/grid_tests.rs
+assertion_line: 365
expression: "format!(\"{:?}\", grid)"
-
---
00 (C): 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
-01 (C): 123456789012345678901
+01 (W): 123456789012345678901
02 (C): This is 132 column mode, dark background.
03 (C): This is 132 column mode, dark background.
04 (C): This is 132 column mode, dark background.
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 8d94744c..e5a7e217 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -3587,7 +3587,6 @@ impl Tab {
if let PaneId::Terminal(_) = pane_with_selection.pid() {
if copy_on_release {
let selected_text = pane_with_selection.get_selected_text();
- pane_with_selection.reset_selection();
if let Some(selected_text) = selected_text {
self.write_selection_to_clipboard(&selected_text)