From dea928b00caf853b60fc19890dcb557beb814936 Mon Sep 17 00:00:00 2001 From: Benjamin Davies Date: Tue, 25 Jun 2024 03:41:33 +1200 Subject: [PATCH] vim: Allow count and repeat for "r" and "shift-r" action (#13287) Fixing the "r" action just involved adapting `normal_replace` to replace multiple characters. Fixing the "shift-r" command was less straightforward. The bindings for `vim::BeforeNormal` in replace mode were being overwritten and several other steps required for action repetition were not performed. Finally, the cursor adjustment after re-entering normal mode was duplicated (`vim::BeforeNormal` was now triggered correctly) so I removed the special case for replace mode. Release Notes: - Fixed vim "r" action to accept a count argument - Fixed vim "shift-r" action to accept a count argument and allow repetition --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 4 ++- crates/vim/src/normal.rs | 33 +++++++++++++++++-- crates/vim/src/normal/repeat.rs | 2 ++ crates/vim/src/replace.rs | 25 ++++++++++++++ crates/vim/src/vim.rs | 7 +--- crates/vim/test_data/test_r.json | 24 ++++++++++++++ .../test_data/test_replace_mode_repeat.json | 10 ++++++ .../test_replace_mode_with_counts.json | 14 ++++++++ 8 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 crates/vim/test_data/test_r.json create mode 100644 crates/vim/test_data/test_replace_mode_repeat.json create mode 100644 crates/vim/test_data/test_replace_mode_with_counts.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 3e674d83b9..97b439904a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -645,11 +645,13 @@ "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", + "tab": "vim::Tab", + "enter": "vim::Enter", "backspace": "vim::UndoReplace" } }, { - "context": "Editor && VimWaiting", + "context": "Editor && vim_mode != replace && VimWaiting", "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 7b8b57e39d..c5b6e78640 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -484,6 +484,7 @@ fn restore_selection_cursors( pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { + let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording(); vim.update_active_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { @@ -506,13 +507,13 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { .into_iter() .map(|selection| { let mut range = selection.range(); - *range.end.column_mut() += 1; - range.end = map.clip_point(range.end, Bias::Right); + range.end = right(&map, range.end, count); + let repeated_text = text.repeat(count); ( range.start.to_offset(&map, Bias::Left) ..range.end.to_offset(&map, Bias::Left), - text.clone(), + repeated_text, ) }) .collect::>(); @@ -523,6 +524,11 @@ pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { editor.set_clip_at_line_ends(true, cx); editor.change_selections(None, cx, |s| { s.select_anchor_ranges(stable_anchors); + if count > 1 { + s.move_cursors_with(|map, point, _| { + (right(map, point, count - 1), SelectionGoal::None) + }); + } }); }); }); @@ -1415,4 +1421,25 @@ mod test { indoc! {"asserˇt_binding"}, ); } + + #[gpui::test] + async fn test_r(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes("r -").await; + cx.shared_state().await.assert_eq("ˇ-ello\n"); + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes("3 r -").await; + cx.shared_state().await.assert_eq("--ˇ-lo\n"); + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes("r - 2 l .").await; + cx.shared_state().await.assert_eq("-eˇ-lo\n"); + + cx.set_shared_state("ˇhello world\n").await; + cx.simulate_shared_keystrokes("2 r - f w .").await; + cx.shared_state().await.assert_eq("--llo -ˇ-rld\n"); + } } diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 258590dd7b..ac626de12c 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -31,6 +31,8 @@ fn repeatable_insert(action: &ReplayableAction) -> Option> { || super::InsertLineBelow.partial_eq(&**action) { Some(super::InsertLineBelow.boxed_clone()) + } else if crate::replace::ToggleReplace.partial_eq(&**action) { + Some(crate::replace::ToggleReplace.boxed_clone()) } else { None } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 873b2ac853..3b00a3fafe 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -16,6 +16,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|_, _: &ToggleReplace, cx: &mut ViewContext| { Vim::update(cx, |vim, cx| { vim.update_state(|state| state.replacements = vec![]); + vim.start_recording(cx); vim.switch_mode(Mode::Replace, false, cx); }); }); @@ -237,6 +238,30 @@ mod test { ); } + #[gpui::test] + async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) { + let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes("3 shift-r - escape").await; + cx.shared_state().await.assert_eq("--ˇ-lo\n"); + + cx.set_shared_state("ˇhello\n").await; + cx.simulate_shared_keystrokes("3 shift-r a b c escape") + .await; + cx.shared_state().await.assert_eq("abcabcabˇc\n"); + } + + #[gpui::test] + async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) { + let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world\n").await; + cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .") + .await; + cx.shared_state().await.assert_eq("---lo --ˇ-ld\n"); + } + #[gpui::test] async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) { let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 6134e4d7e9..56b7de2f6e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -421,7 +421,7 @@ impl Vim { state.current_tx.take(); state.current_anchor.take(); }); - if mode != Mode::Insert { + if mode != Mode::Insert && mode != Mode::Replace { self.take_count(cx); } @@ -488,11 +488,6 @@ impl Vim { if selection.is_empty() { selection.end = movement::right(map, selection.start); } - } else if last_mode == Mode::Replace { - if selection.head().column() != 0 { - let point = movement::left(map, selection.head()); - selection.collapse_to(point, selection.goal) - } } }); }) diff --git a/crates/vim/test_data/test_r.json b/crates/vim/test_data/test_r.json new file mode 100644 index 0000000000..73df028d33 --- /dev/null +++ b/crates/vim/test_data/test_r.json @@ -0,0 +1,24 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"r"} +{"Key":"-"} +{"Get":{"state":"ˇ-ello\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"r"} +{"Key":"-"} +{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"r"} +{"Key":"-"} +{"Key":"2"} +{"Key":"l"} +{"Key":"."} +{"Get":{"state":"-eˇ-lo\n","mode":"Normal"}} +{"Put":{"state":"ˇhello world\n"}} +{"Key":"2"} +{"Key":"r"} +{"Key":"-"} +{"Key":"f"} +{"Key":"w"} +{"Key":"."} +{"Get":{"state":"--llo -ˇ-rld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode_repeat.json b/crates/vim/test_data/test_replace_mode_repeat.json new file mode 100644 index 0000000000..ab7cace51c --- /dev/null +++ b/crates/vim/test_data/test_replace_mode_repeat.json @@ -0,0 +1,10 @@ +{"Put":{"state":"ˇhello world\n"}} +{"Key":"shift-r"} +{"Key":"-"} +{"Key":"-"} +{"Key":"-"} +{"Key":"escape"} +{"Key":"4"} +{"Key":"l"} +{"Key":"."} +{"Get":{"state":"---lo --ˇ-ld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode_with_counts.json b/crates/vim/test_data/test_replace_mode_with_counts.json new file mode 100644 index 0000000000..d88c856fe3 --- /dev/null +++ b/crates/vim/test_data/test_replace_mode_with_counts.json @@ -0,0 +1,14 @@ +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"shift-r"} +{"Key":"-"} +{"Key":"escape"} +{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} +{"Put":{"state":"ˇhello\n"}} +{"Key":"3"} +{"Key":"shift-r"} +{"Key":"a"} +{"Key":"b"} +{"Key":"c"} +{"Key":"escape"} +{"Get":{"state":"abcabcabˇc\n","mode":"Normal"}}