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