@@ -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"
@@ -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<Vim>) {
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>) {
+ 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::<Point>(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>,
+ ) {
+ self.do_helix_substitute(true, window, cx);
+ }
+
+ fn helix_substitute_no_yank(
+ &mut self,
+ _: &HelixSubstituteNoYank,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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,
+ );
+ }
}