Skip to main content

ide/inlay_hints/
closure_captures.rs

1//! Implementation of "closure captures" inlay hints.
2//!
3//! Tests live in [`bind_pat`][super::bind_pat] module.
4use either::Either;
5use ide_db::famous_defs::FamousDefs;
6use span::Edition;
7use stdx::{TupleExt, never};
8use syntax::ast::{self, AstNode};
9
10use crate::{
11    InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintPosition, InlayHintsConfig, InlayKind,
12};
13
14pub(super) fn hints(
15    acc: &mut Vec<InlayHint>,
16    FamousDefs(sema, _): &FamousDefs<'_, '_>,
17    config: &InlayHintsConfig<'_>,
18    expr: Either<ast::ClosureExpr, ast::BlockExpr>,
19    edition: Edition,
20) -> Option<()> {
21    if !config.closure_capture_hints {
22        return None;
23    }
24
25    let (expr, move_token, capture_anchor) = match expr {
26        Either::Left(closure) => {
27            let move_token = closure.move_token();
28            let capture_anchor = closure.param_list()?.pipe_token()?;
29            (closure.into(), move_token, capture_anchor)
30        }
31        Either::Right(block) => {
32            let modifier = block.modifier()?;
33            match modifier {
34                ast::BlockModifier::Async(_)
35                | ast::BlockModifier::Gen(_)
36                | ast::BlockModifier::AsyncGen(_) => (),
37                ast::BlockModifier::Unsafe(_)
38                | ast::BlockModifier::Try { .. }
39                | ast::BlockModifier::Const(_)
40                | ast::BlockModifier::Label(_) => return None,
41            }
42            let move_token = block.move_token();
43            let capture_anchor = block.stmt_list()?.l_curly_token()?;
44            (block.into(), move_token, capture_anchor)
45        }
46    };
47
48    let ty = &sema.type_of_expr(&expr)?.original;
49    let captures = match ty.as_closure() {
50        Some(closure) => closure.captured_items(sema.db),
51        None => ty.as_coroutine()?.captured_items(sema.db),
52    };
53
54    if captures.is_empty() {
55        return None;
56    }
57
58    let (range, label, position, pad_right) = match move_token {
59        Some(token) => {
60            (token.text_range(), InlayHintLabel::default(), InlayHintPosition::After, false)
61        }
62        None => (
63            capture_anchor.text_range(),
64            InlayHintLabel::from("move"),
65            InlayHintPosition::Before,
66            true,
67        ),
68    };
69    let mut hint = InlayHint {
70        range,
71        kind: InlayKind::ClosureCapture,
72        label,
73        text_edit: None,
74        position,
75        pad_left: false,
76        pad_right,
77        resolve_parent: Some(expr.syntax().text_range()),
78    };
79    hint.label.append_str("(");
80    let last = captures.len() - 1;
81    for (idx, capture) in captures.into_iter().enumerate() {
82        let local = capture.local();
83
84        let label = format!(
85            "{}{}",
86            match capture.kind() {
87                hir::CaptureKind::SharedRef => "&",
88                hir::CaptureKind::UniqueSharedRef => "&unique ",
89                hir::CaptureKind::MutableRef => "&mut ",
90                hir::CaptureKind::Move => "",
91            },
92            capture.display_place_source_code(sema.db, edition)
93        );
94        if never!(label.is_empty()) {
95            continue;
96        }
97        hint.label.append_part(InlayHintLabelPart {
98            text: label,
99            linked_location: config.lazy_location_opt(|| {
100                let source = local.primary_source(sema.db);
101
102                // force cache the source file, otherwise sema lookup will potentially panic
103                _ = sema.parse_or_expand(source.file());
104                source.name().and_then(|name| {
105                    name.syntax().original_file_range_opt(sema.db).map(TupleExt::head).map(
106                        |frange| ide_db::FileRange {
107                            file_id: frange.file_id.file_id(sema.db),
108                            range: frange.range,
109                        },
110                    )
111                })
112            }),
113            tooltip: None,
114        });
115
116        if idx != last {
117            hint.label.append_str(", ");
118        }
119    }
120    hint.label.append_str(")");
121    acc.push(hint);
122    Some(())
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::{
128        InlayHintsConfig,
129        inlay_hints::tests::{DISABLED_CONFIG, check_with_config},
130    };
131
132    #[test]
133    fn all_capture_kinds() {
134        check_with_config(
135            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
136            r#"
137//- minicore: copy, derive, fn
138
139
140#[derive(Copy, Clone)]
141struct Copy;
142
143struct NonCopy;
144
145fn main() {
146    let foo = Copy;
147    let bar = NonCopy;
148    let mut baz = NonCopy;
149    let qux = &mut NonCopy;
150    || {
151 // ^ move(&foo, bar, baz, qux)
152        foo;
153        bar;
154        baz;
155        qux;
156    };
157    || {
158 // ^ move(&foo, &bar, &baz, &qux)
159        &foo;
160        &bar;
161        &baz;
162        &qux;
163    };
164    || {
165 // ^ move(&mut baz)
166        &mut baz;
167    };
168    || {
169 // ^ move(&mut baz, &mut *qux)
170        baz = NonCopy;
171        *qux = NonCopy;
172    };
173}
174"#,
175        );
176    }
177
178    #[test]
179    fn all_capture_kinds_async_closure() {
180        check_with_config(
181            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
182            r#"
183//- minicore: copy, derive, fn, future, async_fn
184
185#[derive(Copy, Clone)]
186struct Copy;
187
188struct NonCopy;
189
190fn main() {
191    let foo = Copy;
192    let bar = NonCopy;
193    let mut baz = NonCopy;
194    let qux = &mut NonCopy;
195    async || {
196       // ^ move(&foo, bar, baz, qux)
197        foo;
198        bar;
199        baz;
200        qux;
201    };
202    async || {
203       // ^ move(&foo, &bar, &baz, &qux)
204        &foo;
205        &bar;
206        &baz;
207        &qux;
208    };
209    async || {
210       // ^ move(&mut baz)
211        &mut baz;
212    };
213    async || {
214       // ^ move(&mut baz, &mut *qux)
215        baz = NonCopy;
216        *qux = NonCopy;
217    };
218}
219
220"#,
221        );
222    }
223
224    #[test]
225    fn all_capture_kinds_async_block() {
226        check_with_config(
227            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
228            r#"
229//- minicore: copy, derive, future
230
231#[derive(Copy, Clone)]
232struct Copy;
233
234struct NonCopy;
235
236fn main() {
237    let foo = Copy;
238    let bar = NonCopy;
239    let mut baz = NonCopy;
240    let qux = &mut NonCopy;
241    async {
242       // ^ move(&foo, bar, baz, qux)
243        foo;
244        bar;
245        baz;
246        qux;
247    };
248    async {
249       // ^ move(&foo, &bar, &baz, &qux)
250        &foo;
251        &bar;
252        &baz;
253        &qux;
254    };
255    async {
256       // ^ move(&mut baz)
257        &mut baz;
258    };
259    async {
260       // ^ move(&mut baz, &mut *qux)
261        baz = NonCopy;
262        *qux = NonCopy;
263    };
264}
265"#,
266        );
267    }
268
269    #[test]
270    fn nested_coroutine_does_not_capture_parent_local() {
271        check_with_config(
272            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
273            r#"
274//- minicore: copy, future
275fn main() {
276    async {
277        let foo = 1;
278        async {
279           // ^ move(&foo)
280            foo;
281        }
282    };
283}
284"#,
285        );
286    }
287
288    #[test]
289    fn coroutine_blocks() {
290        check_with_config(
291            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
292            r#"
293//- minicore: copy, future
294fn main() {
295    let foo = 0;
296    gen {
297     // ^ move(&foo)
298        foo;
299        yield ();
300    };
301    async gen {
302           // ^ move(&foo)
303        foo;
304        yield ();
305    };
306}
307"#,
308        );
309    }
310
311    #[test]
312    fn legacy_coroutine() {
313        check_with_config(
314            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
315            r#"
316//- minicore: copy, coroutine
317fn main() {
318    let foo = 0;
319    let coroutine = #[coroutine] || {
320                              // ^ move(&foo)
321        foo;
322        yield ();
323    };
324}
325"#,
326        );
327    }
328
329    #[test]
330    fn move_token() {
331        check_with_config(
332            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
333            r#"
334//- minicore: copy, derive
335fn main() {
336    let foo = u32;
337    move || {
338//  ^^^^ (foo)
339        foo;
340    };
341}
342"#,
343        );
344        check_with_config(
345            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
346            r#"
347//- minicore: copy, derive
348fn main() {
349    let foo = u32;
350    async move || {
351      //  ^^^^ (foo)
352        foo;
353    };
354}
355"#,
356        );
357        check_with_config(
358            InlayHintsConfig { closure_capture_hints: true, ..DISABLED_CONFIG },
359            r#"
360//- minicore: copy, future
361fn main() {
362    let foo = 0;
363    async move {
364      //  ^^^^ (foo)
365        foo;
366    };
367}
368"#,
369        );
370    }
371}