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