Skip to main content

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 = 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            // Only display the hint if the `}` is the last token on the line
164            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}