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_version, buffer_range))| {
49 let buffer_snapshot = buffer.read(cx).snapshot();
50 if language_settings::language_settings(
51 buffer_snapshot.language().map(|language| language.name()),
52 buffer_snapshot.file(),
53 cx,
54 )
55 .colorize_brackets
56 {
57 let fetched_chunks = self
58 .fetched_tree_sitter_chunks
59 .entry(excerpt_id)
60 .or_default();
61
62 let brackets_by_accent = buffer_snapshot
63 .fetch_bracket_ranges(
64 buffer_range.start..buffer_range.end,
65 Some((&buffer_version, fetched_chunks)),
66 )
67 .into_iter()
68 .flat_map(|(chunk_range, pairs)| {
69 if fetched_chunks.insert(chunk_range) {
70 pairs
71 } else {
72 Vec::new()
73 }
74 })
75 .filter_map(|pair| {
76 let color_index = pair.color_index?;
77
78 let buffer_open_range = buffer_snapshot
79 .anchor_before(pair.open_range.start)
80 ..buffer_snapshot.anchor_after(pair.open_range.end);
81 let buffer_close_range = buffer_snapshot
82 .anchor_before(pair.close_range.start)
83 ..buffer_snapshot.anchor_after(pair.close_range.end);
84 let [
85 buffer_open_range_start,
86 buffer_open_range_end,
87 buffer_close_range_start,
88 buffer_close_range_end,
89 ] = anchors_in_multi_buffer(
90 excerpt_id,
91 [
92 buffer_open_range.start,
93 buffer_open_range.end,
94 buffer_close_range.start,
95 buffer_close_range.end,
96 ],
97 )?;
98 let multi_buffer_open_range =
99 buffer_open_range_start.zip(buffer_open_range_end);
100 let multi_buffer_close_range =
101 buffer_close_range_start.zip(buffer_close_range_end);
102
103 let mut ranges = Vec::with_capacity(2);
104 if let Some((open_start, open_end)) = multi_buffer_open_range {
105 ranges.push(open_start..open_end);
106 }
107 if let Some((close_start, close_end)) = multi_buffer_close_range {
108 ranges.push(close_start..close_end);
109 }
110 if ranges.is_empty() {
111 None
112 } else {
113 Some((color_index % accents_count, ranges))
114 }
115 });
116
117 for (accent_number, new_ranges) in brackets_by_accent {
118 let ranges = acc
119 .entry(accent_number)
120 .or_insert_with(Vec::<Range<Anchor>>::new);
121
122 for new_range in new_ranges {
123 let i = ranges
124 .binary_search_by(|probe| {
125 probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
126 })
127 .unwrap_or_else(|i| i);
128 ranges.insert(i, new_range);
129 }
130 }
131 }
132
133 acc
134 },
135 );
136
137 if invalidate {
138 self.clear_highlights::<ColorizedBracketsHighlight>(cx);
139 }
140
141 let editor_background = cx.theme().colors().editor_background;
142 for (accent_number, bracket_highlights) in bracket_matches_by_accent {
143 let bracket_color = cx.theme().accents().color_for_index(accent_number as u32);
144 let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
145 let style = HighlightStyle {
146 color: Some(adjusted_color),
147 ..HighlightStyle::default()
148 };
149
150 self.highlight_text_key::<ColorizedBracketsHighlight>(
151 accent_number,
152 bracket_highlights,
153 style,
154 true,
155 cx,
156 );
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use std::{cmp, sync::Arc, time::Duration};
164
165 use super::*;
166 use crate::{
167 DisplayPoint, 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_when_editing(cx: &mut gpui::TestAppContext) {
353 init_test(cx, |language_settings| {
354 language_settings.defaults.colorize_brackets = Some(true);
355 });
356 let mut cx = EditorLspTestContext::new(
357 Arc::into_inner(rust_lang()).unwrap(),
358 lsp::ServerCapabilities::default(),
359 cx,
360 )
361 .await;
362
363 cx.set_state(indoc! {r#"
364struct Foo<'a, T> {
365 data: Vec<Option<&'a T>>,
366}
367
368fn process_data() {
369 let map:ˇ
370}
371"#});
372
373 cx.update_editor(|editor, window, cx| {
374 editor.handle_input(" Result<", window, cx);
375 });
376 cx.executor().advance_clock(Duration::from_millis(100));
377 cx.executor().run_until_parked();
378 assert_eq!(
379 indoc! {r#"
380struct Foo«1<'a, T>1» «1{
381 data: Vec«2<Option«3<&'a T>3»>2»,
382}1»
383
384fn process_data«1()1» «1{
385 let map: Result<
386}1»
387
3881 hsla(207.80, 16.20%, 69.19%, 1.00)
3892 hsla(29.00, 54.00%, 65.88%, 1.00)
3903 hsla(286.00, 51.00%, 75.25%, 1.00)
391"#},
392 &bracket_colors_markup(&mut cx),
393 "Brackets without pairs should be ignored and not colored"
394 );
395
396 cx.update_editor(|editor, window, cx| {
397 editor.handle_input("Option<Foo<'_, ()", window, cx);
398 });
399 cx.executor().advance_clock(Duration::from_millis(100));
400 cx.executor().run_until_parked();
401 assert_eq!(
402 indoc! {r#"
403struct Foo«1<'a, T>1» «1{
404 data: Vec«2<Option«3<&'a T>3»>2»,
405}1»
406
407fn process_data«1()1» «1{
408 let map: Result<Option<Foo<'_, «2()2»
409}1»
410
4111 hsla(207.80, 16.20%, 69.19%, 1.00)
4122 hsla(29.00, 54.00%, 65.88%, 1.00)
4133 hsla(286.00, 51.00%, 75.25%, 1.00)
414"#},
415 &bracket_colors_markup(&mut cx),
416 );
417
418 cx.update_editor(|editor, window, cx| {
419 editor.handle_input(">", window, cx);
420 });
421 cx.executor().advance_clock(Duration::from_millis(100));
422 cx.executor().run_until_parked();
423 assert_eq!(
424 indoc! {r#"
425struct Foo«1<'a, T>1» «1{
426 data: Vec«2<Option«3<&'a T>3»>2»,
427}1»
428
429fn process_data«1()1» «1{
430 let map: Result<Option<Foo«2<'_, «3()3»>2»
431}1»
432
4331 hsla(207.80, 16.20%, 69.19%, 1.00)
4342 hsla(29.00, 54.00%, 65.88%, 1.00)
4353 hsla(286.00, 51.00%, 75.25%, 1.00)
436"#},
437 &bracket_colors_markup(&mut cx),
438 "When brackets start to get closed, inner brackets are re-colored based on their depth"
439 );
440
441 cx.update_editor(|editor, window, cx| {
442 editor.handle_input(">", window, cx);
443 });
444 cx.executor().advance_clock(Duration::from_millis(100));
445 cx.executor().run_until_parked();
446 assert_eq!(
447 indoc! {r#"
448struct Foo«1<'a, T>1» «1{
449 data: Vec«2<Option«3<&'a T>3»>2»,
450}1»
451
452fn process_data«1()1» «1{
453 let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
454}1»
455
4561 hsla(207.80, 16.20%, 69.19%, 1.00)
4572 hsla(29.00, 54.00%, 65.88%, 1.00)
4583 hsla(286.00, 51.00%, 75.25%, 1.00)
4594 hsla(187.00, 47.00%, 59.22%, 1.00)
460"#},
461 &bracket_colors_markup(&mut cx),
462 );
463
464 cx.update_editor(|editor, window, cx| {
465 editor.handle_input(", ()> = unimplemented!();", window, cx);
466 });
467 cx.executor().advance_clock(Duration::from_millis(100));
468 cx.executor().run_until_parked();
469 assert_eq!(
470 indoc! {r#"
471struct Foo«1<'a, T>1» «1{
472 data: Vec«2<Option«3<&'a T>3»>2»,
473}1»
474
475fn process_data«1()1» «1{
476 let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
477}1»
478
4791 hsla(207.80, 16.20%, 69.19%, 1.00)
4802 hsla(29.00, 54.00%, 65.88%, 1.00)
4813 hsla(286.00, 51.00%, 75.25%, 1.00)
4824 hsla(187.00, 47.00%, 59.22%, 1.00)
4835 hsla(355.00, 65.00%, 75.94%, 1.00)
484"#},
485 &bracket_colors_markup(&mut cx),
486 );
487 }
488
489 #[gpui::test]
490 async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
491 let comment_lines = 100;
492
493 init_test(cx, |language_settings| {
494 language_settings.defaults.colorize_brackets = Some(true);
495 });
496 let mut cx = EditorLspTestContext::new(
497 Arc::into_inner(rust_lang()).unwrap(),
498 lsp::ServerCapabilities::default(),
499 cx,
500 )
501 .await;
502
503 cx.set_state(&separate_with_comment_lines(
504 indoc! {r#"
505mod foo {
506 ˇfn process_data_1() {
507 let map: Option<Vec<()>> = None;
508 }
509"#},
510 indoc! {r#"
511 fn process_data_2() {
512 let map: Option<Vec<()>> = None;
513 }
514}
515"#},
516 comment_lines,
517 ));
518
519 cx.executor().advance_clock(Duration::from_millis(100));
520 cx.executor().run_until_parked();
521 assert_eq!(
522 &separate_with_comment_lines(
523 indoc! {r#"
524mod foo «1{
525 fn process_data_1«2()2» «2{
526 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
527 }2»
528"#},
529 indoc! {r#"
530 fn process_data_2() {
531 let map: Option<Vec<()>> = None;
532 }
533}1»
534
5351 hsla(207.80, 16.20%, 69.19%, 1.00)
5362 hsla(29.00, 54.00%, 65.88%, 1.00)
5373 hsla(286.00, 51.00%, 75.25%, 1.00)
5384 hsla(187.00, 47.00%, 59.22%, 1.00)
5395 hsla(355.00, 65.00%, 75.94%, 1.00)
540"#},
541 comment_lines,
542 ),
543 &bracket_colors_markup(&mut cx),
544 "First, the only visible chunk is getting the bracket highlights"
545 );
546
547 cx.update_editor(|editor, window, cx| {
548 editor.move_to_end(&MoveToEnd, window, cx);
549 editor.move_up(&MoveUp, 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 }2»
565}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 "After scrolling to the bottom, both chunks should have the highlights"
577 );
578
579 cx.update_editor(|editor, window, cx| {
580 editor.handle_input("{{}}}", window, cx);
581 });
582 cx.executor().advance_clock(Duration::from_millis(100));
583 cx.executor().run_until_parked();
584 assert_eq!(
585 &separate_with_comment_lines(
586 indoc! {r#"
587mod foo «1{
588 fn process_data_1() {
589 let map: Option<Vec<()>> = None;
590 }
591"#},
592 indoc! {r#"
593 fn process_data_2«2()2» «2{
594 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
595 }
596 «3{«4{}4»}3»}2»}1»
597
5981 hsla(207.80, 16.20%, 69.19%, 1.00)
5992 hsla(29.00, 54.00%, 65.88%, 1.00)
6003 hsla(286.00, 51.00%, 75.25%, 1.00)
6014 hsla(187.00, 47.00%, 59.22%, 1.00)
6025 hsla(355.00, 65.00%, 75.94%, 1.00)
603"#},
604 comment_lines,
605 ),
606 &bracket_colors_markup(&mut cx),
607 "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
608 );
609
610 cx.update_editor(|editor, window, cx| {
611 editor.move_to_beginning(&MoveToBeginning, window, cx);
612 });
613 cx.executor().advance_clock(Duration::from_millis(100));
614 cx.executor().run_until_parked();
615 assert_eq!(
616 &separate_with_comment_lines(
617 indoc! {r#"
618mod foo «1{
619 fn process_data_1«2()2» «2{
620 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
621 }2»
622"#},
623 indoc! {r#"
624 fn process_data_2«2()2» «2{
625 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
626 }
627 «3{«4{}4»}3»}2»}1»
628
6291 hsla(207.80, 16.20%, 69.19%, 1.00)
6302 hsla(29.00, 54.00%, 65.88%, 1.00)
6313 hsla(286.00, 51.00%, 75.25%, 1.00)
6324 hsla(187.00, 47.00%, 59.22%, 1.00)
6335 hsla(355.00, 65.00%, 75.94%, 1.00)
634"#},
635 comment_lines,
636 ),
637 &bracket_colors_markup(&mut cx),
638 "Scrolling back to top should re-colorize all chunks' brackets"
639 );
640
641 cx.update(|_, cx| {
642 SettingsStore::update_global(cx, |store, cx| {
643 store.update_user_settings(cx, |settings| {
644 settings.project.all_languages.defaults.colorize_brackets = Some(false);
645 });
646 });
647 });
648 assert_eq!(
649 &separate_with_comment_lines(
650 indoc! {r#"
651mod foo {
652 fn process_data_1() {
653 let map: Option<Vec<()>> = None;
654 }
655"#},
656 r#" fn process_data_2() {
657 let map: Option<Vec<()>> = None;
658 }
659 {{}}}}
660
661"#,
662 comment_lines,
663 ),
664 &bracket_colors_markup(&mut cx),
665 "Turning bracket colorization off should remove all bracket colors"
666 );
667
668 cx.update(|_, cx| {
669 SettingsStore::update_global(cx, |store, cx| {
670 store.update_user_settings(cx, |settings| {
671 settings.project.all_languages.defaults.colorize_brackets = Some(true);
672 });
673 });
674 });
675 assert_eq!(
676 &separate_with_comment_lines(
677 indoc! {r#"
678mod foo «1{
679 fn process_data_1«2()2» «2{
680 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
681 }2»
682"#},
683 r#" fn process_data_2() {
684 let map: Option<Vec<()>> = None;
685 }
686 {{}}}}1»
687
6881 hsla(207.80, 16.20%, 69.19%, 1.00)
6892 hsla(29.00, 54.00%, 65.88%, 1.00)
6903 hsla(286.00, 51.00%, 75.25%, 1.00)
6914 hsla(187.00, 47.00%, 59.22%, 1.00)
6925 hsla(355.00, 65.00%, 75.94%, 1.00)
693"#,
694 comment_lines,
695 ),
696 &bracket_colors_markup(&mut cx),
697 "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
698 );
699 }
700
701 #[gpui::test]
702 async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
703 init_test(cx, |language_settings| {
704 language_settings.defaults.colorize_brackets = Some(true);
705 });
706 let mut cx = EditorLspTestContext::new(
707 Arc::into_inner(rust_lang()).unwrap(),
708 lsp::ServerCapabilities::default(),
709 cx,
710 )
711 .await;
712
713 // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
714 cx.set_state(indoc! {r#"ˇ
715 pub(crate) fn inlay_hints(
716 db: &RootDatabase,
717 file_id: FileId,
718 range_limit: Option<TextRange>,
719 config: &InlayHintsConfig,
720 ) -> Vec<InlayHint> {
721 let _p = tracing::info_span!("inlay_hints").entered();
722 let sema = Semantics::new(db);
723 let file_id = sema
724 .attach_first_edition(file_id)
725 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
726 let file = sema.parse(file_id);
727 let file = file.syntax();
728
729 let mut acc = Vec::new();
730
731 let Some(scope) = sema.scope(file) else {
732 return acc;
733 };
734 let famous_defs = FamousDefs(&sema, scope.krate());
735 let display_target = famous_defs.1.to_display_target(sema.db);
736
737 let ctx = &mut InlayHintCtx::default();
738 let mut hints = |event| {
739 if let Some(node) = handle_event(ctx, event) {
740 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
741 }
742 };
743 let mut preorder = file.preorder();
744 salsa::attach(sema.db, || {
745 while let Some(event) = preorder.next() {
746 if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
747 {
748 preorder.skip_subtree();
749 continue;
750 }
751 hints(event);
752 }
753 });
754 if let Some(range_limit) = range_limit {
755 acc.retain(|hint| range_limit.contains_range(hint.range));
756 }
757 acc
758 }
759
760 #[derive(Default)]
761 struct InlayHintCtx {
762 lifetime_stacks: Vec<Vec<SmolStr>>,
763 extern_block_parent: Option<ast::ExternBlock>,
764 }
765
766 pub(crate) fn inlay_hints_resolve(
767 db: &RootDatabase,
768 file_id: FileId,
769 resolve_range: TextRange,
770 hash: u64,
771 config: &InlayHintsConfig,
772 hasher: impl Fn(&InlayHint) -> u64,
773 ) -> Option<InlayHint> {
774 let _p = tracing::info_span!("inlay_hints_resolve").entered();
775 let sema = Semantics::new(db);
776 let file_id = sema
777 .attach_first_edition(file_id)
778 .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
779 let file = sema.parse(file_id);
780 let file = file.syntax();
781
782 let scope = sema.scope(file)?;
783 let famous_defs = FamousDefs(&sema, scope.krate());
784 let mut acc = Vec::new();
785
786 let display_target = famous_defs.1.to_display_target(sema.db);
787
788 let ctx = &mut InlayHintCtx::default();
789 let mut hints = |event| {
790 if let Some(node) = handle_event(ctx, event) {
791 hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
792 }
793 };
794
795 let mut preorder = file.preorder();
796 while let Some(event) = preorder.next() {
797 // This can miss some hints that require the parent of the range to calculate
798 if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
799 {
800 preorder.skip_subtree();
801 continue;
802 }
803 hints(event);
804 }
805 acc.into_iter().find(|hint| hasher(hint) == hash)
806 }
807
808 fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
809 match node {
810 WalkEvent::Enter(node) => {
811 if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
812 let params = node
813 .generic_param_list()
814 .map(|it| {
815 it.lifetime_params()
816 .filter_map(|it| {
817 it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
818 })
819 .collect()
820 })
821 .unwrap_or_default();
822 ctx.lifetime_stacks.push(params);
823 }
824 if let Some(node) = ast::ExternBlock::cast(node.clone()) {
825 ctx.extern_block_parent = Some(node);
826 }
827 Some(node)
828 }
829 WalkEvent::Leave(n) => {
830 if ast::AnyHasGenericParams::can_cast(n.kind()) {
831 ctx.lifetime_stacks.pop();
832 }
833 if ast::ExternBlock::can_cast(n.kind()) {
834 ctx.extern_block_parent = None;
835 }
836 None
837 }
838 }
839 }
840
841 // At some point when our hir infra is fleshed out enough we should flip this and traverse the
842 // HIR instead of the syntax tree.
843 fn hints(
844 hints: &mut Vec<InlayHint>,
845 ctx: &mut InlayHintCtx,
846 famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
847 config: &InlayHintsConfig,
848 file_id: EditionedFileId,
849 display_target: DisplayTarget,
850 node: SyntaxNode,
851 ) {
852 closing_brace::hints(
853 hints,
854 sema,
855 config,
856 display_target,
857 InRealFile { file_id, value: node.clone() },
858 );
859 if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
860 generic_param::hints(hints, famous_defs, config, any_has_generic_args);
861 }
862
863 match_ast! {
864 match node {
865 ast::Expr(expr) => {
866 chaining::hints(hints, famous_defs, config, display_target, &expr);
867 adjustment::hints(hints, famous_defs, config, display_target, &expr);
868 match expr {
869 ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
870 ast::Expr::MethodCallExpr(it) => {
871 param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
872 }
873 ast::Expr::ClosureExpr(it) => {
874 closure_captures::hints(hints, famous_defs, config, it.clone());
875 closure_ret::hints(hints, famous_defs, config, display_target, it)
876 },
877 ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
878 _ => Some(()),
879 }
880 },
881 ast::Pat(it) => {
882 binding_mode::hints(hints, famous_defs, config, &it);
883 match it {
884 ast::Pat::IdentPat(it) => {
885 bind_pat::hints(hints, famous_defs, config, display_target, &it);
886 }
887 ast::Pat::RangePat(it) => {
888 range_exclusive::hints(hints, famous_defs, config, it);
889 }
890 _ => {}
891 }
892 Some(())
893 },
894 ast::Item(it) => match it {
895 ast::Item::Fn(it) => {
896 implicit_drop::hints(hints, famous_defs, config, display_target, &it);
897 if let Some(extern_block) = &ctx.extern_block_parent {
898 extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
899 }
900 lifetime::fn_hints(hints, ctx, famous_defs, config, it)
901 },
902 ast::Item::Static(it) => {
903 if let Some(extern_block) = &ctx.extern_block_parent {
904 extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
905 }
906 implicit_static::hints(hints, famous_defs, config, Either::Left(it))
907 },
908 ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
909 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
910 ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
911 _ => None,
912 },
913 // trait object type elisions
914 ast::Type(ty) => match ty {
915 ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
916 ast::Type::PathType(path) => {
917 lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
918 implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
919 Some(())
920 },
921 ast::Type::DynTraitType(dyn_) => {
922 implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
923 Some(())
924 },
925 _ => Some(()),
926 },
927 ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
928 _ => Some(()),
929 }
930 };
931 }
932 "#});
933 cx.executor().advance_clock(Duration::from_millis(100));
934 cx.executor().run_until_parked();
935
936 let actual_ranges = cx.update_editor(|editor, window, cx| {
937 editor
938 .snapshot(window, cx)
939 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
940 });
941
942 let mut highlighted_brackets = HashMap::default();
943 for (color, range) in actual_ranges.iter().cloned() {
944 highlighted_brackets.insert(range, color);
945 }
946
947 let last_bracket = actual_ranges
948 .iter()
949 .max_by_key(|(_, p)| p.end.row)
950 .unwrap()
951 .clone();
952
953 cx.update_editor(|editor, window, cx| {
954 let was_scrolled = editor.set_scroll_position(
955 gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
956 window,
957 cx,
958 );
959 assert!(was_scrolled.0);
960 });
961 cx.executor().advance_clock(Duration::from_millis(100));
962 cx.executor().run_until_parked();
963
964 let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
965 editor
966 .snapshot(window, cx)
967 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
968 });
969 let new_last_bracket = ranges_after_scrolling
970 .iter()
971 .max_by_key(|(_, p)| p.end.row)
972 .unwrap()
973 .clone();
974
975 assert_ne!(
976 last_bracket, new_last_bracket,
977 "After scrolling down, we should have highlighted more brackets"
978 );
979
980 cx.update_editor(|editor, window, cx| {
981 let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
982 assert!(was_scrolled.0);
983 });
984
985 for _ in 0..200 {
986 cx.update_editor(|editor, window, cx| {
987 editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
988 });
989 cx.executor().advance_clock(Duration::from_millis(100));
990 cx.executor().run_until_parked();
991
992 let colored_brackets = cx.update_editor(|editor, window, cx| {
993 editor
994 .snapshot(window, cx)
995 .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
996 });
997 for (color, range) in colored_brackets.clone() {
998 assert!(
999 highlighted_brackets.entry(range).or_insert(color) == &color,
1000 "Colors should stay consistent while scrolling!"
1001 );
1002 }
1003
1004 let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
1005 let scroll_position = snapshot.scroll_position();
1006 let visible_lines =
1007 cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
1008 let visible_range = DisplayRow(scroll_position.y as u32)
1009 ..DisplayRow((scroll_position.y + visible_lines) as u32);
1010
1011 let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
1012 colored_brackets
1013 .iter()
1014 .flat_map(|(_, range)| [range.start, range.end]),
1015 );
1016
1017 for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
1018 visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
1019 || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
1020 }) {
1021 assert!(
1022 current_highlighted_bracket_set.contains(&highlight_range.start)
1023 || current_highlighted_bracket_set.contains(&highlight_range.end),
1024 "Should not lose highlights while scrolling in the visible range!"
1025 );
1026 }
1027
1028 let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
1029 for bracket_match in buffer_snapshot
1030 .fetch_bracket_ranges(
1031 snapshot
1032 .display_point_to_point(
1033 DisplayPoint::new(visible_range.start, 0),
1034 Bias::Left,
1035 )
1036 .to_offset(&buffer_snapshot)
1037 ..snapshot
1038 .display_point_to_point(
1039 DisplayPoint::new(
1040 visible_range.end,
1041 snapshot.line_len(visible_range.end),
1042 ),
1043 Bias::Right,
1044 )
1045 .to_offset(&buffer_snapshot),
1046 None,
1047 )
1048 .iter()
1049 .flat_map(|entry| entry.1)
1050 .filter(|bracket_match| bracket_match.color_index.is_some())
1051 {
1052 let start = bracket_match.open_range.to_point(buffer_snapshot);
1053 let end = bracket_match.close_range.to_point(buffer_snapshot);
1054 let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
1055 assert!(
1056 start_bracket.is_some(),
1057 "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1058 buffer_snapshot
1059 .text_for_range(start.start..end.end)
1060 .collect::<String>(),
1061 start
1062 );
1063
1064 let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
1065 assert!(
1066 end_bracket.is_some(),
1067 "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
1068 buffer_snapshot
1069 .text_for_range(start.start..end.end)
1070 .collect::<String>(),
1071 start
1072 );
1073
1074 assert_eq!(
1075 start_bracket.unwrap().0,
1076 end_bracket.unwrap().0,
1077 "Bracket pair should be highlighted the same color!"
1078 )
1079 }
1080 }
1081 }
1082
1083 #[gpui::test]
1084 async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
1085 let comment_lines = 100;
1086
1087 init_test(cx, |language_settings| {
1088 language_settings.defaults.colorize_brackets = Some(true);
1089 });
1090 let fs = FakeFs::new(cx.background_executor.clone());
1091 fs.insert_tree(
1092 path!("/a"),
1093 json!({
1094 "main.rs": "fn main() {{()}}",
1095 "lib.rs": separate_with_comment_lines(
1096 indoc! {r#"
1097 mod foo {
1098 fn process_data_1() {
1099 let map: Option<Vec<()>> = None;
1100 // a
1101 // b
1102 // c
1103 }
1104 "#},
1105 indoc! {r#"
1106 fn process_data_2() {
1107 let other_map: Option<Vec<()>> = None;
1108 }
1109 }
1110 "#},
1111 comment_lines,
1112 )
1113 }),
1114 )
1115 .await;
1116
1117 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
1118 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1119 language_registry.add(rust_lang());
1120
1121 let buffer_1 = project
1122 .update(cx, |project, cx| {
1123 project.open_local_buffer(path!("/a/lib.rs"), cx)
1124 })
1125 .await
1126 .unwrap();
1127 let buffer_2 = project
1128 .update(cx, |project, cx| {
1129 project.open_local_buffer(path!("/a/main.rs"), cx)
1130 })
1131 .await
1132 .unwrap();
1133
1134 let multi_buffer = cx.new(|cx| {
1135 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
1136 multi_buffer.push_excerpts(
1137 buffer_2.clone(),
1138 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
1139 cx,
1140 );
1141
1142 let excerpt_rows = 5;
1143 let rest_of_first_except_rows = 3;
1144 multi_buffer.push_excerpts(
1145 buffer_1.clone(),
1146 [
1147 ExcerptRange::new(Point::new(0, 0)..Point::new(excerpt_rows, 0)),
1148 ExcerptRange::new(
1149 Point::new(
1150 comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
1151 0,
1152 )
1153 ..Point::new(
1154 comment_lines as u32
1155 + excerpt_rows
1156 + rest_of_first_except_rows
1157 + excerpt_rows,
1158 0,
1159 ),
1160 ),
1161 ],
1162 cx,
1163 );
1164 multi_buffer
1165 });
1166
1167 let editor = cx.add_window(|window, cx| {
1168 Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
1169 });
1170 cx.executor().advance_clock(Duration::from_millis(100));
1171 cx.executor().run_until_parked();
1172
1173 let editor_snapshot = editor
1174 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1175 .unwrap();
1176 assert_eq!(
1177 indoc! {r#"
1178
1179
1180fn main«1()1» «1{«2{«3()3»}2»}1»
1181
1182
1183mod foo «1{
1184 fn process_data_1«2()2» «2{
1185 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1186 // a
1187 // b
1188
1189
1190 fn process_data_2«2()2» «2{
1191 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1192 }2»
1193}1»
1194
11951 hsla(207.80, 16.20%, 69.19%, 1.00)
11962 hsla(29.00, 54.00%, 65.88%, 1.00)
11973 hsla(286.00, 51.00%, 75.25%, 1.00)
11984 hsla(187.00, 47.00%, 59.22%, 1.00)
11995 hsla(355.00, 65.00%, 75.94%, 1.00)
1200"#,},
1201 &editor_bracket_colors_markup(&editor_snapshot),
1202 "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
1203or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
1204 );
1205
1206 editor
1207 .update(cx, |editor, window, cx| {
1208 editor.handle_input("{[]", window, cx);
1209 })
1210 .unwrap();
1211 cx.executor().advance_clock(Duration::from_millis(100));
1212 cx.executor().run_until_parked();
1213 let editor_snapshot = editor
1214 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1215 .unwrap();
1216 assert_eq!(
1217 indoc! {r#"
1218
1219
1220{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
1221
1222
1223mod foo «1{
1224 fn process_data_1«2()2» «2{
1225 let map: Option«3<Vec«4<«5()5»>4»>3» = None;
1226 // a
1227 // b
1228
1229
1230 fn process_data_2«2()2» «2{
1231 let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
1232 }2»
1233}1»
1234
12351 hsla(207.80, 16.20%, 69.19%, 1.00)
12362 hsla(29.00, 54.00%, 65.88%, 1.00)
12373 hsla(286.00, 51.00%, 75.25%, 1.00)
12384 hsla(187.00, 47.00%, 59.22%, 1.00)
12395 hsla(355.00, 65.00%, 75.94%, 1.00)
1240"#,},
1241 &editor_bracket_colors_markup(&editor_snapshot),
1242 );
1243
1244 cx.update(|cx| {
1245 let theme = cx.theme().name.clone();
1246 SettingsStore::update_global(cx, |store, cx| {
1247 store.update_user_settings(cx, |settings| {
1248 settings.theme.theme_overrides = HashMap::from_iter([(
1249 theme.to_string(),
1250 ThemeStyleContent {
1251 accents: vec![
1252 AccentContent(Some(SharedString::new("#ff0000"))),
1253 AccentContent(Some(SharedString::new("#0000ff"))),
1254 ],
1255 ..ThemeStyleContent::default()
1256 },
1257 )]);
1258 });
1259 });
1260 });
1261 cx.executor().advance_clock(Duration::from_millis(100));
1262 cx.executor().run_until_parked();
1263 let editor_snapshot = editor
1264 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
1265 .unwrap();
1266 assert_eq!(
1267 indoc! {r#"
1268
1269
1270{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
1271
1272
1273mod foo «1{
1274 fn process_data_1«2()2» «2{
1275 let map: Option«1<Vec«2<«1()1»>2»>1» = None;
1276 // a
1277 // b
1278
1279
1280 fn process_data_2«2()2» «2{
1281 let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
1282 }2»
1283}1»
1284
12851 hsla(0.00, 100.00%, 78.12%, 1.00)
12862 hsla(240.00, 100.00%, 82.81%, 1.00)
1287"#,},
1288 &editor_bracket_colors_markup(&editor_snapshot),
1289 "After updating theme accents, the editor should update the bracket coloring"
1290 );
1291 }
1292
1293 fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
1294 let mut result = head.to_string();
1295 result.push_str("\n");
1296 result.push_str(&"//\n".repeat(comment_lines));
1297 result.push_str(tail);
1298 result
1299 }
1300
1301 fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
1302 cx.update_editor(|editor, window, cx| {
1303 editor_bracket_colors_markup(&editor.snapshot(window, cx))
1304 })
1305 }
1306
1307 fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
1308 fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
1309 let mut offset = 0;
1310 for (row_idx, line) in text.lines().enumerate() {
1311 if row_idx < point.row().0 as usize {
1312 offset += line.len() + 1; // +1 for newline
1313 } else {
1314 offset += point.column() as usize;
1315 break;
1316 }
1317 }
1318 offset
1319 }
1320
1321 let actual_ranges = snapshot.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
1322 let editor_text = snapshot.text();
1323
1324 let mut next_index = 1;
1325 let mut color_to_index = HashMap::default();
1326 let mut annotations = Vec::new();
1327 for (color, range) in &actual_ranges {
1328 let color_index = *color_to_index
1329 .entry(*color)
1330 .or_insert_with(|| post_inc(&mut next_index));
1331 let start = snapshot.point_to_display_point(range.start, Bias::Left);
1332 let end = snapshot.point_to_display_point(range.end, Bias::Right);
1333 let start_offset = display_point_to_offset(&editor_text, start);
1334 let end_offset = display_point_to_offset(&editor_text, end);
1335 let bracket_text = &editor_text[start_offset..end_offset];
1336 let bracket_char = bracket_text.chars().next().unwrap();
1337
1338 if matches!(bracket_char, '{' | '[' | '(' | '<') {
1339 annotations.push((start_offset, format!("«{color_index}")));
1340 } else {
1341 annotations.push((end_offset, format!("{color_index}»")));
1342 }
1343 }
1344
1345 annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
1346 pos_a.cmp(pos_b).reverse().then_with(|| {
1347 let a_is_opening = text_a.starts_with('«');
1348 let b_is_opening = text_b.starts_with('«');
1349 match (a_is_opening, b_is_opening) {
1350 (true, false) => cmp::Ordering::Less,
1351 (false, true) => cmp::Ordering::Greater,
1352 _ => cmp::Ordering::Equal,
1353 }
1354 })
1355 });
1356 annotations.dedup();
1357
1358 let mut markup = editor_text;
1359 for (offset, text) in annotations {
1360 markup.insert_str(offset, &text);
1361 }
1362
1363 markup.push_str("\n");
1364 for (index, color) in color_to_index
1365 .iter()
1366 .map(|(color, index)| (*index, *color))
1367 .sorted_by_key(|(index, _)| *index)
1368 {
1369 markup.push_str(&format!("{index} {color}\n"));
1370 }
1371
1372 markup
1373 }
1374}