]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-completion/src/completions/postfix.rs
Rollup merge of #98204 - Kixiron:stable-unzip, r=thomcc
[rust.git] / src / tools / rust-analyzer / crates / ide-completion / src / completions / postfix.rs
1 //! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`.
2
3 mod format_like;
4
5 use hir::{Documentation, HasAttrs};
6 use ide_db::{imports::insert_use::ImportScope, ty_filter::TryEnum, SnippetCap};
7 use syntax::{
8     ast::{self, AstNode, AstToken},
9     SyntaxKind::{EXPR_STMT, STMT_LIST},
10     TextRange, TextSize,
11 };
12 use text_edit::TextEdit;
13
14 use crate::{
15     completions::postfix::format_like::add_format_like_completions,
16     context::{CompletionContext, DotAccess, DotAccessKind},
17     item::{Builder, CompletionRelevancePostfixMatch},
18     CompletionItem, CompletionItemKind, CompletionRelevance, Completions, SnippetScope,
19 };
20
21 pub(crate) fn complete_postfix(
22     acc: &mut Completions,
23     ctx: &CompletionContext<'_>,
24     dot_access: &DotAccess,
25 ) {
26     if !ctx.config.enable_postfix_completions {
27         return;
28     }
29
30     let (dot_receiver, receiver_ty, receiver_is_ambiguous_float_literal) = match dot_access {
31         DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. } => (
32             it,
33             &ty.original,
34             match *kind {
35                 DotAccessKind::Field { receiver_is_ambiguous_float_literal } => {
36                     receiver_is_ambiguous_float_literal
37                 }
38                 DotAccessKind::Method { .. } => false,
39             },
40         ),
41         _ => return,
42     };
43
44     let receiver_text = get_receiver_text(dot_receiver, receiver_is_ambiguous_float_literal);
45
46     let cap = match ctx.config.snippet_cap {
47         Some(it) => it,
48         None => return,
49     };
50
51     let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
52         Some(it) => it,
53         None => return,
54     };
55
56     if let Some(drop_trait) = ctx.famous_defs().core_ops_Drop() {
57         if receiver_ty.impls_trait(ctx.db, drop_trait, &[]) {
58             if let &[hir::AssocItem::Function(drop_fn)] = &*drop_trait.items(ctx.db) {
59                 cov_mark::hit!(postfix_drop_completion);
60                 // FIXME: check that `drop` is in scope, use fully qualified path if it isn't/if shadowed
61                 let mut item = postfix_snippet(
62                     "drop",
63                     "fn drop(&mut self)",
64                     &format!("drop($0{})", receiver_text),
65                 );
66                 item.set_documentation(drop_fn.docs(ctx.db));
67                 item.add_to(acc);
68             }
69         }
70     }
71
72     if !ctx.config.snippets.is_empty() {
73         add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
74     }
75
76     let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
77     if let Some(try_enum) = &try_enum {
78         match try_enum {
79             TryEnum::Result => {
80                 postfix_snippet(
81                     "ifl",
82                     "if let Ok {}",
83                     &format!("if let Ok($1) = {} {{\n    $0\n}}", receiver_text),
84                 )
85                 .add_to(acc);
86
87                 postfix_snippet(
88                     "while",
89                     "while let Ok {}",
90                     &format!("while let Ok($1) = {} {{\n    $0\n}}", receiver_text),
91                 )
92                 .add_to(acc);
93             }
94             TryEnum::Option => {
95                 postfix_snippet(
96                     "ifl",
97                     "if let Some {}",
98                     &format!("if let Some($1) = {} {{\n    $0\n}}", receiver_text),
99                 )
100                 .add_to(acc);
101
102                 postfix_snippet(
103                     "while",
104                     "while let Some {}",
105                     &format!("while let Some($1) = {} {{\n    $0\n}}", receiver_text),
106                 )
107                 .add_to(acc);
108             }
109         }
110     } else if receiver_ty.is_bool() || receiver_ty.is_unknown() {
111         postfix_snippet("if", "if expr {}", &format!("if {} {{\n    $0\n}}", receiver_text))
112             .add_to(acc);
113         postfix_snippet(
114             "while",
115             "while expr {}",
116             &format!("while {} {{\n    $0\n}}", receiver_text),
117         )
118         .add_to(acc);
119         postfix_snippet("not", "!expr", &format!("!{}", receiver_text)).add_to(acc);
120     } else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator() {
121         if receiver_ty.impls_trait(ctx.db, trait_, &[]) {
122             postfix_snippet(
123                 "for",
124                 "for ele in expr {}",
125                 &format!("for ele in {} {{\n    $0\n}}", receiver_text),
126             )
127             .add_to(acc);
128         }
129     }
130
131     postfix_snippet("ref", "&expr", &format!("&{}", receiver_text)).add_to(acc);
132     postfix_snippet("refm", "&mut expr", &format!("&mut {}", receiver_text)).add_to(acc);
133
134     // The rest of the postfix completions create an expression that moves an argument,
135     // so it's better to consider references now to avoid breaking the compilation
136     let dot_receiver = include_references(dot_receiver);
137     let receiver_text = get_receiver_text(&dot_receiver, receiver_is_ambiguous_float_literal);
138     let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, &dot_receiver) {
139         Some(it) => it,
140         None => return,
141     };
142
143     match try_enum {
144         Some(try_enum) => match try_enum {
145             TryEnum::Result => {
146                 postfix_snippet(
147                     "match",
148                     "match expr {}",
149                     &format!("match {} {{\n    Ok(${{1:_}}) => {{$2}},\n    Err(${{3:_}}) => {{$0}},\n}}", receiver_text),
150                 )
151                 .add_to(acc);
152             }
153             TryEnum::Option => {
154                 postfix_snippet(
155                     "match",
156                     "match expr {}",
157                     &format!(
158                         "match {} {{\n    Some(${{1:_}}) => {{$2}},\n    None => {{$0}},\n}}",
159                         receiver_text
160                     ),
161                 )
162                 .add_to(acc);
163             }
164         },
165         None => {
166             postfix_snippet(
167                 "match",
168                 "match expr {}",
169                 &format!("match {} {{\n    ${{1:_}} => {{$0}},\n}}", receiver_text),
170             )
171             .add_to(acc);
172         }
173     }
174
175     postfix_snippet("box", "Box::new(expr)", &format!("Box::new({})", receiver_text)).add_to(acc);
176     postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc); // fixme
177     postfix_snippet("dbgr", "dbg!(&expr)", &format!("dbg!(&{})", receiver_text)).add_to(acc);
178     postfix_snippet("call", "function(expr)", &format!("${{1}}({})", receiver_text)).add_to(acc);
179
180     if let Some(parent) = dot_receiver.syntax().parent().and_then(|p| p.parent()) {
181         if matches!(parent.kind(), STMT_LIST | EXPR_STMT) {
182             postfix_snippet("let", "let", &format!("let $0 = {};", receiver_text)).add_to(acc);
183             postfix_snippet("letm", "let mut", &format!("let mut $0 = {};", receiver_text))
184                 .add_to(acc);
185         }
186     }
187
188     if let ast::Expr::Literal(literal) = dot_receiver.clone() {
189         if let Some(literal_text) = ast::String::cast(literal.token()) {
190             add_format_like_completions(acc, ctx, &dot_receiver, cap, &literal_text);
191         }
192     }
193 }
194
195 fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String {
196     let text = if receiver_is_ambiguous_float_literal {
197         let text = receiver.syntax().text();
198         let without_dot = ..text.len() - TextSize::of('.');
199         text.slice(without_dot).to_string()
200     } else {
201         receiver.to_string()
202     };
203
204     // The receiver texts should be interpreted as-is, as they are expected to be
205     // normal Rust expressions. We escape '\' and '$' so they don't get treated as
206     // snippet-specific constructs.
207     //
208     // Note that we don't need to escape the other characters that can be escaped,
209     // because they wouldn't be treated as snippet-specific constructs without '$'.
210     text.replace('\\', "\\\\").replace('$', "\\$")
211 }
212
213 fn include_references(initial_element: &ast::Expr) -> ast::Expr {
214     let mut resulting_element = initial_element.clone();
215     while let Some(parent_ref_element) =
216         resulting_element.syntax().parent().and_then(ast::RefExpr::cast)
217     {
218         resulting_element = ast::Expr::from(parent_ref_element);
219     }
220     resulting_element
221 }
222
223 fn build_postfix_snippet_builder<'ctx>(
224     ctx: &'ctx CompletionContext<'_>,
225     cap: SnippetCap,
226     receiver: &'ctx ast::Expr,
227 ) -> Option<impl Fn(&str, &str, &str) -> Builder + 'ctx> {
228     let receiver_syntax = receiver.syntax();
229     let receiver_range = ctx.sema.original_range_opt(receiver_syntax)?.range;
230     if ctx.source_range().end() < receiver_range.start() {
231         // This shouldn't happen, yet it does. I assume this might be due to an incorrect token mapping.
232         return None;
233     }
234     let delete_range = TextRange::new(receiver_range.start(), ctx.source_range().end());
235
236     // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that
237     // can't be annotated for the closure, hence fix it by constructing it without the Option first
238     fn build<'ctx>(
239         ctx: &'ctx CompletionContext<'_>,
240         cap: SnippetCap,
241         delete_range: TextRange,
242     ) -> impl Fn(&str, &str, &str) -> Builder + 'ctx {
243         move |label, detail, snippet| {
244             let edit = TextEdit::replace(delete_range, snippet.to_string());
245             let mut item =
246                 CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), label);
247             item.detail(detail).snippet_edit(cap, edit);
248             let postfix_match = if ctx.original_token.text() == label {
249                 cov_mark::hit!(postfix_exact_match_is_high_priority);
250                 Some(CompletionRelevancePostfixMatch::Exact)
251             } else {
252                 cov_mark::hit!(postfix_inexact_match_is_low_priority);
253                 Some(CompletionRelevancePostfixMatch::NonExact)
254             };
255             let relevance = CompletionRelevance { postfix_match, ..Default::default() };
256             item.set_relevance(relevance);
257             item
258         }
259     }
260     Some(build(ctx, cap, delete_range))
261 }
262
263 fn add_custom_postfix_completions(
264     acc: &mut Completions,
265     ctx: &CompletionContext<'_>,
266     postfix_snippet: impl Fn(&str, &str, &str) -> Builder,
267     receiver_text: &str,
268 ) -> Option<()> {
269     if ImportScope::find_insert_use_container(&ctx.token.parent()?, &ctx.sema).is_none() {
270         return None;
271     }
272     ctx.config.postfix_snippets().filter(|(_, snip)| snip.scope == SnippetScope::Expr).for_each(
273         |(trigger, snippet)| {
274             let imports = match snippet.imports(ctx) {
275                 Some(imports) => imports,
276                 None => return,
277             };
278             let body = snippet.postfix_snippet(receiver_text);
279             let mut builder =
280                 postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), &body);
281             builder.documentation(Documentation::new(format!("```rust\n{}\n```", body)));
282             for import in imports.into_iter() {
283                 builder.add_import(import);
284             }
285             builder.add_to(acc);
286         },
287     );
288     None
289 }
290
291 #[cfg(test)]
292 mod tests {
293     use expect_test::{expect, Expect};
294
295     use crate::{
296         tests::{check_edit, check_edit_with_config, completion_list, TEST_CONFIG},
297         CompletionConfig, Snippet,
298     };
299
300     fn check(ra_fixture: &str, expect: Expect) {
301         let actual = completion_list(ra_fixture);
302         expect.assert_eq(&actual)
303     }
304
305     #[test]
306     fn postfix_completion_works_for_trivial_path_expression() {
307         check(
308             r#"
309 fn main() {
310     let bar = true;
311     bar.$0
312 }
313 "#,
314             expect![[r#"
315                 sn box   Box::new(expr)
316                 sn call  function(expr)
317                 sn dbg   dbg!(expr)
318                 sn dbgr  dbg!(&expr)
319                 sn if    if expr {}
320                 sn let   let
321                 sn letm  let mut
322                 sn match match expr {}
323                 sn not   !expr
324                 sn ref   &expr
325                 sn refm  &mut expr
326                 sn while while expr {}
327             "#]],
328         );
329     }
330
331     #[test]
332     fn postfix_completion_works_for_function_calln() {
333         check(
334             r#"
335 fn foo(elt: bool) -> bool {
336     !elt
337 }
338
339 fn main() {
340     let bar = true;
341     foo(bar.$0)
342 }
343 "#,
344             expect![[r#"
345                 sn box   Box::new(expr)
346                 sn call  function(expr)
347                 sn dbg   dbg!(expr)
348                 sn dbgr  dbg!(&expr)
349                 sn if    if expr {}
350                 sn match match expr {}
351                 sn not   !expr
352                 sn ref   &expr
353                 sn refm  &mut expr
354                 sn while while expr {}
355             "#]],
356         );
357     }
358
359     #[test]
360     fn postfix_type_filtering() {
361         check(
362             r#"
363 fn main() {
364     let bar: u8 = 12;
365     bar.$0
366 }
367 "#,
368             expect![[r#"
369                 sn box   Box::new(expr)
370                 sn call  function(expr)
371                 sn dbg   dbg!(expr)
372                 sn dbgr  dbg!(&expr)
373                 sn let   let
374                 sn letm  let mut
375                 sn match match expr {}
376                 sn ref   &expr
377                 sn refm  &mut expr
378             "#]],
379         )
380     }
381
382     #[test]
383     fn let_middle_block() {
384         check(
385             r#"
386 fn main() {
387     baz.l$0
388     res
389 }
390 "#,
391             expect![[r#"
392                 sn box   Box::new(expr)
393                 sn call  function(expr)
394                 sn dbg   dbg!(expr)
395                 sn dbgr  dbg!(&expr)
396                 sn if    if expr {}
397                 sn let   let
398                 sn letm  let mut
399                 sn match match expr {}
400                 sn not   !expr
401                 sn ref   &expr
402                 sn refm  &mut expr
403                 sn while while expr {}
404             "#]],
405         );
406     }
407
408     #[test]
409     fn option_iflet() {
410         check_edit(
411             "ifl",
412             r#"
413 //- minicore: option
414 fn main() {
415     let bar = Some(true);
416     bar.$0
417 }
418 "#,
419             r#"
420 fn main() {
421     let bar = Some(true);
422     if let Some($1) = bar {
423     $0
424 }
425 }
426 "#,
427         );
428     }
429
430     #[test]
431     fn result_match() {
432         check_edit(
433             "match",
434             r#"
435 //- minicore: result
436 fn main() {
437     let bar = Ok(true);
438     bar.$0
439 }
440 "#,
441             r#"
442 fn main() {
443     let bar = Ok(true);
444     match bar {
445     Ok(${1:_}) => {$2},
446     Err(${3:_}) => {$0},
447 }
448 }
449 "#,
450         );
451     }
452
453     #[test]
454     fn postfix_completion_works_for_ambiguous_float_literal() {
455         check_edit("refm", r#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#)
456     }
457
458     #[test]
459     fn works_in_simple_macro() {
460         check_edit(
461             "dbg",
462             r#"
463 macro_rules! m { ($e:expr) => { $e } }
464 fn main() {
465     let bar: u8 = 12;
466     m!(bar.d$0)
467 }
468 "#,
469             r#"
470 macro_rules! m { ($e:expr) => { $e } }
471 fn main() {
472     let bar: u8 = 12;
473     m!(dbg!(bar))
474 }
475 "#,
476         );
477     }
478
479     #[test]
480     fn postfix_completion_for_references() {
481         check_edit("dbg", r#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
482         check_edit("refm", r#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
483         check_edit(
484             "ifl",
485             r#"
486 //- minicore: option
487 fn main() {
488     let bar = &Some(true);
489     bar.$0
490 }
491 "#,
492             r#"
493 fn main() {
494     let bar = &Some(true);
495     if let Some($1) = bar {
496     $0
497 }
498 }
499 "#,
500         )
501     }
502
503     #[test]
504     fn custom_postfix_completion() {
505         let config = CompletionConfig {
506             snippets: vec![Snippet::new(
507                 &[],
508                 &["break".into()],
509                 &["ControlFlow::Break(${receiver})".into()],
510                 "",
511                 &["core::ops::ControlFlow".into()],
512                 crate::SnippetScope::Expr,
513             )
514             .unwrap()],
515             ..TEST_CONFIG
516         };
517
518         check_edit_with_config(
519             config.clone(),
520             "break",
521             r#"
522 //- minicore: try
523 fn main() { 42.$0 }
524 "#,
525             r#"
526 use core::ops::ControlFlow;
527
528 fn main() { ControlFlow::Break(42) }
529 "#,
530         );
531
532         // The receiver texts should be escaped, see comments in `get_receiver_text()`
533         // for detail.
534         //
535         // Note that the last argument is what *lsp clients would see* rather than
536         // what users would see. Unescaping happens thereafter.
537         check_edit_with_config(
538             config.clone(),
539             "break",
540             r#"
541 //- minicore: try
542 fn main() { '\\'.$0 }
543 "#,
544             r#"
545 use core::ops::ControlFlow;
546
547 fn main() { ControlFlow::Break('\\\\') }
548 "#,
549         );
550
551         check_edit_with_config(
552             config.clone(),
553             "break",
554             r#"
555 //- minicore: try
556 fn main() {
557     match true {
558         true => "${1:placeholder}",
559         false => "\$",
560     }.$0
561 }
562 "#,
563             r#"
564 use core::ops::ControlFlow;
565
566 fn main() {
567     ControlFlow::Break(match true {
568         true => "\${1:placeholder}",
569         false => "\\\$",
570     })
571 }
572 "#,
573         );
574     }
575
576     #[test]
577     fn postfix_completion_for_format_like_strings() {
578         check_edit(
579             "format",
580             r#"fn main() { "{some_var:?}".$0 }"#,
581             r#"fn main() { format!("{:?}", some_var) }"#,
582         );
583         check_edit(
584             "panic",
585             r#"fn main() { "Panic with {a}".$0 }"#,
586             r#"fn main() { panic!("Panic with {}", a) }"#,
587         );
588         check_edit(
589             "println",
590             r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#,
591             r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
592         );
593         check_edit(
594             "loge",
595             r#"fn main() { "{2+2}".$0 }"#,
596             r#"fn main() { log::error!("{}", 2+2) }"#,
597         );
598         check_edit(
599             "logt",
600             r#"fn main() { "{2+2}".$0 }"#,
601             r#"fn main() { log::trace!("{}", 2+2) }"#,
602         );
603         check_edit(
604             "logd",
605             r#"fn main() { "{2+2}".$0 }"#,
606             r#"fn main() { log::debug!("{}", 2+2) }"#,
607         );
608         check_edit("logi", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::info!("{}", 2+2) }"#);
609         check_edit("logw", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::warn!("{}", 2+2) }"#);
610         check_edit(
611             "loge",
612             r#"fn main() { "{2+2}".$0 }"#,
613             r#"fn main() { log::error!("{}", 2+2) }"#,
614         );
615     }
616 }