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