bracket_colorization.rs

  1use crate::Editor;
  2use collections::HashMap;
  3use gpui::{Context, HighlightStyle};
  4use language::language_settings;
  5use ui::{ActiveTheme, utils::ensure_minimum_contrast};
  6
  7struct RainbowBracketHighlight;
  8
  9impl Editor {
 10    pub(crate) fn colorize_brackets(&mut self, invalidate: bool, cx: &mut Context<Editor>) {
 11        if !self.mode.is_full() {
 12            return;
 13        }
 14
 15        if invalidate {
 16            self.fetched_tree_sitter_chunks.clear();
 17        }
 18
 19        let accents_count = cx.theme().accents().0.len();
 20        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
 21        let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
 22            HashMap::default(),
 23            |mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
 24                let buffer_snapshot = buffer.read(cx).snapshot();
 25                if language_settings::language_settings(
 26                    buffer_snapshot.language().map(|language| language.name()),
 27                    buffer_snapshot.file(),
 28                    cx,
 29                )
 30                .colorize_brackets
 31                {
 32                    let fetched_chunks = self
 33                        .fetched_tree_sitter_chunks
 34                        .entry(excerpt_id)
 35                        .or_default();
 36
 37                    let brackets_by_accent = buffer_snapshot
 38                        .fetch_bracket_ranges(
 39                            buffer_range.start..buffer_range.end,
 40                            Some((&buffer_version, fetched_chunks)),
 41                        )
 42                        .into_iter()
 43                        .flat_map(|(chunk_range, pairs)| {
 44                            if fetched_chunks.insert(chunk_range) {
 45                                pairs
 46                            } else {
 47                                Vec::new()
 48                            }
 49                        })
 50                        .filter_map(|pair| {
 51                            let color_index = pair.color_index?;
 52                            let buffer_open_range = buffer_snapshot
 53                                .anchor_before(pair.open_range.start)
 54                                ..buffer_snapshot.anchor_after(pair.open_range.end);
 55                            let multi_buffer_open_range = multi_buffer_snapshot
 56                                .anchor_in_excerpt(excerpt_id, buffer_open_range.start)?
 57                                ..multi_buffer_snapshot
 58                                    .anchor_in_excerpt(excerpt_id, buffer_open_range.end)?;
 59                            let buffer_close_range = buffer_snapshot
 60                                .anchor_before(pair.close_range.start)
 61                                ..buffer_snapshot.anchor_after(pair.close_range.end);
 62                            let multi_buffer_close_range = multi_buffer_snapshot
 63                                .anchor_in_excerpt(excerpt_id, buffer_close_range.start)?
 64                                ..multi_buffer_snapshot
 65                                    .anchor_in_excerpt(excerpt_id, buffer_close_range.end)?;
 66                            Some((
 67                                color_index % accents_count,
 68                                multi_buffer_open_range,
 69                                multi_buffer_close_range,
 70                            ))
 71                        });
 72
 73                    for (accent_number, open_range, close_range) in brackets_by_accent {
 74                        let ranges = acc.entry(accent_number).or_insert_with(Vec::new);
 75                        ranges.push(open_range);
 76                        ranges.push(close_range);
 77                    }
 78                }
 79
 80                acc
 81            },
 82        );
 83
 84        if invalidate {
 85            self.clear_highlights::<RainbowBracketHighlight>(cx);
 86        }
 87
 88        let editor_background = cx.theme().colors().editor_background;
 89        for (accent_number, bracket_highlights) in bracket_matches_by_accent {
 90            let bracket_color = cx.theme().accents().color_for_index(accent_number as u32);
 91            let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
 92            let style = HighlightStyle {
 93                color: Some(adjusted_color),
 94                ..HighlightStyle::default()
 95            };
 96
 97            self.highlight_text_key::<RainbowBracketHighlight>(
 98                accent_number,
 99                bracket_highlights,
100                style,
101                true,
102                cx,
103            );
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use std::{collections::HashSet, ops::Range, time::Duration};
111
112    use super::*;
113    use crate::{
114        DisplayPoint,
115        display_map::{DisplayRow, ToDisplayPoint},
116        editor_tests::init_test,
117        test::{
118            editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
119        },
120    };
121    use gpui::Hsla;
122    use indoc::indoc;
123    use language::{BracketPair, BracketPairConfig, Language, LanguageConfig, LanguageMatcher};
124    use multi_buffer::AnchorRangeExt as _;
125    use rope::Point;
126    use text::OffsetRangeExt;
127
128    #[gpui::test]
129    async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
130        fn collect_colored_brackets(
131            cx: &mut EditorTestContext,
132        ) -> Vec<(Option<Hsla>, Range<Point>)> {
133            cx.update_editor(|editor, window, cx| {
134                let snapshot = editor.snapshot(window, cx);
135                snapshot
136                    .all_text_highlight_ranges::<RainbowBracketHighlight>()
137                    .iter()
138                    .flat_map(|ranges| {
139                        ranges.1.iter().map(|range| {
140                            (ranges.0.color, range.to_point(&snapshot.buffer_snapshot()))
141                        })
142                    })
143                    .collect::<Vec<_>>()
144            })
145        }
146
147        init_test(cx, |language_settings| {
148            language_settings.defaults.colorize_brackets = Some(true);
149        });
150
151        let mut cx = EditorLspTestContext::new(
152            Language::new(
153                LanguageConfig {
154                    name: "Rust".into(),
155                    matcher: LanguageMatcher {
156                        path_suffixes: vec!["rs".to_string()],
157                        ..LanguageMatcher::default()
158                    },
159                    brackets: BracketPairConfig {
160                        pairs: vec![
161                            BracketPair {
162                                start: "{".to_string(),
163                                end: "}".to_string(),
164                                close: false,
165                                surround: false,
166                                newline: true,
167                            },
168                            BracketPair {
169                                start: "(".to_string(),
170                                end: ")".to_string(),
171                                close: false,
172                                surround: false,
173                                newline: true,
174                            },
175                        ],
176                        ..BracketPairConfig::default()
177                    },
178                    ..LanguageConfig::default()
179                },
180                Some(tree_sitter_rust::LANGUAGE.into()),
181            )
182            .with_brackets_query(indoc! {r#"
183                ("{" @open "}" @close)
184                ("(" @open ")" @close)
185                "#})
186            .unwrap(),
187            lsp::ServerCapabilities::default(),
188            cx,
189        )
190        .await;
191
192        let mut highlighted_brackets = HashMap::default();
193
194        // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
195        cx.set_state(indoc! {r#196            pub(crate) fn inlay_hints(
197                db: &RootDatabase,
198                file_id: FileId,
199                range_limit: Option<TextRange>,
200                config: &InlayHintsConfig,
201            ) -> Vec<InlayHint> {
202                let _p = tracing::info_span!("inlay_hints").entered();
203                let sema = Semantics::new(db);
204                let file_id = sema
205                    .attach_first_edition(file_id)
206                    .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
207                let file = sema.parse(file_id);
208                let file = file.syntax();
209
210                let mut acc = Vec::new();
211
212                let Some(scope) = sema.scope(file) else {
213                    return acc;
214                };
215                let famous_defs = FamousDefs(&sema, scope.krate());
216                let display_target = famous_defs.1.to_display_target(sema.db);
217
218                let ctx = &mut InlayHintCtx::default();
219                let mut hints = |event| {
220                    if let Some(node) = handle_event(ctx, event) {
221                        hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
222                    }
223                };
224                let mut preorder = file.preorder();
225                salsa::attach(sema.db, || {
226                    while let Some(event) = preorder.next() {
227                        if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
228                        {
229                            preorder.skip_subtree();
230                            continue;
231                        }
232                        hints(event);
233                    }
234                });
235                if let Some(range_limit) = range_limit {
236                    acc.retain(|hint| range_limit.contains_range(hint.range));
237                }
238                acc
239            }
240
241            #[derive(Default)]
242            struct InlayHintCtx {
243                lifetime_stacks: Vec<Vec<SmolStr>>,
244                extern_block_parent: Option<ast::ExternBlock>,
245            }
246
247            pub(crate) fn inlay_hints_resolve(
248                db: &RootDatabase,
249                file_id: FileId,
250                resolve_range: TextRange,
251                hash: u64,
252                config: &InlayHintsConfig,
253                hasher: impl Fn(&InlayHint) -> u64,
254            ) -> Option<InlayHint> {
255                let _p = tracing::info_span!("inlay_hints_resolve").entered();
256                let sema = Semantics::new(db);
257                let file_id = sema
258                    .attach_first_edition(file_id)
259                    .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
260                let file = sema.parse(file_id);
261                let file = file.syntax();
262
263                let scope = sema.scope(file)?;
264                let famous_defs = FamousDefs(&sema, scope.krate());
265                let mut acc = Vec::new();
266
267                let display_target = famous_defs.1.to_display_target(sema.db);
268
269                let ctx = &mut InlayHintCtx::default();
270                let mut hints = |event| {
271                    if let Some(node) = handle_event(ctx, event) {
272                        hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
273                    }
274                };
275
276                let mut preorder = file.preorder();
277                while let Some(event) = preorder.next() {
278                    // FIXME: This can miss some hints that require the parent of the range to calculate
279                    if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
280                    {
281                        preorder.skip_subtree();
282                        continue;
283                    }
284                    hints(event);
285                }
286                acc.into_iter().find(|hint| hasher(hint) == hash)
287            }
288
289            fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
290                match node {
291                    WalkEvent::Enter(node) => {
292                        if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
293                            let params = node
294                                .generic_param_list()
295                                .map(|it| {
296                                    it.lifetime_params()
297                                        .filter_map(|it| {
298                                            it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
299                                        })
300                                        .collect()
301                                })
302                                .unwrap_or_default();
303                            ctx.lifetime_stacks.push(params);
304                        }
305                        if let Some(node) = ast::ExternBlock::cast(node.clone()) {
306                            ctx.extern_block_parent = Some(node);
307                        }
308                        Some(node)
309                    }
310                    WalkEvent::Leave(n) => {
311                        if ast::AnyHasGenericParams::can_cast(n.kind()) {
312                            ctx.lifetime_stacks.pop();
313                        }
314                        if ast::ExternBlock::can_cast(n.kind()) {
315                            ctx.extern_block_parent = None;
316                        }
317                        None
318                    }
319                }
320            }
321
322            // FIXME: At some point when our hir infra is fleshed out enough we should flip this and traverse the
323            // HIR instead of the syntax tree.
324            fn hints(
325                hints: &mut Vec<InlayHint>,
326                ctx: &mut InlayHintCtx,
327                famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
328                config: &InlayHintsConfig,
329                file_id: EditionedFileId,
330                display_target: DisplayTarget,
331                node: SyntaxNode,
332            ) {
333                closing_brace::hints(
334                    hints,
335                    sema,
336                    config,
337                    display_target,
338                    InRealFile { file_id, value: node.clone() },
339                );
340                if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
341                    generic_param::hints(hints, famous_defs, config, any_has_generic_args);
342                }
343
344                match_ast! {
345                    match node {
346                        ast::Expr(expr) => {
347                            chaining::hints(hints, famous_defs, config, display_target, &expr);
348                            adjustment::hints(hints, famous_defs, config, display_target, &expr);
349                            match expr {
350                                ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
351                                ast::Expr::MethodCallExpr(it) => {
352                                    param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
353                                }
354                                ast::Expr::ClosureExpr(it) => {
355                                    closure_captures::hints(hints, famous_defs, config, it.clone());
356                                    closure_ret::hints(hints, famous_defs, config, display_target, it)
357                                },
358                                ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
359                                _ => Some(()),
360                            }
361                        },
362                        ast::Pat(it) => {
363                            binding_mode::hints(hints, famous_defs, config, &it);
364                            match it {
365                                ast::Pat::IdentPat(it) => {
366                                    bind_pat::hints(hints, famous_defs, config, display_target, &it);
367                                }
368                                ast::Pat::RangePat(it) => {
369                                    range_exclusive::hints(hints, famous_defs, config, it);
370                                }
371                                _ => {}
372                            }
373                            Some(())
374                        },
375                        ast::Item(it) => match it {
376                            ast::Item::Fn(it) => {
377                                implicit_drop::hints(hints, famous_defs, config, display_target, &it);
378                                if let Some(extern_block) = &ctx.extern_block_parent {
379                                    extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
380                                }
381                                lifetime::fn_hints(hints, ctx, famous_defs, config,  it)
382                            },
383                            ast::Item::Static(it) => {
384                                if let Some(extern_block) = &ctx.extern_block_parent {
385                                    extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
386                                }
387                                implicit_static::hints(hints, famous_defs, config,  Either::Left(it))
388                            },
389                            ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
390                            ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
391                            ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
392                            _ => None,
393                        },
394                        // FIXME: trait object type elisions
395                        ast::Type(ty) => match ty {
396                            ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config,  ptr),
397                            ast::Type::PathType(path) => {
398                                lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
399                                implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
400                                Some(())
401                            },
402                            ast::Type::DynTraitType(dyn_) => {
403                                implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
404                                Some(())
405                            },
406                            _ => Some(()),
407                        },
408                        ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config,  it),
409                        _ => Some(()),
410                    }
411                };
412            }
413        "#});
414        cx.executor().advance_clock(Duration::from_millis(100));
415        cx.executor().run_until_parked();
416
417        let actual_ranges = collect_colored_brackets(&mut cx);
418
419        for (color, range) in actual_ranges.iter().cloned() {
420            highlighted_brackets.insert(range, color);
421        }
422
423        let last_bracket = actual_ranges
424            .iter()
425            .max_by_key(|(_, p)| p.end.row)
426            .unwrap()
427            .clone();
428
429        cx.update_editor(|editor, window, cx| {
430            let was_scrolled = editor.set_scroll_position(
431                gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
432                window,
433                cx,
434            );
435            assert!(was_scrolled.0);
436        });
437        cx.executor().advance_clock(Duration::from_millis(100));
438        cx.executor().run_until_parked();
439
440        let ranges_after_scrolling = collect_colored_brackets(&mut cx);
441        let new_last_bracket = ranges_after_scrolling
442            .iter()
443            .max_by_key(|(_, p)| p.end.row)
444            .unwrap()
445            .clone();
446
447        assert_ne!(
448            last_bracket, new_last_bracket,
449            "After scrolling down, we should have highlighted more brackets"
450        );
451
452        cx.update_editor(|editor, window, cx| {
453            let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
454            assert!(was_scrolled.0);
455        });
456
457        for _ in 0..200 {
458            cx.update_editor(|editor, window, cx| {
459                editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
460            });
461            cx.executor().run_until_parked();
462
463            let colored_brackets = collect_colored_brackets(&mut cx);
464            for (color, range) in colored_brackets.clone() {
465                assert!(
466                    highlighted_brackets.entry(range).or_insert(color) == &color,
467                    "Colors should stay consistent while scrolling!"
468                );
469            }
470
471            let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
472            let scroll_position = snapshot.scroll_position();
473            let visible_lines =
474                cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
475            let visible_range = DisplayRow(scroll_position.y as u32)
476                ..DisplayRow((scroll_position.y + visible_lines) as u32);
477
478            let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
479                colored_brackets
480                    .iter()
481                    .flat_map(|(_, range)| [range.start, range.end]),
482            );
483
484            for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
485                visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
486                    || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
487            }) {
488                assert!(
489                    current_highlighted_bracket_set.contains(&highlight_range.start)
490                        || current_highlighted_bracket_set.contains(&highlight_range.end),
491                    "Should not lose highlights while scrolling in the visible range!"
492                );
493            }
494
495            let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
496            for (start, end) in snapshot
497                .bracket_ranges(
498                    DisplayPoint::new(visible_range.start, Default::default()).to_point(&snapshot)
499                        ..DisplayPoint::new(
500                            visible_range.end,
501                            snapshot.line_len(visible_range.end),
502                        )
503                        .to_point(&snapshot),
504                )
505                .into_iter()
506                .flatten()
507            {
508                let start_bracket = colored_brackets
509                    .iter()
510                    .find(|(_, range)| range.to_offset(buffer_snapshot) == start);
511                assert!(
512                    start_bracket.is_some(),
513                    "Existing bracket start in the visible range should be highlighted"
514                );
515
516                let end_bracket = colored_brackets
517                    .iter()
518                    .find(|(_, range)| range.to_offset(buffer_snapshot) == end);
519                assert!(
520                    end_bracket.is_some(),
521                    "Existing bracket end in the visible range should be highlighted"
522                );
523
524                assert_eq!(
525                    start_bracket.unwrap().0,
526                    end_bracket.unwrap().0,
527                    "Bracket pair should be highlighted the same color!"
528                )
529            }
530        }
531
532        // todo! more tests, check no brackets missing in range, settings toggle
533    }
534}