diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e200c24b94468b141020e12c0230fb1908ffbe8e..fae810d64c587f96c587057615b138b4baabd227 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -386,6 +386,8 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { + let temp_mode_motion = motion.clone(); + match operator { None => self.move_cursor(motion, times, window, cx), Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx), @@ -475,7 +477,7 @@ impl Vim { } } // Exit temporary normal mode (if active). - self.exit_temporary_normal(window, cx); + self.exit_temporary_normal(Some(&temp_mode_motion), window, cx); } pub fn normal_object( @@ -1052,9 +1054,25 @@ impl Vim { }); } - fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context) { + /// If temporary mode is enabled, switches back to insert mode, using the + /// provided `motion` to determine whether to move the cursor before + /// re-enabling insert mode, for example, when `EndOfLine` ($) is used. + fn exit_temporary_normal( + &mut self, + motion: Option<&Motion>, + window: &mut Window, + cx: &mut Context, + ) { if self.temp_mode { self.switch_mode(Mode::Insert, true, window, cx); + + // Since we're switching from `Normal` mode to `Insert` mode, we'll + // move the cursor one position to the right, to ensure that, for + // motions like `EndOfLine` ($), the cursor is actually at the end + // of line and not on the last character. + if matches!(motion, Some(Motion::EndOfLine { .. })) { + self.move_cursor(Motion::Right, Some(1), window, cx); + } } } } @@ -2269,4 +2287,35 @@ mod test { assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); }); } + + #[gpui::test] + async fn test_temporary_mode(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // Test jumping to the end of the line ($). + cx.set_shared_state(indoc! {"lorem ˇipsum"}).await; + cx.simulate_shared_keystrokes("i").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("ctrl-o $").await; + cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"}); + + // Test jumping to the next word. + cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await; + cx.simulate_shared_keystrokes("a").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("a n d space ctrl-o w").await; + cx.shared_state() + .await + .assert_eq(indoc! {"lorem and ipsum ˇdolor"}); + + // Test yanking to end of line ($). + cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await; + cx.simulate_shared_keystrokes("i").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("a n d space ctrl-o y $") + .await; + cx.shared_state() + .await + .assert_eq(indoc! {"lorem and ˇipsum dolor"}); + } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index ff884e3b7393b39b86114338fe2af11e384e1fa0..9346d76323c4fb6c181fb914587a710c94be4537 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -96,7 +96,7 @@ impl Vim { ) { let amount = by(Vim::take_count(cx).map(|c| c as f32)); Vim::take_forced_motion(cx); - self.exit_temporary_normal(window, cx); + self.exit_temporary_normal(None, window, cx); self.update_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, amount, window, cx) }); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index d5a45fca544d61735f62a8f46e849db2c009847f..4f1274dd88359fe8c3eb1b08ab3910c513b2d98d 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -59,7 +59,7 @@ impl Vim { }); }); }); - self.exit_temporary_normal(window, cx); + self.exit_temporary_normal(None, window, cx); } pub fn yank_object( @@ -90,7 +90,7 @@ impl Vim { }); }); }); - self.exit_temporary_normal(window, cx); + self.exit_temporary_normal(None, window, cx); } pub fn yank_selections_content( diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index ce2bb6eb7b6f77788f3bc002ff979fdbb251cb94..21cdda111c4fdacaf0871dd087bca01de6f83957 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -31,6 +31,7 @@ pub struct SharedState { } impl SharedState { + /// Assert that both Zed and NeoVim have the same content and mode. #[track_caller] pub fn assert_matches(&self) { if self.neovim != self.editor || self.neovim_mode != self.editor_mode { diff --git a/crates/vim/test_data/test_temporary_mode.json b/crates/vim/test_data/test_temporary_mode.json new file mode 100644 index 0000000000000000000000000000000000000000..be370cf744f9fbd9bfed0a89a6db5ef7b6d568ad --- /dev/null +++ b/crates/vim/test_data/test_temporary_mode.json @@ -0,0 +1,27 @@ +{"Put":{"state":"lorem ˇipsum"}} +{"Key":"i"} +{"Get":{"state":"lorem ˇipsum","mode":"Insert"}} +{"Key":"ctrl-o"} +{"Key":"$"} +{"Get":{"state":"lorem ipsumˇ","mode":"Insert"}} +{"Put":{"state":"loremˇ ipsum dolor"}} +{"Key":"a"} +{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} +{"Key":"a"} +{"Key":"n"} +{"Key":"d"} +{"Key":"space"} +{"Key":"ctrl-o"} +{"Key":"w"} +{"Get":{"state":"lorem and ipsum ˇdolor","mode":"Insert"}} +{"Put":{"state":"lorem ˇipsum dolor"}} +{"Key":"i"} +{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} +{"Key":"a"} +{"Key":"n"} +{"Key":"d"} +{"Key":"space"} +{"Key":"ctrl-o"} +{"Key":"y"} +{"Key":"$"} +{"Get":{"state":"lorem and ˇipsum dolor","mode":"Insert"}}