diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index da850662f2..94a271f037 100644 --- a/assets/keymaps/vim.json +++ b/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", diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index fb742af3ab..b8bd256d8a 100644 --- a/crates/vim/src/motion.rs +++ b/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; + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index bf644c08cc..eb52945ced 100644 --- a/crates/vim/src/state.rs +++ b/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, pub search: SearchState, + + pub last_find: Option, } pub struct SearchState { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 82d2e752c3..e31fa4addd 100644 --- a/crates/vim/src/vim.rs +++ b/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), diff --git a/crates/vim/test_data/test_comma_semicolon.json b/crates/vim/test_data/test_comma_semicolon.json new file mode 100644 index 0000000000..8cde887ed1 --- /dev/null +++ b/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"}}