From c32abbdfb7212ab989ad77db7f5df38fe9e47754 Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:47:06 -0500 Subject: [PATCH] vim: Fix `:g/pattern/norm commands` applying on all matches (#43352) Closes #36359 Release Notes: - Fixes bug where `:g/pattern/norm commands` would only apply on the last match --------- Co-authored-by: dino --- crates/vim/src/command.rs | 126 ++++++++++++++---- .../vim/test_data/test_command_g_normal.json | 22 +++ crates/vim/test_data/test_normal_command.json | 14 ++ 3 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 crates/vim/test_data/test_command_g_normal.json diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index caa90406c9a2ac70eb81cd7705c8223799e59f18..22bfc7ec8b1651c7155ec26fdc42059c1f72245f 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -243,6 +243,9 @@ struct VimRead { struct VimNorm { pub range: Option, pub command: String, + /// Places cursors at beginning of each given row. + /// Overrides given range and current cursor. + pub override_rows: Option>, } #[derive(Debug)] @@ -759,9 +762,18 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { .map(|c| Keystroke::parse(&c.to_string()).unwrap()) .collect(); vim.switch_mode(Mode::Normal, true, window, cx); - let initial_selections = - vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc()); - if let Some(range) = &action.range { + if let Some(override_rows) = &action.override_rows { + vim.update_editor(cx, |_, editor, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.replace_cursors_with(|map| { + override_rows + .iter() + .map(|row| Point::new(*row, 0).to_display_point(map)) + .collect() + }); + }); + }); + } else if let Some(range) = &action.range { let result = vim.update_editor(cx, |vim, editor, cx| { let range = range.buffer_range(vim, editor, window, cx)?; editor.change_selections( @@ -790,37 +802,35 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { workspace.send_keystrokes_impl(keystrokes, window, cx) }); let had_range = action.range.is_some(); + let had_override = action.override_rows.is_some(); cx.spawn_in(window, async move |vim, cx| { task.await; vim.update_in(cx, |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - if had_range { - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges([s.newest_anchor().range()]); - }) - } - }); if matches!(vim.mode, Mode::Insert | Mode::Replace) { vim.normal_before(&Default::default(), window, cx); } else { vim.switch_mode(Mode::Normal, true, window, cx); } - vim.update_editor(cx, |_, editor, cx| { - if let Some(first_sel) = initial_selections - && let Some(tx_id) = editor + if had_override || had_range { + vim.update_editor(cx, |_, editor, cx| { + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_anchor_ranges([s.newest_anchor().range()]); + }); + if let Some(tx_id) = editor .buffer() .update(cx, |multi, cx| multi.last_transaction_id(cx)) - { - let last_sel = editor.selections.disjoint_anchors_arc(); - editor.modify_transaction_selection_history(tx_id, |old| { - old.0 = first_sel; - old.1 = Some(last_sel); - }); - } - }); + { + let last_sel = editor.selections.disjoint_anchors_arc(); + editor.modify_transaction_selection_history(tx_id, |old| { + old.0 = old.0.get(..1).unwrap_or(&[]).into(); + old.1 = Some(last_sel); + }); + } + }); + } }) - .ok(); + .log_err(); }) .detach(); }); @@ -1606,6 +1616,7 @@ fn generate_commands(_: &App) -> Vec { VimNorm { command: "".into(), range: None, + override_rows: None, }, ) .args(|_, args| { @@ -1613,6 +1624,7 @@ fn generate_commands(_: &App) -> Vec { VimNorm { command: args, range: None, + override_rows: None, } .boxed_clone(), ) @@ -1863,7 +1875,7 @@ pub fn command_interceptor( } else if on_matching_lines.is_some() { commands(cx) .iter() - .find_map(|command| command.parse(query, &range, cx)) + .find_map(|command| command.parse(query, &None, cx)) } else { None }; @@ -2194,6 +2206,19 @@ impl OnMatchingLines { if new_selections.is_empty() { return; } + + if let Some(vim_norm) = action.as_any().downcast_ref::() { + let mut vim_norm = vim_norm.clone(); + vim_norm.override_rows = + Some(new_selections.iter().map(|point| point.row().0).collect()); + editor + .update_in(cx, |_, window, cx| { + window.dispatch_action(vim_norm.boxed_clone(), cx); + }) + .log_err(); + return; + } + editor .update_in(cx, |editor, window, cx| { editor.start_transaction_at(Instant::now(), window, cx); @@ -2201,6 +2226,7 @@ impl OnMatchingLines { s.replace_cursors_with(|_| new_selections); }); window.dispatch_action(action, cx); + cx.defer_in(window, move |editor, window, cx| { let newest = editor .selections @@ -2216,7 +2242,7 @@ impl OnMatchingLines { editor.end_transaction_at(Instant::now(), cx); }) }) - .ok(); + .log_err(); }) .detach(); }); @@ -3150,9 +3176,61 @@ mod test { jumps over the lazy dog "}); + + cx.set_shared_state(indoc! {" + The« quick + brownˇ» fox + jumps over + the lazy dog + "}) + .await; + + cx.simulate_shared_keystrokes(": n o r m space I 1 2 3") + .await; + cx.simulate_shared_keystrokes("enter").await; + cx.simulate_shared_keystrokes("u").await; + + cx.shared_state().await.assert_eq(indoc! {" + ˇThe quick + brown fox + jumps over + the lazy dog + "}); + // Once ctrl-v to input character literals is added there should be a test for redo } + #[gpui::test] + async fn test_command_g_normal(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇfoo + + foo + "}) + .await; + + cx.simulate_shared_keystrokes(": % g / f o o / n o r m space A b a r") + .await; + cx.simulate_shared_keystrokes("enter").await; + cx.run_until_parked(); + + cx.shared_state().await.assert_eq(indoc! {" + foobar + + foobaˇr + "}); + + cx.simulate_shared_keystrokes("u").await; + + cx.shared_state().await.assert_eq(indoc! {" + foˇo + + foo + "}); + } + #[gpui::test] async fn test_command_tabnew(cx: &mut TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/test_data/test_command_g_normal.json b/crates/vim/test_data/test_command_g_normal.json new file mode 100644 index 0000000000000000000000000000000000000000..9c2fff3fd89cb1e4e4afc12c986029bb88977c34 --- /dev/null +++ b/crates/vim/test_data/test_command_g_normal.json @@ -0,0 +1,22 @@ +{"Put":{"state":"ˇfoo\n\nfoo\n"}} +{"Key":":"} +{"Key":"%"} +{"Key":"g"} +{"Key":"/"} +{"Key":"f"} +{"Key":"o"} +{"Key":"o"} +{"Key":"/"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"A"} +{"Key":"b"} +{"Key":"a"} +{"Key":"r"} +{"Key":"enter"} +{"Get":{"state":"foobar\n\nfoobaˇr\n","mode":"Normal"}} +{"Key":"u"} +{"Get":{"state":"foˇo\n\nfoo\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_normal_command.json b/crates/vim/test_data/test_normal_command.json index efd1d532c4261976a5e1ef00e85fdac9b2b90fab..1f248f5e77e37cb9aaf56eb33f1a3cd561fe45bc 100644 --- a/crates/vim/test_data/test_normal_command.json +++ b/crates/vim/test_data/test_normal_command.json @@ -62,3 +62,17 @@ {"Key":"u"} {"Key":"enter"} {"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} +{"Put":{"state":"The« quick\nbrownˇ» fox\njumps over\nthe lazy dog\n"}} +{"Key":":"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"I"} +{"Key":"1"} +{"Key":"2"} +{"Key":"3"} +{"Key":"enter"} +{"Key":"u"} +{"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}}