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