Add a helix-specific substitute method (#38735)

`vim::Substitute` is a little different from the helix behavior, so this
PR adds helix versions. The most important difference (for my usage, at
least) is that if you're selecting whole lines then helix drops the `\n`
from the selection (much like vim's lines mode, except that helix bases
this behavior on the selection instead of having a different mode).

Release Notes:

- N/A
This commit is contained in:
jneem 2025-10-16 12:23:09 -05:00 committed by GitHub
parent c5a67d85ab
commit 58ff46962d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 120 additions and 2 deletions

View file

@ -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"

View file

@ -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,
);
}
}