@@ -1313,6 +1313,11 @@ struct RowHighlight {
#[derive(Clone, Debug)]
struct AddSelectionsState {
+ groups: Vec<AddSelectionsGroup>,
+}
+
+#[derive(Clone, Debug)]
+struct AddSelectionsGroup {
above: bool,
stack: Vec<usize>,
}
@@ -2717,7 +2722,9 @@ impl Editor {
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
- self.add_selections_state = None;
+ if self.selections.count() == 1 {
+ self.add_selections_state = None;
+ }
self.select_next_state = None;
self.select_prev_state = None;
self.select_syntax_node_history.try_clear();
@@ -12699,49 +12706,74 @@ impl Editor {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let mut selections = self.selections.all::<Point>(cx);
+ let all_selections = self.selections.all::<Point>(cx);
let text_layout_details = self.text_layout_details(window);
- let mut state = self.add_selections_state.take().unwrap_or_else(|| {
- let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
- let range = oldest_selection.display_range(&display_map).sorted();
+ let (mut columnar_selections, new_selections_to_columnarize) = {
+ if let Some(state) = self.add_selections_state.as_ref() {
+ let columnar_selection_ids: HashSet<_> = state
+ .groups
+ .iter()
+ .flat_map(|group| group.stack.iter())
+ .copied()
+ .collect();
+
+ all_selections
+ .into_iter()
+ .partition(|s| columnar_selection_ids.contains(&s.id))
+ } else {
+ (Vec::new(), all_selections)
+ }
+ };
+
+ let mut state = self
+ .add_selections_state
+ .take()
+ .unwrap_or_else(|| AddSelectionsState { groups: Vec::new() });
+
+ for selection in new_selections_to_columnarize {
+ let range = selection.display_range(&display_map).sorted();
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
let positions = start_x.min(end_x)..start_x.max(end_x);
-
- selections.clear();
let mut stack = Vec::new();
for row in range.start.row().0..=range.end.row().0 {
if let Some(selection) = self.selections.build_columnar_selection(
&display_map,
DisplayRow(row),
&positions,
- oldest_selection.reversed,
+ selection.reversed,
&text_layout_details,
) {
stack.push(selection.id);
- selections.push(selection);
+ columnar_selections.push(selection);
}
}
-
- if above {
- stack.reverse();
+ if !stack.is_empty() {
+ if above {
+ stack.reverse();
+ }
+ state.groups.push(AddSelectionsGroup { above, stack });
}
+ }
- AddSelectionsState { above, stack }
- });
+ let mut final_selections = Vec::new();
+ let end_row = if above {
+ DisplayRow(0)
+ } else {
+ display_map.max_point().row()
+ };
- let last_added_selection = *state.stack.last().unwrap();
- let mut new_selections = Vec::new();
- if above == state.above {
- let end_row = if above {
- DisplayRow(0)
- } else {
- display_map.max_point().row()
- };
+ let mut last_added_item_per_group = HashMap::default();
+ for group in state.groups.iter_mut() {
+ if let Some(last_id) = group.stack.last() {
+ last_added_item_per_group.insert(*last_id, group);
+ }
+ }
- 'outer: for selection in selections {
- if selection.id == last_added_selection {
+ for selection in columnar_selections {
+ if let Some(group) = last_added_item_per_group.get_mut(&selection.id) {
+ if above == group.above {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let mut row = range.start.row();
@@ -12756,13 +12788,13 @@ impl Editor {
start_x.min(end_x)..start_x.max(end_x)
};
+ let mut maybe_new_selection = None;
while row != end_row {
if above {
row.0 -= 1;
} else {
row.0 += 1;
}
-
if let Some(new_selection) = self.selections.build_columnar_selection(
&display_map,
row,
@@ -12770,32 +12802,50 @@ impl Editor {
selection.reversed,
&text_layout_details,
) {
- state.stack.push(new_selection.id);
- if above {
- new_selections.push(new_selection);
- new_selections.push(selection);
- } else {
- new_selections.push(selection);
- new_selections.push(new_selection);
- }
+ maybe_new_selection = Some(new_selection);
+ break;
+ }
+ }
- continue 'outer;
+ if let Some(new_selection) = maybe_new_selection {
+ group.stack.push(new_selection.id);
+ if above {
+ final_selections.push(new_selection);
+ final_selections.push(selection);
+ } else {
+ final_selections.push(selection);
+ final_selections.push(new_selection);
}
+ } else {
+ final_selections.push(selection);
}
+ } else {
+ group.stack.pop();
}
-
- new_selections.push(selection);
+ } else {
+ final_selections.push(selection);
}
- } else {
- new_selections = selections;
- new_selections.retain(|s| s.id != last_added_selection);
- state.stack.pop();
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(new_selections);
+ s.select(final_selections);
+ });
+
+ let final_selection_ids: HashSet<_> = self
+ .selections
+ .all::<Point>(cx)
+ .iter()
+ .map(|s| s.id)
+ .collect();
+ state.groups.retain_mut(|group| {
+ // selections might get merged above so we remove invalid items from stacks
+ group.stack.retain(|id| final_selection_ids.contains(id));
+
+ // single selection in stack can be treated as initial state
+ group.stack.len() > 1
});
- if state.stack.len() > 1 {
+
+ if !state.groups.is_empty() {
self.add_selections_state = Some(state);
}
}
@@ -6300,6 +6300,296 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
));
}
+#[gpui::test]
+async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state(indoc!(
+ r#"line onˇe
+ liˇne two
+ line three
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // test multiple cursors expand in the same direction
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ liˇne three
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // test multiple cursors expand below overflow
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ liˇne thˇree
+ liˇne foˇur"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test multiple cursors retrieves back correctly
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ liˇne thˇree
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
+ cx.assert_editor_state(indoc!(
+ r#"liˇne onˇe
+ liˇne two
+ line three
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.undo_selection(&Default::default(), window, cx);
+ });
+
+ // test undo
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ line three
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.redo_selection(&Default::default(), window, cx);
+ });
+
+ // test redo
+ cx.assert_editor_state(indoc!(
+ r#"liˇne onˇe
+ liˇne two
+ line three
+ line four"#
+ ));
+
+ cx.set_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ ijkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test multiple selections expand in the same direction
+ cx.assert_editor_state(indoc!(
+ r#"ab«cdˇ»
+ ef«ghˇ»
+ «iˇ»jkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test multiple selection upward overflow
+ cx.assert_editor_state(indoc!(
+ r#"ab«cdˇ»
+ «eˇ»f«ghˇ»
+ «iˇ»jkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // test multiple selection retrieves back correctly
+ cx.assert_editor_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ «iˇ»jkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
+ cx.assert_editor_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ ij«klˇ»
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.undo_selection(&Default::default(), window, cx);
+ });
+
+ // test undo
+ cx.assert_editor_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ «iˇ»jkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.redo_selection(&Default::default(), window, cx);
+ });
+
+ // test redo
+ cx.assert_editor_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ ij«klˇ»
+ «mˇ»nop"#
+ ));
+}
+
+#[gpui::test]
+async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state(indoc!(
+ r#"line onˇe
+ liˇne two
+ line three
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ editor.add_selection_below(&Default::default(), window, cx);
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // initial state with two multi cursor groups
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ liˇne thˇree
+ liˇne foˇur"#
+ ));
+
+ // add single cursor in middle - simulate opt click
+ cx.update_editor(|editor, window, cx| {
+ let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
+ editor.begin_selection(new_cursor_point, true, 1, window, cx);
+ editor.end_selection(window, cx);
+ });
+
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇne twˇo
+ liˇneˇ thˇree
+ liˇne foˇur"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test new added selection expands above and existing selection shrinks
+ cx.assert_editor_state(indoc!(
+ r#"line onˇe
+ liˇneˇ twˇo
+ liˇneˇ thˇree
+ line four"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ // test new added selection expands above and existing selection shrinks
+ cx.assert_editor_state(indoc!(
+ r#"lineˇ onˇe
+ liˇneˇ twˇo
+ lineˇ three
+ line four"#
+ ));
+
+ // intial state with two selection groups
+ cx.set_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ ijkl
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_above(&Default::default(), window, cx);
+ editor.add_selection_above(&Default::default(), window, cx);
+ });
+
+ cx.assert_editor_state(indoc!(
+ r#"ab«cdˇ»
+ «eˇ»f«ghˇ»
+ «iˇ»jkl
+ «mˇ»nop"#
+ ));
+
+ // add single selection in middle - simulate opt drag
+ cx.update_editor(|editor, window, cx| {
+ let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
+ editor.begin_selection(new_cursor_point, true, 1, window, cx);
+ editor.update_selection(
+ DisplayPoint::new(DisplayRow(2), 4),
+ 0,
+ gpui::Point::<f32>::default(),
+ window,
+ cx,
+ );
+ editor.end_selection(window, cx);
+ });
+
+ cx.assert_editor_state(indoc!(
+ r#"ab«cdˇ»
+ «eˇ»f«ghˇ»
+ «iˇ»jk«lˇ»
+ «mˇ»nop"#
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.add_selection_below(&Default::default(), window, cx);
+ });
+
+ // test new added selection expands below, others shrinks from above
+ cx.assert_editor_state(indoc!(
+ r#"abcd
+ ef«ghˇ»
+ «iˇ»jk«lˇ»
+ «mˇ»no«pˇ»"#
+ ));
+}
+
#[gpui::test]
async fn test_select_next(cx: &mut TestAppContext) {
init_test(cx, |_| {});