1//! Bracket highlights, also known as "rainbow brackets".
2//! Uses tree-sitter queries from brackets.scm to capture bracket pairs,
3//! and theme accents to colorize those.
4
5use std::ops::Range;
6
7use crate::Editor;
8use collections::HashMap;
9use gpui::{Context, HighlightStyle};
10use itertools::Itertools;
11use language::language_settings;
12use multi_buffer::{Anchor, ExcerptId};
13use ui::{ActiveTheme, utils::ensure_minimum_contrast};
14
15struct ColorizedBracketsHighlight;
16
17impl Editor {
18 pub(crate) fn colorize_brackets(&mut self, invalidate: bool, cx: &mut Context<Editor>) {
19 if !self.mode.is_full() {
20 return;
21 }
22
23 if invalidate {
24 self.fetched_tree_sitter_chunks.clear();
25 }
26
27 let accents_count = cx.theme().accents().0.len();
28 let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
29 let all_excerpts = self.buffer().read(cx).excerpt_ids();
30 let anchors_in_multi_buffer = |current_excerpt: ExcerptId,
31 text_anchors: [text::Anchor; 4]|
32 -> Option<[Option<_>; 4]> {
33 multi_buffer_snapshot
34 .anchors_in_excerpt(current_excerpt, text_anchors)
35 .or_else(|| {
36 all_excerpts
37 .iter()
38 .filter(|&&excerpt_id| excerpt_id != current_excerpt)
39 .find_map(|&excerpt_id| {
40 multi_buffer_snapshot.anchors_in_excerpt(excerpt_id, text_anchors)
41 })
42 })?
43 .collect_array()
44 };
45
46 let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
47 HashMap::default(),
48 |mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
49 let buffer_snapshot = buffer.read(cx).snapshot();
50 if language_settings::language_settings(
51 buffer_snapshot.language().map(|language| language.name()),
52 buffer_snapshot.file(),
53 cx,
54 )
55 .colorize_brackets
56 {
57 let fetched_chunks = self
58 .fetched_tree_sitter_chunks
59 .entry(excerpt_id)
60 .or_default();
61
62 let brackets_by_accent = buffer_snapshot
63 .fetch_bracket_ranges(
64 buffer_range.start..buffer_range.end,
65 Some((&buffer_version, fetched_chunks)),
66 )
67 .into_iter()
68 .flat_map(|(chunk_range, pairs)| {
69 if fetched_chunks.insert(chunk_range) {
70 pairs
71 } else {
72 Vec::new()
73 }
74 })
75 .filter_map(|pair| {
76 let color_index = pair.color_index?;
77
78 let buffer_open_range = buffer_snapshot
79 .anchor_before(pair.open_range.start)
80 ..buffer_snapshot.anchor_after(pair.open_range.end);
81 let buffer_close_range = buffer_snapshot
82 .anchor_before(pair.close_range.start)
83 ..buffer_snapshot.anchor_after(pair.close_range.end);
84 let [
85 buffer_open_range_start,
86 buffer_open_range_end,
87 buffer_close_range_start,
88 buffer_close_range_end,
89 ] = anchors_in_multi_buffer(
90 excerpt_id,
91 [
92 buffer_open_range.start,
93 buffer_open_range.end,
94 buffer_close_range.start,
95 buffer_close_range.end,
96 ],
97 )?;
98 let multi_buffer_open_range =
99 buffer_open_range_start.zip(buffer_open_range_end);
100 let multi_buffer_close_range =
101 buffer_close_range_start.zip(buffer_close_range_end);
102
103 let mut ranges = Vec::with_capacity(2);
104 if let Some((open_start, open_end)) = multi_buffer_open_range {
105 ranges.push(open_start..open_end);
106 }
107 if let Some((close_start, close_end)) = multi_buffer_close_range {
108 ranges.push(close_start..close_end);
109 }
110 if ranges.is_empty() {
111 None
112 } else {
113 Some((color_index % accents_count, ranges))
114 }
115 });
116
117 for (accent_number, new_ranges) in brackets_by_accent {
118 let ranges = acc
119 .entry(accent_number)
120 .or_insert_with(Vec::<Range<Anchor>>::new);
121
122 for new_range in new_ranges {
123 let i = ranges
124 .binary_search_by(|probe| {
125 probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
126 })
127 .unwrap_or_else(|i| i);
128 ranges.insert(i, new_range);
129 }
130 }
131 }
132
133 acc
134 },
135 );
136
137 if invalidate {
138 self.clear_highlights::<ColorizedBracketsHighlight>(cx);
139 }
140
141 let editor_background = cx.theme().colors().editor_background;
142 for (accent_number, bracket_highlights) in bracket_matches_by_accent {
143 let bracket_color = cx.theme().accents().color_for_index(accent_number as u32);
144 let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
145 let style = HighlightStyle {
146 color: Some(adjusted_color),
147 ..HighlightStyle::default()
148 };
149
150 self.highlight_text_key::<ColorizedBracketsHighlight>(
151 accent_number,
152 bracket_highlights,
153 style,
154 true,
155 cx,
156 );
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use std::{cmp, sync::Arc, time::Duration};
164
165 use super::*;
166 use crate::{
167 DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
168 display_map::{DisplayRow, ToDisplayPoint},
169 editor_tests::init_test,
170 test::{
171 editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
172 },
173 };
174 use collections::HashSet;
175 use fs::FakeFs;
176 use gpui::{AppContext as _, UpdateGlobal as _};
177 use indoc::indoc;
178 use itertools::Itertools;
179 use language::{Capability, markdown_lang};
180 use languages::rust_lang;
181 use multi_buffer::{ExcerptRange, MultiBuffer};
182 use pretty_assertions::assert_eq;
183 use project::Project;
184 use rope::Point;
185 use serde_json::json;
186 use settings::{AccentContent, SettingsStore};
187 use text::{Bias, OffsetRangeExt, ToOffset};
188 use theme::ThemeStyleContent;
189 use ui::SharedString;
190 use util::{path, post_inc};
191
192 #[gpui::test]
193 async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) {
194 init_test(cx, |language_settings| {
195 language_settings.defaults.colorize_brackets = Some(true);
196 });
197 let mut cx = EditorLspTestContext::new(
198 Arc::into_inner(rust_lang()).unwrap(),
199 lsp::ServerCapabilities::default(),
200 cx,
201 )
202 .await;
203
204 cx.set_state(indoc! {r#"ˇuse std::{collections::HashMap, future::Future};
205
206fn main() {
207 let a = one((), { () }, ());
208 println!("{a}");
209 println!("{a}");
210 for i in 0..a {
211 println!("{i}");
212 }
213
214 let b = {
215 {
216 {
217 [([([([([([([([([([((), ())])])])])])])])])])]
218 }
219 }
220 };
221}
222
223#[rustfmt::skip]
224fn one(a: (), (): (), c: ()) -> usize { 1 }
225
226fn two<T>(a: HashMap<String, Vec<Option<T>>>) -> usize
227where
228 T: Future<Output = HashMap<String, Vec<Option<Box<()>>>>>,
229{
230 2
231}
232"#});
233 cx.executor().advance_clock(Duration::from_millis(100));
234 cx.executor().run_until_parked();
235
236 assert_eq!(
237 r#"use std::«1{collections::HashMap, future::Future}1»;
238
239fn main«1()1» «1{
240 let a = one«2(«3()3», «3{ «4()4» }3», «3()3»)2»;
241 println!«2("{a}")2»;
242 println!«2("{a}")2»;
243 for i in 0..a «2{
244 println!«3("{i}")3»;
245 }2»
246
247 let b = «2{
248 «3{
249 «4{
250 «5[«6(«7[«1(«2[«3(«4[«5(«6[«7(«1[«2(«3[«4(«5[«6(«7[«1(«2[«3(«4()4», «4()4»)3»]2»)1»]7»)6»]5»)4»]3»)2»]1»)7»]6»)5»]4»)3»]2»)1»]7»)6»]5»
251 }4»
252 }3»
253 }2»;
254}1»
255
256#«1[rustfmt::skip]1»
257fn one«1(a: «2()2», «2()2»: «2()2», c: «2()2»)1» -> usize «1{ 1 }1»
258
259fn two«1<T>1»«1(a: HashMap«2<String, Vec«3<Option«4<T>4»>3»>2»)1» -> usize
260where
261 T: Future«1<Output = HashMap«2<String, Vec«3<Option«4<Box«5<«6()6»>5»>4»>3»>2»>1»,
262«1{
263 2
264}1»
265
2661 hsla(207.80, 16.20%, 69.19%, 1.00)
2672 hsla(29.00, 54.00%, 65.88%, 1.00)
2683 hsla(286.00, 51.00%, 75.25%, 1.00)
2694 hsla(187.00, 47.00%, 59.22%, 1.00)
2705 hsla(355.00, 65.00%, 75.94%, 1.00)
2716 hsla(95.00, 38.00%, 62.00%, 1.00)
2727 hsla(39.00, 67.00%, 69.00%, 1.00)
273"#,
274 &bracket_colors_markup(&mut cx),
275 "All brackets should be colored based on their depth"
276 );
277 }
278
279 #[gpui::test]
280 async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
281 init_test(cx, |language_settings| {
282 language_settings.defaults.colorize_brackets = Some(true);
283 });
284 let mut cx = EditorLspTestContext::new(
285 Arc::into_inner(markdown_lang()).unwrap(),
286 lsp::ServerCapabilities::default(),
287 cx,
288 )
289 .await;
290
291 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)"#});
292 cx.executor().advance_clock(Duration::from_millis(100));
293 cx.executor().run_until_parked();
294
295 assert_eq!(
296 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»
2971 hsla(207.80, 16.20%, 69.19%, 1.00)
298"#,
299 &bracket_colors_markup(&mut cx),
300 "All markdown brackets should be colored based on their depth"
301 );
302 }
303
304 #[gpui::test]
305 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
306 init_test(cx, |language_settings| {
307 language_settings.defaults.colorize_brackets = Some(true);
308 });
309 let mut cx = EditorLspTestContext::new(
310 Arc::into_inner(rust_lang()).unwrap(),
311 lsp::ServerCapabilities::default(),
312 cx,
313 )
314 .await;
315
316 cx.set_state(indoc! {r#"
317struct Foo<'a, T> {
318 data: Vec<Option<&'a T>>,
319}
320
321fn process_data() {
322 let map:ˇ
323}
324"#});
325
326 cx.update_editor(|editor, window, cx| {
327 editor.handle_input(" Result<", window, cx);
328 });
329 cx.executor().advance_clock(Duration::from_millis(100));
330 cx.executor().run_until_parked();
331 assert_eq!(
332 indoc! {r#"
333struct Foo«1<'a, T>1» «1{
334 data: Vec«2<Option«3<&'a T>3»>2»,
335}1»
336
337fn process_data«1()1» «1{
338 let map: Result<
339}1»
340
3411 hsla(207.80, 16.20%, 69.19%, 1.00)
3422 hsla(29.00, 54.00%, 65.88%, 1.00)
3433 hsla(286.00, 51.00%, 75.25%, 1.00)
344"#},
345 &bracket_colors_markup(&mut cx),
346 "Brackets without pairs should be ignored and not colored"
347 );
348
349 cx.update_editor(|editor, window, cx| {
350 editor.handle_input("Option<Foo<'_, ()", window, cx);
351 });
352 cx.executor().advance_clock(Duration::from_millis(100));
353 cx.executor().run_until_parked();
354 assert_eq!(
355 indoc! {r#"
356struct Foo«1<'a, T>1» «1{
357 data: Vec«2<Option«3<&'a T>3»>2»,
358}1»
359
360fn process_data«1()1» «1{
361 let map: Result<Option<Foo<'_, «2()2»
362}1»
363
3641 hsla(207.80, 16.20%, 69.19%, 1.00)
3652 hsla(29.00, 54.00%, 65.88%, 1.00)
3663 hsla(286.00, 51.00%, 75.25%, 1.00)
367"#},
368 &bracket_colors_markup(&mut cx),
369 );
370
371 cx.update_editor(|editor, window, cx| {
372 editor.handle_input(">", window, cx);
373 });
374 cx.executor().advance_clock(Duration::from_millis(100));
375 cx.executor().run_until_parked();
376 assert_eq!(
377 indoc! {r#"
378struct Foo«1<'a, T>1» «1{
379 data: Vec«2<Option«3<&'a T>3»>2»,
380}1»
381
382fn process_data«1()1» «1{
383 let map: Result<Option<Foo«2<'_, «3()3»>2»
384}1»
385
3861 hsla(207.80, 16.20%, 69.19%, 1.00)
3872 hsla(29.00, 54.00%, 65.88%, 1.00)
3883 hsla(286.00, 51.00%, 75.25%, 1.00)
389"#},
390 &bracket_colors_markup(&mut cx),
391 "When brackets start to get closed, inner brackets are re-colored based on their depth"
392 );
393
394 cx.update_editor(|editor, window, cx| {
395 editor.handle_input(">", window, cx);
396 });
397 cx.executor().advance_clock(Duration::from_millis(100));
398 cx.executor().run_until_parked();
399 assert_eq!(
400 indoc! {r#"
401struct Foo«1<'a, T>1» «1{
402 data: Vec«2<Option«3<&'a T>3»>2»,
403}1»
404
405fn process_data«1()1» «1{
406 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
407}1»
408
4091 hsla(207.80, 16.20%, 69.19%, 1.00)
4102 hsla(29.00, 54.00%, 65.88%, 1.00)
4113 hsla(286.00, 51.00%, 75.25%, 1.00)
4124 hsla(187.00, 47.00%, 59.22%, 1.00)
413"#},
414 &bracket_colors_markup(&mut cx),
415 );
416
417 cx.update_editor(|editor, window, cx| {
418 editor.handle_input(", ()> = unimplemented!();", window, cx);
419 });
420 cx.executor().advance_clock(Duration::from_millis(100));
421 cx.executor().run_until_parked();
422 assert_eq!(
423 indoc! {r#"
424struct Foo«1<'a, T>1» «1{
425 data: Vec«2<Option«3<&'a T>3»>2»,
426}1»
427
428fn process_data«1()1» «1{
429 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
430}1»
431
4321 hsla(207.80, 16.20%, 69.19%, 1.00)
4332 hsla(29.00, 54.00%, 65.88%, 1.00)
4343 hsla(286.00, 51.00%, 75.25%, 1.00)
4354 hsla(187.00, 47.00%, 59.22%, 1.00)
4365 hsla(355.00, 65.00%, 75.94%, 1.00)
437"#},
438 &bracket_colors_markup(&mut cx),
439 );
440 }
441
442 #[gpui::test]
443 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
444 let comment_lines = 100;
445
446 init_test(cx, |language_settings| {
447 language_settings.defaults.colorize_brackets = Some(true);
448 });
449 let mut cx = EditorLspTestContext::new(
450 Arc::into_inner(rust_lang()).unwrap(),
451 lsp::ServerCapabilities::default(),
452 cx,
453 )
454 .await;
455
456 cx.set_state(&separate_with_comment_lines(
457 indoc! {r#"
458mod foo {
459 ˇfn process_data_1() {
460 let map: Option<Vec<()>> = None;
461 }
462"#},
463 indoc! {r#"
464 fn process_data_2() {
465 let map: Option<Vec<()>> = None;
466 }
467}
468"#},
469 comment_lines,
470 ));
471
472 cx.executor().advance_clock(Duration::from_millis(100));
473 cx.executor().run_until_parked();
474 assert_eq!(
475 &separate_with_comment_lines(
476 indoc! {r#"
477mod foo «1{
478 fn process_data_1«2()2» «2{
479 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
480 }2»
481"#},
482 indoc! {r#"
483 fn process_data_2() {
484 let map: Option<Vec<()>> = None;
485 }
486}1»
487
4881 hsla(207.80, 16.20%, 69.19%, 1.00)
4892 hsla(29.00, 54.00%, 65.88%, 1.00)
4903 hsla(286.00, 51.00%, 75.25%, 1.00)
4914 hsla(187.00, 47.00%, 59.22%, 1.00)
4925 hsla(355.00, 65.00%, 75.94%, 1.00)
493"#},
494 comment_lines,
495 ),
496 &bracket_colors_markup(&mut cx),
497 "First, the only visible chunk is getting the bracket highlights"
498 );
499
500 cx.update_editor(|editor, window, cx| {
501 editor.move_to_end(&MoveToEnd, window, cx);
502 editor.move_up(&MoveUp, window, cx);
503 });
504 cx.executor().advance_clock(Duration::from_millis(100));
505 cx.executor().run_until_parked();
506 assert_eq!(
507 &separate_with_comment_lines(
508 indoc! {r#"
509mod foo «1{
510 fn process_data_1«2()2» «2{
511 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
512 }2»
513"#},
514 indoc! {r#"
515 fn process_data_2«2()2» «2{
516 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
517 }2»
518}1»
519
5201 hsla(207.80, 16.20%, 69.19%, 1.00)
5212 hsla(29.00, 54.00%, 65.88%, 1.00)
5223 hsla(286.00, 51.00%, 75.25%, 1.00)
5234 hsla(187.00, 47.00%, 59.22%, 1.00)
5245 hsla(355.00, 65.00%, 75.94%, 1.00)
525"#},
526 comment_lines,
527 ),
528 &bracket_colors_markup(&mut cx),
529 "After scrolling to the bottom, both chunks should have the highlights"
530 );
531
532 cx.update_editor(|editor, window, cx| {
533 editor.handle_input("{{}}}", window, cx);
534 });
535 cx.executor().advance_clock(Duration::from_millis(100));
536 cx.executor().run_until_parked();
537 assert_eq!(
538 &separate_with_comment_lines(
539 indoc! {r#"
540mod foo «1{
541 fn process_data_1() {
542 let map: Option<Vec<()>> = None;
543 }
544"#},
545 indoc! {r#"
546 fn process_data_2«2()2» «2{
547 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
548 }
549 «3{«4{}4»}3»}2»}1»
550
5511 hsla(207.80, 16.20%, 69.19%, 1.00)
5522 hsla(29.00, 54.00%, 65.88%, 1.00)
5533 hsla(286.00, 51.00%, 75.25%, 1.00)
5544 hsla(187.00, 47.00%, 59.22%, 1.00)
5555 hsla(355.00, 65.00%, 75.94%, 1.00)
556"#},
557 comment_lines,
558 ),
559 &bracket_colors_markup(&mut cx),
560 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
561 );
562
563 cx.update_editor(|editor, window, cx| {
564 editor.move_to_beginning(&MoveToBeginning, window, cx);
565 });
566 cx.executor().advance_clock(Duration::from_millis(100));
567 cx.executor().run_until_parked();
568 assert_eq!(
569 &separate_with_comment_lines(
570 indoc! {r#"
571mod foo «1{
572 fn process_data_1«2()2» «2{
573 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
574 }2»
575"#},
576 indoc! {r#"
577 fn process_data_2«2()2» «2{
578 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
579 }
580 «3{«4{}4»}3»}2»}1»
581
5821 hsla(207.80, 16.20%, 69.19%, 1.00)
5832 hsla(29.00, 54.00%, 65.88%, 1.00)
5843 hsla(286.00, 51.00%, 75.25%, 1.00)
5854 hsla(187.00, 47.00%, 59.22%, 1.00)
5865 hsla(355.00, 65.00%, 75.94%, 1.00)
587"#},
588 comment_lines,
589 ),
590 &bracket_colors_markup(&mut cx),
591 "Scrolling back to top should re-colorize all chunks' brackets"
592 );
593
594 cx.update(|_, cx| {
595 SettingsStore::update_global(cx, |store, cx| {
596 store.update_user_settings(cx, |settings| {
597 settings.project.all_languages.defaults.colorize_brackets = Some(false);
598 });
599 });
600 });
601 assert_eq!(
602 &separate_with_comment_lines(
603 indoc! {r#"
604mod foo {
605 fn process_data_1() {
606 let map: Option<Vec<()>> = None;
607 }
608"#},
609 r#" fn process_data_2() {
610 let map: Option<Vec<()>> = None;
611 }
612 {{}}}}
613
614"#,
615 comment_lines,
616 ),
617 &bracket_colors_markup(&mut cx),
618 "Turning bracket colorization off should remove all bracket colors"
619 );
620
621 cx.update(|_, cx| {
622 SettingsStore::update_global(cx, |store, cx| {
623 store.update_user_settings(cx, |settings| {
624 settings.project.all_languages.defaults.colorize_brackets = Some(true);
625 });
626 });
627 });
628 assert_eq!(
629 &separate_with_comment_lines(
630 indoc! {r#"
631mod foo «1{
632 fn process_data_1«2()2» «2{
633 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
634 }2»
635"#},
636 r#" fn process_data_2() {
637 let map: Option<Vec<()>> = None;
638 }
639 {{}}}}1»
640
6411 hsla(207.80, 16.20%, 69.19%, 1.00)
6422 hsla(29.00, 54.00%, 65.88%, 1.00)
6433 hsla(286.00, 51.00%, 75.25%, 1.00)
6444 hsla(187.00, 47.00%, 59.22%, 1.00)
6455 hsla(355.00, 65.00%, 75.94%, 1.00)
646"#,
647 comment_lines,
648 ),
649 &bracket_colors_markup(&mut cx),
650 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
651 );
652 }
653
654 #[gpui::test]
655 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
656 init_test(cx, |language_settings| {
657 language_settings.defaults.colorize_brackets = Some(true);
658 });
659 let mut cx = EditorLspTestContext::new(
660 Arc::into_inner(rust_lang()).unwrap(),
661 lsp::ServerCapabilities::default(),
662 cx,
663 )
664 .await;
665
666 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
667 cx.set_state(indoc! {r#"ˇ
668 pub(crate) fn inlay_hints(
669 db: &RootDatabase,
670 file_id: FileId,
671 range_limit: Option<TextRange>,
672 config: &InlayHintsConfig,
673 ) -> Vec<InlayHint> {
674 let _p = tracing::info_span!("inlay_hints").entered();
675 let sema = Semantics::new(db);
676 let file_id = sema
677 .attach_first_edition(file_id)
678 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
679 let file = sema.parse(file_id);
680 let file = file.syntax();
681
682 let mut acc = Vec::new();
683
684 let Some(scope) = sema.scope(file) else {
685 return acc;
686 };
687 let famous_defs = FamousDefs(&sema, scope.krate());
688 let display_target = famous_defs.1.to_display_target(sema.db);
689
690 let ctx = &mut InlayHintCtx::default();
691 let mut hints = |event| {
692 if let Some(node) = handle_event(ctx, event) {
693 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
694 }
695 };
696 let mut preorder = file.preorder();
697 salsa::attach(sema.db, || {
698 while let Some(event) = preorder.next() {
699 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
700 {
701 preorder.skip_subtree();
702 continue;
703 }
704 hints(event);
705 }
706 });
707 if let Some(range_limit) = range_limit {
708 acc.retain(|hint| range_limit.contains_range(hint.range));
709 }
710 acc
711 }
712
713 #[derive(Default)]
714 struct InlayHintCtx {
715 lifetime_stacks: Vec<Vec<SmolStr>>,
716 extern_block_parent: Option<ast::ExternBlock>,
717 }
718
719 pub(crate) fn inlay_hints_resolve(
720 db: &RootDatabase,
721 file_id: FileId,
722 resolve_range: TextRange,
723 hash: u64,
724 config: &InlayHintsConfig,
725 hasher: impl Fn(&InlayHint) -> u64,
726 ) -> Option<InlayHint> {
727 let _p = tracing::info_span!("inlay_hints_resolve").entered();
728 let sema = Semantics::new(db);
729 let file_id = sema
730 .attach_first_edition(file_id)
731 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
732 let file = sema.parse(file_id);
733 let file = file.syntax();
734
735 let scope = sema.scope(file)?;
736 let famous_defs = FamousDefs(&sema, scope.krate());
737 let mut acc = Vec::new();
738
739 let display_target = famous_defs.1.to_display_target(sema.db);
740
741 let ctx = &mut InlayHintCtx::default();
742 let mut hints = |event| {
743 if let Some(node) = handle_event(ctx, event) {
744 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
745 }
746 };
747
748 let mut preorder = file.preorder();
749 while let Some(event) = preorder.next() {
750 // This can miss some hints that require the parent of the range to calculate
751 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
752 {
753 preorder.skip_subtree();
754 continue;
755 }
756 hints(event);
757 }
758 acc.into_iter().find(|hint| hasher(hint) == hash)
759 }
760
761 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
762 match node {
763 WalkEvent::Enter(node) => {
764 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
765 let params = node
766 .generic_param_list()
767 .map(|it| {
768 it.lifetime_params()
769 .filter_map(|it| {
770 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
771 })
772 .collect()
773 })
774 .unwrap_or_default();
775 ctx.lifetime_stacks.push(params);
776 }
777 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
778 ctx.extern_block_parent = Some(node);
779 }
780 Some(node)
781 }
782 WalkEvent::Leave(n) => {
783 if ast::AnyHasGenericParams::can_cast(n.kind()) {
784 ctx.lifetime_stacks.pop();
785 }
786 if ast::ExternBlock::can_cast(n.kind()) {
787 ctx.extern_block_parent = None;
788 }
789 None
790 }
791 }
792 }
793
794 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
795 // HIR instead of the syntax tree.
796 fn hints(
797 hints: &mut Vec<InlayHint>,
798 ctx: &mut InlayHintCtx,
799 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
800 config: &InlayHintsConfig,
801 file_id: EditionedFileId,
802 display_target: DisplayTarget,
803 node: SyntaxNode,
804 ) {
805 closing_brace::hints(
806 hints,
807 sema,
808 config,
809 display_target,
810 InRealFile { file_id, value: node.clone() },
811 );
812 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
813 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
814 }
815
816 match_ast! {
817 match node {
818 ast::Expr(expr) => {
819 chaining::hints(hints, famous_defs, config, display_target, &expr);
820 adjustment::hints(hints, famous_defs, config, display_target, &expr);
821 match expr {
822 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
823 ast::Expr::MethodCallExpr(it) => {
824 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
825 }
826 ast::Expr::ClosureExpr(it) => {
827 closure_captures::hints(hints, famous_defs, config, it.clone());
828 closure_ret::hints(hints, famous_defs, config, display_target, it)
829 },
830 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
831 _ => Some(()),
832 }
833 },
834 ast::Pat(it) => {
835 binding_mode::hints(hints, famous_defs, config, &it);
836 match it {
837 ast::Pat::IdentPat(it) => {
838 bind_pat::hints(hints, famous_defs, config, display_target, &it);
839 }
840 ast::Pat::RangePat(it) => {
841 range_exclusive::hints(hints, famous_defs, config, it);
842 }
843 _ => {}
844 }
845 Some(())
846 },
847 ast::Item(it) => match it {
848 ast::Item::Fn(it) => {
849 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
850 if let Some(extern_block) = &ctx.extern_block_parent {
851 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
852 }
853 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
854 },
855 ast::Item::Static(it) => {
856 if let Some(extern_block) = &ctx.extern_block_parent {
857 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
858 }
859 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
860 },
861 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
862 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
863 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
864 _ => None,
865 },
866 // trait object type elisions
867 ast::Type(ty) => match ty {
868 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
869 ast::Type::PathType(path) => {
870 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
871 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
872 Some(())
873 },
874 ast::Type::DynTraitType(dyn_) => {
875 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
876 Some(())
877 },
878 _ => Some(()),
879 },
880 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
881 _ => Some(()),
882 }
883 };
884 }
885 "#});
886 cx.executor().advance_clock(Duration::from_millis(100));
887 cx.executor().run_until_parked();
888
889 let actual_ranges = cx.update_editor(|editor, window, cx| {
890 editor
891 .snapshot(window, cx)
892 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
893 });
894
895 let mut highlighted_brackets = HashMap::default();
896 for (color, range) in actual_ranges.iter().cloned() {
897 highlighted_brackets.insert(range, color);
898 }
899
900 let last_bracket = actual_ranges
901 .iter()
902 .max_by_key(|(_, p)| p.end.row)
903 .unwrap()
904 .clone();
905
906 cx.update_editor(|editor, window, cx| {
907 let was_scrolled = editor.set_scroll_position(
908 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
909 window,
910 cx,
911 );
912 assert!(was_scrolled.0);
913 });
914 cx.executor().advance_clock(Duration::from_millis(100));
915 cx.executor().run_until_parked();
916
917 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
918 editor
919 .snapshot(window, cx)
920 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
921 });
922 let new_last_bracket = ranges_after_scrolling
923 .iter()
924 .max_by_key(|(_, p)| p.end.row)
925 .unwrap()
926 .clone();
927
928 assert_ne!(
929 last_bracket, new_last_bracket,
930 "After scrolling down, we should have highlighted more brackets"
931 );
932
933 cx.update_editor(|editor, window, cx| {
934 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
935 assert!(was_scrolled.0);
936 });
937
938 for _ in 0..200 {
939 cx.update_editor(|editor, window, cx| {
940 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
941 });
942 cx.executor().advance_clock(Duration::from_millis(100));
943 cx.executor().run_until_parked();
944
945 let colored_brackets = cx.update_editor(|editor, window, cx| {
946 editor
947 .snapshot(window, cx)
948 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
949 });
950 for (color, range) in colored_brackets.clone() {
951 assert!(
952 highlighted_brackets.entry(range).or_insert(color) == &color,
953 "Colors should stay consistent while scrolling!"
954 );
955 }
956
957 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
958 let scroll_position = snapshot.scroll_position();
959 let visible_lines =
960 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
961 let visible_range = DisplayRow(scroll_position.y as u32)
962 ..DisplayRow((scroll_position.y + visible_lines) as u32);
963
964 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
965 colored_brackets
966 .iter()
967 .flat_map(|(_, range)| [range.start, range.end]),
968 );
969
970 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
971 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
972 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
973 }) {
974 assert!(
975 current_highlighted_bracket_set.contains(&highlight_range.start)
976 || current_highlighted_bracket_set.contains(&highlight_range.end),
977 "Should not lose highlights while scrolling in the visible range!"
978 );
979 }
980
981 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
982 for bracket_match in buffer_snapshot
983 .fetch_bracket_ranges(
984 snapshot
985 .display_point_to_point(
986 DisplayPoint::new(visible_range.start, 0),
987 Bias::Left,
988 )
989 .to_offset(&buffer_snapshot)
990 ..snapshot
991 .display_point_to_point(
992 DisplayPoint::new(
993 visible_range.end,
994 snapshot.line_len(visible_range.end),
995 ),
996 Bias::Right,
997 )
998 .to_offset(&buffer_snapshot),
999 None,
1000 )
1001 .iter()
1002 .flat_map(|entry| entry.1)
1003 .filter(|bracket_match| bracket_match.color_index.is_some())
1004 {
1005 let start = bracket_match.open_range.to_point(buffer_snapshot);
1006 let end = bracket_match.close_range.to_point(buffer_snapshot);
1007 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
1008 assert!(
1009 start_bracket.is_some(),
1010 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1011 buffer_snapshot
1012 .text_for_range(start.start..end.end)
1013 .collect::<String>(),
1014 start
1015 );
1016
1017 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1018 assert!(
1019 end_bracket.is_some(),
1020 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1021 buffer_snapshot
1022 .text_for_range(start.start..end.end)
1023 .collect::<String>(),
1024 start
1025 );
1026
1027 assert_eq!(
1028 start_bracket.unwrap().0,
1029 end_bracket.unwrap().0,
1030 "Bracket pair should be highlighted the same color!"
1031 )
1032 }
1033 }
1034 }
1035
1036 #[gpui::test]
1037 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1038 let comment_lines = 100;
1039
1040 init_test(cx, |language_settings| {
1041 language_settings.defaults.colorize_brackets = Some(true);
1042 });
1043 let fs = FakeFs::new(cx.background_executor.clone());
1044 fs.insert_tree(
1045 path!("/a"),
1046 json!({
1047 "main.rs": "fn main() {{()}}",
1048 "lib.rs": separate_with_comment_lines(
1049 indoc! {r#"
1050 mod foo {
1051 fn process_data_1() {
1052 let map: Option<Vec<()>> = None;
1053 // a
1054 // b
1055 // c
1056 }
1057 "#},
1058 indoc! {r#"
1059 fn process_data_2() {
1060 let other_map: Option<Vec<()>> = None;
1061 }
1062 }
1063 "#},
1064 comment_lines,
1065 )
1066 }),
1067 )
1068 .await;
1069
1070 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1071 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1072 language_registry.add(rust_lang());
1073
1074 let buffer_1 = project
1075 .update(cx, |project, cx| {
1076 project.open_local_buffer(path!("/a/lib.rs"), cx)
1077 })
1078 .await
1079 .unwrap();
1080 let buffer_2 = project
1081 .update(cx, |project, cx| {
1082 project.open_local_buffer(path!("/a/main.rs"), cx)
1083 })
1084 .await
1085 .unwrap();
1086
1087 let multi_buffer = cx.new(|cx| {
1088 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1089 multi_buffer.push_excerpts(
1090 buffer_2.clone(),
1091 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1092 cx,
1093 );
1094
1095 let excerpt_rows = 5;
1096 let rest_of_first_except_rows = 3;
1097 multi_buffer.push_excerpts(
1098 buffer_1.clone(),
1099 [
1100 ExcerptRange::new(Point::new(0, 0)..Point::new(excerpt_rows, 0)),
1101 ExcerptRange::new(
1102 Point::new(
1103 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1104 0,
1105 )
1106 ..Point::new(
1107 comment_lines as u32
1108 + excerpt_rows
1109 + rest_of_first_except_rows
1110 + excerpt_rows,
1111 0,
1112 ),
1113 ),
1114 ],
1115 cx,
1116 );
1117 multi_buffer
1118 });
1119
1120 let editor = cx.add_window(|window, cx| {
1121 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1122 });
1123 cx.executor().advance_clock(Duration::from_millis(100));
1124 cx.executor().run_until_parked();
1125
1126 let editor_snapshot = editor
1127 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1128 .unwrap();
1129 assert_eq!(
1130 indoc! {r#"
1131
1132
1133fn main«1()1» «1{«2{«3()3»}2»}1»
1134
1135
1136mod foo «1{
1137 fn process_data_1«2()2» «2{
1138 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1139 // a
1140 // b
1141
1142
1143 fn process_data_2«2()2» «2{
1144 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1145 }2»
1146}1»
1147
11481 hsla(207.80, 16.20%, 69.19%, 1.00)
11492 hsla(29.00, 54.00%, 65.88%, 1.00)
11503 hsla(286.00, 51.00%, 75.25%, 1.00)
11514 hsla(187.00, 47.00%, 59.22%, 1.00)
11525 hsla(355.00, 65.00%, 75.94%, 1.00)
1153"#,},
1154 &editor_bracket_colors_markup(&editor_snapshot),
1155 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1156or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1157 );
1158
1159 editor
1160 .update(cx, |editor, window, cx| {
1161 editor.handle_input("{[]", window, cx);
1162 })
1163 .unwrap();
1164 cx.executor().advance_clock(Duration::from_millis(100));
1165 cx.executor().run_until_parked();
1166 let editor_snapshot = editor
1167 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1168 .unwrap();
1169 assert_eq!(
1170 indoc! {r#"
1171
1172
1173{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1174
1175
1176mod foo «1{
1177 fn process_data_1«2()2» «2{
1178 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1179 // a
1180 // b
1181
1182
1183 fn process_data_2«2()2» «2{
1184 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1185 }2»
1186}1»
1187
11881 hsla(207.80, 16.20%, 69.19%, 1.00)
11892 hsla(29.00, 54.00%, 65.88%, 1.00)
11903 hsla(286.00, 51.00%, 75.25%, 1.00)
11914 hsla(187.00, 47.00%, 59.22%, 1.00)
11925 hsla(355.00, 65.00%, 75.94%, 1.00)
1193"#,},
1194 &editor_bracket_colors_markup(&editor_snapshot),
1195 );
1196
1197 cx.update(|cx| {
1198 let theme = cx.theme().name.clone();
1199 SettingsStore::update_global(cx, |store, cx| {
1200 store.update_user_settings(cx, |settings| {
1201 settings.theme.theme_overrides = HashMap::from_iter([(
1202 theme.to_string(),
1203 ThemeStyleContent {
1204 accents: vec![
1205 AccentContent(Some(SharedString::new("#ff0000"))),
1206 AccentContent(Some(SharedString::new("#0000ff"))),
1207 ],
1208 ..ThemeStyleContent::default()
1209 },
1210 )]);
1211 });
1212 });
1213 });
1214 cx.executor().advance_clock(Duration::from_millis(100));
1215 cx.executor().run_until_parked();
1216 let editor_snapshot = editor
1217 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1218 .unwrap();
1219 assert_eq!(
1220 indoc! {r#"
1221
1222
1223{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1224
1225
1226mod foo «1{
1227 fn process_data_1«2()2» «2{
1228 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1229 // a
1230 // b
1231
1232
1233 fn process_data_2«2()2» «2{
1234 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1235 }2»
1236}1»
1237
12381 hsla(0.00, 100.00%, 78.12%, 1.00)
12392 hsla(240.00, 100.00%, 82.81%, 1.00)
1240"#,},
1241 &editor_bracket_colors_markup(&editor_snapshot),
1242 "After updating theme accents, the editor should update the bracket coloring"
1243 );
1244 }
1245
1246 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1247 let mut result = head.to_string();
1248 result.push_str("\n");
1249 result.push_str(&"//\n".repeat(comment_lines));
1250 result.push_str(tail);
1251 result
1252 }
1253
1254 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1255 cx.update_editor(|editor, window, cx| {
1256 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1257 })
1258 }
1259
1260 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1261 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1262 let mut offset = 0;
1263 for (row_idx, line) in text.lines().enumerate() {
1264 if row_idx < point.row().0 as usize {
1265 offset += line.len() + 1; // +1 for newline
1266 } else {
1267 offset += point.column() as usize;
1268 break;
1269 }
1270 }
1271 offset
1272 }
1273
1274 let actual_ranges = snapshot.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
1275 let editor_text = snapshot.text();
1276
1277 let mut next_index = 1;
1278 let mut color_to_index = HashMap::default();
1279 let mut annotations = Vec::new();
1280 for (color, range) in &actual_ranges {
1281 let color_index = *color_to_index
1282 .entry(*color)
1283 .or_insert_with(|| post_inc(&mut next_index));
1284 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1285 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1286 let start_offset = display_point_to_offset(&editor_text, start);
1287 let end_offset = display_point_to_offset(&editor_text, end);
1288 let bracket_text = &editor_text[start_offset..end_offset];
1289 let bracket_char = bracket_text.chars().next().unwrap();
1290
1291 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1292 annotations.push((start_offset, format!("«{color_index}")));
1293 } else {
1294 annotations.push((end_offset, format!("{color_index}»")));
1295 }
1296 }
1297
1298 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1299 pos_a.cmp(pos_b).reverse().then_with(|| {
1300 let a_is_opening = text_a.starts_with('«');
1301 let b_is_opening = text_b.starts_with('«');
1302 match (a_is_opening, b_is_opening) {
1303 (true, false) => cmp::Ordering::Less,
1304 (false, true) => cmp::Ordering::Greater,
1305 _ => cmp::Ordering::Equal,
1306 }
1307 })
1308 });
1309 annotations.dedup();
1310
1311 let mut markup = editor_text;
1312 for (offset, text) in annotations {
1313 markup.insert_str(offset, &text);
1314 }
1315
1316 markup.push_str("\n");
1317 for (index, color) in color_to_index
1318 .iter()
1319 .map(|(color, index)| (*index, *color))
1320 .sorted_by_key(|(index, _)| *index)
1321 {
1322 markup.push_str(&format!("{index} {color}\n"));
1323 }
1324
1325 markup
1326 }
1327}