diff options
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide-completion/src/completions/fn_param.rs')
-rw-r--r-- | src/tools/rust-analyzer/crates/ide-completion/src/completions/fn_param.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/completions/fn_param.rs b/src/tools/rust-analyzer/crates/ide-completion/src/completions/fn_param.rs new file mode 100644 index 000000000..f0ecc595a --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide-completion/src/completions/fn_param.rs @@ -0,0 +1,196 @@ +//! See [`complete_fn_param`]. + +use hir::HirDisplay; +use ide_db::FxHashMap; +use syntax::{ + algo, + ast::{self, HasModuleItem}, + match_ast, AstNode, Direction, SyntaxKind, TextRange, TextSize, +}; + +use crate::{ + context::{ParamContext, ParamKind, PatternContext}, + CompletionContext, CompletionItem, CompletionItemKind, Completions, +}; + +// FIXME: Make this a submodule of [`pattern`] +/// Complete repeated parameters, both name and type. For example, if all +/// functions in a file have a `spam: &mut Spam` parameter, a completion with +/// `spam: &mut Spam` insert text/label will be suggested. +/// +/// Also complete parameters for closure or local functions from the surrounding defined locals. +pub(crate) fn complete_fn_param( + acc: &mut Completions, + ctx: &CompletionContext<'_>, + pattern_ctx: &PatternContext, +) -> Option<()> { + let (ParamContext { param_list, kind, .. }, impl_) = match pattern_ctx { + PatternContext { param_ctx: Some(kind), impl_, .. } => (kind, impl_), + _ => return None, + }; + + let comma_wrapper = comma_wrapper(ctx); + let mut add_new_item_to_acc = |label: &str| { + let mk_item = |label: &str, range: TextRange| { + CompletionItem::new(CompletionItemKind::Binding, range, label) + }; + let item = match &comma_wrapper { + Some((fmt, range)) => mk_item(&fmt(label), *range), + None => mk_item(label, ctx.source_range()), + }; + // Completion lookup is omitted intentionally here. + // See the full discussion: https://github.com/rust-lang/rust-analyzer/issues/12073 + item.add_to(acc) + }; + + match kind { + ParamKind::Function(function) => { + fill_fn_params(ctx, function, param_list, impl_, add_new_item_to_acc); + } + ParamKind::Closure(closure) => { + let stmt_list = closure.syntax().ancestors().find_map(ast::StmtList::cast)?; + params_from_stmt_list_scope(ctx, stmt_list, |name, ty| { + add_new_item_to_acc(&format!("{name}: {ty}")); + }); + } + } + + Some(()) +} + +fn fill_fn_params( + ctx: &CompletionContext<'_>, + function: &ast::Fn, + param_list: &ast::ParamList, + impl_: &Option<ast::Impl>, + mut add_new_item_to_acc: impl FnMut(&str), +) { + let mut file_params = FxHashMap::default(); + + let mut extract_params = |f: ast::Fn| { + f.param_list().into_iter().flat_map(|it| it.params()).for_each(|param| { + if let Some(pat) = param.pat() { + // FIXME: We should be able to turn these into SmolStr without having to allocate a String + let whole_param = param.syntax().text().to_string(); + let binding = pat.syntax().text().to_string(); + file_params.entry(whole_param).or_insert(binding); + } + }); + }; + + for node in ctx.token.parent_ancestors() { + match_ast! { + match node { + ast::SourceFile(it) => it.items().filter_map(|item| match item { + ast::Item::Fn(it) => Some(it), + _ => None, + }).for_each(&mut extract_params), + ast::ItemList(it) => it.items().filter_map(|item| match item { + ast::Item::Fn(it) => Some(it), + _ => None, + }).for_each(&mut extract_params), + ast::AssocItemList(it) => it.assoc_items().filter_map(|item| match item { + ast::AssocItem::Fn(it) => Some(it), + _ => None, + }).for_each(&mut extract_params), + _ => continue, + } + }; + } + + if let Some(stmt_list) = function.syntax().parent().and_then(ast::StmtList::cast) { + params_from_stmt_list_scope(ctx, stmt_list, |name, ty| { + file_params.entry(format!("{name}: {ty}")).or_insert(name.to_string()); + }); + } + remove_duplicated(&mut file_params, param_list.params()); + let self_completion_items = ["self", "&self", "mut self", "&mut self"]; + if should_add_self_completions(ctx.token.text_range().start(), param_list, impl_) { + self_completion_items.into_iter().for_each(|self_item| add_new_item_to_acc(self_item)); + } + + file_params.keys().for_each(|whole_param| add_new_item_to_acc(whole_param)); +} + +fn params_from_stmt_list_scope( + ctx: &CompletionContext<'_>, + stmt_list: ast::StmtList, + mut cb: impl FnMut(hir::Name, String), +) { + let syntax_node = match stmt_list.syntax().last_child() { + Some(it) => it, + None => return, + }; + if let Some(scope) = + ctx.sema.scope_at_offset(stmt_list.syntax(), syntax_node.text_range().end()) + { + let module = scope.module().into(); + scope.process_all_names(&mut |name, def| { + if let hir::ScopeDef::Local(local) = def { + if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module) { + cb(name, ty); + } + } + }); + } +} + +fn remove_duplicated( + file_params: &mut FxHashMap<String, String>, + fn_params: ast::AstChildren<ast::Param>, +) { + fn_params.for_each(|param| { + let whole_param = param.syntax().text().to_string(); + file_params.remove(&whole_param); + + match param.pat() { + // remove suggestions for patterns that already exist + // if the type is missing we are checking the current param to be completed + // in which case this would find itself removing the suggestions due to itself + Some(pattern) if param.ty().is_some() => { + let binding = pattern.syntax().text().to_string(); + file_params.retain(|_, v| v != &binding); + } + _ => (), + } + }) +} + +fn should_add_self_completions( + cursor: TextSize, + param_list: &ast::ParamList, + impl_: &Option<ast::Impl>, +) -> bool { + if impl_.is_none() || param_list.self_param().is_some() { + return false; + } + match param_list.params().next() { + Some(first) => first.pat().map_or(false, |pat| pat.syntax().text_range().contains(cursor)), + None => true, + } +} + +fn comma_wrapper(ctx: &CompletionContext<'_>) -> Option<(impl Fn(&str) -> String, TextRange)> { + let param = ctx.token.parent_ancestors().find(|node| node.kind() == SyntaxKind::PARAM)?; + + let next_token_kind = { + let t = param.last_token()?.next_token()?; + let t = algo::skip_whitespace_token(t, Direction::Next)?; + t.kind() + }; + let prev_token_kind = { + let t = param.first_token()?.prev_token()?; + let t = algo::skip_whitespace_token(t, Direction::Prev)?; + t.kind() + }; + + let has_trailing_comma = + matches!(next_token_kind, SyntaxKind::COMMA | SyntaxKind::R_PAREN | SyntaxKind::PIPE); + let trailing = if has_trailing_comma { "" } else { "," }; + + let has_leading_comma = + matches!(prev_token_kind, SyntaxKind::COMMA | SyntaxKind::L_PAREN | SyntaxKind::PIPE); + let leading = if has_leading_comma { "" } else { ", " }; + + Some((move |label: &_| (format!("{}{}{}", leading, label, trailing)), param.text_range())) +} |