From 40577a8e959a40d11470742580a7ba034146ed43 Mon Sep 17 00:00:00 2001 From: Felix Packard Date: Fri, 21 Feb 2025 16:14:28 +0000 Subject: [PATCH] feat: add `stop_at_first_char` option make move/select/delete to beginning of line actions consistent with other beginning of line actions by adding a `stop_at_first_char` option closes #7863 --- assets/keymaps/default-linux.json | 6 +- assets/keymaps/default-macos.json | 6 +- assets/keymaps/linux/emacs.json | 10 +- assets/keymaps/macos/emacs.json | 10 +- crates/editor/src/actions.rs | 13 +- crates/editor/src/editor.rs | 25 ++-- crates/editor/src/editor_tests.rs | 153 +++++++++++++++++----- crates/project_panel/src/project_panel.rs | 1 + 8 files changed, 166 insertions(+), 58 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 5a1a011a7e0a08..20166d2b60e38e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -107,9 +107,9 @@ "ctrl-a": "editor::SelectAll", "ctrl-l": "editor::SelectLine", "ctrl-shift-i": "editor::Format", - // "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }], - // "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], + // "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_first_char": false }], + // "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_first_char": false }], + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_first_char": false }], // "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], // "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 93650e338b16f6..4f55e76e1a50aa 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -116,9 +116,9 @@ "cmd-a": "editor::SelectAll", "cmd-l": "editor::SelectLine", "cmd-shift-i": "editor::Format", - "cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], - "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }], + "cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_first_char": false }], + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_first_char": false }], + "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_first_char": false }], "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 2c1128d8d66922..dbaf64ef096b44 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -22,11 +22,11 @@ "ctrl-b": "editor::MoveLeft", // backward-char "ctrl-n": "editor::MoveDown", // next-line "ctrl-p": "editor::MoveUp", // previous-line - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "alt-f": "editor::MoveToNextSubwordEnd", // forward-word "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word @@ -74,9 +74,9 @@ "ctrl-b": "editor::SelectLeft", "ctrl-n": "editor::SelectDown", "ctrl-p": "editor::SelectUp", - "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-f": "editor::SelectToNextWordEnd", "alt-b": "editor::SelectToPreviousSubwordStart", diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 2c1128d8d66922..dbaf64ef096b44 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -22,11 +22,11 @@ "ctrl-b": "editor::MoveLeft", // backward-char "ctrl-n": "editor::MoveDown", // next-line "ctrl-p": "editor::MoveUp", // previous-line - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line + "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], // move-beginning-of-line "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line "alt-f": "editor::MoveToNextSubwordEnd", // forward-word "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word @@ -74,9 +74,9 @@ "ctrl-b": "editor::SelectLeft", "ctrl-n": "editor::SelectDown", "ctrl-p": "editor::SelectUp", - "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], "end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], - "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false }], + "ctrl-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_first_char": false }], "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-f": "editor::SelectToNextWordEnd", "alt-b": "editor::SelectToPreviousSubwordStart", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 71cffc315bd22d..33fce8f4ca7838 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -22,6 +22,8 @@ pub struct SelectPrevious { pub struct MoveToBeginningOfLine { #[serde(default = "default_true")] pub stop_at_soft_wraps: bool, + #[serde(default)] + pub stop_at_first_char: bool, } #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] @@ -29,6 +31,15 @@ pub struct MoveToBeginningOfLine { pub struct SelectToBeginningOfLine { #[serde(default)] pub(super) stop_at_soft_wraps: bool, + #[serde(default)] + pub(super) stop_at_first_char: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct DeleteToBeginningOfLine { + #[serde(default)] + pub(super) stop_at_first_char: bool, } #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] @@ -210,6 +221,7 @@ impl_actions!( ConfirmCompletion, DeleteToNextWordEnd, DeleteToPreviousWordStart, + DeleteToBeginningOfLine, ExpandExcerpts, ExpandExcerptsDown, ExpandExcerptsUp, @@ -272,7 +284,6 @@ gpui::actions!( CutToEndOfLine, Delete, DeleteLine, - DeleteToBeginningOfLine, DeleteToEndOfLine, DeleteToNextSubwordEnd, DeleteToPreviousSubwordStart, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 70c541c736cfd9..bd09364e0540df 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8803,10 +8803,13 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_cursors_with(|map, head, _| { - ( - movement::indented_line_beginning(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) + let point = if action.stop_at_first_char { + movement::indented_line_beginning(map, head, true) + } else { + movement::line_beginning(map, head, action.stop_at_soft_wraps) + }; + + (point, SelectionGoal::None) }); }) } @@ -8819,17 +8822,20 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_heads_with(|map, head, _| { - ( - movement::indented_line_beginning(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) + let point = if action.stop_at_first_char { + movement::indented_line_beginning(map, head, true) + } else { + movement::line_beginning(map, head, action.stop_at_soft_wraps) + }; + + (point, SelectionGoal::None) }); }); } pub fn delete_to_beginning_of_line( &mut self, - _: &DeleteToBeginningOfLine, + action: &DeleteToBeginningOfLine, window: &mut Window, cx: &mut Context, ) { @@ -8843,6 +8849,7 @@ impl Editor { this.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: false, + stop_at_first_char: action.stop_at_first_char, }, window, cx, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a3d502f5ed8181..8bb3b4316d789b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1510,6 +1510,11 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); let move_to_beg = MoveToBeginningOfLine { stop_at_soft_wraps: true, + stop_at_first_char: false, + }; + + let delete_to_beg = DeleteToBeginningOfLine { + stop_at_first_char: false, }; let move_to_end = MoveToEndOfLine { @@ -1529,17 +1534,6 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); }); - _ = editor.update(cx, |editor, window, cx| { - editor.move_to_beginning_of_line(&move_to_beg, window, cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[ - DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), - DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), - ] - ); - }); - _ = editor.update(cx, |editor, window, cx| { editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( @@ -1551,13 +1545,14 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); + // Moving to the beginning of the line again is a no-op. _ = editor.update(cx, |editor, window, cx| { editor.move_to_beginning_of_line(&move_to_beg, window, cx); assert_eq!( editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), - DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), + DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0), ] ); }); @@ -1590,23 +1585,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { editor.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, - }, - window, - cx, - ); - assert_eq!( - editor.selections.display_ranges(cx), - &[ - DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), - DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2), - ] - ); - }); - - _ = editor.update(cx, |editor, window, cx| { - editor.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: true, + stop_at_first_char: false, }, window, cx, @@ -1620,10 +1599,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { ); }); + // Selecting to the beginning of the line again is a no-op. _ = editor.update(cx, |editor, window, cx| { editor.select_to_beginning_of_line( &SelectToBeginningOfLine { stop_at_soft_wraps: true, + stop_at_first_char: false, }, window, cx, @@ -1632,7 +1613,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { editor.selections.display_ranges(cx), &[ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), - DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2), + DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0), ] ); }); @@ -1667,7 +1648,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { }); _ = editor.update(cx, |editor, window, cx| { - editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx); + editor.delete_to_beginning_of_line(&delete_to_beg, window, cx); assert_eq!(editor.display_text(cx), "\n"); assert_eq!( editor.selections.display_ranges(cx), @@ -1684,6 +1665,7 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); let move_to_beg = MoveToBeginningOfLine { stop_at_soft_wraps: false, + stop_at_first_char: false, }; let move_to_end = MoveToEndOfLine { @@ -1772,6 +1754,107 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_beginning_of_line_stop_at_first_char(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let move_to_beg = MoveToBeginningOfLine { + stop_at_soft_wraps: true, + stop_at_first_char: true, + }; + + let select_to_beg = SelectToBeginningOfLine { + stop_at_soft_wraps: true, + stop_at_first_char: true, + }; + + let delete_to_beg = DeleteToBeginningOfLine { + stop_at_first_char: true, + }; + + let move_to_end = MoveToEndOfLine { + stop_at_soft_wraps: false, + }; + + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, window, cx) + }); + + _ = editor.update(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), + DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4), + ]); + }); + + // Moving to the beginning of the line should put the first cursor at the beginning of the line, + // and the second cursor at the first non-whitespace character in the line. + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), + ] + ); + + // Moving to the beginning of the line again should be a no-op for the first cursor, + // and should move the second cursor to the beginning of the line. + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0), + ] + ); + + // Moving to the beginning of the line again should still be a no-op for the first cursor, + // and should move the second cursor back to the first non-whitespace character in the line. + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), + ] + ); + + // Selecting to the beginning of the line should select to the beginning of the line for the first cursor, + // and to the first non-whitespace character in the line for the second cursor. + editor.move_to_end_of_line(&move_to_end, window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.select_to_beginning_of_line(&select_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2), + ] + ); + + // Selecting to the beginning of the line again should be a no-op for the first cursor, + // and should select to the beginning of the line for the second cursor. + editor.select_to_beginning_of_line(&select_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[ + DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0), + DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0), + ] + ); + + // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor, + // and should delete to the first non-whitespace character in the line for the second cursor. + editor.move_to_end_of_line(&move_to_end, window, cx); + editor.move_left(&MoveLeft, window, cx); + editor.delete_to_beginning_of_line(&delete_to_beg, window, cx); + assert_eq!(editor.text(cx), "c\n f"); + }); +} + #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -2289,7 +2372,13 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state("one «two threeˇ» four"); cx.update_editor(|editor, window, cx| { - editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, window, cx); + editor.delete_to_beginning_of_line( + &DeleteToBeginningOfLine { + stop_at_first_char: false, + }, + window, + cx, + ); assert_eq!(editor.text(cx), " four"); }); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 078ae81b86ce75..77a5c88603b341 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1031,6 +1031,7 @@ impl ProjectPanel { editor.move_to_beginning_of_line( &editor::actions::MoveToBeginningOfLine { stop_at_soft_wraps: false, + stop_at_first_char: false, }, window, cx,