diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 62f0b5f2..6a9d2c1e 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1835,9 +1835,31 @@ impl Grid { pub fn update_selection(&mut self, to: &Position) { let old_selection = self.selection; if &old_selection.end != to { - self.selection.to(*to); - self.update_selected_lines(&old_selection, &self.selection.clone()); - self.mark_for_rerender(); + if self.click.is_double_click() { + let Some((word_start_position, word_end_position)) = self.word_around_position(&to) + else { + // no-op + return; + }; + self.selection + .add_word_to_position(word_start_position, word_end_position); + let current_selection = self.selection; + self.update_selected_lines(&old_selection, ¤t_selection); + self.mark_for_rerender(); + } else if self.click.is_triple_click() { + let Some(last_index_in_line) = self.last_index_in_line(&to) else { + return; + }; + self.selection + .add_line_to_position(to.line.0, last_index_in_line); + let current_selection = self.selection; + self.update_selected_lines(&old_selection, ¤t_selection); + self.mark_for_rerender(); + } else { + self.selection.to(*to); + self.update_selected_lines(&old_selection, &self.selection.clone()); + self.mark_for_rerender(); + } } } @@ -1940,6 +1962,10 @@ impl Grid { pub fn absolute_position_in_scrollback(&self) -> usize { self.lines_above.len() + self.cursor.y } + pub fn last_index_in_line(&self, position: &Position) -> Option { + let position_row = self.viewport.get(position.line.0 as usize)?; + Some(position_row.last_index_in_line()) + } 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) = @@ -3811,6 +3837,9 @@ impl Row { self.width = None; parts } + pub fn last_index_in_line(&self) -> usize { + self.columns.len() + } 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) { diff --git a/zellij-server/src/panes/selection.rs b/zellij-server/src/panes/selection.rs index cc0fa675..343ebfde 100644 --- a/zellij-server/src/panes/selection.rs +++ b/zellij-server/src/panes/selection.rs @@ -9,6 +9,8 @@ pub struct Selection { pub start: Position, pub end: Position, active: bool, // used to handle moving the selection up and down + last_added_word_position: Option<(Position, Position)>, // (start / end) + last_added_line_index: Option, } impl Default for Selection { @@ -17,6 +19,8 @@ impl Default for Selection { start: Position::new(0, 0), end: Position::new(0, 0), active: false, + last_added_word_position: None, + last_added_line_index: None, } } } @@ -38,8 +42,120 @@ impl Selection { } pub fn set_start_and_end_positions(&mut self, start: Position, end: Position) { + self.active = true; self.start = start; self.end = end; + self.last_added_word_position = Some((start, end)); + self.last_added_line_index = Some(start.line.0); + } + pub fn add_word_to_position(&mut self, word_start: Position, word_end: Position) { + // here we assume word_start is smaller or equal to word_end + let already_added = self + .last_added_word_position + .map(|(last_word_start, last_word_end)| { + last_word_start == word_start && last_word_end == word_end + }) + .unwrap_or(false); + if already_added { + return; + } + let word_is_above_last_added_word = self + .last_added_word_position + .map(|(l_start, _l_end)| word_start.line < l_start.line) + .unwrap_or(false); + let word_is_below_last_added_word = self + .last_added_word_position + .map(|(_l_start, l_end)| word_end.line > l_end.line) + .unwrap_or(false); + if word_is_above_last_added_word && word_start.line < self.start.line { + // extend line above + self.start = word_start; + } else if word_is_below_last_added_word && word_end.line > self.end.line { + // extend line below + self.end = word_end; + } else if word_is_below_last_added_word && word_start.line > self.start.line { + // reduce from above + self.start = word_start; + } else if word_is_above_last_added_word && word_end.line < self.end.line { + // reduce from below + self.end = word_end; + } else { + let word_end_is_to_the_left_of_last_word_start = self + .last_added_word_position + .map(|(l_start, _l_end)| word_end.column <= l_start.column) + .unwrap_or(false); + let word_start_is_to_the_right_of_last_word_end = self + .last_added_word_position + .map(|(_l_start, l_end)| word_start.column >= l_end.column) + .unwrap_or(false); + let last_word_start_equals_word_end = self + .last_added_word_position + .map(|(l_start, _l_end)| l_start.column == word_end.column) + .unwrap_or(false); + let last_word_end_equals_word_start = self + .last_added_word_position + .map(|(_l_start, l_end)| l_end.column == word_start.column) + .unwrap_or(false); + let selection_start_column_is_to_the_right_of_word_start = + self.start.column > word_start.column; + let selection_start_is_on_same_line_as_word_start = self.start.line == word_start.line; + let selection_end_is_to_the_left_of_word_end = self.end.column < word_end.column; + let selection_end_is_on_same_line_as_word_end = self.end.line == word_end.line; + if word_end_is_to_the_left_of_last_word_start + && selection_start_column_is_to_the_right_of_word_start + && selection_start_is_on_same_line_as_word_start + { + // extend selection left + self.start.column = word_start.column; + } else if word_start_is_to_the_right_of_last_word_end + && selection_end_is_to_the_left_of_word_end + && selection_end_is_on_same_line_as_word_end + { + // extend selection right + self.end.column = word_end.column; + } else if last_word_start_equals_word_end { + // reduce selection from the right + self.end.column = word_end.column; + } else if last_word_end_equals_word_start { + // reduce selection from the left + self.start.column = word_start.column; + } + } + self.last_added_word_position = Some((word_start, word_end)); + } + pub fn add_line_to_position(&mut self, line_index: isize, last_index_in_line: usize) { + let already_added = self + .last_added_line_index + .map(|last_added_line_index| last_added_line_index == line_index) + .unwrap_or(false); + if already_added { + return; + } + let line_index_is_smaller_than_last_added_line_index = self + .last_added_line_index + .map(|last| line_index < last) + .unwrap_or(false); + let line_index_is_larger_than_last_added_line_index = self + .last_added_line_index + .map(|last| line_index > last) + .unwrap_or(false); + + if line_index_is_smaller_than_last_added_line_index && self.start.line.0 > line_index { + // extend selection one line upwards + self.start = Position::new(line_index as i32, 0); + } else if line_index_is_larger_than_last_added_line_index && self.end.line.0 < line_index { + // extend selection one line downwards + self.end = Position::new(line_index as i32, last_index_in_line as u16); + } else if line_index_is_smaller_than_last_added_line_index && self.end.line.0 > line_index { + // reduce selection one line from below + self.end = Position::new(line_index as i32, last_index_in_line as u16); + } else if line_index_is_larger_than_last_added_line_index && self.start.line.0 < line_index + { + // reduce selection one line from above + self.start = Position::new(line_index as i32, 0); + } + + self.last_added_line_index = Some(line_index); } pub fn contains(&self, row: usize, col: usize) -> bool { @@ -91,6 +207,8 @@ impl Selection { start, end, active: self.active, + last_added_word_position: self.last_added_word_position, + last_added_line_index: self.last_added_line_index, } } diff --git a/zellij-server/src/panes/unit/selection_tests.rs b/zellij-server/src/panes/unit/selection_tests.rs index 00cd61c7..778172f2 100644 --- a/zellij-server/src/panes/unit/selection_tests.rs +++ b/zellij-server/src/panes/unit/selection_tests.rs @@ -43,6 +43,8 @@ fn contains() { start: Position::new(10, 5), end: Position::new(40, 20), active: false, + last_added_word_position: None, + last_added_line_index: None, }; let test_cases = vec![ @@ -93,6 +95,8 @@ fn sorted() { start: Position::new(1, 1), end: Position::new(10, 2), active: false, + last_added_word_position: None, + last_added_line_index: None, }; let sorted_selection = selection.sorted(); assert_eq!(selection.start, sorted_selection.start); @@ -102,6 +106,8 @@ fn sorted() { start: Position::new(10, 2), end: Position::new(1, 1), active: false, + last_added_word_position: None, + last_added_line_index: None, }; let sorted_selection = selection.sorted(); assert_eq!(selection.end, sorted_selection.start); @@ -114,6 +120,8 @@ fn line_indices() { start: Position::new(1, 1), end: Position::new(10, 2), active: false, + last_added_word_position: None, + last_added_line_index: None, }; assert_eq!(selection.line_indices(), (1..=10)) @@ -127,6 +135,8 @@ fn move_up_inactive() { start, end, active: false, + last_added_word_position: None, + last_added_line_index: None, }; inactive_selection.move_up(2); @@ -145,6 +155,8 @@ fn move_up_active() { start, end, active: true, + last_added_word_position: None, + last_added_line_index: None, }; inactive_selection.move_up(2); @@ -160,6 +172,8 @@ fn move_down_inactive() { start, end, active: false, + last_added_word_position: None, + last_added_line_index: None, }; inactive_selection.move_down(2); @@ -178,9 +192,265 @@ fn move_down_active() { start, end, active: true, + last_added_word_position: None, + last_added_line_index: None, }; inactive_selection.move_down(2); assert_eq!(inactive_selection.start, Position::new(12, 1)); assert_eq!(inactive_selection.end, end); } + +#[test] +fn add_word_to_position_extend_line_above() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(10, 10); + let last_word_end = Position::new(10, 15); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(9, 5); + let word_end = Position::new(9, 6); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, word_start); + assert_eq!(selection.end, selection_end); +} + +#[test] +fn add_word_to_position_extend_line_below() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(20, 15); + let last_word_end = Position::new(20, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(21, 5); + let word_end = Position::new(21, 6); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, selection_start); + assert_eq!(selection.end, word_end); +} + +#[test] +fn add_word_to_position_reduce_from_above() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(10, 10); + let last_word_end = Position::new(10, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(11, 5); + let word_end = Position::new(11, 6); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, word_start); + assert_eq!(selection.end, selection_end); +} + +#[test] +fn add_word_to_position_reduce_from_below() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(20, 10); + let last_word_end = Position::new(20, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(19, 5); + let word_end = Position::new(19, 6); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, selection_start); + assert_eq!(selection.end, word_end); +} + +#[test] +fn add_word_to_position_extend_right() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(20, 10); + let last_word_end = Position::new(20, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(20, 21); + let word_end = Position::new(20, 23); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, selection_start); + assert_eq!(selection.end, word_end); +} + +#[test] +fn add_word_to_position_extend_left() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(10, 10); + let last_word_end = Position::new(10, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(10, 5); + let word_end = Position::new(10, 9); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, word_start); + assert_eq!(selection.end, selection_end); +} + +#[test] +fn add_word_to_position_reduce_from_left() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(10, 10); + let last_word_end = Position::new(10, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(10, 20); + let word_end = Position::new(10, 30); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, word_start); + assert_eq!(selection.end, selection_end); +} + +#[test] +fn add_word_to_position_reduce_from_right() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_word_start = Position::new(20, 10); + let last_word_end = Position::new(20, 20); + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: Some((last_word_start, last_word_end)), + last_added_line_index: None, + }; + let word_start = Position::new(20, 5); + let word_end = Position::new(20, 10); + selection.add_word_to_position(word_start, word_end); + + assert_eq!(selection.start, selection_start); + assert_eq!(selection.end, word_end); +} + +#[test] +fn add_line_to_position_extend_upwards() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_added_line_index = 10; + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: None, + last_added_line_index: Some(last_added_line_index), + }; + let line_index_to_add = 9; + let last_index_in_line = 21; + selection.add_line_to_position(line_index_to_add, last_index_in_line); + + assert_eq!(selection.start, Position::new(line_index_to_add as i32, 0)); + assert_eq!(selection.end, selection_end); +} + +#[test] +fn add_line_to_position_extend_downwards() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_added_line_index = 20; + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: None, + last_added_line_index: Some(last_added_line_index), + }; + let line_index_to_add = 21; + let last_index_in_line = 21; + selection.add_line_to_position(line_index_to_add, last_index_in_line); + + assert_eq!(selection.start, selection_start); + assert_eq!( + selection.end, + Position::new(line_index_to_add as i32, last_index_in_line as u16) + ); +} + +#[test] +fn add_line_to_position_reduce_from_below() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_added_line_index = 20; + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: None, + last_added_line_index: Some(last_added_line_index), + }; + let line_index_to_add = 19; + let last_index_in_line = 21; + selection.add_line_to_position(line_index_to_add, last_index_in_line); + + assert_eq!(selection.start, selection_start); + assert_eq!( + selection.end, + Position::new(line_index_to_add as i32, last_index_in_line as u16) + ); +} + +#[test] +fn add_line_to_position_reduce_from_above() { + let selection_start = Position::new(10, 10); + let selection_end = Position::new(20, 20); + let last_added_line_index = 10; + let mut selection = Selection { + start: selection_start, + end: selection_end, + active: true, + last_added_word_position: None, + last_added_line_index: Some(last_added_line_index), + }; + let line_index_to_add = 9; + let last_index_in_line = 21; + selection.add_line_to_position(line_index_to_add, last_index_in_line); + + assert_eq!(selection.start, Position::new(line_index_to_add as i32, 0)); + assert_eq!(selection.end, selection_end); +}