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;
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_snapshot = buffer.read(cx).snapshot();
50 if language_settings::language_settings(
51 buffer_snapshot.language().map(|language| language.name()),
52 buffer_snapshot.file(),
53 cx,
54 )
55 .colorize_brackets
56 {
57 let fetched_chunks = self
58 .fetched_tree_sitter_chunks
59 .entry(excerpt_id)
60 .or_default();
61
62 let brackets_by_accent = buffer_snapshot
63 .fetch_bracket_ranges(
64 buffer_range.start..buffer_range.end,
65 Some(fetched_chunks),
66 )
67 .into_iter()
68 .flat_map(|(chunk_range, pairs)| {
69 if fetched_chunks.insert(chunk_range) {
70 pairs
71 } else {
72 Vec::new()
73 }
74 })
75 .filter_map(|pair| {
76 let color_index = pair.color_index?;
77
78 let buffer_open_range = buffer_snapshot
79 .anchor_before(pair.open_range.start)
80 ..buffer_snapshot.anchor_after(pair.open_range.end);
81 let buffer_close_range = buffer_snapshot
82 .anchor_before(pair.close_range.start)
83 ..buffer_snapshot.anchor_after(pair.close_range.end);
84 let [
85 buffer_open_range_start,
86 buffer_open_range_end,
87 buffer_close_range_start,
88 buffer_close_range_end,
89 ] = anchors_in_multi_buffer(
90 excerpt_id,
91 [
92 buffer_open_range.start,
93 buffer_open_range.end,
94 buffer_close_range.start,
95 buffer_close_range.end,
96 ],
97 )?;
98 let multi_buffer_open_range =
99 buffer_open_range_start.zip(buffer_open_range_end);
100 let multi_buffer_close_range =
101 buffer_close_range_start.zip(buffer_close_range_end);
102
103 let mut ranges = Vec::with_capacity(2);
104 if let Some((open_start, open_end)) = multi_buffer_open_range {
105 ranges.push(open_start..open_end);
106 }
107 if let Some((close_start, close_end)) = multi_buffer_close_range {
108 ranges.push(close_start..close_end);
109 }
110 if ranges.is_empty() {
111 None
112 } else {
113 Some((color_index % accents_count, ranges))
114 }
115 });
116
117 for (accent_number, new_ranges) in brackets_by_accent {
118 let ranges = acc
119 .entry(accent_number)
120 .or_insert_with(Vec::<Range<Anchor>>::new);
121
122 for new_range in new_ranges {
123 let i = ranges
124 .binary_search_by(|probe| {
125 probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
126 })
127 .unwrap_or_else(|i| i);
128 ranges.insert(i, new_range);
129 }
130 }
131 }
132
133 acc
134 },
135 );
136
137 if invalidate {
138 self.clear_highlights::<ColorizedBracketsHighlight>(cx);
139 }
140
141 let editor_background = cx.theme().colors().editor_background;
142 for (accent_number, bracket_highlights) in bracket_matches_by_accent {
143 let bracket_color = cx.theme().accents().color_for_index(accent_number as u32);
144 let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
145 let style = HighlightStyle {
146 color: Some(adjusted_color),
147 ..HighlightStyle::default()
148 };
149
150 self.highlight_text_key::<ColorizedBracketsHighlight>(
151 accent_number,
152 bracket_highlights,
153 style,
154 true,
155 cx,
156 );
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use std::{cmp, sync::Arc, time::Duration};
164
165 use super::*;
166 use crate::{
167 DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
168 display_map::{DisplayRow, ToDisplayPoint},
169 editor_tests::init_test,
170 test::{
171 editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
172 },
173 };
174 use collections::HashSet;
175 use fs::FakeFs;
176 use gpui::{AppContext as _, UpdateGlobal as _};
177 use indoc::indoc;
178 use itertools::Itertools;
179 use language::{Capability, markdown_lang};
180 use languages::rust_lang;
181 use multi_buffer::{ExcerptRange, MultiBuffer};
182 use pretty_assertions::assert_eq;
183 use project::Project;
184 use rope::Point;
185 use serde_json::json;
186 use settings::{AccentContent, SettingsStore};
187 use text::{Bias, OffsetRangeExt, ToOffset};
188 use theme::ThemeStyleContent;
189
190 use util::{path, post_inc};
191
192 #[gpui::test]
193 async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) {
194 init_test(cx, |language_settings| {
195 language_settings.defaults.colorize_brackets = Some(true);
196 });
197 let mut cx = EditorLspTestContext::new(
198 Arc::into_inner(rust_lang()).unwrap(),
199 lsp::ServerCapabilities::default(),
200 cx,
201 )
202 .await;
203
204 cx.set_state(indoc! {r#"ˇuse std::{collections::HashMap, future::Future};
205
206fn main() {
207 let a = one((), { () }, ());
208 println!("{a}");
209 println!("{a}");
210 for i in 0..a {
211 println!("{i}");
212 }
213
214 let b = {
215 {
216 {
217 [([([([([([([([([([((), ())])])])])])])])])])]
218 }
219 }
220 };
221}
222
223#[rustfmt::skip]
224fn one(a: (), (): (), c: ()) -> usize { 1 }
225
226fn two<T>(a: HashMap<String, Vec<Option<T>>>) -> usize
227where
228 T: Future<Output = HashMap<String, Vec<Option<Box<()>>>>>,
229{
230 2
231}
232"#});
233 cx.executor().advance_clock(Duration::from_millis(100));
234 cx.executor().run_until_parked();
235
236 assert_eq!(
237 r#"use std::«1{collections::HashMap, future::Future}1»;
238
239fn main«1()1» «1{
240 let a = one«2(«3()3», «3{ «4()4» }3», «3()3»)2»;
241 println!«2("{a}")2»;
242 println!«2("{a}")2»;
243 for i in 0..a «2{
244 println!«3("{i}")3»;
245 }2»
246
247 let b = «2{
248 «3{
249 «4{
250 «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»
251 }4»
252 }3»
253 }2»;
254}1»
255
256#«1[rustfmt::skip]1»
257fn one«1(a: «2()2», «2()2»: «2()2», c: «2()2»)1» -> usize «1{ 1 }1»
258
259fn two«1<T>1»«1(a: HashMap«2<String, Vec«3<Option«4<T>4»>3»>2»)1» -> usize
260where
261 T: Future«1<Output = HashMap«2<String, Vec«3<Option«4<Box«5<«6()6»>5»>4»>3»>2»>1»,
262«1{
263 2
264}1»
265
2661 hsla(207.80, 16.20%, 69.19%, 1.00)
2672 hsla(29.00, 54.00%, 65.88%, 1.00)
2683 hsla(286.00, 51.00%, 75.25%, 1.00)
2694 hsla(187.00, 47.00%, 59.22%, 1.00)
2705 hsla(355.00, 65.00%, 75.94%, 1.00)
2716 hsla(95.00, 38.00%, 62.00%, 1.00)
2727 hsla(39.00, 67.00%, 69.00%, 1.00)
273"#,
274 &bracket_colors_markup(&mut cx),
275 "All brackets should be colored based on their depth"
276 );
277 }
278
279 #[gpui::test]
280 async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
281 init_test(cx, |language_settings| {
282 language_settings.defaults.colorize_brackets = Some(true);
283 });
284 let editor = cx.add_window(|window, cx| {
285 let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
286 multi_buffer.update(cx, |multi_buffer, cx| {
287 multi_buffer
288 .as_singleton()
289 .unwrap()
290 .update(cx, |buffer, cx| {
291 buffer.set_language(Some(rust_lang()), cx);
292 });
293 });
294 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
295 });
296
297 cx.executor().advance_clock(Duration::from_millis(100));
298 cx.executor().run_until_parked();
299
300 assert_eq!(
301 "fn main«1()1» «1{}1»
3021 hsla(207.80, 16.20%, 69.19%, 1.00)
303",
304 editor
305 .update(cx, |editor, window, cx| {
306 editor_bracket_colors_markup(&editor.snapshot(window, cx))
307 })
308 .unwrap(),
309 "File-less buffer should still have its brackets colorized"
310 );
311 }
312
313 #[gpui::test]
314 async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
315 init_test(cx, |language_settings| {
316 language_settings.defaults.colorize_brackets = Some(true);
317 });
318 let mut cx = EditorLspTestContext::new(
319 Arc::into_inner(markdown_lang()).unwrap(),
320 lsp::ServerCapabilities::default(),
321 cx,
322 )
323 .await;
324
325 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)"#});
326 cx.executor().advance_clock(Duration::from_millis(100));
327 cx.executor().run_until_parked();
328
329 assert_eq!(
330 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»
3311 hsla(207.80, 16.20%, 69.19%, 1.00)
332"#,
333 &bracket_colors_markup(&mut cx),
334 "All markdown brackets should be colored based on their depth"
335 );
336
337 cx.set_state(indoc! {r#"ˇ{{}}"#});
338 cx.executor().advance_clock(Duration::from_millis(100));
339 cx.executor().run_until_parked();
340
341 assert_eq!(
342 r#"«1{«2{}2»}1»
3431 hsla(207.80, 16.20%, 69.19%, 1.00)
3442 hsla(29.00, 54.00%, 65.88%, 1.00)
345"#,
346 &bracket_colors_markup(&mut cx),
347 "All markdown brackets should be colored based on their depth, again"
348 );
349 }
350
351 #[gpui::test]
352 async fn test_markdown_brackets_in_multiple_hunks(cx: &mut gpui::TestAppContext) {
353 init_test(cx, |language_settings| {
354 language_settings.defaults.colorize_brackets = Some(true);
355 });
356 let mut cx = EditorLspTestContext::new(
357 Arc::into_inner(markdown_lang()).unwrap(),
358 lsp::ServerCapabilities::default(),
359 cx,
360 )
361 .await;
362
363 let rows = 100;
364 let footer = "1 hsla(207.80, 16.20%, 69.19%, 1.00)\n";
365
366 let simple_brackets = (0..rows).map(|_| "ˇ[]\n").collect::<String>();
367 let simple_brackets_highlights = (0..rows).map(|_| "«1[]1»\n").collect::<String>();
368 cx.set_state(&simple_brackets);
369 cx.update_editor(|editor, window, cx| {
370 editor.move_to_end(&MoveToEnd, window, cx);
371 });
372 cx.executor().advance_clock(Duration::from_millis(100));
373 cx.executor().run_until_parked();
374 assert_eq!(
375 format!("{simple_brackets_highlights}\n{footer}"),
376 bracket_colors_markup(&mut cx),
377 "Simple bracket pairs should be colored"
378 );
379
380 let paired_brackets = (0..rows).map(|_| "ˇ[]()\n").collect::<String>();
381 let paired_brackets_highlights = (0..rows).map(|_| "«1[]1»«1()1»\n").collect::<String>();
382 cx.set_state(&paired_brackets);
383 // Wait for reparse to complete after content change
384 cx.executor().advance_clock(Duration::from_millis(100));
385 cx.executor().run_until_parked();
386 cx.update_editor(|editor, _, cx| {
387 // Force invalidation of bracket cache after reparse
388 editor.colorize_brackets(true, cx);
389 });
390 // Scroll to beginning to fetch first chunks
391 cx.update_editor(|editor, window, cx| {
392 editor.move_to_beginning(&MoveToBeginning, window, cx);
393 });
394 cx.executor().advance_clock(Duration::from_millis(100));
395 cx.executor().run_until_parked();
396 // Scroll to end to fetch remaining chunks
397 cx.update_editor(|editor, window, cx| {
398 editor.move_to_end(&MoveToEnd, window, cx);
399 });
400 cx.executor().advance_clock(Duration::from_millis(100));
401 cx.executor().run_until_parked();
402 assert_eq!(
403 format!("{paired_brackets_highlights}\n{footer}"),
404 bracket_colors_markup(&mut cx),
405 "Paired bracket pairs should be colored"
406 );
407 }
408
409 #[gpui::test]
410 async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
411 init_test(cx, |language_settings| {
412 language_settings.defaults.colorize_brackets = Some(true);
413 });
414
415 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
416 language_registry.add(markdown_lang());
417 language_registry.add(rust_lang());
418
419 let mut cx = EditorTestContext::new(cx).await;
420 cx.update_buffer(|buffer, cx| {
421 buffer.set_language_registry(language_registry.clone());
422 buffer.set_language(Some(markdown_lang()), cx);
423 });
424
425 cx.set_state(indoc! {r#"
426 fn main() {
427 let v: Vec<Stringˇ> = vec![];
428 }
429 "#});
430 cx.executor().advance_clock(Duration::from_millis(100));
431 cx.executor().run_until_parked();
432
433 assert_eq!(
434 r#"fn main«1()1» «1{
435 let v: Vec<String> = vec!«2[]2»;
436}1»
437
4381 hsla(207.80, 16.20%, 69.19%, 1.00)
4392 hsla(29.00, 54.00%, 65.88%, 1.00)
440"#,
441 &bracket_colors_markup(&mut cx),
442 "Markdown does not colorize <> brackets"
443 );
444
445 cx.update_buffer(|buffer, cx| {
446 buffer.set_language(Some(rust_lang()), cx);
447 });
448 cx.executor().advance_clock(Duration::from_millis(100));
449 cx.executor().run_until_parked();
450
451 assert_eq!(
452 r#"fn main«1()1» «1{
453 let v: Vec«2<String>2» = vec!«2[]2»;
454}1»
455
4561 hsla(207.80, 16.20%, 69.19%, 1.00)
4572 hsla(29.00, 54.00%, 65.88%, 1.00)
458"#,
459 &bracket_colors_markup(&mut cx),
460 "After switching to Rust, <> brackets are now colorized"
461 );
462 }
463
464 #[gpui::test]
465 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
466 init_test(cx, |language_settings| {
467 language_settings.defaults.colorize_brackets = Some(true);
468 });
469 let mut cx = EditorLspTestContext::new(
470 Arc::into_inner(rust_lang()).unwrap(),
471 lsp::ServerCapabilities::default(),
472 cx,
473 )
474 .await;
475
476 cx.set_state(indoc! {r#"
477struct Foo<'a, T> {
478 data: Vec<Option<&'a T>>,
479}
480
481fn process_data() {
482 let map:ˇ
483}
484"#});
485
486 cx.update_editor(|editor, window, cx| {
487 editor.handle_input(" Result<", window, cx);
488 });
489 cx.executor().advance_clock(Duration::from_millis(100));
490 cx.executor().run_until_parked();
491 assert_eq!(
492 indoc! {r#"
493struct Foo«1<'a, T>1» «1{
494 data: Vec«2<Option«3<&'a T>3»>2»,
495}1»
496
497fn process_data«1()1» «1{
498 let map: Result<
499}1»
500
5011 hsla(207.80, 16.20%, 69.19%, 1.00)
5022 hsla(29.00, 54.00%, 65.88%, 1.00)
5033 hsla(286.00, 51.00%, 75.25%, 1.00)
504"#},
505 &bracket_colors_markup(&mut cx),
506 "Brackets without pairs should be ignored and not colored"
507 );
508
509 cx.update_editor(|editor, window, cx| {
510 editor.handle_input("Option<Foo<'_, ()", window, cx);
511 });
512 cx.executor().advance_clock(Duration::from_millis(100));
513 cx.executor().run_until_parked();
514 assert_eq!(
515 indoc! {r#"
516struct Foo«1<'a, T>1» «1{
517 data: Vec«2<Option«3<&'a T>3»>2»,
518}1»
519
520fn process_data«1()1» «1{
521 let map: Result<Option<Foo<'_, «2()2»
522}1»
523
5241 hsla(207.80, 16.20%, 69.19%, 1.00)
5252 hsla(29.00, 54.00%, 65.88%, 1.00)
5263 hsla(286.00, 51.00%, 75.25%, 1.00)
527"#},
528 &bracket_colors_markup(&mut cx),
529 );
530
531 cx.update_editor(|editor, window, cx| {
532 editor.handle_input(">", window, cx);
533 });
534 cx.executor().advance_clock(Duration::from_millis(100));
535 cx.executor().run_until_parked();
536 assert_eq!(
537 indoc! {r#"
538struct Foo«1<'a, T>1» «1{
539 data: Vec«2<Option«3<&'a T>3»>2»,
540}1»
541
542fn process_data«1()1» «1{
543 let map: Result<Option<Foo«2<'_, «3()3»>2»
544}1»
545
5461 hsla(207.80, 16.20%, 69.19%, 1.00)
5472 hsla(29.00, 54.00%, 65.88%, 1.00)
5483 hsla(286.00, 51.00%, 75.25%, 1.00)
549"#},
550 &bracket_colors_markup(&mut cx),
551 "When brackets start to get closed, inner brackets are re-colored based on their depth"
552 );
553
554 cx.update_editor(|editor, window, cx| {
555 editor.handle_input(">", window, cx);
556 });
557 cx.executor().advance_clock(Duration::from_millis(100));
558 cx.executor().run_until_parked();
559 assert_eq!(
560 indoc! {r#"
561struct Foo«1<'a, T>1» «1{
562 data: Vec«2<Option«3<&'a T>3»>2»,
563}1»
564
565fn process_data«1()1» «1{
566 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
567}1»
568
5691 hsla(207.80, 16.20%, 69.19%, 1.00)
5702 hsla(29.00, 54.00%, 65.88%, 1.00)
5713 hsla(286.00, 51.00%, 75.25%, 1.00)
5724 hsla(187.00, 47.00%, 59.22%, 1.00)
573"#},
574 &bracket_colors_markup(&mut cx),
575 );
576
577 cx.update_editor(|editor, window, cx| {
578 editor.handle_input(", ()> = unimplemented!();", window, cx);
579 });
580 cx.executor().advance_clock(Duration::from_millis(100));
581 cx.executor().run_until_parked();
582 assert_eq!(
583 indoc! {r#"
584struct Foo«1<'a, T>1» «1{
585 data: Vec«2<Option«3<&'a T>3»>2»,
586}1»
587
588fn process_data«1()1» «1{
589 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
590}1»
591
5921 hsla(207.80, 16.20%, 69.19%, 1.00)
5932 hsla(29.00, 54.00%, 65.88%, 1.00)
5943 hsla(286.00, 51.00%, 75.25%, 1.00)
5954 hsla(187.00, 47.00%, 59.22%, 1.00)
5965 hsla(355.00, 65.00%, 75.94%, 1.00)
597"#},
598 &bracket_colors_markup(&mut cx),
599 );
600 }
601
602 #[gpui::test]
603 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
604 let comment_lines = 100;
605
606 init_test(cx, |language_settings| {
607 language_settings.defaults.colorize_brackets = Some(true);
608 });
609 let mut cx = EditorLspTestContext::new(
610 Arc::into_inner(rust_lang()).unwrap(),
611 lsp::ServerCapabilities::default(),
612 cx,
613 )
614 .await;
615
616 cx.set_state(&separate_with_comment_lines(
617 indoc! {r#"
618mod foo {
619 ˇfn process_data_1() {
620 let map: Option<Vec<()>> = None;
621 }
622"#},
623 indoc! {r#"
624 fn process_data_2() {
625 let map: Option<Vec<()>> = None;
626 }
627}
628"#},
629 comment_lines,
630 ));
631
632 cx.executor().advance_clock(Duration::from_millis(100));
633 cx.executor().run_until_parked();
634 assert_eq!(
635 &separate_with_comment_lines(
636 indoc! {r#"
637mod foo «1{
638 fn process_data_1«2()2» «2{
639 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
640 }2»
641"#},
642 indoc! {r#"
643 fn process_data_2() {
644 let map: Option<Vec<()>> = None;
645 }
646}1»
647
6481 hsla(207.80, 16.20%, 69.19%, 1.00)
6492 hsla(29.00, 54.00%, 65.88%, 1.00)
6503 hsla(286.00, 51.00%, 75.25%, 1.00)
6514 hsla(187.00, 47.00%, 59.22%, 1.00)
6525 hsla(355.00, 65.00%, 75.94%, 1.00)
653"#},
654 comment_lines,
655 ),
656 &bracket_colors_markup(&mut cx),
657 "First, the only visible chunk is getting the bracket highlights"
658 );
659
660 cx.update_editor(|editor, window, cx| {
661 editor.move_to_end(&MoveToEnd, window, cx);
662 editor.move_up(&MoveUp, window, cx);
663 });
664 cx.executor().advance_clock(Duration::from_millis(100));
665 cx.executor().run_until_parked();
666 assert_eq!(
667 &separate_with_comment_lines(
668 indoc! {r#"
669mod foo «1{
670 fn process_data_1«2()2» «2{
671 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
672 }2»
673"#},
674 indoc! {r#"
675 fn process_data_2«2()2» «2{
676 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
677 }2»
678}1»
679
6801 hsla(207.80, 16.20%, 69.19%, 1.00)
6812 hsla(29.00, 54.00%, 65.88%, 1.00)
6823 hsla(286.00, 51.00%, 75.25%, 1.00)
6834 hsla(187.00, 47.00%, 59.22%, 1.00)
6845 hsla(355.00, 65.00%, 75.94%, 1.00)
685"#},
686 comment_lines,
687 ),
688 &bracket_colors_markup(&mut cx),
689 "After scrolling to the bottom, both chunks should have the highlights"
690 );
691
692 cx.update_editor(|editor, window, cx| {
693 editor.handle_input("{{}}}", window, cx);
694 });
695 cx.executor().advance_clock(Duration::from_millis(100));
696 cx.executor().run_until_parked();
697 assert_eq!(
698 &separate_with_comment_lines(
699 indoc! {r#"
700mod foo «1{
701 fn process_data_1() {
702 let map: Option<Vec<()>> = None;
703 }
704"#},
705 indoc! {r#"
706 fn process_data_2«2()2» «2{
707 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
708 }
709 «3{«4{}4»}3»}2»}1»
710
7111 hsla(207.80, 16.20%, 69.19%, 1.00)
7122 hsla(29.00, 54.00%, 65.88%, 1.00)
7133 hsla(286.00, 51.00%, 75.25%, 1.00)
7144 hsla(187.00, 47.00%, 59.22%, 1.00)
7155 hsla(355.00, 65.00%, 75.94%, 1.00)
716"#},
717 comment_lines,
718 ),
719 &bracket_colors_markup(&mut cx),
720 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
721 );
722
723 cx.update_editor(|editor, window, cx| {
724 editor.move_to_beginning(&MoveToBeginning, window, cx);
725 });
726 cx.executor().advance_clock(Duration::from_millis(100));
727 cx.executor().run_until_parked();
728 assert_eq!(
729 &separate_with_comment_lines(
730 indoc! {r#"
731mod foo «1{
732 fn process_data_1«2()2» «2{
733 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
734 }2»
735"#},
736 indoc! {r#"
737 fn process_data_2«2()2» «2{
738 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
739 }
740 «3{«4{}4»}3»}2»}1»
741
7421 hsla(207.80, 16.20%, 69.19%, 1.00)
7432 hsla(29.00, 54.00%, 65.88%, 1.00)
7443 hsla(286.00, 51.00%, 75.25%, 1.00)
7454 hsla(187.00, 47.00%, 59.22%, 1.00)
7465 hsla(355.00, 65.00%, 75.94%, 1.00)
747"#},
748 comment_lines,
749 ),
750 &bracket_colors_markup(&mut cx),
751 "Scrolling back to top should re-colorize all chunks' brackets"
752 );
753
754 cx.update(|_, cx| {
755 SettingsStore::update_global(cx, |store, cx| {
756 store.update_user_settings(cx, |settings| {
757 settings.project.all_languages.defaults.colorize_brackets = Some(false);
758 });
759 });
760 });
761 assert_eq!(
762 &separate_with_comment_lines(
763 indoc! {r#"
764mod foo {
765 fn process_data_1() {
766 let map: Option<Vec<()>> = None;
767 }
768"#},
769 r#" fn process_data_2() {
770 let map: Option<Vec<()>> = None;
771 }
772 {{}}}}
773
774"#,
775 comment_lines,
776 ),
777 &bracket_colors_markup(&mut cx),
778 "Turning bracket colorization off should remove all bracket colors"
779 );
780
781 cx.update(|_, cx| {
782 SettingsStore::update_global(cx, |store, cx| {
783 store.update_user_settings(cx, |settings| {
784 settings.project.all_languages.defaults.colorize_brackets = Some(true);
785 });
786 });
787 });
788 assert_eq!(
789 &separate_with_comment_lines(
790 indoc! {r#"
791mod foo «1{
792 fn process_data_1«2()2» «2{
793 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
794 }2»
795"#},
796 r#" fn process_data_2() {
797 let map: Option<Vec<()>> = None;
798 }
799 {{}}}}1»
800
8011 hsla(207.80, 16.20%, 69.19%, 1.00)
8022 hsla(29.00, 54.00%, 65.88%, 1.00)
8033 hsla(286.00, 51.00%, 75.25%, 1.00)
8044 hsla(187.00, 47.00%, 59.22%, 1.00)
8055 hsla(355.00, 65.00%, 75.94%, 1.00)
806"#,
807 comment_lines,
808 ),
809 &bracket_colors_markup(&mut cx),
810 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
811 );
812 }
813
814 #[gpui::test]
815 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
816 init_test(cx, |language_settings| {
817 language_settings.defaults.colorize_brackets = Some(true);
818 });
819 let mut cx = EditorLspTestContext::new(
820 Arc::into_inner(rust_lang()).unwrap(),
821 lsp::ServerCapabilities::default(),
822 cx,
823 )
824 .await;
825
826 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
827 cx.set_state(indoc! {r#"ˇ
828 pub(crate) fn inlay_hints(
829 db: &RootDatabase,
830 file_id: FileId,
831 range_limit: Option<TextRange>,
832 config: &InlayHintsConfig,
833 ) -> Vec<InlayHint> {
834 let _p = tracing::info_span!("inlay_hints").entered();
835 let sema = Semantics::new(db);
836 let file_id = sema
837 .attach_first_edition(file_id)
838 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
839 let file = sema.parse(file_id);
840 let file = file.syntax();
841
842 let mut acc = Vec::new();
843
844 let Some(scope) = sema.scope(file) else {
845 return acc;
846 };
847 let famous_defs = FamousDefs(&sema, scope.krate());
848 let display_target = famous_defs.1.to_display_target(sema.db);
849
850 let ctx = &mut InlayHintCtx::default();
851 let mut hints = |event| {
852 if let Some(node) = handle_event(ctx, event) {
853 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
854 }
855 };
856 let mut preorder = file.preorder();
857 salsa::attach(sema.db, || {
858 while let Some(event) = preorder.next() {
859 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
860 {
861 preorder.skip_subtree();
862 continue;
863 }
864 hints(event);
865 }
866 });
867 if let Some(range_limit) = range_limit {
868 acc.retain(|hint| range_limit.contains_range(hint.range));
869 }
870 acc
871 }
872
873 #[derive(Default)]
874 struct InlayHintCtx {
875 lifetime_stacks: Vec<Vec<SmolStr>>,
876 extern_block_parent: Option<ast::ExternBlock>,
877 }
878
879 pub(crate) fn inlay_hints_resolve(
880 db: &RootDatabase,
881 file_id: FileId,
882 resolve_range: TextRange,
883 hash: u64,
884 config: &InlayHintsConfig,
885 hasher: impl Fn(&InlayHint) -> u64,
886 ) -> Option<InlayHint> {
887 let _p = tracing::info_span!("inlay_hints_resolve").entered();
888 let sema = Semantics::new(db);
889 let file_id = sema
890 .attach_first_edition(file_id)
891 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
892 let file = sema.parse(file_id);
893 let file = file.syntax();
894
895 let scope = sema.scope(file)?;
896 let famous_defs = FamousDefs(&sema, scope.krate());
897 let mut acc = Vec::new();
898
899 let display_target = famous_defs.1.to_display_target(sema.db);
900
901 let ctx = &mut InlayHintCtx::default();
902 let mut hints = |event| {
903 if let Some(node) = handle_event(ctx, event) {
904 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
905 }
906 };
907
908 let mut preorder = file.preorder();
909 while let Some(event) = preorder.next() {
910 // This can miss some hints that require the parent of the range to calculate
911 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
912 {
913 preorder.skip_subtree();
914 continue;
915 }
916 hints(event);
917 }
918 acc.into_iter().find(|hint| hasher(hint) == hash)
919 }
920
921 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
922 match node {
923 WalkEvent::Enter(node) => {
924 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
925 let params = node
926 .generic_param_list()
927 .map(|it| {
928 it.lifetime_params()
929 .filter_map(|it| {
930 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
931 })
932 .collect()
933 })
934 .unwrap_or_default();
935 ctx.lifetime_stacks.push(params);
936 }
937 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
938 ctx.extern_block_parent = Some(node);
939 }
940 Some(node)
941 }
942 WalkEvent::Leave(n) => {
943 if ast::AnyHasGenericParams::can_cast(n.kind()) {
944 ctx.lifetime_stacks.pop();
945 }
946 if ast::ExternBlock::can_cast(n.kind()) {
947 ctx.extern_block_parent = None;
948 }
949 None
950 }
951 }
952 }
953
954 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
955 // HIR instead of the syntax tree.
956 fn hints(
957 hints: &mut Vec<InlayHint>,
958 ctx: &mut InlayHintCtx,
959 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
960 config: &InlayHintsConfig,
961 file_id: EditionedFileId,
962 display_target: DisplayTarget,
963 node: SyntaxNode,
964 ) {
965 closing_brace::hints(
966 hints,
967 sema,
968 config,
969 display_target,
970 InRealFile { file_id, value: node.clone() },
971 );
972 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
973 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
974 }
975
976 match_ast! {
977 match node {
978 ast::Expr(expr) => {
979 chaining::hints(hints, famous_defs, config, display_target, &expr);
980 adjustment::hints(hints, famous_defs, config, display_target, &expr);
981 match expr {
982 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
983 ast::Expr::MethodCallExpr(it) => {
984 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
985 }
986 ast::Expr::ClosureExpr(it) => {
987 closure_captures::hints(hints, famous_defs, config, it.clone());
988 closure_ret::hints(hints, famous_defs, config, display_target, it)
989 },
990 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
991 _ => Some(()),
992 }
993 },
994 ast::Pat(it) => {
995 binding_mode::hints(hints, famous_defs, config, &it);
996 match it {
997 ast::Pat::IdentPat(it) => {
998 bind_pat::hints(hints, famous_defs, config, display_target, &it);
999 }
1000 ast::Pat::RangePat(it) => {
1001 range_exclusive::hints(hints, famous_defs, config, it);
1002 }
1003 _ => {}
1004 }
1005 Some(())
1006 },
1007 ast::Item(it) => match it {
1008 ast::Item::Fn(it) => {
1009 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
1010 if let Some(extern_block) = &ctx.extern_block_parent {
1011 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
1012 }
1013 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
1014 },
1015 ast::Item::Static(it) => {
1016 if let Some(extern_block) = &ctx.extern_block_parent {
1017 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
1018 }
1019 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
1020 },
1021 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
1022 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
1023 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
1024 _ => None,
1025 },
1026 // trait object type elisions
1027 ast::Type(ty) => match ty {
1028 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
1029 ast::Type::PathType(path) => {
1030 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
1031 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
1032 Some(())
1033 },
1034 ast::Type::DynTraitType(dyn_) => {
1035 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
1036 Some(())
1037 },
1038 _ => Some(()),
1039 },
1040 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
1041 _ => Some(()),
1042 }
1043 };
1044 }
1045 "#});
1046 cx.executor().advance_clock(Duration::from_millis(100));
1047 cx.executor().run_until_parked();
1048
1049 let actual_ranges = cx.update_editor(|editor, window, cx| {
1050 editor
1051 .snapshot(window, cx)
1052 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
1053 });
1054
1055 let mut highlighted_brackets = HashMap::default();
1056 for (color, range) in actual_ranges.iter().cloned() {
1057 highlighted_brackets.insert(range, color);
1058 }
1059
1060 let last_bracket = actual_ranges
1061 .iter()
1062 .max_by_key(|(_, p)| p.end.row)
1063 .unwrap()
1064 .clone();
1065
1066 cx.update_editor(|editor, window, cx| {
1067 let was_scrolled = editor.set_scroll_position(
1068 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
1069 window,
1070 cx,
1071 );
1072 assert!(was_scrolled.0);
1073 });
1074 cx.executor().advance_clock(Duration::from_millis(100));
1075 cx.executor().run_until_parked();
1076
1077 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
1078 editor
1079 .snapshot(window, cx)
1080 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
1081 });
1082 let new_last_bracket = ranges_after_scrolling
1083 .iter()
1084 .max_by_key(|(_, p)| p.end.row)
1085 .unwrap()
1086 .clone();
1087
1088 assert_ne!(
1089 last_bracket, new_last_bracket,
1090 "After scrolling down, we should have highlighted more brackets"
1091 );
1092
1093 cx.update_editor(|editor, window, cx| {
1094 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
1095 assert!(was_scrolled.0);
1096 });
1097
1098 for _ in 0..200 {
1099 cx.update_editor(|editor, window, cx| {
1100 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
1101 });
1102 cx.executor().advance_clock(Duration::from_millis(100));
1103 cx.executor().run_until_parked();
1104
1105 let colored_brackets = cx.update_editor(|editor, window, cx| {
1106 editor
1107 .snapshot(window, cx)
1108 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
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.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
1435 let editor_text = snapshot.text();
1436
1437 let mut next_index = 1;
1438 let mut color_to_index = HashMap::default();
1439 let mut annotations = Vec::new();
1440 for (color, range) in &actual_ranges {
1441 let color_index = *color_to_index
1442 .entry(*color)
1443 .or_insert_with(|| post_inc(&mut next_index));
1444 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1445 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1446 let start_offset = display_point_to_offset(&editor_text, start);
1447 let end_offset = display_point_to_offset(&editor_text, end);
1448 let bracket_text = &editor_text[start_offset..end_offset];
1449 let bracket_char = bracket_text.chars().next().unwrap();
1450
1451 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1452 annotations.push((start_offset, format!("«{color_index}")));
1453 } else {
1454 annotations.push((end_offset, format!("{color_index}»")));
1455 }
1456 }
1457
1458 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1459 pos_a.cmp(pos_b).reverse().then_with(|| {
1460 let a_is_opening = text_a.starts_with('«');
1461 let b_is_opening = text_b.starts_with('«');
1462 match (a_is_opening, b_is_opening) {
1463 (true, false) => cmp::Ordering::Less,
1464 (false, true) => cmp::Ordering::Greater,
1465 _ => cmp::Ordering::Equal,
1466 }
1467 })
1468 });
1469 annotations.dedup();
1470
1471 let mut markup = editor_text;
1472 for (offset, text) in annotations {
1473 markup.insert_str(offset, &text);
1474 }
1475
1476 markup.push_str("\n");
1477 for (index, color) in color_to_index
1478 .iter()
1479 .map(|(color, index)| (*index, *color))
1480 .sorted_by_key(|(index, _)| *index)
1481 {
1482 markup.push_str(&format!("{index} {color}\n"));
1483 }
1484
1485 markup
1486 }
1487}