From e8d2627ef594347afd790b302b59130e0c72f076 Mon Sep 17 00:00:00 2001 From: Kai Kozlov <37962720+kaikozlov@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:32:33 -0500 Subject: [PATCH] Fix incorrect rainbow bracket matching in Markdown (#52107) ## 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. image image ## 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 - [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 --- crates/editor/src/bracket_colorization.rs | 14 ++++ crates/language/src/buffer.rs | 87 +++++++++++++++++------ 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs index 657f1e1b23d91ca421da6a38fbeaa382a65863db..ad2fc1bd8b9666dfa5e2c4b0367984c6398c98f8 100644 --- a/crates/editor/src/bracket_colorization.rs +++ b/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] diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6724b5b1c2e6b666b7f0295685e40427279a0b30..8a3886a7832fabbd67340f7f6d19b36557aa24a8 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4610,7 +4610,7 @@ impl BufferSnapshot { continue; } - let mut all_brackets: Vec<(BracketMatch, bool)> = Vec::new(); + let mut all_brackets: Vec<(BracketMatch, 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> = 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.