vim: add , and ;

Conrad Irwin created

Change summary

assets/keymaps/vim.json                        |  7 ++
crates/vim/src/motion.rs                       | 64 +++++++++++++++++++
crates/vim/src/state.rs                        |  4 +
crates/vim/src/vim.rs                          | 12 ++-
crates/vim/test_data/test_comma_semicolon.json | 17 +++++
5 files changed, 98 insertions(+), 6 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -292,6 +292,13 @@
           "backwards": true
         }
       ],
+      ";": "vim::RepeatFind",
+      ",": [
+        "vim::RepeatFind",
+        {
+          "backwards": true
+        }
+      ],
       "ctrl-f": "vim::PageDown",
       "pagedown": "vim::PageDown",
       "ctrl-b": "vim::PageUp",

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,7 +132,10 @@ 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) {
@@ -145,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 {
@@ -742,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/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,
@@ -33,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 {

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::MatchResult, 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;
@@ -246,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"}}