Fix vim `cw` at end of words (#35300)

Julia Ryan created

Fixes #35269

Release Notes:

- N/A

Change summary

crates/vim/src/motion.rs                |  9 +++++++--
crates/vim/src/normal/change.rs         | 16 ++++++++++++++--
crates/vim/test_data/test_change_w.json |  4 ++++
3 files changed, 25 insertions(+), 4 deletions(-)

Detailed changes

crates/vim/src/motion.rs 🔗

@@ -987,7 +987,7 @@ impl Motion {
                 SelectionGoal::None,
             ),
             NextWordEnd { ignore_punctuation } => (
-                next_word_end(map, point, *ignore_punctuation, times, true),
+                next_word_end(map, point, *ignore_punctuation, times, true, true),
                 SelectionGoal::None,
             ),
             PreviousWordStart { ignore_punctuation } => (
@@ -1723,14 +1723,19 @@ pub(crate) fn next_word_end(
     ignore_punctuation: bool,
     times: usize,
     allow_cross_newline: bool,
+    always_advance: bool,
 ) -> DisplayPoint {
     let classifier = map
         .buffer_snapshot
         .char_classifier_at(point.to_point(map))
         .ignore_punctuation(ignore_punctuation);
     for _ in 0..times {
-        let new_point = next_char(map, point, allow_cross_newline);
         let mut need_next_char = false;
+        let new_point = if always_advance {
+            next_char(map, point, allow_cross_newline)
+        } else {
+            point
+        };
         let new_point = movement::find_boundary_exclusive(
             map,
             new_point,

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

@@ -51,6 +51,7 @@ impl Vim {
                                     ignore_punctuation,
                                     &text_layout_details,
                                     motion == Motion::NextSubwordStart { ignore_punctuation },
+                                    !matches!(motion, Motion::NextWordStart { .. }),
                                 )
                             }
                             _ => {
@@ -148,6 +149,7 @@ fn expand_changed_word_selection(
     ignore_punctuation: bool,
     text_layout_details: &TextLayoutDetails,
     use_subword: bool,
+    always_advance: bool,
 ) -> Option<MotionKind> {
     let is_in_word = || {
         let classifier = map
@@ -173,8 +175,14 @@ fn expand_changed_word_selection(
                     selection.end =
                         motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
                 } else {
-                    selection.end =
-                        motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
+                    selection.end = motion::next_word_end(
+                        map,
+                        selection.end,
+                        ignore_punctuation,
+                        1,
+                        false,
+                        always_advance,
+                    );
                 }
                 selection.end = motion::next_char(map, selection.end, false);
             }
@@ -271,6 +279,10 @@ mod test {
         cx.simulate("c shift-w", "Test teˇst-test test")
             .await
             .assert_matches();
+
+        // on last character of word, `cw` doesn't eat subsequent punctuation
+        // see https://github.com/zed-industries/zed/issues/35269
+        cx.simulate("c w", "tesˇt-test").await.assert_matches();
     }
 
     #[gpui::test]

crates/vim/test_data/test_change_w.json 🔗

@@ -30,3 +30,7 @@
 {"Key":"c"}
 {"Key":"shift-w"}
 {"Get":{"state":"Test teˇ test","mode":"Insert"}}
+{"Put":{"state":"tesˇt-test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"tesˇ-test","mode":"Insert"}}