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