]> git.lizzy.rs Git - rust.git/blob - crates/ide_completion/src/completions/fn_param.rs
5bfd2bc1df1b11295d53196e75c49e896ad663aa
[rust.git] / crates / ide_completion / src / completions / fn_param.rs
1 //! See [`complete_fn_param`].
2
3 use hir::HirDisplay;
4 use rustc_hash::FxHashMap;
5 use syntax::{
6     algo,
7     ast::{self, HasModuleItem},
8     match_ast, AstNode, Direction, SyntaxKind, TextRange,
9 };
10
11 use crate::{
12     context::{ParamKind, PatternContext},
13     CompletionContext, CompletionItem, CompletionItemKind, Completions,
14 };
15
16 /// Complete repeated parameters, both name and type. For example, if all
17 /// functions in a file have a `spam: &mut Spam` parameter, a completion with
18 /// `spam: &mut Spam` insert text/label and `spam` lookup string will be
19 /// suggested.
20 ///
21 /// Also complete parameters for closure or local functions from the surrounding defined locals.
22 pub(crate) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> {
23     let (param_list, _, param_kind) = match &ctx.pattern_ctx {
24         Some(PatternContext { param_ctx: Some(kind), .. }) => kind,
25         _ => return None,
26     };
27
28     let comma_wrapper = comma_wrapper(ctx);
29     let mut add_new_item_to_acc = |label: &str, lookup: String| {
30         let mk_item = |label: &str, range: TextRange| {
31             CompletionItem::new(CompletionItemKind::Binding, range, label)
32         };
33         let mut item = match &comma_wrapper {
34             Some((fmt, range)) => mk_item(&fmt(label), *range),
35             None => mk_item(label, ctx.source_range()),
36         };
37         item.lookup_by(lookup);
38         item.add_to(acc)
39     };
40
41     match param_kind {
42         ParamKind::Function(function) => {
43             fill_fn_params(ctx, function, param_list, add_new_item_to_acc);
44         }
45         ParamKind::Closure(closure) => {
46             let stmt_list = closure.syntax().ancestors().find_map(ast::StmtList::cast)?;
47             params_from_stmt_list_scope(ctx, stmt_list, |name, ty| {
48                 add_new_item_to_acc(&format!("{name}: {ty}"), name.to_string());
49             });
50         }
51     }
52
53     Some(())
54 }
55
56 fn fill_fn_params(
57     ctx: &CompletionContext,
58     function: &ast::Fn,
59     param_list: &ast::ParamList,
60     mut add_new_item_to_acc: impl FnMut(&str, String),
61 ) {
62     let mut file_params = FxHashMap::default();
63
64     let mut extract_params = |f: ast::Fn| {
65         f.param_list().into_iter().flat_map(|it| it.params()).for_each(|param| {
66             if let Some(pat) = param.pat() {
67                 // FIXME: We should be able to turn these into SmolStr without having to allocate a String
68                 let whole_param = param.syntax().text().to_string();
69                 let binding = pat.syntax().text().to_string();
70                 file_params.entry(whole_param).or_insert(binding);
71             }
72         });
73     };
74
75     for node in ctx.token.ancestors() {
76         match_ast! {
77             match node {
78                 ast::SourceFile(it) => it.items().filter_map(|item| match item {
79                     ast::Item::Fn(it) => Some(it),
80                     _ => None,
81                 }).for_each(&mut extract_params),
82                 ast::ItemList(it) => it.items().filter_map(|item| match item {
83                     ast::Item::Fn(it) => Some(it),
84                     _ => None,
85                 }).for_each(&mut extract_params),
86                 ast::AssocItemList(it) => it.assoc_items().filter_map(|item| match item {
87                     ast::AssocItem::Fn(it) => Some(it),
88                     _ => None,
89                 }).for_each(&mut extract_params),
90                 _ => continue,
91             }
92         };
93     }
94
95     if let Some(stmt_list) = function.syntax().parent().and_then(ast::StmtList::cast) {
96         params_from_stmt_list_scope(ctx, stmt_list, |name, ty| {
97             file_params.entry(format!("{name}: {ty}")).or_insert(name.to_string());
98         });
99     }
100
101     remove_duplicated(&mut file_params, param_list.params());
102     let self_completion_items = ["self", "&self", "mut self", "&mut self"];
103     if should_add_self_completions(ctx, param_list) {
104         self_completion_items
105             .into_iter()
106             .for_each(|self_item| add_new_item_to_acc(self_item, self_item.to_string()));
107     }
108
109     file_params
110         .into_iter()
111         .for_each(|(whole_param, binding)| add_new_item_to_acc(&whole_param, binding));
112 }
113
114 fn params_from_stmt_list_scope(
115     ctx: &CompletionContext,
116     stmt_list: ast::StmtList,
117     mut cb: impl FnMut(hir::Name, String),
118 ) {
119     let syntax_node = match stmt_list.syntax().last_child() {
120         Some(it) => it,
121         None => return,
122     };
123     if let Some(scope) =
124         ctx.sema.scope_at_offset(stmt_list.syntax(), syntax_node.text_range().end())
125     {
126         let module = scope.module().into();
127         scope.process_all_names(&mut |name, def| {
128             if let hir::ScopeDef::Local(local) = def {
129                 if let Ok(ty) = local.ty(ctx.db).display_source_code(ctx.db, module) {
130                     cb(name, ty);
131                 }
132             }
133         });
134     }
135 }
136
137 fn remove_duplicated(
138     file_params: &mut FxHashMap<String, String>,
139     fn_params: ast::AstChildren<ast::Param>,
140 ) {
141     fn_params.for_each(|param| {
142         let whole_param = param.syntax().text().to_string();
143         file_params.remove(&whole_param);
144
145         match param.pat() {
146             // remove suggestions for patterns that already exist
147             // if the type is missing we are checking the current param to be completed
148             // in which case this would find itself removing the suggestions due to itself
149             Some(pattern) if param.ty().is_some() => {
150                 let binding = pattern.syntax().text().to_string();
151                 file_params.retain(|_, v| v != &binding);
152             }
153             _ => (),
154         }
155     })
156 }
157
158 fn should_add_self_completions(ctx: &CompletionContext, param_list: &ast::ParamList) -> bool {
159     let inside_impl = ctx.impl_def.is_some();
160     let no_params = param_list.params().next().is_none() && param_list.self_param().is_none();
161
162     inside_impl && no_params
163 }
164
165 fn comma_wrapper(ctx: &CompletionContext) -> Option<(impl Fn(&str) -> String, TextRange)> {
166     let param = ctx.token.ancestors().find(|node| node.kind() == SyntaxKind::PARAM)?;
167
168     let next_token_kind = {
169         let t = param.last_token()?.next_token()?;
170         let t = algo::skip_whitespace_token(t, Direction::Next)?;
171         t.kind()
172     };
173     let prev_token_kind = {
174         let t = param.first_token()?.prev_token()?;
175         let t = algo::skip_whitespace_token(t, Direction::Prev)?;
176         t.kind()
177     };
178
179     let has_trailing_comma =
180         matches!(next_token_kind, SyntaxKind::COMMA | SyntaxKind::R_PAREN | SyntaxKind::PIPE);
181     let trailing = if has_trailing_comma { "" } else { "," };
182
183     let has_leading_comma =
184         matches!(prev_token_kind, SyntaxKind::COMMA | SyntaxKind::L_PAREN | SyntaxKind::PIPE);
185     let leading = if has_leading_comma { "" } else { ", " };
186
187     Some((move |label: &_| (format!("{}{}{}", leading, label, trailing)), param.text_range()))
188 }