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