vim: Fix add empty line (#30987)

5brian created

Fixes: 

`] space` does not consume counts, and it gets applied to the next
action.

`] space` on an empty line causes cursor to move to the next line.

Release Notes:

- N/A

Change summary

crates/vim/src/normal.rs                               | 64 +++++++++
crates/vim/test_data/test_insert_empty_line.json       | 78 ++++++++++++
crates/vim/test_data/test_insert_empty_line_above.json | 24 ---
3 files changed, 138 insertions(+), 28 deletions(-)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -548,6 +548,8 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.record_current_action(cx);
+        let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         self.update_editor(window, cx, |_, editor, window, cx| {
             editor.transact(window, cx, |editor, _, cx| {
                 let selections = editor.selections.all::<Point>(cx);
@@ -560,7 +562,7 @@ impl Vim {
                     .into_iter()
                     .map(|row| {
                         let start_of_line = Point::new(row, 0);
-                        (start_of_line..start_of_line, "\n".to_string())
+                        (start_of_line..start_of_line, "\n".repeat(count))
                     })
                     .collect::<Vec<_>>();
                 editor.edit(edits, cx);
@@ -575,10 +577,17 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.record_current_action(cx);
+        let count = Vim::take_count(cx).unwrap_or(1);
+        Vim::take_forced_motion(cx);
         self.update_editor(window, cx, |_, editor, window, cx| {
-            editor.transact(window, cx, |editor, _, cx| {
+            editor.transact(window, cx, |editor, window, cx| {
                 let selections = editor.selections.all::<Point>(cx);
                 let snapshot = editor.buffer().read(cx).snapshot(cx);
+                let (_map, display_selections) = editor.selections.all_display(cx);
+                let original_positions = display_selections
+                    .iter()
+                    .map(|s| (s.id, s.head()))
+                    .collect::<HashMap<_, _>>();
 
                 let selection_end_rows: BTreeSet<u32> = selections
                     .into_iter()
@@ -588,10 +597,18 @@ impl Vim {
                     .into_iter()
                     .map(|row| {
                         let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row)));
-                        (end_of_line..end_of_line, "\n".to_string())
+                        (end_of_line..end_of_line, "\n".repeat(count))
                     })
                     .collect::<Vec<_>>();
                 editor.edit(edits, cx);
+
+                editor.change_selections(None, window, cx, |s| {
+                    s.move_with(|_, selection| {
+                        if let Some(position) = original_positions.get(&selection.id) {
+                            selection.collapse_to(*position, SelectionGoal::None);
+                        }
+                    });
+                });
             });
         });
     }
@@ -1331,10 +1348,19 @@ mod test {
     }
 
     #[gpui::test]
-    async fn test_insert_empty_line_above(cx: &mut gpui::TestAppContext) {
+    async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
         cx.simulate("[ space", "ˇ").await.assert_matches();
         cx.simulate("[ space", "The ˇquick").await.assert_matches();
+        cx.simulate_at_each_offset(
+            "3 [ space",
+            indoc! {"
+            The qˇuick
+            brown ˇfox
+            jumps ˇover"},
+        )
+        .await
+        .assert_matches();
         cx.simulate_at_each_offset(
             "[ space",
             indoc! {"
@@ -1353,6 +1379,36 @@ mod test {
         )
         .await
         .assert_matches();
+
+        cx.simulate("] space", "ˇ").await.assert_matches();
+        cx.simulate("] space", "The ˇquick").await.assert_matches();
+        cx.simulate_at_each_offset(
+            "3 ] space",
+            indoc! {"
+            The qˇuick
+            brown ˇfox
+            jumps ˇover"},
+        )
+        .await
+        .assert_matches();
+        cx.simulate_at_each_offset(
+            "] space",
+            indoc! {"
+            The qˇuick
+            brown ˇfox
+            jumps ˇover"},
+        )
+        .await
+        .assert_matches();
+        cx.simulate(
+            "] space",
+            indoc! {"
+            The quick
+            ˇ
+            brown fox"},
+        )
+        .await
+        .assert_matches();
     }
 
     #[gpui::test]

crates/vim/test_data/test_insert_empty_line.json 🔗

@@ -0,0 +1,78 @@
+{"Put":{"state":"ˇ"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nˇ","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nThe ˇquick","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"3"}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\n\n\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"3"}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\n\n\n\nbrown ˇfox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"3"}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown fox\n\n\n\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\n\nbrown ˇfox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown fox\n\njumps ˇover","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"["}
+{"Key":"space"}
+{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Normal"}}
+{"Put":{"state":"ˇ"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"ˇ\n","mode":"Normal"}}
+{"Put":{"state":"The ˇquick"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The ˇquick\n","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"3"}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The qˇuick\n\n\n\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"3"}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown ˇfox\n\n\n\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"3"}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n\n\n","mode":"Normal"}}
+{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The qˇuick\n\nbrown fox\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown ˇfox\n\njumps over","mode":"Normal"}}
+{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n","mode":"Normal"}}
+{"Put":{"state":"The quick\nˇ\nbrown fox"}}
+{"Key":"]"}
+{"Key":"space"}
+{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Normal"}}

crates/vim/test_data/test_insert_empty_line_above.json 🔗

@@ -1,24 +0,0 @@
-{"Put":{"state":"ˇ"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"\nˇ","mode":"Normal"}}
-{"Put":{"state":"The ˇquick"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"\nThe ˇquick","mode":"Normal"}}
-{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}}
-{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"The quick\n\nbrown ˇfox\njumps over","mode":"Normal"}}
-{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"The quick\nbrown fox\n\njumps ˇover","mode":"Normal"}}
-{"Put":{"state":"The quick\nˇ\nbrown fox"}}
-{"Key":"["}
-{"Key":"space"}
-{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Normal"}}