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