1use hir::{DisplayTarget, HirDisplay, InRealFile, Semantics};
7use ide_db::{FileRange, RootDatabase};
8use syntax::{
9 SyntaxKind, SyntaxNode, T,
10 ast::{self, AstNode, HasLoopBody, HasName},
11 match_ast,
12};
13
14use crate::{
15 InlayHint, InlayHintLabel, InlayHintPosition, InlayHintsConfig, InlayKind,
16 inlay_hints::LazyProperty,
17};
18
19const ELLIPSIS: &str = "…";
20
21pub(super) fn hints(
22 acc: &mut Vec<InlayHint>,
23 sema: &Semantics<'_, RootDatabase>,
24 config: &InlayHintsConfig<'_>,
25 display_target: DisplayTarget,
26 InRealFile { file_id, value: node }: InRealFile<SyntaxNode>,
27) -> Option<()> {
28 let min_lines = config.closing_brace_hints_min_lines?;
29
30 let name = |it: ast::Name| it.syntax().text_range();
31
32 let mut node = node.clone();
33 let mut closing_token;
34 let (label, name_range) = if let Some(item_list) = ast::AssocItemList::cast(node.clone()) {
35 closing_token = item_list.r_curly_token()?;
36
37 let parent = item_list.syntax().parent()?;
38 match_ast! {
39 match parent {
40 ast::Impl(imp) => {
41 let imp = sema.to_def(&imp)?;
42 let ty = imp.self_ty(sema.db);
43 let trait_ = imp.trait_(sema.db);
44 let hint_text = match trait_ {
45 Some(tr) => format!(
46 "impl {} for {}",
47 tr.name(sema.db).display(sema.db, display_target.edition),
48 ty.display_truncated(sema.db, config.max_length, display_target,
49 )),
50 None => format!("impl {}", ty.display_truncated(sema.db, config.max_length, display_target)),
51 };
52 (hint_text, None)
53 },
54 ast::Trait(tr) => {
55 (format!("trait {}", tr.name()?), tr.name().map(name))
56 },
57 _ => return None,
58 }
59 }
60 } else if let Some(list) = ast::ItemList::cast(node.clone()) {
61 closing_token = list.r_curly_token()?;
62
63 let module = ast::Module::cast(list.syntax().parent()?)?;
64 (format!("mod {}", module.name()?), module.name().map(name))
65 } else if let Some(match_arm_list) = ast::MatchArmList::cast(node.clone()) {
66 closing_token = match_arm_list.r_curly_token()?;
67
68 let match_expr = ast::MatchExpr::cast(match_arm_list.syntax().parent()?)?;
69 let label = format_match_label(&match_expr, config)?;
70 (label, None)
71 } else if let Some(label) = ast::Label::cast(node.clone()) {
72 node = node.parent()?;
76
77 let parent = label.syntax().parent()?;
78 let block;
79 match_ast! {
80 match parent {
81 ast::BlockExpr(block_expr) => {
82 block = block_expr.stmt_list()?;
83 },
84 ast::AnyHasLoopBody(loop_expr) => {
85 block = loop_expr.loop_body()?.stmt_list()?;
86 },
87 _ => return None,
88 }
89 }
90 closing_token = block.r_curly_token()?;
91
92 let lifetime = label.lifetime()?.to_string();
93
94 (lifetime, Some(label.syntax().text_range()))
95 } else if let Some(block) = ast::BlockExpr::cast(node.clone()) {
96 closing_token = block.stmt_list()?.r_curly_token()?;
97
98 let parent = block.syntax().parent()?;
99 match_ast! {
100 match parent {
101 ast::Fn(it) => {
102 (format!("{}fn {}", fn_qualifiers(&it), it.name()?), it.name().map(name))
103 },
104 ast::Static(it) => (format!("static {}", it.name()?), it.name().map(name)),
105 ast::Const(it) => {
106 if it.underscore_token().is_some() {
107 ("const _".into(), None)
108 } else {
109 (format!("const {}", it.name()?), it.name().map(name))
110 }
111 },
112 ast::LoopExpr(loop_expr) => {
113 if loop_expr.label().is_some() {
114 return None;
115 }
116 ("loop".into(), None)
117 },
118 ast::WhileExpr(while_expr) => {
119 if while_expr.label().is_some() {
120 return None;
121 }
122 (keyword_with_condition("while", while_expr.condition(), config), None)
123 },
124 ast::ForExpr(for_expr) => {
125 if for_expr.label().is_some() {
126 return None;
127 }
128 let label = format_for_label(&for_expr, config)?;
129 (label, None)
130 },
131 ast::IfExpr(if_expr) => {
132 let label = label_for_if_block(&if_expr, &block, config)?;
133 (label, None)
134 },
135 ast::LetElse(let_else) => {
136 let label = format_let_else_label(&let_else, config)?;
137 (label, None)
138 },
139 _ => return None,
140 }
141 }
142 } else if let Some(mac) = ast::MacroCall::cast(node.clone()) {
143 let last_token = mac.syntax().last_token()?;
144 if last_token.kind() != T![;] && last_token.kind() != SyntaxKind::R_CURLY {
145 return None;
146 }
147 closing_token = last_token;
148
149 (
150 format!("{}!", mac.path()?),
151 mac.path().and_then(|it| it.segment()).map(|it| it.syntax().text_range()),
152 )
153 } else {
154 return None;
155 };
156
157 if let Some(mut next) = closing_token.next_token() {
158 if next.kind() == T![;]
159 && let Some(tok) = next.next_token()
160 {
161 closing_token = next;
162 next = tok;
163 }
164 if !(next.kind() == SyntaxKind::WHITESPACE && next.text().contains('\n')) {
165 return None;
167 }
168 }
169
170 let mut lines = 1;
171 node.text().for_each_chunk(|s| lines += s.matches('\n').count());
172 if lines < min_lines {
173 return None;
174 }
175
176 let linked_location =
177 name_range.map(|range| FileRange { file_id: file_id.file_id(sema.db), range });
178 acc.push(InlayHint {
179 range: closing_token.text_range(),
180 kind: InlayKind::ClosingBrace,
181 label: InlayHintLabel::simple(label, None, linked_location.map(LazyProperty::Computed)),
182 text_edit: None,
183 position: InlayHintPosition::After,
184 pad_left: true,
185 pad_right: false,
186 resolve_parent: Some(node.text_range()),
187 });
188
189 None
190}
191
192fn fn_qualifiers(func: &ast::Fn) -> String {
193 let mut qualifiers = String::new();
194 if func.const_token().is_some() {
195 qualifiers.push_str("const ");
196 }
197 if func.async_token().is_some() {
198 qualifiers.push_str("async ");
199 }
200 if func.unsafe_token().is_some() {
201 qualifiers.push_str("unsafe ");
202 }
203 qualifiers
204}
205
206fn keyword_with_condition(
207 keyword: &str,
208 condition: Option<ast::Expr>,
209 config: &InlayHintsConfig<'_>,
210) -> String {
211 if let Some(expr) = condition {
212 return format!("{keyword} {}", snippet_from_node(expr.syntax(), config));
213 }
214 keyword.to_owned()
215}
216
217fn format_for_label(for_expr: &ast::ForExpr, config: &InlayHintsConfig<'_>) -> Option<String> {
218 let pat = for_expr.pat()?;
219 let iterable = for_expr.iterable()?;
220 Some(format!(
221 "for {} in {}",
222 snippet_from_node(pat.syntax(), config),
223 snippet_from_node(iterable.syntax(), config)
224 ))
225}
226
227fn format_match_label(
228 match_expr: &ast::MatchExpr,
229 config: &InlayHintsConfig<'_>,
230) -> Option<String> {
231 let expr = match_expr.expr()?;
232 Some(format!("match {}", snippet_from_node(expr.syntax(), config)))
233}
234
235fn label_for_if_block(
236 if_expr: &ast::IfExpr,
237 block: &ast::BlockExpr,
238 config: &InlayHintsConfig<'_>,
239) -> Option<String> {
240 if if_expr.then_branch().is_some_and(|then_branch| then_branch.syntax() == block.syntax()) {
241 Some(keyword_with_condition("if", if_expr.condition(), config))
242 } else if matches!(
243 if_expr.else_branch(),
244 Some(ast::ElseBranch::Block(else_block)) if else_block.syntax() == block.syntax()
245 ) {
246 Some("else".into())
247 } else {
248 None
249 }
250}
251
252fn format_let_else_label(let_else: &ast::LetElse, config: &InlayHintsConfig<'_>) -> Option<String> {
253 let stmt = let_else.syntax().parent().and_then(ast::LetStmt::cast)?;
254 let pat = stmt.pat()?;
255 let initializer = stmt.initializer()?;
256 Some(format!(
257 "let {} = {} else",
258 snippet_from_node(pat.syntax(), config),
259 snippet_from_node(initializer.syntax(), config)
260 ))
261}
262
263fn snippet_from_node(node: &SyntaxNode, config: &InlayHintsConfig<'_>) -> String {
264 let mut text = node.text().to_string();
265 if text.contains('\n') {
266 return ELLIPSIS.into();
267 }
268
269 let Some(limit) = config.max_length else {
270 return text;
271 };
272 if limit == 0 {
273 return ELLIPSIS.into();
274 }
275
276 if text.len() <= limit {
277 return text;
278 }
279
280 let boundary = text.floor_char_boundary(limit.min(text.len()));
281 if boundary == text.len() {
282 return text;
283 }
284
285 let cut = text[..boundary]
286 .char_indices()
287 .rev()
288 .find(|&(_, ch)| ch == ' ')
289 .map(|(idx, _)| idx)
290 .unwrap_or(0);
291 text.truncate(cut);
292 text.push_str(ELLIPSIS);
293 text
294}
295
296#[cfg(test)]
297mod tests {
298 use expect_test::expect;
299
300 use crate::{
301 InlayHintsConfig,
302 inlay_hints::tests::{DISABLED_CONFIG, check_expect, check_with_config},
303 };
304
305 #[test]
306 fn hints_closing_brace() {
307 check_with_config(
308 InlayHintsConfig { closing_brace_hints_min_lines: Some(2), ..DISABLED_CONFIG },
309 r#"
310fn a() {}
311
312fn f() {
313} // no hint unless `}` is the last token on the line
314
315fn g() {
316 }
317//^ fn g
318
319fn h<T>(with: T, arguments: u8, ...) {
320 }
321//^ fn h
322
323async fn async_fn() {
324 }
325//^ async fn async_fn
326
327trait Tr {
328 fn f();
329 fn g() {
330 }
331 //^ fn g
332 }
333//^ trait Tr
334impl Tr for () {
335 }
336//^ impl Tr for ()
337impl dyn Tr {
338 }
339//^ impl dyn Tr + 'static
340
341static S0: () = 0;
342static S1: () = {};
343static S2: () = {
344 };
345//^ static S2
346const _: () = {
347 };
348//^ const _
349
350mod m {
351 }
352//^ mod m
353
354m! {}
355m!();
356m!(
357 );
358//^ m!
359
360m! {
361 }
362//^ m!
363
364fn f() {
365 let v = vec![
366 ];
367 }
368//^ fn f
369"#,
370 );
371 }
372
373 #[test]
374 fn hints_closing_brace_for_block_expr() {
375 check_with_config(
376 InlayHintsConfig { closing_brace_hints_min_lines: Some(2), ..DISABLED_CONFIG },
377 r#"
378fn test() {
379 'end: {
380 'do_a: {
381 'do_b: {
382
383 }
384 //^ 'do_b
385 break 'end;
386 }
387 //^ 'do_a
388 }
389 //^ 'end
390
391 'a: loop {
392 'b: for i in 0..5 {
393 'c: while true {
394
395
396 }
397 //^ 'c
398 }
399 //^ 'b
400 }
401 //^ 'a
402
403 }
404//^ fn test
405"#,
406 );
407 }
408
409 #[test]
410 fn hints_closing_brace_additional_blocks() {
411 check_expect(
412 InlayHintsConfig { closing_brace_hints_min_lines: Some(2), ..DISABLED_CONFIG },
413 r#"
414fn demo() {
415 loop {
416
417 }
418
419 while let Some(value) = next() {
420
421 }
422
423 for value in iter {
424
425 }
426
427 if cond {
428
429 }
430
431 if let Some(x) = maybe {
432
433 }
434
435 if other {
436 } else {
437
438 }
439
440 let Some(v) = maybe else {
441
442 };
443
444 match maybe {
445 Some(v) => {
446
447 }
448 value if check(value) => {
449
450 }
451 None => {}
452 }
453}
454"#,
455 expect![[r#"
456 [
457 (
458 364..365,
459 [
460 InlayHintLabelPart {
461 text: "fn demo",
462 linked_location: Some(
463 Computed(
464 FileRangeWrapper {
465 file_id: FileId(
466 0,
467 ),
468 range: 3..7,
469 },
470 ),
471 ),
472 tooltip: "",
473 },
474 ],
475 ),
476 (
477 28..29,
478 [
479 "loop",
480 ],
481 ),
482 (
483 73..74,
484 [
485 "while let Some(value) = next()",
486 ],
487 ),
488 (
489 105..106,
490 [
491 "for value in iter",
492 ],
493 ),
494 (
495 127..128,
496 [
497 "if cond",
498 ],
499 ),
500 (
501 164..165,
502 [
503 "if let Some(x) = maybe",
504 ],
505 ),
506 (
507 200..201,
508 [
509 "else",
510 ],
511 ),
512 (
513 240..241,
514 [
515 "let Some(v) = maybe else",
516 ],
517 ),
518 (
519 362..363,
520 [
521 "match maybe",
522 ],
523 ),
524 ]
525 "#]],
526 );
527 }
528}