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