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