1//! Bracket highlights, also known as "rainbow brackets".
2//! Uses tree-sitter queries from brackets.scm to capture bracket pairs,
3//! and theme accents to colorize those.
4
5use std::ops::Range;
6
7use crate::{Editor, HighlightKey};
8use collections::{HashMap, HashSet};
9use gpui::{AppContext as _, Context, HighlightStyle};
10use language::{BufferRow, BufferSnapshot, language_settings::LanguageSettings};
11use multi_buffer::{Anchor, BufferOffset, ExcerptRange, MultiBufferSnapshot};
12use text::OffsetRangeExt as _;
13use ui::{ActiveTheme, utils::ensure_minimum_contrast};
14
15impl Editor {
16 pub(crate) fn colorize_brackets(&mut self, invalidate: bool, cx: &mut Context<Editor>) {
17 if !self.mode.is_full() {
18 return;
19 }
20
21 if invalidate {
22 self.bracket_fetched_tree_sitter_chunks.clear();
23 }
24
25 let accents_count = cx.theme().accents().0.len();
26 let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
27
28 let visible_excerpts = self.visible_buffer_ranges(cx);
29 let excerpt_data: Vec<(
30 BufferSnapshot,
31 Range<BufferOffset>,
32 ExcerptRange<text::Anchor>,
33 )> = visible_excerpts
34 .into_iter()
35 .filter(|(buffer_snapshot, _, _)| {
36 let Some(buffer) = self.buffer().read(cx).buffer(buffer_snapshot.remote_id())
37 else {
38 return false;
39 };
40 LanguageSettings::for_buffer(buffer.read(cx), cx).colorize_brackets
41 })
42 .collect();
43
44 let mut fetched_tree_sitter_chunks = excerpt_data
45 .iter()
46 .filter_map(|(_, _, excerpt_range)| {
47 let key = excerpt_range.context.clone();
48 Some((
49 key.clone(),
50 self.bracket_fetched_tree_sitter_chunks.get(&key).cloned()?,
51 ))
52 })
53 .collect::<HashMap<Range<text::Anchor>, HashSet<Range<BufferRow>>>>();
54
55 let bracket_matches_by_accent = cx.background_spawn(async move {
56 let bracket_matches_by_accent: HashMap<usize, Vec<Range<Anchor>>> =
57 excerpt_data.into_iter().fold(
58 HashMap::default(),
59 |mut acc, (buffer_snapshot, buffer_range, excerpt_range)| {
60 let fetched_chunks = fetched_tree_sitter_chunks
61 .entry(excerpt_range.context.clone())
62 .or_default();
63
64 let brackets_by_accent = compute_bracket_ranges(
65 &multi_buffer_snapshot,
66 &buffer_snapshot,
67 buffer_range,
68 excerpt_range,
69 fetched_chunks,
70 accents_count,
71 );
72
73 for (accent_number, new_ranges) in brackets_by_accent {
74 let ranges = acc
75 .entry(accent_number)
76 .or_insert_with(Vec::<Range<Anchor>>::new);
77
78 for new_range in new_ranges {
79 let i = ranges
80 .binary_search_by(|probe| {
81 probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
82 })
83 .unwrap_or_else(|i| i);
84 ranges.insert(i, new_range);
85 }
86 }
87
88 acc
89 },
90 );
91
92 (bracket_matches_by_accent, fetched_tree_sitter_chunks)
93 });
94
95 let editor_background = cx.theme().colors().editor_background;
96 let accents = cx.theme().accents().clone();
97
98 self.colorize_brackets_task = cx.spawn(async move |editor, cx| {
99 if invalidate {
100 editor
101 .update(cx, |editor, cx| {
102 editor.clear_highlights_with(
103 &mut |key| matches!(key, HighlightKey::ColorizeBracket(_)),
104 cx,
105 );
106 })
107 .ok();
108 }
109
110 let (bracket_matches_by_accent, updated_chunks) = bracket_matches_by_accent.await;
111
112 editor
113 .update(cx, |editor, cx| {
114 editor
115 .bracket_fetched_tree_sitter_chunks
116 .extend(updated_chunks);
117 for (accent_number, bracket_highlights) in bracket_matches_by_accent {
118 let bracket_color = accents.color_for_index(accent_number as u32);
119 let adjusted_color =
120 ensure_minimum_contrast(bracket_color, editor_background, 55.0);
121 let style = HighlightStyle {
122 color: Some(adjusted_color),
123 ..HighlightStyle::default()
124 };
125
126 editor.highlight_text_key(
127 HighlightKey::ColorizeBracket(accent_number),
128 bracket_highlights,
129 style,
130 true,
131 cx,
132 );
133 }
134 })
135 .ok();
136 });
137 }
138}
139
140fn compute_bracket_ranges(
141 multi_buffer_snapshot: &MultiBufferSnapshot,
142 buffer_snapshot: &BufferSnapshot,
143 buffer_range: Range<BufferOffset>,
144 excerpt_range: ExcerptRange<text::Anchor>,
145 fetched_chunks: &mut HashSet<Range<BufferRow>>,
146 accents_count: usize,
147) -> Vec<(usize, Vec<Range<Anchor>>)> {
148 let context = excerpt_range.context.to_offset(buffer_snapshot);
149 let buffer_range = buffer_range.start.0..buffer_range.end.0;
150 let large_block_pairs = buffer_snapshot.bracket_pairs_for_large_enclosing_blocks(&buffer_range);
151 let large_block_depth = large_block_pairs.len();
152
153 buffer_snapshot
154 .fetch_bracket_ranges(buffer_range, Some(fetched_chunks))
155 .into_iter()
156 .flat_map(|(chunk_range, pairs)| {
157 if fetched_chunks.insert(chunk_range) {
158 pairs
159 } else {
160 Vec::new()
161 }
162 })
163 .map(move |mut pair| {
164 if let Some(idx) = pair.color_index.as_mut() {
165 *idx += large_block_depth;
166 }
167 pair
168 })
169 .chain(large_block_pairs)
170 .filter_map(|pair| {
171 let color_index = pair.color_index?;
172
173 let mut ranges = Vec::new();
174
175 if context.start <= pair.open_range.start && pair.open_range.end <= context.end {
176 let anchors = buffer_snapshot.anchor_range_inside(pair.open_range);
177 ranges.push(
178 multi_buffer_snapshot.anchor_in_buffer(anchors.start)?
179 ..multi_buffer_snapshot.anchor_in_buffer(anchors.end)?,
180 );
181 };
182
183 if context.start <= pair.close_range.start && pair.close_range.end <= context.end {
184 let anchors = buffer_snapshot.anchor_range_inside(pair.close_range);
185 ranges.push(
186 multi_buffer_snapshot.anchor_in_buffer(anchors.start)?
187 ..multi_buffer_snapshot.anchor_in_buffer(anchors.end)?,
188 );
189 };
190
191 Some((color_index % accents_count, ranges))
192 })
193 .collect()
194}
195
196#[cfg(test)]
197mod tests {
198 use std::{cmp, sync::Arc, time::Duration};
199
200 use super::*;
201 use crate::{
202 DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
203 display_map::{DisplayRow, ToDisplayPoint},
204 editor_tests::init_test,
205 test::{
206 editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
207 },
208 };
209 use collections::HashSet;
210 use fs::FakeFs;
211 use gpui::UpdateGlobal as _;
212 use indoc::indoc;
213 use itertools::Itertools;
214 use language::{Capability, markdown_lang};
215 use languages::rust_lang;
216 use multi_buffer::{MultiBuffer, PathKey};
217 use pretty_assertions::assert_eq;
218 use project::Project;
219 use rope::Point;
220 use serde_json::json;
221 use settings::{AccentContent, SettingsStore};
222 use text::{Bias, OffsetRangeExt, ToOffset};
223 use theme_settings::ThemeStyleContent;
224
225 use util::{path, post_inc};
226
227 #[gpui::test]
228 async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) {
229 init_test(cx, |language_settings| {
230 language_settings.defaults.colorize_brackets = Some(true);
231 });
232 let mut cx = EditorLspTestContext::new(
233 Arc::into_inner(rust_lang()).unwrap(),
234 lsp::ServerCapabilities::default(),
235 cx,
236 )
237 .await;
238
239 cx.set_state(indoc! {r#"ˇuse std::{collections::HashMap, future::Future};
240
241fn main() {
242 let a = one((), { () }, ());
243 println!("{a}");
244 println!("{a}");
245 for i in 0..a {
246 println!("{i}");
247 }
248
249 let b = {
250 {
251 {
252 [([([([([([([([([([((), ())])])])])])])])])])]
253 }
254 }
255 };
256}
257
258#[rustfmt::skip]
259fn one(a: (), (): (), c: ()) -> usize { 1 }
260
261fn two<T>(a: HashMap<String, Vec<Option<T>>>) -> usize
262where
263 T: Future<Output = HashMap<String, Vec<Option<Box<()>>>>>,
264{
265 2
266}
267"#});
268 cx.executor().advance_clock(Duration::from_millis(100));
269 cx.executor().run_until_parked();
270
271 assert_eq!(
272 r#"use std::«1{collections::HashMap, future::Future}1»;
273
274fn main«1()1» «1{
275 let a = one«2(«3()3», «3{ «4()4» }3», «3()3»)2»;
276 println!«2("{a}")2»;
277 println!«2("{a}")2»;
278 for i in 0..a «2{
279 println!«3("{i}")3»;
280 }2»
281
282 let b = «2{
283 «3{
284 «4{
285 «5[«6(«7[«1(«2[«3(«4[«5(«6[«7(«1[«2(«3[«4(«5[«6(«7[«1(«2[«3(«4()4», «4()4»)3»]2»)1»]7»)6»]5»)4»]3»)2»]1»)7»]6»)5»]4»)3»]2»)1»]7»)6»]5»
286 }4»
287 }3»
288 }2»;
289}1»
290
291#«1[rustfmt::skip]1»
292fn one«1(a: «2()2», «2()2»: «2()2», c: «2()2»)1» -> usize «1{ 1 }1»
293
294fn two«1<T>1»«1(a: HashMap«2<String, Vec«3<Option«4<T>4»>3»>2»)1» -> usize
295where
296 T: Future«1<Output = HashMap«2<String, Vec«3<Option«4<Box«5<«6()6»>5»>4»>3»>2»>1»,
297«1{
298 2
299}1»
300
3011 hsla(207.80, 16.20%, 69.19%, 1.00)
3022 hsla(29.00, 54.00%, 65.88%, 1.00)
3033 hsla(286.00, 51.00%, 75.25%, 1.00)
3044 hsla(187.00, 47.00%, 59.22%, 1.00)
3055 hsla(355.00, 65.00%, 75.94%, 1.00)
3066 hsla(95.00, 38.00%, 62.00%, 1.00)
3077 hsla(39.00, 67.00%, 69.00%, 1.00)
308"#,
309 &bracket_colors_markup(&mut cx),
310 "All brackets should be colored based on their depth"
311 );
312 }
313
314 #[gpui::test]
315 async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
316 init_test(cx, |language_settings| {
317 language_settings.defaults.colorize_brackets = Some(true);
318 });
319 let editor = cx.add_window(|window, cx| {
320 let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
321 multi_buffer.update(cx, |multi_buffer, cx| {
322 multi_buffer
323 .as_singleton()
324 .unwrap()
325 .update(cx, |buffer, cx| {
326 buffer.set_language(Some(rust_lang()), cx);
327 });
328 });
329 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
330 });
331
332 cx.executor().advance_clock(Duration::from_millis(100));
333 cx.executor().run_until_parked();
334
335 assert_eq!(
336 "fn main«1()1» «1{}1»
3371 hsla(207.80, 16.20%, 69.19%, 1.00)
338",
339 editor
340 .update(cx, |editor, window, cx| {
341 editor_bracket_colors_markup(&editor.snapshot(window, cx))
342 })
343 .unwrap(),
344 "File-less buffer should still have its brackets colorized"
345 );
346 }
347
348 #[gpui::test]
349 async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
350 init_test(cx, |language_settings| {
351 language_settings.defaults.colorize_brackets = Some(true);
352 });
353 let mut cx = EditorLspTestContext::new(
354 Arc::into_inner(markdown_lang()).unwrap(),
355 lsp::ServerCapabilities::default(),
356 cx,
357 )
358 .await;
359
360 cx.set_state(indoc! {r#"ˇ[LLM-powered features](./ai/overview.md), [bring and configure your own API keys](./ai/llm-providers.md#use-your-own-keys)"#});
361 cx.executor().advance_clock(Duration::from_millis(100));
362 cx.executor().run_until_parked();
363
364 assert_eq!(
365 r#"«1[LLM-powered features]1»«1(./ai/overview.md)1», «1[bring and configure your own API keys]1»«1(./ai/llm-providers.md#use-your-own-keys)1»
3661 hsla(207.80, 16.20%, 69.19%, 1.00)
367"#,
368 &bracket_colors_markup(&mut cx),
369 "All markdown brackets should be colored based on their depth"
370 );
371
372 cx.set_state(indoc! {r#"ˇ{{}}"#});
373 cx.executor().advance_clock(Duration::from_millis(100));
374 cx.executor().run_until_parked();
375
376 assert_eq!(
377 r#"«1{«2{}2»}1»
3781 hsla(207.80, 16.20%, 69.19%, 1.00)
3792 hsla(29.00, 54.00%, 65.88%, 1.00)
380"#,
381 &bracket_colors_markup(&mut cx),
382 "All markdown brackets should be colored based on their depth, again"
383 );
384
385 cx.set_state(indoc! {r#"ˇ('')('')
386
387((''))('')
388
389('')((''))"#});
390 cx.executor().advance_clock(Duration::from_millis(100));
391 cx.executor().run_until_parked();
392
393 assert_eq!(
394 "«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",
395 &bracket_colors_markup(&mut cx),
396 "Markdown quote pairs should not interfere with parenthesis pairing"
397 );
398 }
399
400 #[gpui::test]
401 async fn test_markdown_brackets_in_multiple_hunks(cx: &mut gpui::TestAppContext) {
402 init_test(cx, |language_settings| {
403 language_settings.defaults.colorize_brackets = Some(true);
404 });
405 let mut cx = EditorLspTestContext::new(
406 Arc::into_inner(markdown_lang()).unwrap(),
407 lsp::ServerCapabilities::default(),
408 cx,
409 )
410 .await;
411
412 let rows = 100;
413 let footer = "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n";
414
415 let simple_brackets = (0..rows).map(|_| "ˇ[]\n").collect::<String>();
416 let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::<String>();
417 cx.set_state(&simple_brackets);
418 cx.update_editor(|editor, window, cx| {
419 editor.move_to_end(&MoveToEnd, window, cx);
420 });
421 cx.executor().advance_clock(Duration::from_millis(100));
422 cx.executor().run_until_parked();
423 assert_eq!(
424 format!("{simple_brackets_highlights}\n{footer}"),
425 bracket_colors_markup(&mut cx),
426 "Simple bracket pairs should be colored"
427 );
428
429 let paired_brackets = (0..rows).map(|_| "ˇ[]()\n").collect::<String>();
430 let paired_brackets_highlights = (0..rows).map(|_| "«1[]1»«1()1»\n").collect::<String>();
431 cx.set_state(&paired_brackets);
432 // Wait for reparse to complete after content change
433 cx.executor().advance_clock(Duration::from_millis(100));
434 cx.executor().run_until_parked();
435 cx.update_editor(|editor, _, cx| {
436 // Force invalidation of bracket cache after reparse
437 editor.colorize_brackets(true, cx);
438 });
439 // Scroll to beginning to fetch first chunks
440 cx.update_editor(|editor, window, cx| {
441 editor.move_to_beginning(&MoveToBeginning, window, cx);
442 });
443 cx.executor().advance_clock(Duration::from_millis(100));
444 cx.executor().run_until_parked();
445 // Scroll to end to fetch remaining chunks
446 cx.update_editor(|editor, window, cx| {
447 editor.move_to_end(&MoveToEnd, window, cx);
448 });
449 cx.executor().advance_clock(Duration::from_millis(100));
450 cx.executor().run_until_parked();
451 assert_eq!(
452 format!("{paired_brackets_highlights}\n{footer}"),
453 bracket_colors_markup(&mut cx),
454 "Paired bracket pairs should be colored"
455 );
456 }
457
458 #[gpui::test]
459 async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
460 init_test(cx, |language_settings| {
461 language_settings.defaults.colorize_brackets = Some(true);
462 });
463
464 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
465 language_registry.add(markdown_lang());
466 language_registry.add(rust_lang());
467
468 let mut cx = EditorTestContext::new(cx).await;
469 cx.update_buffer(|buffer, cx| {
470 buffer.set_language_registry(language_registry.clone());
471 buffer.set_language(Some(markdown_lang()), cx);
472 });
473
474 cx.set_state(indoc! {r#"
475 fn main() {
476 let v: Vec<Stringˇ> = vec![];
477 }
478 "#});
479 cx.executor().advance_clock(Duration::from_millis(100));
480 cx.executor().run_until_parked();
481
482 assert_eq!(
483 r#"fn main«1()1» «1{
484 let v: Vec<String> = vec!«2[]2»;
485}1»
486
4871 hsla(207.80, 16.20%, 69.19%, 1.00)
4882 hsla(29.00, 54.00%, 65.88%, 1.00)
489"#,
490 &bracket_colors_markup(&mut cx),
491 "Markdown does not colorize <> brackets"
492 );
493
494 cx.update_buffer(|buffer, cx| {
495 buffer.set_language(Some(rust_lang()), cx);
496 });
497 cx.executor().advance_clock(Duration::from_millis(100));
498 cx.executor().run_until_parked();
499
500 assert_eq!(
501 r#"fn main«1()1» «1{
502 let v: Vec«2<String>2» = vec!«2[]2»;
503}1»
504
5051 hsla(207.80, 16.20%, 69.19%, 1.00)
5062 hsla(29.00, 54.00%, 65.88%, 1.00)
507"#,
508 &bracket_colors_markup(&mut cx),
509 "After switching to Rust, <> brackets are now colorized"
510 );
511 }
512
513 #[gpui::test]
514 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
515 init_test(cx, |language_settings| {
516 language_settings.defaults.colorize_brackets = Some(true);
517 });
518 let mut cx = EditorLspTestContext::new(
519 Arc::into_inner(rust_lang()).unwrap(),
520 lsp::ServerCapabilities::default(),
521 cx,
522 )
523 .await;
524
525 cx.set_state(indoc! {r#"
526struct Foo<'a, T> {
527 data: Vec<Option<&'a T>>,
528}
529
530fn process_data() {
531 let map:ˇ
532}
533"#});
534
535 cx.update_editor(|editor, window, cx| {
536 editor.handle_input(" Result<", window, cx);
537 });
538 cx.executor().advance_clock(Duration::from_millis(100));
539 cx.executor().run_until_parked();
540 assert_eq!(
541 indoc! {r#"
542struct Foo«1<'a, T>1» «1{
543 data: Vec«2<Option«3<&'a T>3»>2»,
544}1»
545
546fn process_data«1()1» «1{
547 let map: Result<
548}1»
549
5501 hsla(207.80, 16.20%, 69.19%, 1.00)
5512 hsla(29.00, 54.00%, 65.88%, 1.00)
5523 hsla(286.00, 51.00%, 75.25%, 1.00)
553"#},
554 &bracket_colors_markup(&mut cx),
555 "Brackets without pairs should be ignored and not colored"
556 );
557
558 cx.update_editor(|editor, window, cx| {
559 editor.handle_input("Option<Foo<'_, ()", window, cx);
560 });
561 cx.executor().advance_clock(Duration::from_millis(100));
562 cx.executor().run_until_parked();
563 assert_eq!(
564 indoc! {r#"
565struct Foo«1<'a, T>1» «1{
566 data: Vec«2<Option«3<&'a T>3»>2»,
567}1»
568
569fn process_data«1()1» «1{
570 let map: Result<Option<Foo<'_, «2()2»
571}1»
572
5731 hsla(207.80, 16.20%, 69.19%, 1.00)
5742 hsla(29.00, 54.00%, 65.88%, 1.00)
5753 hsla(286.00, 51.00%, 75.25%, 1.00)
576"#},
577 &bracket_colors_markup(&mut cx),
578 );
579
580 cx.update_editor(|editor, window, cx| {
581 editor.handle_input(">", window, cx);
582 });
583 cx.executor().advance_clock(Duration::from_millis(100));
584 cx.executor().run_until_parked();
585 assert_eq!(
586 indoc! {r#"
587struct Foo«1<'a, T>1» «1{
588 data: Vec«2<Option«3<&'a T>3»>2»,
589}1»
590
591fn process_data«1()1» «1{
592 let map: Result<Option<Foo«2<'_, «3()3»>2»
593}1»
594
5951 hsla(207.80, 16.20%, 69.19%, 1.00)
5962 hsla(29.00, 54.00%, 65.88%, 1.00)
5973 hsla(286.00, 51.00%, 75.25%, 1.00)
598"#},
599 &bracket_colors_markup(&mut cx),
600 "When brackets start to get closed, inner brackets are re-colored based on their depth"
601 );
602
603 cx.update_editor(|editor, window, cx| {
604 editor.handle_input(">", window, cx);
605 });
606 cx.executor().advance_clock(Duration::from_millis(100));
607 cx.executor().run_until_parked();
608 assert_eq!(
609 indoc! {r#"
610struct Foo«1<'a, T>1» «1{
611 data: Vec«2<Option«3<&'a T>3»>2»,
612}1»
613
614fn process_data«1()1» «1{
615 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
616}1»
617
6181 hsla(207.80, 16.20%, 69.19%, 1.00)
6192 hsla(29.00, 54.00%, 65.88%, 1.00)
6203 hsla(286.00, 51.00%, 75.25%, 1.00)
6214 hsla(187.00, 47.00%, 59.22%, 1.00)
622"#},
623 &bracket_colors_markup(&mut cx),
624 );
625
626 cx.update_editor(|editor, window, cx| {
627 editor.handle_input(", ()> = unimplemented!();", window, cx);
628 });
629 cx.executor().advance_clock(Duration::from_millis(100));
630 cx.executor().run_until_parked();
631 assert_eq!(
632 indoc! {r#"
633struct Foo«1<'a, T>1» «1{
634 data: Vec«2<Option«3<&'a T>3»>2»,
635}1»
636
637fn process_data«1()1» «1{
638 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
639}1»
640
6411 hsla(207.80, 16.20%, 69.19%, 1.00)
6422 hsla(29.00, 54.00%, 65.88%, 1.00)
6433 hsla(286.00, 51.00%, 75.25%, 1.00)
6444 hsla(187.00, 47.00%, 59.22%, 1.00)
6455 hsla(355.00, 65.00%, 75.94%, 1.00)
646"#},
647 &bracket_colors_markup(&mut cx),
648 );
649 }
650
651 #[gpui::test]
652 async fn test_bracket_colorization_large_block(cx: &mut gpui::TestAppContext) {
653 // Each padded comment line is 27 bytes; 620 lines = 16740 bytes,
654 // just over MAX_BYTES_TO_QUERY (16 KB) with head/tail overhead.
655 let comment_lines = 620;
656
657 init_test(cx, |language_settings| {
658 language_settings.defaults.colorize_brackets = Some(true);
659 });
660 let mut cx = EditorLspTestContext::new(
661 Arc::into_inner(rust_lang()).unwrap(),
662 lsp::ServerCapabilities::default(),
663 cx,
664 )
665 .await;
666
667 cx.set_state(&separate_with_comment_lines(
668 indoc! {r#"
669mod foo {
670 ˇfn process_data_1() {
671 let map: Option<Vec<()>> = None;
672 }
673"#},
674 indoc! {r#"
675 fn process_data_2() {
676 let map: Option<Vec<()>> = None;
677 }
678}
679"#},
680 comment_lines,
681 ));
682
683 let colored_head = "mod foo «1{\n\
684 \x20 fn process_data_1«2()2» «2{\n\
685 \x20 let map: Option«3<Vec«4<«5()5»>4»>3» = None;\n\
686 \x20 }2»";
687 let uncolored_tail = " fn process_data_2() {\n\
688 \x20 let map: Option<Vec<()>> = None;\n\
689 \x20 }\n\
690 }1»";
691 let colored_tail = " fn process_data_2«2()2» «2{\n\
692 \x20 let map: Option«3<Vec«4<«5()5»>4»>3» = None;\n\
693 \x20 }2»\n\
694 }1»";
695
696 cx.executor().advance_clock(Duration::from_millis(100));
697 cx.executor().run_until_parked();
698 let markup = bracket_colors_markup(&mut cx);
699 let relevant = filter_bracket_relevant_lines(&markup);
700 assert_eq!(
701 relevant,
702 format!("{colored_head}\n{uncolored_tail}"),
703 "Top chunk: visible brackets should be colorized even when the \
704 enclosing block exceeds MAX_BYTES_TO_QUERY"
705 );
706
707 cx.update_editor(|editor, window, cx| {
708 editor.move_to_end(&MoveToEnd, window, cx);
709 editor.move_up(&MoveUp, window, cx);
710 });
711 cx.executor().advance_clock(Duration::from_millis(100));
712 cx.executor().run_until_parked();
713 let markup = bracket_colors_markup(&mut cx);
714 let relevant = filter_bracket_relevant_lines(&markup);
715 assert_eq!(
716 relevant,
717 format!("{colored_head}\n{colored_tail}"),
718 "After scrolling to bottom, both chunks should have bracket \
719 highlights across a large block"
720 );
721 }
722
723 #[gpui::test]
724 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
725 let comment_lines = 100;
726
727 init_test(cx, |language_settings| {
728 language_settings.defaults.colorize_brackets = Some(true);
729 });
730 let mut cx = EditorLspTestContext::new(
731 Arc::into_inner(rust_lang()).unwrap(),
732 lsp::ServerCapabilities::default(),
733 cx,
734 )
735 .await;
736
737 cx.set_state(&separate_with_comment_lines(
738 indoc! {r#"
739mod foo {
740 ˇfn process_data_1() {
741 let map: Option<Vec<()>> = None;
742 }
743"#},
744 indoc! {r#"
745 fn process_data_2() {
746 let map: Option<Vec<()>> = None;
747 }
748}
749"#},
750 comment_lines,
751 ));
752
753 cx.executor().advance_clock(Duration::from_millis(100));
754 cx.executor().run_until_parked();
755 assert_eq!(
756 &separate_with_comment_lines(
757 indoc! {r#"
758mod foo «1{
759 fn process_data_1«2()2» «2{
760 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
761 }2»
762"#},
763 indoc! {r#"
764 fn process_data_2() {
765 let map: Option<Vec<()>> = None;
766 }
767}1»
768
7691 hsla(207.80, 16.20%, 69.19%, 1.00)
7702 hsla(29.00, 54.00%, 65.88%, 1.00)
7713 hsla(286.00, 51.00%, 75.25%, 1.00)
7724 hsla(187.00, 47.00%, 59.22%, 1.00)
7735 hsla(355.00, 65.00%, 75.94%, 1.00)
774"#},
775 comment_lines,
776 ),
777 &bracket_colors_markup(&mut cx),
778 "First, the only visible chunk is getting the bracket highlights"
779 );
780
781 cx.update_editor(|editor, window, cx| {
782 editor.move_to_end(&MoveToEnd, window, cx);
783 editor.move_up(&MoveUp, window, cx);
784 });
785 cx.executor().advance_clock(Duration::from_millis(100));
786 cx.executor().run_until_parked();
787 assert_eq!(
788 &separate_with_comment_lines(
789 indoc! {r#"
790mod foo «1{
791 fn process_data_1«2()2» «2{
792 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
793 }2»
794"#},
795 indoc! {r#"
796 fn process_data_2«2()2» «2{
797 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
798 }2»
799}1»
800
8011 hsla(207.80, 16.20%, 69.19%, 1.00)
8022 hsla(29.00, 54.00%, 65.88%, 1.00)
8033 hsla(286.00, 51.00%, 75.25%, 1.00)
8044 hsla(187.00, 47.00%, 59.22%, 1.00)
8055 hsla(355.00, 65.00%, 75.94%, 1.00)
806"#},
807 comment_lines,
808 ),
809 &bracket_colors_markup(&mut cx),
810 "After scrolling to the bottom, both chunks should have the highlights"
811 );
812
813 cx.update_editor(|editor, window, cx| {
814 editor.handle_input("{{}}}", window, cx);
815 });
816 cx.executor().advance_clock(Duration::from_millis(100));
817 cx.executor().run_until_parked();
818 assert_eq!(
819 &separate_with_comment_lines(
820 indoc! {r#"
821mod foo «1{
822 fn process_data_1() {
823 let map: Option<Vec<()>> = None;
824 }
825"#},
826 indoc! {r#"
827 fn process_data_2«2()2» «2{
828 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
829 }
830 «3{«4{}4»}3»}2»}1»
831
8321 hsla(207.80, 16.20%, 69.19%, 1.00)
8332 hsla(29.00, 54.00%, 65.88%, 1.00)
8343 hsla(286.00, 51.00%, 75.25%, 1.00)
8354 hsla(187.00, 47.00%, 59.22%, 1.00)
8365 hsla(355.00, 65.00%, 75.94%, 1.00)
837"#},
838 comment_lines,
839 ),
840 &bracket_colors_markup(&mut cx),
841 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
842 );
843
844 cx.update_editor(|editor, window, cx| {
845 editor.move_to_beginning(&MoveToBeginning, window, cx);
846 });
847 cx.executor().advance_clock(Duration::from_millis(100));
848 cx.executor().run_until_parked();
849 assert_eq!(
850 &separate_with_comment_lines(
851 indoc! {r#"
852mod foo «1{
853 fn process_data_1«2()2» «2{
854 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
855 }2»
856"#},
857 indoc! {r#"
858 fn process_data_2«2()2» «2{
859 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
860 }
861 «3{«4{}4»}3»}2»}1»
862
8631 hsla(207.80, 16.20%, 69.19%, 1.00)
8642 hsla(29.00, 54.00%, 65.88%, 1.00)
8653 hsla(286.00, 51.00%, 75.25%, 1.00)
8664 hsla(187.00, 47.00%, 59.22%, 1.00)
8675 hsla(355.00, 65.00%, 75.94%, 1.00)
868"#},
869 comment_lines,
870 ),
871 &bracket_colors_markup(&mut cx),
872 "Scrolling back to top should re-colorize all chunks' brackets"
873 );
874
875 cx.update(|_, cx| {
876 SettingsStore::update_global(cx, |store, cx| {
877 store.update_user_settings(cx, |settings| {
878 settings.project.all_languages.defaults.colorize_brackets = Some(false);
879 });
880 });
881 });
882 cx.executor().run_until_parked();
883 assert_eq!(
884 &separate_with_comment_lines(
885 indoc! {r#"
886mod foo {
887 fn process_data_1() {
888 let map: Option<Vec<()>> = None;
889 }
890"#},
891 r#" fn process_data_2() {
892 let map: Option<Vec<()>> = None;
893 }
894 {{}}}}
895
896"#,
897 comment_lines,
898 ),
899 &bracket_colors_markup(&mut cx),
900 "Turning bracket colorization off should remove all bracket colors"
901 );
902
903 cx.update(|_, cx| {
904 SettingsStore::update_global(cx, |store, cx| {
905 store.update_user_settings(cx, |settings| {
906 settings.project.all_languages.defaults.colorize_brackets = Some(true);
907 });
908 });
909 });
910 cx.executor().run_until_parked();
911 assert_eq!(
912 &separate_with_comment_lines(
913 indoc! {r#"
914mod foo «1{
915 fn process_data_1«2()2» «2{
916 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
917 }2»
918"#},
919 r#" fn process_data_2() {
920 let map: Option<Vec<()>> = None;
921 }
922 {{}}}}1»
923
9241 hsla(207.80, 16.20%, 69.19%, 1.00)
9252 hsla(29.00, 54.00%, 65.88%, 1.00)
9263 hsla(286.00, 51.00%, 75.25%, 1.00)
9274 hsla(187.00, 47.00%, 59.22%, 1.00)
9285 hsla(355.00, 65.00%, 75.94%, 1.00)
929"#,
930 comment_lines,
931 ),
932 &bracket_colors_markup(&mut cx),
933 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
934 );
935 }
936
937 #[gpui::test]
938 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
939 init_test(cx, |language_settings| {
940 language_settings.defaults.colorize_brackets = Some(true);
941 });
942 let mut cx = EditorLspTestContext::new(
943 Arc::into_inner(rust_lang()).unwrap(),
944 lsp::ServerCapabilities::default(),
945 cx,
946 )
947 .await;
948
949 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
950 cx.set_state(indoc! {r#"ˇ
951 pub(crate) fn inlay_hints(
952 db: &RootDatabase,
953 file_id: FileId,
954 range_limit: Option<TextRange>,
955 config: &InlayHintsConfig,
956 ) -> Vec<InlayHint> {
957 let _p = tracing::info_span!("inlay_hints").entered();
958 let sema = Semantics::new(db);
959 let file_id = sema
960 .attach_first_edition(file_id)
961 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
962 let file = sema.parse(file_id);
963 let file = file.syntax();
964
965 let mut acc = Vec::new();
966
967 let Some(scope) = sema.scope(file) else {
968 return acc;
969 };
970 let famous_defs = FamousDefs(&sema, scope.krate());
971 let display_target = famous_defs.1.to_display_target(sema.db);
972
973 let ctx = &mut InlayHintCtx::default();
974 let mut hints = |event| {
975 if let Some(node) = handle_event(ctx, event) {
976 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
977 }
978 };
979 let mut preorder = file.preorder();
980 salsa::attach(sema.db, || {
981 while let Some(event) = preorder.next() {
982 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
983 {
984 preorder.skip_subtree();
985 continue;
986 }
987 hints(event);
988 }
989 });
990 if let Some(range_limit) = range_limit {
991 acc.retain(|hint| range_limit.contains_range(hint.range));
992 }
993 acc
994 }
995
996 #[derive(Default)]
997 struct InlayHintCtx {
998 lifetime_stacks: Vec<Vec<SmolStr>>,
999 extern_block_parent: Option<ast::ExternBlock>,
1000 }
1001
1002 pub(crate) fn inlay_hints_resolve(
1003 db: &RootDatabase,
1004 file_id: FileId,
1005 resolve_range: TextRange,
1006 hash: u64,
1007 config: &InlayHintsConfig,
1008 hasher: impl Fn(&InlayHint) -> u64,
1009 ) -> Option<InlayHint> {
1010 let _p = tracing::info_span!("inlay_hints_resolve").entered();
1011 let sema = Semantics::new(db);
1012 let file_id = sema
1013 .attach_first_edition(file_id)
1014 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
1015 let file = sema.parse(file_id);
1016 let file = file.syntax();
1017
1018 let scope = sema.scope(file)?;
1019 let famous_defs = FamousDefs(&sema, scope.krate());
1020 let mut acc = Vec::new();
1021
1022 let display_target = famous_defs.1.to_display_target(sema.db);
1023
1024 let ctx = &mut InlayHintCtx::default();
1025 let mut hints = |event| {
1026 if let Some(node) = handle_event(ctx, event) {
1027 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
1028 }
1029 };
1030
1031 let mut preorder = file.preorder();
1032 while let Some(event) = preorder.next() {
1033 // This can miss some hints that require the parent of the range to calculate
1034 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
1035 {
1036 preorder.skip_subtree();
1037 continue;
1038 }
1039 hints(event);
1040 }
1041 acc.into_iter().find(|hint| hasher(hint) == hash)
1042 }
1043
1044 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
1045 match node {
1046 WalkEvent::Enter(node) => {
1047 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
1048 let params = node
1049 .generic_param_list()
1050 .map(|it| {
1051 it.lifetime_params()
1052 .filter_map(|it| {
1053 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
1054 })
1055 .collect()
1056 })
1057 .unwrap_or_default();
1058 ctx.lifetime_stacks.push(params);
1059 }
1060 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
1061 ctx.extern_block_parent = Some(node);
1062 }
1063 Some(node)
1064 }
1065 WalkEvent::Leave(n) => {
1066 if ast::AnyHasGenericParams::can_cast(n.kind()) {
1067 ctx.lifetime_stacks.pop();
1068 }
1069 if ast::ExternBlock::can_cast(n.kind()) {
1070 ctx.extern_block_parent = None;
1071 }
1072 None
1073 }
1074 }
1075 }
1076
1077 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
1078 // HIR instead of the syntax tree.
1079 fn hints(
1080 hints: &mut Vec<InlayHint>,
1081 ctx: &mut InlayHintCtx,
1082 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
1083 config: &InlayHintsConfig,
1084 file_id: EditionedFileId,
1085 display_target: DisplayTarget,
1086 node: SyntaxNode,
1087 ) {
1088 closing_brace::hints(
1089 hints,
1090 sema,
1091 config,
1092 display_target,
1093 InRealFile { file_id, value: node.clone() },
1094 );
1095 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
1096 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
1097 }
1098
1099 match_ast! {
1100 match node {
1101 ast::Expr(expr) => {
1102 chaining::hints(hints, famous_defs, config, display_target, &expr);
1103 adjustment::hints(hints, famous_defs, config, display_target, &expr);
1104 match expr {
1105 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
1106 ast::Expr::MethodCallExpr(it) => {
1107 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
1108 }
1109 ast::Expr::ClosureExpr(it) => {
1110 closure_captures::hints(hints, famous_defs, config, it.clone());
1111 closure_ret::hints(hints, famous_defs, config, display_target, it)
1112 },
1113 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
1114 _ => Some(()),
1115 }
1116 },
1117 ast::Pat(it) => {
1118 binding_mode::hints(hints, famous_defs, config, &it);
1119 match it {
1120 ast::Pat::IdentPat(it) => {
1121 bind_pat::hints(hints, famous_defs, config, display_target, &it);
1122 }
1123 ast::Pat::RangePat(it) => {
1124 range_exclusive::hints(hints, famous_defs, config, it);
1125 }
1126 _ => {}
1127 }
1128 Some(())
1129 },
1130 ast::Item(it) => match it {
1131 ast::Item::Fn(it) => {
1132 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
1133 if let Some(extern_block) = &ctx.extern_block_parent {
1134 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
1135 }
1136 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
1137 },
1138 ast::Item::Static(it) => {
1139 if let Some(extern_block) = &ctx.extern_block_parent {
1140 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
1141 }
1142 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
1143 },
1144 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
1145 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
1146 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
1147 _ => None,
1148 },
1149 // trait object type elisions
1150 ast::Type(ty) => match ty {
1151 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
1152 ast::Type::PathType(path) => {
1153 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
1154 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
1155 Some(())
1156 },
1157 ast::Type::DynTraitType(dyn_) => {
1158 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
1159 Some(())
1160 },
1161 _ => Some(()),
1162 },
1163 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
1164 _ => Some(()),
1165 }
1166 };
1167 }
1168 "#});
1169 cx.executor().advance_clock(Duration::from_millis(100));
1170 cx.executor().run_until_parked();
1171
1172 let actual_ranges = cx.update_editor(|editor, window, cx| {
1173 editor
1174 .snapshot(window, cx)
1175 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)))
1176 });
1177
1178 let mut highlighted_brackets = HashMap::default();
1179 for (color, range) in actual_ranges.iter().cloned() {
1180 highlighted_brackets.insert(range, color);
1181 }
1182
1183 let last_bracket = actual_ranges
1184 .iter()
1185 .max_by_key(|(_, p)| p.end.row)
1186 .unwrap()
1187 .clone();
1188
1189 cx.update_editor(|editor, window, cx| {
1190 let was_scrolled = editor.set_scroll_position(
1191 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
1192 window,
1193 cx,
1194 );
1195 assert!(was_scrolled.0);
1196 });
1197 cx.executor().advance_clock(Duration::from_millis(100));
1198 cx.executor().run_until_parked();
1199
1200 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
1201 editor
1202 .snapshot(window, cx)
1203 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)))
1204 });
1205 let new_last_bracket = ranges_after_scrolling
1206 .iter()
1207 .max_by_key(|(_, p)| p.end.row)
1208 .unwrap()
1209 .clone();
1210
1211 assert_ne!(
1212 last_bracket, new_last_bracket,
1213 "After scrolling down, we should have highlighted more brackets"
1214 );
1215
1216 cx.update_editor(|editor, window, cx| {
1217 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
1218 assert!(was_scrolled.0);
1219 });
1220
1221 for _ in 0..200 {
1222 cx.update_editor(|editor, window, cx| {
1223 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
1224 });
1225 cx.executor().advance_clock(Duration::from_millis(100));
1226 cx.executor().run_until_parked();
1227
1228 let colored_brackets = cx.update_editor(|editor, window, cx| {
1229 editor
1230 .snapshot(window, cx)
1231 .all_text_highlight_ranges(&|key| {
1232 matches!(key, HighlightKey::ColorizeBracket(_))
1233 })
1234 });
1235 for (color, range) in colored_brackets.clone() {
1236 assert!(
1237 highlighted_brackets.entry(range).or_insert(color) == &color,
1238 "Colors should stay consistent while scrolling!"
1239 );
1240 }
1241
1242 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
1243 let scroll_position = snapshot.scroll_position();
1244 let visible_lines =
1245 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
1246 let visible_range = DisplayRow(scroll_position.y as u32)
1247 ..DisplayRow((scroll_position.y + visible_lines) as u32);
1248
1249 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
1250 colored_brackets
1251 .iter()
1252 .flat_map(|(_, range)| [range.start, range.end]),
1253 );
1254
1255 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
1256 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
1257 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
1258 }) {
1259 assert!(
1260 current_highlighted_bracket_set.contains(&highlight_range.start)
1261 || current_highlighted_bracket_set.contains(&highlight_range.end),
1262 "Should not lose highlights while scrolling in the visible range!"
1263 );
1264 }
1265
1266 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap();
1267 for bracket_match in buffer_snapshot
1268 .fetch_bracket_ranges(
1269 snapshot
1270 .display_point_to_point(
1271 DisplayPoint::new(visible_range.start, 0),
1272 Bias::Left,
1273 )
1274 .to_offset(&buffer_snapshot)
1275 ..snapshot
1276 .display_point_to_point(
1277 DisplayPoint::new(
1278 visible_range.end,
1279 snapshot.line_len(visible_range.end),
1280 ),
1281 Bias::Right,
1282 )
1283 .to_offset(&buffer_snapshot),
1284 None,
1285 )
1286 .iter()
1287 .flat_map(|entry| entry.1)
1288 .filter(|bracket_match| bracket_match.color_index.is_some())
1289 {
1290 let start = bracket_match.open_range.to_point(buffer_snapshot);
1291 let end = bracket_match.close_range.to_point(buffer_snapshot);
1292 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
1293 assert!(
1294 start_bracket.is_some(),
1295 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1296 buffer_snapshot
1297 .text_for_range(start.start..end.end)
1298 .collect::<String>(),
1299 start
1300 );
1301
1302 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1303 assert!(
1304 end_bracket.is_some(),
1305 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1306 buffer_snapshot
1307 .text_for_range(start.start..end.end)
1308 .collect::<String>(),
1309 start
1310 );
1311
1312 assert_eq!(
1313 start_bracket.unwrap().0,
1314 end_bracket.unwrap().0,
1315 "Bracket pair should be highlighted the same color!"
1316 )
1317 }
1318 }
1319 }
1320
1321 #[gpui::test]
1322 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1323 let comment_lines = 100;
1324
1325 init_test(cx, |language_settings| {
1326 language_settings.defaults.colorize_brackets = Some(true);
1327 });
1328 let fs = FakeFs::new(cx.background_executor.clone());
1329 fs.insert_tree(
1330 path!("/a"),
1331 json!({
1332 "main.rs": "fn main() {{()}}",
1333 "lib.rs": separate_with_comment_lines(
1334 indoc! {r#"
1335 mod foo {
1336 fn process_data_1() {
1337 let map: Option<Vec<()>> = None;
1338 // a
1339 // b
1340 // c
1341 }
1342 "#},
1343 indoc! {r#"
1344 fn process_data_2() {
1345 let other_map: Option<Vec<()>> = None;
1346 }
1347 }
1348 "#},
1349 comment_lines,
1350 )
1351 }),
1352 )
1353 .await;
1354
1355 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1356 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1357 language_registry.add(rust_lang());
1358
1359 let buffer_1 = project
1360 .update(cx, |project, cx| {
1361 project.open_local_buffer(path!("/a/lib.rs"), cx)
1362 })
1363 .await
1364 .unwrap();
1365 let buffer_2 = project
1366 .update(cx, |project, cx| {
1367 project.open_local_buffer(path!("/a/main.rs"), cx)
1368 })
1369 .await
1370 .unwrap();
1371
1372 let multi_buffer = cx.new(|cx| {
1373 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1374 multi_buffer.set_excerpts_for_path(
1375 PathKey::sorted(0),
1376 buffer_2.clone(),
1377 [Point::new(0, 0)..Point::new(1, 0)],
1378 0,
1379 cx,
1380 );
1381
1382 let excerpt_rows = 5;
1383 let rest_of_first_except_rows = 3;
1384 multi_buffer.set_excerpts_for_path(
1385 PathKey::sorted(1),
1386 buffer_1.clone(),
1387 [
1388 Point::new(0, 0)..Point::new(excerpt_rows, 0),
1389 Point::new(
1390 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1391 0,
1392 )
1393 ..Point::new(
1394 comment_lines as u32
1395 + excerpt_rows
1396 + rest_of_first_except_rows
1397 + excerpt_rows,
1398 0,
1399 ),
1400 ],
1401 0,
1402 cx,
1403 );
1404 multi_buffer
1405 });
1406
1407 let editor = cx.add_window(|window, cx| {
1408 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1409 });
1410 cx.executor().advance_clock(Duration::from_millis(100));
1411 cx.executor().run_until_parked();
1412
1413 let editor_snapshot = editor
1414 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1415 .unwrap();
1416 assert_eq!(
1417 indoc! {r#"
1418
1419
1420fn main«1()1» «1{«2{«3()3»}2»}1»
1421
1422
1423mod foo «1{
1424 fn process_data_1«2()2» «2{
1425 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1426 // a
1427 // b
1428 // c
1429
1430 fn process_data_2«2()2» «2{
1431 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1432 }2»
1433}1»
1434
14351 hsla(207.80, 16.20%, 69.19%, 1.00)
14362 hsla(29.00, 54.00%, 65.88%, 1.00)
14373 hsla(286.00, 51.00%, 75.25%, 1.00)
14384 hsla(187.00, 47.00%, 59.22%, 1.00)
14395 hsla(355.00, 65.00%, 75.94%, 1.00)
1440"#,},
1441 &editor_bracket_colors_markup(&editor_snapshot),
1442 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1443or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1444 );
1445
1446 editor
1447 .update(cx, |editor, window, cx| {
1448 editor.handle_input("{[]", window, cx);
1449 })
1450 .unwrap();
1451 cx.executor().advance_clock(Duration::from_millis(100));
1452 cx.executor().run_until_parked();
1453 let editor_snapshot = editor
1454 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1455 .unwrap();
1456 assert_eq!(
1457 indoc! {r#"
1458
1459
1460{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1461
1462
1463mod foo «1{
1464 fn process_data_1«2()2» «2{
1465 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1466 // a
1467 // b
1468 // c
1469
1470 fn process_data_2«2()2» «2{
1471 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1472 }2»
1473}1»
1474
14751 hsla(207.80, 16.20%, 69.19%, 1.00)
14762 hsla(29.00, 54.00%, 65.88%, 1.00)
14773 hsla(286.00, 51.00%, 75.25%, 1.00)
14784 hsla(187.00, 47.00%, 59.22%, 1.00)
14795 hsla(355.00, 65.00%, 75.94%, 1.00)
1480"#,},
1481 &editor_bracket_colors_markup(&editor_snapshot),
1482 );
1483
1484 cx.update(|cx| {
1485 let theme = cx.theme().name.clone();
1486 SettingsStore::update_global(cx, |store, cx| {
1487 store.update_user_settings(cx, |settings| {
1488 settings.theme.theme_overrides = HashMap::from_iter([(
1489 theme.to_string(),
1490 ThemeStyleContent {
1491 accents: vec![
1492 AccentContent(Some("#ff0000".to_string())),
1493 AccentContent(Some("#0000ff".to_string())),
1494 ],
1495 ..ThemeStyleContent::default()
1496 },
1497 )]);
1498 });
1499 });
1500 });
1501 cx.executor().advance_clock(Duration::from_millis(100));
1502 cx.executor().run_until_parked();
1503 let editor_snapshot = editor
1504 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1505 .unwrap();
1506 assert_eq!(
1507 indoc! {r#"
1508
1509
1510{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1511
1512
1513mod foo «1{
1514 fn process_data_1«2()2» «2{
1515 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1516 // a
1517 // b
1518 // c
1519
1520 fn process_data_2«2()2» «2{
1521 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1522 }2»
1523}1»
1524
15251 hsla(0.00, 100.00%, 78.12%, 1.00)
15262 hsla(240.00, 100.00%, 82.81%, 1.00)
1527"#,},
1528 &editor_bracket_colors_markup(&editor_snapshot),
1529 "After updating theme accents, the editor should update the bracket coloring"
1530 );
1531 }
1532
1533 #[gpui::test]
1534 async fn test_multi_buffer_close_excerpts(cx: &mut gpui::TestAppContext) {
1535 let comment_lines = 5;
1536
1537 init_test(cx, |language_settings| {
1538 language_settings.defaults.colorize_brackets = Some(true);
1539 });
1540 let fs = FakeFs::new(cx.background_executor.clone());
1541 fs.insert_tree(
1542 path!("/a"),
1543 json!({
1544 "lib.rs": separate_with_comment_lines(
1545 indoc! {r#"
1546 fn process_data_1() {
1547 let map: Option<Vec<()>> = None;
1548 }
1549 "#},
1550 indoc! {r#"
1551 fn process_data_2() {
1552 let other_map: Option<Vec<()>> = None;
1553 }
1554 "#},
1555 comment_lines,
1556 )
1557 }),
1558 )
1559 .await;
1560
1561 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1562 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1563 language_registry.add(rust_lang());
1564
1565 let buffer_1 = project
1566 .update(cx, |project, cx| {
1567 project.open_local_buffer(path!("/a/lib.rs"), cx)
1568 })
1569 .await
1570 .unwrap();
1571
1572 let second_excerpt_start = buffer_1.read_with(cx, |buffer, _| {
1573 let text = buffer.text();
1574 text.lines()
1575 .enumerate()
1576 .find(|(_, line)| line.contains("process_data_2"))
1577 .map(|(row, _)| row as u32)
1578 .unwrap()
1579 });
1580
1581 let multi_buffer = cx.new(|cx| {
1582 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1583 multi_buffer.set_excerpts_for_path(
1584 PathKey::sorted(0),
1585 buffer_1.clone(),
1586 [
1587 Point::new(0, 0)..Point::new(3, 0),
1588 Point::new(second_excerpt_start, 0)..Point::new(second_excerpt_start + 3, 0),
1589 ],
1590 0,
1591 cx,
1592 );
1593 multi_buffer
1594 });
1595
1596 let editor = cx.add_window(|window, cx| {
1597 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1598 });
1599 cx.executor().advance_clock(Duration::from_millis(100));
1600 cx.executor().run_until_parked();
1601
1602 let editor_snapshot = editor
1603 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1604 .unwrap();
1605 assert_eq!(
1606 concat!(
1607 "\n",
1608 "\n",
1609 "fn process_data_1\u{00ab}1()1\u{00bb} \u{00ab}1{\n",
1610 " let map: Option\u{00ab}2<Vec\u{00ab}3<\u{00ab}4()4\u{00bb}>3\u{00bb}>2\u{00bb} = None;\n",
1611 "}1\u{00bb}\n",
1612 "\n",
1613 "\n",
1614 "fn process_data_2\u{00ab}1()1\u{00bb} \u{00ab}1{\n",
1615 " let other_map: Option\u{00ab}2<Vec\u{00ab}3<\u{00ab}4()4\u{00bb}>3\u{00bb}>2\u{00bb} = None;\n",
1616 "}1\u{00bb}\n",
1617 "\n",
1618 "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n",
1619 "2 hsla(29.00, 54.00%, 65.88%, 1.00)\n",
1620 "3 hsla(286.00, 51.00%, 75.25%, 1.00)\n",
1621 "4 hsla(187.00, 47.00%, 59.22%, 1.00)\n",
1622 ),
1623 &editor_bracket_colors_markup(&editor_snapshot),
1624 "Two close excerpts from the same buffer (within same tree-sitter chunk) should both have bracket colors"
1625 );
1626 }
1627
1628 #[gpui::test]
1629 // reproduction of #47846
1630 async fn test_bracket_colorization_with_folds(cx: &mut gpui::TestAppContext) {
1631 init_test(cx, |language_settings| {
1632 language_settings.defaults.colorize_brackets = Some(true);
1633 });
1634 let mut cx = EditorLspTestContext::new(
1635 Arc::into_inner(rust_lang()).unwrap(),
1636 lsp::ServerCapabilities::default(),
1637 cx,
1638 )
1639 .await;
1640
1641 // Generate a large function body. When folded, this collapses
1642 // to a single display line, making small_function visible on screen.
1643 let mut big_body = String::new();
1644 for i in 0..700 {
1645 big_body.push_str(&format!(" let var_{i:04} = ({i});\n"));
1646 }
1647 let source = format!(
1648 "ˇfn big_function() {{\n{big_body}}}\n\nfn small_function() {{\n let x = (1, (2, 3));\n}}\n"
1649 );
1650
1651 cx.set_state(&source);
1652 cx.executor().advance_clock(Duration::from_millis(100));
1653 cx.executor().run_until_parked();
1654
1655 cx.update_editor(|editor, window, cx| {
1656 editor.fold_ranges(
1657 vec![Point::new(0, 0)..Point::new(701, 1)],
1658 false,
1659 window,
1660 cx,
1661 );
1662 });
1663 cx.executor().advance_clock(Duration::from_millis(100));
1664 cx.executor().run_until_parked();
1665
1666 assert_eq!(
1667 indoc! {r#"
1668⋯1»
1669
1670fn small_function«1()1» «1{
1671 let x = «2(1, «3(2, 3)3»)2»;
1672}1»
1673
16741 hsla(207.80, 16.20%, 69.19%, 1.00)
16752 hsla(29.00, 54.00%, 65.88%, 1.00)
16763 hsla(286.00, 51.00%, 75.25%, 1.00)
1677"#,},
1678 bracket_colors_markup(&mut cx),
1679 );
1680 }
1681
1682 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1683 let mut result = head.to_string();
1684 result.push('\n');
1685 for _ in 0..comment_lines {
1686 result.push_str("// padding padding padding\n");
1687 }
1688 result.push_str(tail);
1689 result
1690 }
1691
1692 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1693 cx.update_editor(|editor, window, cx| {
1694 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1695 })
1696 }
1697
1698 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1699 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1700 let mut offset = 0;
1701 for (row_idx, line) in text.lines().enumerate() {
1702 if row_idx < point.row().0 as usize {
1703 offset += line.len() + 1; // +1 for newline
1704 } else {
1705 offset += point.column() as usize;
1706 break;
1707 }
1708 }
1709 offset
1710 }
1711
1712 let actual_ranges = snapshot
1713 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)));
1714 let editor_text = snapshot.text();
1715
1716 let mut next_index = 1;
1717 let mut color_to_index = HashMap::default();
1718 let mut annotations = Vec::new();
1719 for (color, range) in &actual_ranges {
1720 let color_index = *color_to_index
1721 .entry(*color)
1722 .or_insert_with(|| post_inc(&mut next_index));
1723 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1724 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1725 let start_offset = display_point_to_offset(&editor_text, start);
1726 let end_offset = display_point_to_offset(&editor_text, end);
1727 let bracket_text = &editor_text[start_offset..end_offset];
1728 let bracket_char = bracket_text.chars().next().unwrap();
1729
1730 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1731 annotations.push((start_offset, format!("«{color_index}")));
1732 } else {
1733 annotations.push((end_offset, format!("{color_index}»")));
1734 }
1735 }
1736
1737 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1738 pos_a.cmp(pos_b).reverse().then_with(|| {
1739 let a_is_opening = text_a.starts_with('«');
1740 let b_is_opening = text_b.starts_with('«');
1741 match (a_is_opening, b_is_opening) {
1742 (true, false) => cmp::Ordering::Less,
1743 (false, true) => cmp::Ordering::Greater,
1744 _ => cmp::Ordering::Equal,
1745 }
1746 })
1747 });
1748 annotations.dedup();
1749
1750 let mut markup = editor_text;
1751 for (offset, text) in annotations {
1752 markup.insert_str(offset, &text);
1753 }
1754
1755 markup.push_str("\n");
1756 for (index, color) in color_to_index
1757 .iter()
1758 .map(|(color, index)| (*index, *color))
1759 .sorted_by_key(|(index, _)| *index)
1760 {
1761 markup.push_str(&format!("{index} {color}\n"));
1762 }
1763
1764 markup
1765 }
1766
1767 fn filter_bracket_relevant_lines(markup: &str) -> String {
1768 markup
1769 .lines()
1770 .filter(|line| {
1771 let trimmed = line.trim();
1772 !trimmed.is_empty()
1773 && !trimmed.starts_with("//")
1774 && !trimmed.starts_with("hsla(")
1775 && !trimmed.chars().next().is_some_and(|c| c.is_ascii_digit())
1776 })
1777 .collect::<Vec<_>>()
1778 .join("\n")
1779 }
1780}