From 80a4042ff584ea68b5d5a072926bdb9af4792b45 Mon Sep 17 00:00:00 2001 From: Fanteria Date: Wed, 27 May 2026 16:36:41 +0200 Subject: [PATCH] vim: Fix dot repeat after macro replay not capturing insertion text (#57684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #47251 Fix dot (`.`) repeat not correctly repeating the last change after replaying a macro (`@register`) ([#47251](https://github.com/zed-industries/zed/issues/47251)) When replaying a macro that contains text insertions, `replay_insert_event` calls `handle_input` directly and never emits `InputHandled`, so the `observe_insertion` subscription never fires. This left the dot register stale — `.` after `@register` would repeat an earlier change instead of the last one made by the macro. Fix by calling `observe_insertion` explicitly in the `ReplayableAction::Insertion` branch of `Replayer::next`. Release Notes: - Fixed dot (`.`) repeat not repeating the last change made by a macro (`@register`). --------- Co-authored-by: Smit Barmase --- crates/editor/src/input.rs | 6 +++++ crates/vim/src/normal/repeat.rs | 28 +++++++++++++++++++++++ crates/vim/test_data/test_dot_repeat.json | 22 ++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/crates/editor/src/input.rs b/crates/editor/src/input.rs index 473c2ad53b6..afd0850c6ad 100644 --- a/crates/editor/src/input.rs +++ b/crates/editor/src/input.rs @@ -34,6 +34,12 @@ impl Editor { cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: relative_utf16_range.clone(), + text: text.into(), + }); + if let Some(relative_utf16_range) = relative_utf16_range { let selections = self .selections diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 387bca0912b..d1b79de4f57 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -457,6 +457,34 @@ mod test { cx.run_until_parked(); cx.simulate_shared_keystrokes(".").await; cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox"); + + // "q l" (note after macro should be used last change made by macro) + cx.set_shared_state("ˇ").await; + cx.simulate_shared_keystrokes("q l shift-o h e l l o space w o r l d escape q") + .await; + cx.simulate_shared_keystrokes("@ l").await; + cx.shared_state() + .await + .assert_eq("hello worlˇd\nhello world\n"); + cx.simulate_shared_keystrokes(".").await; + cx.shared_state() + .await + .assert_eq("hello worlˇd\nhello world\nhello world\n"); + } + + #[gpui::test] + async fn test_dot_repeat_after_macro_change_motion(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("ˇfoo foo", Mode::Normal); + cx.simulate_keystrokes("q l c f o x escape q"); + cx.assert_state("ˇxo foo", Mode::Normal); + + cx.simulate_keystrokes("w @ l"); + cx.assert_state("xo ˇxo", Mode::Normal); + + cx.simulate_keystrokes("."); + cx.assert_state("xo ˇx", Mode::Normal); } #[gpui::test] diff --git a/crates/vim/test_data/test_dot_repeat.json b/crates/vim/test_data/test_dot_repeat.json index 331ef52ecb9..b22cd96981c 100644 --- a/crates/vim/test_data/test_dot_repeat.json +++ b/crates/vim/test_data/test_dot_repeat.json @@ -36,3 +36,25 @@ {"Put":{"state":"THE QUIˇck brown fox"}} {"Key":"."} {"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}} +{"Put":{"state":"ˇ"}} +{"Key":"q"} +{"Key":"l"} +{"Key":"shift-o"} +{"Key":"h"} +{"Key":"e"} +{"Key":"l"} +{"Key":"l"} +{"Key":"o"} +{"Key":"space"} +{"Key":"w"} +{"Key":"o"} +{"Key":"r"} +{"Key":"l"} +{"Key":"d"} +{"Key":"escape"} +{"Key":"q"} +{"Key":"@"} +{"Key":"l"} +{"Get":{"state":"hello worlˇd\nhello world\n","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"hello worlˇd\nhello world\nhello world\n","mode":"Normal"}}