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