Fix incorrect rainbow bracket matching in Markdown (#52107)

Kai Kozlov created

## Context

Fixes #52022.

Rainbow bracket matching could become incorrect when tree-sitter
returned ambiguous bracket pairs for the same opening delimiter. The
repair path rebuilt pairs using a shared stack across all bracket query
patterns, which let excluded delimiters like Markdown single quotes
interfere with parenthesis matching.

This change scopes that repair logic to each bracket query pattern so
ambiguous matches are rebuilt without mixing unrelated delimiter types.
It also adds a regression test for the Markdown repro from the issue.

<img width="104" height="137" alt="image"
src="https://github.com/user-attachments/assets/4318bb4d-7072-4671-8fb5-c4478a179c07"
/>

<img width="104" height="137" alt="image"
src="https://github.com/user-attachments/assets/07a8a0fc-7618-4edb-a14e-645358d8d307"
/>


## How to Review

Review `crates/language/src/buffer.rs` first, especially the fallback
repair path for bogus tree-sitter bracket matches.

Then review `crates/editor/src/bracket_colorization.rs`, which adds
regression coverage for the issue repro.

## Self-Review Checklist

<!-- Check before requesting review: -->
- [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:

- Fixed rainbow brackets in Markdown when quotes caused parentheses to
match incorrectly

Change summary

crates/editor/src/bracket_colorization.rs | 14 ++++
crates/language/src/buffer.rs             | 87 ++++++++++++++++++------
2 files changed, 78 insertions(+), 23 deletions(-)

Detailed changes

crates/editor/src/bracket_colorization.rs 🔗

@@ -392,6 +392,20 @@ where
             &bracket_colors_markup(&mut cx),
             "All markdown brackets should be colored based on their depth, again"
         );
+
+        cx.set_state(indoc! {r#"ˇ('')('')
+
+((''))('')
+
+('')((''))"#});
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            "«1('')1»«1('')1»\n\n«1(«2('')2»)1»«1('')1»\n\n«1('')1»«1(«2('')2»)1»\n1 hsla(207.80, 16.20%, 69.19%, 1.00)\n2 hsla(29.00, 54.00%, 65.88%, 1.00)\n",
+            &bracket_colors_markup(&mut cx),
+            "Markdown quote pairs should not interfere with parenthesis pairing"
+        );
     }
 
     #[gpui::test]

crates/language/src/buffer.rs 🔗

@@ -4610,7 +4610,7 @@ impl BufferSnapshot {
                 continue;
             }
 
-            let mut all_brackets: Vec<(BracketMatch<usize>, bool)> = Vec::new();
+            let mut all_brackets: Vec<(BracketMatch<usize>, usize, bool)> = Vec::new();
             let mut opens = Vec::new();
             let mut color_pairs = Vec::new();
 
@@ -4636,8 +4636,9 @@ impl BufferSnapshot {
                 let mut open = None;
                 let mut close = None;
                 let syntax_layer_depth = mat.depth;
+                let pattern_index = mat.pattern_index;
                 let config = configs[mat.grammar_index];
-                let pattern = &config.patterns[mat.pattern_index];
+                let pattern = &config.patterns[pattern_index];
                 for capture in mat.captures {
                     if capture.index == config.open_capture_ix {
                         open = Some(capture.node.byte_range());
@@ -4658,7 +4659,7 @@ impl BufferSnapshot {
                 }
 
                 open_to_close_ranges
-                    .entry((open_range.start, open_range.end))
+                    .entry((open_range.start, open_range.end, pattern_index))
                     .or_insert_with(BTreeMap::new)
                     .insert(
                         (close_range.start, close_range.end),
@@ -4679,6 +4680,7 @@ impl BufferSnapshot {
                         newline_only: pattern.newline_only,
                         color_index: None,
                     },
+                    pattern_index,
                     pattern.rainbow_exclude,
                 ));
             }
@@ -4692,22 +4694,43 @@ impl BufferSnapshot {
                 // For each close, we know the expected open_len from tree-sitter matches.
 
                 // Map each close to its expected open length (for inferring opens)
-                let close_to_open_len: HashMap<(usize, usize), usize> = all_brackets
+                let close_to_open_len: HashMap<(usize, usize, usize), usize> = all_brackets
                     .iter()
-                    .map(|(m, _)| ((m.close_range.start, m.close_range.end), m.open_range.len()))
+                    .map(|(bracket_match, pattern_index, _)| {
+                        (
+                            (
+                                bracket_match.close_range.start,
+                                bracket_match.close_range.end,
+                                *pattern_index,
+                            ),
+                            bracket_match.open_range.len(),
+                        )
+                    })
                     .collect();
 
                 // Collect unique opens and closes within this chunk
-                let mut unique_opens: HashSet<(usize, usize)> = all_brackets
+                let mut unique_opens: HashSet<(usize, usize, usize)> = all_brackets
                     .iter()
-                    .map(|(m, _)| (m.open_range.start, m.open_range.end))
-                    .filter(|(start, _)| chunk_range.contains(start))
+                    .map(|(bracket_match, pattern_index, _)| {
+                        (
+                            bracket_match.open_range.start,
+                            bracket_match.open_range.end,
+                            *pattern_index,
+                        )
+                    })
+                    .filter(|(start, _, _)| chunk_range.contains(start))
                     .collect();
 
-                let mut unique_closes: Vec<(usize, usize)> = all_brackets
+                let mut unique_closes: Vec<(usize, usize, usize)> = all_brackets
                     .iter()
-                    .map(|(m, _)| (m.close_range.start, m.close_range.end))
-                    .filter(|(start, _)| chunk_range.contains(start))
+                    .map(|(bracket_match, pattern_index, _)| {
+                        (
+                            bracket_match.close_range.start,
+                            bracket_match.close_range.end,
+                            *pattern_index,
+                        )
+                    })
+                    .filter(|(start, _, _)| chunk_range.contains(start))
                     .collect();
                 unique_closes.sort();
                 unique_closes.dedup();
@@ -4716,8 +4739,9 @@ impl BufferSnapshot {
                 let mut unique_opens_vec: Vec<_> = unique_opens.iter().copied().collect();
                 unique_opens_vec.sort();
 
-                let mut valid_pairs: HashSet<((usize, usize), (usize, usize))> = HashSet::default();
-                let mut open_stack: Vec<(usize, usize)> = Vec::new();
+                let mut valid_pairs: HashSet<((usize, usize, usize), (usize, usize, usize))> =
+                    HashSet::default();
+                let mut open_stacks: HashMap<usize, Vec<(usize, usize)>> = HashMap::default();
                 let mut open_idx = 0;
 
                 for close in &unique_closes {
@@ -4725,36 +4749,53 @@ impl BufferSnapshot {
                     while open_idx < unique_opens_vec.len()
                         && unique_opens_vec[open_idx].0 < close.0
                     {
-                        open_stack.push(unique_opens_vec[open_idx]);
+                        let (start, end, pattern_index) = unique_opens_vec[open_idx];
+                        open_stacks
+                            .entry(pattern_index)
+                            .or_default()
+                            .push((start, end));
                         open_idx += 1;
                     }
 
                     // Try to match with most recent open
-                    if let Some(open) = open_stack.pop() {
-                        valid_pairs.insert((open, *close));
+                    let (close_start, close_end, pattern_index) = *close;
+                    if let Some(open) = open_stacks
+                        .get_mut(&pattern_index)
+                        .and_then(|open_stack| open_stack.pop())
+                    {
+                        valid_pairs.insert(((open.0, open.1, pattern_index), *close));
                     } else if let Some(&open_len) = close_to_open_len.get(close) {
                         // No open on stack - infer one based on expected open_len
-                        if close.0 >= open_len {
-                            let inferred = (close.0 - open_len, close.0);
+                        if close_start >= open_len {
+                            let inferred = (close_start - open_len, close_start, pattern_index);
                             unique_opens.insert(inferred);
                             valid_pairs.insert((inferred, *close));
                             all_brackets.push((
                                 BracketMatch {
                                     open_range: inferred.0..inferred.1,
-                                    close_range: close.0..close.1,
+                                    close_range: close_start..close_end,
                                     newline_only: false,
                                     syntax_layer_depth: 0,
                                     color_index: None,
                                 },
+                                pattern_index,
                                 false,
                             ));
                         }
                     }
                 }
 
-                all_brackets.retain(|(m, _)| {
-                    let open = (m.open_range.start, m.open_range.end);
-                    let close = (m.close_range.start, m.close_range.end);
+                all_brackets.retain(|(bracket_match, pattern_index, _)| {
+                    let open = (
+                        bracket_match.open_range.start,
+                        bracket_match.open_range.end,
+                        *pattern_index,
+                    );
+                    let close = (
+                        bracket_match.close_range.start,
+                        bracket_match.close_range.end,
+                        *pattern_index,
+                    );
                     valid_pairs.contains(&(open, close))
                 });
             }
@@ -4762,7 +4803,7 @@ impl BufferSnapshot {
             let mut all_brackets = all_brackets
                 .into_iter()
                 .enumerate()
-                .map(|(index, (bracket_match, rainbow_exclude))| {
+                .map(|(index, (bracket_match, _, rainbow_exclude))| {
                     // Certain languages have "brackets" that are not brackets, e.g. tags. and such
                     // bracket will match the entire tag with all text inside.
                     // For now, avoid highlighting any pair that has more than single char in each bracket.