Vim: substitute support (#2641)

Max Brunsfeld created

Release Notes:

- vim mode now supports `s` for substitute

Change summary

assets/keymaps/vim.json             |  6 +
crates/vim/src/normal.rs            |  9 ++++
crates/vim/src/normal/substitute.rs | 69 +++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+), 2 deletions(-)

Detailed changes

assets/keymaps/vim.json 🔗

@@ -137,7 +137,7 @@
     }
   },
   {
-    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
+    "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
     "bindings": {
       "c": [
         "vim::PushOperator",
@@ -220,7 +220,8 @@
       "r": [
         "vim::PushOperator",
         "Replace"
-      ]
+      ],
+      "s": "vim::Substitute"
     }
   },
   {
@@ -307,6 +308,7 @@
       "x": "vim::VisualDelete",
       "y": "vim::VisualYank",
       "p": "vim::VisualPaste",
+      "s": "vim::Substitute",
       "r": [
         "vim::PushOperator",
         "Replace"

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},
 };
 
@@ -45,6 +47,7 @@ actions!(
         DeleteToEndOfLine,
         Paste,
         Yank,
+        Substitute,
     ]
 );
 
@@ -56,6 +59,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(insert_end_of_line);
     cx.add_action(insert_line_above);
     cx.add_action(insert_line_below);
+    cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
+        Vim::update(cx, |vim, cx| {
+            let times = vim.pop_number_operator(cx);
+            substitute(vim, times, cx);
+        })
+    });
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
             let times = vim.pop_number_operator(cx);

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");
+    }
+}