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