ide_completion/completions/
fn_param.rs1use hir::HirDisplay;
4use ide_db::FxHashMap;
5use itertools::Either;
6use syntax::{
7 AstNode, Direction, SmolStr, SyntaxKind, TextRange, TextSize, ToSmolStr, algo,
8 ast::{self, HasModuleItem},
9 format_smolstr, match_ast,
10};
11
12use crate::{
13 CompletionContext, CompletionItem, CompletionItemKind, Completions,
14 context::{ParamContext, ParamKind, PatternContext},
15};
16
17pub(crate) fn complete_fn_param(
24 acc: &mut Completions,
25 ctx: &CompletionContext<'_>,
26 pattern_ctx: &PatternContext,
27) -> Option<()> {
28 let (ParamContext { param_list, kind, param, .. }, impl_or_trait) = match pattern_ctx {
29 PatternContext { param_ctx: Some(kind), impl_or_trait, .. } => (kind, impl_or_trait),
30 _ => return None,
31 };
32
33 let qualifier = param_qualifier(param);
34 let comma_wrapper = comma_wrapper(ctx);
35 let mut add_new_item_to_acc = |label: &str| {
36 let label = label.strip_prefix(qualifier.as_str()).unwrap_or(label);
37 let insert = if label.starts_with('#') {
38 label.to_smolstr()
40 } else {
41 format_smolstr!("{qualifier}{label}")
42 };
43 let mk_item = |insert_text: &str, range: TextRange| {
44 let mut item =
45 CompletionItem::new(CompletionItemKind::Binding, range, label, ctx.edition);
46 if insert_text != label {
47 item.insert_text(insert_text);
48 }
49 item
50 };
51 let item = match &comma_wrapper {
52 Some((fmt, range)) => mk_item(&fmt(&insert), *range),
53 None => mk_item(&insert, ctx.source_range()),
54 };
55 item.add_to(acc, ctx.db)
58 };
59
60 match kind {
61 ParamKind::Function(function) => {
62 fill_fn_params(ctx, function, param_list, param, impl_or_trait, add_new_item_to_acc);
63 }
64 ParamKind::Closure(closure) => {
65 if is_simple_param(param) {
66 let stmt_list = closure.syntax().ancestors().find_map(ast::StmtList::cast)?;
67 params_from_stmt_list_scope(ctx, stmt_list, |name, ty| {
68 add_new_item_to_acc(&format_smolstr!(
69 "{}: {ty}",
70 name.display(ctx.db, ctx.edition)
71 ));
72 });
73 }
74 }
75 }
76
77 Some(())
78}
79
80fn fill_fn_params(
81 ctx: &CompletionContext<'_>,
82 function: &ast::Fn,
83 param_list: &ast::ParamList,
84 current_param: &ast::Param,
85 impl_or_trait: &Option<Either<ast::Impl, ast::Trait>>,
86 mut add_new_item_to_acc: impl FnMut(&str),
87) {
88 let mut file_params = FxHashMap::default();
89
90 let mut extract_params = |f: ast::Fn| {
91 f.param_list().into_iter().flat_map(|it| it.params()).for_each(|param| {
92 if let Some(pat) = param.pat() {
93 let whole_param = param.to_smolstr();
94 let binding = pat.to_smolstr();
95 file_params.entry(whole_param).or_insert(binding);
96 }
97 });
98 };
99
100 for node in ctx.token.parent_ancestors() {
101 if !is_simple_param(current_param) {
102 break;
103 }
104 match_ast! {
105 match node {
106 ast::SourceFile(it) => it.items().filter_map(|item| match item {
107 ast::Item::Fn(it) => Some(it),
108 _ => None,
109 }).for_each(&mut extract_params),
110 ast::ItemList(it) => it.items().filter_map(|item| match item {
111 ast::Item::Fn(it) => Some(it),
112 _ => None,
113 }).for_each(&mut extract_params),
114 ast::AssocItemList(it) => it.assoc_items().filter_map(|item| match item {
115 ast::AssocItem::Fn(it) => Some(it),
116 _ => None,
117 }).for_each(&mut extract_params),
118 _ => continue,
119 }
120 };
121 }
122
123 if let Some(stmt_list) = function.syntax().parent().and_then(ast::StmtList::cast)
124 && is_simple_param(current_param)
125 {
126 params_from_stmt_list_scope(ctx, stmt_list, |name, ty| {
127 file_params
128 .entry(format_smolstr!("{}: {ty}", name.display(ctx.db, ctx.edition)))
129 .or_insert(name.display(ctx.db, ctx.edition).to_smolstr());
130 });
131 }
132 remove_duplicated(&mut file_params, param_list.params());
133 let self_completion_items = ["self", "&self", "mut self", "&mut self"];
134 if should_add_self_completions(ctx.token.text_range().start(), param_list, impl_or_trait) {
135 self_completion_items.into_iter().for_each(&mut add_new_item_to_acc);
136 }
137
138 file_params.keys().for_each(|whole_param| add_new_item_to_acc(whole_param));
139}
140
141fn params_from_stmt_list_scope(
142 ctx: &CompletionContext<'_>,
143 stmt_list: ast::StmtList,
144 mut cb: impl FnMut(hir::Name, String),
145) {
146 let syntax_node = match stmt_list.syntax().last_child() {
147 Some(it) => it,
148 None => return,
149 };
150 if let Some(scope) =
151 ctx.sema.scope_at_offset(stmt_list.syntax(), syntax_node.text_range().end())
152 {
153 let module = scope.module().into();
154 scope.process_all_names(&mut |name, def| {
155 if let hir::ScopeDef::Local(local) = def
156 && let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module, true)
157 {
158 cb(name, ty);
159 }
160 });
161 }
162}
163
164fn remove_duplicated(
165 file_params: &mut FxHashMap<SmolStr, SmolStr>,
166 fn_params: ast::AstChildren<ast::Param>,
167) {
168 fn_params.for_each(|param| {
169 let whole_param = param.to_smolstr();
170 file_params.remove(&whole_param);
171
172 match param.pat() {
173 Some(pattern) if param.ty().is_some() => {
177 let binding = pattern.to_smolstr();
178 file_params.retain(|_, v| v != &binding);
179 }
180 _ => (),
181 }
182 })
183}
184
185fn should_add_self_completions(
186 cursor: TextSize,
187 param_list: &ast::ParamList,
188 impl_or_trait: &Option<Either<ast::Impl, ast::Trait>>,
189) -> bool {
190 if impl_or_trait.is_none() || param_list.self_param().is_some() {
191 return false;
192 }
193 match param_list.params().next() {
194 Some(first) => first.pat().is_some_and(|pat| pat.syntax().text_range().contains(cursor)),
195 None => true,
196 }
197}
198
199fn comma_wrapper(ctx: &CompletionContext<'_>) -> Option<(impl Fn(&str) -> SmolStr, TextRange)> {
200 let param =
201 ctx.original_token.parent_ancestors().find(|node| node.kind() == SyntaxKind::PARAM)?;
202
203 let next_token_kind = {
204 let t = param.last_token()?.next_token()?;
205 let t = algo::skip_whitespace_token(t, Direction::Next)?;
206 t.kind()
207 };
208 let prev_token_kind = {
209 let t = param.first_token()?.prev_token()?;
210 let t = algo::skip_whitespace_token(t, Direction::Prev)?;
211 t.kind()
212 };
213
214 let has_trailing_comma =
215 matches!(next_token_kind, SyntaxKind::COMMA | SyntaxKind::R_PAREN | SyntaxKind::PIPE);
216 let trailing = if has_trailing_comma { "" } else { "," };
217
218 let has_leading_comma =
219 matches!(prev_token_kind, SyntaxKind::COMMA | SyntaxKind::L_PAREN | SyntaxKind::PIPE);
220 let leading = if has_leading_comma { "" } else { ", " };
221
222 Some((move |label: &_| format_smolstr!("{leading}{label}{trailing}"), param.text_range()))
223}
224
225fn is_simple_param(param: &ast::Param) -> bool {
226 param
227 .pat()
228 .is_none_or(|pat| matches!(pat, ast::Pat::IdentPat(ident_pat) if ident_pat.pat().is_none()))
229}
230
231fn param_qualifier(param: &ast::Param) -> SmolStr {
232 let mut b = syntax::SmolStrBuilder::new();
233 if let Some(ast::Pat::IdentPat(pat)) = param.pat() {
234 if pat.ref_token().is_some() {
235 b.push_str("ref ");
236 }
237 if pat.mut_token().is_some() {
238 b.push_str("mut ");
239 }
240 }
241 b.finish()
242}