1 //! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`.
5 use hir
::{ImportPathConfig, ItemInNs}
;
7 documentation
::{Documentation, HasDocs}
,
8 imports
::insert_use
::ImportScope
,
14 ast
::{self, make, AstNode, AstToken}
,
15 SyntaxKind
::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR}
,
18 use text_edit
::TextEdit
;
21 completions
::postfix
::format_like
::add_format_like_completions
,
22 context
::{BreakableKind, CompletionContext, DotAccess, DotAccessKind}
,
23 item
::{Builder, CompletionRelevancePostfixMatch}
,
24 CompletionItem
, CompletionItemKind
, CompletionRelevance
, Completions
, SnippetScope
,
27 pub(crate) fn complete_postfix(
28 acc
: &mut Completions
,
29 ctx
: &CompletionContext
<'_
>,
30 dot_access
: &DotAccess
,
32 if !ctx
.config
.enable_postfix_completions
{
36 let (dot_receiver
, receiver_ty
, receiver_is_ambiguous_float_literal
) = match dot_access
{
37 DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. }
=> (
41 DotAccessKind
::Field { receiver_is_ambiguous_float_literal }
=> {
42 receiver_is_ambiguous_float_literal
44 DotAccessKind
::Method { .. }
=> false,
49 let expr_ctx
= &dot_access
.ctx
;
51 let receiver_text
= get_receiver_text(dot_receiver
, receiver_is_ambiguous_float_literal
);
53 let cap
= match ctx
.config
.snippet_cap
{
58 let postfix_snippet
= match build_postfix_snippet_builder(ctx
, cap
, dot_receiver
) {
63 let cfg
= ImportPathConfig
{
64 prefer_no_std
: ctx
.config
.prefer_no_std
,
65 prefer_prelude
: ctx
.config
.prefer_prelude
,
68 if let Some(drop_trait
) = ctx
.famous_defs().core_ops_Drop() {
69 if receiver_ty
.impls_trait(ctx
.db
, drop_trait
, &[]) {
70 if let Some(drop_fn
) = ctx
.famous_defs().core_mem_drop() {
72 ctx
.module
.find_path(ctx
.db
, ItemInNs
::Values(drop_fn
.into()), cfg
)
74 cov_mark
::hit
!(postfix_drop_completion
);
75 let mut item
= postfix_snippet(
78 &format
!("{path}($0{receiver_text})", path
= path
.display(ctx
.db
)),
80 item
.set_documentation(drop_fn
.docs(ctx
.db
));
81 item
.add_to(acc
, ctx
.db
);
87 let try_enum
= TryEnum
::from_ty(&ctx
.sema
, &receiver_ty
.strip_references());
88 if let Some(try_enum
) = &try_enum
{
94 &format
!("if let Ok($1) = {receiver_text} {{\n $0\n}}"),
101 &format
!("let Ok($1) = {receiver_text} else {{\n $2\n}};\n$0"),
103 .add_to(acc
, ctx
.db
);
108 &format
!("while let Ok($1) = {receiver_text} {{\n $0\n}}"),
110 .add_to(acc
, ctx
.db
);
116 &format
!("if let Some($1) = {receiver_text} {{\n $0\n}}"),
118 .add_to(acc
, ctx
.db
);
123 &format
!("let Some($1) = {receiver_text} else {{\n $2\n}};\n$0"),
125 .add_to(acc
, ctx
.db
);
130 &format
!("while let Some($1) = {receiver_text} {{\n $0\n}}"),
132 .add_to(acc
, ctx
.db
);
135 } else if receiver_ty
.is_bool() || receiver_ty
.is_unknown() {
136 postfix_snippet("if", "if expr {}", &format
!("if {receiver_text} {{\n $0\n}}"))
137 .add_to(acc
, ctx
.db
);
138 postfix_snippet("while", "while expr {}", &format
!("while {receiver_text} {{\n $0\n}}"))
139 .add_to(acc
, ctx
.db
);
140 postfix_snippet("not", "!expr", &format
!("!{receiver_text}")).add_to(acc
, ctx
.db
);
141 } else if let Some(trait_
) = ctx
.famous_defs().core_iter_IntoIterator() {
142 if receiver_ty
.impls_trait(ctx
.db
, trait_
, &[]) {
145 "for ele in expr {}",
146 &format
!("for ele in {receiver_text} {{\n $0\n}}"),
148 .add_to(acc
, ctx
.db
);
152 postfix_snippet("ref", "&expr", &format
!("&{receiver_text}")).add_to(acc
, ctx
.db
);
153 postfix_snippet("refm", "&mut expr", &format
!("&mut {receiver_text}")).add_to(acc
, ctx
.db
);
154 postfix_snippet("deref", "*expr", &format
!("*{receiver_text}")).add_to(acc
, ctx
.db
);
156 let mut unsafe_should_be_wrapped
= true;
157 if dot_receiver
.syntax().kind() == BLOCK_EXPR
{
158 unsafe_should_be_wrapped
= false;
159 if let Some(parent
) = dot_receiver
.syntax().parent() {
160 if matches
!(parent
.kind(), IF_EXPR
| WHILE_EXPR
| LOOP_EXPR
| FOR_EXPR
) {
161 unsafe_should_be_wrapped
= true;
165 let unsafe_completion_string
= if unsafe_should_be_wrapped
{
166 format
!("unsafe {{ {receiver_text} }}")
168 format
!("unsafe {receiver_text}")
170 postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string
).add_to(acc
, ctx
.db
);
172 // The rest of the postfix completions create an expression that moves an argument,
173 // so it's better to consider references now to avoid breaking the compilation
175 let (dot_receiver
, node_to_replace_with
) = include_references(dot_receiver
);
177 get_receiver_text(&node_to_replace_with
, receiver_is_ambiguous_float_literal
);
178 let postfix_snippet
= match build_postfix_snippet_builder(ctx
, cap
, &dot_receiver
) {
183 if !ctx
.config
.snippets
.is_empty() {
184 add_custom_postfix_completions(acc
, ctx
, &postfix_snippet
, &receiver_text
);
188 Some(try_enum
) => match try_enum
{
193 &format
!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"),
195 .add_to(acc
, ctx
.db
);
202 "match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}"
205 .add_to(acc
, ctx
.db
);
212 &format
!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"),
214 .add_to(acc
, ctx
.db
);
218 postfix_snippet("box", "Box::new(expr)", &format
!("Box::new({receiver_text})"))
219 .add_to(acc
, ctx
.db
);
220 postfix_snippet("dbg", "dbg!(expr)", &format
!("dbg!({receiver_text})")).add_to(acc
, ctx
.db
); // fixme
221 postfix_snippet("dbgr", "dbg!(&expr)", &format
!("dbg!(&{receiver_text})")).add_to(acc
, ctx
.db
);
222 postfix_snippet("call", "function(expr)", &format
!("${{1}}({receiver_text})"))
223 .add_to(acc
, ctx
.db
);
225 if let Some(parent
) = dot_receiver
.syntax().parent().and_then(|p
| p
.parent()) {
226 if matches
!(parent
.kind(), STMT_LIST
| EXPR_STMT
) {
227 postfix_snippet("let", "let", &format
!("let $0 = {receiver_text};"))
228 .add_to(acc
, ctx
.db
);
229 postfix_snippet("letm", "let mut", &format
!("let mut $0 = {receiver_text};"))
230 .add_to(acc
, ctx
.db
);
234 if let ast
::Expr
::Literal(literal
) = dot_receiver
.clone() {
235 if let Some(literal_text
) = ast
::String
::cast(literal
.token()) {
236 add_format_like_completions(acc
, ctx
, &dot_receiver
, cap
, &literal_text
);
244 "return {receiver_text}{semi}",
245 semi
= if expr_ctx
.in_block_expr { ";" }
else { "" }
248 .add_to(acc
, ctx
.db
);
250 if let BreakableKind
::Block
| BreakableKind
::Loop
= expr_ctx
.in_breakable
{
255 "break {receiver_text}{semi}",
256 semi
= if expr_ctx
.in_block_expr { ";" }
else { "" }
259 .add_to(acc
, ctx
.db
);
263 fn get_receiver_text(receiver
: &ast
::Expr
, receiver_is_ambiguous_float_literal
: bool
) -> String
{
264 let mut text
= if receiver_is_ambiguous_float_literal
{
265 let text
= receiver
.syntax().text();
266 let without_dot
= ..text
.len() - TextSize
::of('
.'
);
267 text
.slice(without_dot
).to_string()
272 // The receiver texts should be interpreted as-is, as they are expected to be
273 // normal Rust expressions.
274 escape_snippet_bits(&mut text
);
278 /// Escapes `\` and `$` so that they don't get interpreted as snippet-specific constructs.
280 /// Note that we don't need to escape the other characters that can be escaped,
281 /// because they wouldn't be treated as snippet-specific constructs without '$'.
282 fn escape_snippet_bits(text
: &mut String
) {
283 stdx
::replace(text
, '
\\'
, "\\\\");
284 stdx
::replace(text
, '$'
, "\\$");
287 fn include_references(initial_element
: &ast
::Expr
) -> (ast
::Expr
, ast
::Expr
) {
288 let mut resulting_element
= initial_element
.clone();
290 while let Some(field_expr
) = resulting_element
.syntax().parent().and_then(ast
::FieldExpr
::cast
)
292 resulting_element
= ast
::Expr
::from(field_expr
);
295 let mut new_element_opt
= initial_element
.clone();
297 if let Some(first_ref_expr
) = resulting_element
.syntax().parent().and_then(ast
::RefExpr
::cast
) {
298 if let Some(expr
) = first_ref_expr
.expr() {
299 resulting_element
= expr
;
302 while let Some(parent_ref_element
) =
303 resulting_element
.syntax().parent().and_then(ast
::RefExpr
::cast
)
305 resulting_element
= ast
::Expr
::from(parent_ref_element
);
307 new_element_opt
= make
::expr_ref(new_element_opt
, false);
310 // If we do not find any ref expressions, restore
311 // all the progress of tree climbing
312 resulting_element
= initial_element
.clone();
315 (resulting_element
, new_element_opt
)
318 fn build_postfix_snippet_builder
<'ctx
>(
319 ctx
: &'ctx CompletionContext
<'_
>,
321 receiver
: &'ctx ast
::Expr
,
322 ) -> Option
<impl Fn(&str, &str, &str) -> Builder
+ 'ctx
> {
323 let receiver_range
= ctx
.sema
.original_range_opt(receiver
.syntax())?
.range
;
324 if ctx
.source_range().end() < receiver_range
.start() {
325 // This shouldn't happen, yet it does. I assume this might be due to an incorrect token
330 let delete_range
= TextRange
::new(receiver_range
.start(), ctx
.source_range().end());
332 // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that
333 // can't be annotated for the closure, hence fix it by constructing it without the Option first
335 ctx
: &'ctx CompletionContext
<'_
>,
337 delete_range
: TextRange
,
338 ) -> impl Fn(&str, &str, &str) -> Builder
+ 'ctx
{
339 move |label
, detail
, snippet
| {
340 let edit
= TextEdit
::replace(delete_range
, snippet
.to_owned());
342 CompletionItem
::new(CompletionItemKind
::Snippet
, ctx
.source_range(), label
);
343 item
.detail(detail
).snippet_edit(cap
, edit
);
344 let postfix_match
= if ctx
.original_token
.text() == label
{
345 cov_mark
::hit
!(postfix_exact_match_is_high_priority
);
346 Some(CompletionRelevancePostfixMatch
::Exact
)
348 cov_mark
::hit
!(postfix_inexact_match_is_low_priority
);
349 Some(CompletionRelevancePostfixMatch
::NonExact
)
351 let relevance
= CompletionRelevance { postfix_match, ..Default::default() }
;
352 item
.set_relevance(relevance
);
356 Some(build(ctx
, cap
, delete_range
))
359 fn add_custom_postfix_completions(
360 acc
: &mut Completions
,
361 ctx
: &CompletionContext
<'_
>,
362 postfix_snippet
: impl Fn(&str, &str, &str) -> Builder
,
365 ImportScope
::find_insert_use_container(&ctx
.token
.parent()?
, &ctx
.sema
)?
;
366 ctx
.config
.postfix_snippets().filter(|(_
, snip
)| snip
.scope
== SnippetScope
::Expr
).for_each(
367 |(trigger
, snippet
)| {
368 let imports
= match snippet
.imports(ctx
) {
369 Some(imports
) => imports
,
372 let body
= snippet
.postfix_snippet(receiver_text
);
374 postfix_snippet(trigger
, snippet
.description
.as_deref().unwrap_or_default(), &body
);
375 builder
.documentation(Documentation
::new(format
!("```rust\n{body}\n```")));
376 for import
in imports
.into_iter() {
377 builder
.add_import(import
);
379 builder
.add_to(acc
, ctx
.db
);
387 use expect_test
::{expect, Expect}
;
390 tests
::{check_edit, check_edit_with_config, completion_list, TEST_CONFIG}
,
391 CompletionConfig
, Snippet
,
394 fn check(ra_fixture
: &str, expect
: Expect
) {
395 let actual
= completion_list(ra_fixture
);
396 expect
.assert_eq(&actual
)
400 fn postfix_completion_works_for_trivial_path_expression() {
409 sn box Box::new(expr)
410 sn call function(expr)
417 sn match match expr {}
421 sn return return expr
423 sn while while expr {}
429 fn postfix_completion_works_for_function_calln() {
432 fn foo(elt: bool) -> bool {
442 sn box Box::new(expr)
443 sn call function(expr)
448 sn match match expr {}
452 sn return return expr
454 sn while while expr {}
460 fn postfix_type_filtering() {
469 sn box Box::new(expr)
470 sn call function(expr)
476 sn match match expr {}
479 sn return return expr
486 fn let_middle_block() {
495 sn box Box::new(expr)
496 sn call function(expr)
503 sn match match expr {}
507 sn return return expr
509 sn while while expr {}
521 let bar = Some(true);
527 let bar = Some(true);
528 if let Some($1) = bar {
537 fn option_letelse() {
543 let bar = Some(true);
549 let bar = Some(true);
550 let Some($1) = bar else {
583 fn postfix_completion_works_for_ambiguous_float_literal() {
584 check_edit("refm", r
#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#)
588 fn works_in_simple_macro() {
592 macro_rules! m { ($e:expr) => { $e } }
599 macro_rules! m { ($e:expr) => { $e } }
609 fn postfix_completion_for_references() {
610 check_edit("dbg", r
#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
611 check_edit("refm", r
#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
617 let bar = &Some(true);
623 let bar = &Some(true);
624 if let Some($1) = bar {
633 fn postfix_completion_for_unsafe() {
634 check_edit("unsafe", r
#"fn main() { foo.$0 }"#, r#"fn main() { unsafe { foo } }"#);
635 check_edit("unsafe", r
#"fn main() { { foo }.$0 }"#, r#"fn main() { unsafe { foo } }"#);
638 r
#"fn main() { if x { foo }.$0 }"#,
639 r
#"fn main() { unsafe { if x { foo } } }"#,
643 r
#"fn main() { loop { foo }.$0 }"#,
644 r
#"fn main() { unsafe { loop { foo } } }"#,
648 r
#"fn main() { if true {}.$0 }"#,
649 r
#"fn main() { unsafe { if true {} } }"#,
653 r
#"fn main() { while true {}.$0 }"#,
654 r
#"fn main() { unsafe { while true {} } }"#,
658 r
#"fn main() { for i in 0..10 {}.$0 }"#,
659 r
#"fn main() { unsafe { for i in 0..10 {} } }"#,
663 r
#"fn main() { let x = if true {1} else {2}.$0 }"#,
664 r
#"fn main() { let x = unsafe { if true {1} else {2} } }"#,
667 // completion will not be triggered
670 r
#"fn main() { let x = true else {panic!()}.$0}"#,
671 r
#"fn main() { let x = true else {panic!()}.unsafe}"#,
676 fn custom_postfix_completion() {
677 let config
= CompletionConfig
{
678 snippets
: vec
![Snippet
::new(
681 &["ControlFlow::Break(${receiver})".into()],
683 &["core::ops::ControlFlow".into()],
684 crate::SnippetScope
::Expr
,
690 check_edit_with_config(
698 use core::ops::ControlFlow;
700 fn main() { ControlFlow::Break(42) }
704 // The receiver texts should be escaped, see comments in `get_receiver_text()`
707 // Note that the last argument is what *lsp clients would see* rather than
708 // what users would see. Unescaping happens thereafter.
709 check_edit_with_config(
714 fn main() { '\\'.$0 }
717 use core::ops::ControlFlow;
719 fn main() { ControlFlow::Break('\\\\') }
723 check_edit_with_config(
730 true => "${1:placeholder}",
736 use core::ops::ControlFlow;
739 ControlFlow::Break(match true {
740 true => "\${1:placeholder}",
749 fn postfix_completion_for_format_like_strings() {
752 r
#"fn main() { "{some_var:?}".$0 }"#,
753 r#"fn main() { format!("{some_var:?}") }"#,
757 r
#"fn main() { "Panic with {a}".$0 }"#,
758 r#"fn main() { panic!("Panic with {a}") }"#,
762 r
#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#,
763 r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
767 r
#"fn main() { "{2+2}".$0 }"#,
768 r#"fn main() { log::error!("{}", 2+2) }"#,
772 r
#"fn main() { "{2+2}".$0 }"#,
773 r#"fn main() { log::trace!("{}", 2+2) }"#,
777 r
#"fn main() { "{2+2}".$0 }"#,
778 r#"fn main() { log::debug!("{}", 2+2) }"#,
780 check_edit("logi", r
#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::info!("{}", 2+2) }"#);
781 check_edit("logw", r
#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::warn!("{}", 2+2) }"#);
784 r
#"fn main() { "{2+2}".$0 }"#,
785 r#"fn main() { log::error!("{}", 2+2) }"#,
790 fn postfix_custom_snippets_completion_for_references() {
791 // https://github.com/rust-lang/rust-analyzer/issues/7929
793 let snippet
= Snippet
::new(
796 &["Ok(${receiver})".into()],
799 crate::SnippetScope
::Expr
,
803 check_edit_with_config(
804 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }
,
806 r
#"fn main() { &&42.o$0 }"#,
807 r
#"fn main() { Ok(&&42) }"#,
810 check_edit_with_config(
811 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }
,
813 r
#"fn main() { &&42.$0 }"#,
814 r
#"fn main() { Ok(&&42) }"#,
817 check_edit_with_config(
818 CompletionConfig { snippets: vec![snippet], ..TEST_CONFIG }
,
844 fn no_postfix_completions_in_if_block_that_has_an_else() {
848 if true {}.$0 else {}