diff --git a/crates/editor/src/selection.rs b/crates/editor/src/selection.rs index c8b56d1682..a4373b1de1 100644 --- a/crates/editor/src/selection.rs +++ b/crates/editor/src/selection.rs @@ -87,6 +87,25 @@ impl Editor { result } + /// Re-resolves selection anchors so they reference live buffer fragments. + /// + /// Deleted CRDT fragments remain as tombstones. An anchor associated with a + /// tombstone can resolve to the correct visible offset while causing later + /// insertions to be ordered on the wrong side of the cursor. Round-tripping + /// selections through their offsets creates fresh anchors associated with + /// live fragments. + /// + /// This does not emit selection effects, but it replaces pending selection + /// state with the resolved selections. + pub fn refresh_selection_anchors(&mut self, cx: &mut Context) { + let snapshot = self.display_snapshot(cx); + let selections = self.selections.all::(&snapshot); + self.selections + .change_with(&snapshot, |selection_collection| { + selection_collection.select(selections); + }); + } + /// Defers the effects of selection change, so that the effects of multiple calls to /// `change_selections` are applied at the end. This way these intermediate states aren't added /// to selection history and the state of popovers based on selection position aren't diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index df1cd9606d..fb6e8cc423 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1882,7 +1882,7 @@ mod test { use super::{HELIX_JUMP_LABEL_LIMIT, HelixJumpToWord}; use crate::{ - HELIX_JUMP_OVERLAY_KEY, Vim, VimAddon, + HELIX_JUMP_OVERLAY_KEY, SwitchToHelixNormalMode, Vim, VimAddon, state::{Mode, Operator}, test::VimTestContext, }; @@ -4671,4 +4671,24 @@ mod test { cx.simulate_keystrokes("x"); cx.assert_state("« aa \nˇ»bbb\nccc\n", Mode::HelixNormal); } + + #[gpui::test] + async fn test_insert_line_with_multi_keybinding_to_helix_normal(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "j j", + SwitchToHelixNormalMode, + Some("vim_mode == insert"), + )]); + }); + + cx.set_state("hello worldˇ\n", Mode::Insert); + cx.simulate_keystrokes("j j"); + cx.assert_state("hello worldˇ\n", Mode::HelixNormal); + cx.simulate_keystrokes("o"); + cx.assert_state("hello world\nˇ\n", Mode::Insert); + } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index b4cac5772c..0fa0439379 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -35,7 +35,10 @@ use serde_json::json; use workspace::notifications::{NotificationId, simple_message_notification::MessageNotification}; use workspace::{DeploySearch, MultiWorkspace}; -use crate::{PushSneak, PushSneakBackward, VimAddon, insert::NormalBefore, motion, state::Mode}; +use crate::{ + PushSneak, PushSneakBackward, SwitchToNormalMode, VimAddon, insert::NormalBefore, motion, + state::Mode, +}; use util_macros::perf; @@ -387,6 +390,25 @@ async fn test_escape_cancels(cx: &mut gpui::TestAppContext) { cx.assert_state("aˇbc", Mode::Normal); } +#[gpui::test] +async fn test_insert_line_with_multi_keybinding_to_normal(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "j j", + SwitchToNormalMode, + Some("vim_mode == insert"), + )]); + }); + + cx.set_state("hello worldˇ\n", Mode::Insert); + cx.simulate_keystrokes("j j"); + cx.assert_state("hello worldˇ\n", Mode::Normal); + cx.simulate_keystrokes("o"); + cx.assert_state("hello world\nˇ\n", Mode::Insert); +} + #[cfg(target_os = "windows")] #[gpui::test] async fn test_escape_dismisses_workspace_notification_in_normal_modes( diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 0c6c59f36e..b9bbb1ee44 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1266,6 +1266,17 @@ impl Vim { } } + // Multi-key bindings in Insert mode temporarily insert their pending keys + // and remove them only after the action runs. Refresh the selection anchors + // afterward so they re-attach to the remaining text. + if last_mode == Mode::Insert && matches!(self.mode, Mode::Normal | Mode::HelixNormal) { + cx.defer_in(window, |vim, _window, cx| { + vim.update_editor(cx, |_, editor, cx| { + editor.refresh_selection_anchors(cx); + }); + }) + } + if leave_selections { return; }