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}