editor: Add multi cursor support for AddSelectionAbove/AddSelectionBelow (#32204)

Closes #31648

This PR adds support for:
- Expanding multiple cursors above/below
- Expanding multiple selections above/below
- Adding new cursors/selections when expansion has already been done.
Existing expansions preserve their state and expand/shrink according to
the action, while new cursors/selections act like freshly created ones.

Tests for both cursor and selections:
- below/above cases
- undo/redo cases
- adding new cursors/selections with existing expansion

Before/After:


https://github.com/user-attachments/assets/d2fd556b-8972-4719-bd86-e633d42a1aa3


Release Notes:

- Improved `AddSelectionAbove` and `AddSelectionBelow` to extend
multiple cursors/selections.
This commit is contained in:
Smit Barmase 2025-06-06 06:20:12 +05:30 committed by GitHub
parent 711a9e5753
commit 6a8fdbfd62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 383 additions and 43 deletions

View file

@ -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
«»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ˇ»
«»jkl
«»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ˇ»
«»f«ghˇ»
«»jkl
«»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ˇ»
«»jkl
«»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ˇ»
«»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ˇ»
«»jkl
«»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ˇ»
«»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
«»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ˇ»
«»f«ghˇ»
«»jkl
«»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ˇ»
«»f«ghˇ»
«»jk«»
«»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ˇ»
«»jk«»
«»no«»"#
));
}
#[gpui::test]
async fn test_select_next(cx: &mut TestAppContext) {
init_test(cx, |_| {});