Improve Helix insert (#34765)

Closes #34763 

Release Notes:

- Improved insert in `helix_mode` when a selection exists to better
match helix's behavior: collapse selection to avoid replacing it
- Improved append (`insert_after`) to better match helix's behavior:
move cursor to end of selection if it exists
This commit is contained in:
Pablo Ramón Guevara 2025-07-24 07:27:07 +02:00 committed by GitHub
parent 8b0ec287a5
commit a6956eebcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 112 additions and 4 deletions

View file

@ -220,6 +220,8 @@
{ {
"context": "vim_mode == normal", "context": "vim_mode == normal",
"bindings": { "bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"ctrl-[": "editor::Cancel", "ctrl-[": "editor::Cancel",
":": "command_palette::Toggle", ":": "command_palette::Toggle",
"c": "vim::PushChange", "c": "vim::PushChange",
@ -353,9 +355,7 @@
"shift-d": "vim::DeleteToEndOfLine", "shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines", "shift-j": "vim::JoinLines",
"shift-y": "vim::YankLine", "shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace", "shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine", "shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow", "o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove", "shift-o": "vim::InsertLineAbove",
@ -377,6 +377,8 @@
{ {
"context": "vim_mode == helix_normal && !menu", "context": "vim_mode == helix_normal && !menu",
"bindings": { "bindings": {
"i": "vim::HelixInsert",
"a": "vim::HelixAppend",
"ctrl-[": "editor::Cancel", "ctrl-[": "editor::Cancel",
";": "vim::HelixCollapseSelection", ";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle", ":": "command_palette::Toggle",

View file

@ -4,18 +4,28 @@ use gpui::{Context, Window};
use language::{CharClassifier, CharKind}; use language::{CharClassifier, CharKind};
use text::SelectionGoal; use text::SelectionGoal;
use crate::{Vim, motion::Motion, state::Mode}; use crate::{
Vim,
motion::{Motion, right},
state::Mode,
};
actions!( actions!(
vim, vim,
[ [
/// Switches to normal mode after the cursor (Helix-style). /// Switches to normal mode after the cursor (Helix-style).
HelixNormalAfter HelixNormalAfter,
/// Inserts at the beginning of the selection.
HelixInsert,
/// Appends at the end of the selection.
HelixAppend,
] ]
); );
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after); Vim::action(editor, cx, Vim::helix_normal_after);
Vim::action(editor, cx, Vim::helix_insert);
Vim::action(editor, cx, Vim::helix_append);
} }
impl Vim { impl Vim {
@ -299,6 +309,38 @@ impl Vim {
_ => self.helix_move_and_collapse(motion, times, window, cx), _ => self.helix_move_and_collapse(motion, times, window, cx),
} }
} }
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| {
// In helix normal mode, move cursor to start of selection and collapse
if !selection.is_empty() {
selection.collapse_to(selection.start, SelectionGoal::None);
}
});
});
});
self.switch_mode(Mode::Insert, false, window, cx);
}
fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let point = if selection.is_empty() {
right(map, selection.head(), 1)
} else {
selection.end
};
selection.collapse_to(point, SelectionGoal::None);
});
});
});
}
} }
#[cfg(test)] #[cfg(test)]
@ -497,4 +539,68 @@ mod test {
cx.assert_state("«ˇaa»\n", Mode::HelixNormal); cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
} }
#[gpui::test]
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
«The ˇ»quick brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("i");
cx.assert_state(
indoc! {"
ˇThe quick brown
fox jumps over
the lazy dog."},
Mode::Insert,
);
}
#[gpui::test]
async fn test_append(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// test from the end of the selection
cx.set_state(
indoc! {"
«Theˇ» quick brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("a");
cx.assert_state(
indoc! {"
Theˇ quick brown
fox jumps over
the lazy dog."},
Mode::Insert,
);
// test from the beginning of the selection
cx.set_state(
indoc! {"
«ˇThe» quick brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("a");
cx.assert_state(
indoc! {"
Theˇ quick brown
fox jumps over
the lazy dog."},
Mode::Insert,
);
}
} }