//! Renderer for function calls. use hir::{db::HirDatabase, AsAssocItem, HirDisplay}; use ide_db::{SnippetCap, SymbolKind}; use itertools::Itertools; use stdx::{format_to, to_lower_snake_case}; use syntax::{format_smolstr, AstNode, Edition, SmolStr, ToSmolStr}; use crate::{ context::{ CompleteSemicolon, CompletionContext, DotAccess, DotAccessKind, PathCompletionCtx, PathKind, }, item::{ Builder, CompletionItem, CompletionItemKind, CompletionRelevance, CompletionRelevanceFn, CompletionRelevanceReturnType, CompletionRelevanceTraitInfo, }, render::{ compute_exact_name_match, compute_ref_match, compute_type_match, match_types, RenderContext, }, CallableSnippets, }; #[derive(Debug)] enum FuncKind<'ctx> { Function(&'ctx PathCompletionCtx), Method(&'ctx DotAccess, Option), } pub(crate) fn render_fn( ctx: RenderContext<'_>, path_ctx: &PathCompletionCtx, local_name: Option, func: hir::Function, ) -> Builder { let _p = tracing::info_span!("render_fn").entered(); render(ctx, local_name, func, FuncKind::Function(path_ctx)) } pub(crate) fn render_method( ctx: RenderContext<'_>, dot_access: &DotAccess, receiver: Option, local_name: Option, func: hir::Function, ) -> Builder { let _p = tracing::info_span!("render_method").entered(); render(ctx, local_name, func, FuncKind::Method(dot_access, receiver)) } fn render( ctx @ RenderContext { completion, .. }: RenderContext<'_>, local_name: Option, func: hir::Function, func_kind: FuncKind<'_>, ) -> Builder { let db = completion.db; let name = local_name.unwrap_or_else(|| func.name(db)); let (call, escaped_call) = match &func_kind { FuncKind::Method(_, Some(receiver)) => ( format_smolstr!("{}.{}", receiver, name.unescaped().display(ctx.db())), format_smolstr!("{}.{}", receiver, name.display(ctx.db(), completion.edition)), ), _ => ( name.unescaped().display(db).to_smolstr(), name.display(db, completion.edition).to_smolstr(), ), }; let has_self_param = func.self_param(db).is_some(); let mut item = CompletionItem::new( CompletionItemKind::SymbolKind(if has_self_param { SymbolKind::Method } else { SymbolKind::Function }), ctx.source_range(), call.clone(), completion.edition, ); let ret_type = func.ret_type(db); let assoc_item = func.as_assoc_item(db); let trait_info = assoc_item.and_then(|trait_| trait_.container_or_implemented_trait(db)).map(|trait_| { CompletionRelevanceTraitInfo { notable_trait: completion.is_doc_notable_trait(trait_), is_op_method: completion.is_ops_trait(trait_), } }); let (has_dot_receiver, has_call_parens, cap) = match func_kind { FuncKind::Function(&PathCompletionCtx { kind: PathKind::Expr { .. }, has_call_parens, .. }) => (false, has_call_parens, ctx.completion.config.snippet_cap), FuncKind::Method(&DotAccess { kind: DotAccessKind::Method { has_parens }, .. }, _) => { (true, has_parens, ctx.completion.config.snippet_cap) } FuncKind::Method(DotAccess { kind: DotAccessKind::Field { .. }, .. }, _) => { (true, false, ctx.completion.config.snippet_cap) } _ => (false, false, None), }; let complete_call_parens = cap .filter(|_| !has_call_parens) .and_then(|cap| Some((cap, params(ctx.completion, func, &func_kind, has_dot_receiver)?))); let function = assoc_item .and_then(|assoc_item| assoc_item.implementing_ty(db)) .map(|self_type| compute_return_type_match(db, &ctx, self_type, &ret_type)) .map(|return_type| CompletionRelevanceFn { has_params: has_self_param || func.num_params(db) > 0, has_self_param, return_type, }); item.set_relevance(CompletionRelevance { type_match: if has_call_parens || complete_call_parens.is_some() { compute_type_match(completion, &ret_type) } else { compute_type_match(completion, &func.ty(db)) }, exact_name_match: compute_exact_name_match(completion, &call), function, trait_: trait_info, ..ctx.completion_relevance() }); match func_kind { FuncKind::Function(path_ctx) => { super::path_ref_match(completion, path_ctx, &ret_type, &mut item); } FuncKind::Method(DotAccess { receiver: Some(receiver), .. }, _) => { if let Some(original_expr) = completion.sema.original_ast_node(receiver.clone()) { if let Some(ref_match) = compute_ref_match(completion, &ret_type) { item.ref_match(ref_match, original_expr.syntax().text_range().start()); } } } _ => (), } let detail = if ctx.completion.config.full_function_signatures { detail_full(db, func, ctx.completion.edition) } else { detail(db, func, ctx.completion.edition) }; item.set_documentation(ctx.docs(func)) .set_deprecated(ctx.is_deprecated(func) || ctx.is_deprecated_assoc_item(func)) .detail(detail) .lookup_by(name.unescaped().display(db).to_smolstr()); if let Some((cap, (self_param, params))) = complete_call_parens { add_call_parens( &mut item, completion, cap, call, escaped_call, self_param, params, &ret_type, ); } match ctx.import_to_add { Some(import_to_add) => { item.add_import(import_to_add); } None => { if let Some(actm) = assoc_item { if let Some(trt) = actm.container_or_implemented_trait(db) { item.trait_name( trt.name(db).display_no_db(ctx.completion.edition).to_smolstr(), ); } } } } item.doc_aliases(ctx.doc_aliases); item } fn compute_return_type_match( db: &dyn HirDatabase, ctx: &RenderContext<'_>, self_type: hir::Type, ret_type: &hir::Type, ) -> CompletionRelevanceReturnType { if match_types(ctx.completion, &self_type, ret_type).is_some() { // fn([..]) -> Self CompletionRelevanceReturnType::DirectConstructor } else if ret_type .type_arguments() .any(|ret_type_arg| match_types(ctx.completion, &self_type, &ret_type_arg).is_some()) { // fn([..]) -> Result OR Wrapped CompletionRelevanceReturnType::Constructor } else if ret_type .as_adt() .map(|adt| adt.name(db).as_str().ends_with("Builder")) .unwrap_or(false) { // fn([..]) -> [..]Builder CompletionRelevanceReturnType::Builder } else { CompletionRelevanceReturnType::Other } } pub(super) fn add_call_parens<'b>( builder: &'b mut Builder, ctx: &CompletionContext<'_>, cap: SnippetCap, name: SmolStr, escaped_name: SmolStr, self_param: Option, params: Vec, ret_type: &hir::Type, ) -> &'b mut Builder { cov_mark::hit!(inserts_parens_for_function_calls); let (mut snippet, label_suffix) = if self_param.is_none() && params.is_empty() { (format!("{escaped_name}()$0"), "()") } else { builder.trigger_call_info(); let snippet = if let Some(CallableSnippets::FillArguments) = ctx.config.callable { let offset = if self_param.is_some() { 2 } else { 1 }; let function_params_snippet = params.iter().enumerate().format_with(", ", |(index, param), f| { match param.name(ctx.db) { Some(n) => { let smol_str = n.display_no_db(ctx.edition).to_smolstr(); let text = smol_str.as_str().trim_start_matches('_'); let ref_ = ref_of_param(ctx, text, param.ty()); f(&format_args!("${{{}:{ref_}{text}}}", index + offset)) } None => { let name = match param.ty().as_adt() { None => "_".to_owned(), Some(adt) => to_lower_snake_case(adt.name(ctx.db).as_str()), }; f(&format_args!("${{{}:{name}}}", index + offset)) } } }); match self_param { Some(self_param) => { format!( "{}(${{1:{}}}{}{})$0", escaped_name, self_param.display(ctx.db, ctx.edition), if params.is_empty() { "" } else { ", " }, function_params_snippet ) } None => { format!("{escaped_name}({function_params_snippet})$0") } } } else { cov_mark::hit!(suppress_arg_snippets); format!("{escaped_name}($0)") }; (snippet, "(…)") }; if ret_type.is_unit() { match ctx.complete_semicolon { CompleteSemicolon::DoNotComplete => {} CompleteSemicolon::CompleteSemi | CompleteSemicolon::CompleteComma => { cov_mark::hit!(complete_semicolon); let ch = if matches!(ctx.complete_semicolon, CompleteSemicolon::CompleteComma) { ',' } else { ';' }; if snippet.ends_with("$0") { snippet.insert(snippet.len() - "$0".len(), ch); } else { snippet.push(ch); } } } } builder.label(SmolStr::from_iter([&name, label_suffix])).insert_snippet(cap, snippet) } fn ref_of_param(ctx: &CompletionContext<'_>, arg: &str, ty: &hir::Type) -> &'static str { if let Some(derefed_ty) = ty.remove_ref() { for (name, local) in ctx.locals.iter().sorted_by_key(|&(k, _)| k.clone()) { if name.as_str() == arg { return if local.ty(ctx.db) == derefed_ty { if ty.is_mutable_reference() { "&mut " } else { "&" } } else { "" }; } } } "" } fn detail(db: &dyn HirDatabase, func: hir::Function, edition: Edition) -> String { let mut ret_ty = func.ret_type(db); let mut detail = String::new(); if func.is_const(db) { format_to!(detail, "const "); } if func.is_async(db) { format_to!(detail, "async "); if let Some(async_ret) = func.async_ret_type(db) { ret_ty = async_ret; } } if func.is_unsafe_to_call(db) { format_to!(detail, "unsafe "); } format_to!(detail, "fn({})", params_display(db, func, edition)); if !ret_ty.is_unit() { format_to!(detail, " -> {}", ret_ty.display(db, edition)); } detail } fn detail_full(db: &dyn HirDatabase, func: hir::Function, edition: Edition) -> String { let signature = format!("{}", func.display(db, edition)); let mut detail = String::with_capacity(signature.len()); for segment in signature.split_whitespace() { if !detail.is_empty() { detail.push(' '); } detail.push_str(segment); } detail } fn params_display(db: &dyn HirDatabase, func: hir::Function, edition: Edition) -> String { if let Some(self_param) = func.self_param(db) { let assoc_fn_params = func.assoc_fn_params(db); let params = assoc_fn_params .iter() .skip(1) // skip the self param because we are manually handling that .map(|p| p.ty().display(db, edition)); format!( "{}{}", self_param.display(db, edition), params.format_with("", |display, f| { f(&", ")?; f(&display) }) ) } else { let assoc_fn_params = func.assoc_fn_params(db); assoc_fn_params.iter().map(|p| p.ty().display(db, edition)).join(", ") } } fn params( ctx: &CompletionContext<'_>, func: hir::Function, func_kind: &FuncKind<'_>, has_dot_receiver: bool, ) -> Option<(Option, Vec)> { ctx.config.callable.as_ref()?; // Don't add parentheses if the expected type is a function reference with the same signature. if let Some(expected) = ctx.expected_type.as_ref().filter(|e| e.is_fn()) { if let Some(expected) = expected.as_callable(ctx.db) { if let Some(completed) = func.ty(ctx.db).as_callable(ctx.db) { if expected.sig() == completed.sig() { cov_mark::hit!(no_call_parens_if_fn_ptr_needed); return None; } } } } let self_param = if has_dot_receiver || matches!(func_kind, FuncKind::Method(_, Some(_))) { None } else { func.self_param(ctx.db) }; Some((self_param, func.params_without_self(ctx.db))) } #[cfg(test)] mod tests { use crate::{ tests::{check_edit, check_edit_with_config, TEST_CONFIG}, CallableSnippets, CompletionConfig, }; #[test] fn inserts_parens_for_function_calls() { cov_mark::check!(inserts_parens_for_function_calls); check_edit( "no_args", r#" fn no_args() {} fn main() { no_$0 } "#, r#" fn no_args() {} fn main() { no_args();$0 } "#, ); check_edit( "with_args", r#" fn with_args(x: i32, y: String) {} fn main() { with_$0 } "#, r#" fn with_args(x: i32, y: String) {} fn main() { with_args(${1:x}, ${2:y});$0 } "#, ); check_edit( "foo", r#" struct S; impl S { fn foo(&self) -> i32 { 0 } } fn bar(s: &S) { s.f$0 } "#, r#" struct S; impl S { fn foo(&self) -> i32 { 0 } } fn bar(s: &S) { s.foo()$0 } "#, ); check_edit( "foo", r#" struct S {} impl S { fn foo(&self, x: i32) {} } fn bar(s: &S) { s.f$0 } "#, r#" struct S {} impl S { fn foo(&self, x: i32) {} } fn bar(s: &S) { s.foo(${1:x});$0 } "#, ); check_edit( "foo", r#" struct S {} impl S { fn foo(&self, x: i32) { $0 } } "#, r#" struct S {} impl S { fn foo(&self, x: i32) { self.foo(${1:x});$0 } } "#, ); } #[test] fn parens_for_method_call_as_assoc_fn() { check_edit( "foo", r#" struct S; impl S { fn foo(&self) {} } fn main() { S::f$0 } "#, r#" struct S; impl S { fn foo(&self) {} } fn main() { S::foo(${1:&self});$0 } "#, ); } #[test] fn suppress_arg_snippets() { cov_mark::check!(suppress_arg_snippets); check_edit_with_config( CompletionConfig { callable: Some(CallableSnippets::AddParentheses), ..TEST_CONFIG }, "with_args", r#" fn with_args(x: i32, y: String) {} fn main() { with_$0 } "#, r#" fn with_args(x: i32, y: String) {} fn main() { with_args($0); } "#, ); } #[test] fn strips_underscores_from_args() { check_edit( "foo", r#" fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {} fn main() { f$0 } "#, r#" fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {} fn main() { foo(${1:foo}, ${2:bar}, ${3:ho_ge_});$0 } "#, ); } #[test] fn insert_ref_when_matching_local_in_scope() { check_edit( "ref_arg", r#" struct Foo {} fn ref_arg(x: &Foo) {} fn main() { let x = Foo {}; ref_ar$0 } "#, r#" struct Foo {} fn ref_arg(x: &Foo) {} fn main() { let x = Foo {}; ref_arg(${1:&x});$0 } "#, ); } #[test] fn insert_mut_ref_when_matching_local_in_scope() { check_edit( "ref_arg", r#" struct Foo {} fn ref_arg(x: &mut Foo) {} fn main() { let x = Foo {}; ref_ar$0 } "#, r#" struct Foo {} fn ref_arg(x: &mut Foo) {} fn main() { let x = Foo {}; ref_arg(${1:&mut x});$0 } "#, ); } #[test] fn insert_ref_when_matching_local_in_scope_for_method() { check_edit( "apply_foo", r#" struct Foo {} struct Bar {} impl Bar { fn apply_foo(&self, x: &Foo) {} } fn main() { let x = Foo {}; let y = Bar {}; y.$0 } "#, r#" struct Foo {} struct Bar {} impl Bar { fn apply_foo(&self, x: &Foo) {} } fn main() { let x = Foo {}; let y = Bar {}; y.apply_foo(${1:&x});$0 } "#, ); } #[test] fn trim_mut_keyword_in_func_completion() { check_edit( "take_mutably", r#" fn take_mutably(mut x: &i32) {} fn main() { take_m$0 } "#, r#" fn take_mutably(mut x: &i32) {} fn main() { take_mutably(${1:x});$0 } "#, ); } #[test] fn complete_pattern_args_with_type_name_if_adt() { check_edit( "qux", r#" struct Foo { bar: i32 } fn qux(Foo { bar }: Foo) { println!("{}", bar); } fn main() { qu$0 } "#, r#" struct Foo { bar: i32 } fn qux(Foo { bar }: Foo) { println!("{}", bar); } fn main() { qux(${1:foo});$0 } "#, ); } #[test] fn complete_fn_param() { // has mut kw check_edit( "mut bar: u32", r#" fn f(foo: (), mut bar: u32) {} fn g(foo: (), mut ba$0) "#, r#" fn f(foo: (), mut bar: u32) {} fn g(foo: (), mut bar: u32) "#, ); // has type param check_edit( "mut bar: u32", r#" fn g(foo: (), mut ba$0: u32) fn f(foo: (), mut bar: u32) {} "#, r#" fn g(foo: (), mut bar: u32) fn f(foo: (), mut bar: u32) {} "#, ); } #[test] fn complete_fn_mut_param_add_comma() { // add leading and trailing comma check_edit( ", mut bar: u32,", r#" fn f(foo: (), mut bar: u32) {} fn g(foo: ()mut ba$0 baz: ()) "#, r#" fn f(foo: (), mut bar: u32) {} fn g(foo: (), mut bar: u32, baz: ()) "#, ); } #[test] fn complete_fn_mut_param_has_attribute() { check_edit( r#"#[baz = "qux"] mut bar: u32"#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: (), mut ba$0) "#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: (), #[baz = "qux"] mut bar: u32) "#, ); check_edit( r#"#[baz = "qux"] mut bar: u32"#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: (), #[baz = "qux"] mut ba$0) "#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: (), #[baz = "qux"] mut bar: u32) "#, ); check_edit( r#", #[baz = "qux"] mut bar: u32"#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: ()#[baz = "qux"] mut ba$0) "#, r#" fn f(foo: (), #[baz = "qux"] mut bar: u32) {} fn g(foo: (), #[baz = "qux"] mut bar: u32) "#, ); } #[test] fn complete_semicolon_for_unit() { cov_mark::check!(complete_semicolon); check_edit( r#"foo"#, r#" fn foo() {} fn bar() { foo$0 } "#, r#" fn foo() {} fn bar() { foo();$0 } "#, ); check_edit( r#"foo"#, r#" fn foo(a: i32) {} fn bar() { foo$0 } "#, r#" fn foo(a: i32) {} fn bar() { foo(${1:a});$0 } "#, ); check_edit( r#"foo"#, r#" fn foo(a: i32) {} fn bar() { foo$0; } "#, r#" fn foo(a: i32) {} fn bar() { foo(${1:a})$0; } "#, ); check_edit_with_config( CompletionConfig { add_semicolon_to_unit: false, ..TEST_CONFIG }, r#"foo"#, r#" fn foo(a: i32) {} fn bar() { foo$0 } "#, r#" fn foo(a: i32) {} fn bar() { foo(${1:a})$0 } "#, ); } #[test] fn complete_comma_for_unit_match_arm() { cov_mark::check!(complete_semicolon); check_edit( r#"foo"#, r#" fn foo() {} fn bar() { match Some(false) { v => fo$0 } } "#, r#" fn foo() {} fn bar() { match Some(false) { v => foo(),$0 } } "#, ); check_edit( r#"foo"#, r#" fn foo() {} fn bar() { match Some(false) { v => fo$0, } } "#, r#" fn foo() {} fn bar() { match Some(false) { v => foo()$0, } } "#, ); } #[test] fn no_semicolon_in_closure_ret() { check_edit( r#"foo"#, r#" fn foo() {} fn baz(_: impl FnOnce()) {} fn bar() { baz(|| fo$0); } "#, r#" fn foo() {} fn baz(_: impl FnOnce()) {} fn bar() { baz(|| foo()$0); } "#, ); } }