Vim shortcuts (#2760)

Conrad Irwin created

Refactors some of the vim bindings to make the vim.json file less
obtuse.

Release Notes:

- vim: add `;` and `,` to repeat last `{f,F,t,T}`
- vim: add zed-specific shortcuts for common IDE actions:
- - `g A` to find all references
- - `g .` to open the code actions menu.
- - `c d` for rename

Change summary

assets/keymaps/vim.json                        | 79 ++++++++-----------
crates/vim/src/motion.rs                       | 69 ++++++++++++++++-
crates/vim/src/normal.rs                       | 25 -----
crates/vim/src/state.rs                        | 13 +--
crates/vim/src/vim.rs                          | 17 +++-
crates/vim/test_data/test_comma_semicolon.json | 17 ++++
6 files changed, 135 insertions(+), 85 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -2,12 +2,6 @@
   {
     "context": "Editor && VimControl && !VimWaiting && !menu",
     "bindings": {
-      "g": [
-        "vim::PushOperator",
-        {
-          "Namespace": "G"
-        }
-      ],
       "i": [
         "vim::PushOperator",
         {
@@ -110,6 +104,32 @@
       "*": "vim::MoveToNext",
       "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+      // "g" commands
+      "g g": "vim::StartOfDocument",
+      "g h": "editor::Hover",
+      "g t": "pane::ActivateNextItem",
+      "g shift-t": "pane::ActivatePrevItem",
+      "g d": "editor::GoToDefinition",
+      "g shift-d": "editor::GoToTypeDefinition",
+      "g .": "editor::ToggleCodeActions", // zed specific
+      "g shift-a": "editor::FindAllReferences", // zed specific
+      "g *": [
+        "vim::MoveToNext",
+        {
+          "partialWord": true
+        }
+      ],
+      "g #": [
+        "vim::MoveToPrev",
+        {
+          "partialWord": true
+        }
+      ],
+      // z commands
+      "z t": "editor::ScrollCursorTop",
+      "z z": "editor::ScrollCursorCenter",
+      "z b": "editor::ScrollCursorBottom",
+      // Count support
       "1": [
         "vim::Number",
         1
@@ -234,12 +254,6 @@
         "vim::PushOperator",
         "Yank"
       ],
-      "z": [
-        "vim::PushOperator",
-        {
-          "Namespace": "Z"
-        }
-      ],
       "i": [
         "vim::SwitchMode",
         "Insert"
@@ -278,6 +292,13 @@
           "backwards": true
         }
       ],
+      ";": "vim::RepeatFind",
+      ",": [
+        "vim::RepeatFind",
+        {
+          "backwards": true
+        }
+      ],
       "ctrl-f": "vim::PageDown",
       "pagedown": "vim::PageDown",
       "ctrl-b": "vim::PageUp",
@@ -306,33 +327,11 @@
       ]
     }
   },
-  {
-    "context": "Editor && vim_operator == g",
-    "bindings": {
-      "g": "vim::StartOfDocument",
-      "h": "editor::Hover",
-      "t": "pane::ActivateNextItem",
-      "shift-t": "pane::ActivatePrevItem",
-      "d": "editor::GoToDefinition",
-      "shift-d": "editor::GoToTypeDefinition",
-      "*": [
-        "vim::MoveToNext",
-        {
-          "partialWord": true
-        }
-      ],
-      "#": [
-        "vim::MoveToPrev",
-        {
-          "partialWord": true
-        }
-      ]
-    }
-  },
   {
     "context": "Editor && vim_operator == c",
     "bindings": {
-      "c": "vim::CurrentLine"
+      "c": "vim::CurrentLine",
+      "d": "editor::Rename" // zed specific
     }
   },
   {
@@ -347,14 +346,6 @@
       "y": "vim::CurrentLine"
     }
   },
