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