fix(selection): add proper multi-click dragging options (#4052)

* properly extend/reduce word-bound selection on drag

* properly extend/reduce line bound selection on drag

* fix scrolling

* style(fmt): rustfmt

---------

Co-authored-by: aram <aram@green.green>
This commit is contained in:
Aram Drevekenin 2025-03-10 17:21:25 +01:00 committed by GitHub
parent 4fd0bac675
commit ff595fccb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 420 additions and 3 deletions

View file

@ -1835,9 +1835,31 @@ impl Grid {
pub fn update_selection(&mut self, to: &Position) { pub fn update_selection(&mut self, to: &Position) {
let old_selection = self.selection; let old_selection = self.selection;
if &old_selection.end != to { if &old_selection.end != to {
self.selection.to(*to); if self.click.is_double_click() {
self.update_selected_lines(&old_selection, &self.selection.clone()); let Some((word_start_position, word_end_position)) = self.word_around_position(&to)
self.mark_for_rerender(); 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, &current_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, &current_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 { pub fn absolute_position_in_scrollback(&self) -> usize {
self.lines_above.len() + self.cursor.y self.lines_above.len() + self.cursor.y
} }
pub fn last_index_in_line(&self, position: &Position) -> Option<usize> {
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)> { pub fn word_around_position(&self, position: &Position) -> Option<(Position, Position)> {
let position_row = self.viewport.get(position.line.0 as usize)?; let position_row = self.viewport.get(position.line.0 as usize)?;
let (index_start, index_end) = let (index_start, index_end) =
@ -3811,6 +3837,9 @@ impl Row {
self.width = None; self.width = None;
parts 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)> { pub fn word_indices_around_character_index(&self, index: usize) -> Option<(usize, usize)> {
let character_at_index = self.columns.get(index)?; let character_at_index = self.columns.get(index)?;
if is_selection_boundary_character(character_at_index.character) { if is_selection_boundary_character(character_at_index.character) {

View file

@ -9,6 +9,8 @@ pub struct Selection {
pub start: Position, pub start: Position,
pub end: Position, pub end: Position,
active: bool, // used to handle moving the selection up and down 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<isize>,
} }
impl Default for Selection { impl Default for Selection {
@ -17,6 +19,8 @@ impl Default for Selection {
start: Position::new(0, 0), start: Position::new(0, 0),
end: Position::new(0, 0), end: Position::new(0, 0),
active: false, 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) { pub fn set_start_and_end_positions(&mut self, start: Position, end: Position) {
self.active = true;
self.start = start; self.start = start;
self.end = end; 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 { pub fn contains(&self, row: usize, col: usize) -> bool {
@ -91,6 +207,8 @@ impl Selection {
start, start,
end, end,
active: self.active, active: self.active,
last_added_word_position: self.last_added_word_position,
last_added_line_index: self.last_added_line_index,
} }
} }

View file

@ -43,6 +43,8 @@ fn contains() {
start: Position::new(10, 5), start: Position::new(10, 5),
end: Position::new(40, 20), end: Position::new(40, 20),
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
let test_cases = vec![ let test_cases = vec![
@ -93,6 +95,8 @@ fn sorted() {
start: Position::new(1, 1), start: Position::new(1, 1),
end: Position::new(10, 2), end: Position::new(10, 2),
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
let sorted_selection = selection.sorted(); let sorted_selection = selection.sorted();
assert_eq!(selection.start, sorted_selection.start); assert_eq!(selection.start, sorted_selection.start);
@ -102,6 +106,8 @@ fn sorted() {
start: Position::new(10, 2), start: Position::new(10, 2),
end: Position::new(1, 1), end: Position::new(1, 1),
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
let sorted_selection = selection.sorted(); let sorted_selection = selection.sorted();
assert_eq!(selection.end, sorted_selection.start); assert_eq!(selection.end, sorted_selection.start);
@ -114,6 +120,8 @@ fn line_indices() {
start: Position::new(1, 1), start: Position::new(1, 1),
end: Position::new(10, 2), end: Position::new(10, 2),
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
assert_eq!(selection.line_indices(), (1..=10)) assert_eq!(selection.line_indices(), (1..=10))
@ -127,6 +135,8 @@ fn move_up_inactive() {
start, start,
end, end,
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
inactive_selection.move_up(2); inactive_selection.move_up(2);
@ -145,6 +155,8 @@ fn move_up_active() {
start, start,
end, end,
active: true, active: true,
last_added_word_position: None,
last_added_line_index: None,
}; };
inactive_selection.move_up(2); inactive_selection.move_up(2);
@ -160,6 +172,8 @@ fn move_down_inactive() {
start, start,
end, end,
active: false, active: false,
last_added_word_position: None,
last_added_line_index: None,
}; };
inactive_selection.move_down(2); inactive_selection.move_down(2);
@ -178,9 +192,265 @@ fn move_down_active() {
start, start,
end, end,
active: true, active: true,
last_added_word_position: None,
last_added_line_index: None,
}; };
inactive_selection.move_down(2); inactive_selection.move_down(2);
assert_eq!(inactive_selection.start, Position::new(12, 1)); assert_eq!(inactive_selection.start, Position::new(12, 1));
assert_eq!(inactive_selection.end, end); 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);
}