Add "shift-r" and "g ." support for helix mode (#35468)

Romans Malinovskis created

Related #4642
Compatible with #34136

Release Notes:

- Helix: `Shift+R` works as Paste instead of taking you to ReplaceMode
- Helix: `g .` goes to last modification place (similar to `. in vim)

Change summary

assets/keymaps/vim.json |  2 
crates/vim/src/helix.rs | 87 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 89 insertions(+)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -428,11 +428,13 @@
       "g h": "vim::StartOfLine",
       "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
       "g e": "vim::EndOfDocument",
+      "g .": "vim::HelixGotoLastModification", // go to last modification
       "g r": "editor::FindAllReferences", // zed specific
       "g t": "vim::WindowTop",
       "g c": "vim::WindowMiddle",
       "g b": "vim::WindowBottom",
 
+      "shift-r": "editor::Paste",
       "x": "editor::SelectLine",
       "shift-x": "editor::SelectLine",
       "%": "editor::SelectAll",

crates/vim/src/helix.rs 🔗

@@ -23,6 +23,8 @@ actions!(
         HelixInsert,
         /// Appends at the end of the selection.
         HelixAppend,
+        /// Goes to the location of the last modification.
+        HelixGotoLastModification,
     ]
 );
 
@@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, Vim::helix_insert);
     Vim::action(editor, cx, Vim::helix_append);
     Vim::action(editor, cx, Vim::helix_yank);
+    Vim::action(editor, cx, Vim::helix_goto_last_modification);
 }
 
 impl Vim {
@@ -430,6 +433,15 @@ impl Vim {
         });
         self.switch_mode(Mode::HelixNormal, true, window, cx);
     }
+
+    pub fn helix_goto_last_modification(
+        &mut self,
+        _: &HelixGotoLastModification,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.jump(".".into(), false, false, window, cx);
+    }
 }
 
 #[cfg(test)]
@@ -441,6 +453,7 @@ mod test {
     #[gpui::test]
     async fn test_word_motions(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
         // «
         // ˇ
         // »
@@ -502,6 +515,7 @@ mod test {
     #[gpui::test]
     async fn test_delete(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
 
         // test delete a selection
         cx.set_state(
@@ -582,6 +596,7 @@ mod test {
     #[gpui::test]
     async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
 
         cx.set_state(
             indoc! {"
@@ -635,6 +650,7 @@ mod test {
     #[gpui::test]
     async fn test_newline_char(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
 
         cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
 
@@ -652,6 +668,7 @@ mod test {
     #[gpui::test]
     async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
         cx.set_state(
             indoc! {"
             «The ˇ»quick brown
@@ -674,6 +691,7 @@ mod test {
     #[gpui::test]
     async fn test_append(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
         // test from the end of the selection
         cx.set_state(
             indoc! {"
@@ -716,6 +734,7 @@ mod test {
     #[gpui::test]
     async fn test_replace(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
 
         // No selection (single character)
         cx.set_state("ˇaa", Mode::HelixNormal);
@@ -763,4 +782,72 @@ mod test {
         cx.shared_clipboard().assert_eq("worl");
         cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
     }
+    #[gpui::test]
+    async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+
+        // First copy some text to clipboard
+        cx.set_state("«hello worldˇ»", Mode::HelixNormal);
+        cx.simulate_keystrokes("y");
+
+        // Test paste with shift-r on single cursor
+        cx.set_state("foo ˇbar", Mode::HelixNormal);
+        cx.simulate_keystrokes("shift-r");
+
+        cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
+
+        // Test paste with shift-r on selection
+        cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
+        cx.simulate_keystrokes("shift-r");
+
+        cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
+    }
+
+    #[gpui::test]
+    async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+
+        // Make a modification at a specific location
+        cx.set_state("ˇhello", Mode::HelixNormal);
+        assert_eq!(cx.mode(), Mode::HelixNormal);
+        cx.simulate_keystrokes("i");
+        assert_eq!(cx.mode(), Mode::Insert);
+        cx.simulate_keystrokes("escape");
+        assert_eq!(cx.mode(), Mode::HelixNormal);
+    }
+
+    #[gpui::test]
+    async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+
+        // Make a modification at a specific location
+        cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
+        cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
+        cx.simulate_keystrokes("i");
+        cx.simulate_keystrokes("escape");
+        cx.simulate_keystrokes("i");
+        cx.simulate_keystrokes("m o d i f i e d space");
+        cx.simulate_keystrokes("escape");
+
+        // TODO: this fails, because state is no longer helix
+        cx.assert_state(
+            "line one\nline modified ˇtwo\nline three",
+            Mode::HelixNormal,
+        );
+
+        // Move cursor away from the modification
+        cx.simulate_keystrokes("up");
+
+        // Use "g ." to go back to last modification
+        cx.simulate_keystrokes("g .");
+
+        // Verify we're back at the modification location and still in HelixNormal mode
+        cx.assert_state(
+            "line one\nline modifiedˇ two\nline three",
+            Mode::HelixNormal,
+        );
+    }
 }