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