diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd35845c2e46c31c74d8ef4baf422aaf3..0a88baee027a3ae4d72409f5f142ceda3f4d9717 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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 diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d8d834f31c0c5e6a0fcc24f329d13b9..abde3a8ce6e8755bb49826fb408a6af36661f00c 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -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::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, + ) { + 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::(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, + ); + } }