helix: Fix crash while moving through selections (#52922)

Closes #51573
Closes #52852 * I believe, looking for verification of that.

A very similar issue was belived to be fixed with #51642 , however it
seems like there is still some edge cases that were causing crashes.
These issues appeared to me to be caused by the dedup method failing to
catch sub-ranges (created by entering SelectMode after the search) as
duplicates.

two new tests that isolate the case in 51573 and 52852 respectively.

fixed vid:


https://github.com/user-attachments/assets/f62d5210-6cb3-4bdf-a061-efc265eb2804



Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable



Release Notes:

- helix: Fix search selection range deduplication logic
This commit is contained in:
Finn Eitreim authored and GitHub committed 2026-04-02 06:42:04 +00:00
1 parent d3fe409110
commit 2e736d8835
1 file changed
+92 -10
+92 -10
View File
@@ -12,7 +12,6 @@ use editor::{
};
use gpui::actions;
use gpui::{Context, Window};
use itertools::Itertools as _;
use language::{CharClassifier, CharKind, Point};
use search::{BufferSearchBar, SearchOptions};
use settings::Settings;
@@ -941,19 +940,15 @@ impl Vim {
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
let buffer = snapshot.buffer_snapshot();
s.select_anchor_ranges(
s.select_ranges(
prior_selections
.iter()
.cloned()
.chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
.sorted_by(|a, b| {
a.start
.cmp(&b.start, buffer)
.then_with(|| a.end.cmp(&b.end, buffer))
})
.dedup_by(|a, b| {
a.start.cmp(&b.start, buffer).is_eq()
&& a.end.cmp(&b.end, buffer).is_eq()
.map(|range| {
let start = range.start.to_offset(buffer);
let end = range.end.to_offset(buffer);
start..end
}),
);
})
@@ -2152,6 +2147,93 @@ mod test {
cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
}
#[gpui::test]
async fn test_helix_select_next_match_wrapping_from_normal(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Exact repro for #51573: start in HelixNormal, search, then `v` to
// enter HelixSelect, then `n` past last match.
//
// In HelixNormal, search collapses the cursor to the match start.
// Pressing `v` expands by only one character, creating a partial
// selection that overlaps the full match range when the search wraps.
// The overlapping ranges must be merged (not just deduped) to avoid
// a backward-seeking rope cursor panic.
cx.set_state(
indoc! {"
searˇch term
stuff
search term
other stuff
"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("/ t e r m");
cx.simulate_keystrokes("enter");
cx.simulate_keystrokes("v");
cx.simulate_keystrokes("n");
cx.simulate_keystrokes("n");
// Should not panic when wrapping past last match.
cx.assert_state(
indoc! {"
search «termˇ»
stuff
search «termˇ»
other stuff
"},
Mode::HelixSelect,
);
}
#[gpui::test]
async fn test_helix_select_star_then_match(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.enable_helix();
// Repro attempts for #52852: `*` searches for word under cursor,
// `v` enters select, `n` accumulates matches, `m` triggers match mode.
// Try multiple cursor positions and match counts.
// Cursor on first occurrence, 3 more occurrences to select through
cx.set_state(
indoc! {"
ˇone two one three one four one
"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("*");
cx.simulate_keystrokes("v");
cx.simulate_keystrokes("n n n");
// Should not panic on wrapping `n`.
// Cursor in the middle of text before matches
cx.set_state(
indoc! {"
heˇllo one two one three one
"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("*");
cx.simulate_keystrokes("v");
cx.simulate_keystrokes("n");
// Should not panic.
// The original #52852 sequence: * v n n n then m m
cx.set_state(
indoc! {"
fn ˇfoo() { bar(foo()) }
fn baz() { foo() }
"},
Mode::HelixNormal,
);
cx.simulate_keystrokes("*");
cx.simulate_keystrokes("v");
cx.simulate_keystrokes("n n n");
cx.simulate_keystrokes("m m");
// Should not panic.
}
#[gpui::test]
async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;