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;
9use gpui::{Context, HighlightStyle};
10use itertools::Itertools;
11use language::language_settings;
12use multi_buffer::{Anchor, ExcerptId};
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.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 let anchors_in_multi_buffer = |current_excerpt: ExcerptId,
28 text_anchors: [text::Anchor; 4]|
29 -> Option<[Option<_>; 4]> {
30 multi_buffer_snapshot
31 .anchors_in_excerpt(current_excerpt, text_anchors)?
32 .collect_array()
33 };
34
35 let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold(
36 HashMap::default(),
37 |mut acc, (excerpt_id, (buffer, _, buffer_range))| {
38 let buffer_snapshot = buffer.read(cx).snapshot();
39 if language_settings::language_settings(
40 buffer_snapshot.language().map(|language| language.name()),
41 buffer_snapshot.file(),
42 cx,
43 )
44 .colorize_brackets
45 {
46 let fetched_chunks = self
47 .fetched_tree_sitter_chunks
48 .entry(excerpt_id)
49 .or_default();
50
51 let brackets_by_accent = buffer_snapshot
52 .fetch_bracket_ranges(
53 buffer_range.start..buffer_range.end,
54 Some(fetched_chunks),
55 )
56 .into_iter()
57 .flat_map(|(chunk_range, pairs)| {
58 if fetched_chunks.insert(chunk_range) {
59 pairs
60 } else {
61 Vec::new()
62 }
63 })
64 .filter_map(|pair| {
65 let color_index = pair.color_index?;
66
67 let buffer_open_range =
68 buffer_snapshot.anchor_range_around(pair.open_range);
69 let buffer_close_range =
70 buffer_snapshot.anchor_range_around(pair.close_range);
71 let [
72 buffer_open_range_start,
73 buffer_open_range_end,
74 buffer_close_range_start,
75 buffer_close_range_end,
76 ] = anchors_in_multi_buffer(
77 excerpt_id,
78 [
79 buffer_open_range.start,
80 buffer_open_range.end,
81 buffer_close_range.start,
82 buffer_close_range.end,
83 ],
84 )?;
85 let multi_buffer_open_range =
86 buffer_open_range_start.zip(buffer_open_range_end);
87 let multi_buffer_close_range =
88 buffer_close_range_start.zip(buffer_close_range_end);
89
90 let mut ranges = Vec::with_capacity(2);
91 if let Some((open_start, open_end)) = multi_buffer_open_range {
92 ranges.push(open_start..open_end);
93 }
94 if let Some((close_start, close_end)) = multi_buffer_close_range {
95 ranges.push(close_start..close_end);
96 }
97 if ranges.is_empty() {
98 None
99 } else {
100 Some((color_index % accents_count, ranges))
101 }
102 });
103
104 for (accent_number, new_ranges) in brackets_by_accent {
105 let ranges = acc
106 .entry(accent_number)
107 .or_insert_with(Vec::<Range<Anchor>>::new);
108
109 for new_range in new_ranges {
110 let i = ranges
111 .binary_search_by(|probe| {
112 probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
113 })
114 .unwrap_or_else(|i| i);
115 ranges.insert(i, new_range);
116 }
117 }
118 }
119
120 acc
121 },
122 );
123
124 if invalidate {
125 self.clear_highlights_with(
126 &mut |key| matches!(key, HighlightKey::ColorizeBracket(_)),
127 cx,
128 );
129 }
130
131 let editor_background = cx.theme().colors().editor_background;
132 let accents = cx.theme().accents().clone();
133 for (accent_number, bracket_highlights) in bracket_matches_by_accent {
134 let bracket_color = accents.color_for_index(accent_number as u32);
135 let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
136 let style = HighlightStyle {
137 color: Some(adjusted_color),
138 ..HighlightStyle::default()
139 };
140
141 self.highlight_text_key(
142 HighlightKey::ColorizeBracket(accent_number),
143 bracket_highlights,
144 style,
145 true,
146 cx,
147 );
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use std::{cmp, sync::Arc, time::Duration};
155
156 use super::*;
157 use crate::{
158 DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
159 display_map::{DisplayRow, ToDisplayPoint},
160 editor_tests::init_test,
161 test::{
162 editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
163 },
164 };
165 use collections::HashSet;
166 use fs::FakeFs;
167 use gpui::{AppContext as _, UpdateGlobal as _};
168 use indoc::indoc;
169 use itertools::Itertools;
170 use language::{Capability, markdown_lang};
171 use languages::rust_lang;
172 use multi_buffer::{MultiBuffer, PathKey};
173 use pretty_assertions::assert_eq;
174 use project::Project;
175 use rope::Point;
176 use serde_json::json;
177 use settings::{AccentContent, SettingsStore};
178 use text::{Bias, OffsetRangeExt, ToOffset};
179 use theme::ThemeStyleContent;
180
181 use util::{path, post_inc};
182
183 #[gpui::test]
184 async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) {
185 init_test(cx, |language_settings| {
186 language_settings.defaults.colorize_brackets = Some(true);
187 });
188 let mut cx = EditorLspTestContext::new(
189 Arc::into_inner(rust_lang()).unwrap(),
190 lsp::ServerCapabilities::default(),
191 cx,
192 )
193 .await;
194
195 cx.set_state(indoc! {r#"ˇuse std::{collections::HashMap, future::Future};
196
197fn main() {
198 let a = one((), { () }, ());
199 println!("{a}");
200 println!("{a}");
201 for i in 0..a {
202 println!("{i}");
203 }
204
205 let b = {
206 {
207 {
208 [([([([([([([([([([((), ())])])])])])])])])])]
209 }
210 }
211 };
212}
213
214#[rustfmt::skip]
215fn one(a: (), (): (), c: ()) -> usize { 1 }
216
217fn two<T>(a: HashMap<String, Vec<Option<T>>>) -> usize
218where
219 T: Future<Output = HashMap<String, Vec<Option<Box<()>>>>>,
220{
221 2
222}
223"#});
224 cx.executor().advance_clock(Duration::from_millis(100));
225 cx.executor().run_until_parked();
226
227 assert_eq!(
228 r#"use std::«1{collections::HashMap, future::Future}1»;
229
230fn main«1()1» «1{
231 let a = one«2(«3()3», «3{ «4()4» }3», «3()3»)2»;
232 println!«2("{a}")2»;
233 println!«2("{a}")2»;
234 for i in 0..a «2{
235 println!«3("{i}")3»;
236 }2»
237
238 let b = «2{
239 «3{
240 «4{
241 «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»
242 }4»
243 }3»
244 }2»;
245}1»
246
247#«1[rustfmt::skip]1»
248fn one«1(a: «2()2», «2()2»: «2()2», c: «2()2»)1» -> usize «1{ 1 }1»
249
250fn two«1<T>1»«1(a: HashMap«2<String, Vec«3<Option«4<T>4»>3»>2»)1» -> usize
251where
252 T: Future«1<Output = HashMap«2<String, Vec«3<Option«4<Box«5<«6()6»>5»>4»>3»>2»>1»,
253«1{
254 2
255}1»
256
2571 hsla(207.80, 16.20%, 69.19%, 1.00)
2582 hsla(29.00, 54.00%, 65.88%, 1.00)
2593 hsla(286.00, 51.00%, 75.25%, 1.00)
2604 hsla(187.00, 47.00%, 59.22%, 1.00)
2615 hsla(355.00, 65.00%, 75.94%, 1.00)
2626 hsla(95.00, 38.00%, 62.00%, 1.00)
2637 hsla(39.00, 67.00%, 69.00%, 1.00)
264"#,
265 &bracket_colors_markup(&mut cx),
266 "All brackets should be colored based on their depth"
267 );
268 }
269
270 #[gpui::test]
271 async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
272 init_test(cx, |language_settings| {
273 language_settings.defaults.colorize_brackets = Some(true);
274 });
275 let editor = cx.add_window(|window, cx| {
276 let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
277 multi_buffer.update(cx, |multi_buffer, cx| {
278 multi_buffer
279 .as_singleton()
280 .unwrap()
281 .update(cx, |buffer, cx| {
282 buffer.set_language(Some(rust_lang()), cx);
283 });
284 });
285 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
286 });
287
288 cx.executor().advance_clock(Duration::from_millis(100));
289 cx.executor().run_until_parked();
290
291 assert_eq!(
292 "fn main«1()1» «1{}1»
2931 hsla(207.80, 16.20%, 69.19%, 1.00)
294",
295 editor
296 .update(cx, |editor, window, cx| {
297 editor_bracket_colors_markup(&editor.snapshot(window, cx))
298 })
299 .unwrap(),
300 "File-less buffer should still have its brackets colorized"
301 );
302 }
303
304 #[gpui::test]
305 async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
306 init_test(cx, |language_settings| {
307 language_settings.defaults.colorize_brackets = Some(true);
308 });
309 let mut cx = EditorLspTestContext::new(
310 Arc::into_inner(markdown_lang()).unwrap(),
311 lsp::ServerCapabilities::default(),
312 cx,
313 )
314 .await;
315
316 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)"#});
317 cx.executor().advance_clock(Duration::from_millis(100));
318 cx.executor().run_until_parked();
319
320 assert_eq!(
321 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»
3221 hsla(207.80, 16.20%, 69.19%, 1.00)
323"#,
324 &bracket_colors_markup(&mut cx),
325 "All markdown brackets should be colored based on their depth"
326 );
327
328 cx.set_state(indoc! {r#"ˇ{{}}"#});
329 cx.executor().advance_clock(Duration::from_millis(100));
330 cx.executor().run_until_parked();
331
332 assert_eq!(
333 r#"«1{«2{}2»}1»
3341 hsla(207.80, 16.20%, 69.19%, 1.00)
3352 hsla(29.00, 54.00%, 65.88%, 1.00)
336"#,
337 &bracket_colors_markup(&mut cx),
338 "All markdown brackets should be colored based on their depth, again"
339 );
340 }
341
342 #[gpui::test]
343 async fn test_markdown_brackets_in_multiple_hunks(cx: &mut gpui::TestAppContext) {
344 init_test(cx, |language_settings| {
345 language_settings.defaults.colorize_brackets = Some(true);
346 });
347 let mut cx = EditorLspTestContext::new(
348 Arc::into_inner(markdown_lang()).unwrap(),
349 lsp::ServerCapabilities::default(),
350 cx,
351 )
352 .await;
353
354 let rows = 100;
355 let footer = "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n";
356
357 let simple_brackets = (0..rows).map(|_| "ˇ[]\n").collect::<String>();
358 let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::<String>();
359 cx.set_state(&simple_brackets);
360 cx.update_editor(|editor, window, cx| {
361 editor.move_to_end(&MoveToEnd, window, cx);
362 });
363 cx.executor().advance_clock(Duration::from_millis(100));
364 cx.executor().run_until_parked();
365 assert_eq!(
366 format!("{simple_brackets_highlights}\n{footer}"),
367 bracket_colors_markup(&mut cx),
368 "Simple bracket pairs should be colored"
369 );
370
371 let paired_brackets = (0..rows).map(|_| "ˇ[]()\n").collect::<String>();
372 let paired_brackets_highlights = (0..rows).map(|_| "«1[]1»«1()1»\n").collect::<String>();
373 cx.set_state(&paired_brackets);
374 // Wait for reparse to complete after content change
375 cx.executor().advance_clock(Duration::from_millis(100));
376 cx.executor().run_until_parked();
377 cx.update_editor(|editor, _, cx| {
378 // Force invalidation of bracket cache after reparse
379 editor.colorize_brackets(true, cx);
380 });
381 // Scroll to beginning to fetch first chunks
382 cx.update_editor(|editor, window, cx| {
383 editor.move_to_beginning(&MoveToBeginning, window, cx);
384 });
385 cx.executor().advance_clock(Duration::from_millis(100));
386 cx.executor().run_until_parked();
387 // Scroll to end to fetch remaining chunks
388 cx.update_editor(|editor, window, cx| {
389 editor.move_to_end(&MoveToEnd, window, cx);
390 });
391 cx.executor().advance_clock(Duration::from_millis(100));
392 cx.executor().run_until_parked();
393 assert_eq!(
394 format!("{paired_brackets_highlights}\n{footer}"),
395 bracket_colors_markup(&mut cx),
396 "Paired bracket pairs should be colored"
397 );
398 }
399
400 #[gpui::test]
401 async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
402 init_test(cx, |language_settings| {
403 language_settings.defaults.colorize_brackets = Some(true);
404 });
405
406 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
407 language_registry.add(markdown_lang());
408 language_registry.add(rust_lang());
409
410 let mut cx = EditorTestContext::new(cx).await;
411 cx.update_buffer(|buffer, cx| {
412 buffer.set_language_registry(language_registry.clone());
413 buffer.set_language(Some(markdown_lang()), cx);
414 });
415
416 cx.set_state(indoc! {r#"
417 fn main() {
418 let v: Vec<Stringˇ> = vec![];
419 }
420 "#});
421 cx.executor().advance_clock(Duration::from_millis(100));
422 cx.executor().run_until_parked();
423
424 assert_eq!(
425 r#"fn main«1()1» «1{
426 let v: Vec<String> = vec!«2[]2»;
427}1»
428
4291 hsla(207.80, 16.20%, 69.19%, 1.00)
4302 hsla(29.00, 54.00%, 65.88%, 1.00)
431"#,
432 &bracket_colors_markup(&mut cx),
433 "Markdown does not colorize <> brackets"
434 );
435
436 cx.update_buffer(|buffer, cx| {
437 buffer.set_language(Some(rust_lang()), cx);
438 });
439 cx.executor().advance_clock(Duration::from_millis(100));
440 cx.executor().run_until_parked();
441
442 assert_eq!(
443 r#"fn main«1()1» «1{
444 let v: Vec«2<String>2» = vec!«2[]2»;
445}1»
446
4471 hsla(207.80, 16.20%, 69.19%, 1.00)
4482 hsla(29.00, 54.00%, 65.88%, 1.00)
449"#,
450 &bracket_colors_markup(&mut cx),
451 "After switching to Rust, <> brackets are now colorized"
452 );
453 }
454
455 #[gpui::test]
456 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
457 init_test(cx, |language_settings| {
458 language_settings.defaults.colorize_brackets = Some(true);
459 });
460 let mut cx = EditorLspTestContext::new(
461 Arc::into_inner(rust_lang()).unwrap(),
462 lsp::ServerCapabilities::default(),
463 cx,
464 )
465 .await;
466
467 cx.set_state(indoc! {r#"
468struct Foo<'a, T> {
469 data: Vec<Option<&'a T>>,
470}
471
472fn process_data() {
473 let map:ˇ
474}
475"#});
476
477 cx.update_editor(|editor, window, cx| {
478 editor.handle_input(" Result<", window, cx);
479 });
480 cx.executor().advance_clock(Duration::from_millis(100));
481 cx.executor().run_until_parked();
482 assert_eq!(
483 indoc! {r#"
484struct Foo«1<'a, T>1» «1{
485 data: Vec«2<Option«3<&'a T>3»>2»,
486}1»
487
488fn process_data«1()1» «1{
489 let map: Result<
490}1»
491
4921 hsla(207.80, 16.20%, 69.19%, 1.00)
4932 hsla(29.00, 54.00%, 65.88%, 1.00)
4943 hsla(286.00, 51.00%, 75.25%, 1.00)
495"#},
496 &bracket_colors_markup(&mut cx),
497 "Brackets without pairs should be ignored and not colored"
498 );
499
500 cx.update_editor(|editor, window, cx| {
501 editor.handle_input("Option<Foo<'_, ()", window, cx);
502 });
503 cx.executor().advance_clock(Duration::from_millis(100));
504 cx.executor().run_until_parked();
505 assert_eq!(
506 indoc! {r#"
507struct Foo«1<'a, T>1» «1{
508 data: Vec«2<Option«3<&'a T>3»>2»,
509}1»
510
511fn process_data«1()1» «1{
512 let map: Result<Option<Foo<'_, «2()2»
513}1»
514
5151 hsla(207.80, 16.20%, 69.19%, 1.00)
5162 hsla(29.00, 54.00%, 65.88%, 1.00)
5173 hsla(286.00, 51.00%, 75.25%, 1.00)
518"#},
519 &bracket_colors_markup(&mut cx),
520 );
521
522 cx.update_editor(|editor, window, cx| {
523 editor.handle_input(">", window, cx);
524 });
525 cx.executor().advance_clock(Duration::from_millis(100));
526 cx.executor().run_until_parked();
527 assert_eq!(
528 indoc! {r#"
529struct Foo«1<'a, T>1» «1{
530 data: Vec«2<Option«3<&'a T>3»>2»,
531}1»
532
533fn process_data«1()1» «1{
534 let map: Result<Option<Foo«2<'_, «3()3»>2»
535}1»
536
5371 hsla(207.80, 16.20%, 69.19%, 1.00)
5382 hsla(29.00, 54.00%, 65.88%, 1.00)
5393 hsla(286.00, 51.00%, 75.25%, 1.00)
540"#},
541 &bracket_colors_markup(&mut cx),
542 "When brackets start to get closed, inner brackets are re-colored based on their depth"
543 );
544
545 cx.update_editor(|editor, window, cx| {
546 editor.handle_input(">", window, cx);
547 });
548 cx.executor().advance_clock(Duration::from_millis(100));
549 cx.executor().run_until_parked();
550 assert_eq!(
551 indoc! {r#"
552struct Foo«1<'a, T>1» «1{
553 data: Vec«2<Option«3<&'a T>3»>2»,
554}1»
555
556fn process_data«1()1» «1{
557 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
558}1»
559
5601 hsla(207.80, 16.20%, 69.19%, 1.00)
5612 hsla(29.00, 54.00%, 65.88%, 1.00)
5623 hsla(286.00, 51.00%, 75.25%, 1.00)
5634 hsla(187.00, 47.00%, 59.22%, 1.00)
564"#},
565 &bracket_colors_markup(&mut cx),
566 );
567
568 cx.update_editor(|editor, window, cx| {
569 editor.handle_input(", ()> = unimplemented!();", window, cx);
570 });
571 cx.executor().advance_clock(Duration::from_millis(100));
572 cx.executor().run_until_parked();
573 assert_eq!(
574 indoc! {r#"
575struct Foo«1<'a, T>1» «1{
576 data: Vec«2<Option«3<&'a T>3»>2»,
577}1»
578
579fn process_data«1()1» «1{
580 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
581}1»
582
5831 hsla(207.80, 16.20%, 69.19%, 1.00)
5842 hsla(29.00, 54.00%, 65.88%, 1.00)
5853 hsla(286.00, 51.00%, 75.25%, 1.00)
5864 hsla(187.00, 47.00%, 59.22%, 1.00)
5875 hsla(355.00, 65.00%, 75.94%, 1.00)
588"#},
589 &bracket_colors_markup(&mut cx),
590 );
591 }
592
593 #[gpui::test]
594 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
595 let comment_lines = 100;
596
597 init_test(cx, |language_settings| {
598 language_settings.defaults.colorize_brackets = Some(true);
599 });
600 let mut cx = EditorLspTestContext::new(
601 Arc::into_inner(rust_lang()).unwrap(),
602 lsp::ServerCapabilities::default(),
603 cx,
604 )
605 .await;
606
607 cx.set_state(&separate_with_comment_lines(
608 indoc! {r#"
609mod foo {
610 ˇfn process_data_1() {
611 let map: Option<Vec<()>> = None;
612 }
613"#},
614 indoc! {r#"
615 fn process_data_2() {
616 let map: Option<Vec<()>> = None;
617 }
618}
619"#},
620 comment_lines,
621 ));
622
623 cx.executor().advance_clock(Duration::from_millis(100));
624 cx.executor().run_until_parked();
625 assert_eq!(
626 &separate_with_comment_lines(
627 indoc! {r#"
628mod foo «1{
629 fn process_data_1«2()2» «2{
630 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
631 }2»
632"#},
633 indoc! {r#"
634 fn process_data_2() {
635 let map: Option<Vec<()>> = None;
636 }
637}1»
638
6391 hsla(207.80, 16.20%, 69.19%, 1.00)
6402 hsla(29.00, 54.00%, 65.88%, 1.00)
6413 hsla(286.00, 51.00%, 75.25%, 1.00)
6424 hsla(187.00, 47.00%, 59.22%, 1.00)
6435 hsla(355.00, 65.00%, 75.94%, 1.00)
644"#},
645 comment_lines,
646 ),
647 &bracket_colors_markup(&mut cx),
648 "First, the only visible chunk is getting the bracket highlights"
649 );
650
651 cx.update_editor(|editor, window, cx| {
652 editor.move_to_end(&MoveToEnd, window, cx);
653 editor.move_up(&MoveUp, window, cx);
654 });
655 cx.executor().advance_clock(Duration::from_millis(100));
656 cx.executor().run_until_parked();
657 assert_eq!(
658 &separate_with_comment_lines(
659 indoc! {r#"
660mod foo «1{
661 fn process_data_1«2()2» «2{
662 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
663 }2»
664"#},
665 indoc! {r#"
666 fn process_data_2«2()2» «2{
667 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
668 }2»
669}1»
670
6711 hsla(207.80, 16.20%, 69.19%, 1.00)
6722 hsla(29.00, 54.00%, 65.88%, 1.00)
6733 hsla(286.00, 51.00%, 75.25%, 1.00)
6744 hsla(187.00, 47.00%, 59.22%, 1.00)
6755 hsla(355.00, 65.00%, 75.94%, 1.00)
676"#},
677 comment_lines,
678 ),
679 &bracket_colors_markup(&mut cx),
680 "After scrolling to the bottom, both chunks should have the highlights"
681 );
682
683 cx.update_editor(|editor, window, cx| {
684 editor.handle_input("{{}}}", window, cx);
685 });
686 cx.executor().advance_clock(Duration::from_millis(100));
687 cx.executor().run_until_parked();
688 assert_eq!(
689 &separate_with_comment_lines(
690 indoc! {r#"
691mod foo «1{
692 fn process_data_1() {
693 let map: Option<Vec<()>> = None;
694 }
695"#},
696 indoc! {r#"
697 fn process_data_2«2()2» «2{
698 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
699 }
700 «3{«4{}4»}3»}2»}1»
701
7021 hsla(207.80, 16.20%, 69.19%, 1.00)
7032 hsla(29.00, 54.00%, 65.88%, 1.00)
7043 hsla(286.00, 51.00%, 75.25%, 1.00)
7054 hsla(187.00, 47.00%, 59.22%, 1.00)
7065 hsla(355.00, 65.00%, 75.94%, 1.00)
707"#},
708 comment_lines,
709 ),
710 &bracket_colors_markup(&mut cx),
711 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
712 );
713
714 cx.update_editor(|editor, window, cx| {
715 editor.move_to_beginning(&MoveToBeginning, window, cx);
716 });
717 cx.executor().advance_clock(Duration::from_millis(100));
718 cx.executor().run_until_parked();
719 assert_eq!(
720 &separate_with_comment_lines(
721 indoc! {r#"
722mod foo «1{
723 fn process_data_1«2()2» «2{
724 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
725 }2»
726"#},
727 indoc! {r#"
728 fn process_data_2«2()2» «2{
729 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
730 }
731 «3{«4{}4»}3»}2»}1»
732
7331 hsla(207.80, 16.20%, 69.19%, 1.00)
7342 hsla(29.00, 54.00%, 65.88%, 1.00)
7353 hsla(286.00, 51.00%, 75.25%, 1.00)
7364 hsla(187.00, 47.00%, 59.22%, 1.00)
7375 hsla(355.00, 65.00%, 75.94%, 1.00)
738"#},
739 comment_lines,
740 ),
741 &bracket_colors_markup(&mut cx),
742 "Scrolling back to top should re-colorize all chunks' brackets"
743 );
744
745 cx.update(|_, cx| {
746 SettingsStore::update_global(cx, |store, cx| {
747 store.update_user_settings(cx, |settings| {
748 settings.project.all_languages.defaults.colorize_brackets = Some(false);
749 });
750 });
751 });
752 assert_eq!(
753 &separate_with_comment_lines(
754 indoc! {r#"
755mod foo {
756 fn process_data_1() {
757 let map: Option<Vec<()>> = None;
758 }
759"#},
760 r#" fn process_data_2() {
761 let map: Option<Vec<()>> = None;
762 }
763 {{}}}}
764
765"#,
766 comment_lines,
767 ),
768 &bracket_colors_markup(&mut cx),
769 "Turning bracket colorization off should remove all bracket colors"
770 );
771
772 cx.update(|_, cx| {
773 SettingsStore::update_global(cx, |store, cx| {
774 store.update_user_settings(cx, |settings| {
775 settings.project.all_languages.defaults.colorize_brackets = Some(true);
776 });
777 });
778 });
779 assert_eq!(
780 &separate_with_comment_lines(
781 indoc! {r#"
782mod foo «1{
783 fn process_data_1«2()2» «2{
784 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
785 }2»
786"#},
787 r#" fn process_data_2() {
788 let map: Option<Vec<()>> = None;
789 }
790 {{}}}}1»
791
7921 hsla(207.80, 16.20%, 69.19%, 1.00)
7932 hsla(29.00, 54.00%, 65.88%, 1.00)
7943 hsla(286.00, 51.00%, 75.25%, 1.00)
7954 hsla(187.00, 47.00%, 59.22%, 1.00)
7965 hsla(355.00, 65.00%, 75.94%, 1.00)
797"#,
798 comment_lines,
799 ),
800 &bracket_colors_markup(&mut cx),
801 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
802 );
803 }
804
805 #[gpui::test]
806 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
807 init_test(cx, |language_settings| {
808 language_settings.defaults.colorize_brackets = Some(true);
809 });
810 let mut cx = EditorLspTestContext::new(
811 Arc::into_inner(rust_lang()).unwrap(),
812 lsp::ServerCapabilities::default(),
813 cx,
814 )
815 .await;
816
817 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
818 cx.set_state(indoc! {r#"ˇ
819 pub(crate) fn inlay_hints(
820 db: &RootDatabase,
821 file_id: FileId,
822 range_limit: Option<TextRange>,
823 config: &InlayHintsConfig,
824 ) -> Vec<InlayHint> {
825 let _p = tracing::info_span!("inlay_hints").entered();
826 let sema = Semantics::new(db);
827 let file_id = sema
828 .attach_first_edition(file_id)
829 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
830 let file = sema.parse(file_id);
831 let file = file.syntax();
832
833 let mut acc = Vec::new();
834
835 let Some(scope) = sema.scope(file) else {
836 return acc;
837 };
838 let famous_defs = FamousDefs(&sema, scope.krate());
839 let display_target = famous_defs.1.to_display_target(sema.db);
840
841 let ctx = &mut InlayHintCtx::default();
842 let mut hints = |event| {
843 if let Some(node) = handle_event(ctx, event) {
844 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
845 }
846 };
847 let mut preorder = file.preorder();
848 salsa::attach(sema.db, || {
849 while let Some(event) = preorder.next() {
850 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
851 {
852 preorder.skip_subtree();
853 continue;
854 }
855 hints(event);
856 }
857 });
858 if let Some(range_limit) = range_limit {
859 acc.retain(|hint| range_limit.contains_range(hint.range));
860 }
861 acc
862 }
863
864 #[derive(Default)]
865 struct InlayHintCtx {
866 lifetime_stacks: Vec<Vec<SmolStr>>,
867 extern_block_parent: Option<ast::ExternBlock>,
868 }
869
870 pub(crate) fn inlay_hints_resolve(
871 db: &RootDatabase,
872 file_id: FileId,
873 resolve_range: TextRange,
874 hash: u64,
875 config: &InlayHintsConfig,
876 hasher: impl Fn(&InlayHint) -> u64,
877 ) -> Option<InlayHint> {
878 let _p = tracing::info_span!("inlay_hints_resolve").entered();
879 let sema = Semantics::new(db);
880 let file_id = sema
881 .attach_first_edition(file_id)
882 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
883 let file = sema.parse(file_id);
884 let file = file.syntax();
885
886 let scope = sema.scope(file)?;
887 let famous_defs = FamousDefs(&sema, scope.krate());
888 let mut acc = Vec::new();
889
890 let display_target = famous_defs.1.to_display_target(sema.db);
891
892 let ctx = &mut InlayHintCtx::default();
893 let mut hints = |event| {
894 if let Some(node) = handle_event(ctx, event) {
895 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
896 }
897 };
898
899 let mut preorder = file.preorder();
900 while let Some(event) = preorder.next() {
901 // This can miss some hints that require the parent of the range to calculate
902 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
903 {
904 preorder.skip_subtree();
905 continue;
906 }
907 hints(event);
908 }
909 acc.into_iter().find(|hint| hasher(hint) == hash)
910 }
911
912 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
913 match node {
914 WalkEvent::Enter(node) => {
915 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
916 let params = node
917 .generic_param_list()
918 .map(|it| {
919 it.lifetime_params()
920 .filter_map(|it| {
921 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
922 })
923 .collect()
924 })
925 .unwrap_or_default();
926 ctx.lifetime_stacks.push(params);
927 }
928 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
929 ctx.extern_block_parent = Some(node);
930 }
931 Some(node)
932 }
933 WalkEvent::Leave(n) => {
934 if ast::AnyHasGenericParams::can_cast(n.kind()) {
935 ctx.lifetime_stacks.pop();
936 }
937 if ast::ExternBlock::can_cast(n.kind()) {
938 ctx.extern_block_parent = None;
939 }
940 None
941 }
942 }
943 }
944
945 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
946 // HIR instead of the syntax tree.
947 fn hints(
948 hints: &mut Vec<InlayHint>,
949 ctx: &mut InlayHintCtx,
950 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
951 config: &InlayHintsConfig,
952 file_id: EditionedFileId,
953 display_target: DisplayTarget,
954 node: SyntaxNode,
955 ) {
956 closing_brace::hints(
957 hints,
958 sema,
959 config,
960 display_target,
961 InRealFile { file_id, value: node.clone() },
962 );
963 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
964 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
965 }
966
967 match_ast! {
968 match node {
969 ast::Expr(expr) => {
970 chaining::hints(hints, famous_defs, config, display_target, &expr);
971 adjustment::hints(hints, famous_defs, config, display_target, &expr);
972 match expr {
973 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
974 ast::Expr::MethodCallExpr(it) => {
975 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
976 }
977 ast::Expr::ClosureExpr(it) => {
978 closure_captures::hints(hints, famous_defs, config, it.clone());
979 closure_ret::hints(hints, famous_defs, config, display_target, it)
980 },
981 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
982 _ => Some(()),
983 }
984 },
985 ast::Pat(it) => {
986 binding_mode::hints(hints, famous_defs, config, &it);
987 match it {
988 ast::Pat::IdentPat(it) => {
989 bind_pat::hints(hints, famous_defs, config, display_target, &it);
990 }
991 ast::Pat::RangePat(it) => {
992 range_exclusive::hints(hints, famous_defs, config, it);
993 }
994 _ => {}
995 }
996 Some(())
997 },
998 ast::Item(it) => match it {
999 ast::Item::Fn(it) => {
1000 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
1001 if let Some(extern_block) = &ctx.extern_block_parent {
1002 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
1003 }
1004 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
1005 },
1006 ast::Item::Static(it) => {
1007 if let Some(extern_block) = &ctx.extern_block_parent {
1008 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
1009 }
1010 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
1011 },
1012 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
1013 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
1014 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
1015 _ => None,
1016 },
1017 // trait object type elisions
1018 ast::Type(ty) => match ty {
1019 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
1020 ast::Type::PathType(path) => {
1021 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
1022 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
1023 Some(())
1024 },
1025 ast::Type::DynTraitType(dyn_) => {
1026 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
1027 Some(())
1028 },
1029 _ => Some(()),
1030 },
1031 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
1032 _ => Some(()),
1033 }
1034 };
1035 }
1036 "#});
1037 cx.executor().advance_clock(Duration::from_millis(100));
1038 cx.executor().run_until_parked();
1039
1040 let actual_ranges = cx.update_editor(|editor, window, cx| {
1041 editor
1042 .snapshot(window, cx)
1043 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)))
1044 });
1045
1046 let mut highlighted_brackets = HashMap::default();
1047 for (color, range) in actual_ranges.iter().cloned() {
1048 highlighted_brackets.insert(range, color);
1049 }
1050
1051 let last_bracket = actual_ranges
1052 .iter()
1053 .max_by_key(|(_, p)| p.end.row)
1054 .unwrap()
1055 .clone();
1056
1057 cx.update_editor(|editor, window, cx| {
1058 let was_scrolled = editor.set_scroll_position(
1059 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
1060 window,
1061 cx,
1062 );
1063 assert!(was_scrolled.0);
1064 });
1065 cx.executor().advance_clock(Duration::from_millis(100));
1066 cx.executor().run_until_parked();
1067
1068 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
1069 editor
1070 .snapshot(window, cx)
1071 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)))
1072 });
1073 let new_last_bracket = ranges_after_scrolling
1074 .iter()
1075 .max_by_key(|(_, p)| p.end.row)
1076 .unwrap()
1077 .clone();
1078
1079 assert_ne!(
1080 last_bracket, new_last_bracket,
1081 "After scrolling down, we should have highlighted more brackets"
1082 );
1083
1084 cx.update_editor(|editor, window, cx| {
1085 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
1086 assert!(was_scrolled.0);
1087 });
1088
1089 for _ in 0..200 {
1090 cx.update_editor(|editor, window, cx| {
1091 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
1092 });
1093 cx.executor().advance_clock(Duration::from_millis(100));
1094 cx.executor().run_until_parked();
1095
1096 let colored_brackets = cx.update_editor(|editor, window, cx| {
1097 editor
1098 .snapshot(window, cx)
1099 .all_text_highlight_ranges(&|key| {
1100 matches!(key, HighlightKey::ColorizeBracket(_))
1101 })
1102 });
1103 for (color, range) in colored_brackets.clone() {
1104 assert!(
1105 highlighted_brackets.entry(range).or_insert(color) == &color,
1106 "Colors should stay consistent while scrolling!"
1107 );
1108 }
1109
1110 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
1111 let scroll_position = snapshot.scroll_position();
1112 let visible_lines =
1113 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
1114 let visible_range = DisplayRow(scroll_position.y as u32)
1115 ..DisplayRow((scroll_position.y + visible_lines) as u32);
1116
1117 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
1118 colored_brackets
1119 .iter()
1120 .flat_map(|(_, range)| [range.start, range.end]),
1121 );
1122
1123 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
1124 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
1125 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
1126 }) {
1127 assert!(
1128 current_highlighted_bracket_set.contains(&highlight_range.start)
1129 || current_highlighted_bracket_set.contains(&highlight_range.end),
1130 "Should not lose highlights while scrolling in the visible range!"
1131 );
1132 }
1133
1134 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
1135 for bracket_match in buffer_snapshot
1136 .fetch_bracket_ranges(
1137 snapshot
1138 .display_point_to_point(
1139 DisplayPoint::new(visible_range.start, 0),
1140 Bias::Left,
1141 )
1142 .to_offset(&buffer_snapshot)
1143 ..snapshot
1144 .display_point_to_point(
1145 DisplayPoint::new(
1146 visible_range.end,
1147 snapshot.line_len(visible_range.end),
1148 ),
1149 Bias::Right,
1150 )
1151 .to_offset(&buffer_snapshot),
1152 None,
1153 )
1154 .iter()
1155 .flat_map(|entry| entry.1)
1156 .filter(|bracket_match| bracket_match.color_index.is_some())
1157 {
1158 let start = bracket_match.open_range.to_point(buffer_snapshot);
1159 let end = bracket_match.close_range.to_point(buffer_snapshot);
1160 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
1161 assert!(
1162 start_bracket.is_some(),
1163 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1164 buffer_snapshot
1165 .text_for_range(start.start..end.end)
1166 .collect::<String>(),
1167 start
1168 );
1169
1170 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1171 assert!(
1172 end_bracket.is_some(),
1173 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1174 buffer_snapshot
1175 .text_for_range(start.start..end.end)
1176 .collect::<String>(),
1177 start
1178 );
1179
1180 assert_eq!(
1181 start_bracket.unwrap().0,
1182 end_bracket.unwrap().0,
1183 "Bracket pair should be highlighted the same color!"
1184 )
1185 }
1186 }
1187 }
1188
1189 #[gpui::test]
1190 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1191 let comment_lines = 100;
1192
1193 init_test(cx, |language_settings| {
1194 language_settings.defaults.colorize_brackets = Some(true);
1195 });
1196 let fs = FakeFs::new(cx.background_executor.clone());
1197 fs.insert_tree(
1198 path!("/a"),
1199 json!({
1200 "main.rs": "fn main() {{()}}",
1201 "lib.rs": separate_with_comment_lines(
1202 indoc! {r#"
1203 mod foo {
1204 fn process_data_1() {
1205 let map: Option<Vec<()>> = None;
1206 // a
1207 // b
1208 // c
1209 }
1210 "#},
1211 indoc! {r#"
1212 fn process_data_2() {
1213 let other_map: Option<Vec<()>> = None;
1214 }
1215 }
1216 "#},
1217 comment_lines,
1218 )
1219 }),
1220 )
1221 .await;
1222
1223 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1224 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1225 language_registry.add(rust_lang());
1226
1227 let buffer_1 = project
1228 .update(cx, |project, cx| {
1229 project.open_local_buffer(path!("/a/lib.rs"), cx)
1230 })
1231 .await
1232 .unwrap();
1233 let buffer_2 = project
1234 .update(cx, |project, cx| {
1235 project.open_local_buffer(path!("/a/main.rs"), cx)
1236 })
1237 .await
1238 .unwrap();
1239
1240 let multi_buffer = cx.new(|cx| {
1241 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1242 multi_buffer.set_excerpts_for_path(
1243 PathKey::sorted(0),
1244 buffer_2.clone(),
1245 [Point::new(0, 0)..Point::new(1, 0)],
1246 0,
1247 cx,
1248 );
1249
1250 let excerpt_rows = 5;
1251 let rest_of_first_except_rows = 3;
1252 multi_buffer.set_excerpts_for_path(
1253 PathKey::sorted(1),
1254 buffer_1.clone(),
1255 [
1256 Point::new(0, 0)..Point::new(excerpt_rows, 0),
1257 Point::new(
1258 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1259 0,
1260 )
1261 ..Point::new(
1262 comment_lines as u32
1263 + excerpt_rows
1264 + rest_of_first_except_rows
1265 + excerpt_rows,
1266 0,
1267 ),
1268 ],
1269 0,
1270 cx,
1271 );
1272 multi_buffer
1273 });
1274
1275 let editor = cx.add_window(|window, cx| {
1276 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1277 });
1278 cx.executor().advance_clock(Duration::from_millis(100));
1279 cx.executor().run_until_parked();
1280
1281 let editor_snapshot = editor
1282 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1283 .unwrap();
1284 assert_eq!(
1285 indoc! {r#"
1286
1287
1288fn main«1()1» «1{«2{«3()3»}2»}1»
1289
1290
1291mod foo «1{
1292 fn process_data_1«2()2» «2{
1293 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1294 // a
1295 // b
1296 // c
1297
1298 fn process_data_2«2()2» «2{
1299 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1300 }2»
1301}1»
1302
13031 hsla(207.80, 16.20%, 69.19%, 1.00)
13042 hsla(29.00, 54.00%, 65.88%, 1.00)
13053 hsla(286.00, 51.00%, 75.25%, 1.00)
13064 hsla(187.00, 47.00%, 59.22%, 1.00)
13075 hsla(355.00, 65.00%, 75.94%, 1.00)
1308"#,},
1309 &editor_bracket_colors_markup(&editor_snapshot),
1310 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1311or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1312 );
1313
1314 editor
1315 .update(cx, |editor, window, cx| {
1316 editor.handle_input("{[]", window, cx);
1317 })
1318 .unwrap();
1319 cx.executor().advance_clock(Duration::from_millis(100));
1320 cx.executor().run_until_parked();
1321 let editor_snapshot = editor
1322 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1323 .unwrap();
1324 assert_eq!(
1325 indoc! {r#"
1326
1327
1328{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1329
1330
1331mod foo «1{
1332 fn process_data_1«2()2» «2{
1333 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1334 // a
1335 // b
1336 // c
1337
1338 fn process_data_2«2()2» «2{
1339 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1340 }2»
1341}1»
1342
13431 hsla(207.80, 16.20%, 69.19%, 1.00)
13442 hsla(29.00, 54.00%, 65.88%, 1.00)
13453 hsla(286.00, 51.00%, 75.25%, 1.00)
13464 hsla(187.00, 47.00%, 59.22%, 1.00)
13475 hsla(355.00, 65.00%, 75.94%, 1.00)
1348"#,},
1349 &editor_bracket_colors_markup(&editor_snapshot),
1350 );
1351
1352 cx.update(|cx| {
1353 let theme = cx.theme().name.clone();
1354 SettingsStore::update_global(cx, |store, cx| {
1355 store.update_user_settings(cx, |settings| {
1356 settings.theme.theme_overrides = HashMap::from_iter([(
1357 theme.to_string(),
1358 ThemeStyleContent {
1359 accents: vec![
1360 AccentContent(Some("#ff0000".to_string())),
1361 AccentContent(Some("#0000ff".to_string())),
1362 ],
1363 ..ThemeStyleContent::default()
1364 },
1365 )]);
1366 });
1367 });
1368 });
1369 cx.executor().advance_clock(Duration::from_millis(100));
1370 cx.executor().run_until_parked();
1371 let editor_snapshot = editor
1372 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1373 .unwrap();
1374 assert_eq!(
1375 indoc! {r#"
1376
1377
1378{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1379
1380
1381mod foo «1{
1382 fn process_data_1«2()2» «2{
1383 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1384 // a
1385 // b
1386 // c
1387
1388 fn process_data_2«2()2» «2{
1389 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1390 }2»
1391}1»
1392
13931 hsla(0.00, 100.00%, 78.12%, 1.00)
13942 hsla(240.00, 100.00%, 82.81%, 1.00)
1395"#,},
1396 &editor_bracket_colors_markup(&editor_snapshot),
1397 "After updating theme accents, the editor should update the bracket coloring"
1398 );
1399 }
1400
1401 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1402 let mut result = head.to_string();
1403 result.push_str("\n");
1404 result.push_str(&"//\n".repeat(comment_lines));
1405 result.push_str(tail);
1406 result
1407 }
1408
1409 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1410 cx.update_editor(|editor, window, cx| {
1411 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1412 })
1413 }
1414
1415 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1416 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1417 let mut offset = 0;
1418 for (row_idx, line) in text.lines().enumerate() {
1419 if row_idx < point.row().0 as usize {
1420 offset += line.len() + 1; // +1 for newline
1421 } else {
1422 offset += point.column() as usize;
1423 break;
1424 }
1425 }
1426 offset
1427 }
1428
1429 let actual_ranges = snapshot
1430 .all_text_highlight_ranges(&|key| matches!(key, HighlightKey::ColorizeBracket(_)));
1431 let editor_text = snapshot.text();
1432
1433 let mut next_index = 1;
1434 let mut color_to_index = HashMap::default();
1435 let mut annotations = Vec::new();
1436 for (color, range) in &actual_ranges {
1437 let color_index = *color_to_index
1438 .entry(*color)
1439 .or_insert_with(|| post_inc(&mut next_index));
1440 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1441 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1442 let start_offset = display_point_to_offset(&editor_text, start);
1443 let end_offset = display_point_to_offset(&editor_text, end);
1444 let bracket_text = &editor_text[start_offset..end_offset];
1445 let bracket_char = bracket_text.chars().next().unwrap();
1446
1447 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1448 annotations.push((start_offset, format!("«{color_index}")));
1449 } else {
1450 annotations.push((end_offset, format!("{color_index}»")));
1451 }
1452 }
1453
1454 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1455 pos_a.cmp(pos_b).reverse().then_with(|| {
1456 let a_is_opening = text_a.starts_with('«');
1457 let b_is_opening = text_b.starts_with('«');
1458 match (a_is_opening, b_is_opening) {
1459 (true, false) => cmp::Ordering::Less,
1460 (false, true) => cmp::Ordering::Greater,
1461 _ => cmp::Ordering::Equal,
1462 }
1463 })
1464 });
1465 annotations.dedup();
1466
1467 let mut markup = editor_text;
1468 for (offset, text) in annotations {
1469 markup.insert_str(offset, &text);
1470 }
1471
1472 markup.push_str("\n");
1473 for (index, color) in color_to_index
1474 .iter()
1475 .map(|(color, index)| (*index, *color))
1476 .sorted_by_key(|(index, _)| *index)
1477 {
1478 markup.push_str(&format!("{index} {color}\n"));
1479 }
1480
1481 markup
1482 }
1483}