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