//! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`. mod format_like; use base_db::SourceDatabase; use hir::{ItemInNs, Semantics}; use ide_db::{ RootDatabase, SnippetCap, documentation::{Documentation, HasDocs}, imports::insert_use::ImportScope, syntax_helpers::suggest_name::NameGenerator, text_edit::TextEdit, ty_filter::TryEnum, }; use itertools::Itertools; use stdx::never; use syntax::{ SmolStr, SyntaxKind::{BLOCK_EXPR, EXPR_STMT, STMT_LIST}, T, TextRange, TextSize, ToSmolStr, ast::{self, AstNode, AstToken}, format_smolstr, match_ast, }; use crate::{ CompletionItem, CompletionItemKind, CompletionRelevance, Completions, SnippetScope, completions::postfix::format_like::add_format_like_completions, context::{BreakableKind, CompletionContext, DotAccess, DotAccessKind}, item::{Builder, CompletionRelevancePostfixMatch}, }; pub(crate) fn complete_postfix( acc: &mut Completions, ctx: &CompletionContext<'_>, dot_access: &DotAccess<'_>, ) { if !ctx.config.enable_postfix_completions { return; } let (dot_receiver, receiver_ty, receiver_is_ambiguous_float_literal) = match dot_access { DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. } => ( it, &ty.original, match *kind { DotAccessKind::Field { receiver_is_ambiguous_float_literal } => { receiver_is_ambiguous_float_literal } DotAccessKind::Method => false, }, ), _ => return, }; let expr_ctx = &dot_access.ctx; let receiver_text = get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal); let cap = match ctx.config.snippet_cap { Some(it) => it, None => return, }; let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) { Some(it) => it, None => return, }; let cfg = ctx.config.find_path_config(ctx.is_nightly); if let Some(drop_trait) = ctx.famous_defs().core_ops_Drop() && receiver_ty.impls_trait(ctx.db, drop_trait, &[]) && let Some(drop_fn) = ctx.famous_defs().core_mem_drop() && let Some(path) = ctx.module.find_path(ctx.db, ItemInNs::Values(drop_fn.into()), cfg) { cov_mark::hit!(postfix_drop_completion); let mut item = postfix_snippet( "drop", "fn drop(&mut self)", &format!("{path}($0{receiver_text})", path = path.display(ctx.db, ctx.edition)), ); item.set_documentation(drop_fn.docs(ctx.db)); item.add_to(acc, ctx.db); } postfix_snippet("ref", "&expr", &format!("&{receiver_text}")).add_to(acc, ctx.db); postfix_snippet("refm", "&mut expr", &format!("&mut {receiver_text}")).add_to(acc, ctx.db); postfix_snippet("deref", "*expr", &format!("*{receiver_text}")).add_to(acc, ctx.db); // The rest of the postfix completions create an expression that moves an argument, // so it's better to consider references now to avoid breaking the compilation let (dot_receiver_including_refs, prefix) = include_references(dot_receiver); let mut receiver_text = receiver_text; receiver_text.insert_str(0, &prefix); let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, &dot_receiver_including_refs) { Some(it) => it, None => return, }; if !ctx.config.snippets.is_empty() { add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text); } postfix_snippet("box", "Box::new(expr)", &format!("Box::new({receiver_text})")) .add_to(acc, ctx.db); postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({receiver_text})")).add_to(acc, ctx.db); // fixme postfix_snippet("dbgr", "dbg!(&expr)", &format!("dbg!(&{receiver_text})")).add_to(acc, ctx.db); postfix_snippet("call", "function(expr)", &format!("${{1}}({receiver_text})")) .add_to(acc, ctx.db); let try_enum = TryEnum::from_ty(&ctx.sema, receiver_ty); let mut is_in_cond = false; if let Some(parent) = dot_receiver_including_refs.syntax().parent() && let Some(second_ancestor) = parent.parent() { if let Some(parent_expr) = ast::Expr::cast(parent) { is_in_cond = is_in_condition(&parent_expr); } let placeholder = suggest_receiver_name(dot_receiver, "0", &ctx.sema); match &try_enum { Some(try_enum) if is_in_cond => match try_enum { TryEnum::Result => { postfix_snippet( "let", "let Ok(_)", &format!("let Ok({placeholder}) = {receiver_text}"), ) .add_to(acc, ctx.db); postfix_snippet( "letm", "let Ok(mut _)", &format!("let Ok(mut {placeholder}) = {receiver_text}"), ) .add_to(acc, ctx.db); } TryEnum::Option => { postfix_snippet( "let", "let Some(_)", &format!("let Some({placeholder}) = {receiver_text}"), ) .add_to(acc, ctx.db); postfix_snippet( "letm", "let Some(mut _)", &format!("let Some(mut {placeholder}) = {receiver_text}"), ) .add_to(acc, ctx.db); } }, _ if is_in_cond => { postfix_snippet("let", "let", &format!("let $1 = {receiver_text}")) .add_to(acc, ctx.db); } _ if matches!(second_ancestor.kind(), STMT_LIST | EXPR_STMT | BLOCK_EXPR) => { postfix_snippet("let", "let", &format!("let $0 = {receiver_text};")) .add_to(acc, ctx.db); postfix_snippet("letm", "let mut", &format!("let mut $0 = {receiver_text};")) .add_to(acc, ctx.db); } _ if ast::MatchArm::can_cast(second_ancestor.kind()) => { postfix_snippet( "let", "let", &format!("{{\n let $1 = {receiver_text};\n $0\n}}"), ) .add_to(acc, ctx.db); postfix_snippet( "letm", "let mut", &format!("{{\n let mut $1 = {receiver_text};\n $0\n}}"), ) .add_to(acc, ctx.db); } _ => (), } } if !is_in_cond { match try_enum { Some(try_enum) => match try_enum { TryEnum::Result => { postfix_snippet( "match", "match expr {}", &format!("match {receiver_text} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}"), ) .add_to(acc, ctx.db); } TryEnum::Option => { postfix_snippet( "match", "match expr {}", &format!( "match {receiver_text} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}" ), ) .add_to(acc, ctx.db); } }, None => { postfix_snippet( "match", "match expr {}", &format!("match {receiver_text} {{\n ${{1:_}} => {{$0}},\n}}"), ) .add_to(acc, ctx.db); } } if let Some(try_enum) = &try_enum { let placeholder = suggest_receiver_name(dot_receiver, "1", &ctx.sema); match try_enum { TryEnum::Result => { postfix_snippet( "ifl", "if let Ok {}", &format!("if let Ok({placeholder}) = {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); postfix_snippet( "lete", "let Ok else {}", &format!( "let Ok({placeholder}) = {receiver_text} else {{\n $2\n}};\n$0" ), ) .add_to(acc, ctx.db); postfix_snippet( "while", "while let Ok {}", &format!("while let Ok({placeholder}) = {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); } TryEnum::Option => { postfix_snippet( "ifl", "if let Some {}", &format!("if let Some({placeholder}) = {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); postfix_snippet( "lete", "let Some else {}", &format!( "let Some({placeholder}) = {receiver_text} else {{\n $2\n}};\n$0" ), ) .add_to(acc, ctx.db); postfix_snippet( "while", "while let Some {}", &format!("while let Some({placeholder}) = {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); } } } else if receiver_ty.is_bool() || receiver_ty.is_unknown() { postfix_snippet("if", "if expr {}", &format!("if {receiver_text} {{\n $0\n}}")) .add_to(acc, ctx.db); postfix_snippet( "while", "while expr {}", &format!("while {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); } else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator() && receiver_ty.impls_trait(ctx.db, trait_, &[]) { postfix_snippet( "for", "for ele in expr {}", &format!("for ele in {receiver_text} {{\n $0\n}}"), ) .add_to(acc, ctx.db); } } if receiver_ty.is_bool() || receiver_ty.is_unknown() { postfix_snippet("not", "!expr", &format!("!{receiver_text}")).add_to(acc, ctx.db); } let block_should_be_wrapped = if let ast::Expr::BlockExpr(block) = dot_receiver { block.modifier().is_some() || !block.is_standalone() } else { true }; { let (open_brace, close_brace) = if block_should_be_wrapped { ("{ ", " }") } else { ("", "") }; // FIXME: Why add parentheses let (open_paren, close_paren) = if is_in_cond { ("(", ")") } else { ("", "") }; let unsafe_completion_string = format!("{open_paren}unsafe {open_brace}{receiver_text}{close_brace}{close_paren}"); postfix_snippet("unsafe", "unsafe {}", &unsafe_completion_string).add_to(acc, ctx.db); let const_completion_string = format!("{open_paren}const {open_brace}{receiver_text}{close_brace}{close_paren}"); postfix_snippet("const", "const {}", &const_completion_string).add_to(acc, ctx.db); } if let ast::Expr::Literal(literal) = dot_receiver_including_refs.clone() && let Some(literal_text) = ast::String::cast(literal.token()) { add_format_like_completions(acc, ctx, &dot_receiver_including_refs, cap, &literal_text); } postfix_snippet( "return", "return expr", &format!( "return {receiver_text}{semi}", semi = if expr_ctx.in_block_expr { ";" } else { "" } ), ) .add_to(acc, ctx.db); if let Some(BreakableKind::Block | BreakableKind::Loop) = expr_ctx.in_breakable { postfix_snippet( "break", "break expr", &format!( "break {receiver_text}{semi}", semi = if expr_ctx.in_block_expr { ";" } else { "" } ), ) .add_to(acc, ctx.db); } } fn suggest_receiver_name( receiver: &ast::Expr, n: &str, sema: &Semantics<'_, RootDatabase>, ) -> SmolStr { let placeholder = |name| format_smolstr!("${{{n}:{name}}}"); match receiver { ast::Expr::PathExpr(path) => { if let Some(name) = path.path().and_then(|it| it.as_single_name_ref()) { return placeholder(name.text().as_str()); } } ast::Expr::RefExpr(it) => { if let Some(receiver) = it.expr() { return suggest_receiver_name(&receiver, n, sema); } } _ => {} } let name = NameGenerator::new_with_names([].into_iter()).try_for_variable(receiver, sema); match name { Some(name) => placeholder(&name), None => format_smolstr!("${n}"), } } fn get_receiver_text( sema: &Semantics<'_, RootDatabase>, receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool, ) -> String { // Do not just call `receiver.to_string()`, as that will mess up whitespaces inside macros. let Some(mut range) = sema.original_range_opt(receiver.syntax()) else { return receiver.to_string(); }; if receiver_is_ambiguous_float_literal { range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.')) } let file_text = sema.db.file_text(range.file_id.file_id(sema.db)); let mut text = file_text.text(sema.db)[range.range].to_owned(); // The receiver texts should be interpreted as-is, as they are expected to be // normal Rust expressions. escape_snippet_bits(&mut text); text } /// Escapes `\` and `$` so that they don't get interpreted as snippet-specific constructs. /// /// Note that we don't need to escape the other characters that can be escaped, /// because they wouldn't be treated as snippet-specific constructs without '$'. fn escape_snippet_bits(text: &mut String) { stdx::replace(text, '\\', "\\\\"); stdx::replace(text, '$', "\\$"); } fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) { let mut resulting_element = initial_element.clone(); while let Some(field_expr) = resulting_element.syntax().parent().and_then(ast::FieldExpr::cast) { resulting_element = ast::Expr::from(field_expr); } let mut prefix = String::new(); let mut found_ref_or_deref = false; while let Some(parent_deref_element) = resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast) { if parent_deref_element.op_kind() != Some(ast::UnaryOp::Deref) { break; } found_ref_or_deref = true; resulting_element = ast::Expr::from(parent_deref_element); prefix.insert(0, '*'); } while let Some(parent_ref_element) = resulting_element.syntax().parent().and_then(ast::RefExpr::cast) { found_ref_or_deref = true; let last_child_or_token = parent_ref_element.syntax().last_child_or_token(); prefix.insert_str( 0, parent_ref_element .syntax() .children_with_tokens() .filter(|it| Some(it) != last_child_or_token.as_ref()) .format("") .to_smolstr() .as_str(), ); resulting_element = ast::Expr::from(parent_ref_element); } if !found_ref_or_deref { // If we do not find any ref/deref expressions, restore // all the progress of tree climbing prefix.clear(); resulting_element = initial_element.clone(); } (resulting_element, prefix) } fn build_postfix_snippet_builder<'ctx>( ctx: &'ctx CompletionContext<'_>, cap: SnippetCap, receiver: &'ctx ast::Expr, ) -> Option Builder + 'ctx> { let receiver_range = ctx.sema.original_range_opt(receiver.syntax())?.range; if ctx.source_range().end() < receiver_range.start() { // This shouldn't happen, yet it does. I assume this might be due to an incorrect token // mapping. never!(); return None; } let delete_range = TextRange::new(receiver_range.start(), ctx.source_range().end()); // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that // can't be annotated for the closure, hence fix it by constructing it without the Option first fn build<'ctx>( ctx: &'ctx CompletionContext<'_>, cap: SnippetCap, delete_range: TextRange, ) -> impl Fn(&str, &str, &str) -> Builder + 'ctx { move |label, detail, snippet| { let edit = TextEdit::replace(delete_range, snippet.to_owned()); let mut item = CompletionItem::new( CompletionItemKind::Snippet, ctx.source_range(), label, ctx.edition, ); item.detail(detail).snippet_edit(cap, edit); let postfix_match = if ctx.original_token.text() == label { cov_mark::hit!(postfix_exact_match_is_high_priority); Some(CompletionRelevancePostfixMatch::Exact) } else { cov_mark::hit!(postfix_inexact_match_is_low_priority); Some(CompletionRelevancePostfixMatch::NonExact) }; let relevance = CompletionRelevance { postfix_match, ..Default::default() }; item.set_relevance(relevance); item } } Some(build(ctx, cap, delete_range)) } fn add_custom_postfix_completions( acc: &mut Completions, ctx: &CompletionContext<'_>, postfix_snippet: impl Fn(&str, &str, &str) -> Builder, receiver_text: &str, ) -> Option<()> { ImportScope::find_insert_use_container(&ctx.token.parent()?, &ctx.sema)?; ctx.config.postfix_snippets().filter(|(_, snip)| snip.scope == SnippetScope::Expr).for_each( |(trigger, snippet)| { let imports = match snippet.imports(ctx) { Some(imports) => imports, None => return, }; let body = snippet.postfix_snippet(receiver_text); let mut builder = postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), &body); builder.documentation(Documentation::new_owned(format!("```rust\n{body}\n```"))); for import in imports.into_iter() { builder.add_import(import); } builder.add_to(acc, ctx.db); }, ); None } pub(crate) fn is_in_condition(it: &ast::Expr) -> bool { it.syntax() .parent() .and_then(|parent| { Some(match_ast! { match parent { ast::IfExpr(expr) => expr.condition()? == *it, ast::WhileExpr(expr) => expr.condition()? == *it, ast::MatchGuard(guard) => guard.condition()? == *it, ast::BinExpr(bin_expr) => (bin_expr.op_token()?.kind() == T![&&]) .then(|| is_in_condition(&bin_expr.into()))?, ast::Expr(expr) => (expr.syntax().text_range().start() == it.syntax().text_range().start()) .then(|| is_in_condition(&expr))?, _ => return None, } }) }) .unwrap_or(false) } #[cfg(test)] mod tests { use expect_test::expect; use crate::{ CompletionConfig, Snippet, tests::{TEST_CONFIG, check, check_edit, check_edit_with_config}, }; #[test] fn postfix_completion_works_for_trivial_path_expression() { check( r#" fn main() { let bar = true; bar.$0 } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn let let sn letm let mut sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); } #[test] fn postfix_completion_works_for_function_calln() { check( r#" fn foo(elt: bool) -> bool { !elt } fn main() { let bar = true; foo(bar.$0) } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); } #[test] fn postfix_completion_works_in_if_condition() { check( r#" fn foo(cond: bool) { if cond.$0 } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn let let sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} "#]], ); } #[test] fn postfix_type_filtering() { check( r#" fn main() { let bar: u8 = 12; bar.$0 } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn let let sn letm let mut sn match match expr {} sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} "#]], ) } #[test] fn let_middle_block() { check( r#" fn main() { baz.l$0 res } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn let let sn letm let mut sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); check( r#" fn main() { &baz.l$0 res } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn let let sn letm let mut sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); } #[test] fn let_tail_block() { check( r#" fn main() { baz.l$0 } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn let let sn letm let mut sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); check( r#" fn main() { &baz.l$0 } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn if if expr {} sn let let sn letm let mut sn match match expr {} sn not !expr sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} sn while while expr {} "#]], ); } #[test] fn option_iflet() { check_edit( "ifl", r#" //- minicore: option fn main() { let bar = Some(true); bar.$0 } "#, r#" fn main() { let bar = Some(true); if let Some(${1:bar}) = bar { $0 } } "#, ); } #[test] fn option_iflet_cond() { check( r#" //- minicore: option fn main() { let bar = Some(true); if bar.$0 } "#, expect![[r#" me and(…) fn(self, Option) -> Option me as_ref() const fn(&self) -> Option<&T> me ok_or(…) const fn(self, E) -> Result me unwrap() const fn(self) -> T me unwrap_or(…) fn(self, T) -> T sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn let let Some(_) sn letm let Some(mut _) sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} "#]], ); check_edit( "let", r#" //- minicore: option fn main() { let bar = Some(true); if bar.$0 } "#, r#" fn main() { let bar = Some(true); if let Some(${0:bar}) = bar } "#, ); check_edit( "let", r#" //- minicore: option fn main() { let bar = Some(true); if true && bar.$0 } "#, r#" fn main() { let bar = Some(true); if true && let Some(${0:bar}) = bar } "#, ); check_edit( "let", r#" //- minicore: option fn main() { let bar = Some(true); if true && true && bar.$0 } "#, r#" fn main() { let bar = Some(true); if true && true && let Some(${0:bar}) = bar } "#, ); } #[test] fn iflet_fallback_cond() { check_edit( "let", r#" fn main() { let bar = 2; if bar.$0 } "#, r#" fn main() { let bar = 2; if let $1 = bar } "#, ); } #[test] fn match_arm_let_block() { check( r#" fn main() { match 2 { bar => bar.$0 } } "#, expect![[r#" sn box Box::new(expr) sn call function(expr) sn const const {} sn dbg dbg!(expr) sn dbgr dbg!(&expr) sn deref *expr sn let let sn letm let mut sn match match expr {} sn ref &expr sn refm &mut expr sn return return expr sn unsafe unsafe {} "#]], ); check_edit( "let", r#" fn main() { match 2 { bar => bar.$0 } } "#, r#" fn main() { match 2 { bar => { let $1 = bar; $0 } } } "#, ); } #[test] fn option_letelse() { check_edit( "lete", r#" //- minicore: option fn main() { let bar = Some(true); bar.$0 } "#, r#" fn main() { let bar = Some(true); let Some(${1:bar}) = bar else { $2 }; $0 } "#, ); } #[test] fn result_match() { check_edit( "match", r#" //- minicore: result fn main() { let bar = Ok(true); bar.$0 } "#, r#" fn main() { let bar = Ok(true); match bar { Ok(${1:_}) => {$2}, Err(${3:_}) => {$0}, } } "#, ); } #[test] fn postfix_completion_works_for_ambiguous_float_literal() { check_edit("refm", r#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#) } #[test] fn works_in_simple_macro() { check_edit( "dbg", r#" macro_rules! m { ($e:expr) => { $e } } fn main() { let bar: u8 = 12; m!(bar.d$0) } "#, r#" macro_rules! m { ($e:expr) => { $e } } fn main() { let bar: u8 = 12; m!(dbg!(bar)) } "#, ); } #[test] fn postfix_completion_for_references() { check_edit("dbg", r#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#); check_edit("refm", r#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#); check_edit( "ifl", r#" //- minicore: option fn main() { let bar = &Some(true); bar.$0 } "#, r#" fn main() { let bar = &Some(true); if let Some(${1:bar}) = bar { $0 } } "#, ) } #[test] fn postfix_completion_for_unsafe() { postfix_completion_for_block("unsafe"); } #[test] fn postfix_completion_for_const() { postfix_completion_for_block("const"); } fn postfix_completion_for_block(kind: &str) { check_edit(kind, r#"fn main() { foo.$0 }"#, &format!("fn main() {{ {kind} {{ foo }} }}")); check_edit( kind, r#"fn main() { { foo }.$0 }"#, &format!("fn main() {{ {kind} {{ foo }} }}"), ); check_edit( kind, r#"fn main() { if x { foo }.$0 }"#, &format!("fn main() {{ {kind} {{ if x {{ foo }} }} }}"), ); check_edit( kind, r#"fn main() { loop { foo }.$0 }"#, &format!("fn main() {{ {kind} {{ loop {{ foo }} }} }}"), ); check_edit( kind, r#"fn main() { if true {}.$0 }"#, &format!("fn main() {{ {kind} {{ if true {{}} }} }}"), ); check_edit( kind, r#"fn main() { while true {}.$0 }"#, &format!("fn main() {{ {kind} {{ while true {{}} }} }}"), ); check_edit( kind, r#"fn main() { for i in 0..10 {}.$0 }"#, &format!("fn main() {{ {kind} {{ for i in 0..10 {{}} }} }}"), ); check_edit( kind, r#"fn main() { let x = if true {1} else {2}.$0 }"#, &format!("fn main() {{ let x = {kind} {{ if true {{1}} else {{2}} }} }}"), ); if kind == "const" { check_edit( kind, r#"fn main() { unsafe {1}.$0 }"#, &format!("fn main() {{ {kind} {{ unsafe {{1}} }} }}"), ); } else { check_edit( kind, r#"fn main() { const {1}.$0 }"#, &format!("fn main() {{ {kind} {{ const {{1}} }} }}"), ); } // completion will not be triggered check_edit( kind, r#"fn main() { let x = true else {panic!()}.$0}"#, &format!("fn main() {{ let x = true else {{panic!()}}.{kind} $0}}"), ); } #[test] fn custom_postfix_completion() { let config = CompletionConfig { snippets: vec![ Snippet::new( &[], &["break".into()], &["ControlFlow::Break(${receiver})".into()], "", &["core::ops::ControlFlow".into()], crate::SnippetScope::Expr, ) .unwrap(), ], ..TEST_CONFIG }; check_edit_with_config( config.clone(), "break", r#" //- minicore: try fn main() { 42.$0 } "#, r#" use core::ops::ControlFlow; fn main() { ControlFlow::Break(42) } "#, ); // The receiver texts should be escaped, see comments in `get_receiver_text()` // for detail. // // Note that the last argument is what *lsp clients would see* rather than // what users would see. Unescaping happens thereafter. check_edit_with_config( config.clone(), "break", r#" //- minicore: try fn main() { '\\'.$0 } "#, r#" use core::ops::ControlFlow; fn main() { ControlFlow::Break('\\\\') } "#, ); check_edit_with_config( config, "break", r#" //- minicore: try fn main() { match true { true => "${1:placeholder}", false => "\$", }.$0 } "#, r#" use core::ops::ControlFlow; fn main() { ControlFlow::Break(match true { true => "\${1:placeholder}", false => "\\\$", }) } "#, ); } #[test] fn postfix_completion_for_format_like_strings() { check_edit( "format", r#"fn main() { "{some_var:?}".$0 }"#, r#"fn main() { format!("{some_var:?}") }"#, ); check_edit( "panic", r#"fn main() { "Panic with {a}".$0 }"#, r#"fn main() { panic!("Panic with {a}") }"#, ); check_edit( "println", r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#, r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }) }"#, ); check_edit( "loge", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::error!("{}", 2+2) }"#, ); check_edit( "logt", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::trace!("{}", 2+2) }"#, ); check_edit( "logd", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::debug!("{}", 2+2) }"#, ); check_edit("logi", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::info!("{}", 2+2) }"#); check_edit("logw", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::warn!("{}", 2+2) }"#); check_edit( "loge", r#"fn main() { "{2+2}".$0 }"#, r#"fn main() { log::error!("{}", 2+2) }"#, ); } #[test] fn postfix_custom_snippets_completion_for_references() { // https://github.com/rust-lang/rust-analyzer/issues/7929 let snippet = Snippet::new( &[], &["ok".into()], &["Ok(${receiver})".into()], "", &[], crate::SnippetScope::Expr, ) .unwrap(); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "ok", r#"fn main() { &&42.o$0 }"#, r#"fn main() { Ok(&&42) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "ok", r#"fn main() { &&42.$0 }"#, r#"fn main() { Ok(&&42) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "ok", r#"fn main() { &raw mut 42.$0 }"#, r#"fn main() { Ok(&raw mut 42) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "ok", r#"fn main() { &raw const 42.$0 }"#, r#"fn main() { Ok(&raw const 42) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet], ..TEST_CONFIG }, "ok", r#" struct A { a: i32, } fn main() { let a = A {a :1}; &a.a.$0 } "#, r#" struct A { a: i32, } fn main() { let a = A {a :1}; Ok(&a.a) } "#, ); } #[test] fn postfix_custom_snippets_completion_for_reference_expr() { // https://github.com/rust-lang/rust-analyzer/issues/21035 let snippet = Snippet::new( &[], &["group".into()], &["(${receiver})".into()], "", &[], crate::SnippetScope::Expr, ) .unwrap(); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "group", r#"fn main() { &[1, 2, 3].g$0 }"#, r#"fn main() { (&[1, 2, 3]) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "group", r#"fn main() { &&foo(a, b, 1+1).$0 }"#, r#"fn main() { (&&foo(a, b, 1+1)) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "group", r#"fn main() { &mut Foo { a: 1, b: 2, c: 3 }.$0 }"#, r#"fn main() { (&mut Foo { a: 1, b: 2, c: 3 }) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "group", r#"fn main() { &raw mut Foo::new().$0 }"#, r#"fn main() { (&raw mut Foo::new()) }"#, ); check_edit_with_config( CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG }, "group", r#"fn main() { &raw const Foo::bar::SOME_CONST.$0 }"#, r#"fn main() { (&raw const Foo::bar::SOME_CONST) }"#, ); } #[test] fn no_postfix_completions_in_if_block_that_has_an_else() { check( r#" fn test() { if true {}.$0 else {} } "#, expect![[r#""#]], ); } #[test] fn mut_ref_consuming() { check_edit( "call", r#" fn main() { let mut x = &mut 2; &mut x.$0; } "#, r#" fn main() { let mut x = &mut 2; ${1}(&mut x); } "#, ); } #[test] fn deref_consuming() { check_edit( "call", r#" fn main() { let mut x = &mut 2; &mut *x.$0; } "#, r#" fn main() { let mut x = &mut 2; ${1}(&mut *x); } "#, ); } #[test] fn inside_macro() { check_edit( "box", r#" macro_rules! assert { ( $it:expr $(,)? ) => { $it }; } fn foo() { let a = true; assert!(if a == false { true } else { false }.$0); } "#, r#" macro_rules! assert { ( $it:expr $(,)? ) => { $it }; } fn foo() { let a = true; assert!(Box::new(if a == false { true } else { false })); } "#, ); } }