bracket_colorization.rs

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