vim: Fix `insert before` in visual modes (#25603)

brian tan created

Closes #22536

Changes:
- Visual and visual block: Cursor at start of selection.
- Visual line: Cursor at start on line.
- Uses different handling since the selection does not actually change
in vline.

Release Notes:

- vim: Fixed insert before (`shift-i`) in visual modes.

Change summary

crates/vim/src/normal.rs                                       | 46 ++++
crates/vim/test_data/test_visual_mode_insert_before_after.json | 14 +
2 files changed, 60 insertions(+)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -291,6 +291,21 @@ impl Vim {
 
     fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context<Self>) {
         self.start_recording(cx);
+        if self.mode.is_visual() {
+            let current_mode = self.mode;
+            self.update_editor(window, cx, |_, editor, window, cx| {
+                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+                    s.move_with(|map, selection| {
+                        if current_mode == Mode::VisualLine {
+                            let start_of_line = motion::start_of_line(map, false, selection.start);
+                            selection.collapse_to(start_of_line, SelectionGoal::None)
+                        } else {
+                            selection.collapse_to(selection.start, SelectionGoal::None)
+                        }
+                    });
+                });
+            });
+        }
         self.switch_mode(Mode::Insert, false, window, cx);
     }
 
@@ -1589,4 +1604,35 @@ mod test {
         cx.simulate_shared_keystrokes("p p").await;
         cx.shared_state().await.assert_eq("\nhello\nˇhello");
     }
+
+    #[gpui::test]
+    async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("heˇllo").await;
+        cx.simulate_shared_keystrokes("v i w shift-i").await;
+        cx.shared_state().await.assert_eq("ˇhello");
+
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox ˇjumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("shift-v shift-i").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick brown
+            ˇfox jumps over
+            the lazy dog"});
+
+        cx.set_shared_state(indoc! {"
+            The quick brown
+            fox ˇjumps over
+            the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes("shift-v shift-a").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            The quick brown
+            fox jˇumps over
+            the lazy dog"});
+    }
 }

crates/vim/test_data/test_visual_mode_insert_before_after.json 🔗

@@ -0,0 +1,14 @@
+{"Put":{"state":"heˇllo"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"shift-i"}
+{"Get":{"state":"ˇhello","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"shift-i"}
+{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Insert"}}
+{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"shift-a"}
+{"Get":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog","mode":"Insert"}}