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