Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6384,6 +6384,7 @@ enum Callee<'db> {
BuiltinDeriveImplMethod { method: BuiltinDeriveImplMethod, impl_: BuiltinDeriveImplId },
}

#[derive(Debug)]
pub enum CallableKind<'db> {
Function(Function),
TupleStruct(Struct),
Expand Down Expand Up @@ -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<GenericSubstitution<'db>> {
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)]
Expand Down
116 changes: 115 additions & 1 deletion crates/ide-completion/src/completions/fn_param.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(())
}
39 changes: 37 additions & 2 deletions crates/ide-completion/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand Down Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions crates/ide-completion/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>,
) {
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<char>,
) {
let ra_fixture_after = trim_indent(ra_fixture_after);
let (db, position) = position(ra_fixture_before);
let completions: Vec<CompletionItem> =
hir::attach_db(&db, || crate::completions(&db, &config, position, None).unwrap());
let completions: Vec<CompletionItem> = 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:#?}")
Expand Down
Loading