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