bracket_colorization.rs

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