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