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(false, cx).into_iter().fold(
47 HashMap::default(),
48 |mut acc, (excerpt_id, (buffer, _, 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(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, EditorMode, 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_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
281 init_test(cx, |language_settings| {
282 language_settings.defaults.colorize_brackets = Some(true);
283 });
284 let editor = cx.add_window(|window, cx| {
285 let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
286 multi_buffer.update(cx, |multi_buffer, cx| {
287 multi_buffer
288 .as_singleton()
289 .unwrap()
290 .update(cx, |buffer, cx| {
291 buffer.set_language(Some(rust_lang()), cx);
292 });
293 });
294 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
295 });
296
297 cx.executor().advance_clock(Duration::from_millis(100));
298 cx.executor().run_until_parked();
299
300 assert_eq!(
301 "fn main«1()1» «1{}1»
3021 hsla(207.80, 16.20%, 69.19%, 1.00)
303",
304 editor
305 .update(cx, |editor, window, cx| {
306 editor_bracket_colors_markup(&editor.snapshot(window, cx))
307 })
308 .unwrap(),
309 "File-less buffer should still have its brackets colorized"
310 );
311 }
312
313 #[gpui::test]
314 async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
315 init_test(cx, |language_settings| {
316 language_settings.defaults.colorize_brackets = Some(true);
317 });
318 let mut cx = EditorLspTestContext::new(
319 Arc::into_inner(markdown_lang()).unwrap(),
320 lsp::ServerCapabilities::default(),
321 cx,
322 )
323 .await;
324
325 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)"#});
326 cx.executor().advance_clock(Duration::from_millis(100));
327 cx.executor().run_until_parked();
328
329 assert_eq!(
330 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»
3311 hsla(207.80, 16.20%, 69.19%, 1.00)
332"#,
333 &bracket_colors_markup(&mut cx),
334 "All markdown brackets should be colored based on their depth"
335 );
336
337 cx.set_state(indoc! {r#"ˇ{{}}"#});
338 cx.executor().advance_clock(Duration::from_millis(100));
339 cx.executor().run_until_parked();
340
341 assert_eq!(
342 r#"«1{«2{}2»}1»
3431 hsla(207.80, 16.20%, 69.19%, 1.00)
3442 hsla(29.00, 54.00%, 65.88%, 1.00)
345"#,
346 &bracket_colors_markup(&mut cx),
347 "All markdown brackets should be colored based on their depth, again"
348 );
349 }
350
351 #[gpui::test]
352 async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
353 init_test(cx, |language_settings| {
354 language_settings.defaults.colorize_brackets = Some(true);
355 });
356
357 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
358 language_registry.add(markdown_lang());
359 language_registry.add(rust_lang());
360
361 let mut cx = EditorTestContext::new(cx).await;
362 cx.update_buffer(|buffer, cx| {
363 buffer.set_language_registry(language_registry.clone());
364 buffer.set_language(Some(markdown_lang()), cx);
365 });
366
367 cx.set_state(indoc! {r#"
368 fn main() {
369 let v: Vec<Stringˇ> = vec![];
370 }
371 "#});
372 cx.executor().advance_clock(Duration::from_millis(100));
373 cx.executor().run_until_parked();
374
375 assert_eq!(
376 r#"fn main«1()1» «1{
377 let v: Vec<String> = vec!«2[]2»;
378}1»
379
3801 hsla(207.80, 16.20%, 69.19%, 1.00)
3812 hsla(29.00, 54.00%, 65.88%, 1.00)
382"#,
383 &bracket_colors_markup(&mut cx),
384 "Markdown does not colorize <> brackets"
385 );
386
387 cx.update_buffer(|buffer, cx| {
388 buffer.set_language(Some(rust_lang()), cx);
389 });
390 cx.executor().advance_clock(Duration::from_millis(100));
391 cx.executor().run_until_parked();
392
393 assert_eq!(
394 r#"fn main«1()1» «1{
395 let v: Vec«2<String>2» = vec!«2[]2»;
396}1»
397
3981 hsla(207.80, 16.20%, 69.19%, 1.00)
3992 hsla(29.00, 54.00%, 65.88%, 1.00)
400"#,
401 &bracket_colors_markup(&mut cx),
402 "After switching to Rust, <> brackets are now colorized"
403 );
404 }
405
406 #[gpui::test]
407 async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
408 init_test(cx, |language_settings| {
409 language_settings.defaults.colorize_brackets = Some(true);
410 });
411 let mut cx = EditorLspTestContext::new(
412 Arc::into_inner(rust_lang()).unwrap(),
413 lsp::ServerCapabilities::default(),
414 cx,
415 )
416 .await;
417
418 cx.set_state(indoc! {r#"
419struct Foo<'a, T> {
420 data: Vec<Option<&'a T>>,
421}
422
423fn process_data() {
424 let map:ˇ
425}
426"#});
427
428 cx.update_editor(|editor, window, cx| {
429 editor.handle_input(" Result<", window, cx);
430 });
431 cx.executor().advance_clock(Duration::from_millis(100));
432 cx.executor().run_until_parked();
433 assert_eq!(
434 indoc! {r#"
435struct Foo«1<'a, T>1» «1{
436 data: Vec«2<Option«3<&'a T>3»>2»,
437}1»
438
439fn process_data«1()1» «1{
440 let map: Result<
441}1»
442
4431 hsla(207.80, 16.20%, 69.19%, 1.00)
4442 hsla(29.00, 54.00%, 65.88%, 1.00)
4453 hsla(286.00, 51.00%, 75.25%, 1.00)
446"#},
447 &bracket_colors_markup(&mut cx),
448 "Brackets without pairs should be ignored and not colored"
449 );
450
451 cx.update_editor(|editor, window, cx| {
452 editor.handle_input("Option<Foo<'_, ()", window, cx);
453 });
454 cx.executor().advance_clock(Duration::from_millis(100));
455 cx.executor().run_until_parked();
456 assert_eq!(
457 indoc! {r#"
458struct Foo«1<'a, T>1» «1{
459 data: Vec«2<Option«3<&'a T>3»>2»,
460}1»
461
462fn process_data«1()1» «1{
463 let map: Result<Option<Foo<'_, «2()2»
464}1»
465
4661 hsla(207.80, 16.20%, 69.19%, 1.00)
4672 hsla(29.00, 54.00%, 65.88%, 1.00)
4683 hsla(286.00, 51.00%, 75.25%, 1.00)
469"#},
470 &bracket_colors_markup(&mut cx),
471 );
472
473 cx.update_editor(|editor, window, cx| {
474 editor.handle_input(">", window, cx);
475 });
476 cx.executor().advance_clock(Duration::from_millis(100));
477 cx.executor().run_until_parked();
478 assert_eq!(
479 indoc! {r#"
480struct Foo«1<'a, T>1» «1{
481 data: Vec«2<Option«3<&'a T>3»>2»,
482}1»
483
484fn process_data«1()1» «1{
485 let map: Result<Option<Foo«2<'_, «3()3»>2»
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)
491"#},
492 &bracket_colors_markup(&mut cx),
493 "When brackets start to get closed, inner brackets are re-colored based on their depth"
494 );
495
496 cx.update_editor(|editor, window, cx| {
497 editor.handle_input(">", window, cx);
498 });
499 cx.executor().advance_clock(Duration::from_millis(100));
500 cx.executor().run_until_parked();
501 assert_eq!(
502 indoc! {r#"
503struct Foo«1<'a, T>1» «1{
504 data: Vec«2<Option«3<&'a T>3»>2»,
505}1»
506
507fn process_data«1()1» «1{
508 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
509}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)
515"#},
516 &bracket_colors_markup(&mut cx),
517 );
518
519 cx.update_editor(|editor, window, cx| {
520 editor.handle_input(", ()> = unimplemented!();", window, cx);
521 });
522 cx.executor().advance_clock(Duration::from_millis(100));
523 cx.executor().run_until_parked();
524 assert_eq!(
525 indoc! {r#"
526struct Foo«1<'a, T>1» «1{
527 data: Vec«2<Option«3<&'a T>3»>2»,
528}1»
529
530fn process_data«1()1» «1{
531 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
532}1»
533
5341 hsla(207.80, 16.20%, 69.19%, 1.00)
5352 hsla(29.00, 54.00%, 65.88%, 1.00)
5363 hsla(286.00, 51.00%, 75.25%, 1.00)
5374 hsla(187.00, 47.00%, 59.22%, 1.00)
5385 hsla(355.00, 65.00%, 75.94%, 1.00)
539"#},
540 &bracket_colors_markup(&mut cx),
541 );
542 }
543
544 #[gpui::test]
545 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
546 let comment_lines = 100;
547
548 init_test(cx, |language_settings| {
549 language_settings.defaults.colorize_brackets = Some(true);
550 });
551 let mut cx = EditorLspTestContext::new(
552 Arc::into_inner(rust_lang()).unwrap(),
553 lsp::ServerCapabilities::default(),
554 cx,
555 )
556 .await;
557
558 cx.set_state(&separate_with_comment_lines(
559 indoc! {r#"
560mod foo {
561 ˇfn process_data_1() {
562 let map: Option<Vec<()>> = None;
563 }
564"#},
565 indoc! {r#"
566 fn process_data_2() {
567 let map: Option<Vec<()>> = None;
568 }
569}
570"#},
571 comment_lines,
572 ));
573
574 cx.executor().advance_clock(Duration::from_millis(100));
575 cx.executor().run_until_parked();
576 assert_eq!(
577 &separate_with_comment_lines(
578 indoc! {r#"
579mod foo «1{
580 fn process_data_1«2()2» «2{
581 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
582 }2»
583"#},
584 indoc! {r#"
585 fn process_data_2() {
586 let map: Option<Vec<()>> = None;
587 }
588}1»
589
5901 hsla(207.80, 16.20%, 69.19%, 1.00)
5912 hsla(29.00, 54.00%, 65.88%, 1.00)
5923 hsla(286.00, 51.00%, 75.25%, 1.00)
5934 hsla(187.00, 47.00%, 59.22%, 1.00)
5945 hsla(355.00, 65.00%, 75.94%, 1.00)
595"#},
596 comment_lines,
597 ),
598 &bracket_colors_markup(&mut cx),
599 "First, the only visible chunk is getting the bracket highlights"
600 );
601
602 cx.update_editor(|editor, window, cx| {
603 editor.move_to_end(&MoveToEnd, window, cx);
604 editor.move_up(&MoveUp, window, cx);
605 });
606 cx.executor().advance_clock(Duration::from_millis(100));
607 cx.executor().run_until_parked();
608 assert_eq!(
609 &separate_with_comment_lines(
610 indoc! {r#"
611mod foo «1{
612 fn process_data_1«2()2» «2{
613 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
614 }2»
615"#},
616 indoc! {r#"
617 fn process_data_2«2()2» «2{
618 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
619 }2»
620}1»
621
6221 hsla(207.80, 16.20%, 69.19%, 1.00)
6232 hsla(29.00, 54.00%, 65.88%, 1.00)
6243 hsla(286.00, 51.00%, 75.25%, 1.00)
6254 hsla(187.00, 47.00%, 59.22%, 1.00)
6265 hsla(355.00, 65.00%, 75.94%, 1.00)
627"#},
628 comment_lines,
629 ),
630 &bracket_colors_markup(&mut cx),
631 "After scrolling to the bottom, both chunks should have the highlights"
632 );
633
634 cx.update_editor(|editor, window, cx| {
635 editor.handle_input("{{}}}", window, cx);
636 });
637 cx.executor().advance_clock(Duration::from_millis(100));
638 cx.executor().run_until_parked();
639 assert_eq!(
640 &separate_with_comment_lines(
641 indoc! {r#"
642mod foo «1{
643 fn process_data_1() {
644 let map: Option<Vec<()>> = None;
645 }
646"#},
647 indoc! {r#"
648 fn process_data_2«2()2» «2{
649 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
650 }
651 «3{«4{}4»}3»}2»}1»
652
6531 hsla(207.80, 16.20%, 69.19%, 1.00)
6542 hsla(29.00, 54.00%, 65.88%, 1.00)
6553 hsla(286.00, 51.00%, 75.25%, 1.00)
6564 hsla(187.00, 47.00%, 59.22%, 1.00)
6575 hsla(355.00, 65.00%, 75.94%, 1.00)
658"#},
659 comment_lines,
660 ),
661 &bracket_colors_markup(&mut cx),
662 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
663 );
664
665 cx.update_editor(|editor, window, cx| {
666 editor.move_to_beginning(&MoveToBeginning, window, cx);
667 });
668 cx.executor().advance_clock(Duration::from_millis(100));
669 cx.executor().run_until_parked();
670 assert_eq!(
671 &separate_with_comment_lines(
672 indoc! {r#"
673mod foo «1{
674 fn process_data_1«2()2» «2{
675 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
676 }2»
677"#},
678 indoc! {r#"
679 fn process_data_2«2()2» «2{
680 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
681 }
682 «3{«4{}4»}3»}2»}1»
683
6841 hsla(207.80, 16.20%, 69.19%, 1.00)
6852 hsla(29.00, 54.00%, 65.88%, 1.00)
6863 hsla(286.00, 51.00%, 75.25%, 1.00)
6874 hsla(187.00, 47.00%, 59.22%, 1.00)
6885 hsla(355.00, 65.00%, 75.94%, 1.00)
689"#},
690 comment_lines,
691 ),
692 &bracket_colors_markup(&mut cx),
693 "Scrolling back to top should re-colorize all chunks' brackets"
694 );
695
696 cx.update(|_, cx| {
697 SettingsStore::update_global(cx, |store, cx| {
698 store.update_user_settings(cx, |settings| {
699 settings.project.all_languages.defaults.colorize_brackets = Some(false);
700 });
701 });
702 });
703 assert_eq!(
704 &separate_with_comment_lines(
705 indoc! {r#"
706mod foo {
707 fn process_data_1() {
708 let map: Option<Vec<()>> = None;
709 }
710"#},
711 r#" fn process_data_2() {
712 let map: Option<Vec<()>> = None;
713 }
714 {{}}}}
715
716"#,
717 comment_lines,
718 ),
719 &bracket_colors_markup(&mut cx),
720 "Turning bracket colorization off should remove all bracket colors"
721 );
722
723 cx.update(|_, cx| {
724 SettingsStore::update_global(cx, |store, cx| {
725 store.update_user_settings(cx, |settings| {
726 settings.project.all_languages.defaults.colorize_brackets = Some(true);
727 });
728 });
729 });
730 assert_eq!(
731 &separate_with_comment_lines(
732 indoc! {r#"
733mod foo «1{
734 fn process_data_1«2()2» «2{
735 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
736 }2»
737"#},
738 r#" fn process_data_2() {
739 let map: Option<Vec<()>> = None;
740 }
741 {{}}}}1»
742
7431 hsla(207.80, 16.20%, 69.19%, 1.00)
7442 hsla(29.00, 54.00%, 65.88%, 1.00)
7453 hsla(286.00, 51.00%, 75.25%, 1.00)
7464 hsla(187.00, 47.00%, 59.22%, 1.00)
7475 hsla(355.00, 65.00%, 75.94%, 1.00)
748"#,
749 comment_lines,
750 ),
751 &bracket_colors_markup(&mut cx),
752 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
753 );
754 }
755
756 #[gpui::test]
757 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
758 init_test(cx, |language_settings| {
759 language_settings.defaults.colorize_brackets = Some(true);
760 });
761 let mut cx = EditorLspTestContext::new(
762 Arc::into_inner(rust_lang()).unwrap(),
763 lsp::ServerCapabilities::default(),
764 cx,
765 )
766 .await;
767
768 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
769 cx.set_state(indoc! {r#"ˇ
770 pub(crate) fn inlay_hints(
771 db: &RootDatabase,
772 file_id: FileId,
773 range_limit: Option<TextRange>,
774 config: &InlayHintsConfig,
775 ) -> Vec<InlayHint> {
776 let _p = tracing::info_span!("inlay_hints").entered();
777 let sema = Semantics::new(db);
778 let file_id = sema
779 .attach_first_edition(file_id)
780 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
781 let file = sema.parse(file_id);
782 let file = file.syntax();
783
784 let mut acc = Vec::new();
785
786 let Some(scope) = sema.scope(file) else {
787 return acc;
788 };
789 let famous_defs = FamousDefs(&sema, scope.krate());
790 let display_target = famous_defs.1.to_display_target(sema.db);
791
792 let ctx = &mut InlayHintCtx::default();
793 let mut hints = |event| {
794 if let Some(node) = handle_event(ctx, event) {
795 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
796 }
797 };
798 let mut preorder = file.preorder();
799 salsa::attach(sema.db, || {
800 while let Some(event) = preorder.next() {
801 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
802 {
803 preorder.skip_subtree();
804 continue;
805 }
806 hints(event);
807 }
808 });
809 if let Some(range_limit) = range_limit {
810 acc.retain(|hint| range_limit.contains_range(hint.range));
811 }
812 acc
813 }
814
815 #[derive(Default)]
816 struct InlayHintCtx {
817 lifetime_stacks: Vec<Vec<SmolStr>>,
818 extern_block_parent: Option<ast::ExternBlock>,
819 }
820
821 pub(crate) fn inlay_hints_resolve(
822 db: &RootDatabase,
823 file_id: FileId,
824 resolve_range: TextRange,
825 hash: u64,
826 config: &InlayHintsConfig,
827 hasher: impl Fn(&InlayHint) -> u64,
828 ) -> Option<InlayHint> {
829 let _p = tracing::info_span!("inlay_hints_resolve").entered();
830 let sema = Semantics::new(db);
831 let file_id = sema
832 .attach_first_edition(file_id)
833 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
834 let file = sema.parse(file_id);
835 let file = file.syntax();
836
837 let scope = sema.scope(file)?;
838 let famous_defs = FamousDefs(&sema, scope.krate());
839 let mut acc = Vec::new();
840
841 let display_target = famous_defs.1.to_display_target(sema.db);
842
843 let ctx = &mut InlayHintCtx::default();
844 let mut hints = |event| {
845 if let Some(node) = handle_event(ctx, event) {
846 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
847 }
848 };
849
850 let mut preorder = file.preorder();
851 while let Some(event) = preorder.next() {
852 // This can miss some hints that require the parent of the range to calculate
853 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
854 {
855 preorder.skip_subtree();
856 continue;
857 }
858 hints(event);
859 }
860 acc.into_iter().find(|hint| hasher(hint) == hash)
861 }
862
863 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
864 match node {
865 WalkEvent::Enter(node) => {
866 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
867 let params = node
868 .generic_param_list()
869 .map(|it| {
870 it.lifetime_params()
871 .filter_map(|it| {
872 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
873 })
874 .collect()
875 })
876 .unwrap_or_default();
877 ctx.lifetime_stacks.push(params);
878 }
879 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
880 ctx.extern_block_parent = Some(node);
881 }
882 Some(node)
883 }
884 WalkEvent::Leave(n) => {
885 if ast::AnyHasGenericParams::can_cast(n.kind()) {
886 ctx.lifetime_stacks.pop();
887 }
888 if ast::ExternBlock::can_cast(n.kind()) {
889 ctx.extern_block_parent = None;
890 }
891 None
892 }
893 }
894 }
895
896 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
897 // HIR instead of the syntax tree.
898 fn hints(
899 hints: &mut Vec<InlayHint>,
900 ctx: &mut InlayHintCtx,
901 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
902 config: &InlayHintsConfig,
903 file_id: EditionedFileId,
904 display_target: DisplayTarget,
905 node: SyntaxNode,
906 ) {
907 closing_brace::hints(
908 hints,
909 sema,
910 config,
911 display_target,
912 InRealFile { file_id, value: node.clone() },
913 );
914 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
915 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
916 }
917
918 match_ast! {
919 match node {
920 ast::Expr(expr) => {
921 chaining::hints(hints, famous_defs, config, display_target, &expr);
922 adjustment::hints(hints, famous_defs, config, display_target, &expr);
923 match expr {
924 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
925 ast::Expr::MethodCallExpr(it) => {
926 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
927 }
928 ast::Expr::ClosureExpr(it) => {
929 closure_captures::hints(hints, famous_defs, config, it.clone());
930 closure_ret::hints(hints, famous_defs, config, display_target, it)
931 },
932 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
933 _ => Some(()),
934 }
935 },
936 ast::Pat(it) => {
937 binding_mode::hints(hints, famous_defs, config, &it);
938 match it {
939 ast::Pat::IdentPat(it) => {
940 bind_pat::hints(hints, famous_defs, config, display_target, &it);
941 }
942 ast::Pat::RangePat(it) => {
943 range_exclusive::hints(hints, famous_defs, config, it);
944 }
945 _ => {}
946 }
947 Some(())
948 },
949 ast::Item(it) => match it {
950 ast::Item::Fn(it) => {
951 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
952 if let Some(extern_block) = &ctx.extern_block_parent {
953 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
954 }
955 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
956 },
957 ast::Item::Static(it) => {
958 if let Some(extern_block) = &ctx.extern_block_parent {
959 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
960 }
961 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
962 },
963 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
964 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
965 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
966 _ => None,
967 },
968 // trait object type elisions
969 ast::Type(ty) => match ty {
970 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
971 ast::Type::PathType(path) => {
972 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
973 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
974 Some(())
975 },
976 ast::Type::DynTraitType(dyn_) => {
977 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
978 Some(())
979 },
980 _ => Some(()),
981 },
982 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
983 _ => Some(()),
984 }
985 };
986 }
987 "#});
988 cx.executor().advance_clock(Duration::from_millis(100));
989 cx.executor().run_until_parked();
990
991 let actual_ranges = cx.update_editor(|editor, window, cx| {
992 editor
993 .snapshot(window, cx)
994 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
995 });
996
997 let mut highlighted_brackets = HashMap::default();
998 for (color, range) in actual_ranges.iter().cloned() {
999 highlighted_brackets.insert(range, color);
1000 }
1001
1002 let last_bracket = actual_ranges
1003 .iter()
1004 .max_by_key(|(_, p)| p.end.row)
1005 .unwrap()
1006 .clone();
1007
1008 cx.update_editor(|editor, window, cx| {
1009 let was_scrolled = editor.set_scroll_position(
1010 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
1011 window,
1012 cx,
1013 );
1014 assert!(was_scrolled.0);
1015 });
1016 cx.executor().advance_clock(Duration::from_millis(100));
1017 cx.executor().run_until_parked();
1018
1019 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
1020 editor
1021 .snapshot(window, cx)
1022 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
1023 });
1024 let new_last_bracket = ranges_after_scrolling
1025 .iter()
1026 .max_by_key(|(_, p)| p.end.row)
1027 .unwrap()
1028 .clone();
1029
1030 assert_ne!(
1031 last_bracket, new_last_bracket,
1032 "After scrolling down, we should have highlighted more brackets"
1033 );
1034
1035 cx.update_editor(|editor, window, cx| {
1036 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
1037 assert!(was_scrolled.0);
1038 });
1039
1040 for _ in 0..200 {
1041 cx.update_editor(|editor, window, cx| {
1042 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
1043 });
1044 cx.executor().advance_clock(Duration::from_millis(100));
1045 cx.executor().run_until_parked();
1046
1047 let colored_brackets = cx.update_editor(|editor, window, cx| {
1048 editor
1049 .snapshot(window, cx)
1050 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
1051 });
1052 for (color, range) in colored_brackets.clone() {
1053 assert!(
1054 highlighted_brackets.entry(range).or_insert(color) == &color,
1055 "Colors should stay consistent while scrolling!"
1056 );
1057 }
1058
1059 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
1060 let scroll_position = snapshot.scroll_position();
1061 let visible_lines =
1062 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
1063 let visible_range = DisplayRow(scroll_position.y as u32)
1064 ..DisplayRow((scroll_position.y + visible_lines) as u32);
1065
1066 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
1067 colored_brackets
1068 .iter()
1069 .flat_map(|(_, range)| [range.start, range.end]),
1070 );
1071
1072 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
1073 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
1074 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
1075 }) {
1076 assert!(
1077 current_highlighted_bracket_set.contains(&highlight_range.start)
1078 || current_highlighted_bracket_set.contains(&highlight_range.end),
1079 "Should not lose highlights while scrolling in the visible range!"
1080 );
1081 }
1082
1083 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
1084 for bracket_match in buffer_snapshot
1085 .fetch_bracket_ranges(
1086 snapshot
1087 .display_point_to_point(
1088 DisplayPoint::new(visible_range.start, 0),
1089 Bias::Left,
1090 )
1091 .to_offset(&buffer_snapshot)
1092 ..snapshot
1093 .display_point_to_point(
1094 DisplayPoint::new(
1095 visible_range.end,
1096 snapshot.line_len(visible_range.end),
1097 ),
1098 Bias::Right,
1099 )
1100 .to_offset(&buffer_snapshot),
1101 None,
1102 )
1103 .iter()
1104 .flat_map(|entry| entry.1)
1105 .filter(|bracket_match| bracket_match.color_index.is_some())
1106 {
1107 let start = bracket_match.open_range.to_point(buffer_snapshot);
1108 let end = bracket_match.close_range.to_point(buffer_snapshot);
1109 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
1110 assert!(
1111 start_bracket.is_some(),
1112 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1113 buffer_snapshot
1114 .text_for_range(start.start..end.end)
1115 .collect::<String>(),
1116 start
1117 );
1118
1119 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1120 assert!(
1121 end_bracket.is_some(),
1122 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1123 buffer_snapshot
1124 .text_for_range(start.start..end.end)
1125 .collect::<String>(),
1126 start
1127 );
1128
1129 assert_eq!(
1130 start_bracket.unwrap().0,
1131 end_bracket.unwrap().0,
1132 "Bracket pair should be highlighted the same color!"
1133 )
1134 }
1135 }
1136 }
1137
1138 #[gpui::test]
1139 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1140 let comment_lines = 100;
1141
1142 init_test(cx, |language_settings| {
1143 language_settings.defaults.colorize_brackets = Some(true);
1144 });
1145 let fs = FakeFs::new(cx.background_executor.clone());
1146 fs.insert_tree(
1147 path!("/a"),
1148 json!({
1149 "main.rs": "fn main() {{()}}",
1150 "lib.rs": separate_with_comment_lines(
1151 indoc! {r#"
1152 mod foo {
1153 fn process_data_1() {
1154 let map: Option<Vec<()>> = None;
1155 // a
1156 // b
1157 // c
1158 }
1159 "#},
1160 indoc! {r#"
1161 fn process_data_2() {
1162 let other_map: Option<Vec<()>> = None;
1163 }
1164 }
1165 "#},
1166 comment_lines,
1167 )
1168 }),
1169 )
1170 .await;
1171
1172 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1173 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1174 language_registry.add(rust_lang());
1175
1176 let buffer_1 = project
1177 .update(cx, |project, cx| {
1178 project.open_local_buffer(path!("/a/lib.rs"), cx)
1179 })
1180 .await
1181 .unwrap();
1182 let buffer_2 = project
1183 .update(cx, |project, cx| {
1184 project.open_local_buffer(path!("/a/main.rs"), cx)
1185 })
1186 .await
1187 .unwrap();
1188
1189 let multi_buffer = cx.new(|cx| {
1190 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1191 multi_buffer.push_excerpts(
1192 buffer_2.clone(),
1193 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1194 cx,
1195 );
1196
1197 let excerpt_rows = 5;
1198 let rest_of_first_except_rows = 3;
1199 multi_buffer.push_excerpts(
1200 buffer_1.clone(),
1201 [
1202 ExcerptRange::new(Point::new(0, 0)..Point::new(excerpt_rows, 0)),
1203 ExcerptRange::new(
1204 Point::new(
1205 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1206 0,
1207 )
1208 ..Point::new(
1209 comment_lines as u32
1210 + excerpt_rows
1211 + rest_of_first_except_rows
1212 + excerpt_rows,
1213 0,
1214 ),
1215 ),
1216 ],
1217 cx,
1218 );
1219 multi_buffer
1220 });
1221
1222 let editor = cx.add_window(|window, cx| {
1223 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1224 });
1225 cx.executor().advance_clock(Duration::from_millis(100));
1226 cx.executor().run_until_parked();
1227
1228 let editor_snapshot = editor
1229 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1230 .unwrap();
1231 assert_eq!(
1232 indoc! {r#"
1233
1234
1235fn main«1()1» «1{«2{«3()3»}2»}1»
1236
1237
1238mod foo «1{
1239 fn process_data_1«2()2» «2{
1240 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1241 // a
1242 // b
1243
1244
1245 fn process_data_2«2()2» «2{
1246 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1247 }2»
1248}1»
1249
12501 hsla(207.80, 16.20%, 69.19%, 1.00)
12512 hsla(29.00, 54.00%, 65.88%, 1.00)
12523 hsla(286.00, 51.00%, 75.25%, 1.00)
12534 hsla(187.00, 47.00%, 59.22%, 1.00)
12545 hsla(355.00, 65.00%, 75.94%, 1.00)
1255"#,},
1256 &editor_bracket_colors_markup(&editor_snapshot),
1257 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1258or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1259 );
1260
1261 editor
1262 .update(cx, |editor, window, cx| {
1263 editor.handle_input("{[]", window, cx);
1264 })
1265 .unwrap();
1266 cx.executor().advance_clock(Duration::from_millis(100));
1267 cx.executor().run_until_parked();
1268 let editor_snapshot = editor
1269 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1270 .unwrap();
1271 assert_eq!(
1272 indoc! {r#"
1273
1274
1275{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1276
1277
1278mod foo «1{
1279 fn process_data_1«2()2» «2{
1280 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1281 // a
1282 // b
1283
1284
1285 fn process_data_2«2()2» «2{
1286 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1287 }2»
1288}1»
1289
12901 hsla(207.80, 16.20%, 69.19%, 1.00)
12912 hsla(29.00, 54.00%, 65.88%, 1.00)
12923 hsla(286.00, 51.00%, 75.25%, 1.00)
12934 hsla(187.00, 47.00%, 59.22%, 1.00)
12945 hsla(355.00, 65.00%, 75.94%, 1.00)
1295"#,},
1296 &editor_bracket_colors_markup(&editor_snapshot),
1297 );
1298
1299 cx.update(|cx| {
1300 let theme = cx.theme().name.clone();
1301 SettingsStore::update_global(cx, |store, cx| {
1302 store.update_user_settings(cx, |settings| {
1303 settings.theme.theme_overrides = HashMap::from_iter([(
1304 theme.to_string(),
1305 ThemeStyleContent {
1306 accents: vec![
1307 AccentContent(Some(SharedString::new("#ff0000"))),
1308 AccentContent(Some(SharedString::new("#0000ff"))),
1309 ],
1310 ..ThemeStyleContent::default()
1311 },
1312 )]);
1313 });
1314 });
1315 });
1316 cx.executor().advance_clock(Duration::from_millis(100));
1317 cx.executor().run_until_parked();
1318 let editor_snapshot = editor
1319 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1320 .unwrap();
1321 assert_eq!(
1322 indoc! {r#"
1323
1324
1325{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1326
1327
1328mod foo «1{
1329 fn process_data_1«2()2» «2{
1330 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1331 // a
1332 // b
1333
1334
1335 fn process_data_2«2()2» «2{
1336 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1337 }2»
1338}1»
1339
13401 hsla(0.00, 100.00%, 78.12%, 1.00)
13412 hsla(240.00, 100.00%, 82.81%, 1.00)
1342"#,},
1343 &editor_bracket_colors_markup(&editor_snapshot),
1344 "After updating theme accents, the editor should update the bracket coloring"
1345 );
1346 }
1347
1348 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1349 let mut result = head.to_string();
1350 result.push_str("\n");
1351 result.push_str(&"//\n".repeat(comment_lines));
1352 result.push_str(tail);
1353 result
1354 }
1355
1356 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1357 cx.update_editor(|editor, window, cx| {
1358 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1359 })
1360 }
1361
1362 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1363 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1364 let mut offset = 0;
1365 for (row_idx, line) in text.lines().enumerate() {
1366 if row_idx < point.row().0 as usize {
1367 offset += line.len() + 1; // +1 for newline
1368 } else {
1369 offset += point.column() as usize;
1370 break;
1371 }
1372 }
1373 offset
1374 }
1375
1376 let actual_ranges = snapshot.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
1377 let editor_text = snapshot.text();
1378
1379 let mut next_index = 1;
1380 let mut color_to_index = HashMap::default();
1381 let mut annotations = Vec::new();
1382 for (color, range) in &actual_ranges {
1383 let color_index = *color_to_index
1384 .entry(*color)
1385 .or_insert_with(|| post_inc(&mut next_index));
1386 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1387 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1388 let start_offset = display_point_to_offset(&editor_text, start);
1389 let end_offset = display_point_to_offset(&editor_text, end);
1390 let bracket_text = &editor_text[start_offset..end_offset];
1391 let bracket_char = bracket_text.chars().next().unwrap();
1392
1393 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1394 annotations.push((start_offset, format!("«{color_index}")));
1395 } else {
1396 annotations.push((end_offset, format!("{color_index}»")));
1397 }
1398 }
1399
1400 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1401 pos_a.cmp(pos_b).reverse().then_with(|| {
1402 let a_is_opening = text_a.starts_with('«');
1403 let b_is_opening = text_b.starts_with('«');
1404 match (a_is_opening, b_is_opening) {
1405 (true, false) => cmp::Ordering::Less,
1406 (false, true) => cmp::Ordering::Greater,
1407 _ => cmp::Ordering::Equal,
1408 }
1409 })
1410 });
1411 annotations.dedup();
1412
1413 let mut markup = editor_text;
1414 for (offset, text) in annotations {
1415 markup.insert_str(offset, &text);
1416 }
1417
1418 markup.push_str("\n");
1419 for (index, color) in color_to_index
1420 .iter()
1421 .map(|(color, index)| (*index, *color))
1422 .sorted_by_key(|(index, _)| *index)
1423 {
1424 markup.push_str(&format!("{index} {color}\n"));
1425 }
1426
1427 markup
1428 }
1429}