vim: Add neovim 0.11 default mappings (#28602)

5brian created

Update the keymap to include:
https://neovim.io/doc/user/news-0.11.html#_defaults

This does conflict with `gr` replace with register though, is `gR` a
good alternative?

Release Notes:

- vim: Update the keymap to include: https://neovim.io/doc/user/news-0.11.html#_defaults
- vim: Replace with register has been remapped from `gr` to `gR`.

Change summary

assets/keymaps/vim.json                                | 23 ++
crates/vim/src/normal.rs                               | 84 ++++++++++++
crates/vim/src/normal/paste.rs                         | 12 
crates/vim/src/state.rs                                |  2 
crates/vim/test_data/test_insert_empty_line_above.json | 24 +++
docs/src/vim.md                                        |  2 
6 files changed, 134 insertions(+), 13 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -50,6 +50,12 @@
       "] -": "vim::NextLesserIndent",
       "] +": "vim::NextGreaterIndent",
       "] =": "vim::NextSameIndent",
+      "] b": "pane::ActivateNextItem",
+      "[ b": "pane::ActivatePreviousItem",
+      "] shift-b": "pane::ActivateLastItem",
+      "[ shift-b": ["pane::ActivateItem", 0],
+      "] space": "vim::InsertEmptyLineBelow",
+      "[ space": "vim::InsertEmptyLineAbove",
       // Word motions
       "w": "vim::NextWordStart",
       "e": "vim::NextWordEnd",
@@ -108,7 +114,11 @@
       "ctrl-e": "vim::LineDown",
       "ctrl-y": "vim::LineUp",
       // "g" commands
-      "g r": "vim::PushReplaceWithRegister",
+      "g shift-r": "vim::PushReplaceWithRegister",
+      "g r n": "editor::Rename",
+      "g r r": "editor::FindAllReferences",
+      "g r i": "editor::GoToImplementation",
+      "g r a": "editor::ToggleCodeActions",
       "g g": "vim::StartOfDocument",
       "g h": "editor::Hover",
       "g t": "pane::ActivateNextItem",
@@ -127,6 +137,7 @@
       "g <": ["editor::SelectPrevious", { "replace_newest": true }],
       "g a": "editor::SelectAllMatches",
       "g s": "outline::Toggle",
+      "g shift-o": "outline::Toggle",
       "g shift-s": "project_symbols::Toggle",
       "g .": "editor::ToggleCodeActions", // zed specific
       "g shift-a": "editor::FindAllReferences", // zed specific
@@ -305,7 +316,7 @@
       "!": "vim::ShellCommand",
       "i": ["vim::PushObject", { "around": false }],
       "a": ["vim::PushObject", { "around": true }],
-      "g r": ["vim::Paste", { "preserve_clipboard": true }],
+      "g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
       "g c": "vim::ToggleComments",
       "g q": "vim::Rewrap",
       "g ?": "vim::ConvertToRot13",
@@ -339,7 +350,8 @@
       "ctrl-shift-q": ["vim::PushLiteral", {}],
       "ctrl-r": "vim::PushRegister",
       "insert": "vim::ToggleReplace",
-      "ctrl-o": "vim::TemporaryNormal"
+      "ctrl-o": "vim::TemporaryNormal",
+      "ctrl-s": "editor::ShowSignatureHelp"
     }
   },
   {
@@ -630,9 +642,10 @@
     }
   },
   {
-    "context": "vim_operator == gr",
+    "context": "vim_operator == gR",
     "bindings": {
-      "r": "vim::CurrentLine"
+      "r": "vim::CurrentLine",
+      "shift-r": "vim::CurrentLine"
     }
   },
   {

crates/vim/src/normal.rs 🔗

@@ -43,6 +43,8 @@ actions!(
         InsertEndOfLine,
         InsertLineAbove,
         InsertLineBelow,
+        InsertEmptyLineAbove,
+        InsertEmptyLineBelow,
         InsertAtPrevious,
         JoinLines,
         JoinLinesNoWhitespace,
@@ -72,6 +74,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, Vim::insert_end_of_line);
     Vim::action(editor, cx, Vim::insert_line_above);
     Vim::action(editor, cx, Vim::insert_line_below);
+    Vim::action(editor, cx, Vim::insert_empty_line_above);
+    Vim::action(editor, cx, Vim::insert_empty_line_below);
     Vim::action(editor, cx, Vim::insert_at_previous);
     Vim::action(editor, cx, Vim::change_case);
     Vim::action(editor, cx, Vim::convert_to_upper_case);
@@ -537,6 +541,61 @@ impl Vim {
         });
     }
 
