Merge pull request #1803 from zed-industries/fix-vim-motion-panic

Kay Simmons created

Add more explicit neovim testcase exemptions

Change summary

crates/vim/src/motion.rs                                            |  61 
crates/vim/src/normal.rs                                            |  11 
crates/vim/src/normal/change.rs                                     | 520 
crates/vim/src/normal/delete.rs                                     | 498 
crates/vim/src/object.rs                                            | 178 
crates/vim/src/test/neovim_backed_binding_test_context.rs           |  28 
crates/vim/src/test/neovim_backed_test_context.rs                   |  71 
crates/vim/src/test/vim_binding_test_context.rs                     |   5 
crates/vim/test_data/test_change_0.json                             |   1 
crates/vim/test_data/test_change_b.json                             |   1 
crates/vim/test_data/test_change_backspace.json                     |   1 
crates/vim/test_data/test_change_e.json                             |   1 
crates/vim/test_data/test_change_end_of_document.json               |   1 
crates/vim/test_data/test_change_end_of_line.json                   |   1 
crates/vim/test_data/test_change_gg.json                            |   1 
crates/vim/test_data/test_change_h.json                             |   1 
crates/vim/test_data/test_change_j.json                             |   1 
crates/vim/test_data/test_change_k.json                             |   1 
crates/vim/test_data/test_change_l.json                             |   1 
crates/vim/test_data/test_change_surrounding_character_objects.json |   0 
crates/vim/test_data/test_change_w.json                             |   1 
crates/vim/test_data/test_dd.json                                   |   2 
crates/vim/test_data/test_delete_0.json                             |   1 
crates/vim/test_data/test_delete_b.json                             |   1 
crates/vim/test_data/test_delete_e.json                             |   1 
crates/vim/test_data/test_delete_end_of_document.json               |   1 
crates/vim/test_data/test_delete_end_of_line.json                   |   1 
crates/vim/test_data/test_delete_gg.json                            |   1 
crates/vim/test_data/test_delete_h.json                             |   1 
crates/vim/test_data/test_delete_j.json                             |   1 
crates/vim/test_data/test_delete_k.json                             |   1 
crates/vim/test_data/test_delete_l.json                             |   1 
crates/vim/test_data/test_delete_surrounding_character_objects.json |   0 
crates/vim/test_data/test_delete_w.json                             |   1 
crates/vim/test_data/test_visual_sentence_object.json               |   0 
35 files changed, 691 insertions(+), 706 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -15,7 +15,7 @@ use crate::{
     Vim,
 };
 
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum Motion {
     Left,
     Backspace,
@@ -139,14 +139,22 @@ impl Motion {
 
     pub fn inclusive(self) -> bool {
         use Motion::*;
-        if self.linewise() {
-            return true;
-        }
-
         match self {
-            EndOfLine | NextWordEnd { .. } | Matching => true,
-            Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
-            _ => panic!("Exclusivity not defined for {self:?}"),
+            Down
+            | Up
+            | StartOfDocument
+            | EndOfDocument
+            | CurrentLine
+            | EndOfLine
+            | NextWordEnd { .. }
+            | Matching => true,
+            Left
+            | Backspace
+            | Right
+            | StartOfLine
+            | NextWordStart { .. }
+            | PreviousWordStart { .. }
+            | FirstNonWhitespace => false,
         }
     }
 
@@ -194,27 +202,29 @@ impl Motion {
         times: usize,
         expand_to_surrounding_newline: bool,
     ) {
-        let (head, goal) = self.move_point(map, selection.head(), selection.goal, times);
-        selection.set_head(head, goal);
+        let (new_head, goal) = self.move_point(map, selection.head(), selection.goal, times);
+        selection.set_head(new_head, goal);
 
         if self.linewise() {
-            selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
-
-            if expand_to_surrounding_newline {
-                if selection.end.row() < map.max_point().row() {
-                    *selection.end.row_mut() += 1;
-                    *selection.end.column_mut() = 0;
-                    selection.end = map.clip_point(selection.end, Bias::Right);
-                    // Don't reset the end here
-                    return;
-                } else if selection.start.row() > 0 {
-                    *selection.start.row_mut() -= 1;
-                    *selection.start.column_mut() = map.line_len(selection.start.row());
-                    selection.start = map.clip_point(selection.start, Bias::Left);
+            if selection.start != selection.end {
+                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
+
+                if expand_to_surrounding_newline {
+                    if selection.end.row() < map.max_point().row() {
+                        *selection.end.row_mut() += 1;
+                        *selection.end.column_mut() = 0;
+                        selection.end = map.clip_point(selection.end, Bias::Right);
+                        // Don't reset the end here
+                        return;
+                    } else if selection.start.row() > 0 {
+                        *selection.start.row_mut() -= 1;
+                        *selection.start.column_mut() = map.line_len(selection.start.row());
+                        selection.start = map.clip_point(selection.start, Bias::Left);
+                    }
                 }
-            }
 
-            (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
+                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
+            }
         } else {
             // If the motion is exclusive and the end of the motion is in column 1, the
             // end of the motion is moved to the end of the previous line and the motion
@@ -222,6 +232,7 @@ impl Motion {
             // but "d}" will not include that line.
             let mut inclusive = self.inclusive();
             if !inclusive
+                && self != Motion::Backspace
                 && selection.end.row() > selection.start.row()
                 && selection.end.column() == 0
                 && selection.end.row() > 0

crates/vim/src/normal.rs 🔗

@@ -372,7 +372,7 @@ mod test {
             Mode::{self, *},
             Namespace, Operator,
         },
-        test::{NeovimBackedTestContext, VimTestContext},
+        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
     };
 
     #[gpui::test]
@@ -741,11 +741,14 @@ mod test {
                 brown ˇfox
                 jumps ˇover"})
             .await;
-        cx.assert(indoc! {"
+        cx.assert_exempted(
+            indoc! {"
                 The quick
                 ˇ
-                brown fox"})
-            .await;
+                brown fox"},
+            ExemptionFeatures::DeletionOnEmptyLine,
+        )
+        .await;
     }
 
     #[gpui::test]

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

@@ -85,372 +85,271 @@ fn expand_changed_word_selection(
 mod test {
     use indoc::indoc;
 
-    use crate::{
-        state::Mode,
-        test::{NeovimBackedTestContext, VimTestContext},
-    };
+    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
 
     #[gpui::test]
     async fn test_change_h(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
-        cx.assert("Teˇst", "Tˇst");
-        cx.assert("Tˇest", "ˇest");
-        cx.assert("ˇTest", "ˇTest");
-        cx.assert(
-            indoc! {"
-                Test
-                ˇtest"},
-            indoc! {"
-                Test
-                ˇtest"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tˇest").await;
+        cx.assert("ˇTest").await;
+        cx.assert(indoc! {"
+            Test
+            ˇtest"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["c", "backspace"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tˇest").await;
+        cx.assert("ˇTest").await;
+        cx.assert(indoc! {"
+            Test
+            ˇtest"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_change_l(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
-        cx.assert("Teˇst", "Teˇt");
-        cx.assert("Tesˇt", "Tesˇ");
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tesˇt").await;
     }
 
     #[gpui::test]
     async fn test_change_w(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
-        cx.assert("Teˇst", "Teˇ");
-        cx.assert("Tˇest test", "Tˇ test");
-        cx.assert("Testˇ  test", "Testˇtest");
-        cx.assert(
-            indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tˇest test").await;
+        cx.assert("Testˇ  test").await;
+        cx.assert(indoc! {"
                 Test teˇst
-                test"},
-            indoc! {"
-                Test teˇ
-                test"},
-        );
-        cx.assert(
-            indoc! {"
+                test"})
+            .await;
+        cx.assert(indoc! {"
                 Test tesˇt
-                test"},
-            indoc! {"
-                Test tesˇ
-                test"},
-        );
-        cx.assert(
-            indoc! {"
-                Test test
-                ˇ
-                test"},
-            indoc! {"
+                test"})
+            .await;
+        cx.assert(indoc! {"
                 Test test
                 ˇ
-                test"},
-        );
+                test"})
+            .await;
 
         let mut cx = cx.binding(["c", "shift-w"]);
-        cx.assert("Test teˇst-test test", "Test teˇ test");
+        cx.assert("Test teˇst-test test").await;
     }
 
     #[gpui::test]
     async fn test_change_e(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
-        cx.assert("Teˇst Test", "Teˇ Test");
-        cx.assert("Tˇest test", "Tˇ test");
-        cx.assert(
-            indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
+        cx.assert("Teˇst Test").await;
+        cx.assert("Tˇest test").await;
+        cx.assert(indoc! {"
                 Test teˇst
-                test"},
-            indoc! {"
-                Test teˇ
-                test"},
-        );
-        cx.assert(
-            indoc! {"
+                test"})
+            .await;
+        cx.assert(indoc! {"
                 Test tesˇt
-                test"},
-            "Test tesˇ",
-        );
-        cx.assert(
-            indoc! {"
+                test"})
+            .await;
+        cx.assert(indoc! {"
                 Test test
                 ˇ
-                test"},
-            indoc! {"
-                Test test
-                ˇ"},
-        );
+                test"})
+            .await;
 
         let mut cx = cx.binding(["c", "shift-e"]);
-        cx.assert("Test teˇst-test test", "Test teˇ test");
+        cx.assert("Test teˇst-test test").await;
     }
 
     #[gpui::test]
     async fn test_change_b(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
-        cx.assert("Teˇst Test", "ˇst Test");
-        cx.assert("Test ˇtest", "ˇtest");
-        cx.assert("Test1 test2 ˇtest3", "Test1 ˇtest3");
-        cx.assert(
-            indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
+        cx.assert("Teˇst Test").await;
+        cx.assert("Test ˇtest").await;
+        cx.assert("Test1 test2 ˇtest3").await;
+        cx.assert(indoc! {"
                 Test test
-                ˇtest"},
-            indoc! {"
-                Test ˇ
-                test"},
-        );
+                ˇtest"})
+            .await;
         println!("Marker");
-        cx.assert(
-            indoc! {"
+        cx.assert(indoc! {"
                 Test test
                 ˇ
-                test"},
-            indoc! {"
-                Test ˇ
-                
-                test"},
-        );
+                test"})
+            .await;
 
         let mut cx = cx.binding(["c", "shift-b"]);
-        cx.assert("Test test-test ˇtest", "Test ˇtest");
+        cx.assert("Test test-test ˇtest").await;
     }
 
     #[gpui::test]
     async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox"},
-            indoc! {"
-                The qˇ
-                brown fox"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            ˇ
+            brown fox"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_change_0(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox"},
-            indoc! {"
-                ˇuick
-                brown fox"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            ˇ
+            brown fox"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_change_k(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The quick
-                brown ˇfox
-                jumps over"},
-            indoc! {"
-                ˇ
-                jumps over"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps ˇover"},
-            indoc! {"
-                The quick
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over"},
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over"},
-        );
-        cx.assert(
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over"},
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
+        cx.assert(indoc! {"
+            The quick
+            brown ˇfox
+            jumps over"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            jumps ˇover"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The qˇuick
+            brown fox
+            jumps over"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert_exempted(
+            indoc! {"
+            ˇ
+            brown fox
+            jumps over"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_change_j(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The quick
-                brown ˇfox
-                jumps over"},
-            indoc! {"
-                The quick
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps ˇover"},
-            indoc! {"
-                The quick
-                brown fox
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over"},
-            indoc! {"
-                ˇ
-                jumps over"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                ˇ"},
-            indoc! {"
-                The quick
-                brown fox
-                ˇ"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
+        cx.assert(indoc! {"
+            The quick
+            brown ˇfox
+            jumps over"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            jumps ˇover"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox
+            jumps over"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            ˇ"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                The quick
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                The quick
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                the lˇazy"},
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                ˇ"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                ˇ"},
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                ˇ"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["c", "shift-g"]);
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the lˇazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            ˇ"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_change_gg(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                ˇ
-                jumps over
-                the lazy"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                the lˇazy"},
-            "ˇ",
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over
-                the lazy"},
-        );
-        cx.assert(
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over
-                the lazy"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["c", "g", "g"]);
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the lˇazy"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The qˇuick
+            brown fox
+            jumps over
+            the lazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert_exempted(
+            indoc! {"
+            ˇ
+            brown fox
+            jumps over
+            the lazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]
@@ -493,14 +392,15 @@ mod test {
     async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
-        // Changing back any number of times from the start of the file doesn't
-        // switch to insert mode in vim. This is weird and painful to implement
-        cx.add_initial_state_exemption(indoc! {"
+        cx.add_initial_state_exemptions(
+            indoc! {"
             ˇThe quick brown
-            
+
             fox jumps-over
             the lazy dog
-            "});
+            "},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        );
 
         for count in 1..=5 {
             cx.assert_binding_matches_all(

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

@@ -96,354 +96,256 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
 mod test {
     use indoc::indoc;
 
-    use crate::{state::Mode, test::VimTestContext};
+    use crate::{
+        state::Mode,
+        test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
+    };
 
     #[gpui::test]
     async fn test_delete_h(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "h"]);
-        cx.assert("Teˇst", "Tˇst");
-        cx.assert("Tˇest", "ˇest");
-        cx.assert("ˇTest", "ˇTest");
-        cx.assert(
-            indoc! {"
-                Test
-                ˇtest"},
-            indoc! {"
-                Test
-                ˇtest"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "h"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tˇest").await;
+        cx.assert("ˇTest").await;
+        cx.assert(indoc! {"
+            Test
+            ˇtest"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_l(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "l"]);
-        cx.assert("ˇTest", "ˇest");
-        cx.assert("Teˇst", "Teˇt");
-        cx.assert("Tesˇt", "Teˇs");
-        cx.assert(
-            indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "l"]);
+        cx.assert("ˇTest").await;
+        cx.assert("Teˇst").await;
+        cx.assert("Tesˇt").await;
+        cx.assert(indoc! {"
                 Tesˇt
-                test"},
-            indoc! {"
-                Teˇs
-                test"},
-        );
+                test"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_w(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "w"]);
-        cx.assert("Teˇst", "Tˇe");
-        cx.assert("Tˇest test", "Tˇtest");
-        cx.assert(
-            indoc! {"
-                Test teˇst
-                test"},
-            indoc! {"
-                Test tˇe
-                test"},
-        );
-        cx.assert(
-            indoc! {"
-                Test tesˇt
-                test"},
-            indoc! {"
-                Test teˇs
-                test"},
-        );
-        cx.assert(
-            indoc! {"
-                Test test
-                ˇ
-                test"},
-            indoc! {"
-                Test test
-                ˇ
-                test"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "w"]);
+        cx.assert("Teˇst").await;
+        cx.assert("Tˇest test").await;
+        cx.assert(indoc! {"
+            Test teˇst
+            test"})
+            .await;
+        cx.assert(indoc! {"
+            Test tesˇt
+            test"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            Test test
+            ˇ
+            test"},
+            ExemptionFeatures::DeletionOnEmptyLine,
+        )
+        .await;
 
         let mut cx = cx.binding(["d", "shift-w"]);
-        cx.assert("Test teˇst-test test", "Test teˇtest");
+        cx.assert("Test teˇst-test test").await;
     }
 
     #[gpui::test]
     async fn test_delete_e(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "e"]);
-        cx.assert("Teˇst Test", "Teˇ Test");
-        cx.assert("Tˇest test", "Tˇ test");
-        cx.assert(
-            indoc! {"
-                Test teˇst
-                test"},
-            indoc! {"
-                Test tˇe
-                test"},
-        );
-        cx.assert(
-            indoc! {"
-                Test tesˇt
-                test"},
-            "Test teˇs",
-        );
-        cx.assert(
-            indoc! {"
-                Test test
-                ˇ
-                test"},
-            indoc! {"
-                Test test
-                ˇ"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
+        cx.assert("Teˇst Test").await;
+        cx.assert("Tˇest test").await;
+        cx.assert(indoc! {"
+            Test teˇst
+            test"})
+            .await;
+        cx.assert(indoc! {"
+            Test tesˇt
+            test"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            Test test
+            ˇ
+            test"},
+            ExemptionFeatures::DeletionOnEmptyLine,
+        )
+        .await;
 
         let mut cx = cx.binding(["d", "shift-e"]);
-        cx.assert("Test teˇst-test test", "Test teˇ test");
+        cx.assert("Test teˇst-test test").await;
     }
 
     #[gpui::test]
     async fn test_delete_b(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "b"]);
-        cx.assert("Teˇst Test", "ˇst Test");
-        cx.assert("Test ˇtest", "ˇtest");
-        cx.assert("Test1 test2 ˇtest3", "Test1 ˇtest3");
-        cx.assert(
-            indoc! {"
-                Test test
-                ˇtest"},
-            // Trailing whitespace after cursor
-            indoc! {"
-                Testˇ 
-                test"},
-        );
-        cx.assert(
-            indoc! {"
-                Test test
-                ˇ
-                test"},
-            // Trailing whitespace after cursor
-            indoc! {"
-                Testˇ 
-                
-                test"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "b"]);
+        cx.assert("Teˇst Test").await;
+        cx.assert("Test ˇtest").await;
+        cx.assert("Test1 test2 ˇtest3").await;
+        cx.assert(indoc! {"
+            Test test
+            ˇtest"})
+            .await;
+        cx.assert(indoc! {"
+            Test test
+            ˇ
+            test"})
+            .await;
 
         let mut cx = cx.binding(["d", "shift-b"]);
-        cx.assert("Test test-test ˇtest", "Test ˇtest");
+        cx.assert("Test test-test ˇtest").await;
     }
 
     #[gpui::test]
     async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "$"]);
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox"},
-            indoc! {"
-                The ˇq
-                brown fox"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "$"]);
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            ˇ
+            brown fox"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_0(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "0"]);
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox"},
-            indoc! {"
-                ˇuick
-                brown fox"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-            indoc! {"
-                The quick
-                ˇ
-                brown fox"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "0"]);
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            ˇ
+            brown fox"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_k(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "k"]);
-        cx.assert(
-            indoc! {"
-                The quick
-                brown ˇfox
-                jumps over"},
-            "jumps ˇover",
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps ˇover"},
-            "The quˇick",
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over"},
-            indoc! {"
-                brownˇ fox
-                jumps over"},
-        );
-        cx.assert(
-            indoc! {"
-                ˇbrown fox
-                jumps over"},
-            "ˇjumps over",
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "k"]);
+        cx.assert(indoc! {"
+            The quick
+            brown ˇfox
+            jumps over"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            jumps ˇover"})
+            .await;
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox
+            jumps over"})
+            .await;
+        cx.assert(indoc! {"
+            ˇbrown fox
+            jumps over"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_j(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "j"]);
-        cx.assert(
-            indoc! {"
-                The quick
-                brown ˇfox
-                jumps over"},
-            "The quˇick",
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps ˇover"},
-            indoc! {"
-                The quick
-                brown ˇfox"},
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over"},
-            "jumpsˇ over",
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                ˇ"},
-            indoc! {"
-                The quick
-                ˇbrown fox"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "j"]);
+        cx.assert(indoc! {"
+            The quick
+            brown ˇfox
+            jumps over"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            jumps ˇover"})
+            .await;
+        cx.assert(indoc! {"
+            The qˇuick
+            brown fox
+            jumps over"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            ˇ"})
+            .await;
     }
 
     #[gpui::test]
     async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "shift-g"]);
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            "The qˇuick",
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            "The qˇuick",
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                the lˇazy"},
-            indoc! {"
-                The quick
-                brown fox
-                jumpsˇ over"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                ˇ"},
-            indoc! {"
-                The quick
-                brown fox
-                ˇjumps over"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["d", "shift-g"]);
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the lˇazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert_exempted(
+            indoc! {"
+            The quick
+            brown fox
+            jumps over
+            ˇ"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
-        let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "g", "g"]);
-        cx.assert(
-            indoc! {"
-                The quick
-                brownˇ fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                jumpsˇ over
-                the lazy"},
-        );
-        cx.assert(
-            indoc! {"
-                The quick
-                brown fox
-                jumps over
-                the lˇazy"},
-            "ˇ",
-        );
-        cx.assert(
-            indoc! {"
-                The qˇuick
-                brown fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                brownˇ fox
-                jumps over
-                the lazy"},
-        );
-        cx.assert(
-            indoc! {"
-                ˇ
-                brown fox
-                jumps over
-                the lazy"},
-            indoc! {"
-                ˇbrown fox
-                jumps over
-                the lazy"},
-        );
+        let mut cx = NeovimBackedTestContext::new(cx)
+            .await
+            .binding(["d", "g", "g"]);
+        cx.assert(indoc! {"
+            The quick
+            brownˇ fox
+            jumps over
+            the lazy"})
+            .await;
+        cx.assert(indoc! {"
+            The quick
+            brown fox
+            jumps over
+            the lˇazy"})
+            .await;
+        cx.assert_exempted(
+            indoc! {"
+            The qˇuick
+            brown fox
+            jumps over
+            the lazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
+        cx.assert_exempted(
+            indoc! {"
+            ˇ
+            brown fox
+            jumps over
+            the lazy"},
+            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+        )
+        .await;
     }
 
     #[gpui::test]

crates/vim/src/object.rs 🔗

@@ -431,7 +431,7 @@ fn surrounding_markers(
 mod test {
     use indoc::indoc;
 
-    use crate::test::NeovimBackedTestContext;
+    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
 
     const WORD_LOCATIONS: &'static str = indoc! {"
         The quick ˇbrowˇnˇ   
@@ -482,25 +482,46 @@ mod test {
 
         cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
             .await;
-        // Visual text objects are slightly broken when used with non empty selections
-        // cx.assert_binding_matches_all(["v", "h", "i", "w"], WORD_LOCATIONS)
-        //     .await;
-        // cx.assert_binding_matches_all(["v", "l", "i", "w"], WORD_LOCATIONS)
-        //     .await;
+        cx.assert_binding_matches_all_exempted(
+            ["v", "h", "i", "w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::NonEmptyVisualTextObjects,
+        )
+        .await;
+        cx.assert_binding_matches_all_exempted(
+            ["v", "l", "i", "w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::NonEmptyVisualTextObjects,
+        )
+        .await;
         cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
             .await;
 
-        // Visual text objects are slightly broken when used with non empty selections
-        // cx.assert_binding_matches_all(["v", "i", "h", "shift-w"], WORD_LOCATIONS)
-        //     .await;
-        // cx.assert_binding_matches_all(["v", "i", "l", "shift-w"], WORD_LOCATIONS)
-        //     .await;
-
-        // Visual around words is somewhat broken right now when it comes to newlines
-        // cx.assert_binding_matches_all(["v", "a", "w"], WORD_LOCATIONS)
-        //     .await;
-        // cx.assert_binding_matches_all(["v", "a", "shift-w"], WORD_LOCATIONS)
-        //     .await;
+        cx.assert_binding_matches_all_exempted(
+            ["v", "i", "h", "shift-w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::NonEmptyVisualTextObjects,
+        )
+        .await;
+        cx.assert_binding_matches_all_exempted(
+            ["v", "i", "l", "shift-w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::NonEmptyVisualTextObjects,
+        )
+        .await;
+
+        cx.assert_binding_matches_all_exempted(
+            ["v", "a", "w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
+        )
+        .await;
+        cx.assert_binding_matches_all_exempted(
+            ["v", "a", "shift-w"],
+            WORD_LOCATIONS,
+            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
+        )
+        .await;
     }
 
     const SENTENCE_EXAMPLES: &[&'static str] = &[
@@ -511,17 +532,15 @@ mod test {
             the lazy doˇgˇ.ˇ ˇThe quick ˇ
             brown fox jumps over
         "},
-        // Position of the cursor after deletion between lines isn't quite right.
-        // Deletion in a sentence at the start of a line with whitespace is incorrect.
-        // indoc! {"
-        //     The quick brown fox jumps.
-        //     Over the lazy dog
-        //     ˇ
-        //     ˇ
-        //     ˇ  fox-jumpˇs over
-        //     the lazy dog.ˇ
-        //     ˇ
-        // "},
+        indoc! {"
+            The quick brown fox jumps.
+            Over the lazy dog
+            ˇ
+            ˇ
+            ˇ  fox-jumpˇs over
+            the lazy dog.ˇ
+            ˇ
+        "},
         r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
     ];
 
@@ -530,15 +549,28 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx)
             .await
             .binding(["c", "i", "s"]);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
+            ExemptionFeatures::SentenceOnEmptyLines);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
+            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
+            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
         for sentence_example in SENTENCE_EXAMPLES {
             cx.assert_all(sentence_example).await;
         }
 
         let mut cx = cx.binding(["c", "a", "s"]);
-        // Resulting position is slightly incorrect for unintuitive reasons.
-        cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
-        // Changing around the sentence at the end of the line doesn't remove whitespace.'
-        cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
+        cx.add_initial_state_exemptions(
+            "The quick brown?ˇ Fox Jumps! Over the lazy.",
+            ExemptionFeatures::IncorrectLandingPosition,
+        );
+        cx.add_initial_state_exemptions(
+            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
+            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
+        );
 
         for sentence_example in SENTENCE_EXAMPLES {
             cx.assert_all(sentence_example).await;
@@ -550,15 +582,29 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx)
             .await
             .binding(["d", "i", "s"]);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\nˇ\nˇ\n  fox-jumps over\nthe lazy dog.\n\n",
+            ExemptionFeatures::SentenceOnEmptyLines);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\n\n\nˇ  foxˇ-ˇjumpˇs over\nthe lazy dog.\n\n",
+            ExemptionFeatures::SentenceAtStartOfLineWithWhitespace);
+        cx.add_initial_state_exemptions(
+            "The quick brown fox jumps.\nOver the lazy dog\n\n\n  fox-jumps over\nthe lazy dog.ˇ\nˇ\n",
+            ExemptionFeatures::SentenceAfterPunctuationAtEndOfFile);
+
         for sentence_example in SENTENCE_EXAMPLES {
             cx.assert_all(sentence_example).await;
         }
 
         let mut cx = cx.binding(["d", "a", "s"]);
-        // Resulting position is slightly incorrect for unintuitive reasons.
-        cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
-        // Changing around the sentence at the end of the line doesn't remove whitespace.'
-        cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
+        cx.add_initial_state_exemptions(
+            "The quick brown?ˇ Fox Jumps! Over the lazy.",
+            ExemptionFeatures::IncorrectLandingPosition,
+        );
+        cx.add_initial_state_exemptions(
+            "The quick brown.)]\'\" Brown fox jumps.ˇ ",
+            ExemptionFeatures::AroundObjectLeavesWhitespaceAtEndOfLine,
+        );
 
         for sentence_example in SENTENCE_EXAMPLES {
             cx.assert_all(sentence_example).await;
@@ -571,14 +617,18 @@ mod test {
             .await
             .binding(["v", "i", "s"]);
         for sentence_example in SENTENCE_EXAMPLES {
-            cx.assert_all(sentence_example).await;
+            cx.assert_all_exempted(sentence_example, ExemptionFeatures::SentenceOnEmptyLines)
+                .await;
         }
 
-        // Visual around sentences is somewhat broken right now when it comes to newlines
-        // let mut cx = cx.binding(["d", "a", "s"]);
-        // for sentence_example in SENTENCE_EXAMPLES {
-        //     cx.assert_all(sentence_example).await;
-        // }
+        let mut cx = cx.binding(["v", "a", "s"]);
+        for sentence_example in SENTENCE_EXAMPLES {
+            cx.assert_all_exempted(
+                sentence_example,
+                ExemptionFeatures::AroundSentenceStartingBetweenIncludesWrongWhitespace,
+            )
+            .await;
+        }
     }
 
     // Test string with "`" for opening surrounders and "'" for closing surrounders
@@ -588,14 +638,13 @@ mod test {
         the ˇlazy dˇ'ˇoˇ`ˇg"};
 
     const SURROUNDING_OBJECTS: &[(char, char)] = &[
-        // ('\'', '\''), // Quote,
-        // ('`', '`'),   // Back Quote
-        // ('"', '"'),   // Double Quote
-        // ('"', '"'),   // Double Quote
-        ('(', ')'), // Parentheses
-        ('[', ']'), // SquareBrackets
-        ('{', '}'), // CurlyBrackets
-        ('<', '>'), // AngleBrackets
+        ('\'', '\''), // Quote
+        ('`', '`'),   // Back Quote
+        ('"', '"'),   // Double Quote
+        ('(', ')'),   // Parentheses
+        ('[', ']'),   // SquareBrackets
+        ('{', '}'),   // CurlyBrackets
+        ('<', '>'),   // AngleBrackets
     ];
 
     #[gpui::test]
@@ -603,16 +652,23 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         for (start, end) in SURROUNDING_OBJECTS {
+            if ((start == &'\'' || start == &'`' || start == &'"')
+                && !ExemptionFeatures::QuotesSeekForward.supported())
+                || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
+            {
+                continue;
+            }
+
             let marked_string = SURROUNDING_MARKER_STRING
                 .replace('`', &start.to_string())
                 .replace('\'', &end.to_string());
 
-            // cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
-            //     .await;
+            cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
+                .await;
             cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
                 .await;
-            // cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
-            //     .await;
+            cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
+                .await;
             cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
                 .await;
         }
@@ -623,16 +679,22 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         for (start, end) in SURROUNDING_OBJECTS {
+            if ((start == &'\'' || start == &'`' || start == &'"')
+                && !ExemptionFeatures::QuotesSeekForward.supported())
+                || (start == &'<' && !ExemptionFeatures::AngleBracketsFreezeNeovim.supported())
+            {
+                continue;
+            }
             let marked_string = SURROUNDING_MARKER_STRING
                 .replace('`', &start.to_string())
                 .replace('\'', &end.to_string());
 
-            // cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
-            //     .await;
+            cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
+                .await;
             cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
                 .await;
-            // cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
-            //     .await;
+            cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
+                .await;
             cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
                 .await;
         }

crates/vim/src/test/neovim_backed_binding_test_context.rs 🔗

@@ -4,7 +4,7 @@ use gpui::ContextHandle;
 
 use crate::state::Mode;
 
-use super::NeovimBackedTestContext;
+use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
 
 pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
     cx: NeovimBackedTestContext<'a>,
@@ -42,6 +42,20 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
             .await
     }
 
+    pub async fn assert_exempted(
+        &mut self,
+        marked_positions: &str,
+        feature: ExemptionFeatures,
+    ) -> Option<(ContextHandle, ContextHandle)> {
+        if SUPPORTED_FEATURES.contains(&feature) {
+            self.cx
+                .assert_binding_matches(self.keystrokes_under_test, marked_positions)
+                .await
+        } else {
+            None
+        }
+    }
+
     pub fn assert_manual(
         &mut self,
         initial_state: &str,
@@ -63,6 +77,18 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
             .assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
             .await
     }
+
+    pub async fn assert_all_exempted(
+        &mut self,
+        marked_positions: &str,
+        feature: ExemptionFeatures,
+    ) {
+        if SUPPORTED_FEATURES.contains(&feature) {
+            self.cx
+                .assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
+                .await
+        }
+    }
 }
 
 impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -8,6 +8,45 @@ use util::test::marked_text_offsets;
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
 
+pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
+
+/// Enum representing features we have tests for but which don't work, yet. Used
+/// to add exemptions and automatically
+#[derive(PartialEq, Eq)]
+pub enum ExemptionFeatures {
+    // MOTIONS
+    // Deletions on empty lines miss some newlines
+    DeletionOnEmptyLine,
+    // When a motion fails, it should should not apply linewise operations
+    OperatorAbortsOnFailedMotion,
+
+    // OBJECTS
+    // Resulting position after the operation is slightly incorrect for unintuitive reasons.
+    IncorrectLandingPosition,
+    // Operator around the text object at the end of the line doesn't remove whitespace.
+    AroundObjectLeavesWhitespaceAtEndOfLine,
+    // Sentence object on empty lines
+    SentenceOnEmptyLines,
+    // Whitespace isn't included with text objects at the start of the line
+    SentenceAtStartOfLineWithWhitespace,
+    // Whitespace around sentences is slightly incorrect when starting between sentences
+    AroundSentenceStartingBetweenIncludesWrongWhitespace,
+    // Non empty selection with text objects in visual mode
+    NonEmptyVisualTextObjects,
+    // Quote style surrounding text objects don't seek forward properly
+    QuotesSeekForward,
+    // Neovim freezes up for some reason with angle brackets
+    AngleBracketsFreezeNeovim,
+    // Sentence Doesn't backtrack when its at the end of the file
+    SentenceAfterPunctuationAtEndOfFile,
+}
+
+impl ExemptionFeatures {
+    pub fn supported(&self) -> bool {
+        SUPPORTED_FEATURES.contains(self)
+    }
+}
+
 pub struct NeovimBackedTestContext<'a> {
     cx: VimTestContext<'a>,
     // Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
@@ -27,10 +66,22 @@ impl<'a> NeovimBackedTestContext<'a> {
         }
     }
 
-    pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
-        let initial_state = initial_state.to_string();
-        // None represents all keybindings being exempted for that initial state
-        self.exemptions.insert(initial_state, None);
+    pub fn add_initial_state_exemptions(
+        &mut self,
+        marked_positions: &str,
+        missing_feature: ExemptionFeatures, // Feature required to support this exempted test case
+    ) {
+        if !missing_feature.supported() {
+            let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
+
+            for cursor_offset in cursor_offsets.iter() {
+                let mut marked_text = unmarked_text.clone();
+                marked_text.insert(*cursor_offset, 'ˇ');
+
+                // None represents all keybindings being exempted for that initial state
+                self.exemptions.insert(marked_text, None);
+            }
+        }
     }
 
     pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
@@ -120,6 +171,18 @@ impl<'a> NeovimBackedTestContext<'a> {
         }
     }
 
+    pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
+        &mut self,
+        keystrokes: [&str; COUNT],
+        marked_positions: &str,
+        feature: ExemptionFeatures,
+    ) {
+        if SUPPORTED_FEATURES.contains(&feature) {
+            self.assert_binding_matches_all(keystrokes, marked_positions)
+                .await
+        }
+    }
+
     pub fn binding<const COUNT: usize>(
         self,
         keystrokes: [&'static str; COUNT],

crates/vim/src/test/vim_binding_test_context.rs 🔗

@@ -38,11 +38,6 @@ impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
         }
     }
 
-    pub fn mode_after(mut self, mode_after: Mode) -> Self {
-        self.mode_after = mode_after;
-        self
-    }
-
     pub fn assert(&mut self, initial_state: &str, state_after: &str) {
         self.cx.assert_binding(
             self.keystrokes_under_test,

crates/vim/test_data/test_change_0.json 🔗

@@ -0,0 +1 @@
+[{"Text":"uick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_b.json 🔗

@@ -0,0 +1 @@
+[{"Text":"st Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test1 test3"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"Test \ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test \n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"Test test"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_backspace.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_e.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Te Test"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_end_of_line.json 🔗

@@ -0,0 +1 @@
+[{"Text":"The q\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_gg.json 🔗

@@ -0,0 +1 @@
+[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_h.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Tst"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"est"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"Test\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_j.json 🔗

@@ -0,0 +1 @@
+[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_k.json 🔗

@@ -0,0 +1 @@
+[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_l.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Tet"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"Tes"},{"Mode":"Insert"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Insert"}]

crates/vim/test_data/test_change_w.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Te"},{"Mode":"Insert"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Insert"},{"Text":"T test"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":"Testtest"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"Test te\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"},{"Text":"Test tes\ntest"},{"Mode":"Insert"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Insert"},{"Text":"Test test\n\ntest"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"Test te test"},{"Mode":"Insert"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Insert"}]

crates/vim/test_data/test_dd.json 🔗

@@ -1 +1 @@
-[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
+[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_0.json 🔗

@@ -0,0 +1 @@
+[{"Text":"uick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_b.json 🔗

@@ -0,0 +1 @@
+[{"Text":"st Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test1 test3"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test \ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test \n\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test test"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_e.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Te Test"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"T test"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test te test"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_end_of_line.json 🔗

@@ -0,0 +1 @@
+[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_gg.json 🔗

@@ -0,0 +1 @@
+[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_h.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_j.json 🔗

@@ -0,0 +1 @@
+[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_k.json 🔗

@@ -0,0 +1 @@
+[{"Text":"jumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_l.json 🔗

@@ -0,0 +1 @@
+[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]

crates/vim/test_data/test_delete_w.json 🔗

@@ -0,0 +1 @@
+[{"Text":"Te"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Ttest"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Test te\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"Test tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"Test tetest"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"}]