-  {
-    "context": "Editor && vim_operator == z",
-    "bindings": {
-      "t": "editor::ScrollCursorTop",
-      "z": "editor::ScrollCursorCenter",
-      "b": "editor::ScrollCursorBottom"
-    }
-  },
   {
     "context": "Editor && VimObject",
     "bindings": {

crates/vim/src/motion.rs 🔗

@@ -62,6 +62,12 @@ struct PreviousWordStart {
     ignore_punctuation: bool,
 }
 
+#[derive(Clone, Deserialize, PartialEq)]
+struct RepeatFind {
+    #[serde(default)]
+    backwards: bool,
+}
+
 actions!(
     vim,
     [
@@ -82,7 +88,10 @@ actions!(
         NextLineStart,
     ]
 );
-impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
+impl_actions!(
+    vim,
+    [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
+);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
@@ -123,13 +132,15 @@ pub fn init(cx: &mut AppContext) {
          &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
-    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
+    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
+    cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
+        repeat_motion(action.backwards, cx)
+    })
 }
 
 pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
-    if let Some(Operator::Namespace(_))
-    | Some(Operator::FindForward { .. })
-    | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
+    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
+        Vim::read(cx).active_operator()
     {
         Vim::update(cx, |vim, cx| vim.pop_operator(cx));
     }
@@ -146,6 +157,35 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| vim.clear_operator(cx));
 }
 
+fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
+    let find = match Vim::read(cx).state.last_find.clone() {
+        Some(Motion::FindForward { before, text }) => {
+            if backwards {
+                Motion::FindBackward {
+                    after: before,
+                    text,
+                }
+            } else {
+                Motion::FindForward { before, text }
+            }
+        }
+
+        Some(Motion::FindBackward { after, text }) => {
+            if backwards {
+                Motion::FindForward {
+                    before: after,
+                    text,
+                }
+            } else {
+                Motion::FindBackward { after, text }
+            }
+        }
+        _ => return,
+    };
+
+    motion(find, cx)
+}
+
 // Motion handling is specified here:
 // https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
 impl Motion {
@@ -743,4 +783,23 @@ mod test {
         cx.simulate_shared_keystrokes(["%"]).await;
         cx.assert_shared_state("func boop(ˇ) {\n}").await;
     }
+
+    #[gpui::test]
+    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("ˇone two three four").await;
+        cx.simulate_shared_keystrokes(["f", "o"]).await;
+        cx.assert_shared_state("one twˇo three four").await;
+        cx.simulate_shared_keystrokes([","]).await;
+        cx.assert_shared_state("ˇone two three four").await;
+        cx.simulate_shared_keystrokes(["2", ";"]).await;
+        cx.assert_shared_state("one two three fˇour").await;
+        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
+        cx.assert_shared_state("one two threeˇ four").await;
+        cx.simulate_shared_keystrokes(["3", ";"]).await;
+        cx.assert_shared_state("oneˇ two three four").await;
+        cx.simulate_shared_keystrokes([","]).await;
+        cx.assert_shared_state("one two thˇree four").await;
+    }
 }

crates/vim/src/normal.rs 🔗

@@ -107,7 +107,7 @@ pub fn normal_motion(
             Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
             Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
             Some(operator) => {
-                // Can't do anything for text objects or namespace operators. Ignoring
+                // Can't do anything for text objects, Ignoring
                 error!("Unexpected normal mode motion operator: {:?}", operator)
             }
         }
