From 00e3b2eb9104188994a3c35c2c82f909136db740 Mon Sep 17 00:00:00 2001 From: Dino Date: Thu, 8 Jan 2026 16:41:19 +0000 Subject: [PATCH] vim: Fix bug where repeat operator could lead to unrecoverable replaying state (#46376) When a recorded action moves focus away from the editor (e.g., `buffer_search::Deploy`), the `EndRepeat` action handler is not invoked because is node is no longer on the dispatch path. This left `dot_replaying` set to `true`, causing subsequent repeats to malfunction and the `VimGlobals.pre_count` value to never be reset. Reset `dot_replaying` as a fail-safe when the replayer exhausts its action queue, ensuring the state is always cleaned up regardless of whether `EndRepeat` was handled. Release Notes: - Fixed vim repeat (`.`) breaking when the recorded action moves focus away from the editor Co-authored-by: neel --- crates/vim/src/normal/repeat.rs | 51 ++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index e47b2b350f9644f99fe7d8ec924ff0f0b9ab23f7..5b480a86d846ff719d8784f619be861db9e44c9f 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -145,7 +145,13 @@ impl Replayer { lock.ix += 1; drop(lock); let Some(action) = action else { - Vim::globals(cx).replayer.take(); + // The `globals.dot_replaying = false` is a fail-safe to ensure that + // this value is always reset, in the case that the focus is moved + // away from the editor, effectively preventing the `EndRepeat` + // action from being handled. + let globals = Vim::globals(cx); + globals.replayer.take(); + globals.dot_replaying = false; return; }; match action { @@ -396,6 +402,7 @@ mod test { use gpui::EntityInputHandler; use crate::{ + VimGlobals, state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; @@ -739,6 +746,48 @@ mod test { cx.shared_state().await.assert_eq("ˇx hello\n"); } + #[gpui::test] + async fn test_repeat_after_blur_resets_dot_replaying(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Bind `ctrl-f` to the `buffer_search::Deploy` action so that this can + // be triggered while in Insert mode, ensuring that an action which + // moves the focus away from the editor, gets recorded. + cx.update(|_, cx| { + cx.bind_keys([gpui::KeyBinding::new( + "ctrl-f", + search::buffer_search::Deploy::find(), + None, + )]) + }); + + cx.set_state("ˇhello", Mode::Normal); + + // We're going to enter insert mode, which will start recording, type a + // character and then immediately use `ctrl-f` to trigger the buffer + // search. Triggering the buffer search will move focus away from the + // editor, effectively stopping the recording immediately after + // `buffer_search::Deploy` is recorded. The first `escape` is used to + // dismiss the search bar, while the second is used to move from Insert + // to Normal mode. + cx.simulate_keystrokes("i x ctrl-f escape escape"); + cx.run_until_parked(); + + // Using the `.` key will dispatch the `vim::Repeat` action, repeating + // the set of recorded actions. This will eventually focus on the search + // bar, preventing the `EndRepeat` action from being correctly handled. + cx.simulate_keystrokes("."); + cx.run_until_parked(); + + // After replay finishes, even though the `EndRepeat` action wasn't + // handled, seeing as the editor lost focus during replay, the + // `dot_replaying` value should be set back to `false`. + assert!( + !cx.update(|_, cx| cx.global::().dot_replaying), + "dot_replaying should be false after repeat completes" + ); + } + #[gpui::test] async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await;