Fix Helix mode search & selection (#42928)

This PR redoes the desired behavior changes of #41583 (reverted in
#42892) but less invasively

Closes #41125
Closes #41164

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Andrew Farkas
2025-11-19 18:30:55 +00:00
committed by GitHub
co-authored by Conrad Irwin
parent 829be71061
commit 27cb01f4af
5 changed files with 146 additions and 8 deletions
+9
View File
@@ -421,6 +421,12 @@
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {
@@ -470,6 +476,9 @@
"alt-p": "editor::SelectPreviousSyntaxNode",
"alt-n": "editor::SelectNextSyntaxNode",
"n": "vim::HelixSelectNext",
"shift-n": "vim::HelixSelectPrevious",
// Goto mode
"g e": "vim::EndOfDocument",
"g h": "vim::StartOfLine",
+1 -1
View File
@@ -1099,7 +1099,7 @@ pub struct Editor {
searchable: bool,
cursor_shape: CursorShape,
current_line_highlight: Option<CurrentLineHighlight>,
collapse_matches: bool,
pub collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
input_enabled: bool,
+133 -4
View File
@@ -15,8 +15,8 @@ use language::{CharClassifier, CharKind, Point};
use search::{BufferSearchBar, SearchOptions};
use settings::Settings;
use text::{Bias, SelectionGoal};
use workspace::searchable;
use workspace::searchable::FilteredSearchRange;
use workspace::searchable::{self, Direction};
use crate::motion::{self, MotionKind};
use crate::state::SearchState;
@@ -52,6 +52,10 @@ actions!(
HelixSubstitute,
/// Delete the selection and enter edit mode, without yanking the selection.
HelixSubstituteNoYank,
/// Delete the selection and enter edit mode.
HelixSelectNext,
/// Delete the selection and enter edit mode, without yanking the selection.
HelixSelectPrevious,
]
);
@@ -74,6 +78,8 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, Vim::helix_substitute);
Vim::action(editor, cx, Vim::helix_substitute_no_yank);
Vim::action(editor, cx, Vim::helix_select_next);
Vim::action(editor, cx, Vim::helix_select_previous);
}
impl Vim {
@@ -97,6 +103,11 @@ impl Vim {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
if let Motion::ZedSearchResult { new_selections, .. } = &motion {
s.select_anchor_ranges(new_selections.clone());
return;
};
s.move_with(|map, selection| {
let current_head = selection.head();
@@ -664,6 +675,68 @@ impl Vim {
) {
self.do_helix_substitute(false, window, cx);
}
fn helix_select_next(
&mut self,
_: &HelixSelectNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_helix_select(Direction::Next, window, cx);
}
fn helix_select_previous(
&mut self,
_: &HelixSelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.do_helix_select(Direction::Prev, window, cx);
}
fn do_helix_select(
&mut self,
direction: searchable::Direction,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(pane) = self.pane(window, cx) else {
return;
};
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
let prior_selections = self.editor_selections(window, cx);
let success = pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return false;
};
search_bar.update(cx, |search_bar, cx| {
if !search_bar.has_active_match() || !search_bar.show(window, cx) {
return false;
}
search_bar.select_match(direction, count, window, cx);
true
})
});
if !success {
return;
}
if self.mode == Mode::HelixSelect {
self.update_editor(cx, |_vim, editor, cx| {
let snapshot = editor.snapshot(window, cx);
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_anchor_ranges(
prior_selections
.iter()
.cloned()
.chain(s.all_anchors(&snapshot).iter().map(|s| s.range())),
);
})
});
}
}
}
#[cfg(test)]
@@ -1278,6 +1351,24 @@ mod test {
cx.assert_state("«one ˇ»two", Mode::HelixSelect);
}
#[gpui::test]
async fn test_exit_visual_mode(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);
cx.simulate_keystrokes("escape");
cx.assert_state("one ˇtwo", Mode::Normal);
cx.enable_helix();
cx.set_state("ˇone two", Mode::HelixNormal);
cx.simulate_keystrokes("v w");
cx.assert_state("«one ˇ»two", Mode::HelixSelect);
cx.simulate_keystrokes("escape");
cx.assert_state("«one ˇ»two", Mode::HelixNormal);
}
#[gpui::test]
async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
@@ -1297,9 +1388,47 @@ mod test {
cx.simulate_keystrokes("enter");
cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
cx.set_state("ˇone two one", Mode::HelixNormal);
cx.simulate_keystrokes("s o n e enter");
cx.assert_state("ˇone two one", Mode::HelixNormal);
// TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
// cx.set_state("ˇstuff one two one", Mode::HelixNormal);
// cx.simulate_keystrokes("s o n e enter");
// cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
}
#[gpui::test]
async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("ˇhello two one two one two one", Mode::Visual);
cx.simulate_keystrokes("/ o n e");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("n n");
cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
cx.set_state("ˇhello two one two one two one", Mode::Normal);
cx.simulate_keystrokes("/ o n e");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("n n");
cx.assert_state("hello two one two one two ˇone", Mode::Normal);
cx.set_state("ˇhello two one two one two one", Mode::Normal);
cx.simulate_keystrokes("/ o n e");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("n g n g n");
cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
cx.enable_helix();
cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
cx.simulate_keystrokes("/ o n e");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("n n");
cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
cx.simulate_keystrokes("/ o n e");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("n n");
cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
}
#[gpui::test]
-1
View File
@@ -691,7 +691,6 @@ impl Vim {
return;
}
}
Mode::HelixNormal | Mode::HelixSelect => {}
}
}
+3 -2
View File
@@ -666,7 +666,7 @@ impl Vim {
editor,
cx,
|vim, _: &SwitchToHelixNormalMode, window, cx| {
vim.switch_mode(Mode::HelixNormal, false, window, cx)
vim.switch_mode(Mode::HelixNormal, true, window, cx)
},
);
Vim::action(editor, cx, |_, _: &PushForcedMotion, _, cx| {
@@ -1928,7 +1928,8 @@ impl Vim {
self.update_editor(cx, |vim, editor, cx| {
editor.set_cursor_shape(vim.cursor_shape(cx), cx);
editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx);
editor.set_collapse_matches(true);
let collapse_matches = !HelixModeSetting::get_global(cx).0;
editor.set_collapse_matches(collapse_matches);
editor.set_input_enabled(vim.editor_input_enabled());
editor.set_autoindent(vim.should_autoindent());
editor