vim: substitute handles multibyte characters

Conrad Irwin created

And is now in its own file

Change summary

crates/vim/src/normal.rs            | 21 --------
crates/vim/src/normal/substitute.rs | 69 +++++++++++++++++++++++++++++++
crates/vim/src/test.rs              | 24 ----------
3 files changed, 71 insertions(+), 43 deletions(-)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -1,5 +1,6 @@
 mod change;
 mod delete;
+mod substitute;
 mod yank;
 
 use std::{borrow::Cow, cmp::Ordering, sync::Arc};
@@ -25,6 +26,7 @@ use workspace::Workspace;
 use self::{
     change::{change_motion, change_object},
     delete::{delete_motion, delete_object},
+    substitute::substitute,
     yank::{yank_motion, yank_object},
 };
 
@@ -478,25 +480,6 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
     });
 }
 
-pub fn substitute(vim: &mut Vim, count: usize, cx: &mut WindowContext) {
-    vim.update_active_editor(cx, |editor, cx| {
-        editor.transact(cx, |editor, cx| {
-            let selections = editor.selections.all::<Point>(cx);
-            for selection in selections.into_iter().rev() {
-                let end = if selection.start == selection.end {
-                    selection.start + Point::new(0, count as u32)
-                } else {
-                    selection.end
-                };
-                editor.buffer().update(cx, |buffer, cx| {
-                    buffer.edit([(selection.start..end, "")], None, cx)
-                })
-            }
-        })
-    });
-    vim.switch_mode(Mode::Insert, true, cx)
-}
-
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

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

@@ -0,0 +1,69 @@
+use gpui::WindowContext;
+use language::Point;
+
+use crate::{motion::Motion, Mode, Vim};
+
+pub fn substitute(vim: &mut Vim, count: usize, cx: &mut WindowContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        editor.set_clip_at_line_ends(false, cx);
+        editor.change_selections(None, cx, |s| {
+            s.move_with(|map, selection| {
+                if selection.start == selection.end {
+                    Motion::Right.expand_selection(map, selection, count, true);
+                }
+            })
+        });
+        editor.transact(cx, |editor, cx| {
+            let selections = editor.selections.all::<Point>(cx);
+            for selection in selections.into_iter().rev() {
+                editor.buffer().update(cx, |buffer, cx| {
+                    buffer.edit([(selection.start..selection.end, "")], None, cx)
+                })
+            }
+        });
+        editor.set_clip_at_line_ends(true, cx);
+    });
+    vim.switch_mode(Mode::Insert, true, cx)
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{state::Mode, test::VimTestContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_substitute(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // supports a single cursor
+        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["s", "x"]);
+        cx.assert_editor_state("xˇbc\n");
+
+        // supports a selection
+        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
+        cx.assert_editor_state("a«bcˇ»\n");
+        cx.simulate_keystrokes(["s", "x"]);
+        cx.assert_editor_state("axˇ\n");
+
+        // supports counts
+        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["2", "s", "x"]);
+        cx.assert_editor_state("xˇc\n");
+
+        // supports multiple cursors
+        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["2", "s", "x"]);
+        cx.assert_editor_state("axˇdexˇg\n");
+
+        // does not read beyond end of line
+        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["5", "s", "x"]);
+        cx.assert_editor_state("xˇ\n");
+
+        // it handles multibyte characters
+        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
+        cx.simulate_keystrokes(["4", "s", "x"]);
+        cx.assert_editor_state("xˇ\n");
+    }
+}

crates/vim/src/test.rs 🔗

@@ -98,27 +98,3 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
         assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
     })
 }
-
-#[gpui::test]
-async fn test_substitute(cx: &mut gpui::TestAppContext) {
-    let mut cx = VimTestContext::new(cx, true).await;
-
-    // supports a single cursor
-    cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
-    cx.simulate_keystrokes(["s", "x"]);
-    cx.assert_editor_state("xˇbc\n");
-
-    // supports a selection
-    cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
-    cx.simulate_keystrokes(["s", "x"]);
-    cx.assert_editor_state("axˇ\n");
-
-    // supports multiple cursors
-    cx.set_state(indoc! {"a«bcˇ»deˇfg\n"}, Mode::Normal);
-    cx.simulate_keystrokes(["s", "x"]);
-    cx.assert_editor_state("axˇdexˇg\n");
-
-    cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
-    cx.simulate_keystrokes(["2", "s", "x"]);
-    cx.assert_editor_state("xˇc\n");
-}