1 //! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`.
6 documentation
::{Documentation, HasDocs}
,
7 imports
::insert_use
::ImportScope
,
12 ast
::{self, make, AstNode, AstToken}
,
13 SyntaxKind
::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR}
,
16 use text_edit
::TextEdit
;
19 completions
::postfix
::format_like
::add_format_like_completions
,
20 context
::{CompletionContext, DotAccess, DotAccessKind}
,
21 item
::{Builder, CompletionRelevancePostfixMatch}
,
22 CompletionItem
, CompletionItemKind
, CompletionRelevance
, Completions
, SnippetScope
,
25 pub(crate) fn complete_postfix(
26 acc
: &mut Completions
,
27 ctx
: &CompletionContext
<'_
>,
28 dot_access
: &DotAccess
,
30 if !ctx
.config
.enable_postfix_completions
{
34 let (dot_receiver
, receiver_ty
, receiver_is_ambiguous_float_literal
) = match dot_access
{
35 DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. }
=> (
39 DotAccessKind
::Field { receiver_is_ambiguous_float_literal }
=> {
40 receiver_is_ambiguous_float_literal
42 DotAccessKind
::Method { .. }
=> false,
48 let receiver_text
= get_receiver_text(dot_receiver
, receiver_is_ambiguous_float_literal
);
50 let cap
= match ctx
.config
.snippet_cap
{
55 let postfix_snippet
= match build_postfix_snippet_builder(ctx
, cap
, dot_receiver
) {
60 if let Some(drop_trait
) = ctx
.famous_defs().core_ops_Drop() {
61 if receiver_ty
.impls_trait(ctx
.db
, drop_trait
, &[]) {
62 if let &[hir
::AssocItem
::Function(drop_fn
)] = &*drop_trait
.items(ctx
.db
) {
63 cov_mark
::hit
!(postfix_drop_completion
);
64 // FIXME: check that `drop` is in scope, use fully qualified path if it isn't/if shadowed
65 let mut item
= postfix_snippet(
68 &format
!("drop($0{receiver_text})"),
70 item
.set_documentation(drop_fn
.docs(ctx
.db
));
71 item
.add_to(acc
, ctx
.db
);
76 let try_enum
= TryEnum
::from_ty(&ctx
.sema
, &receiver_ty
.strip_references());
77 if let Some(try_enum
) = &try_enum
{
83 &format
!("if let Ok($1) = {receiver_text} {{\n $0\n}}"),
90 &format
!("while let Ok($1) = {receiver_text} {{\n $0\n}}"),
98 &format
!("if let Some($1) = {receiver_text} {{\n $0\n}}"),
100 .add_to(acc
, ctx
.db
);
105 &format
!("while let Some($1) = {receiver_text} {{\n $0\n}}"),
107 .add_to(acc
, ctx
.db
);
110 } else if receiver_ty
.is_bool() || receiver_ty
.is_unknown() {
111 postfix_snippet("if", "if expr {}", &format
!("if {receiver_text} {{\n $0\n}}"))
112 .add_to(acc
, ctx
.db
);
113 postfix_snippet("while", "while expr {}", &format
!("while {receiver_text} {{\n $0\n}}"))
114 .add_to(acc
, ctx
.db
);
115 postfix_snippet("not", "!expr", &format
!("!{receiver_text}")).add_to(acc
, ctx
.db
);
116 } else if let Some(trait_
) = ctx
.famous_defs().core_iter_IntoIterator() {
117 if receiver_ty
.impls_trait(ctx
.db
, trait_
, &[]) {
120 "for ele in expr {}",
121 &format
!("for ele in {receiver_text} {{\n $0\n}}"),
123 .add_to(acc
, ctx
.db
);
127 postfix_snippet("ref", "&expr", &format
!("&{receiver_text}")).add_to(acc
, ctx
.db
);
128 postfix_snippet("refm", "&mut expr", &format
!("&mut {receiver_text}")).add_to(acc
, ctx
.db
);
130 let mut unsafe_should_be_wrapped
= true;
131 if dot_receiver
.syntax().kind() == BLOCK_EXPR
{
132 unsafe_should_be_wrapped
= false;
133 if let Some(parent
) = dot_receiver
.syntax().parent() {
134 if matches
!(parent
.kind(), IF_EXPR
| WHILE_EXPR
| LOOP_EXPR
| FOR_EXPR
) {
135 unsafe_should_be_wrapped
= true;
139 let unsafe_completion_string
= if unsafe_should_be_wrapped
{
140 format
!("unsafe {{ {receiver_text} }}")
142 format
!("unsafe {receiver_text}")
144 postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string
).add_to(acc
, ctx
.db
);
146 // The rest of the postfix completions create an expression that moves an argument,
147 // so it's better to consider references now to avoid breaking the compilation
149 let (dot_receiver
, node_to_replace_with
) = include_references(dot_receiver
);
151 get_receiver_text(&node_to_replace_with
, receiver_is_ambiguous_float_literal
);
152 let postfix_snippet
= match build_postfix_snippet_builder(ctx
, cap
, &dot_receiver
) {
157 if !ctx
.config
.snippets
.is_empty() {
158 add_custom_postfix_completions(acc
, ctx
, &postfix_snippet
, &receiver_text
);
162 Some(try_enum
) => match try_enum
{
167 &format
!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"),
169 .add_to(acc
, ctx
.db
);
176 "match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}"
179 .add_to(acc
, ctx
.db
);
186 &format
!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"),
188 .add_to(acc
, ctx
.db
);
192 postfix_snippet("box", "Box::new(expr)", &format
!("Box::new({receiver_text})"))
193 .add_to(acc
, ctx
.db
);
194 postfix_snippet("dbg", "dbg!(expr)", &format
!("dbg!({receiver_text})")).add_to(acc
, ctx
.db
); // fixme
195 postfix_snippet("dbgr", "dbg!(&expr)", &format
!("dbg!(&{receiver_text})")).add_to(acc
, ctx
.db
);
196 postfix_snippet("call", "function(expr)", &format
!("${{1}}({receiver_text})"))
197 .add_to(acc
, ctx
.db
);
199 if let Some(parent
) = dot_receiver
.syntax().parent().and_then(|p
| p
.parent()) {
200 if matches
!(parent
.kind(), STMT_LIST
| EXPR_STMT
) {
201 postfix_snippet("let", "let", &format
!("let $0 = {receiver_text};"))
202 .add_to(acc
, ctx
.db
);
203 postfix_snippet("letm", "let mut", &format
!("let mut $0 = {receiver_text};"))
204 .add_to(acc
, ctx
.db
);
208 if let ast
::Expr
::Literal(literal
) = dot_receiver
.clone() {
209 if let Some(literal_text
) = ast
::String
::cast(literal
.token()) {
210 add_format_like_completions(acc
, ctx
, &dot_receiver
, cap
, &literal_text
);
215 fn get_receiver_text(receiver
: &ast
::Expr
, receiver_is_ambiguous_float_literal
: bool
) -> String
{
216 let text
= if receiver_is_ambiguous_float_literal
{
217 let text
= receiver
.syntax().text();
218 let without_dot
= ..text
.len() - TextSize
::of('
.'
);
219 text
.slice(without_dot
).to_string()
224 // The receiver texts should be interpreted as-is, as they are expected to be
225 // normal Rust expressions. We escape '\' and '$' so they don't get treated as
226 // snippet-specific constructs.
228 // Note that we don't need to escape the other characters that can be escaped,
229 // because they wouldn't be treated as snippet-specific constructs without '$'.
230 text
.replace('
\\'
, "\\\\").replace('$'
, "\\$")
233 fn include_references(initial_element
: &ast
::Expr
) -> (ast
::Expr
, ast
::Expr
) {
234 let mut resulting_element
= initial_element
.clone();
236 while let Some(field_expr
) = resulting_element
.syntax().parent().and_then(ast
::FieldExpr
::cast
)
238 resulting_element
= ast
::Expr
::from(field_expr
);
241 let mut new_element_opt
= initial_element
.clone();
243 if let Some(first_ref_expr
) = resulting_element
.syntax().parent().and_then(ast
::RefExpr
::cast
) {
244 if let Some(expr
) = first_ref_expr
.expr() {
245 resulting_element
= expr
;
248 while let Some(parent_ref_element
) =
249 resulting_element
.syntax().parent().and_then(ast
::RefExpr
::cast
)
251 resulting_element
= ast
::Expr
::from(parent_ref_element
);
253 new_element_opt
= make
::expr_ref(new_element_opt
, false);
256 // If we do not find any ref expressions, restore
257 // all the progress of tree climbing
258 resulting_element
= initial_element
.clone();
261 (resulting_element
, new_element_opt
)
264 fn build_postfix_snippet_builder
<'ctx
>(
265 ctx
: &'ctx CompletionContext
<'_
>,
267 receiver
: &'ctx ast
::Expr
,
268 ) -> Option
<impl Fn(&str, &str, &str) -> Builder
+ 'ctx
> {
269 let receiver_range
= ctx
.sema
.original_range_opt(receiver
.syntax())?
.range
;
270 if ctx
.source_range().end() < receiver_range
.start() {
271 // This shouldn't happen, yet it does. I assume this might be due to an incorrect token mapping.
274 let delete_range
= TextRange
::new(receiver_range
.start(), ctx
.source_range().end());
276 // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that
277 // can't be annotated for the closure, hence fix it by constructing it without the Option first
279 ctx
: &'ctx CompletionContext
<'_
>,
281 delete_range
: TextRange
,
282 ) -> impl Fn(&str, &str, &str) -> Builder
+ 'ctx
{
283 move |label
, detail
, snippet
| {
284 let edit
= TextEdit
::replace(delete_range
, snippet
.to_string());
286 CompletionItem
::new(CompletionItemKind
::Snippet
, ctx
.source_range(), label
);
287 item
.detail(detail
).snippet_edit(cap
, edit
);
288 let postfix_match
= if ctx
.original_token
.text() == label
{
289 cov_mark
::hit
!(postfix_exact_match_is_high_priority
);
290 Some(CompletionRelevancePostfixMatch
::Exact
)
292 cov_mark
::hit
!(postfix_inexact_match_is_low_priority
);
293 Some(CompletionRelevancePostfixMatch
::NonExact
)
295 let relevance
= CompletionRelevance { postfix_match, ..Default::default() }
;
296 item
.set_relevance(relevance
);
300 Some(build(ctx
, cap
, delete_range
))
303 fn add_custom_postfix_completions(
304 acc
: &mut Completions
,
305 ctx
: &CompletionContext
<'_
>,
306 postfix_snippet
: impl Fn(&str, &str, &str) -> Builder
,
309 if ImportScope
::find_insert_use_container(&ctx
.token
.parent()?
, &ctx
.sema
).is_none() {
312 ctx
.config
.postfix_snippets().filter(|(_
, snip
)| snip
.scope
== SnippetScope
::Expr
).for_each(
313 |(trigger
, snippet
)| {
314 let imports
= match snippet
.imports(ctx
) {
315 Some(imports
) => imports
,
318 let body
= snippet
.postfix_snippet(receiver_text
);
320 postfix_snippet(trigger
, snippet
.description
.as_deref().unwrap_or_default(), &body
);
321 builder
.documentation(Documentation
::new(format
!("```rust\n{body}\n```")));
322 for import
in imports
.into_iter() {
323 builder
.add_import(import
);
325 builder
.add_to(acc
, ctx
.db
);
333 use expect_test
::{expect, Expect}
;
336 tests
::{check_edit, check_edit_with_config, completion_list, TEST_CONFIG}
,
337 CompletionConfig
, Snippet
,
340 fn check(ra_fixture
: &str, expect
: Expect
) {
341 let actual
= completion_list(ra_fixture
);
342 expect
.assert_eq(&actual
)
346 fn postfix_completion_works_for_trivial_path_expression() {
355 sn box Box::new(expr)
356 sn call function(expr)
362 sn match match expr {}
367 sn while while expr {}
373 fn postfix_completion_works_for_function_calln() {
376 fn foo(elt: bool) -> bool {
386 sn box Box::new(expr)
387 sn call function(expr)
391 sn match match expr {}
396 sn while while expr {}
402 fn postfix_type_filtering() {
411 sn box Box::new(expr)
412 sn call function(expr)
417 sn match match expr {}
426 fn let_middle_block() {
435 sn box Box::new(expr)
436 sn call function(expr)
442 sn match match expr {}
447 sn while while expr {}
459 let bar = Some(true);
465 let bar = Some(true);
466 if let Some($1) = bar {
498 fn postfix_completion_works_for_ambiguous_float_literal() {
499 check_edit("refm", r
#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#)
503 fn works_in_simple_macro() {
507 macro_rules! m { ($e:expr) => { $e } }
514 macro_rules! m { ($e:expr) => { $e } }
524 fn postfix_completion_for_references() {
525 check_edit("dbg", r
#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
526 check_edit("refm", r
#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
532 let bar = &Some(true);
538 let bar = &Some(true);
539 if let Some($1) = bar {
548 fn postfix_completion_for_unsafe() {
549 check_edit("unsafe", r
#"fn main() { foo.$0 }"#, r#"fn main() { unsafe { foo } }"#);
550 check_edit("unsafe", r
#"fn main() { { foo }.$0 }"#, r#"fn main() { unsafe { foo } }"#);
553 r
#"fn main() { if x { foo }.$0 }"#,
554 r
#"fn main() { unsafe { if x { foo } } }"#,
558 r
#"fn main() { loop { foo }.$0 }"#,
559 r
#"fn main() { unsafe { loop { foo } } }"#,
563 r
#"fn main() { if true {}.$0 }"#,
564 r
#"fn main() { unsafe { if true {} } }"#,
568 r
#"fn main() { while true {}.$0 }"#,
569 r
#"fn main() { unsafe { while true {} } }"#,
573 r
#"fn main() { for i in 0..10 {}.$0 }"#,
574 r
#"fn main() { unsafe { for i in 0..10 {} } }"#,
578 r
#"fn main() { let x = if true {1} else {2}.$0 }"#,
579 r
#"fn main() { let x = unsafe { if true {1} else {2} } }"#,
582 // completion will not be triggered
585 r
#"fn main() { let x = true else {panic!()}.$0}"#,
586 r
#"fn main() { let x = true else {panic!()}.unsafe}"#,
591 fn custom_postfix_completion() {
592 let config
= CompletionConfig
{
593 snippets
: vec
![Snippet
::new(
596 &["ControlFlow::Break(${receiver})".into()],
598 &["core::ops::ControlFlow".into()],
599 crate::SnippetScope
::Expr
,
605 check_edit_with_config(
613 use core::ops::ControlFlow;
615 fn main() { ControlFlow::Break(42) }
619 // The receiver texts should be escaped, see comments in `get_receiver_text()`
622 // Note that the last argument is what *lsp clients would see* rather than
623 // what users would see. Unescaping happens thereafter.
624 check_edit_with_config(
629 fn main() { '\\'.$0 }
632 use core::ops::ControlFlow;
634 fn main() { ControlFlow::Break('\\\\') }
638 check_edit_with_config(
645 true => "${1:placeholder}",
651 use core::ops::ControlFlow;
654 ControlFlow::Break(match true {
655 true => "\${1:placeholder}",
664 fn postfix_completion_for_format_like_strings() {
667 r
#"fn main() { "{some_var:?}".$0 }"#,
668 r#"fn main() { format!("{some_var:?}") }"#,
672 r
#"fn main() { "Panic with {a}".$0 }"#,
673 r#"fn main() { panic!("Panic with {a}") }"#,
677 r
#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#,
678 r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#,
682 r
#"fn main() { "{2+2}".$0 }"#,
683 r#"fn main() { log::error!("{}", 2+2) }"#,
687 r
#"fn main() { "{2+2}".$0 }"#,
688 r#"fn main() { log::trace!("{}", 2+2) }"#,
692 r
#"fn main() { "{2+2}".$0 }"#,
693 r#"fn main() { log::debug!("{}", 2+2) }"#,
695 check_edit("logi", r
#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::info!("{}", 2+2) }"#);
696 check_edit("logw", r
#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::warn!("{}", 2+2) }"#);
699 r
#"fn main() { "{2+2}".$0 }"#,
700 r#"fn main() { log::error!("{}", 2+2) }"#,
705 fn postfix_custom_snippets_completion_for_references() {
706 // https://github.com/rust-lang/rust-analyzer/issues/7929
708 let snippet
= Snippet
::new(
711 &["Ok(${receiver})".into()],
714 crate::SnippetScope
::Expr
,
718 check_edit_with_config(
719 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }
,
721 r
#"fn main() { &&42.o$0 }"#,
722 r
#"fn main() { Ok(&&42) }"#,
725 check_edit_with_config(
726 CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }
,
728 r
#"fn main() { &&42.$0 }"#,
729 r
#"fn main() { Ok(&&42) }"#,
732 check_edit_with_config(
733 CompletionConfig { snippets: vec![snippet], ..TEST_CONFIG }
,
759 fn no_postfix_completions_in_if_block_that_has_an_else() {
763 if true {}.$0 else {}