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, markdown_lang};
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_markdown_bracket_colorization(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(markdown_lang()).unwrap(),
271 lsp::ServerCapabilities::default(),
272 cx,
273 )
274 .await;
275
276 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)"#});
277 cx.executor().advance_clock(Duration::from_millis(100));
278 cx.executor().run_until_parked();
279
280 assert_eq!(
281 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»
2821 hsla(207.80, 16.20%, 69.19%, 1.00)
283"#,
284 &bracket_colors_markup(&mut cx),
285 "All markdown brackets should be colored based on their depth"
286 );
287 }
288
289 #[gpui::test]
290 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
291 init_test(cx, |language_settings| {
292 language_settings.defaults.colorize_brackets = Some(true);
293 });
294 let mut cx = EditorLspTestContext::new(
295 Arc::into_inner(rust_lang()).unwrap(),
296 lsp::ServerCapabilities::default(),
297 cx,
298 )
299 .await;
300
301 cx.set_state(indoc! {r#"
302struct Foo<'a, T> {
303 data: Vec<Option<&'a T>>,
304}
305
306fn process_data() {
307 let map:ˇ
308}
309"#});
310
311 cx.update_editor(|editor, window, cx| {
312 editor.handle_input(" Result<", window, cx);
313 });
314 cx.executor().advance_clock(Duration::from_millis(100));
315 cx.executor().run_until_parked();
316 assert_eq!(
317 indoc! {r#"
318struct Foo«1<'a, T>1» «1{
319 data: Vec«2<Option«3<&'a T>3»>2»,
320}1»
321
322fn process_data«1()1» «1{
323 let map: Result<
324}1»
325
3261 hsla(207.80, 16.20%, 69.19%, 1.00)
3272 hsla(29.00, 54.00%, 65.88%, 1.00)
3283 hsla(286.00, 51.00%, 75.25%, 1.00)
329"#},
330 &bracket_colors_markup(&mut cx),
331 "Brackets without pairs should be ignored and not colored"
332 );
333
334 cx.update_editor(|editor, window, cx| {
335 editor.handle_input("Option<Foo<'_, ()", window, cx);
336 });
337 cx.executor().advance_clock(Duration::from_millis(100));
338 cx.executor().run_until_parked();
339 assert_eq!(
340 indoc! {r#"
341struct Foo«1<'a, T>1» «1{
342 data: Vec«2<Option«3<&'a T>3»>2»,
343}1»
344
345fn process_data«1()1» «1{
346 let map: Result<Option<Foo<'_, «2()2»
347}1»
348
3491 hsla(207.80, 16.20%, 69.19%, 1.00)
3502 hsla(29.00, 54.00%, 65.88%, 1.00)
3513 hsla(286.00, 51.00%, 75.25%, 1.00)
352"#},
353 &bracket_colors_markup(&mut cx),
354 );
355
356 cx.update_editor(|editor, window, cx| {
357 editor.handle_input(">", window, cx);
358 });
359 cx.executor().advance_clock(Duration::from_millis(100));
360 cx.executor().run_until_parked();
361 assert_eq!(
362 indoc! {r#"
363struct Foo«1<'a, T>1» «1{
364 data: Vec«2<Option«3<&'a T>3»>2»,
365}1»
366
367fn process_data«1()1» «1{
368 let map: Result<Option<Foo«2<'_, «3()3»>2»
369}1»
370
3711 hsla(207.80, 16.20%, 69.19%, 1.00)
3722 hsla(29.00, 54.00%, 65.88%, 1.00)
3733 hsla(286.00, 51.00%, 75.25%, 1.00)
374"#},
375 &bracket_colors_markup(&mut cx),
376 "When brackets start to get closed, inner brackets are re-colored based on their depth"
377 );
378
379 cx.update_editor(|editor, window, cx| {
380 editor.handle_input(">", window, cx);
381 });
382 cx.executor().advance_clock(Duration::from_millis(100));
383 cx.executor().run_until_parked();
384 assert_eq!(
385 indoc! {r#"
386struct Foo«1<'a, T>1» «1{
387 data: Vec«2<Option«3<&'a T>3»>2»,
388}1»
389
390fn process_data«1()1» «1{
391 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
392}1»
393
3941 hsla(207.80, 16.20%, 69.19%, 1.00)
3952 hsla(29.00, 54.00%, 65.88%, 1.00)
3963 hsla(286.00, 51.00%, 75.25%, 1.00)
3974 hsla(187.00, 47.00%, 59.22%, 1.00)
398"#},
399 &bracket_colors_markup(&mut cx),
400 );
401
402 cx.update_editor(|editor, window, cx| {
403 editor.handle_input(", ()> = unimplemented!();", window, cx);
404 });
405 cx.executor().advance_clock(Duration::from_millis(100));
406 cx.executor().run_until_parked();
407 assert_eq!(
408 indoc! {r#"
409struct Foo«1<'a, T>1» «1{
410 data: Vec«2<Option«3<&'a T>3»>2»,
411}1»
412
413fn process_data«1()1» «1{
414 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
415}1»
416
4171 hsla(207.80, 16.20%, 69.19%, 1.00)
4182 hsla(29.00, 54.00%, 65.88%, 1.00)
4193 hsla(286.00, 51.00%, 75.25%, 1.00)
4204 hsla(187.00, 47.00%, 59.22%, 1.00)
4215 hsla(355.00, 65.00%, 75.94%, 1.00)
422"#},
423 &bracket_colors_markup(&mut cx),
424 );
425 }
426
427 #[gpui::test]
428 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
429 let comment_lines = 100;
430
431 init_test(cx, |language_settings| {
432 language_settings.defaults.colorize_brackets = Some(true);
433 });
434 let mut cx = EditorLspTestContext::new(
435 Arc::into_inner(rust_lang()).unwrap(),
436 lsp::ServerCapabilities::default(),
437 cx,
438 )
439 .await;
440
441 cx.set_state(&separate_with_comment_lines(
442 indoc! {r#"
443mod foo {
444 ˇfn process_data_1() {
445 let map: Option<Vec<()>> = None;
446 }
447"#},
448 indoc! {r#"
449 fn process_data_2() {
450 let map: Option<Vec<()>> = None;
451 }
452}
453"#},
454 comment_lines,
455 ));
456
457 cx.executor().advance_clock(Duration::from_millis(100));
458 cx.executor().run_until_parked();
459 assert_eq!(
460 &separate_with_comment_lines(
461 indoc! {r#"
462mod foo «1{
463 fn process_data_1«2()2» «2{
464 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
465 }2»
466"#},
467 indoc! {r#"
468 fn process_data_2() {
469 let map: Option<Vec<()>> = None;
470 }
471}1»
472
4731 hsla(207.80, 16.20%, 69.19%, 1.00)
4742 hsla(29.00, 54.00%, 65.88%, 1.00)
4753 hsla(286.00, 51.00%, 75.25%, 1.00)
4764 hsla(187.00, 47.00%, 59.22%, 1.00)
4775 hsla(355.00, 65.00%, 75.94%, 1.00)
478"#},
479 comment_lines,
480 ),
481 &bracket_colors_markup(&mut cx),
482 "First, the only visible chunk is getting the bracket highlights"
483 );
484
485 cx.update_editor(|editor, window, cx| {
486 editor.move_to_end(&MoveToEnd, window, cx);
487 editor.move_up(&MoveUp, window, cx);
488 });
489 cx.executor().advance_clock(Duration::from_millis(100));
490 cx.executor().run_until_parked();
491 assert_eq!(
492 &separate_with_comment_lines(
493 indoc! {r#"
494mod foo «1{
495 fn process_data_1«2()2» «2{
496 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
497 }2»
498"#},
499 indoc! {r#"
500 fn process_data_2«2()2» «2{
501 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
502 }2»
503}1»
504
5051 hsla(207.80, 16.20%, 69.19%, 1.00)
5062 hsla(29.00, 54.00%, 65.88%, 1.00)
5073 hsla(286.00, 51.00%, 75.25%, 1.00)
5084 hsla(187.00, 47.00%, 59.22%, 1.00)
5095 hsla(355.00, 65.00%, 75.94%, 1.00)
510"#},
511 comment_lines,
512 ),
513 &bracket_colors_markup(&mut cx),
514 "After scrolling to the bottom, both chunks should have the highlights"
515 );
516
517 cx.update_editor(|editor, window, cx| {
518 editor.handle_input("{{}}}", window, cx);
519 });
520 cx.executor().advance_clock(Duration::from_millis(100));
521 cx.executor().run_until_parked();
522 assert_eq!(
523 &separate_with_comment_lines(
524 indoc! {r#"
525mod foo «1{
526 fn process_data_1() {
527 let map: Option<Vec<()>> = None;
528 }
529"#},
530 indoc! {r#"
531 fn process_data_2«2()2» «2{
532 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
533 }
534 «3{«4{}4»}3»}2»}1»
535
5361 hsla(207.80, 16.20%, 69.19%, 1.00)
5372 hsla(29.00, 54.00%, 65.88%, 1.00)
5383 hsla(286.00, 51.00%, 75.25%, 1.00)
5394 hsla(187.00, 47.00%, 59.22%, 1.00)
5405 hsla(355.00, 65.00%, 75.94%, 1.00)
541"#},
542 comment_lines,
543 ),
544 &bracket_colors_markup(&mut cx),
545 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
546 );
547
548 cx.update_editor(|editor, window, cx| {
549 editor.move_to_beginning(&MoveToBeginning, window, cx);
550 });
551 cx.executor().advance_clock(Duration::from_millis(100));
552 cx.executor().run_until_parked();
553 assert_eq!(
554 &separate_with_comment_lines(
555 indoc! {r#"
556mod foo «1{
557 fn process_data_1«2()2» «2{
558 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
559 }2»
560"#},
561 indoc! {r#"
562 fn process_data_2«2()2» «2{
563 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
564 }
565 «3{«4{}4»}3»}2»}1»
566
5671 hsla(207.80, 16.20%, 69.19%, 1.00)
5682 hsla(29.00, 54.00%, 65.88%, 1.00)
5693 hsla(286.00, 51.00%, 75.25%, 1.00)
5704 hsla(187.00, 47.00%, 59.22%, 1.00)
5715 hsla(355.00, 65.00%, 75.94%, 1.00)
572"#},
573 comment_lines,
574 ),
575 &bracket_colors_markup(&mut cx),
576 "Scrolling back to top should re-colorize all chunks' brackets"
577 );
578
579 cx.update(|_, cx| {
580 SettingsStore::update_global(cx, |store, cx| {
581 store.update_user_settings(cx, |settings| {
582 settings.project.all_languages.defaults.colorize_brackets = Some(false);
583 });
584 });
585 });
586 assert_eq!(
587 &separate_with_comment_lines(
588 indoc! {r#"
589mod foo {
590 fn process_data_1() {
591 let map: Option<Vec<()>> = None;
592 }
593"#},
594 r#" fn process_data_2() {
595 let map: Option<Vec<()>> = None;
596 }
597 {{}}}}
598
599"#,
600 comment_lines,
601 ),
602 &bracket_colors_markup(&mut cx),
603 "Turning bracket colorization off should remove all bracket colors"
604 );
605
606 cx.update(|_, cx| {
607 SettingsStore::update_global(cx, |store, cx| {
608 store.update_user_settings(cx, |settings| {
609 settings.project.all_languages.defaults.colorize_brackets = Some(true);
610 });
611 });
612 });
613 assert_eq!(
614 &separate_with_comment_lines(
615 indoc! {r#"
616mod foo «1{
617 fn process_data_1«2()2» «2{
618 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
619 }2»
620"#},
621 r#" fn process_data_2() {
622 let map: Option<Vec<()>> = None;
623 }
624 {{}}}}1»
625
6261 hsla(207.80, 16.20%, 69.19%, 1.00)
6272 hsla(29.00, 54.00%, 65.88%, 1.00)
6283 hsla(286.00, 51.00%, 75.25%, 1.00)
6294 hsla(187.00, 47.00%, 59.22%, 1.00)
6305 hsla(355.00, 65.00%, 75.94%, 1.00)
631"#,
632 comment_lines,
633 ),
634 &bracket_colors_markup(&mut cx),
635 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
636 );
637 }
638
639 #[gpui::test]
640 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
641 init_test(cx, |language_settings| {
642 language_settings.defaults.colorize_brackets = Some(true);
643 });
644 let mut cx = EditorLspTestContext::new(
645 Arc::into_inner(rust_lang()).unwrap(),
646 lsp::ServerCapabilities::default(),
647 cx,
648 )
649 .await;
650
651 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
652 cx.set_state(indoc! {r#"ˇ
653 pub(crate) fn inlay_hints(
654 db: &RootDatabase,
655 file_id: FileId,
656 range_limit: Option<TextRange>,
657 config: &InlayHintsConfig,
658 ) -> Vec<InlayHint> {
659 let _p = tracing::info_span!("inlay_hints").entered();
660 let sema = Semantics::new(db);
661 let file_id = sema
662 .attach_first_edition(file_id)
663 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
664 let file = sema.parse(file_id);
665 let file = file.syntax();
666
667 let mut acc = Vec::new();
668
669 let Some(scope) = sema.scope(file) else {
670 return acc;
671 };
672 let famous_defs = FamousDefs(&sema, scope.krate());
673 let display_target = famous_defs.1.to_display_target(sema.db);
674
675 let ctx = &mut InlayHintCtx::default();
676 let mut hints = |event| {
677 if let Some(node) = handle_event(ctx, event) {
678 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
679 }
680 };
681 let mut preorder = file.preorder();
682 salsa::attach(sema.db, || {
683 while let Some(event) = preorder.next() {
684 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
685 {
686 preorder.skip_subtree();
687 continue;
688 }
689 hints(event);
690 }
691 });
692 if let Some(range_limit) = range_limit {
693 acc.retain(|hint| range_limit.contains_range(hint.range));
694 }
695 acc
696 }
697
698 #[derive(Default)]
699 struct InlayHintCtx {
700 lifetime_stacks: Vec<Vec<SmolStr>>,
701 extern_block_parent: Option<ast::ExternBlock>,
702 }
703
704 pub(crate) fn inlay_hints_resolve(
705 db: &RootDatabase,
706 file_id: FileId,
707 resolve_range: TextRange,
708 hash: u64,
709 config: &InlayHintsConfig,
710 hasher: impl Fn(&InlayHint) -> u64,
711 ) -> Option<InlayHint> {
712 let _p = tracing::info_span!("inlay_hints_resolve").entered();
713 let sema = Semantics::new(db);
714 let file_id = sema
715 .attach_first_edition(file_id)
716 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
717 let file = sema.parse(file_id);
718 let file = file.syntax();
719
720 let scope = sema.scope(file)?;
721 let famous_defs = FamousDefs(&sema, scope.krate());
722 let mut acc = Vec::new();
723
724 let display_target = famous_defs.1.to_display_target(sema.db);
725
726 let ctx = &mut InlayHintCtx::default();
727 let mut hints = |event| {
728 if let Some(node) = handle_event(ctx, event) {
729 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
730 }
731 };
732
733 let mut preorder = file.preorder();
734 while let Some(event) = preorder.next() {
735 // This can miss some hints that require the parent of the range to calculate
736 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
737 {
738 preorder.skip_subtree();
739 continue;
740 }
741 hints(event);
742 }
743 acc.into_iter().find(|hint| hasher(hint) == hash)
744 }
745
746 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
747 match node {
748 WalkEvent::Enter(node) => {
749 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
750 let params = node
751 .generic_param_list()
752 .map(|it| {
753 it.lifetime_params()
754 .filter_map(|it| {
755 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
756 })
757 .collect()
758 })
759 .unwrap_or_default();
760 ctx.lifetime_stacks.push(params);
761 }
762 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
763 ctx.extern_block_parent = Some(node);
764 }
765 Some(node)
766 }
767 WalkEvent::Leave(n) => {
768 if ast::AnyHasGenericParams::can_cast(n.kind()) {
769 ctx.lifetime_stacks.pop();
770 }
771 if ast::ExternBlock::can_cast(n.kind()) {
772 ctx.extern_block_parent = None;
773 }
774 None
775 }
776 }
777 }
778
779 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
780 // HIR instead of the syntax tree.
781 fn hints(
782 hints: &mut Vec<InlayHint>,
783 ctx: &mut InlayHintCtx,
784 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
785 config: &InlayHintsConfig,
786 file_id: EditionedFileId,
787 display_target: DisplayTarget,
788 node: SyntaxNode,
789 ) {
790 closing_brace::hints(
791 hints,
792 sema,
793 config,
794 display_target,
795 InRealFile { file_id, value: node.clone() },
796 );
797 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
798 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
799 }
800
801 match_ast! {
802 match node {
803 ast::Expr(expr) => {
804 chaining::hints(hints, famous_defs, config, display_target, &expr);
805 adjustment::hints(hints, famous_defs, config, display_target, &expr);
806 match expr {
807 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
808 ast::Expr::MethodCallExpr(it) => {
809 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
810 }
811 ast::Expr::ClosureExpr(it) => {
812 closure_captures::hints(hints, famous_defs, config, it.clone());
813 closure_ret::hints(hints, famous_defs, config, display_target, it)
814 },
815 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
816 _ => Some(()),
817 }
818 },
819 ast::Pat(it) => {
820 binding_mode::hints(hints, famous_defs, config, &it);
821 match it {
822 ast::Pat::IdentPat(it) => {
823 bind_pat::hints(hints, famous_defs, config, display_target, &it);
824 }
825 ast::Pat::RangePat(it) => {
826 range_exclusive::hints(hints, famous_defs, config, it);
827 }
828 _ => {}
829 }
830 Some(())
831 },
832 ast::Item(it) => match it {
833 ast::Item::Fn(it) => {
834 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
835 if let Some(extern_block) = &ctx.extern_block_parent {
836 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
837 }
838 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
839 },
840 ast::Item::Static(it) => {
841 if let Some(extern_block) = &ctx.extern_block_parent {
842 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
843 }
844 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
845 },
846 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
847 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
848 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
849 _ => None,
850 },
851 // trait object type elisions
852 ast::Type(ty) => match ty {
853 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
854 ast::Type::PathType(path) => {
855 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
856 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
857 Some(())
858 },
859 ast::Type::DynTraitType(dyn_) => {
860 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
861 Some(())
862 },
863 _ => Some(()),
864 },
865 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
866 _ => Some(()),
867 }
868 };
869 }
870 "#});
871 cx.executor().advance_clock(Duration::from_millis(100));
872 cx.executor().run_until_parked();
873
874 let actual_ranges = cx.update_editor(|editor, window, cx| {
875 editor
876 .snapshot(window, cx)
877 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
878 });
879
880 let mut highlighted_brackets = HashMap::default();
881 for (color, range) in actual_ranges.iter().cloned() {
882 highlighted_brackets.insert(range, color);
883 }
884
885 let last_bracket = actual_ranges
886 .iter()
887 .max_by_key(|(_, p)| p.end.row)
888 .unwrap()
889 .clone();
890
891 cx.update_editor(|editor, window, cx| {
892 let was_scrolled = editor.set_scroll_position(
893 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
894 window,
895 cx,
896 );
897 assert!(was_scrolled.0);
898 });
899 cx.executor().advance_clock(Duration::from_millis(100));
900 cx.executor().run_until_parked();
901
902 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
903 editor
904 .snapshot(window, cx)
905 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
906 });
907 let new_last_bracket = ranges_after_scrolling
908 .iter()
909 .max_by_key(|(_, p)| p.end.row)
910 .unwrap()
911 .clone();
912
913 assert_ne!(
914 last_bracket, new_last_bracket,
915 "After scrolling down, we should have highlighted more brackets"
916 );
917
918 cx.update_editor(|editor, window, cx| {
919 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
920 assert!(was_scrolled.0);
921 });
922
923 for _ in 0..200 {
924 cx.update_editor(|editor, window, cx| {
925 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
926 });
927 cx.executor().advance_clock(Duration::from_millis(100));
928 cx.executor().run_until_parked();
929
930 let colored_brackets = cx.update_editor(|editor, window, cx| {
931 editor
932 .snapshot(window, cx)
933 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
934 });
935 for (color, range) in colored_brackets.clone() {
936 assert!(
937 highlighted_brackets.entry(range).or_insert(color) == &color,
938 "Colors should stay consistent while scrolling!"
939 );
940 }
941
942 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
943 let scroll_position = snapshot.scroll_position();
944 let visible_lines =
945 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
946 let visible_range = DisplayRow(scroll_position.y as u32)
947 ..DisplayRow((scroll_position.y + visible_lines) as u32);
948
949 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
950 colored_brackets
951 .iter()
952 .flat_map(|(_, range)| [range.start, range.end]),
953 );
954
955 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
956 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
957 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
958 }) {
959 assert!(
960 current_highlighted_bracket_set.contains(&highlight_range.start)
961 || current_highlighted_bracket_set.contains(&highlight_range.end),
962 "Should not lose highlights while scrolling in the visible range!"
963 );
964 }
965
966 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
967 for bracket_match in buffer_snapshot
968 .fetch_bracket_ranges(
969 snapshot
970 .display_point_to_point(
971 DisplayPoint::new(visible_range.start, 0),
972 Bias::Left,
973 )
974 .to_offset(&buffer_snapshot)
975 ..snapshot
976 .display_point_to_point(
977 DisplayPoint::new(
978 visible_range.end,
979 snapshot.line_len(visible_range.end),
980 ),
981 Bias::Right,
982 )
983 .to_offset(&buffer_snapshot),
984 None,
985 )
986 .iter()
987 .flat_map(|entry| entry.1)
988 .filter(|bracket_match| bracket_match.color_index.is_some())
989 {
990 let start = bracket_match.open_range.to_point(buffer_snapshot);
991 let end = bracket_match.close_range.to_point(buffer_snapshot);
992 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
993 assert!(
994 start_bracket.is_some(),
995 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
996 buffer_snapshot
997 .text_for_range(start.start..end.end)
998 .collect::<String>(),
999 start
1000 );
1001
1002 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1003 assert!(
1004 end_bracket.is_some(),
1005 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1006 buffer_snapshot
1007 .text_for_range(start.start..end.end)
1008 .collect::<String>(),
1009 start
1010 );
1011
1012 assert_eq!(
1013 start_bracket.unwrap().0,
1014 end_bracket.unwrap().0,
1015 "Bracket pair should be highlighted the same color!"
1016 )
1017 }
1018 }
1019 }
1020
1021 #[gpui::test]
1022 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1023 let comment_lines = 100;
1024
1025 init_test(cx, |language_settings| {
1026 language_settings.defaults.colorize_brackets = Some(true);
1027 });
1028 let fs = FakeFs::new(cx.background_executor.clone());
1029 fs.insert_tree(
1030 path!("/a"),
1031 json!({
1032 "main.rs": "fn main() {{()}}",
1033 "lib.rs": separate_with_comment_lines(
1034 indoc! {r#"
1035 mod foo {
1036 fn process_data_1() {
1037 let map: Option<Vec<()>> = None;
1038 // a
1039 // b
1040 // c
1041 }
1042 "#},
1043 indoc! {r#"
1044 fn process_data_2() {
1045 let other_map: Option<Vec<()>> = None;
1046 }
1047 }
1048 "#},
1049 comment_lines,
1050 )
1051 }),
1052 )
1053 .await;
1054
1055 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1056 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1057 language_registry.add(rust_lang());
1058
1059 let buffer_1 = project
1060 .update(cx, |project, cx| {
1061 project.open_local_buffer(path!("/a/lib.rs"), cx)
1062 })
1063 .await
1064 .unwrap();
1065 let buffer_2 = project
1066 .update(cx, |project, cx| {
1067 project.open_local_buffer(path!("/a/main.rs"), cx)
1068 })
1069 .await
1070 .unwrap();
1071
1072 let multi_buffer = cx.new(|cx| {
1073 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1074 multi_buffer.push_excerpts(
1075 buffer_2.clone(),
1076 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1077 cx,
1078 );
1079
1080 let excerpt_rows = 5;
1081 let rest_of_first_except_rows = 3;
1082 multi_buffer.push_excerpts(
1083 buffer_1.clone(),
1084 [
1085 ExcerptRange::new(Point::new(0, 0)..Point::new(excerpt_rows, 0)),
1086 ExcerptRange::new(
1087 Point::new(
1088 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1089 0,
1090 )
1091 ..Point::new(
1092 comment_lines as u32
1093 + excerpt_rows
1094 + rest_of_first_except_rows
1095 + excerpt_rows,
1096 0,
1097 ),
1098 ),
1099 ],
1100 cx,
1101 );
1102 multi_buffer
1103 });
1104
1105 let editor = cx.add_window(|window, cx| {
1106 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1107 });
1108 cx.executor().advance_clock(Duration::from_millis(100));
1109 cx.executor().run_until_parked();
1110
1111 let editor_snapshot = editor
1112 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1113 .unwrap();
1114 assert_eq!(
1115 indoc! {r#"
1116
1117
1118fn main«1()1» «1{«2{«3()3»}2»}1»
1119
1120
1121mod foo «1{
1122 fn process_data_1«2()2» «2{
1123 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1124 // a
1125 // b
1126
1127
1128 fn process_data_2«2()2» «2{
1129 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1130 }2»
1131}1»
1132
11331 hsla(207.80, 16.20%, 69.19%, 1.00)
11342 hsla(29.00, 54.00%, 65.88%, 1.00)
11353 hsla(286.00, 51.00%, 75.25%, 1.00)
11364 hsla(187.00, 47.00%, 59.22%, 1.00)
11375 hsla(355.00, 65.00%, 75.94%, 1.00)
1138"#,},
1139 &editor_bracket_colors_markup(&editor_snapshot),
1140 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1141or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1142 );
1143
1144 editor
1145 .update(cx, |editor, window, cx| {
1146 editor.handle_input("{[]", window, cx);
1147 })
1148 .unwrap();
1149 cx.executor().advance_clock(Duration::from_millis(100));
1150 cx.executor().run_until_parked();
1151 let editor_snapshot = editor
1152 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1153 .unwrap();
1154 assert_eq!(
1155 indoc! {r#"
1156
1157
1158{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1159
1160
1161mod foo «1{
1162 fn process_data_1«2()2» «2{
1163 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1164 // a
1165 // b
1166
1167
1168 fn process_data_2«2()2» «2{
1169 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1170 }2»
1171}1»
1172
11731 hsla(207.80, 16.20%, 69.19%, 1.00)
11742 hsla(29.00, 54.00%, 65.88%, 1.00)
11753 hsla(286.00, 51.00%, 75.25%, 1.00)
11764 hsla(187.00, 47.00%, 59.22%, 1.00)
11775 hsla(355.00, 65.00%, 75.94%, 1.00)
1178"#,},
1179 &editor_bracket_colors_markup(&editor_snapshot),
1180 );
1181
1182 cx.update(|cx| {
1183 let theme = cx.theme().name.clone();
1184 SettingsStore::update_global(cx, |store, cx| {
1185 store.update_user_settings(cx, |settings| {
1186 settings.theme.theme_overrides = HashMap::from_iter([(
1187 theme.to_string(),
1188 ThemeStyleContent {
1189 accents: vec![
1190 AccentContent(Some(SharedString::new("#ff0000"))),
1191 AccentContent(Some(SharedString::new("#0000ff"))),
1192 ],
1193 ..ThemeStyleContent::default()
1194 },
1195 )]);
1196 });
1197 });
1198 });
1199 cx.executor().advance_clock(Duration::from_millis(100));
1200 cx.executor().run_until_parked();
1201 let editor_snapshot = editor
1202 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1203 .unwrap();
1204 assert_eq!(
1205 indoc! {r#"
1206
1207
1208{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1209
1210
1211mod foo «1{
1212 fn process_data_1«2()2» «2{
1213 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1214 // a
1215 // b
1216
1217
1218 fn process_data_2«2()2» «2{
1219 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1220 }2»
1221}1»
1222
12231 hsla(0.00, 100.00%, 78.12%, 1.00)
12242 hsla(240.00, 100.00%, 82.81%, 1.00)
1225"#,},
1226 &editor_bracket_colors_markup(&editor_snapshot),
1227 "After updating theme accents, the editor should update the bracket coloring"
1228 );
1229 }
1230
1231 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1232 let mut result = head.to_string();
1233 result.push_str("\n");
1234 result.push_str(&"//\n".repeat(comment_lines));
1235 result.push_str(tail);
1236 result
1237 }
1238
1239 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1240 cx.update_editor(|editor, window, cx| {
1241 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1242 })
1243 }
1244
1245 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1246 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1247 let mut offset = 0;
1248 for (row_idx, line) in text.lines().enumerate() {
1249 if row_idx < point.row().0 as usize {
1250 offset += line.len() + 1; // +1 for newline
1251 } else {
1252 offset += point.column() as usize;
1253 break;
1254 }
1255 }
1256 offset
1257 }
1258
1259 let actual_ranges = snapshot.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
1260 let editor_text = snapshot.text();
1261
1262 let mut next_index = 1;
1263 let mut color_to_index = HashMap::default();
1264 let mut annotations = Vec::new();
1265 for (color, range) in &actual_ranges {
1266 let color_index = *color_to_index
1267 .entry(*color)
1268 .or_insert_with(|| post_inc(&mut next_index));
1269 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1270 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1271 let start_offset = display_point_to_offset(&editor_text, start);
1272 let end_offset = display_point_to_offset(&editor_text, end);
1273 let bracket_text = &editor_text[start_offset..end_offset];
1274 let bracket_char = bracket_text.chars().next().unwrap();
1275
1276 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1277 annotations.push((start_offset, format!("«{color_index}")));
1278 } else {
1279 annotations.push((end_offset, format!("{color_index}»")));
1280 }
1281 }
1282
1283 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1284 pos_a.cmp(pos_b).reverse().then_with(|| {
1285 let a_is_opening = text_a.starts_with('«');
1286 let b_is_opening = text_b.starts_with('«');
1287 match (a_is_opening, b_is_opening) {
1288 (true, false) => cmp::Ordering::Less,
1289 (false, true) => cmp::Ordering::Greater,
1290 _ => cmp::Ordering::Equal,
1291 }
1292 })
1293 });
1294 annotations.dedup();
1295
1296 let mut markup = editor_text;
1297 for (offset, text) in annotations {
1298 markup.insert_str(offset, &text);
1299 }
1300
1301 markup.push_str("\n");
1302 for (index, color) in color_to_index
1303 .iter()
1304 .map(|(color, index)| (*index, *color))
1305 .sorted_by_key(|(index, _)| *index)
1306 {
1307 markup.push_str(&format!("{index} {color}\n"));
1308 }
1309
1310 markup
1311 }
1312}