vim: Fix mini delimiters in multibuffer (#41834)

Dino created

- Update `vim::object::find_mini_delimiters` in order to filter out the
  ranges before calling `vim::object::cover_or_next`, ensuring that the
  provided ranges are converted from multibuffer space into buffer
  space.
- Remove the `range_filter` from `vim::object::cover_or_next` was the
  `find_mini_delimiters` function is the only caller and no longer uses
  it

Closes #41346 

Release Notes:

- Fixed a crash that could occur when using `vim::MiniQuotes` and
`vim::MiniBrackets` in a multibuffer

Change summary

crates/vim/src/object.rs | 109 +++++++++++++++++++++++++++++++++++++----
1 file changed, 98 insertions(+), 11 deletions(-)

Detailed changes

crates/vim/src/object.rs 🔗

@@ -124,7 +124,6 @@ fn cover_or_next<I: Iterator<Item = (Range<usize>, Range<usize>)>>(
     candidates: Option<I>,
     caret: DisplayPoint,
     map: &DisplaySnapshot,
-    range_filter: Option<&dyn Fn(Range<usize>, Range<usize>) -> bool>,
 ) -> Option<CandidateWithRanges> {
     let caret_offset = caret.to_offset(map, Bias::Left);
     let mut covering = vec![];
@@ -135,11 +134,6 @@ fn cover_or_next<I: Iterator<Item = (Range<usize>, Range<usize>)>>(
         for (open_range, close_range) in ranges {
             let start_off = open_range.start;
             let end_off = close_range.end;
-            if let Some(range_filter) = range_filter
-                && !range_filter(open_range.clone(), close_range.clone())
-            {
-                continue;
-            }
             let candidate = CandidateWithRanges {
                 candidate: CandidateRange {
                     start: start_off.to_display_point(map),
@@ -214,16 +208,35 @@ fn find_mini_delimiters(
     let visible_line_range = get_visible_line_range(&line_range);
 
     let snapshot = &map.buffer_snapshot();
-    let excerpt = snapshot.excerpt_containing(offset..offset)?;
+    let mut excerpt = snapshot.excerpt_containing(offset..offset)?;
     let buffer = excerpt.buffer();
+    let buffer_offset = excerpt.map_offset_to_buffer(offset);
 
     let bracket_filter = |open: Range<usize>, close: Range<usize>| {
         is_valid_delimiter(buffer, open.start, close.start)
     };
 
     // Try to find delimiters in visible range first
-    let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
-    if let Some(candidate) = cover_or_next(ranges, display_point, map, Some(&bracket_filter)) {
+    let ranges = map
+        .buffer_snapshot()
+        .bracket_ranges(visible_line_range)
+        .map(|ranges| {
+            ranges.filter_map(move |(open, close)| {
+                // Convert the ranges from multibuffer space to buffer space as
+                // that is what `is_valid_delimiter` expects, otherwise it might
+                // panic as the values might be out of bounds.
+                let buffer_open = excerpt.map_range_to_buffer(open.clone());
+                let buffer_close = excerpt.map_range_to_buffer(close.clone());
+
+                if is_valid_delimiter(buffer, buffer_open.start, buffer_close.start) {
+                    Some((open, close))
+                } else {
+                    None
+                }
+            })
+        });
+
+    if let Some(candidate) = cover_or_next(ranges, display_point, map) {
         return Some(
             DelimiterRange {
                 open: candidate.open_range,
@@ -234,8 +247,8 @@ fn find_mini_delimiters(
     }
 
     // Fall back to innermost enclosing brackets
-    let (open_bracket, close_bracket) =
-        buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?;
+    let (open_bracket, close_bracket) = buffer
+        .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?;
 
     Some(
         DelimiterRange {
@@ -1736,8 +1749,10 @@ pub fn surrounding_markers(
 
 #[cfg(test)]
 mod test {
+    use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext};
     use gpui::KeyBinding;
     use indoc::indoc;
+    use text::Point;
 
     use crate::{
         object::{AnyBrackets, AnyQuotes, MiniBrackets},
@@ -3185,6 +3200,78 @@ mod test {
         }
     }
 
+    #[gpui::test]
+    async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) {
+        // Initialize test context with the TypeScript language loaded, so we
+        // can actually get brackets definition.
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Update `b` to `MiniBrackets` so we can later use it when simulating
+        // keystrokes.
+        cx.update(|_, cx| {
+            cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]);
+        });
+
+        let (editor, cx) = cx.add_window_view(|window, cx| {
+            let multi_buffer = MultiBuffer::build_multi(
+                [
+                    ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]),
+                    ("111\na {bracket} example\n", vec![Point::row_range(0..2)]),
+                ],
+                cx,
+            );
+
+            // In order for the brackets to actually be found, we need to update
+            // the language used for the second buffer. This is something that
+            // is handled automatically when simply using `VimTestContext::new`
+            // but, since this is being set manually, the language isn't
+            // automatically set.
+            let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
+            let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
+            if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_language(Some(language::rust_lang()), cx);
+                })
+            };
+
+            editor
+        });
+
+        let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
+
+        cx.assert_excerpts_with_selections(indoc! {"
+            [EXCERPT]
+            ˇ111
+            222
+            [EXCERPT]
+            111
+            a {bracket} example
+            "
+        });
+
+        cx.simulate_keystrokes("j j j j f r");
+        cx.assert_excerpts_with_selections(indoc! {"
+            [EXCERPT]
+            111
+            222
+            [EXCERPT]
+            111
+            a {bˇracket} example
+            "
+        });
+
+        cx.simulate_keystrokes("d i b");
+        cx.assert_excerpts_with_selections(indoc! {"
+            [EXCERPT]
+            111
+            222
+            [EXCERPT]
+            111
+            a {ˇ} example
+            "
+        });
+    }
+
     #[gpui::test]
     async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;