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