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