diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index 9dbee16dae6b..0c1c0d32e191 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -6384,6 +6384,7 @@ enum Callee<'db> { BuiltinDeriveImplMethod { method: BuiltinDeriveImplMethod, impl_: BuiltinDeriveImplId }, } +#[derive(Debug)] pub enum CallableKind<'db> { Function(Function), TupleStruct(Struct), @@ -6461,6 +6462,15 @@ impl<'db> Callable<'db> { pub fn ty(&self) -> &Type<'db> { &self.ty } + + /// Returns the generic substitution for this callable, if it is a function. + pub fn substitution(&self) -> Option> { + let fun = self.as_function()?; + match self.ty.ty.kind() { + TyKind::FnDef(_, substs) => GenericSubstitution::new_from_fn(fun, substs, self.ty.env), + _ => None, + } + } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/ide-completion/src/completions/fn_param.rs b/crates/ide-completion/src/completions/fn_param.rs index 96dac66b8a19..6aecb98acaef 100644 --- a/crates/ide-completion/src/completions/fn_param.rs +++ b/crates/ide-completion/src/completions/fn_param.rs @@ -1,7 +1,9 @@ //! See [`complete_fn_param`]. +use std::fmt::Write; + use hir::HirDisplay; -use ide_db::FxHashMap; +use ide_db::{FxHashMap, FxHashSet, active_parameter::callable_for_token}; use itertools::Either; use syntax::{ AstNode, Direction, SmolStr, SyntaxKind, TextRange, TextSize, ToSmolStr, algo, @@ -214,3 +216,115 @@ fn is_simple_param(param: &ast::Param) -> bool { .pat() .is_none_or(|pat| matches!(pat, ast::Pat::IdentPat(ident_pat) if ident_pat.pat().is_none())) } + +/// When completing inside a closure's param list that is an argument to a call, +/// suggest parameter lists based on the expected `Fn*` trait signature. +/// +/// E.g. for `foo(|$0|)` where `foo` expects `impl Fn(usize, String) -> bool`, +/// suggest `a, b` and `a: usize, b: String`. +pub(crate) fn complete_closure_within_param( + acc: &mut Completions, + ctx: &CompletionContext<'_>, +) -> Option<()> { + // Walk up: PARAM_LIST -> CLOSURE_EXPR -> ARG_LIST -> CALL_EXPR/METHOD_CALL_EXPR + let closure_param_list = ctx.token.parent().filter(|n| n.kind() == SyntaxKind::PARAM_LIST)?; + let closure = closure_param_list.parent().filter(|n| n.kind() == SyntaxKind::CLOSURE_EXPR)?; + let arg_list = closure.parent().filter(|n| n.kind() == SyntaxKind::ARG_LIST)?; + _ = arg_list + .parent() + .filter(|n| matches!(n.kind(), SyntaxKind::CALL_EXPR | SyntaxKind::METHOD_CALL_EXPR))?; + + let (callable, index) = callable_for_token(&ctx.sema, closure.first_token()?)?; + let index = index?; + + // We must look at the *generic* function definition's param type, not the + // instantiated one from the callable. When the closure is just `|`, inference + // yields `{unknown}` for the instantiated type. The generic param type + // (e.g. `impl Fn(usize) -> u32`) lives in the function's param env, so + // `as_callable` can resolve the Fn trait bound from there. + let hir::CallableKind::Function(fun) = callable.kind() else { + return None; + }; + // Use the absolute index (which includes self) to index into assoc_fn_params, + // so that method calls with self don't cause an off-by-one. + let abs_index = callable.params().into_iter().nth(index)?.index(); + let generic_param_ty = fun.assoc_fn_params(ctx.db).into_iter().nth(abs_index)?.ty().clone(); + + if !generic_param_ty.impls_fnonce(ctx.db) { + return None; + } + + let fn_callable = generic_param_ty.as_callable(ctx.db)?; + let closure_params = fn_callable.params(); + + // Build a set of generic param names that have already been resolved + // (via turbofish or inference from other arguments). If a substituted + // type is concrete (not unknown), the corresponding param is resolved. + let resolved_param_names: FxHashSet<_> = callable + .substitution() + .map(|subst| { + subst + .types(ctx.db) + .into_iter() + .filter(|(_, ty)| !ty.contains_unknown()) + .map(|(name, _)| name) + .collect() + }) + .unwrap_or_default(); + + let module = ctx.scope.module().into(); + let source_range = ctx.source_range(); + let cap = ctx.config.snippet_cap; + + // For each closure param, include a type annotation only if the type + // contains generic type parameters (meaning inference alone can't determine it) + // AND the instantiated type hasn't already resolved them. + let mut label = String::from("|"); + let mut snippet = String::new(); + let mut plain = String::new(); + let mut tab_stop = 1; + + for (i, p) in closure_params.iter().enumerate() { + let sep = if i > 0 { ", " } else { "" }; + let ty = p.ty(); + // A type annotation is needed only if the type contains generic params + // that haven't been resolved by the calling context. + let needs_annotation = ty.generic_params(ctx.db).iter().any(|gp| { + let name = gp.name(ctx.db); + !resolved_param_names.contains(name.symbol()) + }); + + if needs_annotation { + if let Ok(ty_str) = ty.display_source_code(ctx.db, module, true) { + write!(label, "{sep}_: {ty_str}").unwrap(); + write!(snippet, "{sep}${{{tab_stop}:_}}: ${{{}:{ty_str}}}", tab_stop + 1).unwrap(); + write!(plain, "{sep}_: {ty_str}").unwrap(); + tab_stop += 2; + } else { + write!(label, "{sep}_").unwrap(); + write!(snippet, "{sep}${{{tab_stop}:_}}").unwrap(); + write!(plain, "{sep}_").unwrap(); + tab_stop += 1; + } + } else { + write!(label, "{sep}_").unwrap(); + write!(snippet, "{sep}${{{tab_stop}:_}}").unwrap(); + write!(plain, "{sep}_").unwrap(); + tab_stop += 1; + } + } + + label.push_str("| "); + snippet.push_str("| $0"); + plain.push_str("| "); + + let mut item = + CompletionItem::new(CompletionItemKind::Binding, source_range, &label, ctx.edition); + match cap { + Some(cap) => item.insert_snippet(cap, &snippet), + None => item.insert_text(&plain), + }; + item.add_to(acc, ctx.db); + + Some(()) +} diff --git a/crates/ide-completion/src/lib.rs b/crates/ide-completion/src/lib.rs index 69ca2af7721b..d8302862f10f 100644 --- a/crates/ide-completion/src/lib.rs +++ b/crates/ide-completion/src/lib.rs @@ -28,8 +28,8 @@ use syntax::ast::make; use crate::{ completions::Completions, context::{ - CompletionAnalysis, CompletionContext, NameRefContext, NameRefKind, PathCompletionCtx, - PathKind, + CompletionAnalysis, CompletionContext, NameContext, NameKind, NameRefContext, NameRefKind, + ParamContext, ParamKind, PathCompletionCtx, PathKind, PatternContext, }, }; @@ -212,6 +212,41 @@ pub fn completions( return Some(completions.into()); } + if trigger_character == Some('|') { + // 2026-02-13T11:30:41.704583Z ERROR CompletionAnalysis: Name(NameContext { + // name: None, + // kind: IdentPat(PatternContext { + // refutability: Irrefutable, + // param_ctx: Some(ParamContext { + // param_list: ParamList { + // syntax: PARAM_LIST@7497..7498 }, + // param: Param { + // syntax: + // PARAM@7498..7516 + // }, + // kind: Closure(ClosureExpr { + // syntax: CLOSURE_EXPR@7497..7498 + // } + // ) + // }), + // has_type_ascription: false, should_suggest_name: true, after_if_expr: false, parent_pat: + // None, ref_token: None, mut_token: None, record_pat: None, impl_or_trait: None, + // missing_variants: [] }) }) + + if let CompletionAnalysis::Name(NameContext { + kind: + NameKind::IdentPat(PatternContext { + param_ctx: Some(ParamContext { kind: ParamKind::Closure(_), .. }), + .. + }), + .. + }) = analysis + { + completions::fn_param::complete_closure_within_param(&mut completions, ctx); + } + return Some(completions.into()); + } + tracing::error!("CompletionAnalysis: {:?}", &analysis); // when the user types a bare `_` (that is it does not belong to an identifier) // the user might just wanted to type a `_` for type inference or pattern discarding // so try to suppress completions in those cases diff --git a/crates/ide-completion/src/tests.rs b/crates/ide-completion/src/tests.rs index cb1adfcfb65e..8901da68a188 100644 --- a/crates/ide-completion/src/tests.rs +++ b/crates/ide-completion/src/tests.rs @@ -241,11 +241,33 @@ pub(crate) fn check_edit_with_config( what: &str, ra_fixture_before: &str, ra_fixture_after: &str, +) { + check_edit_impl(config, what, ra_fixture_before, ra_fixture_after, None) +} + +#[track_caller] +pub(crate) fn check_edit_with_trigger_character( + what: &str, + #[rust_analyzer::rust_fixture] ra_fixture_before: &str, + #[rust_analyzer::rust_fixture] ra_fixture_after: &str, + trigger_character: Option, +) { + check_edit_impl(TEST_CONFIG, what, ra_fixture_before, ra_fixture_after, trigger_character) +} + +#[track_caller] +fn check_edit_impl( + config: CompletionConfig<'_>, + what: &str, + ra_fixture_before: &str, + ra_fixture_after: &str, + trigger_character: Option, ) { let ra_fixture_after = trim_indent(ra_fixture_after); let (db, position) = position(ra_fixture_before); - let completions: Vec = - hir::attach_db(&db, || crate::completions(&db, &config, position, None).unwrap()); + let completions: Vec = hir::attach_db(&db, || { + crate::completions(&db, &config, position, trigger_character).unwrap() + }); let Some((completion,)) = completions.iter().filter(|it| it.lookup() == what).collect_tuple() else { panic!("can't find {what:?} completion in {completions:#?}") diff --git a/crates/ide-completion/src/tests/fn_param.rs b/crates/ide-completion/src/tests/fn_param.rs index d6d73da3f140..df58547a7a85 100644 --- a/crates/ide-completion/src/tests/fn_param.rs +++ b/crates/ide-completion/src/tests/fn_param.rs @@ -1,6 +1,6 @@ use expect_test::expect; -use crate::tests::{check, check_with_trigger_character}; +use crate::tests::{check, check_edit_with_trigger_character, check_with_trigger_character}; #[test] fn only_param() { @@ -358,3 +358,330 @@ fn g(foo: (), #[baz = "qux"] mut ba$0) "##]], ) } + +#[test] +fn closure_within_param_fn_single() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(u32)) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn(u32)) {} +fn main() { + foo(|${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_fn_multiple() { + check_edit_with_trigger_character( + "|_, _| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(u32, i64) -> bool) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn(u32, i64) -> bool) {} +fn main() { + foo(|${1:_}, ${2:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_fn_mut() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(f: impl FnMut(u32)) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl FnMut(u32)) {} +fn main() { + foo(|${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_fn_once() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(f: impl FnOnce(String) -> String) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl FnOnce(String) -> String) {} +fn main() { + foo(|${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_second_arg() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(x: u32, f: impl Fn(bool) -> bool) {} +fn main() { + foo(0, |$0); +} +"#, + r#" +fn foo(x: u32, f: impl Fn(bool) -> bool) {} +fn main() { + foo(0, |${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_method_call() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +struct S; +impl S { + fn foo(&self, f: impl FnOnce(u32) -> bool) {} +} +fn main() { + S.foo(|$0); +} +"#, + r#" +struct S; +impl S { + fn foo(&self, f: impl FnOnce(u32) -> bool) {} +} +fn main() { + S.foo(|${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_method_second_closure_arg() { + check_edit_with_trigger_character( + "|_, _| ", + r#" +//- minicore: fn +struct S; +impl S { + fn foo(&self, a: impl Fn(u32), b: impl Fn(bool, i64)) {} +} +fn main() { + S.foo(|_| {}, |$0); +} +"#, + r#" +struct S; +impl S { + fn foo(&self, a: impl Fn(u32), b: impl Fn(bool, i64)) {} +} +fn main() { + S.foo(|_| {}, |${1:_}, ${2:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_where_clause() { + check_edit_with_trigger_character( + "|_, _| ", + r#" +//- minicore: fn +fn foo(f: F) where F: Fn(u32, bool) -> bool {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: F) where F: Fn(u32, bool) -> bool {} +fn main() { + foo(|${1:_}, ${2:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_single() { + check_edit_with_trigger_character( + "|_: T| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(T) -> T) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn(T) -> T) {} +fn main() { + foo(|${1:_}: ${2:T}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_mixed() { + check_edit_with_trigger_character( + "|_, _: T| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(u32, T) -> bool) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn(u32, T) -> bool) {} +fn main() { + foo(|${1:_}, ${2:_}: ${3:T}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_multiple() { + check_edit_with_trigger_character( + "|_: T, _: U| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(T, U)) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn(T, U)) {} +fn main() { + foo(|${1:_}: ${2:T}, ${3:_}: ${4:U}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_no_args() { + check_edit_with_trigger_character( + "|| ", + r#" +//- minicore: fn +fn foo(f: impl Fn()) {} +fn main() { + foo(|$0); +} +"#, + r#" +fn foo(f: impl Fn()) {} +fn main() { + foo(|| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_resolved_by_turbofish() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(f: impl Fn(T)) {} +fn main() { + foo::(|$0); +} +"#, + r#" +fn foo(f: impl Fn(T)) {} +fn main() { + foo::(|${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_resolved_by_inference() { + check_edit_with_trigger_character( + "|_| ", + r#" +//- minicore: fn +fn foo(x: T, f: impl Fn(T)) {} +fn main() { + foo(42u32, |$0); +} +"#, + r#" +fn foo(x: T, f: impl Fn(T)) {} +fn main() { + foo(42u32, |${1:_}| $0); +} +"#, + Some('|'), + ); +} + +#[test] +fn closure_within_param_generic_partially_resolved() { + check_edit_with_trigger_character( + "|_, _: U| ", + r#" +//- minicore: fn +fn foo(x: T, f: impl Fn(T, U)) {} +fn main() { + foo(42u32, |$0); +} +"#, + r#" +fn foo(x: T, f: impl Fn(T, U)) {} +fn main() { + foo(42u32, |${1:_}, ${2:_}: ${3:U}| $0); +} +"#, + Some('|'), + ); +} diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index ad07da77597d..cdb5f1a02ee0 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -1145,7 +1145,7 @@ pub(crate) fn handle_completion( None => return Ok(None), Some(items) => items, }; - + tracing::error!("{:?}", &items); let items = to_proto::completion_items( &snap.config, &completion_config.fields_to_resolve, diff --git a/crates/rust-analyzer/src/lsp/capabilities.rs b/crates/rust-analyzer/src/lsp/capabilities.rs index d6a694be9121..aa10d7822db3 100644 --- a/crates/rust-analyzer/src/lsp/capabilities.rs +++ b/crates/rust-analyzer/src/lsp/capabilities.rs @@ -55,6 +55,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities { ".".to_owned(), "'".to_owned(), "(".to_owned(), + "|".to_owned(), ]), all_commit_characters: None, completion_item: config.caps().completion_item(),