@@ -441,11 +441,8 @@ mod test {
     use indoc::indoc;
 
     use crate::{
-        state::{
-            Mode::{self, *},
-            Namespace, Operator,
-        },
-        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
+        state::Mode::{self},
+        test::{ExemptionFeatures, NeovimBackedTestContext},
     };
 
     #[gpui::test]
@@ -610,22 +607,6 @@ mod test {
             .await;
     }
 
-    #[gpui::test]
-    async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
-        let mut cx = VimTestContext::new(cx, true).await;
-
-        // Can abort with escape to get back to normal mode
-        cx.simulate_keystroke("g");
-        assert_eq!(cx.mode(), Normal);
-        assert_eq!(
-            cx.active_operator(),
-            Some(Operator::Namespace(Namespace::G))
-        );
-        cx.simulate_keystroke("escape");
-        assert_eq!(cx.mode(), Normal);
-        assert_eq!(cx.active_operator(), None);
-    }
-
     #[gpui::test]
     async fn test_gg(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/state.rs 🔗

@@ -3,6 +3,8 @@ use language::CursorShape;
 use serde::{Deserialize, Serialize};
 use workspace::searchable::Direction;
 
+use crate::motion::Motion;
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
 pub enum Mode {
     Normal,
@@ -16,16 +18,9 @@ impl Default for Mode {
     }
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
-pub enum Namespace {
-    G,
-    Z,
-}
-
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
 pub enum Operator {
     Number(usize),
-    Namespace(Namespace),
     Change,
     Delete,
     Yank,
@@ -40,6 +35,8 @@ pub struct VimState {
     pub mode: Mode,
     pub operator_stack: Vec<Operator>,
     pub search: SearchState,
+
+    pub last_find: Option<Motion>,
 }
 
 pub struct SearchState {
@@ -126,8 +123,6 @@ impl Operator {
     pub fn id(&self) -> &'static str {
         match self {
             Operator::Number(_) => "n",
-            Operator::Namespace(Namespace::G) => "g",
-            Operator::Namespace(Namespace::Z) => "z",
             Operator::Object { around: false } => "i",
             Operator::Object { around: true } => "a",
             Operator::Change => "c",

crates/vim/src/vim.rs 🔗

@@ -14,8 +14,8 @@ use anyhow::Result;
 use collections::CommandPaletteFilter;
 use editor::{Bias, Editor, EditorMode, Event};
 use gpui::{
-    actions, impl_actions, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext,
-    ViewHandle, WeakViewHandle, WindowContext,
+    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
+    Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use language::CursorShape;
 use motion::Motion;
@@ -90,7 +90,10 @@ pub fn init(cx: &mut AppContext) {
 }
 
 pub fn observe_keystrokes(cx: &mut WindowContext) {
-    cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
+    cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
+        if result == &MatchResult::Pending {
+            return true;
+        }
         if let Some(handled_by) = handled_by {
             // Keystroke is handled by the vim system, so continue forward
             if handled_by.namespace() == "vim" {
@@ -243,10 +246,14 @@ impl Vim {
 
         match Vim::read(cx).active_operator() {
             Some(Operator::FindForward { before }) => {
-                motion::motion(Motion::FindForward { before, text }, cx)
+                let find = Motion::FindForward { before, text };
+                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                motion::motion(find, cx)
             }
             Some(Operator::FindBackward { after }) => {
-                motion::motion(Motion::FindBackward { after, text }, cx)
+                let find = Motion::FindBackward { after, text };
+                Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
+                motion::motion(find, cx)
             }
             Some(Operator::Replace) => match Vim::read(cx).state.mode {
                 Mode::Normal => normal_replace(text, cx),

crates/vim/test_data/test_comma_semicolon.json 🔗

@@ -0,0 +1,17 @@
+{"Put":{"state":"ˇone two three four"}}
+{"Key":"f"}
+{"Key":"o"}
+{"Get":{"state":"one twˇo three four","mode":"Normal"}}
+{"Key":","}
+{"Get":{"state":"ˇone two three four","mode":"Normal"}}
+{"Key":"2"}
+{"Key":";"}
+{"Get":{"state":"one two three fˇour","mode":"Normal"}}
+{"Key":"shift-t"}
+{"Key":"e"}
+{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
+{"Key":"3"}
+{"Key":";"}
+{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
+{"Key":","}
+{"Get":{"state":"one two thˇree four","mode":"Normal"}}