vim: Add support for vim::PreviousLineStart motion (#14193)

sherwyn and Conrad Irwin created

Release Notes:

- vim: Added `-`/`+` to go to beginning of line above/below
([#14183](https://github.com/zed-industries/zed/issues/14183)).
- vim: (Breaking) Removed non-standard builtin binding from `-` to open
the project panel. You can re-add it to your keymap file with:
`{"context":"VimControl", "bindings":{ "-":
"pane::RevealInProjectPanel"}}`


Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.


https://github.com/zed-industries/zed/assets/32429059/0e9e9348-265e-4a81-a45a-4739034dc5d9

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

assets/keymaps/vim.json                   |  3 ++-
crates/vim/src/motion.rs                  | 16 +++++++++++++++-
crates/vim/src/test.rs                    | 19 +++++++++++++++++++
crates/vim/test_data/test_plus_minus.json |  7 +++++++
4 files changed, 43 insertions(+), 2 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -12,6 +12,8 @@
       "down": "vim::Down",
       "enter": "vim::NextLineStart",
       "ctrl-m": "vim::NextLineStart",
+      "+": "vim::NextLineStart",
+      "-": "vim::PreviousLineStart",
       "tab": "vim::Tab",
       "shift-tab": "vim::Tab",
       "k": "vim::Up",
@@ -192,7 +194,6 @@
       "ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
       "ctrl-w space": "editor::OpenExcerptsSplit",
       "ctrl-w g space": "editor::OpenExcerptsSplit",
-      "-": "pane::RevealInProjectPanel",
       "ctrl-6": "pane::AlternateFile"
     }
   },

crates/vim/src/motion.rs 🔗

@@ -91,6 +91,7 @@ pub enum Motion {
         last_find: Box<Motion>,
     },
     NextLineStart,
+    PreviousLineStart,
     StartOfLineDownward,
     EndOfLineDownward,
     GoToColumn,
@@ -235,6 +236,7 @@ actions!(
         EndOfDocument,
         Matching,
         NextLineStart,
+        PreviousLineStart,
         StartOfLineDownward,
         EndOfLineDownward,
         GoToColumn,
@@ -353,6 +355,9 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
     workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
         motion(Motion::NextLineStart, cx)
     });
+    workspace.register_action(|_: &mut Workspace, &PreviousLineStart, cx: _| {
+        motion(Motion::PreviousLineStart, cx)
+    });
     workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
         motion(Motion::StartOfLineDownward, cx)
     });
@@ -468,6 +473,7 @@ impl Motion {
             | EndOfDocument
             | CurrentLine
             | NextLineStart
+            | PreviousLineStart
             | StartOfLineDownward
             | StartOfParagraph
             | WindowTop
@@ -537,6 +543,7 @@ impl Motion {
             | WindowMiddle
             | WindowBottom
             | NextLineStart
+            | PreviousLineStart
             | ZedSearchResult { .. }
             | Jump { .. } => false,
         }
@@ -561,7 +568,8 @@ impl Motion {
             | PreviousWordEnd { .. }
             | NextSubwordEnd { .. }
             | PreviousSubwordEnd { .. }
-            | NextLineStart => true,
+            | NextLineStart
+            | PreviousLineStart => true,
             Left
             | Backspace
             | Right
@@ -763,6 +771,7 @@ impl Motion {
                 _ => return None,
             },
             NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
+            PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
             StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
             EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
             GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
@@ -1655,6 +1664,11 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     first_non_whitespace(map, false, correct_line)
 }
 
+fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
+    let correct_line = start_of_relative_buffer_row(map, point, (times as isize) * -1);
+    first_non_whitespace(map, false, correct_line)
+}
+
 fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
     let correct_line = start_of_relative_buffer_row(map, point, 0);
     right(map, correct_line, times.saturating_sub(1))

crates/vim/src/test.rs 🔗

@@ -1279,3 +1279,22 @@ async fn test_find_multibyte(cx: &mut gpui::TestAppContext) {
         .await
         .assert_eq(r#"<label for="guests">ˇo</label>"#);
 }
+
+#[gpui::test]
+async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {
+        "one
+           two
+        thrˇee
+    "})
+        .await;
+
+    cx.simulate_shared_keystrokes("-").await;
+    cx.shared_state().await.assert_matches();
+    cx.simulate_shared_keystrokes("-").await;
+    cx.shared_state().await.assert_matches();
+    cx.simulate_shared_keystrokes("+").await;
+    cx.shared_state().await.assert_matches();
+}

crates/vim/test_data/test_plus_minus.json 🔗

@@ -0,0 +1,7 @@
+{"Put":{"state":"one\n   two\nthrˇee\n"}}
+{"Key":"-"}
+{"Get":{"state":"one\n   ˇtwo\nthree\n","mode":"Normal"}}
+{"Key":"-"}
+{"Get":{"state":"ˇone\n   two\nthree\n","mode":"Normal"}}
+{"Key":"+"}
+{"Get":{"state":"one\n   ˇtwo\nthree\n","mode":"Normal"}}