diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d9e13e36b4eb5b5d04c8e51adadc516054dc0775..ac83f906627912e0938e892ca0a8afcac395b856 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -497,7 +497,8 @@ "shift-u": "editor::Redo", "ctrl-c": "editor::ToggleComments", "d": "vim::HelixDelete", - "c": "vim::Substitute", + "c": "vim::HelixSubstitute", + "alt-c": "vim::HelixSubstituteNoYank", "shift-c": "vim::HelixDuplicateBelow", "alt-shift-c": "vim::HelixDuplicateAbove", ",": "vim::HelixKeepNewestSelection" diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 764a00d3487dd6f6c83112e586ed1f1e8c97307a..ed7abaa11c1a6b95e436b019de1605792c82fc9d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -18,7 +18,7 @@ use text::{Bias, SelectionGoal}; use workspace::searchable; use workspace::searchable::FilteredSearchRange; -use crate::motion; +use crate::motion::{self, MotionKind}; use crate::state::SearchState; use crate::{ Vim, @@ -48,6 +48,10 @@ actions!( HelixDuplicateBelow, /// Copies all selections above. HelixDuplicateAbove, + /// Delete the selection and enter edit mode. + HelixSubstitute, + /// Delete the selection and enter edit mode, without yanking the selection. + HelixSubstituteNoYank, ] ); @@ -68,6 +72,8 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let times = Vim::take_count(cx); vim.helix_duplicate_selections_above(times, window, cx); }); + Vim::action(editor, cx, Vim::helix_substitute); + Vim::action(editor, cx, Vim::helix_substitute_no_yank); } impl Vim { @@ -604,6 +610,54 @@ impl Vim { editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest])); }); } + + fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context) { + self.update_editor(cx, |vim, editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.transact(window, cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.move_with(|map, selection| { + if selection.start == selection.end { + selection.end = movement::right(map, selection.end); + } + + // If the selection starts and ends on a newline, we exclude the last one. + if !selection.is_empty() + && selection.start.column() == 0 + && selection.end.column() == 0 + { + selection.end = movement::left(map, selection.end); + } + }) + }); + if yank { + vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); + } + let selections = editor.selections.all::(cx).into_iter(); + let edits = selections.map(|selection| (selection.start..selection.end, "")); + editor.edit(edits, cx); + }); + }); + self.switch_mode(Mode::Insert, true, window, cx); + } + + fn helix_substitute( + &mut self, + _: &HelixSubstitute, + window: &mut Window, + cx: &mut Context, + ) { + self.do_helix_substitute(true, window, cx); + } + + fn helix_substitute_no_yank( + &mut self, + _: &HelixSubstituteNoYank, + window: &mut Window, + cx: &mut Context, + ) { + self.do_helix_substitute(false, window, cx); + } } #[cfg(test)] @@ -1241,4 +1295,67 @@ mod test { cx.simulate_keystrokes("s o n e enter"); cx.assert_state("ˇone two one", Mode::HelixNormal); } + + #[gpui::test] + async fn test_helix_substitute(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("ˇone two", Mode::HelixNormal); + cx.simulate_keystrokes("c"); + cx.assert_state("ˇne two", Mode::Insert); + + cx.set_state("«oneˇ» two", Mode::HelixNormal); + cx.simulate_keystrokes("c"); + cx.assert_state("ˇ two", Mode::Insert); + + cx.set_state( + indoc! {" + oneˇ two + three + "}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("x c"); + cx.assert_state( + indoc! {" + ˇ + three + "}, + Mode::Insert, + ); + + cx.set_state( + indoc! {" + one twoˇ + three + "}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("c"); + cx.assert_state( + indoc! {" + one twoˇthree + "}, + Mode::Insert, + ); + + // Helix doesn't set the cursor to the first non-blank one when + // replacing lines: it uses language-dependent indent queries instead. + cx.set_state( + indoc! {" + one two + « indented + three not indentedˇ» + "}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("c"); + cx.set_state( + indoc! {" + one two + ˇ + "}, + Mode::Insert, + ); + } }