From 813a9bb0bcb40a24a9fc44c48fdba0cc6ab38308 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Sun, 14 Sep 2025 09:32:12 +0100 Subject: [PATCH] Fix select in Helix mode (#38117) Hotfixes issue I have introduced in #37748. Without this, helix mode select not working at all in `main` branch. Release Notes: - N/A --- assets/keymaps/vim.json | 2 +- crates/vim/src/helix.rs | 74 ++++++++++++++++++++++++++++++++++++++++ crates/vim/src/motion.rs | 5 ++- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index ed79b00c16a7586f4a121c3fbd6e71f883dbb2d4..8464e03d251afc166ac45a349894ecf2f7247944 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -328,7 +328,7 @@ { "context": "vim_mode == helix_select", "bindings": { - "escape": "vim::NormalBefore", + "v": "vim::NormalBefore", ";": "vim::HelixCollapseSelection", "~": "vim::ChangeCase", "ctrl-a": "vim::Increment", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 9f5580d75cc4f3588e4211d1ef2bbfb7df49c92f..369ec2df11e2d5cf99a0b3d7a72ccd5619161f2f 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -53,6 +53,35 @@ impl Vim { self.helix_move_cursor(motion, times, window, cx); } + pub fn helix_select_motion( + &mut self, + motion: Motion, + times: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(window); + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let current_head = selection.head(); + + let Some((new_head, goal)) = motion.move_point( + map, + current_head, + selection.goal, + times, + &text_layout_details, + ) else { + return; + }; + + selection.set_head(new_head, goal); + }) + }); + }); + } + /// Updates all selections based on where the cursors are. fn helix_new_selections( &mut self, @@ -1033,4 +1062,49 @@ mod test { Mode::HelixNormal, ); } + + #[gpui::test] + async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + assert_eq!(cx.mode(), Mode::Normal); + cx.enable_helix(); + + cx.set_state("ˇhello", Mode::HelixNormal); + cx.simulate_keystrokes("l v l l"); + cx.assert_state("h«ellˇ»o", Mode::HelixSelect); + } + + #[gpui::test] + async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + assert_eq!(cx.mode(), Mode::Normal); + cx.enable_helix(); + + // Start with multiple cursors (no selections) + cx.set_state("ˇhello\nˇworld", Mode::HelixNormal); + + // Enter select mode and move right twice + cx.simulate_keystrokes("v l l"); + + // Each cursor should independently create and extend its own selection + cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect); + } + + #[gpui::test] + async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("ˇone two", Mode::Normal); + cx.simulate_keystrokes("v w"); + cx.assert_state("«one tˇ»wo", Mode::Visual); + + // In Vim, this selects "t". In helix selections stops just before "t" + + cx.enable_helix(); + cx.set_state("ˇone two", Mode::HelixNormal); + cx.simulate_keystrokes("v w"); + cx.assert_state("«one ˇ»two", Mode::HelixSelect); + } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e2dfb24008c2f23044ca1f3633dcc02930b41e54..829e8bf1117fea3dc5d6c98b9c59effbf9c3f4c1 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -726,9 +726,8 @@ impl Vim { self.visual_motion(motion, count, window, cx) } - Mode::HelixNormal | Mode::HelixSelect => { - self.helix_normal_motion(motion, count, window, cx) - } + Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx), + Mode::HelixSelect => self.helix_select_motion(motion, count, window, cx), } self.clear_operator(window, cx); if let Some(operator) = waiting_operator {