Improve Helix insert (#34765)

Pablo Ramón Guevara created

Closes #34763 

Release Notes:

- Improved insert in `helix_mode` when a selection exists to better
match helix's behavior: collapse selection to avoid replacing it
- Improved append (`insert_after`) to better match helix's behavior:
move cursor to end of selection if it exists

Change summary

assets/keymaps/vim.json |   6 +
crates/vim/src/helix.rs | 110 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 112 insertions(+), 4 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -220,6 +220,8 @@
   {
     "context": "vim_mode == normal",
     "bindings": {
+      "i": "vim::InsertBefore",
+      "a": "vim::InsertAfter",
       "ctrl-[": "editor::Cancel",
       ":": "command_palette::Toggle",
       "c": "vim::PushChange",
@@ -353,9 +355,7 @@
       "shift-d": "vim::DeleteToEndOfLine",
       "shift-j": "vim::JoinLines",
       "shift-y": "vim::YankLine",
-      "i": "vim::InsertBefore",
       "shift-i": "vim::InsertFirstNonWhitespace",
-      "a": "vim::InsertAfter",
       "shift-a": "vim::InsertEndOfLine",
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
@@ -377,6 +377,8 @@
   {
     "context": "vim_mode == helix_normal && !menu",
     "bindings": {
+      "i": "vim::HelixInsert",
+      "a": "vim::HelixAppend",
       "ctrl-[": "editor::Cancel",
       ";": "vim::HelixCollapseSelection",
       ":": "command_palette::Toggle",

crates/vim/src/helix.rs 🔗

@@ -4,18 +4,28 @@ use gpui::{Context, Window};
 use language::{CharClassifier, CharKind};
 use text::SelectionGoal;
 
-use crate::{Vim, motion::Motion, state::Mode};
+use crate::{
+    Vim,
+    motion::{Motion, right},
+    state::Mode,
+};
 
 actions!(
     vim,
     [
         /// Switches to normal mode after the cursor (Helix-style).
-        HelixNormalAfter
+        HelixNormalAfter,
+        /// Inserts at the beginning of the selection.
+        HelixInsert,
+        /// Appends at the end of the selection.
+        HelixAppend,
     ]
 );
 
 pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, Vim::helix_normal_after);
+    Vim::action(editor, cx, Vim::helix_insert);
+    Vim::action(editor, cx, Vim::helix_append);
 }
 
 impl Vim {
@@ -299,6 +309,38 @@ impl Vim {
             _ => self.helix_move_and_collapse(motion, times, window, cx),
         }
     }
+
+    fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
+        self.start_recording(cx);
+        self.update_editor(window, cx, |_, editor, window, cx| {
+            editor.change_selections(Default::default(), window, cx, |s| {
+                s.move_with(|_map, selection| {
+                    // In helix normal mode, move cursor to start of selection and collapse
+                    if !selection.is_empty() {
+                        selection.collapse_to(selection.start, SelectionGoal::None);
+                    }
+                });
+            });
+        });
+        self.switch_mode(Mode::Insert, false, window, cx);
+    }
+
+    fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
+        self.start_recording(cx);
+        self.switch_mode(Mode::Insert, false, window, cx);
+        self.update_editor(window, cx, |_, editor, window, cx| {
+            editor.change_selections(Default::default(), window, cx, |s| {
+                s.move_with(|map, selection| {
+                    let point = if selection.is_empty() {
+                        right(map, selection.head(), 1)
+                    } else {
+                        selection.end
+                    };
+                    selection.collapse_to(point, SelectionGoal::None);
+                });
+            });
+        });
+    }
 }
 
 #[cfg(test)]
@@ -497,4 +539,68 @@ mod test {
 
         cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
     }
+
+    #[gpui::test]
+    async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.set_state(
+            indoc! {"
+            «The ˇ»quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        cx.simulate_keystrokes("i");
+
+        cx.assert_state(
+            indoc! {"
+            ˇThe quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Insert,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_append(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        // test from the end of the selection
+        cx.set_state(
+            indoc! {"
+            «Theˇ» quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        cx.simulate_keystrokes("a");
+
+        cx.assert_state(
+            indoc! {"
+            Theˇ quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Insert,
+        );
+
+        // test from the beginning of the selection
+        cx.set_state(
+            indoc! {"
+            «ˇThe» quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::HelixNormal,
+        );
+
+        cx.simulate_keystrokes("a");
+
+        cx.assert_state(
+            indoc! {"
+            Theˇ quick brown
+            fox jumps over
+            the lazy dog."},
+            Mode::Insert,
+        );
+    }
 }