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