diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 05159e9446..43b778d9b8 100644 --- a/assets/keymaps/vim.json +++ b/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" diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2a526880a6..c54f739628 100644 --- a/crates/vim/src/normal.rs +++ b/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); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs new file mode 100644 index 0000000000..e208983b11 --- /dev/null +++ b/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::(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"); + } +}