helix: Improve "x" behavior (#35611)

Closes #32020 

Release Notes:
- Helix: Improve `x` behaviour. Will respect modifiers (`5 x`). Pressing
`x` on a empty line, will select current+next line, because helix
considers current line to be already selected without the need of
pressing `x`.
This commit is contained in:
Romans Malinovskis
2025-08-27 20:25:00 -05:00
committed by GitHub
parent 8af212e785
commit 4e1a901059
2 changed files with 187 additions and 3 deletions
+1 -1
View File
@@ -435,7 +435,7 @@
"g b": "vim::WindowBottom",
"shift-r": "editor::Paste",
"x": "editor::SelectLine",
"x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
+186 -2
View File
@@ -1,8 +1,10 @@
use editor::display_map::DisplaySnapshot;
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
use editor::{
DisplayPoint, Editor, HideMouseCursorOrigin, SelectionEffects, ToOffset, ToPoint, movement,
};
use gpui::{Action, actions};
use gpui::{Context, Window};
use language::{CharClassifier, CharKind};
use language::{CharClassifier, CharKind, Point};
use text::{Bias, SelectionGoal};
use crate::motion;
@@ -25,11 +27,14 @@ actions!(
HelixAppend,
/// Goes to the location of the last modification.
HelixGotoLastModification,
/// Select entire line or multiple lines, extending downwards.
HelixSelectLine,
]
);
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after);
Vim::action(editor, cx, Vim::helix_select_lines);
Vim::action(editor, cx, Vim::helix_insert);
Vim::action(editor, cx, Vim::helix_append);
Vim::action(editor, cx, Vim::helix_yank);
@@ -442,6 +447,47 @@ impl Vim {
) {
self.jump(".".into(), false, false, window, cx);
}
pub fn helix_select_lines(
&mut self,
_: &HelixSelectLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = Vim::take_count(cx).unwrap_or(1);
self.update_editor(cx, |_, editor, cx| {
editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = editor.selections.all::<Point>(cx);
let max_point = display_map.buffer_snapshot.max_point();
let buffer_snapshot = &display_map.buffer_snapshot;
for selection in &mut selections {
// Start always goes to column 0 of the first selected line
let start_row = selection.start.row;
let current_end_row = selection.end.row;
// Check if cursor is on empty line by checking first character
let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
let first_char = buffer_snapshot.chars_at(line_start_offset).next();
let extra_line = if first_char == Some('\n') { 1 } else { 0 };
let end_row = current_end_row + count as u32 + extra_line;
selection.start = Point::new(start_row, 0);
selection.end = if end_row > max_point.row {
max_point
} else {
Point::new(end_row, 0)
};
selection.reversed = false;
}
editor.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
});
}
}
#[cfg(test)]
@@ -850,4 +896,142 @@ mod test {
Mode::HelixNormal,
);
}
#[gpui::test]
async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
"line one\nline ˇtwo\nline three\nline four",
Mode::HelixNormal,
);
cx.simulate_keystrokes("2 x");
cx.assert_state(
"line one\n«line two\nline three\nˇ»line four",
Mode::HelixNormal,
);
// Test extending existing line selection
cx.set_state(
indoc! {"
li«ˇne one
li»ne two
line three
line four"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
«line one
line two
ˇ»line three
line four"},
Mode::HelixNormal,
);
// Pressing x in empty line, select next line (because helix considers cursor a selection)
cx.set_state(
indoc! {"
line one
ˇ
line three
line four"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
line one
«
line three
ˇ»line four"},
Mode::HelixNormal,
);
// Empty line with count selects extra + count lines
cx.set_state(
indoc! {"
line one
ˇ
line three
line four
line five"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("2 x");
cx.assert_state(
indoc! {"
line one
«
line three
line four
ˇ»line five"},
Mode::HelixNormal,
);
// Compare empty vs non-empty line behavior
cx.set_state(
indoc! {"
ˇnon-empty line
line two
line three"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
«non-empty line
ˇ»line two
line three"},
Mode::HelixNormal,
);
// Same test but with empty line - should select one extra
cx.set_state(
indoc! {"
ˇ
line two
line three"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
«
line two
ˇ»line three"},
Mode::HelixNormal,
);
// Test selecting multiple lines with count
cx.set_state(
indoc! {"
ˇline one
line two
line threeˇ
line four
line five"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
«line one
ˇ»line two
«line three
ˇ»line four
line five"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("x");
cx.assert_state(
indoc! {"
«line one
line two
line three
line four
ˇ»line five"},
Mode::HelixNormal,
);
}
}