diff --git a/crates/editor/src/input.rs b/crates/editor/src/input.rs index afd0850c6ad..dc48eaecf4a 100644 --- a/crates/editor/src/input.rs +++ b/crates/editor/src/input.rs @@ -1827,7 +1827,15 @@ impl Editor { .text_highlights(HighlightKey::PendingInput, cx) .is_none() { - self.ime_transaction.take(); + let ime_tx = self.ime_transaction.take(); + self.buffer().update(cx, |buffer, cx| { + if let Some(transaction) = ime_tx { + buffer.forget_transaction(transaction, cx); + } + if let Some(tx) = transaction { + buffer.forget_transaction(tx, cx); + } + }); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f5292840299..036f3effccc 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4687,6 +4687,7 @@ impl Window { } let mut currently_pending = self.pending_input.take().unwrap_or_default(); + let had_pending_input = !currently_pending.keystrokes.is_empty(); if currently_pending.focus.is_some() && currently_pending.focus != self.focus { currently_pending = PendingInput::default(); } @@ -4697,6 +4698,12 @@ impl Window { &dispatch_path, ); + let pending_input_was_cleared = had_pending_input && match_result.pending.is_empty(); + if pending_input_was_cleared { + // Synchronous binding actions may edit immediately, so clear pending input first. + self.pending_input_changed(cx); + } + if !match_result.to_replay.is_empty() { self.replay_pending_input(match_result.to_replay, cx); cx.propagate_event = true; @@ -4781,14 +4788,18 @@ impl Window { match_result.context_stack, cx, ); - self.pending_input_changed(cx); + if !pending_input_was_cleared { + self.pending_input_changed(cx); + } return; } } } self.finish_dispatch_key_event(event, dispatch_path, match_result.context_stack, cx); - self.pending_input_changed(cx); + if !pending_input_was_cleared { + self.pending_input_changed(cx); + } } fn finish_dispatch_key_event( diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 99eda3d6c48..ab8f75fcb9e 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -8,7 +8,7 @@ use collections::HashMap; use command_palette::CommandPalette; use editor::{ AnchorRangeExt, DisplayPoint, Editor, EditorMode, MultiBuffer, MultiBufferOffset, - actions::{DeleteLine, WrapSelectionsInTag}, + actions::{DeleteLine, HandleInput, WrapSelectionsInTag}, code_context_menus::CodeContextMenu, display_map::DisplayRow, test::editor_test_context::EditorTestContext, @@ -1402,6 +1402,74 @@ async fn test_undo(cx: &mut gpui::TestAppContext) { 3"}); } +#[perf] +#[gpui::test] +async fn test_ime_transaction_undo(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "j k", + NormalBefore, + Some("vim_mode == insert"), + )]) + }); + + cx.set_state("ˇone", Mode::Normal); + cx.simulate_keystrokes("i j"); + cx.assert_state("ˇjone", Mode::Insert); + assert_pending_input(&mut cx, "«j»one"); + cx.simulate_keystrokes("k"); + cx.assert_state("ˇone", Mode::Normal); + + cx.simulate_keystrokes("u"); + cx.assert_state("ˇ", Mode::Normal); +} + +#[perf] +#[gpui::test] +async fn test_pending_input_mapping_output_undo(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "a b c", + HandleInput("d".to_string()), + Some("vim_mode == insert"), + )]) + }); + + cx.set_state("ˇone", Mode::Normal); + cx.simulate_keystrokes("i a b c"); + cx.assert_state("dˇone", Mode::Insert); + + cx.simulate_keystrokes("escape u"); + cx.assert_state("ˇone", Mode::Normal); +} + +#[perf] +#[gpui::test] +async fn test_pending_input_mapping_output_undo_delay(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "a b c", + HandleInput("d".to_string()), + Some("vim_mode == insert"), + )]) + }); + + cx.set_state("ˇone", Mode::Normal); + cx.simulate_keystrokes("i a b"); + cx.executor().advance_clock(Duration::from_millis(1500)); + cx.run_until_parked(); + cx.assert_state("abˇone", Mode::Insert); + + cx.simulate_keystrokes("escape u"); + cx.assert_state("ˇone", Mode::Normal); +} + #[perf] #[gpui::test] async fn test_lsp_completions_undo(cx: &mut gpui::TestAppContext) {