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