+    fn insert_empty_line_above(
+        &mut self,
+        _: &InsertEmptyLineAbove,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.record_current_action(cx);
+        self.update_editor(window, cx, |_, editor, window, cx| {
+            editor.transact(window, cx, |editor, _, cx| {
+                let selections = editor.selections.all::<Point>(cx);
+
+                let selection_start_rows: BTreeSet<u32> = selections
+                    .into_iter()
+                    .map(|selection| selection.start.row)
+                    .collect();
+                let edits = selection_start_rows
+                    .into_iter()
+                    .map(|row| {
+                        let start_of_line = Point::new(row, 0);
+                        (start_of_line..start_of_line, "\n".to_string())
+                    })
+                    .collect::<Vec<_>>();
+                editor.edit(edits, cx);
+            });
+        });
+    }
+
+    fn insert_empty_line_below(
+        &mut self,
+        _: &InsertEmptyLineBelow,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.record_current_action(cx);
+        self.update_editor(window, cx, |_, editor, window, cx| {
+            editor.transact(window, cx, |editor, _, cx| {
+                let selections = editor.selections.all::<Point>(cx);
+                let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+                let selection_end_rows: BTreeSet<u32> = selections
+                    .into_iter()
+                    .map(|selection| selection.end.row)
+                    .collect();
+                let edits = selection_end_rows
+                    .into_iter()
+                    .map(|row| {
+                        let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
+                        (end_of_line..end_of_line, "\n".to_string())
+                    })
+                    .collect::<Vec<_>>();
+                editor.edit(edits, cx);
+            });
+        });
+    }
+
     fn join_lines_impl(
         &mut self,
         insert_whitespace: bool,
@@ -1267,6 +1326,31 @@ mod test {
         );
     }
 
+    #[gpui::test]
+    async fn test_insert_empty_line_above(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.simulate("[ space", "ˇ").await.assert_matches();
+        cx.simulate("[ space", "The ˇquick").await.assert_matches();
+        cx.simulate_at_each_offset(
+            "[ space",
+            indoc! {"
+            The qˇuick
+            brown ˇfox
+            jumps ˇover"},
+        )
+        .await
+        .assert_matches();
+        cx.simulate(
+            "[ space",
+            indoc! {"
+            The quick
+            ˇ
+            brown fox"},
+        )
+        .await
+        .assert_matches();
+    }
+
     #[gpui::test]
     async fn test_dd(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/normal/paste.rs 🔗

@@ -917,7 +917,7 @@ mod test {
         );
         cx.simulate_keystrokes("y i w");
         cx.simulate_keystrokes("w");
-        cx.simulate_keystrokes("g r i w");
+        cx.simulate_keystrokes("g shift-r i w");
         cx.assert_state(
             indoc! {"
                 fish fisˇh
@@ -925,7 +925,7 @@ mod test {
                 "},
             Mode::Normal,
         );
-        cx.simulate_keystrokes("j b g r e");
+        cx.simulate_keystrokes("j b g shift-r e");
         cx.assert_state(
             indoc! {"
             fish fish
@@ -945,7 +945,7 @@ mod test {
         );
         cx.simulate_keystrokes("y i w");
         cx.simulate_keystrokes("w");
-        cx.simulate_keystrokes("v i w g r");
+        cx.simulate_keystrokes("v i w g shift-r");
         cx.assert_state(
             indoc! {"
                 fish fisˇh
@@ -953,7 +953,7 @@ mod test {
                 "},
             Mode::Normal,
         );
-        cx.simulate_keystrokes("g r r");
+        cx.simulate_keystrokes("g shift-r r");
         cx.assert_state(
             indoc! {"
                 fisˇh
@@ -961,7 +961,7 @@ mod test {
                 "},
             Mode::Normal,
         );
-        cx.simulate_keystrokes("j w g r $");
+        cx.simulate_keystrokes("j w g shift-r $");
         cx.assert_state(
             indoc! {"
                 fish
@@ -986,7 +986,7 @@ mod test {
         );
         cx.simulate_keystrokes("y i w");
         cx.simulate_keystrokes("w");
-        cx.simulate_keystrokes("g r i w");
+        cx.simulate_keystrokes("g shift-r i w");
         cx.assert_state(
             indoc! {"
                 fish fisˇh

crates/vim/src/state.rs 🔗

@@ -954,7 +954,7 @@ impl Operator {
             Operator::AutoIndent => "eq",
             Operator::ShellCommand => "sh",
             Operator::Rewrap => "gq",
-            Operator::ReplaceWithRegister => "gr",
+            Operator::ReplaceWithRegister => "gR",
             Operator::Exchange => "cx",
             Operator::Outdent => "<",
             Operator::Uppercase => "gU",

crates/vim/test_data/test_insert_empty_line_above.json 🔗

@@ -0,0 +1,24 @@
+{"Put":{"state":"ˇ"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nˇ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nThe ˇquick","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\n\nbrown ˇfox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown fox\n\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Normal"}}

docs/src/vim.md 🔗

@@ -164,7 +164,7 @@ Zed's vim mode includes some features that are usually provided by very popular
 - You can comment and uncomment selections with `gc` in visual mode and `gcc` in normal mode.
 - The project panel supports many shortcuts modeled after the Vim plugin `netrw`: navigation with `hjkl`, open file with `o`, open file in a new tab with `t`, etc.
 - You can add key bindings to your keymap to navigate "camelCase" names. [Head down to the Optional key bindings](#optional-key-bindings) section to learn how.
-- You can use `gr` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister).
+- You can use `gR` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister).
 - You can use `cx` for [vim-exchange](https://github.com/tommcdo/vim-exchange) functionality. Note that it does not have a default binding in visual mode, but you can add one to your keymap (refer to the [optional key bindings](#optional-key-bindings) section).
 - You can navigate to indent depths relative to your cursor with the [indent wise](https://github.com/jeetsukumaran/vim-indentwise) plugin `[-`, `]-`, `[+`, `]+`, `[=`, `]=`.