vim: Fix helix select next match panic when search wraps around (#51642)

Lukas Wirth created

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

Change summary

crates/vim/src/helix.rs | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)

Detailed changes

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;