ide/inlay_hints/
closing_brace.rs

1//! Implementation of "closing brace" inlay hints:
2//! ```no_run
3//! fn g() {
4//! } /* fn g */
5//! ```
6use 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        // in this case, `ast::Label` could be seen as a part of `ast::BlockExpr`
73        // the actual number of lines in this case should be the line count of the parent BlockExpr,
74        // which the `min_lines` config cares about
75        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            // Only display the hint if the `}` is the last token on the line
166            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}