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