From 357ee0fa3f51f0558f7c7c7e2e22d4518fff5177 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 16 Mar 2026 11:14:26 +0100 Subject: [PATCH] vim: Fix helix select next match panic when search wraps around (#51642) Fixes ZED-4YP Sort and deduplicate anchor ranges in do_helix_select before passing them to select_anchor_ranges. When the search wraps past the end of the document back to the beginning, the new selection is at a lower offset than the accumulated prior selections, producing unsorted anchors that crash the rope cursor with 'cannot summarize backward'. Release Notes: - Fixed a panic in helix mode with search selecting wrapping around the document end --- crates/vim/src/helix.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 06630d18edfe0d1f3e643f02a1f50e5a1f4a0682..60d87572eb3151f8e36c06f91501921ea9affb3b 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -12,6 +12,7 @@ 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; @@ -876,11 +877,22 @@ impl Vim { self.update_editor(cx, |_vim, editor, cx| { let snapshot = editor.snapshot(window, cx); editor.change_selections(SelectionEffects::default(), window, cx, |s| { + let buffer = snapshot.buffer_snapshot(); + s.select_anchor_ranges( prior_selections .iter() .cloned() - .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())), + .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() + }), ); }) }); @@ -1670,6 +1682,25 @@ mod test { cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect); } + #[gpui::test] + async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Three occurrences of "one". After selecting all three with `n n`, + // pressing `n` again wraps the search to the first occurrence. + // The prior selections (at higher offsets) are chained before the + // wrapped selection (at a lower offset), producing unsorted anchors + // that cause `rope::Cursor::summary` to panic with + // "cannot summarize backward". + 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 n"); + // Should not panic; all three occurrences should remain selected. + cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect); + } + #[gpui::test] async fn test_helix_substitute(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await;