@@ -348,6 +348,64 @@ where
);
}
+ #[gpui::test]
+ async fn test_markdown_brackets_in_multiple_hunks(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let mut cx = EditorLspTestContext::new(
+ Arc::into_inner(markdown_lang()).unwrap(),
+ lsp::ServerCapabilities::default(),
+ cx,
+ )
+ .await;
+
+ let rows = 100;
+ let footer = "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n";
+
+ let simple_brackets = (0..rows).map(|_| "ˇ[]\n").collect::<String>();
+ let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::<String>();
+ cx.set_state(&simple_brackets);
+ cx.update_editor(|editor, window, cx| {
+ editor.move_to_end(&MoveToEnd, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ format!("{simple_brackets_highlights}\n{footer}"),
+ bracket_colors_markup(&mut cx),
+ "Simple bracket pairs should be colored"
+ );
+
+ let paired_brackets = (0..rows).map(|_| "ˇ[]()\n").collect::<String>();
+ let paired_brackets_highlights = (0..rows).map(|_| "«1[]1»«1()1»\n").collect::<String>();
+ cx.set_state(&paired_brackets);
+ // Wait for reparse to complete after content change
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ cx.update_editor(|editor, _, cx| {
+ // Force invalidation of bracket cache after reparse
+ editor.colorize_brackets(true, cx);
+ });
+ // Scroll to beginning to fetch first chunks
+ cx.update_editor(|editor, window, cx| {
+ editor.move_to_beginning(&MoveToBeginning, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ // Scroll to end to fetch remaining chunks
+ cx.update_editor(|editor, window, cx| {
+ editor.move_to_end(&MoveToEnd, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ format!("{paired_brackets_highlights}\n{footer}"),
+ bracket_colors_markup(&mut cx),
+ "Paired bracket pairs should be colored"
+ );
+ }
+
#[gpui::test]
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
@@ -4444,7 +4444,7 @@ impl BufferSnapshot {
continue;
}
- let mut all_brackets = Vec::new();
+ let mut all_brackets: Vec<(BracketMatch<usize>, bool)> = Vec::new();
let mut opens = Vec::new();
let mut color_pairs = Vec::new();
@@ -4463,6 +4463,9 @@ impl BufferSnapshot {
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>();
+ // Group matches by open range so we can either trust grammar output
+ // or repair it by picking a single closest close per open.
+ let mut open_to_close_ranges = BTreeMap::new();
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
@@ -4488,27 +4491,131 @@ impl BufferSnapshot {
continue;
}
- let index = all_brackets.len();
- all_brackets.push(BracketMatch {
- open_range: open_range.clone(),
- close_range: close_range.clone(),
- newline_only: pattern.newline_only,
- syntax_layer_depth,
- color_index: None,
- });
+ open_to_close_ranges
+ .entry((open_range.start, open_range.end))
+ .or_insert_with(BTreeMap::new)
+ .insert(
+ (close_range.start, close_range.end),
+ BracketMatch {
+ open_range: open_range.clone(),
+ close_range: close_range.clone(),
+ syntax_layer_depth,
+ newline_only: pattern.newline_only,
+ color_index: None,
+ },
+ );
+
+ all_brackets.push((
+ BracketMatch {
+ open_range,
+ close_range,
+ syntax_layer_depth,
+ newline_only: pattern.newline_only,
+ color_index: None,
+ },
+ pattern.rainbow_exclude,
+ ));
+ }
+
+ let has_bogus_matches = open_to_close_ranges
+ .iter()
+ .any(|(_, end_ranges)| end_ranges.len() > 1);
+ if has_bogus_matches {
+ // Grammar is producing bogus matches where one open is paired with multiple
+ // closes. Build a valid stack by walking through positions in order.
+ // 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
+ .iter()
+ .map(|(m, _)| ((m.close_range.start, m.close_range.end), m.open_range.len()))
+ .collect();
+
+ // Collect unique opens and closes within this chunk
+ let mut unique_opens: HashSet<(usize, usize)> = all_brackets
+ .iter()
+ .map(|(m, _)| (m.open_range.start, m.open_range.end))
+ .filter(|(start, _)| chunk_range.contains(start))
+ .collect();
+
+ let mut unique_closes: Vec<(usize, usize)> = all_brackets
+ .iter()
+ .map(|(m, _)| (m.close_range.start, m.close_range.end))
+ .filter(|(start, _)| chunk_range.contains(start))
+ .collect();
+ unique_closes.sort();
+ unique_closes.dedup();
+
+ // Build valid pairs by walking through closes in order
+ 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 open_idx = 0;
+
+ for close in &unique_closes {
+ // Push all opens before this close onto stack
+ while open_idx < unique_opens_vec.len()
+ && unique_opens_vec[open_idx].0 < close.0
+ {
+ open_stack.push(unique_opens_vec[open_idx]);
+ open_idx += 1;
+ }
- // 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.
- // We need to colorize `<Element/>` bracket pairs, so cannot make this check stricter.
- let should_color =
- !pattern.rainbow_exclude && (open_range.len() == 1 || close_range.len() == 1);
- if should_color {
- opens.push(open_range.clone());
- color_pairs.push((open_range, close_range, index));
+ // Try to match with most recent open
+ if let Some(open) = open_stack.pop() {
+ valid_pairs.insert((open, *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);
+ 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,
+ newline_only: false,
+ syntax_layer_depth: 0,
+ color_index: None,
+ },
+ 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);
+ valid_pairs.contains(&(open, close))
+ });
}
+ let mut all_brackets = all_brackets
+ .into_iter()
+ .enumerate()
+ .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.
+ // We need to colorize `<Element/>` bracket pairs, so cannot make this check stricter.
+ let should_color = !rainbow_exclude
+ && (bracket_match.open_range.len() == 1
+ || bracket_match.close_range.len() == 1);
+ if should_color {
+ opens.push(bracket_match.open_range.clone());
+ color_pairs.push((
+ bracket_match.open_range.clone(),
+ bracket_match.close_range.clone(),
+ index,
+ ));
+ }
+ bracket_match
+ })
+ .collect::<Vec<_>>();
+
opens.sort_by_key(|r| (r.start, r.end));
opens.dedup_by(|a, b| a.start == b.start && a.end == b.end);
color_pairs.sort_by_key(|(_, close, _)| close.end);