vim: Add ability to spawn multicursors at beginning/end of line (#18183)

Sergio C. created

Closes #17842

Release Notes:

- Added the ability to spawn multiple cursors through the g-A and g-I
motions while in visual select mode.

Change summary

assets/keymaps/vim.json  |  2 
crates/vim/src/visual.rs | 85 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 1 deletion(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -292,6 +292,8 @@
       "g ctrl-x": ["vim::Decrement", { "step": true }],
       "shift-i": "vim::InsertBefore",
       "shift-a": "vim::InsertAfter",
+      "g I": "vim::VisualInsertFirstNonWhiteSpace",
+      "g A": "vim::VisualInsertEndOfLine",
       "shift-j": "vim::JoinLines",
       "r": ["vim::PushOperator", "Replace"],
       "ctrl-c": ["vim::SwitchMode", "Normal"],

crates/vim/src/visual.rs 🔗

@@ -15,7 +15,7 @@ use util::ResultExt;
 use workspace::searchable::Direction;
 
 use crate::{
-    motion::{start_of_line, Motion},
+    motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
     object::Object,
     state::{Mode, Operator},
     Vim,
@@ -37,6 +37,8 @@ actions!(
         SelectNextMatch,
         SelectPreviousMatch,
         RestoreVisualSelection,
+        VisualInsertEndOfLine,
+        VisualInsertFirstNonWhiteSpace,
     ]
 );
 
@@ -51,6 +53,8 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
         vim.toggle_mode(Mode::VisualBlock, cx)
     });
     Vim::action(editor, cx, Vim::other_end);
+    Vim::action(editor, cx, Vim::visual_insert_end_of_line);
+    Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
     Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
         vim.record_current_action(cx);
         vim.visual_delete(false, cx);
@@ -374,6 +378,39 @@ impl Vim {
         }
     }
 
+    fn visual_insert_end_of_line(&mut self, _: &VisualInsertEndOfLine, cx: &mut ViewContext<Self>) {
+        self.update_editor(cx, |_, editor, cx| {
+            editor.split_selection_into_lines(&Default::default(), cx);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.move_cursors_with(|map, cursor, _| {
+                    (next_line_end(map, cursor, 1), SelectionGoal::None)
+                });
+            });
+        });
+
+        self.switch_mode(Mode::Insert, false, cx);
+    }
+
+    fn visual_insert_first_non_white_space(
+        &mut self,
+        _: &VisualInsertFirstNonWhiteSpace,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.update_editor(cx, |_, editor, cx| {
+            editor.split_selection_into_lines(&Default::default(), cx);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                s.move_cursors_with(|map, cursor, _| {
+                    (
+                        first_non_whitespace(map, false, cursor),
+                        SelectionGoal::None,
+                    )
+                });
+            });
+        });
+
+        self.switch_mode(Mode::Insert, false, cx);
+    }
+
     fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
         if self.mode == mode {
             self.switch_mode(Mode::Normal, false, cx);
@@ -714,6 +751,52 @@ mod test {
             ˇ"});
     }
 
+    #[gpui::test]
+    async fn test_visual_insert_first_non_whitespace(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::Visual,
+        );
+        cx.simulate_keystrokes("g I");
+        cx.assert_state(
+            indoc! {
+                "ˇThe quick brown
+                ˇfox jumps over
+                ˇthe lazy dog"
+            },
+            Mode::Insert,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_visual_insert_end_of_line(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::Visual,
+        );
+        cx.simulate_keystrokes("g A");
+        cx.assert_state(
+            indoc! {
+                "The quick brownˇ
+                fox jumps overˇ
+                the lazy dogˇ"
+            },
+            Mode::Insert,
+        );
+    }
+
     #[gpui::test]
     async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;