Fix VIM cw on last character of a word doesn't work as expected: (#10963)

Hans created

At the moment, using the default expand_selection seems to do the job
well, without the need for some additional logic, which may also make
the code a little clearer, Fix #10945



Release Notes:


- N/A

Change summary

crates/vim/src/normal/change.rs         | 116 +++++++++++++-------------
crates/vim/test_data/test_change_w.json |   4 
2 files changed, 61 insertions(+), 59 deletions(-)

Detailed changes

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

@@ -31,48 +31,42 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
             editor.set_clip_at_line_ends(false, cx);
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 s.move_with(|map, selection| {
-                    motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
-                    {
-                        expand_changed_word_selection(
-                            map,
-                            selection,
-                            times,
-                            ignore_punctuation,
-                            &text_layout_details,
-                            false,
-                        )
-                    } else if let Motion::NextSubwordStart { ignore_punctuation } = motion {
-                        expand_changed_word_selection(
-                            map,
-                            selection,
-                            times,
-                            ignore_punctuation,
-                            &text_layout_details,
-                            true,
-                        )
-                    } else {
-                        let result = motion.expand_selection(
-                            map,
-                            selection,
-                            times,
-                            false,
-                            &text_layout_details,
-                        );
-                        if let Motion::CurrentLine = motion {
-                            let mut start_offset = selection.start.to_offset(map, Bias::Left);
-                            let scope = map
-                                .buffer_snapshot
-                                .language_scope_at(selection.start.to_point(&map));
-                            for (ch, offset) in map.buffer_chars_at(start_offset) {
-                                if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
-                                    break;
+                    motion_succeeded |= match motion {
+                        Motion::NextWordStart { ignore_punctuation }
+                        | Motion::NextSubwordStart { ignore_punctuation } => {
+                            expand_changed_word_selection(
+                                map,
+                                selection,
+                                times,
+                                ignore_punctuation,
+                                &text_layout_details,
+                                motion == Motion::NextSubwordStart { ignore_punctuation },
+                            )
+                        }
+                        _ => {
+                            let result = motion.expand_selection(
+                                map,
+                                selection,
+                                times,
+                                false,
+                                &text_layout_details,
+                            );
+                            if let Motion::CurrentLine = motion {
+                                let mut start_offset = selection.start.to_offset(map, Bias::Left);
+                                let scope = map
+                                    .buffer_snapshot
+                                    .language_scope_at(selection.start.to_point(&map));
+                                for (ch, offset) in map.buffer_chars_at(start_offset) {
+                                    if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
+                                        break;
+                                    }
+                                    start_offset = offset + ch.len_utf8();
                                 }
-                                start_offset = offset + ch.len_utf8();
+                                selection.start = start_offset.to_display_point(map);
                             }
-                            selection.start = start_offset.to_display_point(map);
+                            result
                         }
-                        result
-                    };
+                    }
                 });
             });
             copy_selections_content(vim, editor, motion.linewise(), cx);
@@ -116,8 +110,8 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
 // Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
 // on a non-blank.  This is because "cw" is interpreted as change-word, and a
 // word does not include the following white space.  {Vi: "cw" when on a blank
-//     followed by other blanks changes only the first blank; this is probably a
-//     bug, because "dw" deletes all the blanks}
+// followed by other blanks changes only the first blank; this is probably a
+// bug, because "dw" deletes all the blanks}
 fn expand_changed_word_selection(
     map: &DisplaySnapshot,
     selection: &mut Selection<DisplayPoint>,
@@ -126,7 +120,7 @@ fn expand_changed_word_selection(
     text_layout_details: &TextLayoutDetails,
     use_subword: bool,
 ) -> bool {
-    if times.is_none() || times.unwrap() == 1 {
+    let is_in_word = || {
         let scope = map
             .buffer_snapshot
             .language_scope_at(selection.start.to_point(map));
@@ -135,25 +129,28 @@ fn expand_changed_word_selection(
             .next()
             .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
             .unwrap_or_default();
-
-        if in_word {
-            if use_subword {
-                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);
+        return in_word;
+    };
+    if (times.is_none() || times.unwrap() == 1) && is_in_word() {
+        let next_char = map
+            .buffer_chars_at(
+                motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
+            )
+            .next();
+        match next_char {
+            Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
+            _ => {
+                if use_subword {
+                    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_char(map, selection.end, false);
             }
-            selection.end = motion::next_char(map, selection.end, false);
-            true
-        } else {
-            let motion = if use_subword {
-                Motion::NextSubwordStart { ignore_punctuation }
-            } else {
-                Motion::NextWordStart { ignore_punctuation }
-            };
-            motion.expand_selection(map, selection, None, false, &text_layout_details)
         }
+        true
     } else {
         let motion = if use_subword {
             Motion::NextSubwordStart { ignore_punctuation }
@@ -209,6 +206,7 @@ mod test {
         cx.assert("Teˇst").await;
         cx.assert("Tˇest test").await;
         cx.assert("Testˇ  test").await;
+        cx.assert("Tesˇt  test").await;
         cx.assert(indoc! {"
                 Test teˇst
                 test"})

crates/vim/test_data/test_change_w.json 🔗

@@ -10,6 +10,10 @@
 {"Key":"c"}
 {"Key":"w"}
 {"Get":{"state":"Testˇtest","mode":"Insert"}}
+{"Put":{"state":"Tesˇt  test"}}
+{"Key":"c"}
+{"Key":"w"}
+{"Get":{"state":"Tesˇ  test","mode":"Insert"}}
 {"Put":{"state":"Test teˇst\ntest"}}
 {"Key":"c"}
 {"Key":"w"}