Skip to content

Commit 58ff469

Browse files
authored
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
1 parent c5a67d8 commit 58ff469

File tree

2 files changed

+120
-2
lines changed

2 files changed

+120
-2
lines changed

assets/keymaps/vim.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,8 @@
497497
"shift-u": "editor::Redo",
498498
"ctrl-c": "editor::ToggleComments",
499499
"d": "vim::HelixDelete",
500-
"c": "vim::Substitute",
500+
"c": "vim::HelixSubstitute",
501+
"alt-c": "vim::HelixSubstituteNoYank",
501502
"shift-c": "vim::HelixDuplicateBelow",
502503
"alt-shift-c": "vim::HelixDuplicateAbove",
503504
",": "vim::HelixKeepNewestSelection"

crates/vim/src/helix.rs

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use text::{Bias, SelectionGoal};
1818
use workspace::searchable;
1919
use workspace::searchable::FilteredSearchRange;
2020

21-
use crate::motion;
21+
use crate::motion::{self, MotionKind};
2222
use crate::state::SearchState;
2323
use crate::{
2424
Vim,
@@ -48,6 +48,10 @@ actions!(
4848
HelixDuplicateBelow,
4949
/// Copies all selections above.
5050
HelixDuplicateAbove,
51+
/// Delete the selection and enter edit mode.
52+
HelixSubstitute,
53+
/// Delete the selection and enter edit mode, without yanking the selection.
54+
HelixSubstituteNoYank,
5155
]
5256
);
5357

@@ -68,6 +72,8 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
6872
let times = Vim::take_count(cx);
6973
vim.helix_duplicate_selections_above(times, window, cx);
7074
});
75+
Vim::action(editor, cx, Vim::helix_substitute);
76+
Vim::action(editor, cx, Vim::helix_substitute_no_yank);
7177
}
7278

7379
impl Vim {
@@ -604,6 +610,54 @@ impl Vim {
604610
editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
605611
});
606612
}
613+
614+
fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
615+
self.update_editor(cx, |vim, editor, cx| {
616+
editor.set_clip_at_line_ends(false, cx);
617+
editor.transact(window, cx, |editor, window, cx| {
618+
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
619+
s.move_with(|map, selection| {
620+
if selection.start == selection.end {
621+
selection.end = movement::right(map, selection.end);
622+
}
623+
624+
// If the selection starts and ends on a newline, we exclude the last one.
625+
if !selection.is_empty()
626+
&& selection.start.column() == 0
627+
&& selection.end.column() == 0
628+
{
629+
selection.end = movement::left(map, selection.end);
630+
}
631+
})
632+
});
633+
if yank {
634+
vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
635+
}
636+
let selections = editor.selections.all::<Point>(cx).into_iter();
637+
let edits = selections.map(|selection| (selection.start..selection.end, ""));
638+
editor.edit(edits, cx);
639+
});
640+
});
641+
self.switch_mode(Mode::Insert, true, window, cx);
642+
}
643+
644+
fn helix_substitute(
645+
&mut self,
646+
_: &HelixSubstitute,
647+
window: &mut Window,
648+
cx: &mut Context<Self>,
649+
) {
650+
self.do_helix_substitute(true, window, cx);
651+
}
652+
653+
fn helix_substitute_no_yank(
654+
&mut self,
655+
_: &HelixSubstituteNoYank,
656+
window: &mut Window,
657+
cx: &mut Context<Self>,
658+
) {
659+
self.do_helix_substitute(false, window, cx);
660+
}
607661
}
608662

609663
#[cfg(test)]
@@ -1241,4 +1295,67 @@ mod test {
12411295
cx.simulate_keystrokes("s o n e enter");
12421296
cx.assert_state("ˇone two one", Mode::HelixNormal);
12431297
}
1298+
1299+
#[gpui::test]
1300+
async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1301+
let mut cx = VimTestContext::new(cx, true).await;
1302+
1303+
cx.set_state("ˇone two", Mode::HelixNormal);
1304+
cx.simulate_keystrokes("c");
1305+
cx.assert_state("ˇne two", Mode::Insert);
1306+
1307+
cx.set_state("«oneˇ» two", Mode::HelixNormal);
1308+
cx.simulate_keystrokes("c");
1309+
cx.assert_state("ˇ two", Mode::Insert);
1310+
1311+
cx.set_state(
1312+
indoc! {"
1313+
oneˇ two
1314+
three
1315+
"},
1316+
Mode::HelixNormal,
1317+
);
1318+
cx.simulate_keystrokes("x c");
1319+
cx.assert_state(
1320+
indoc! {"
1321+
ˇ
1322+
three
1323+
"},
1324+
Mode::Insert,
1325+
);
1326+
1327+
cx.set_state(
1328+
indoc! {"
1329+
one twoˇ
1330+
three
1331+
"},
1332+
Mode::HelixNormal,
1333+
);
1334+
cx.simulate_keystrokes("c");
1335+
cx.assert_state(
1336+
indoc! {"
1337+
one twoˇthree
1338+
"},
1339+
Mode::Insert,
1340+
);
1341+
1342+
// Helix doesn't set the cursor to the first non-blank one when
1343+
// replacing lines: it uses language-dependent indent queries instead.
1344+
cx.set_state(
1345+
indoc! {"
1346+
one two
1347+
« indented
1348+
three not indentedˇ»
1349+
"},
1350+
Mode::HelixNormal,
1351+
);
1352+
cx.simulate_keystrokes("c");
1353+
cx.set_state(
1354+
indoc! {"
1355+
one two
1356+
ˇ
1357+
"},
1358+
Mode::Insert,
1359+
);
1360+
}
12441361
}

0 commit comments

Comments
 (0)