vim: Fix deletion with sentence-motion (#22289)

Thorsten Ball created

Fixes #22151.

Turns out Vim also has some weird behavior with sentence deletion in
case it's on the first character of a line.

Release Notes:

- vim: Fixed deleting sentence-wise (i.e. `d(` and `d)`), which would
previously delete the whole line instead of just a sentence.

Change summary

crates/vim/src/motion.rs                       |  4 
crates/vim/src/normal/delete.rs                | 86 +++++++++++++++++--
crates/vim/test_data/test_delete_sentence.json | 22 +++++
3 files changed, 98 insertions(+), 14 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -585,8 +585,6 @@ impl Motion {
             | NextLineStart
             | PreviousLineStart
             | StartOfLineDownward
-            | SentenceBackward
-            | SentenceForward
             | StartOfParagraph
             | EndOfParagraph
             | WindowTop
@@ -611,6 +609,8 @@ impl Motion {
             | Left
             | Backspace
             | Right
+            | SentenceBackward
+            | SentenceForward
             | Space
             | StartOfLine { .. }
             | EndOfLineDownward

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

@@ -28,23 +28,27 @@ impl Vim {
                         original_columns.insert(selection.id, original_head.column());
                         motion.expand_selection(map, selection, times, true, &text_layout_details);
 
+                        let start_point = selection.start.to_point(map);
+                        let next_line = map
+                            .buffer_snapshot
+                            .clip_point(Point::new(start_point.row + 1, 0), Bias::Left)
+                            .to_display_point(map);
                         match motion {
                             // Motion::NextWordStart on an empty line should delete it.
-                            Motion::NextWordStart { .. } => {
+                            Motion::NextWordStart { .. }
                                 if selection.is_empty()
                                     && map
                                         .buffer_snapshot
-                                        .line_len(MultiBufferRow(selection.start.to_point(map).row))
-                                        == 0
-                                {
-                                    selection.end = map
-                                        .buffer_snapshot
-                                        .clip_point(
-                                            Point::new(selection.start.to_point(map).row + 1, 0),
-                                            Bias::Left,
-                                        )
-                                        .to_display_point(map)
-                                }
+                                        .line_len(MultiBufferRow(start_point.row))
+                                        == 0 =>
+                            {
+                                selection.end = next_line
+                            }
+                            // Sentence motions, when done from start of line, include the newline
+                            Motion::SentenceForward | Motion::SentenceBackward
+                                if selection.start.column() == 0 =>
+                            {
+                                selection.end = next_line
                             }
                             Motion::EndOfDocument {} => {
                                 // Deleting until the end of the document includes the last line, including
@@ -604,4 +608,62 @@ mod test {
         cx.simulate("d t x", "ˇax").await.assert_matches();
         cx.simulate("d t x", "aˇx").await.assert_matches();
     }
+
+    #[gpui::test]
+    async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.simulate(
+            "d )",
+            indoc! {"
+            Fiˇrst. Second. Third.
+            Fourth.
+            "},
+        )
+        .await
+        .assert_matches();
+
+        cx.simulate(
+            "d )",
+            indoc! {"
+            First. Secˇond. Third.
+            Fourth.
+            "},
+        )
+        .await
+        .assert_matches();
+
+        // Two deletes
+        cx.simulate(
+            "d ) d )",
+            indoc! {"
+            First. Second. Thirˇd.
+            Fourth.
+            "},
+        )
+        .await
+        .assert_matches();
+
+        // Should delete whole line if done on first column
+        cx.simulate(
+            "d )",
+            indoc! {"
+            ˇFirst.
+            Fourth.
+            "},
+        )
+        .await
+        .assert_matches();
+
+        // Backwards it should also delete the whole first line
+        cx.simulate(
+            "d (",
+            indoc! {"
+            First.
+            ˇSecond.
+            Fourth.
+            "},
+        )
+        .await
+        .assert_matches();
+    }
 }

crates/vim/test_data/test_delete_sentence.json 🔗

@@ -0,0 +1,22 @@
+{"Put":{"state":"Fiˇrst. Second. Third.\nFourth.\n"}}
+{"Key":"d"}
+{"Key":")"}
+{"Get":{"state":"FiˇSecond. Third.\nFourth.\n","mode":"Normal"}}
+{"Put":{"state":"First. Secˇond. Third.\nFourth.\n"}}
+{"Key":"d"}
+{"Key":")"}
+{"Get":{"state":"First. SecˇThird.\nFourth.\n","mode":"Normal"}}
+{"Put":{"state":"First. Second. Thirˇd.\nFourth.\n"}}
+{"Key":"d"}
+{"Key":")"}
+{"Key":"d"}
+{"Key":")"}
+{"Get":{"state":"First. Second. Thˇi\n","mode":"Normal"}}
+{"Put":{"state":"ˇFirst.\nFourth.\n"}}
+{"Key":"d"}
+{"Key":")"}
+{"Get":{"state":"ˇFourth.\n","mode":"Normal"}}
+{"Put":{"state":"First.\nˇSecond.\nFourth.\n"}}
+{"Key":"d"}
+{"Key":"("}
+{"Get":{"state":"ˇSecond.\nFourth.\n","mode":"Normal"}}