Fix vim full line operations failing when no trailing newline (#24409)

Ben Kunkle created

Closes #24270

Release Notes:

- Fixed an issue where doing line-wise operations in vim mode on the
last line of a file with no trailing newline would not work properly

Change summary

crates/vim/src/normal.rs                                               | 36 
crates/vim/src/normal/yank.rs                                          | 15 
crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json  |  7 
crates/vim/test_data/test_increment_bin_wrapping_and_padding.json      |  4 
crates/vim/test_data/test_increment_hex_wrapping_and_padding.json      |  4 
crates/vim/test_data/test_increment_inline.json                        |  4 
crates/vim/test_data/test_increment_sign_change.json                   |  2 
crates/vim/test_data/test_increment_wrapping.json                      |  8 
crates/vim/test_data/test_yank_line_with_trailing_newline.json         |  5 
crates/vim/test_data/test_yank_line_without_trailing_newline.json      |  5 
crates/vim/test_data/test_yank_multiline_without_trailing_newline.json |  6 
11 files changed, 79 insertions(+), 17 deletions(-)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -1545,4 +1545,40 @@ mod test {
         cx.simulate_shared_keystrokes("x escape shift-o").await;
         cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n");
     }
+
+    #[gpui::test]
+    async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("heˇllo\n").await;
+        cx.simulate_shared_keystrokes("y y p").await;
+        cx.shared_state().await.assert_eq("hello\nˇhello\n");
+    }
+
+    #[gpui::test]
+    async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("heˇllo").await;
+        cx.simulate_shared_keystrokes("y y p").await;
+        cx.shared_state().await.assert_eq("hello\nˇhello");
+    }
+
+    #[gpui::test]
+    async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("heˇllo\nhello").await;
+        cx.simulate_shared_keystrokes("2 y y p").await;
+        cx.shared_state()
+            .await
+            .assert_eq("hello\nˇhello\nhello\nhello");
+    }
+
+    #[gpui::test]
+    async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state("heˇllo").await;
+        cx.simulate_shared_keystrokes("d d").await;
+        cx.shared_state().await.assert_eq("ˇ");
+        cx.simulate_shared_keystrokes("p p").await;
+        cx.shared_state().await.assert_eq("\nhello\nˇhello");
+    }
 }

crates/vim/src/normal/yank.rs 🔗

@@ -162,13 +162,16 @@ impl Vim {
                 // that line, we will have expanded the start of the selection to ensure it
                 // contains a newline (so that delete works as expected). We undo that change
                 // here.
-                let is_last_line = linewise
-                    && end.row == buffer.max_row().0
-                    && buffer.max_point().column > 0
-                    && start.row < buffer.max_row().0
+                let max_point = buffer.max_point();
+                let should_adjust_start = linewise
+                    && end.row == max_point.row
+                    && max_point.column > 0
+                    && start.row < max_point.row
                     && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
+                let should_add_newline =
+                    should_adjust_start || (end == max_point && max_point.column > 0 && linewise);
 
-                if is_last_line {
+                if should_adjust_start {
                     start = Point::new(start.row + 1, 0);
                 }
 
@@ -179,7 +182,7 @@ impl Vim {
                 for chunk in buffer.text_for_range(start..end) {
                     text.push_str(chunk);
                 }
-                if is_last_line {
+                if should_add_newline {
                     text.push('\n');
                 }
                 clipboard_selections.push(ClipboardSelection {

crates/vim/test_data/test_increment_bin_wrapping_and_padding.json 🔗

@@ -1,10 +1,10 @@
 {"Put":{"state":"0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1\n"}}
 {"Key":"ctrl-a"}
-{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n", "mode":"Normal"}}
+{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
 {"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
 {"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1\n","mode":"Normal"}}
 {"Key":"2"}
 {"Key":"ctrl-x"}
-{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n", "mode":"Normal"}}
+{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}}

crates/vim/test_data/test_increment_hex_wrapping_and_padding.json 🔗

@@ -1,10 +1,10 @@
 {"Put":{"state":"0xfffffffffffffffffffˇf\n"}}
 {"Key":"ctrl-a"}
-{"Get":{"state":"0x0000fffffffffffffffˇf\n", "mode":"Normal"}}
+{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
 {"Get":{"state":"0x0000000000000000000ˇ0\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
 {"Get":{"state":"0x0000000000000000000ˇ1\n","mode":"Normal"}}
 {"Key":"2"}
 {"Key":"ctrl-x"}
-{"Get":{"state":"0x0000fffffffffffffffˇf\n", "mode":"Normal"}}
+{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}}

crates/vim/test_data/test_increment_inline.json 🔗

@@ -2,9 +2,9 @@
 {"Key":"ctrl-a"}
 {"Get":{"state":"inline0x3ˇau32\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
-{"Get":{"state":"inline0x3ˇbu32\n", "mode":"Normal"}}
+{"Get":{"state":"inline0x3ˇbu32\n","mode":"Normal"}}
 {"Key":"l"}
 {"Key":"l"}
 {"Key":"l"}
 {"Key":"ctrl-a"}
-{"Get":{"state":"inline0x3bu3ˇ3\n", "mode":"Normal"}}
+{"Get":{"state":"inline0x3bu3ˇ3\n","mode":"Normal"}}

crates/vim/test_data/test_increment_wrapping.json 🔗

@@ -2,12 +2,12 @@
 {"Key":"ctrl-a"}
 {"Get":{"state":"1844674407370955161ˇ5\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
-{"Get":{"state":"-1844674407370955161ˇ5\n", "mode":"Normal"}}
+{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}}
 {"Key":"ctrl-a"}
-{"Get":{"state":"-1844674407370955161ˇ4\n", "mode":"Normal"}}
+{"Get":{"state":"-1844674407370955161ˇ4\n","mode":"Normal"}}
 {"Key":"3"}
 {"Key":"ctrl-x"}
-{"Get":{"state":"1844674407370955161ˇ4\n", "mode":"Normal"}}
+{"Get":{"state":"1844674407370955161ˇ4\n","mode":"Normal"}}
 {"Key":"2"}
 {"Key":"ctrl-a"}
-{"Get":{"state":"-1844674407370955161ˇ5\n", "mode":"Normal"}}
+{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}}