From 8d15ec7f9944e206e7cc25b5c1b2b15e0db22a6c Mon Sep 17 00:00:00 2001 From: Dino Date: Tue, 4 Nov 2025 11:08:51 +0000 Subject: [PATCH] vim: Fix mini delimiters in multibuffer (#41834) - 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 --- crates/vim/src/object.rs | 109 +++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 5d0ac722872f3c39067a668c4ed5d56847c61898..f361dd8f274879f067c49bf04c0a73ebbc34be06 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -124,7 +124,6 @@ fn cover_or_next, Range)>>( candidates: Option, caret: DisplayPoint, map: &DisplaySnapshot, - range_filter: Option<&dyn Fn(Range, Range) -> bool>, ) -> Option { let caret_offset = caret.to_offset(map, Bias::Left); let mut covering = vec![]; @@ -135,11 +134,6 @@ fn cover_or_next, Range)>>( 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, close: Range| { 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;