diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-08-13 16:59:50 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-08-13 16:59:50 +0100 |
commit | 018a6cac072767dfd630c22e6d9ce134b7bb09af (patch) | |
tree | 4293492e643f9a604c5f30e051289bcea182694c /crates/ide/src | |
parent | 00fb411f3edea72a1a9739f7df6f21cca045730b (diff) | |
parent | 6bc2633c90cedad057c5201d1ab7f67b57247004 (diff) |
Merge #5750
5750: Rename ra_ide -> ide
r=matklad a=matklad
bors r+
🤖
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide/src')
54 files changed, 23586 insertions, 0 deletions
diff --git a/crates/ide/src/call_hierarchy.rs b/crates/ide/src/call_hierarchy.rs new file mode 100644 index 000000000..58e26b94c --- /dev/null +++ b/crates/ide/src/call_hierarchy.rs | |||
@@ -0,0 +1,393 @@ | |||
1 | //! Entry point for call-hierarchy | ||
2 | |||
3 | use indexmap::IndexMap; | ||
4 | |||
5 | use hir::Semantics; | ||
6 | use ide_db::RootDatabase; | ||
7 | use syntax::{ast, match_ast, AstNode, TextRange}; | ||
8 | |||
9 | use crate::{ | ||
10 | call_info::FnCallNode, display::ToNav, goto_definition, references, FilePosition, | ||
11 | NavigationTarget, RangeInfo, | ||
12 | }; | ||
13 | |||
14 | #[derive(Debug, Clone)] | ||
15 | pub struct CallItem { | ||
16 | pub target: NavigationTarget, | ||
17 | pub ranges: Vec<TextRange>, | ||
18 | } | ||
19 | |||
20 | impl CallItem { | ||
21 | #[cfg(test)] | ||
22 | pub(crate) fn assert_match(&self, expected: &str) { | ||
23 | let actual = self.debug_render(); | ||
24 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
25 | } | ||
26 | |||
27 | #[cfg(test)] | ||
28 | pub(crate) fn debug_render(&self) -> String { | ||
29 | format!("{} : {:?}", self.target.debug_render(), self.ranges) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | pub(crate) fn call_hierarchy( | ||
34 | db: &RootDatabase, | ||
35 | position: FilePosition, | ||
36 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
37 | goto_definition::goto_definition(db, position) | ||
38 | } | ||
39 | |||
40 | pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> { | ||
41 | let sema = Semantics::new(db); | ||
42 | |||
43 | // 1. Find all refs | ||
44 | // 2. Loop through refs and determine unique fndef. This will become our `from: CallHierarchyItem,` in the reply. | ||
45 | // 3. Add ranges relative to the start of the fndef. | ||
46 | let refs = references::find_all_refs(&sema, position, None)?; | ||
47 | |||
48 | let mut calls = CallLocations::default(); | ||
49 | |||
50 | for reference in refs.info.references() { | ||
51 | let file_id = reference.file_range.file_id; | ||
52 | let file = sema.parse(file_id); | ||
53 | let file = file.syntax(); | ||
54 | let token = file.token_at_offset(reference.file_range.range.start()).next()?; | ||
55 | let token = sema.descend_into_macros(token); | ||
56 | let syntax = token.parent(); | ||
57 | |||
58 | // This target is the containing function | ||
59 | if let Some(nav) = syntax.ancestors().find_map(|node| { | ||
60 | match_ast! { | ||
61 | match node { | ||
62 | ast::Fn(it) => { | ||
63 | let def = sema.to_def(&it)?; | ||
64 | Some(def.to_nav(sema.db)) | ||
65 | }, | ||
66 | _ => None, | ||
67 | } | ||
68 | } | ||
69 | }) { | ||
70 | let relative_range = reference.file_range.range; | ||
71 | calls.add(&nav, relative_range); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | Some(calls.into_items()) | ||
76 | } | ||
77 | |||
78 | pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> { | ||
79 | let sema = Semantics::new(db); | ||
80 | let file_id = position.file_id; | ||
81 | let file = sema.parse(file_id); | ||
82 | let file = file.syntax(); | ||
83 | let token = file.token_at_offset(position.offset).next()?; | ||
84 | let token = sema.descend_into_macros(token); | ||
85 | let syntax = token.parent(); | ||
86 | |||
87 | let mut calls = CallLocations::default(); | ||
88 | |||
89 | syntax | ||
90 | .descendants() | ||
91 | .filter_map(|node| FnCallNode::with_node_exact(&node)) | ||
92 | .filter_map(|call_node| { | ||
93 | let name_ref = call_node.name_ref()?; | ||
94 | |||
95 | if let Some(func_target) = match &call_node { | ||
96 | FnCallNode::CallExpr(expr) => { | ||
97 | //FIXME: Type::as_callable is broken | ||
98 | let callable = sema.type_of_expr(&expr.expr()?)?.as_callable(db)?; | ||
99 | match callable.kind() { | ||
100 | hir::CallableKind::Function(it) => { | ||
101 | let fn_def: hir::Function = it.into(); | ||
102 | let nav = fn_def.to_nav(db); | ||
103 | Some(nav) | ||
104 | } | ||
105 | _ => None, | ||
106 | } | ||
107 | } | ||
108 | FnCallNode::MethodCallExpr(expr) => { | ||
109 | let function = sema.resolve_method_call(&expr)?; | ||
110 | Some(function.to_nav(db)) | ||
111 | } | ||
112 | } { | ||
113 | Some((func_target, name_ref.syntax().text_range())) | ||
114 | } else { | ||
115 | None | ||
116 | } | ||
117 | }) | ||
118 | .for_each(|(nav, range)| calls.add(&nav, range)); | ||
119 | |||
120 | Some(calls.into_items()) | ||
121 | } | ||
122 | |||
123 | #[derive(Default)] | ||
124 | struct CallLocations { | ||
125 | funcs: IndexMap<NavigationTarget, Vec<TextRange>>, | ||
126 | } | ||
127 | |||
128 | impl CallLocations { | ||
129 | fn add(&mut self, target: &NavigationTarget, range: TextRange) { | ||
130 | self.funcs.entry(target.clone()).or_default().push(range); | ||
131 | } | ||
132 | |||
133 | fn into_items(self) -> Vec<CallItem> { | ||
134 | self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() | ||
135 | } | ||
136 | } | ||
137 | |||
138 | #[cfg(test)] | ||
139 | mod tests { | ||
140 | use base_db::FilePosition; | ||
141 | |||
142 | use crate::mock_analysis::analysis_and_position; | ||
143 | |||
144 | fn check_hierarchy( | ||
145 | ra_fixture: &str, | ||
146 | expected: &str, | ||
147 | expected_incoming: &[&str], | ||
148 | expected_outgoing: &[&str], | ||
149 | ) { | ||
150 | let (analysis, pos) = analysis_and_position(ra_fixture); | ||
151 | |||
152 | let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; | ||
153 | assert_eq!(navs.len(), 1); | ||
154 | let nav = navs.pop().unwrap(); | ||
155 | nav.assert_match(expected); | ||
156 | |||
157 | let item_pos = | ||
158 | FilePosition { file_id: nav.file_id, offset: nav.focus_or_full_range().start() }; | ||
159 | let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap(); | ||
160 | assert_eq!(incoming_calls.len(), expected_incoming.len()); | ||
161 | |||
162 | for call in 0..incoming_calls.len() { | ||
163 | incoming_calls[call].assert_match(expected_incoming[call]); | ||
164 | } | ||
165 | |||
166 | let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap(); | ||
167 | assert_eq!(outgoing_calls.len(), expected_outgoing.len()); | ||
168 | |||
169 | for call in 0..outgoing_calls.len() { | ||
170 | outgoing_calls[call].assert_match(expected_outgoing[call]); | ||
171 | } | ||
172 | } | ||
173 | |||
174 | #[test] | ||
175 | fn test_call_hierarchy_on_ref() { | ||
176 | check_hierarchy( | ||
177 | r#" | ||
178 | //- /lib.rs | ||
179 | fn callee() {} | ||
180 | fn caller() { | ||
181 | call<|>ee(); | ||
182 | } | ||
183 | "#, | ||
184 | "callee FN FileId(1) 0..14 3..9", | ||
185 | &["caller FN FileId(1) 15..44 18..24 : [33..39]"], | ||
186 | &[], | ||
187 | ); | ||
188 | } | ||
189 | |||
190 | #[test] | ||
191 | fn test_call_hierarchy_on_def() { | ||
192 | check_hierarchy( | ||
193 | r#" | ||
194 | //- /lib.rs | ||
195 | fn call<|>ee() {} | ||
196 | fn caller() { | ||
197 | callee(); | ||
198 | } | ||
199 | "#, | ||
200 | "callee FN FileId(1) 0..14 3..9", | ||
201 | &["caller FN FileId(1) 15..44 18..24 : [33..39]"], | ||
202 | &[], | ||
203 | ); | ||
204 | } | ||
205 | |||
206 | #[test] | ||
207 | fn test_call_hierarchy_in_same_fn() { | ||
208 | check_hierarchy( | ||
209 | r#" | ||
210 | //- /lib.rs | ||
211 | fn callee() {} | ||
212 | fn caller() { | ||
213 | call<|>ee(); | ||
214 | callee(); | ||
215 | } | ||
216 | "#, | ||
217 | "callee FN FileId(1) 0..14 3..9", | ||
218 | &["caller FN FileId(1) 15..58 18..24 : [33..39, 47..53]"], | ||
219 | &[], | ||
220 | ); | ||
221 | } | ||
222 | |||
223 | #[test] | ||
224 | fn test_call_hierarchy_in_different_fn() { | ||
225 | check_hierarchy( | ||
226 | r#" | ||
227 | //- /lib.rs | ||
228 | fn callee() {} | ||
229 | fn caller1() { | ||
230 | call<|>ee(); | ||
231 | } | ||
232 | |||
233 | fn caller2() { | ||
234 | callee(); | ||
235 | } | ||
236 | "#, | ||
237 | "callee FN FileId(1) 0..14 3..9", | ||
238 | &[ | ||
239 | "caller1 FN FileId(1) 15..45 18..25 : [34..40]", | ||
240 | "caller2 FN FileId(1) 47..77 50..57 : [66..72]", | ||
241 | ], | ||
242 | &[], | ||
243 | ); | ||
244 | } | ||
245 | |||
246 | #[test] | ||
247 | fn test_call_hierarchy_in_tests_mod() { | ||
248 | check_hierarchy( | ||
249 | r#" | ||
250 | //- /lib.rs cfg:test | ||
251 | fn callee() {} | ||
252 | fn caller1() { | ||
253 | call<|>ee(); | ||
254 | } | ||
255 | |||
256 | #[cfg(test)] | ||
257 | mod tests { | ||
258 | use super::*; | ||
259 | |||
260 | #[test] | ||
261 | fn test_caller() { | ||
262 | callee(); | ||
263 | } | ||
264 | } | ||
265 | "#, | ||
266 | "callee FN FileId(1) 0..14 3..9", | ||
267 | &[ | ||
268 | "caller1 FN FileId(1) 15..45 18..25 : [34..40]", | ||
269 | "test_caller FN FileId(1) 95..149 110..121 : [134..140]", | ||
270 | ], | ||
271 | &[], | ||
272 | ); | ||
273 | } | ||
274 | |||
275 | #[test] | ||
276 | fn test_call_hierarchy_in_different_files() { | ||
277 | check_hierarchy( | ||
278 | r#" | ||
279 | //- /lib.rs | ||
280 | mod foo; | ||
281 | use foo::callee; | ||
282 | |||
283 | fn caller() { | ||
284 | call<|>ee(); | ||
285 | } | ||
286 | |||
287 | //- /foo/mod.rs | ||
288 | pub fn callee() {} | ||
289 | "#, | ||
290 | "callee FN FileId(2) 0..18 7..13", | ||
291 | &["caller FN FileId(1) 27..56 30..36 : [45..51]"], | ||
292 | &[], | ||
293 | ); | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn test_call_hierarchy_outgoing() { | ||
298 | check_hierarchy( | ||
299 | r#" | ||
300 | //- /lib.rs | ||
301 | fn callee() {} | ||
302 | fn call<|>er() { | ||
303 | callee(); | ||
304 | callee(); | ||
305 | } | ||
306 | "#, | ||
307 | "caller FN FileId(1) 15..58 18..24", | ||
308 | &[], | ||
309 | &["callee FN FileId(1) 0..14 3..9 : [33..39, 47..53]"], | ||
310 | ); | ||
311 | } | ||
312 | |||
313 | #[test] | ||
314 | fn test_call_hierarchy_outgoing_in_different_files() { | ||
315 | check_hierarchy( | ||
316 | r#" | ||
317 | //- /lib.rs | ||
318 | mod foo; | ||
319 | use foo::callee; | ||
320 | |||
321 | fn call<|>er() { | ||
322 | callee(); | ||
323 | } | ||
324 | |||
325 | //- /foo/mod.rs | ||
326 | pub fn callee() {} | ||
327 | "#, | ||
328 | "caller FN FileId(1) 27..56 30..36", | ||
329 | &[], | ||
330 | &["callee FN FileId(2) 0..18 7..13 : [45..51]"], | ||
331 | ); | ||
332 | } | ||
333 | |||
334 | #[test] | ||
335 | fn test_call_hierarchy_incoming_outgoing() { | ||
336 | check_hierarchy( | ||
337 | r#" | ||
338 | //- /lib.rs | ||
339 | fn caller1() { | ||
340 | call<|>er2(); | ||
341 | } | ||
342 | |||
343 | fn caller2() { | ||
344 | caller3(); | ||
345 | } | ||
346 | |||
347 | fn caller3() { | ||
348 | |||
349 | } | ||
350 | "#, | ||
351 | "caller2 FN FileId(1) 33..64 36..43", | ||
352 | &["caller1 FN FileId(1) 0..31 3..10 : [19..26]"], | ||
353 | &["caller3 FN FileId(1) 66..83 69..76 : [52..59]"], | ||
354 | ); | ||
355 | } | ||
356 | |||
357 | #[test] | ||
358 | fn test_call_hierarchy_issue_5103() { | ||
359 | check_hierarchy( | ||
360 | r#" | ||
361 | fn a() { | ||
362 | b() | ||
363 | } | ||
364 | |||
365 | fn b() {} | ||
366 | |||
367 | fn main() { | ||
368 | a<|>() | ||
369 | } | ||
370 | "#, | ||
371 | "a FN FileId(1) 0..18 3..4", | ||
372 | &["main FN FileId(1) 31..52 34..38 : [47..48]"], | ||
373 | &["b FN FileId(1) 20..29 23..24 : [13..14]"], | ||
374 | ); | ||
375 | |||
376 | check_hierarchy( | ||
377 | r#" | ||
378 | fn a() { | ||
379 | b<|>() | ||
380 | } | ||
381 | |||
382 | fn b() {} | ||
383 | |||
384 | fn main() { | ||
385 | a() | ||
386 | } | ||
387 | "#, | ||
388 | "b FN FileId(1) 20..29 23..24", | ||
389 | &["a FN FileId(1) 0..18 3..4 : [13..14]"], | ||
390 | &[], | ||
391 | ); | ||
392 | } | ||
393 | } | ||
diff --git a/crates/ide/src/call_info.rs b/crates/ide/src/call_info.rs new file mode 100644 index 000000000..86abd2d8c --- /dev/null +++ b/crates/ide/src/call_info.rs | |||
@@ -0,0 +1,742 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | use either::Either; | ||
3 | use hir::{Docs, HirDisplay, Semantics, Type}; | ||
4 | use ide_db::RootDatabase; | ||
5 | use stdx::format_to; | ||
6 | use syntax::{ | ||
7 | ast::{self, ArgListOwner}, | ||
8 | match_ast, AstNode, SyntaxNode, SyntaxToken, TextRange, TextSize, | ||
9 | }; | ||
10 | use test_utils::mark; | ||
11 | |||
12 | use crate::FilePosition; | ||
13 | |||
14 | /// Contains information about a call site. Specifically the | ||
15 | /// `FunctionSignature`and current parameter. | ||
16 | #[derive(Debug)] | ||
17 | pub struct CallInfo { | ||
18 | pub doc: Option<String>, | ||
19 | pub signature: String, | ||
20 | pub active_parameter: Option<usize>, | ||
21 | parameters: Vec<TextRange>, | ||
22 | } | ||
23 | |||
24 | impl CallInfo { | ||
25 | pub fn parameter_labels(&self) -> impl Iterator<Item = &str> + '_ { | ||
26 | self.parameters.iter().map(move |&it| &self.signature[it]) | ||
27 | } | ||
28 | pub fn parameter_ranges(&self) -> &[TextRange] { | ||
29 | &self.parameters | ||
30 | } | ||
31 | fn push_param(&mut self, param: &str) { | ||
32 | if !self.signature.ends_with('(') { | ||
33 | self.signature.push_str(", "); | ||
34 | } | ||
35 | let start = TextSize::of(&self.signature); | ||
36 | self.signature.push_str(param); | ||
37 | let end = TextSize::of(&self.signature); | ||
38 | self.parameters.push(TextRange::new(start, end)) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | /// Computes parameter information for the given call expression. | ||
43 | pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option<CallInfo> { | ||
44 | let sema = Semantics::new(db); | ||
45 | let file = sema.parse(position.file_id); | ||
46 | let file = file.syntax(); | ||
47 | let token = file.token_at_offset(position.offset).next()?; | ||
48 | let token = sema.descend_into_macros(token); | ||
49 | |||
50 | let (callable, active_parameter) = call_info_impl(&sema, token)?; | ||
51 | |||
52 | let mut res = | ||
53 | CallInfo { doc: None, signature: String::new(), parameters: vec![], active_parameter }; | ||
54 | |||
55 | match callable.kind() { | ||
56 | hir::CallableKind::Function(func) => { | ||
57 | res.doc = func.docs(db).map(|it| it.as_str().to_string()); | ||
58 | format_to!(res.signature, "fn {}", func.name(db)); | ||
59 | } | ||
60 | hir::CallableKind::TupleStruct(strukt) => { | ||
61 | res.doc = strukt.docs(db).map(|it| it.as_str().to_string()); | ||
62 | format_to!(res.signature, "struct {}", strukt.name(db)); | ||
63 | } | ||
64 | hir::CallableKind::TupleEnumVariant(variant) => { | ||
65 | res.doc = variant.docs(db).map(|it| it.as_str().to_string()); | ||
66 | format_to!( | ||
67 | res.signature, | ||
68 | "enum {}::{}", | ||
69 | variant.parent_enum(db).name(db), | ||
70 | variant.name(db) | ||
71 | ); | ||
72 | } | ||
73 | hir::CallableKind::Closure => (), | ||
74 | } | ||
75 | |||
76 | res.signature.push('('); | ||
77 | { | ||
78 | if let Some(self_param) = callable.receiver_param(db) { | ||
79 | format_to!(res.signature, "{}", self_param) | ||
80 | } | ||
81 | let mut buf = String::new(); | ||
82 | for (pat, ty) in callable.params(db) { | ||
83 | buf.clear(); | ||
84 | if let Some(pat) = pat { | ||
85 | match pat { | ||
86 | Either::Left(_self) => format_to!(buf, "self: "), | ||
87 | Either::Right(pat) => format_to!(buf, "{}: ", pat), | ||
88 | } | ||
89 | } | ||
90 | format_to!(buf, "{}", ty.display(db)); | ||
91 | res.push_param(&buf); | ||
92 | } | ||
93 | } | ||
94 | res.signature.push(')'); | ||
95 | |||
96 | match callable.kind() { | ||
97 | hir::CallableKind::Function(_) | hir::CallableKind::Closure => { | ||
98 | let ret_type = callable.return_type(); | ||
99 | if !ret_type.is_unit() { | ||
100 | format_to!(res.signature, " -> {}", ret_type.display(db)); | ||
101 | } | ||
102 | } | ||
103 | hir::CallableKind::TupleStruct(_) | hir::CallableKind::TupleEnumVariant(_) => {} | ||
104 | } | ||
105 | Some(res) | ||
106 | } | ||
107 | |||
108 | fn call_info_impl( | ||
109 | sema: &Semantics<RootDatabase>, | ||
110 | token: SyntaxToken, | ||
111 | ) -> Option<(hir::Callable, Option<usize>)> { | ||
112 | // Find the calling expression and it's NameRef | ||
113 | let calling_node = FnCallNode::with_node(&token.parent())?; | ||
114 | |||
115 | let callable = match &calling_node { | ||
116 | FnCallNode::CallExpr(call) => sema.type_of_expr(&call.expr()?)?.as_callable(sema.db)?, | ||
117 | FnCallNode::MethodCallExpr(call) => sema.resolve_method_call_as_callable(call)?, | ||
118 | }; | ||
119 | let active_param = if let Some(arg_list) = calling_node.arg_list() { | ||
120 | // Number of arguments specified at the call site | ||
121 | let num_args_at_callsite = arg_list.args().count(); | ||
122 | |||
123 | let arg_list_range = arg_list.syntax().text_range(); | ||
124 | if !arg_list_range.contains_inclusive(token.text_range().start()) { | ||
125 | mark::hit!(call_info_bad_offset); | ||
126 | return None; | ||
127 | } | ||
128 | let param = std::cmp::min( | ||
129 | num_args_at_callsite, | ||
130 | arg_list | ||
131 | .args() | ||
132 | .take_while(|arg| arg.syntax().text_range().end() <= token.text_range().start()) | ||
133 | .count(), | ||
134 | ); | ||
135 | |||
136 | Some(param) | ||
137 | } else { | ||
138 | None | ||
139 | }; | ||
140 | Some((callable, active_param)) | ||
141 | } | ||
142 | |||
143 | #[derive(Debug)] | ||
144 | pub(crate) struct ActiveParameter { | ||
145 | pub(crate) ty: Type, | ||
146 | pub(crate) name: String, | ||
147 | } | ||
148 | |||
149 | impl ActiveParameter { | ||
150 | pub(crate) fn at(db: &RootDatabase, position: FilePosition) -> Option<Self> { | ||
151 | let sema = Semantics::new(db); | ||
152 | let file = sema.parse(position.file_id); | ||
153 | let file = file.syntax(); | ||
154 | let token = file.token_at_offset(position.offset).next()?; | ||
155 | let token = sema.descend_into_macros(token); | ||
156 | Self::at_token(&sema, token) | ||
157 | } | ||
158 | |||
159 | pub(crate) fn at_token(sema: &Semantics<RootDatabase>, token: SyntaxToken) -> Option<Self> { | ||
160 | let (signature, active_parameter) = call_info_impl(&sema, token)?; | ||
161 | |||
162 | let idx = active_parameter?; | ||
163 | let mut params = signature.params(sema.db); | ||
164 | if !(idx < params.len()) { | ||
165 | mark::hit!(too_many_arguments); | ||
166 | return None; | ||
167 | } | ||
168 | let (pat, ty) = params.swap_remove(idx); | ||
169 | let name = pat?.to_string(); | ||
170 | Some(ActiveParameter { ty, name }) | ||
171 | } | ||
172 | } | ||
173 | |||
174 | #[derive(Debug)] | ||
175 | pub(crate) enum FnCallNode { | ||
176 | CallExpr(ast::CallExpr), | ||
177 | MethodCallExpr(ast::MethodCallExpr), | ||
178 | } | ||
179 | |||
180 | impl FnCallNode { | ||
181 | fn with_node(syntax: &SyntaxNode) -> Option<FnCallNode> { | ||
182 | syntax.ancestors().find_map(|node| { | ||
183 | match_ast! { | ||
184 | match node { | ||
185 | ast::CallExpr(it) => Some(FnCallNode::CallExpr(it)), | ||
186 | ast::MethodCallExpr(it) => { | ||
187 | let arg_list = it.arg_list()?; | ||
188 | if !arg_list.syntax().text_range().contains_range(syntax.text_range()) { | ||
189 | return None; | ||
190 | } | ||
191 | Some(FnCallNode::MethodCallExpr(it)) | ||
192 | }, | ||
193 | _ => None, | ||
194 | } | ||
195 | } | ||
196 | }) | ||
197 | } | ||
198 | |||
199 | pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option<FnCallNode> { | ||
200 | match_ast! { | ||
201 | match node { | ||
202 | ast::CallExpr(it) => Some(FnCallNode::CallExpr(it)), | ||
203 | ast::MethodCallExpr(it) => Some(FnCallNode::MethodCallExpr(it)), | ||
204 | _ => None, | ||
205 | } | ||
206 | } | ||
207 | } | ||
208 | |||
209 | pub(crate) fn name_ref(&self) -> Option<ast::NameRef> { | ||
210 | match self { | ||
211 | FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { | ||
212 | ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, | ||
213 | _ => return None, | ||
214 | }), | ||
215 | |||
216 | FnCallNode::MethodCallExpr(call_expr) => { | ||
217 | call_expr.syntax().children().filter_map(ast::NameRef::cast).next() | ||
218 | } | ||
219 | } | ||
220 | } | ||
221 | |||
222 | fn arg_list(&self) -> Option<ast::ArgList> { | ||
223 | match self { | ||
224 | FnCallNode::CallExpr(expr) => expr.arg_list(), | ||
225 | FnCallNode::MethodCallExpr(expr) => expr.arg_list(), | ||
226 | } | ||
227 | } | ||
228 | } | ||
229 | |||
230 | #[cfg(test)] | ||
231 | mod tests { | ||
232 | use expect::{expect, Expect}; | ||
233 | use test_utils::mark; | ||
234 | |||
235 | use crate::mock_analysis::analysis_and_position; | ||
236 | |||
237 | fn check(ra_fixture: &str, expect: Expect) { | ||
238 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
239 | let call_info = analysis.call_info(position).unwrap(); | ||
240 | let actual = match call_info { | ||
241 | Some(call_info) => { | ||
242 | let docs = match &call_info.doc { | ||
243 | None => "".to_string(), | ||
244 | Some(docs) => format!("{}\n------\n", docs.as_str()), | ||
245 | }; | ||
246 | let params = call_info | ||
247 | .parameter_labels() | ||
248 | .enumerate() | ||
249 | .map(|(i, param)| { | ||
250 | if Some(i) == call_info.active_parameter { | ||
251 | format!("<{}>", param) | ||
252 | } else { | ||
253 | param.to_string() | ||
254 | } | ||
255 | }) | ||
256 | .collect::<Vec<_>>() | ||
257 | .join(", "); | ||
258 | format!("{}{}\n({})\n", docs, call_info.signature, params) | ||
259 | } | ||
260 | None => String::new(), | ||
261 | }; | ||
262 | expect.assert_eq(&actual); | ||
263 | } | ||
264 | |||
265 | #[test] | ||
266 | fn test_fn_signature_two_args() { | ||
267 | check( | ||
268 | r#" | ||
269 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
270 | fn bar() { foo(<|>3, ); } | ||
271 | "#, | ||
272 | expect![[r#" | ||
273 | fn foo(x: u32, y: u32) -> u32 | ||
274 | (<x: u32>, y: u32) | ||
275 | "#]], | ||
276 | ); | ||
277 | check( | ||
278 | r#" | ||
279 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
280 | fn bar() { foo(3<|>, ); } | ||
281 | "#, | ||
282 | expect![[r#" | ||
283 | fn foo(x: u32, y: u32) -> u32 | ||
284 | (<x: u32>, y: u32) | ||
285 | "#]], | ||
286 | ); | ||
287 | check( | ||
288 | r#" | ||
289 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
290 | fn bar() { foo(3,<|> ); } | ||
291 | "#, | ||
292 | expect![[r#" | ||
293 | fn foo(x: u32, y: u32) -> u32 | ||
294 | (x: u32, <y: u32>) | ||
295 | "#]], | ||
296 | ); | ||
297 | check( | ||
298 | r#" | ||
299 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
300 | fn bar() { foo(3, <|>); } | ||
301 | "#, | ||
302 | expect![[r#" | ||
303 | fn foo(x: u32, y: u32) -> u32 | ||
304 | (x: u32, <y: u32>) | ||
305 | "#]], | ||
306 | ); | ||
307 | } | ||
308 | |||
309 | #[test] | ||
310 | fn test_fn_signature_two_args_empty() { | ||
311 | check( | ||
312 | r#" | ||
313 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
314 | fn bar() { foo(<|>); } | ||
315 | "#, | ||
316 | expect![[r#" | ||
317 | fn foo(x: u32, y: u32) -> u32 | ||
318 | (<x: u32>, y: u32) | ||
319 | "#]], | ||
320 | ); | ||
321 | } | ||
322 | |||
323 | #[test] | ||
324 | fn test_fn_signature_two_args_first_generics() { | ||
325 | check( | ||
326 | r#" | ||
327 | fn foo<T, U: Copy + Display>(x: T, y: U) -> u32 | ||
328 | where T: Copy + Display, U: Debug | ||
329 | { x + y } | ||
330 | |||
331 | fn bar() { foo(<|>3, ); } | ||
332 | "#, | ||
333 | expect![[r#" | ||
334 | fn foo(x: i32, y: {unknown}) -> u32 | ||
335 | (<x: i32>, y: {unknown}) | ||
336 | "#]], | ||
337 | ); | ||
338 | } | ||
339 | |||
340 | #[test] | ||
341 | fn test_fn_signature_no_params() { | ||
342 | check( | ||
343 | r#" | ||
344 | fn foo<T>() -> T where T: Copy + Display {} | ||
345 | fn bar() { foo(<|>); } | ||
346 | "#, | ||
347 | expect![[r#" | ||
348 | fn foo() -> {unknown} | ||
349 | () | ||
350 | "#]], | ||
351 | ); | ||
352 | } | ||
353 | |||
354 | #[test] | ||
355 | fn test_fn_signature_for_impl() { | ||
356 | check( | ||
357 | r#" | ||
358 | struct F; | ||
359 | impl F { pub fn new() { } } | ||
360 | fn bar() { | ||
361 | let _ : F = F::new(<|>); | ||
362 | } | ||
363 | "#, | ||
364 | expect![[r#" | ||
365 | fn new() | ||
366 | () | ||
367 | "#]], | ||
368 | ); | ||
369 | } | ||
370 | |||
371 | #[test] | ||
372 | fn test_fn_signature_for_method_self() { | ||
373 | check( | ||
374 | r#" | ||
375 | struct S; | ||
376 | impl S { pub fn do_it(&self) {} } | ||
377 | |||
378 | fn bar() { | ||
379 | let s: S = S; | ||
380 | s.do_it(<|>); | ||
381 | } | ||
382 | "#, | ||
383 | expect![[r#" | ||
384 | fn do_it(&self) | ||
385 | () | ||
386 | "#]], | ||
387 | ); | ||
388 | } | ||
389 | |||
390 | #[test] | ||
391 | fn test_fn_signature_for_method_with_arg() { | ||
392 | check( | ||
393 | r#" | ||
394 | struct S; | ||
395 | impl S { | ||
396 | fn foo(&self, x: i32) {} | ||
397 | } | ||
398 | |||
399 | fn main() { S.foo(<|>); } | ||
400 | "#, | ||
401 | expect![[r#" | ||
402 | fn foo(&self, x: i32) | ||
403 | (<x: i32>) | ||
404 | "#]], | ||
405 | ); | ||
406 | } | ||
407 | |||
408 | #[test] | ||
409 | fn test_fn_signature_for_method_with_arg_as_assoc_fn() { | ||
410 | check( | ||
411 | r#" | ||
412 | struct S; | ||
413 | impl S { | ||
414 | fn foo(&self, x: i32) {} | ||
415 | } | ||
416 | |||
417 | fn main() { S::foo(<|>); } | ||
418 | "#, | ||
419 | expect![[r#" | ||
420 | fn foo(self: &S, x: i32) | ||
421 | (<self: &S>, x: i32) | ||
422 | "#]], | ||
423 | ); | ||
424 | } | ||
425 | |||
426 | #[test] | ||
427 | fn test_fn_signature_with_docs_simple() { | ||
428 | check( | ||
429 | r#" | ||
430 | /// test | ||
431 | // non-doc-comment | ||
432 | fn foo(j: u32) -> u32 { | ||
433 | j | ||
434 | } | ||
435 | |||
436 | fn bar() { | ||
437 | let _ = foo(<|>); | ||
438 | } | ||
439 | "#, | ||
440 | expect![[r#" | ||
441 | test | ||
442 | ------ | ||
443 | fn foo(j: u32) -> u32 | ||
444 | (<j: u32>) | ||
445 | "#]], | ||
446 | ); | ||
447 | } | ||
448 | |||
449 | #[test] | ||
450 | fn test_fn_signature_with_docs() { | ||
451 | check( | ||
452 | r#" | ||
453 | /// Adds one to the number given. | ||
454 | /// | ||
455 | /// # Examples | ||
456 | /// | ||
457 | /// ``` | ||
458 | /// let five = 5; | ||
459 | /// | ||
460 | /// assert_eq!(6, my_crate::add_one(5)); | ||
461 | /// ``` | ||
462 | pub fn add_one(x: i32) -> i32 { | ||
463 | x + 1 | ||
464 | } | ||
465 | |||
466 | pub fn do() { | ||
467 | add_one(<|> | ||
468 | }"#, | ||
469 | expect![[r##" | ||
470 | Adds one to the number given. | ||
471 | |||
472 | # Examples | ||
473 | |||
474 | ``` | ||
475 | let five = 5; | ||
476 | |||
477 | assert_eq!(6, my_crate::add_one(5)); | ||
478 | ``` | ||
479 | ------ | ||
480 | fn add_one(x: i32) -> i32 | ||
481 | (<x: i32>) | ||
482 | "##]], | ||
483 | ); | ||
484 | } | ||
485 | |||
486 | #[test] | ||
487 | fn test_fn_signature_with_docs_impl() { | ||
488 | check( | ||
489 | r#" | ||
490 | struct addr; | ||
491 | impl addr { | ||
492 | /// Adds one to the number given. | ||
493 | /// | ||
494 | /// # Examples | ||
495 | /// | ||
496 | /// ``` | ||
497 | /// let five = 5; | ||
498 | /// | ||
499 | /// assert_eq!(6, my_crate::add_one(5)); | ||
500 | /// ``` | ||
501 | pub fn add_one(x: i32) -> i32 { | ||
502 | x + 1 | ||
503 | } | ||
504 | } | ||
505 | |||
506 | pub fn do_it() { | ||
507 | addr {}; | ||
508 | addr::add_one(<|>); | ||
509 | } | ||
510 | "#, | ||
511 | expect![[r##" | ||
512 | Adds one to the number given. | ||
513 | |||
514 | # Examples | ||
515 | |||
516 | ``` | ||
517 | let five = 5; | ||
518 | |||
519 | assert_eq!(6, my_crate::add_one(5)); | ||
520 | ``` | ||
521 | ------ | ||
522 | fn add_one(x: i32) -> i32 | ||
523 | (<x: i32>) | ||
524 | "##]], | ||
525 | ); | ||
526 | } | ||
527 | |||
528 | #[test] | ||
529 | fn test_fn_signature_with_docs_from_actix() { | ||
530 | check( | ||
531 | r#" | ||
532 | struct WriteHandler<E>; | ||
533 | |||
534 | impl<E> WriteHandler<E> { | ||
535 | /// Method is called when writer emits error. | ||
536 | /// | ||
537 | /// If this method returns `ErrorAction::Continue` writer processing | ||
538 | /// continues otherwise stream processing stops. | ||
539 | fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { | ||
540 | Running::Stop | ||
541 | } | ||
542 | |||
543 | /// Method is called when writer finishes. | ||
544 | /// | ||
545 | /// By default this method stops actor's `Context`. | ||
546 | fn finished(&mut self, ctx: &mut Self::Context) { | ||
547 | ctx.stop() | ||
548 | } | ||
549 | } | ||
550 | |||
551 | pub fn foo(mut r: WriteHandler<()>) { | ||
552 | r.finished(<|>); | ||
553 | } | ||
554 | "#, | ||
555 | expect![[r#" | ||
556 | Method is called when writer finishes. | ||
557 | |||
558 | By default this method stops actor's `Context`. | ||
559 | ------ | ||
560 | fn finished(&mut self, ctx: &mut {unknown}) | ||
561 | (<ctx: &mut {unknown}>) | ||
562 | "#]], | ||
563 | ); | ||
564 | } | ||
565 | |||
566 | #[test] | ||
567 | fn call_info_bad_offset() { | ||
568 | mark::check!(call_info_bad_offset); | ||
569 | check( | ||
570 | r#" | ||
571 | fn foo(x: u32, y: u32) -> u32 {x + y} | ||
572 | fn bar() { foo <|> (3, ); } | ||
573 | "#, | ||
574 | expect![[""]], | ||
575 | ); | ||
576 | } | ||
577 | |||
578 | #[test] | ||
579 | fn test_nested_method_in_lambda() { | ||
580 | check( | ||
581 | r#" | ||
582 | struct Foo; | ||
583 | impl Foo { fn bar(&self, _: u32) { } } | ||
584 | |||
585 | fn bar(_: u32) { } | ||
586 | |||
587 | fn main() { | ||
588 | let foo = Foo; | ||
589 | std::thread::spawn(move || foo.bar(<|>)); | ||
590 | } | ||
591 | "#, | ||
592 | expect![[r#" | ||
593 | fn bar(&self, _: u32) | ||
594 | (<_: u32>) | ||
595 | "#]], | ||
596 | ); | ||
597 | } | ||
598 | |||
599 | #[test] | ||
600 | fn works_for_tuple_structs() { | ||
601 | check( | ||
602 | r#" | ||
603 | /// A cool tuple struct | ||
604 | struct S(u32, i32); | ||
605 | fn main() { | ||
606 | let s = S(0, <|>); | ||
607 | } | ||
608 | "#, | ||
609 | expect![[r#" | ||
610 | A cool tuple struct | ||
611 | ------ | ||
612 | struct S(u32, i32) | ||
613 | (u32, <i32>) | ||
614 | "#]], | ||
615 | ); | ||
616 | } | ||
617 | |||
618 | #[test] | ||
619 | fn generic_struct() { | ||
620 | check( | ||
621 | r#" | ||
622 | struct S<T>(T); | ||
623 | fn main() { | ||
624 | let s = S(<|>); | ||
625 | } | ||
626 | "#, | ||
627 | expect![[r#" | ||
628 | struct S({unknown}) | ||
629 | (<{unknown}>) | ||
630 | "#]], | ||
631 | ); | ||
632 | } | ||
633 | |||
634 | #[test] | ||
635 | fn works_for_enum_variants() { | ||
636 | check( | ||
637 | r#" | ||
638 | enum E { | ||
639 | /// A Variant | ||
640 | A(i32), | ||
641 | /// Another | ||
642 | B, | ||
643 | /// And C | ||
644 | C { a: i32, b: i32 } | ||
645 | } | ||
646 | |||
647 | fn main() { | ||
648 | let a = E::A(<|>); | ||
649 | } | ||
650 | "#, | ||
651 | expect![[r#" | ||
652 | A Variant | ||
653 | ------ | ||
654 | enum E::A(i32) | ||
655 | (<i32>) | ||
656 | "#]], | ||
657 | ); | ||
658 | } | ||
659 | |||
660 | #[test] | ||
661 | fn cant_call_struct_record() { | ||
662 | check( | ||
663 | r#" | ||
664 | struct S { x: u32, y: i32 } | ||
665 | fn main() { | ||
666 | let s = S(<|>); | ||
667 | } | ||
668 | "#, | ||
669 | expect![[""]], | ||
670 | ); | ||
671 | } | ||
672 | |||
673 | #[test] | ||
674 | fn cant_call_enum_record() { | ||
675 | check( | ||
676 | r#" | ||
677 | enum E { | ||
678 | /// A Variant | ||
679 | A(i32), | ||
680 | /// Another | ||
681 | B, | ||
682 | /// And C | ||
683 | C { a: i32, b: i32 } | ||
684 | } | ||
685 | |||
686 | fn main() { | ||
687 | let a = E::C(<|>); | ||
688 | } | ||
689 | "#, | ||
690 | expect![[""]], | ||
691 | ); | ||
692 | } | ||
693 | |||
694 | #[test] | ||
695 | fn fn_signature_for_call_in_macro() { | ||
696 | check( | ||
697 | r#" | ||
698 | macro_rules! id { ($($tt:tt)*) => { $($tt)* } } | ||
699 | fn foo() { } | ||
700 | id! { | ||
701 | fn bar() { foo(<|>); } | ||
702 | } | ||
703 | "#, | ||
704 | expect![[r#" | ||
705 | fn foo() | ||
706 | () | ||
707 | "#]], | ||
708 | ); | ||
709 | } | ||
710 | |||
711 | #[test] | ||
712 | fn call_info_for_lambdas() { | ||
713 | check( | ||
714 | r#" | ||
715 | struct S; | ||
716 | fn foo(s: S) -> i32 { 92 } | ||
717 | fn main() { | ||
718 | (|s| foo(s))(<|>) | ||
719 | } | ||
720 | "#, | ||
721 | expect![[r#" | ||
722 | (S) -> i32 | ||
723 | (<S>) | ||
724 | "#]], | ||
725 | ) | ||
726 | } | ||
727 | |||
728 | #[test] | ||
729 | fn call_info_for_fn_ptr() { | ||
730 | check( | ||
731 | r#" | ||
732 | fn main(f: fn(i32, f64) -> char) { | ||
733 | f(0, <|>) | ||
734 | } | ||
735 | "#, | ||
736 | expect![[r#" | ||
737 | (i32, f64) -> char | ||
738 | (i32, <f64>) | ||
739 | "#]], | ||
740 | ) | ||
741 | } | ||
742 | } | ||
diff --git a/crates/ide/src/completion.rs b/crates/ide/src/completion.rs new file mode 100644 index 000000000..7fb4d687e --- /dev/null +++ b/crates/ide/src/completion.rs | |||
@@ -0,0 +1,206 @@ | |||
1 | mod completion_config; | ||
2 | mod completion_item; | ||
3 | mod completion_context; | ||
4 | mod presentation; | ||
5 | mod patterns; | ||
6 | #[cfg(test)] | ||
7 | mod test_utils; | ||
8 | |||
9 | mod complete_attribute; | ||
10 | mod complete_dot; | ||
11 | mod complete_record; | ||
12 | mod complete_pattern; | ||
13 | mod complete_fn_param; | ||
14 | mod complete_keyword; | ||
15 | mod complete_snippet; | ||
16 | mod complete_qualified_path; | ||
17 | mod complete_unqualified_path; | ||
18 | mod complete_postfix; | ||
19 | mod complete_macro_in_item_position; | ||
20 | mod complete_trait_impl; | ||
21 | |||
22 | use ide_db::RootDatabase; | ||
23 | |||
24 | use crate::{ | ||
25 | completion::{ | ||
26 | completion_context::CompletionContext, | ||
27 | completion_item::{CompletionKind, Completions}, | ||
28 | }, | ||
29 | FilePosition, | ||
30 | }; | ||
31 | |||
32 | pub use crate::completion::{ | ||
33 | completion_config::CompletionConfig, | ||
34 | completion_item::{CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat}, | ||
35 | }; | ||
36 | |||
37 | //FIXME: split the following feature into fine-grained features. | ||
38 | |||
39 | // Feature: Magic Completions | ||
40 | // | ||
41 | // In addition to usual reference completion, rust-analyzer provides some ✨magic✨ | ||
42 | // completions as well: | ||
43 | // | ||
44 | // Keywords like `if`, `else` `while`, `loop` are completed with braces, and cursor | ||
45 | // is placed at the appropriate position. Even though `if` is easy to type, you | ||
46 | // still want to complete it, to get ` { }` for free! `return` is inserted with a | ||
47 | // space or `;` depending on the return type of the function. | ||
48 | // | ||
49 | // When completing a function call, `()` are automatically inserted. If a function | ||
50 | // takes arguments, the cursor is positioned inside the parenthesis. | ||
51 | // | ||
52 | // There are postfix completions, which can be triggered by typing something like | ||
53 | // `foo().if`. The word after `.` determines postfix completion. Possible variants are: | ||
54 | // | ||
55 | // - `expr.if` -> `if expr {}` or `if let ... {}` for `Option` or `Result` | ||
56 | // - `expr.match` -> `match expr {}` | ||
57 | // - `expr.while` -> `while expr {}` or `while let ... {}` for `Option` or `Result` | ||
58 | // - `expr.ref` -> `&expr` | ||
59 | // - `expr.refm` -> `&mut expr` | ||
60 | // - `expr.not` -> `!expr` | ||
61 | // - `expr.dbg` -> `dbg!(expr)` | ||
62 | // | ||
63 | // There also snippet completions: | ||
64 | // | ||
65 | // .Expressions | ||
66 | // - `pd` -> `eprintln!(" = {:?}", );` | ||
67 | // - `ppd` -> `eprintln!(" = {:#?}", );` | ||
68 | // | ||
69 | // .Items | ||
70 | // - `tfn` -> `#[test] fn feature(){}` | ||
71 | // - `tmod` -> | ||
72 | // ```rust | ||
73 | // #[cfg(test)] | ||
74 | // mod tests { | ||
75 | // use super::*; | ||
76 | // | ||
77 | // #[test] | ||
78 | // fn test_name() {} | ||
79 | // } | ||
80 | // ``` | ||
81 | |||
82 | /// Main entry point for completion. We run completion as a two-phase process. | ||
83 | /// | ||
84 | /// First, we look at the position and collect a so-called `CompletionContext. | ||
85 | /// This is a somewhat messy process, because, during completion, syntax tree is | ||
86 | /// incomplete and can look really weird. | ||
87 | /// | ||
88 | /// Once the context is collected, we run a series of completion routines which | ||
89 | /// look at the context and produce completion items. One subtlety about this | ||
90 | /// phase is that completion engine should not filter by the substring which is | ||
91 | /// already present, it should give all possible variants for the identifier at | ||
92 | /// the caret. In other words, for | ||
93 | /// | ||
94 | /// ```no-run | ||
95 | /// fn f() { | ||
96 | /// let foo = 92; | ||
97 | /// let _ = bar<|> | ||
98 | /// } | ||
99 | /// ``` | ||
100 | /// | ||
101 | /// `foo` *should* be present among the completion variants. Filtering by | ||
102 | /// identifier prefix/fuzzy match should be done higher in the stack, together | ||
103 | /// with ordering of completions (currently this is done by the client). | ||
104 | pub(crate) fn completions( | ||
105 | db: &RootDatabase, | ||
106 | config: &CompletionConfig, | ||
107 | position: FilePosition, | ||
108 | ) -> Option<Completions> { | ||
109 | let ctx = CompletionContext::new(db, position, config)?; | ||
110 | |||
111 | let mut acc = Completions::default(); | ||
112 | complete_attribute::complete_attribute(&mut acc, &ctx); | ||
113 | complete_fn_param::complete_fn_param(&mut acc, &ctx); | ||
114 | complete_keyword::complete_expr_keyword(&mut acc, &ctx); | ||
115 | complete_keyword::complete_use_tree_keyword(&mut acc, &ctx); | ||
116 | complete_snippet::complete_expr_snippet(&mut acc, &ctx); | ||
117 | complete_snippet::complete_item_snippet(&mut acc, &ctx); | ||
118 | complete_qualified_path::complete_qualified_path(&mut acc, &ctx); | ||
119 | complete_unqualified_path::complete_unqualified_path(&mut acc, &ctx); | ||
120 | complete_dot::complete_dot(&mut acc, &ctx); | ||
121 | complete_record::complete_record(&mut acc, &ctx); | ||
122 | complete_pattern::complete_pattern(&mut acc, &ctx); | ||
123 | complete_postfix::complete_postfix(&mut acc, &ctx); | ||
124 | complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx); | ||
125 | complete_trait_impl::complete_trait_impl(&mut acc, &ctx); | ||
126 | |||
127 | Some(acc) | ||
128 | } | ||
129 | |||
130 | #[cfg(test)] | ||
131 | mod tests { | ||
132 | use crate::completion::completion_config::CompletionConfig; | ||
133 | use crate::mock_analysis::analysis_and_position; | ||
134 | |||
135 | struct DetailAndDocumentation<'a> { | ||
136 | detail: &'a str, | ||
137 | documentation: &'a str, | ||
138 | } | ||
139 | |||
140 | fn check_detail_and_documentation(ra_fixture: &str, expected: DetailAndDocumentation) { | ||
141 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
142 | let config = CompletionConfig::default(); | ||
143 | let completions = analysis.completions(&config, position).unwrap().unwrap(); | ||
144 | for item in completions { | ||
145 | if item.detail() == Some(expected.detail) { | ||
146 | let opt = item.documentation(); | ||
147 | let doc = opt.as_ref().map(|it| it.as_str()); | ||
148 | assert_eq!(doc, Some(expected.documentation)); | ||
149 | return; | ||
150 | } | ||
151 | } | ||
152 | panic!("completion detail not found: {}", expected.detail) | ||
153 | } | ||
154 | |||
155 | #[test] | ||
156 | fn test_completion_detail_from_macro_generated_struct_fn_doc_attr() { | ||
157 | check_detail_and_documentation( | ||
158 | r#" | ||
159 | //- /lib.rs | ||
160 | macro_rules! bar { | ||
161 | () => { | ||
162 | struct Bar; | ||
163 | impl Bar { | ||
164 | #[doc = "Do the foo"] | ||
165 | fn foo(&self) {} | ||
166 | } | ||
167 | } | ||
168 | } | ||
169 | |||
170 | bar!(); | ||
171 | |||
172 | fn foo() { | ||
173 | let bar = Bar; | ||
174 | bar.fo<|>; | ||
175 | } | ||
176 | "#, | ||
177 | DetailAndDocumentation { detail: "fn foo(&self)", documentation: "Do the foo" }, | ||
178 | ); | ||
179 | } | ||
180 | |||
181 | #[test] | ||
182 | fn test_completion_detail_from_macro_generated_struct_fn_doc_comment() { | ||
183 | check_detail_and_documentation( | ||
184 | r#" | ||
185 | //- /lib.rs | ||
186 | macro_rules! bar { | ||
187 | () => { | ||
188 | struct Bar; | ||
189 | impl Bar { | ||
190 | /// Do the foo | ||
191 | fn foo(&self) {} | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | bar!(); | ||
197 | |||
198 | fn foo() { | ||
199 | let bar = Bar; | ||
200 | bar.fo<|>; | ||
201 | } | ||
202 | "#, | ||
203 | DetailAndDocumentation { detail: "fn foo(&self)", documentation: " Do the foo" }, | ||
204 | ); | ||
205 | } | ||
206 | } | ||
diff --git a/crates/ide/src/completion/complete_attribute.rs b/crates/ide/src/completion/complete_attribute.rs new file mode 100644 index 000000000..603d935de --- /dev/null +++ b/crates/ide/src/completion/complete_attribute.rs | |||
@@ -0,0 +1,644 @@ | |||
1 | //! Completion for attributes | ||
2 | //! | ||
3 | //! This module uses a bit of static metadata to provide completions | ||
4 | //! for built-in attributes. | ||
5 | |||
6 | use rustc_hash::FxHashSet; | ||
7 | use syntax::{ast, AstNode, SyntaxKind}; | ||
8 | |||
9 | use crate::completion::{ | ||
10 | completion_context::CompletionContext, | ||
11 | completion_item::{CompletionItem, CompletionItemKind, CompletionKind, Completions}, | ||
12 | }; | ||
13 | |||
14 | pub(super) fn complete_attribute(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> { | ||
15 | let attribute = ctx.attribute_under_caret.as_ref()?; | ||
16 | match (attribute.path(), attribute.token_tree()) { | ||
17 | (Some(path), Some(token_tree)) if path.to_string() == "derive" => { | ||
18 | complete_derive(acc, ctx, token_tree) | ||
19 | } | ||
20 | (Some(path), Some(token_tree)) | ||
21 | if ["allow", "warn", "deny", "forbid"] | ||
22 | .iter() | ||
23 | .any(|lint_level| lint_level == &path.to_string()) => | ||
24 | { | ||
25 | complete_lint(acc, ctx, token_tree) | ||
26 | } | ||
27 | (_, Some(_token_tree)) => {} | ||
28 | _ => complete_attribute_start(acc, ctx, attribute), | ||
29 | } | ||
30 | Some(()) | ||
31 | } | ||
32 | |||
33 | fn complete_attribute_start(acc: &mut Completions, ctx: &CompletionContext, attribute: &ast::Attr) { | ||
34 | for attr_completion in ATTRIBUTES { | ||
35 | let mut item = CompletionItem::new( | ||
36 | CompletionKind::Attribute, | ||
37 | ctx.source_range(), | ||
38 | attr_completion.label, | ||
39 | ) | ||
40 | .kind(CompletionItemKind::Attribute); | ||
41 | |||
42 | if let Some(lookup) = attr_completion.lookup { | ||
43 | item = item.lookup_by(lookup); | ||
44 | } | ||
45 | |||
46 | match (attr_completion.snippet, ctx.config.snippet_cap) { | ||
47 | (Some(snippet), Some(cap)) => { | ||
48 | item = item.insert_snippet(cap, snippet); | ||
49 | } | ||
50 | _ => {} | ||
51 | } | ||
52 | |||
53 | if attribute.kind() == ast::AttrKind::Inner || !attr_completion.prefer_inner { | ||
54 | acc.add(item); | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | struct AttrCompletion { | ||
60 | label: &'static str, | ||
61 | lookup: Option<&'static str>, | ||
62 | snippet: Option<&'static str>, | ||
63 | prefer_inner: bool, | ||
64 | } | ||
65 | |||
66 | impl AttrCompletion { | ||
67 | const fn prefer_inner(self) -> AttrCompletion { | ||
68 | AttrCompletion { prefer_inner: true, ..self } | ||
69 | } | ||
70 | } | ||
71 | |||
72 | const fn attr( | ||
73 | label: &'static str, | ||
74 | lookup: Option<&'static str>, | ||
75 | snippet: Option<&'static str>, | ||
76 | ) -> AttrCompletion { | ||
77 | AttrCompletion { label, lookup, snippet, prefer_inner: false } | ||
78 | } | ||
79 | |||
80 | const ATTRIBUTES: &[AttrCompletion] = &[ | ||
81 | attr("allow(…)", Some("allow"), Some("allow(${0:lint})")), | ||
82 | attr("cfg_attr(…)", Some("cfg_attr"), Some("cfg_attr(${1:predicate}, ${0:attr})")), | ||
83 | attr("cfg(…)", Some("cfg"), Some("cfg(${0:predicate})")), | ||
84 | attr("deny(…)", Some("deny"), Some("deny(${0:lint})")), | ||
85 | attr(r#"deprecated = "…""#, Some("deprecated"), Some(r#"deprecated = "${0:reason}""#)), | ||
86 | attr("derive(…)", Some("derive"), Some(r#"derive(${0:Debug})"#)), | ||
87 | attr(r#"doc = "…""#, Some("doc"), Some(r#"doc = "${0:docs}""#)), | ||
88 | attr("feature(…)", Some("feature"), Some("feature(${0:flag})")).prefer_inner(), | ||
89 | attr("forbid(…)", Some("forbid"), Some("forbid(${0:lint})")), | ||
90 | // FIXME: resolve through macro resolution? | ||
91 | attr("global_allocator", None, None).prefer_inner(), | ||
92 | attr(r#"ignore = "…""#, Some("ignore"), Some(r#"ignore = "${0:reason}""#)), | ||
93 | attr("inline(…)", Some("inline"), Some("inline(${0:lint})")), | ||
94 | attr(r#"link_name = "…""#, Some("link_name"), Some(r#"link_name = "${0:symbol_name}""#)), | ||
95 | attr("link", None, None), | ||
96 | attr("macro_export", None, None), | ||
97 | attr("macro_use", None, None), | ||
98 | attr(r#"must_use = "…""#, Some("must_use"), Some(r#"must_use = "${0:reason}""#)), | ||
99 | attr("no_mangle", None, None), | ||
100 | attr("no_std", None, None).prefer_inner(), | ||
101 | attr("non_exhaustive", None, None), | ||
102 | attr("panic_handler", None, None).prefer_inner(), | ||
103 | attr("path = \"…\"", Some("path"), Some("path =\"${0:path}\"")), | ||
104 | attr("proc_macro", None, None), | ||
105 | attr("proc_macro_attribute", None, None), | ||
106 | attr("proc_macro_derive(…)", Some("proc_macro_derive"), Some("proc_macro_derive(${0:Trait})")), | ||
107 | attr("recursion_limit = …", Some("recursion_limit"), Some("recursion_limit = ${0:128}")) | ||
108 | .prefer_inner(), | ||
109 | attr("repr(…)", Some("repr"), Some("repr(${0:C})")), | ||
110 | attr( | ||
111 | "should_panic(…)", | ||
112 | Some("should_panic"), | ||
113 | Some(r#"should_panic(expected = "${0:reason}")"#), | ||
114 | ), | ||
115 | attr( | ||
116 | r#"target_feature = "…""#, | ||
117 | Some("target_feature"), | ||
118 | Some("target_feature = \"${0:feature}\""), | ||
119 | ), | ||
120 | attr("test", None, None), | ||
121 | attr("used", None, None), | ||
122 | attr("warn(…)", Some("warn"), Some("warn(${0:lint})")), | ||
123 | attr( | ||
124 | r#"windows_subsystem = "…""#, | ||
125 | Some("windows_subsystem"), | ||
126 | Some(r#"windows_subsystem = "${0:subsystem}""#), | ||
127 | ) | ||
128 | .prefer_inner(), | ||
129 | ]; | ||
130 | |||
131 | fn complete_derive(acc: &mut Completions, ctx: &CompletionContext, derive_input: ast::TokenTree) { | ||
132 | if let Ok(existing_derives) = parse_comma_sep_input(derive_input) { | ||
133 | for derive_completion in DEFAULT_DERIVE_COMPLETIONS | ||
134 | .into_iter() | ||
135 | .filter(|completion| !existing_derives.contains(completion.label)) | ||
136 | { | ||
137 | let mut label = derive_completion.label.to_owned(); | ||
138 | for dependency in derive_completion | ||
139 | .dependencies | ||
140 | .into_iter() | ||
141 | .filter(|&&dependency| !existing_derives.contains(dependency)) | ||
142 | { | ||
143 | label.push_str(", "); | ||
144 | label.push_str(dependency); | ||
145 | } | ||
146 | acc.add( | ||
147 | CompletionItem::new(CompletionKind::Attribute, ctx.source_range(), label) | ||
148 | .kind(CompletionItemKind::Attribute), | ||
149 | ); | ||
150 | } | ||
151 | |||
152 | for custom_derive_name in get_derive_names_in_scope(ctx).difference(&existing_derives) { | ||
153 | acc.add( | ||
154 | CompletionItem::new( | ||
155 | CompletionKind::Attribute, | ||
156 | ctx.source_range(), | ||
157 | custom_derive_name, | ||
158 | ) | ||
159 | .kind(CompletionItemKind::Attribute), | ||
160 | ); | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | |||
165 | fn complete_lint(acc: &mut Completions, ctx: &CompletionContext, derive_input: ast::TokenTree) { | ||
166 | if let Ok(existing_lints) = parse_comma_sep_input(derive_input) { | ||
167 | for lint_completion in DEFAULT_LINT_COMPLETIONS | ||
168 | .into_iter() | ||
169 | .filter(|completion| !existing_lints.contains(completion.label)) | ||
170 | { | ||
171 | acc.add( | ||
172 | CompletionItem::new( | ||
173 | CompletionKind::Attribute, | ||
174 | ctx.source_range(), | ||
175 | lint_completion.label, | ||
176 | ) | ||
177 | .kind(CompletionItemKind::Attribute) | ||
178 | .detail(lint_completion.description), | ||
179 | ); | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | |||
184 | fn parse_comma_sep_input(derive_input: ast::TokenTree) -> Result<FxHashSet<String>, ()> { | ||
185 | match (derive_input.left_delimiter_token(), derive_input.right_delimiter_token()) { | ||
186 | (Some(left_paren), Some(right_paren)) | ||
187 | if left_paren.kind() == SyntaxKind::L_PAREN | ||
188 | && right_paren.kind() == SyntaxKind::R_PAREN => | ||
189 | { | ||
190 | let mut input_derives = FxHashSet::default(); | ||
191 | let mut current_derive = String::new(); | ||
192 | for token in derive_input | ||
193 | .syntax() | ||
194 | .children_with_tokens() | ||
195 | .filter_map(|token| token.into_token()) | ||
196 | .skip_while(|token| token != &left_paren) | ||
197 | .skip(1) | ||
198 | .take_while(|token| token != &right_paren) | ||
199 | { | ||
200 | if SyntaxKind::COMMA == token.kind() { | ||
201 | if !current_derive.is_empty() { | ||
202 | input_derives.insert(current_derive); | ||
203 | current_derive = String::new(); | ||
204 | } | ||
205 | } else { | ||
206 | current_derive.push_str(token.to_string().trim()); | ||
207 | } | ||
208 | } | ||
209 | |||
210 | if !current_derive.is_empty() { | ||
211 | input_derives.insert(current_derive); | ||
212 | } | ||
213 | Ok(input_derives) | ||
214 | } | ||
215 | _ => Err(()), | ||
216 | } | ||
217 | } | ||
218 | |||
219 | fn get_derive_names_in_scope(ctx: &CompletionContext) -> FxHashSet<String> { | ||
220 | let mut result = FxHashSet::default(); | ||
221 | ctx.scope.process_all_names(&mut |name, scope_def| { | ||
222 | if let hir::ScopeDef::MacroDef(mac) = scope_def { | ||
223 | if mac.is_derive_macro() { | ||
224 | result.insert(name.to_string()); | ||
225 | } | ||
226 | } | ||
227 | }); | ||
228 | result | ||
229 | } | ||
230 | |||
231 | struct DeriveCompletion { | ||
232 | label: &'static str, | ||
233 | dependencies: &'static [&'static str], | ||
234 | } | ||
235 | |||
236 | /// Standard Rust derives and the information about their dependencies | ||
237 | /// (the dependencies are needed so that the main derive don't break the compilation when added) | ||
238 | #[rustfmt::skip] | ||
239 | const DEFAULT_DERIVE_COMPLETIONS: &[DeriveCompletion] = &[ | ||
240 | DeriveCompletion { label: "Clone", dependencies: &[] }, | ||
241 | DeriveCompletion { label: "Copy", dependencies: &["Clone"] }, | ||
242 | DeriveCompletion { label: "Debug", dependencies: &[] }, | ||
243 | DeriveCompletion { label: "Default", dependencies: &[] }, | ||
244 | DeriveCompletion { label: "Hash", dependencies: &[] }, | ||
245 | DeriveCompletion { label: "PartialEq", dependencies: &[] }, | ||
246 | DeriveCompletion { label: "Eq", dependencies: &["PartialEq"] }, | ||
247 | DeriveCompletion { label: "PartialOrd", dependencies: &["PartialEq"] }, | ||
248 | DeriveCompletion { label: "Ord", dependencies: &["PartialOrd", "Eq", "PartialEq"] }, | ||
249 | ]; | ||
250 | |||
251 | struct LintCompletion { | ||
252 | label: &'static str, | ||
253 | description: &'static str, | ||
254 | } | ||
255 | |||
256 | #[rustfmt::skip] | ||
257 | const DEFAULT_LINT_COMPLETIONS: &[LintCompletion] = &[ | ||
258 | LintCompletion { label: "absolute_paths_not_starting_with_crate", description: r#"fully qualified paths that start with a module name instead of `crate`, `self`, or an extern crate name"# }, | ||
259 | LintCompletion { label: "anonymous_parameters", description: r#"detects anonymous parameters"# }, | ||
260 | LintCompletion { label: "box_pointers", description: r#"use of owned (Box type) heap memory"# }, | ||
261 | LintCompletion { label: "deprecated_in_future", description: r#"detects use of items that will be deprecated in a future version"# }, | ||
262 | LintCompletion { label: "elided_lifetimes_in_paths", description: r#"hidden lifetime parameters in types are deprecated"# }, | ||
263 | LintCompletion { label: "explicit_outlives_requirements", description: r#"outlives requirements can be inferred"# }, | ||
264 | LintCompletion { label: "indirect_structural_match", description: r#"pattern with const indirectly referencing non-structural-match type"# }, | ||
265 | LintCompletion { label: "keyword_idents", description: r#"detects edition keywords being used as an identifier"# }, | ||
266 | LintCompletion { label: "macro_use_extern_crate", description: r#"the `#[macro_use]` attribute is now deprecated in favor of using macros via the module system"# }, | ||
267 | LintCompletion { label: "meta_variable_misuse", description: r#"possible meta-variable misuse at macro definition"# }, | ||
268 | LintCompletion { label: "missing_copy_implementations", description: r#"detects potentially-forgotten implementations of `Copy`"# }, | ||
269 | LintCompletion { label: "missing_crate_level_docs", description: r#"detects crates with no crate-level documentation"# }, | ||
270 | LintCompletion { label: "missing_debug_implementations", description: r#"detects missing implementations of Debug"# }, | ||
271 | LintCompletion { label: "missing_docs", description: r#"detects missing documentation for public members"# }, | ||
272 | LintCompletion { label: "missing_doc_code_examples", description: r#"detects publicly-exported items without code samples in their documentation"# }, | ||
273 | LintCompletion { label: "non_ascii_idents", description: r#"detects non-ASCII identifiers"# }, | ||
274 | LintCompletion { label: "private_doc_tests", description: r#"detects code samples in docs of private items not documented by rustdoc"# }, | ||
275 | LintCompletion { label: "single_use_lifetimes", description: r#"detects lifetime parameters that are only used once"# }, | ||
276 | LintCompletion { label: "trivial_casts", description: r#"detects trivial casts which could be removed"# }, | ||
277 | LintCompletion { label: "trivial_numeric_casts", description: r#"detects trivial casts of numeric types which could be removed"# }, | ||
278 | LintCompletion { label: "unaligned_references", description: r#"detects unaligned references to fields of packed structs"# }, | ||
279 | LintCompletion { label: "unreachable_pub", description: r#"`pub` items not reachable from crate root"# }, | ||
280 | LintCompletion { label: "unsafe_code", description: r#"usage of `unsafe` code"# }, | ||
281 | LintCompletion { label: "unsafe_op_in_unsafe_fn", description: r#"unsafe operations in unsafe functions without an explicit unsafe block are deprecated"# }, | ||
282 | LintCompletion { label: "unstable_features", description: r#"enabling unstable features (deprecated. do not use)"# }, | ||
283 | LintCompletion { label: "unused_crate_dependencies", description: r#"crate dependencies that are never used"# }, | ||
284 | LintCompletion { label: "unused_extern_crates", description: r#"extern crates that are never used"# }, | ||
285 | LintCompletion { label: "unused_import_braces", description: r#"unnecessary braces around an imported item"# }, | ||
286 | LintCompletion { label: "unused_lifetimes", description: r#"detects lifetime parameters that are never used"# }, | ||
287 | LintCompletion { label: "unused_qualifications", description: r#"detects unnecessarily qualified names"# }, | ||
288 | LintCompletion { label: "unused_results", description: r#"unused result of an expression in a statement"# }, | ||
289 | LintCompletion { label: "variant_size_differences", description: r#"detects enums with widely varying variant sizes"# }, | ||
290 | LintCompletion { label: "array_into_iter", description: r#"detects calling `into_iter` on arrays"# }, | ||
291 | LintCompletion { label: "asm_sub_register", description: r#"using only a subset of a register for inline asm inputs"# }, | ||
292 | LintCompletion { label: "bare_trait_objects", description: r#"suggest using `dyn Trait` for trait objects"# }, | ||
293 | LintCompletion { label: "bindings_with_variant_name", description: r#"detects pattern bindings with the same name as one of the matched variants"# }, | ||
294 | LintCompletion { label: "cenum_impl_drop_cast", description: r#"a C-like enum implementing Drop is cast"# }, | ||
295 | LintCompletion { label: "clashing_extern_declarations", description: r#"detects when an extern fn has been declared with the same name but different types"# }, | ||
296 | LintCompletion { label: "coherence_leak_check", description: r#"distinct impls distinguished only by the leak-check code"# }, | ||
297 | LintCompletion { label: "confusable_idents", description: r#"detects visually confusable pairs between identifiers"# }, | ||
298 | LintCompletion { label: "dead_code", description: r#"detect unused, unexported items"# }, | ||
299 | LintCompletion { label: "deprecated", description: r#"detects use of deprecated items"# }, | ||
300 | LintCompletion { label: "ellipsis_inclusive_range_patterns", description: r#"`...` range patterns are deprecated"# }, | ||
301 | LintCompletion { label: "exported_private_dependencies", description: r#"public interface leaks type from a private dependency"# }, | ||
302 | LintCompletion { label: "illegal_floating_point_literal_pattern", description: r#"floating-point literals cannot be used in patterns"# }, | ||
303 | LintCompletion { label: "improper_ctypes", description: r#"proper use of libc types in foreign modules"# }, | ||
304 | LintCompletion { label: "improper_ctypes_definitions", description: r#"proper use of libc types in foreign item definitions"# }, | ||
305 | LintCompletion { label: "incomplete_features", description: r#"incomplete features that may function improperly in some or all cases"# }, | ||
306 | LintCompletion { label: "inline_no_sanitize", description: r#"detects incompatible use of `#[inline(always)]` and `#[no_sanitize(...)]`"# }, | ||
307 | LintCompletion { label: "intra_doc_link_resolution_failure", description: r#"failures in resolving intra-doc link targets"# }, | ||
308 | LintCompletion { label: "invalid_codeblock_attributes", description: r#"codeblock attribute looks a lot like a known one"# }, | ||
309 | LintCompletion { label: "invalid_value", description: r#"an invalid value is being created (such as a NULL reference)"# }, | ||
310 | LintCompletion { label: "irrefutable_let_patterns", description: r#"detects irrefutable patterns in if-let and while-let statements"# }, | ||
311 | LintCompletion { label: "late_bound_lifetime_arguments", description: r#"detects generic lifetime arguments in path segments with late bound lifetime parameters"# }, | ||
312 | LintCompletion { label: "mixed_script_confusables", description: r#"detects Unicode scripts whose mixed script confusables codepoints are solely used"# }, | ||
313 | LintCompletion { label: "mutable_borrow_reservation_conflict", description: r#"reservation of a two-phased borrow conflicts with other shared borrows"# }, | ||
314 | LintCompletion { label: "non_camel_case_types", description: r#"types, variants, traits and type parameters should have camel case names"# }, | ||
315 | LintCompletion { label: "non_shorthand_field_patterns", description: r#"using `Struct { x: x }` instead of `Struct { x }` in a pattern"# }, | ||
316 | LintCompletion { label: "non_snake_case", description: r#"variables, methods, functions, lifetime parameters and modules should have snake case names"# }, | ||
317 | LintCompletion { label: "non_upper_case_globals", description: r#"static constants should have uppercase identifiers"# }, | ||
318 | LintCompletion { label: "no_mangle_generic_items", description: r#"generic items must be mangled"# }, | ||
319 | LintCompletion { label: "overlapping_patterns", description: r#"detects overlapping patterns"# }, | ||
320 | LintCompletion { label: "path_statements", description: r#"path statements with no effect"# }, | ||
321 | LintCompletion { label: "private_in_public", description: r#"detect private items in public interfaces not caught by the old implementation"# }, | ||
322 | LintCompletion { label: "proc_macro_derive_resolution_fallback", description: r#"detects proc macro derives using inaccessible names from parent modules"# }, | ||
323 | LintCompletion { label: "redundant_semicolons", description: r#"detects unnecessary trailing semicolons"# }, | ||
324 | LintCompletion { label: "renamed_and_removed_lints", description: r#"lints that have been renamed or removed"# }, | ||
325 | LintCompletion { label: "safe_packed_borrows", description: r#"safe borrows of fields of packed structs were erroneously allowed"# }, | ||
326 | LintCompletion { label: "stable_features", description: r#"stable features found in `#[feature]` directive"# }, | ||
327 | LintCompletion { label: "trivial_bounds", description: r#"these bounds don't depend on an type parameters"# }, | ||
328 | LintCompletion { label: "type_alias_bounds", description: r#"bounds in type aliases are not enforced"# }, | ||
329 | LintCompletion { label: "tyvar_behind_raw_pointer", description: r#"raw pointer to an inference variable"# }, | ||
330 | LintCompletion { label: "uncommon_codepoints", description: r#"detects uncommon Unicode codepoints in identifiers"# }, | ||
331 | LintCompletion { label: "unconditional_recursion", description: r#"functions that cannot return without calling themselves"# }, | ||
332 | LintCompletion { label: "unknown_lints", description: r#"unrecognized lint attribute"# }, | ||
333 | LintCompletion { label: "unnameable_test_items", description: r#"detects an item that cannot be named being marked as `#[test_case]`"# }, | ||
334 | LintCompletion { label: "unreachable_code", description: r#"detects unreachable code paths"# }, | ||
335 | LintCompletion { label: "unreachable_patterns", description: r#"detects unreachable patterns"# }, | ||
336 | LintCompletion { label: "unstable_name_collisions", description: r#"detects name collision with an existing but unstable method"# }, | ||
337 | LintCompletion { label: "unused_allocation", description: r#"detects unnecessary allocations that can be eliminated"# }, | ||
338 | LintCompletion { label: "unused_assignments", description: r#"detect assignments that will never be read"# }, | ||
339 | LintCompletion { label: "unused_attributes", description: r#"detects attributes that were not used by the compiler"# }, | ||
340 | LintCompletion { label: "unused_braces", description: r#"unnecessary braces around an expression"# }, | ||
341 | LintCompletion { label: "unused_comparisons", description: r#"comparisons made useless by limits of the types involved"# }, | ||
342 | LintCompletion { label: "unused_doc_comments", description: r#"detects doc comments that aren't used by rustdoc"# }, | ||
343 | LintCompletion { label: "unused_features", description: r#"unused features found in crate-level `#[feature]` directives"# }, | ||
344 | LintCompletion { label: "unused_imports", description: r#"imports that are never used"# }, | ||
345 | LintCompletion { label: "unused_labels", description: r#"detects labels that are never used"# }, | ||
346 | LintCompletion { label: "unused_macros", description: r#"detects macros that were not used"# }, | ||
347 | LintCompletion { label: "unused_must_use", description: r#"unused result of a type flagged as `#[must_use]`"# }, | ||
348 | LintCompletion { label: "unused_mut", description: r#"detect mut variables which don't need to be mutable"# }, | ||
349 | LintCompletion { label: "unused_parens", description: r#"`if`, `match`, `while` and `return` do not need parentheses"# }, | ||
350 | LintCompletion { label: "unused_unsafe", description: r#"unnecessary use of an `unsafe` block"# }, | ||
351 | LintCompletion { label: "unused_variables", description: r#"detect variables which are not used in any way"# }, | ||
352 | LintCompletion { label: "warnings", description: r#"mass-change the level for lints which produce warnings"# }, | ||
353 | LintCompletion { label: "where_clauses_object_safety", description: r#"checks the object safety of where clauses"# }, | ||
354 | LintCompletion { label: "while_true", description: r#"suggest using `loop { }` instead of `while true { }`"# }, | ||
355 | LintCompletion { label: "ambiguous_associated_items", description: r#"ambiguous associated items"# }, | ||
356 | LintCompletion { label: "arithmetic_overflow", description: r#"arithmetic operation overflows"# }, | ||
357 | LintCompletion { label: "conflicting_repr_hints", description: r#"conflicts between `#[repr(..)]` hints that were previously accepted and used in practice"# }, | ||
358 | LintCompletion { label: "const_err", description: r#"constant evaluation detected erroneous expression"# }, | ||
359 | LintCompletion { label: "ill_formed_attribute_input", description: r#"ill-formed attribute inputs that were previously accepted and used in practice"# }, | ||
360 | LintCompletion { label: "incomplete_include", description: r#"trailing content in included file"# }, | ||
361 | LintCompletion { label: "invalid_type_param_default", description: r#"type parameter default erroneously allowed in invalid location"# }, | ||
362 | LintCompletion { label: "macro_expanded_macro_exports_accessed_by_absolute_paths", description: r#"macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths"# }, | ||
363 | LintCompletion { label: "missing_fragment_specifier", description: r#"detects missing fragment specifiers in unused `macro_rules!` patterns"# }, | ||
364 | LintCompletion { label: "mutable_transmutes", description: r#"mutating transmuted &mut T from &T may cause undefined behavior"# }, | ||
365 | LintCompletion { label: "no_mangle_const_items", description: r#"const items will not have their symbols exported"# }, | ||
366 | LintCompletion { label: "order_dependent_trait_objects", description: r#"trait-object types were treated as different depending on marker-trait order"# }, | ||
367 | LintCompletion { label: "overflowing_literals", description: r#"literal out of range for its type"# }, | ||
368 | LintCompletion { label: "patterns_in_fns_without_body", description: r#"patterns in functions without body were erroneously allowed"# }, | ||
369 | LintCompletion { label: "pub_use_of_private_extern_crate", description: r#"detect public re-exports of private extern crates"# }, | ||
370 | LintCompletion { label: "soft_unstable", description: r#"a feature gate that doesn't break dependent crates"# }, | ||
371 | LintCompletion { label: "unconditional_panic", description: r#"operation will cause a panic at runtime"# }, | ||
372 | LintCompletion { label: "unknown_crate_types", description: r#"unknown crate type found in `#[crate_type]` directive"# }, | ||
373 | ]; | ||
374 | |||
375 | #[cfg(test)] | ||
376 | mod tests { | ||
377 | use expect::{expect, Expect}; | ||
378 | |||
379 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
380 | |||
381 | fn check(ra_fixture: &str, expect: Expect) { | ||
382 | let actual = completion_list(ra_fixture, CompletionKind::Attribute); | ||
383 | expect.assert_eq(&actual); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn empty_derive_completion() { | ||
388 | check( | ||
389 | r#" | ||
390 | #[derive(<|>)] | ||
391 | struct Test {} | ||
392 | "#, | ||
393 | expect![[r#" | ||
394 | at Clone | ||
395 | at Copy, Clone | ||
396 | at Debug | ||
397 | at Default | ||
398 | at Eq, PartialEq | ||
399 | at Hash | ||
400 | at Ord, PartialOrd, Eq, PartialEq | ||
401 | at PartialEq | ||
402 | at PartialOrd, PartialEq | ||
403 | "#]], | ||
404 | ); | ||
405 | } | ||
406 | |||
407 | #[test] | ||
408 | fn empty_lint_completion() { | ||
409 | check( | ||
410 | r#"#[allow(<|>)]"#, | ||
411 | expect![[r#" | ||
412 | at absolute_paths_not_starting_with_crate fully qualified paths that start with a module name instead of `crate`, `self`, or an extern crate name | ||
413 | at ambiguous_associated_items ambiguous associated items | ||
414 | at anonymous_parameters detects anonymous parameters | ||
415 | at arithmetic_overflow arithmetic operation overflows | ||
416 | at array_into_iter detects calling `into_iter` on arrays | ||
417 | at asm_sub_register using only a subset of a register for inline asm inputs | ||
418 | at bare_trait_objects suggest using `dyn Trait` for trait objects | ||
419 | at bindings_with_variant_name detects pattern bindings with the same name as one of the matched variants | ||
420 | at box_pointers use of owned (Box type) heap memory | ||
421 | at cenum_impl_drop_cast a C-like enum implementing Drop is cast | ||
422 | at clashing_extern_declarations detects when an extern fn has been declared with the same name but different types | ||
423 | at coherence_leak_check distinct impls distinguished only by the leak-check code | ||
424 | at conflicting_repr_hints conflicts between `#[repr(..)]` hints that were previously accepted and used in practice | ||
425 | at confusable_idents detects visually confusable pairs between identifiers | ||
426 | at const_err constant evaluation detected erroneous expression | ||
427 | at dead_code detect unused, unexported items | ||
428 | at deprecated detects use of deprecated items | ||
429 | at deprecated_in_future detects use of items that will be deprecated in a future version | ||
430 | at elided_lifetimes_in_paths hidden lifetime parameters in types are deprecated | ||
431 | at ellipsis_inclusive_range_patterns `...` range patterns are deprecated | ||
432 | at explicit_outlives_requirements outlives requirements can be inferred | ||
433 | at exported_private_dependencies public interface leaks type from a private dependency | ||
434 | at ill_formed_attribute_input ill-formed attribute inputs that were previously accepted and used in practice | ||
435 | at illegal_floating_point_literal_pattern floating-point literals cannot be used in patterns | ||
436 | at improper_ctypes proper use of libc types in foreign modules | ||
437 | at improper_ctypes_definitions proper use of libc types in foreign item definitions | ||
438 | at incomplete_features incomplete features that may function improperly in some or all cases | ||
439 | at incomplete_include trailing content in included file | ||
440 | at indirect_structural_match pattern with const indirectly referencing non-structural-match type | ||
441 | at inline_no_sanitize detects incompatible use of `#[inline(always)]` and `#[no_sanitize(...)]` | ||
442 | at intra_doc_link_resolution_failure failures in resolving intra-doc link targets | ||
443 | at invalid_codeblock_attributes codeblock attribute looks a lot like a known one | ||
444 | at invalid_type_param_default type parameter default erroneously allowed in invalid location | ||
445 | at invalid_value an invalid value is being created (such as a NULL reference) | ||
446 | at irrefutable_let_patterns detects irrefutable patterns in if-let and while-let statements | ||
447 | at keyword_idents detects edition keywords being used as an identifier | ||
448 | at late_bound_lifetime_arguments detects generic lifetime arguments in path segments with late bound lifetime parameters | ||
449 | at macro_expanded_macro_exports_accessed_by_absolute_paths macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths | ||
450 | at macro_use_extern_crate the `#[macro_use]` attribute is now deprecated in favor of using macros via the module system | ||
451 | at meta_variable_misuse possible meta-variable misuse at macro definition | ||
452 | at missing_copy_implementations detects potentially-forgotten implementations of `Copy` | ||
453 | at missing_crate_level_docs detects crates with no crate-level documentation | ||
454 | at missing_debug_implementations detects missing implementations of Debug | ||
455 | at missing_doc_code_examples detects publicly-exported items without code samples in their documentation | ||
456 | at missing_docs detects missing documentation for public members | ||
457 | at missing_fragment_specifier detects missing fragment specifiers in unused `macro_rules!` patterns | ||
458 | at mixed_script_confusables detects Unicode scripts whose mixed script confusables codepoints are solely used | ||
459 | at mutable_borrow_reservation_conflict reservation of a two-phased borrow conflicts with other shared borrows | ||
460 | at mutable_transmutes mutating transmuted &mut T from &T may cause undefined behavior | ||
461 | at no_mangle_const_items const items will not have their symbols exported | ||
462 | at no_mangle_generic_items generic items must be mangled | ||
463 | at non_ascii_idents detects non-ASCII identifiers | ||
464 | at non_camel_case_types types, variants, traits and type parameters should have camel case names | ||
465 | at non_shorthand_field_patterns using `Struct { x: x }` instead of `Struct { x }` in a pattern | ||
466 | at non_snake_case variables, methods, functions, lifetime parameters and modules should have snake case names | ||
467 | at non_upper_case_globals static constants should have uppercase identifiers | ||
468 | at order_dependent_trait_objects trait-object types were treated as different depending on marker-trait order | ||
469 | at overflowing_literals literal out of range for its type | ||
470 | at overlapping_patterns detects overlapping patterns | ||
471 | at path_statements path statements with no effect | ||
472 | at patterns_in_fns_without_body patterns in functions without body were erroneously allowed | ||
473 | at private_doc_tests detects code samples in docs of private items not documented by rustdoc | ||
474 | at private_in_public detect private items in public interfaces not caught by the old implementation | ||
475 | at proc_macro_derive_resolution_fallback detects proc macro derives using inaccessible names from parent modules | ||
476 | at pub_use_of_private_extern_crate detect public re-exports of private extern crates | ||
477 | at redundant_semicolons detects unnecessary trailing semicolons | ||
478 | at renamed_and_removed_lints lints that have been renamed or removed | ||
479 | at safe_packed_borrows safe borrows of fields of packed structs were erroneously allowed | ||
480 | at single_use_lifetimes detects lifetime parameters that are only used once | ||
481 | at soft_unstable a feature gate that doesn't break dependent crates | ||
482 | at stable_features stable features found in `#[feature]` directive | ||
483 | at trivial_bounds these bounds don't depend on an type parameters | ||
484 | at trivial_casts detects trivial casts which could be removed | ||
485 | at trivial_numeric_casts detects trivial casts of numeric types which could be removed | ||
486 | at type_alias_bounds bounds in type aliases are not enforced | ||
487 | at tyvar_behind_raw_pointer raw pointer to an inference variable | ||
488 | at unaligned_references detects unaligned references to fields of packed structs | ||
489 | at uncommon_codepoints detects uncommon Unicode codepoints in identifiers | ||
490 | at unconditional_panic operation will cause a panic at runtime | ||
491 | at unconditional_recursion functions that cannot return without calling themselves | ||
492 | at unknown_crate_types unknown crate type found in `#[crate_type]` directive | ||
493 | at unknown_lints unrecognized lint attribute | ||
494 | at unnameable_test_items detects an item that cannot be named being marked as `#[test_case]` | ||
495 | at unreachable_code detects unreachable code paths | ||
496 | at unreachable_patterns detects unreachable patterns | ||
497 | at unreachable_pub `pub` items not reachable from crate root | ||
498 | at unsafe_code usage of `unsafe` code | ||
499 | at unsafe_op_in_unsafe_fn unsafe operations in unsafe functions without an explicit unsafe block are deprecated | ||
500 | at unstable_features enabling unstable features (deprecated. do not use) | ||
501 | at unstable_name_collisions detects name collision with an existing but unstable method | ||
502 | at unused_allocation detects unnecessary allocations that can be eliminated | ||
503 | at unused_assignments detect assignments that will never be read | ||
504 | at unused_attributes detects attributes that were not used by the compiler | ||
505 | at unused_braces unnecessary braces around an expression | ||
506 | at unused_comparisons comparisons made useless by limits of the types involved | ||
507 | at unused_crate_dependencies crate dependencies that are never used | ||
508 | at unused_doc_comments detects doc comments that aren't used by rustdoc | ||
509 | at unused_extern_crates extern crates that are never used | ||
510 | at unused_features unused features found in crate-level `#[feature]` directives | ||
511 | at unused_import_braces unnecessary braces around an imported item | ||
512 | at unused_imports imports that are never used | ||
513 | at unused_labels detects labels that are never used | ||
514 | at unused_lifetimes detects lifetime parameters that are never used | ||
515 | at unused_macros detects macros that were not used | ||
516 | at unused_must_use unused result of a type flagged as `#[must_use]` | ||
517 | at unused_mut detect mut variables which don't need to be mutable | ||
518 | at unused_parens `if`, `match`, `while` and `return` do not need parentheses | ||
519 | at unused_qualifications detects unnecessarily qualified names | ||
520 | at unused_results unused result of an expression in a statement | ||
521 | at unused_unsafe unnecessary use of an `unsafe` block | ||
522 | at unused_variables detect variables which are not used in any way | ||
523 | at variant_size_differences detects enums with widely varying variant sizes | ||
524 | at warnings mass-change the level for lints which produce warnings | ||
525 | at where_clauses_object_safety checks the object safety of where clauses | ||
526 | at while_true suggest using `loop { }` instead of `while true { }` | ||
527 | "#]], | ||
528 | ) | ||
529 | } | ||
530 | |||
531 | #[test] | ||
532 | fn no_completion_for_incorrect_derive() { | ||
533 | check( | ||
534 | r#" | ||
535 | #[derive{<|>)] | ||
536 | struct Test {} | ||
537 | "#, | ||
538 | expect![[r#""#]], | ||
539 | ) | ||
540 | } | ||
541 | |||
542 | #[test] | ||
543 | fn derive_with_input_completion() { | ||
544 | check( | ||
545 | r#" | ||
546 | #[derive(serde::Serialize, PartialEq, <|>)] | ||
547 | struct Test {} | ||
548 | "#, | ||
549 | expect![[r#" | ||
550 | at Clone | ||
551 | at Copy, Clone | ||
552 | at Debug | ||
553 | at Default | ||
554 | at Eq | ||
555 | at Hash | ||
556 | at Ord, PartialOrd, Eq | ||
557 | at PartialOrd | ||
558 | "#]], | ||
559 | ) | ||
560 | } | ||
561 | |||
562 | #[test] | ||
563 | fn test_attribute_completion() { | ||
564 | check( | ||
565 | r#"#[<|>]"#, | ||
566 | expect![[r#" | ||
567 | at allow(…) | ||
568 | at cfg(…) | ||
569 | at cfg_attr(…) | ||
570 | at deny(…) | ||
571 | at deprecated = "…" | ||
572 | at derive(…) | ||
573 | at doc = "…" | ||
574 | at forbid(…) | ||
575 | at ignore = "…" | ||
576 | at inline(…) | ||
577 | at link | ||
578 | at link_name = "…" | ||
579 | at macro_export | ||
580 | at macro_use | ||
581 | at must_use = "…" | ||
582 | at no_mangle | ||
583 | at non_exhaustive | ||
584 | at path = "…" | ||
585 | at proc_macro | ||
586 | at proc_macro_attribute | ||
587 | at proc_macro_derive(…) | ||
588 | at repr(…) | ||
589 | at should_panic(…) | ||
590 | at target_feature = "…" | ||
591 | at test | ||
592 | at used | ||
593 | at warn(…) | ||
594 | "#]], | ||
595 | ) | ||
596 | } | ||
597 | |||
598 | #[test] | ||
599 | fn test_attribute_completion_inside_nested_attr() { | ||
600 | check(r#"#[cfg(<|>)]"#, expect![[]]) | ||
601 | } | ||
602 | |||
603 | #[test] | ||
604 | fn test_inner_attribute_completion() { | ||
605 | check( | ||
606 | r"#![<|>]", | ||
607 | expect![[r#" | ||
608 | at allow(…) | ||
609 | at cfg(…) | ||
610 | at cfg_attr(…) | ||
611 | at deny(…) | ||
612 | at deprecated = "…" | ||
613 | at derive(…) | ||
614 | at doc = "…" | ||
615 | at feature(…) | ||
616 | at forbid(…) | ||
617 | at global_allocator | ||
618 | at ignore = "…" | ||
619 | at inline(…) | ||
620 | at link | ||
621 | at link_name = "…" | ||
622 | at macro_export | ||
623 | at macro_use | ||
624 | at must_use = "…" | ||
625 | at no_mangle | ||
626 | at no_std | ||
627 | at non_exhaustive | ||
628 | at panic_handler | ||
629 | at path = "…" | ||
630 | at proc_macro | ||
631 | at proc_macro_attribute | ||
632 | at proc_macro_derive(…) | ||
633 | at recursion_limit = … | ||
634 | at repr(…) | ||
635 | at should_panic(…) | ||
636 | at target_feature = "…" | ||
637 | at test | ||
638 | at used | ||
639 | at warn(…) | ||
640 | at windows_subsystem = "…" | ||
641 | "#]], | ||
642 | ); | ||
643 | } | ||
644 | } | ||
diff --git a/crates/ide/src/completion/complete_dot.rs b/crates/ide/src/completion/complete_dot.rs new file mode 100644 index 000000000..532665285 --- /dev/null +++ b/crates/ide/src/completion/complete_dot.rs | |||
@@ -0,0 +1,416 @@ | |||
1 | //! Completes references after dot (fields and method calls). | ||
2 | |||
3 | use hir::{HasVisibility, Type}; | ||
4 | use rustc_hash::FxHashSet; | ||
5 | use test_utils::mark; | ||
6 | |||
7 | use crate::completion::{completion_context::CompletionContext, completion_item::Completions}; | ||
8 | |||
9 | /// Complete dot accesses, i.e. fields or methods. | ||
10 | pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) { | ||
11 | let dot_receiver = match &ctx.dot_receiver { | ||
12 | Some(expr) => expr, | ||
13 | _ => return, | ||
14 | }; | ||
15 | |||
16 | let receiver_ty = match ctx.sema.type_of_expr(&dot_receiver) { | ||
17 | Some(ty) => ty, | ||
18 | _ => return, | ||
19 | }; | ||
20 | |||
21 | if ctx.is_call { | ||
22 | mark::hit!(test_no_struct_field_completion_for_method_call); | ||
23 | } else { | ||
24 | complete_fields(acc, ctx, &receiver_ty); | ||
25 | } | ||
26 | complete_methods(acc, ctx, &receiver_ty); | ||
27 | } | ||
28 | |||
29 | fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) { | ||
30 | for receiver in receiver.autoderef(ctx.db) { | ||
31 | for (field, ty) in receiver.fields(ctx.db) { | ||
32 | if ctx.scope.module().map_or(false, |m| !field.is_visible_from(ctx.db, m)) { | ||
33 | // Skip private field. FIXME: If the definition location of the | ||
34 | // field is editable, we should show the completion | ||
35 | continue; | ||
36 | } | ||
37 | acc.add_field(ctx, field, &ty); | ||
38 | } | ||
39 | for (i, ty) in receiver.tuple_fields(ctx.db).into_iter().enumerate() { | ||
40 | // FIXME: Handle visibility | ||
41 | acc.add_tuple_field(ctx, i, &ty); | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | |||
46 | fn complete_methods(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) { | ||
47 | if let Some(krate) = ctx.krate { | ||
48 | let mut seen_methods = FxHashSet::default(); | ||
49 | let traits_in_scope = ctx.scope.traits_in_scope(); | ||
50 | receiver.iterate_method_candidates(ctx.db, krate, &traits_in_scope, None, |_ty, func| { | ||
51 | if func.has_self_param(ctx.db) | ||
52 | && ctx.scope.module().map_or(true, |m| func.is_visible_from(ctx.db, m)) | ||
53 | && seen_methods.insert(func.name(ctx.db)) | ||
54 | { | ||
55 | acc.add_function(ctx, func, None); | ||
56 | } | ||
57 | None::<()> | ||
58 | }); | ||
59 | } | ||
60 | } | ||
61 | |||
62 | #[cfg(test)] | ||
63 | mod tests { | ||
64 | use expect::{expect, Expect}; | ||
65 | use test_utils::mark; | ||
66 | |||
67 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
68 | |||
69 | fn check(ra_fixture: &str, expect: Expect) { | ||
70 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
71 | expect.assert_eq(&actual); | ||
72 | } | ||
73 | |||
74 | #[test] | ||
75 | fn test_struct_field_and_method_completion() { | ||
76 | check( | ||
77 | r#" | ||
78 | struct S { foo: u32 } | ||
79 | impl S { | ||
80 | fn bar(&self) {} | ||
81 | } | ||
82 | fn foo(s: S) { s.<|> } | ||
83 | "#, | ||
84 | expect![[r#" | ||
85 | me bar() fn bar(&self) | ||
86 | fd foo u32 | ||
87 | "#]], | ||
88 | ); | ||
89 | } | ||
90 | |||
91 | #[test] | ||
92 | fn test_struct_field_completion_self() { | ||
93 | check( | ||
94 | r#" | ||
95 | struct S { the_field: (u32,) } | ||
96 | impl S { | ||
97 | fn foo(self) { self.<|> } | ||
98 | } | ||
99 | "#, | ||
100 | expect![[r#" | ||
101 | me foo() fn foo(self) | ||
102 | fd the_field (u32,) | ||
103 | "#]], | ||
104 | ) | ||
105 | } | ||
106 | |||
107 | #[test] | ||
108 | fn test_struct_field_completion_autoderef() { | ||
109 | check( | ||
110 | r#" | ||
111 | struct A { the_field: (u32, i32) } | ||
112 | impl A { | ||
113 | fn foo(&self) { self.<|> } | ||
114 | } | ||
115 | "#, | ||
116 | expect![[r#" | ||
117 | me foo() fn foo(&self) | ||
118 | fd the_field (u32, i32) | ||
119 | "#]], | ||
120 | ) | ||
121 | } | ||
122 | |||
123 | #[test] | ||
124 | fn test_no_struct_field_completion_for_method_call() { | ||
125 | mark::check!(test_no_struct_field_completion_for_method_call); | ||
126 | check( | ||
127 | r#" | ||
128 | struct A { the_field: u32 } | ||
129 | fn foo(a: A) { a.<|>() } | ||
130 | "#, | ||
131 | expect![[""]], | ||
132 | ); | ||
133 | } | ||
134 | |||
135 | #[test] | ||
136 | fn test_visibility_filtering() { | ||
137 | check( | ||
138 | r#" | ||
139 | mod inner { | ||
140 | pub struct A { | ||
141 | private_field: u32, | ||
142 | pub pub_field: u32, | ||
143 | pub(crate) crate_field: u32, | ||
144 | pub(super) super_field: u32, | ||
145 | } | ||
146 | } | ||
147 | fn foo(a: inner::A) { a.<|> } | ||
148 | "#, | ||
149 | expect![[r#" | ||
150 | fd crate_field u32 | ||
151 | fd pub_field u32 | ||
152 | fd super_field u32 | ||
153 | "#]], | ||
154 | ); | ||
155 | |||
156 | check( | ||
157 | r#" | ||
158 | struct A {} | ||
159 | mod m { | ||
160 | impl super::A { | ||
161 | fn private_method(&self) {} | ||
162 | pub(super) fn the_method(&self) {} | ||
163 | } | ||
164 | } | ||
165 | fn foo(a: A) { a.<|> } | ||
166 | "#, | ||
167 | expect![[r#" | ||
168 | me the_method() pub(super) fn the_method(&self) | ||
169 | "#]], | ||
170 | ); | ||
171 | } | ||
172 | |||
173 | #[test] | ||
174 | fn test_union_field_completion() { | ||
175 | check( | ||
176 | r#" | ||
177 | union U { field: u8, other: u16 } | ||
178 | fn foo(u: U) { u.<|> } | ||
179 | "#, | ||
180 | expect![[r#" | ||
181 | fd field u8 | ||
182 | fd other u16 | ||
183 | "#]], | ||
184 | ); | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn test_method_completion_only_fitting_impls() { | ||
189 | check( | ||
190 | r#" | ||
191 | struct A<T> {} | ||
192 | impl A<u32> { | ||
193 | fn the_method(&self) {} | ||
194 | } | ||
195 | impl A<i32> { | ||
196 | fn the_other_method(&self) {} | ||
197 | } | ||
198 | fn foo(a: A<u32>) { a.<|> } | ||
199 | "#, | ||
200 | expect![[r#" | ||
201 | me the_method() fn the_method(&self) | ||
202 | "#]], | ||
203 | ) | ||
204 | } | ||
205 | |||
206 | #[test] | ||
207 | fn test_trait_method_completion() { | ||
208 | check( | ||
209 | r#" | ||
210 | struct A {} | ||
211 | trait Trait { fn the_method(&self); } | ||
212 | impl Trait for A {} | ||
213 | fn foo(a: A) { a.<|> } | ||
214 | "#, | ||
215 | expect![[r#" | ||
216 | me the_method() fn the_method(&self) | ||
217 | "#]], | ||
218 | ); | ||
219 | } | ||
220 | |||
221 | #[test] | ||
222 | fn test_trait_method_completion_deduplicated() { | ||
223 | check( | ||
224 | r" | ||
225 | struct A {} | ||
226 | trait Trait { fn the_method(&self); } | ||
227 | impl<T> Trait for T {} | ||
228 | fn foo(a: &A) { a.<|> } | ||
229 | ", | ||
230 | expect![[r#" | ||
231 | me the_method() fn the_method(&self) | ||
232 | "#]], | ||
233 | ); | ||
234 | } | ||
235 | |||
236 | #[test] | ||
237 | fn completes_trait_method_from_other_module() { | ||
238 | check( | ||
239 | r" | ||
240 | struct A {} | ||
241 | mod m { | ||
242 | pub trait Trait { fn the_method(&self); } | ||
243 | } | ||
244 | use m::Trait; | ||
245 | impl Trait for A {} | ||
246 | fn foo(a: A) { a.<|> } | ||
247 | ", | ||
248 | expect![[r#" | ||
249 | me the_method() fn the_method(&self) | ||
250 | "#]], | ||
251 | ); | ||
252 | } | ||
253 | |||
254 | #[test] | ||
255 | fn test_no_non_self_method() { | ||
256 | check( | ||
257 | r#" | ||
258 | struct A {} | ||
259 | impl A { | ||
260 | fn the_method() {} | ||
261 | } | ||
262 | fn foo(a: A) { | ||
263 | a.<|> | ||
264 | } | ||
265 | "#, | ||
266 | expect![[""]], | ||
267 | ); | ||
268 | } | ||
269 | |||
270 | #[test] | ||
271 | fn test_tuple_field_completion() { | ||
272 | check( | ||
273 | r#" | ||
274 | fn foo() { | ||
275 | let b = (0, 3.14); | ||
276 | b.<|> | ||
277 | } | ||
278 | "#, | ||
279 | expect![[r#" | ||
280 | fd 0 i32 | ||
281 | fd 1 f64 | ||
282 | "#]], | ||
283 | ) | ||
284 | } | ||
285 | |||
286 | #[test] | ||
287 | fn test_tuple_field_inference() { | ||
288 | check( | ||
289 | r#" | ||
290 | pub struct S; | ||
291 | impl S { pub fn blah(&self) {} } | ||
292 | |||
293 | struct T(S); | ||
294 | |||
295 | impl T { | ||
296 | fn foo(&self) { | ||
297 | // FIXME: This doesn't work without the trailing `a` as `0.` is a float | ||
298 | self.0.a<|> | ||
299 | } | ||
300 | } | ||
301 | "#, | ||
302 | expect![[r#" | ||
303 | me blah() pub fn blah(&self) | ||
304 | "#]], | ||
305 | ); | ||
306 | } | ||
307 | |||
308 | #[test] | ||
309 | fn test_completion_works_in_consts() { | ||
310 | check( | ||
311 | r#" | ||
312 | struct A { the_field: u32 } | ||
313 | const X: u32 = { | ||
314 | A { the_field: 92 }.<|> | ||
315 | }; | ||
316 | "#, | ||
317 | expect![[r#" | ||
318 | fd the_field u32 | ||
319 | "#]], | ||
320 | ); | ||
321 | } | ||
322 | |||
323 | #[test] | ||
324 | fn works_in_simple_macro_1() { | ||
325 | check( | ||
326 | r#" | ||
327 | macro_rules! m { ($e:expr) => { $e } } | ||
328 | struct A { the_field: u32 } | ||
329 | fn foo(a: A) { | ||
330 | m!(a.x<|>) | ||
331 | } | ||
332 | "#, | ||
333 | expect![[r#" | ||
334 | fd the_field u32 | ||
335 | "#]], | ||
336 | ); | ||
337 | } | ||
338 | |||
339 | #[test] | ||
340 | fn works_in_simple_macro_2() { | ||
341 | // this doesn't work yet because the macro doesn't expand without the token -- maybe it can be fixed with better recovery | ||
342 | check( | ||
343 | r#" | ||
344 | macro_rules! m { ($e:expr) => { $e } } | ||
345 | struct A { the_field: u32 } | ||
346 | fn foo(a: A) { | ||
347 | m!(a.<|>) | ||
348 | } | ||
349 | "#, | ||
350 | expect![[r#" | ||
351 | fd the_field u32 | ||
352 | "#]], | ||
353 | ); | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn works_in_simple_macro_recursive_1() { | ||
358 | check( | ||
359 | r#" | ||
360 | macro_rules! m { ($e:expr) => { $e } } | ||
361 | struct A { the_field: u32 } | ||
362 | fn foo(a: A) { | ||
363 | m!(m!(m!(a.x<|>))) | ||
364 | } | ||
365 | "#, | ||
366 | expect![[r#" | ||
367 | fd the_field u32 | ||
368 | "#]], | ||
369 | ); | ||
370 | } | ||
371 | |||
372 | #[test] | ||
373 | fn macro_expansion_resilient() { | ||
374 | check( | ||
375 | r#" | ||
376 | macro_rules! dbg { | ||
377 | () => {}; | ||
378 | ($val:expr) => { | ||
379 | match $val { tmp => { tmp } } | ||
380 | }; | ||
381 | // Trailing comma with single argument is ignored | ||
382 | ($val:expr,) => { $crate::dbg!($val) }; | ||
383 | ($($val:expr),+ $(,)?) => { | ||
384 | ($($crate::dbg!($val)),+,) | ||
385 | }; | ||
386 | } | ||
387 | struct A { the_field: u32 } | ||
388 | fn foo(a: A) { | ||
389 | dbg!(a.<|>) | ||
390 | } | ||
391 | "#, | ||
392 | expect![[r#" | ||
393 | fd the_field u32 | ||
394 | "#]], | ||
395 | ); | ||
396 | } | ||
397 | |||
398 | #[test] | ||
399 | fn test_method_completion_issue_3547() { | ||
400 | check( | ||
401 | r#" | ||
402 | struct HashSet<T> {} | ||
403 | impl<T> HashSet<T> { | ||
404 | pub fn the_method(&self) {} | ||
405 | } | ||
406 | fn foo() { | ||
407 | let s: HashSet<_>; | ||
408 | s.<|> | ||
409 | } | ||
410 | "#, | ||
411 | expect![[r#" | ||
412 | me the_method() pub fn the_method(&self) | ||
413 | "#]], | ||
414 | ); | ||
415 | } | ||
416 | } | ||
diff --git a/crates/ide/src/completion/complete_fn_param.rs b/crates/ide/src/completion/complete_fn_param.rs new file mode 100644 index 000000000..7c63ce58f --- /dev/null +++ b/crates/ide/src/completion/complete_fn_param.rs | |||
@@ -0,0 +1,135 @@ | |||
1 | //! See `complete_fn_param`. | ||
2 | |||
3 | use rustc_hash::FxHashMap; | ||
4 | use syntax::{ | ||
5 | ast::{self, ModuleItemOwner}, | ||
6 | match_ast, AstNode, | ||
7 | }; | ||
8 | |||
9 | use crate::completion::{CompletionContext, CompletionItem, CompletionKind, Completions}; | ||
10 | |||
11 | /// Complete repeated parameters, both name and type. For example, if all | ||
12 | /// functions in a file have a `spam: &mut Spam` parameter, a completion with | ||
13 | /// `spam: &mut Spam` insert text/label and `spam` lookup string will be | ||
14 | /// suggested. | ||
15 | pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { | ||
16 | if !ctx.is_param { | ||
17 | return; | ||
18 | } | ||
19 | |||
20 | let mut params = FxHashMap::default(); | ||
21 | |||
22 | let me = ctx.token.ancestors().find_map(ast::Fn::cast); | ||
23 | let mut process_fn = |func: ast::Fn| { | ||
24 | if Some(&func) == me.as_ref() { | ||
25 | return; | ||
26 | } | ||
27 | func.param_list().into_iter().flat_map(|it| it.params()).for_each(|param| { | ||
28 | let text = param.syntax().text().to_string(); | ||
29 | params.entry(text).or_insert(param); | ||
30 | }) | ||
31 | }; | ||
32 | |||
33 | for node in ctx.token.parent().ancestors() { | ||
34 | match_ast! { | ||
35 | match node { | ||
36 | ast::SourceFile(it) => it.items().filter_map(|item| match item { | ||
37 | ast::Item::Fn(it) => Some(it), | ||
38 | _ => None, | ||
39 | }).for_each(&mut process_fn), | ||
40 | ast::ItemList(it) => it.items().filter_map(|item| match item { | ||
41 | ast::Item::Fn(it) => Some(it), | ||
42 | _ => None, | ||
43 | }).for_each(&mut process_fn), | ||
44 | ast::AssocItemList(it) => it.assoc_items().filter_map(|item| match item { | ||
45 | ast::AssocItem::Fn(it) => Some(it), | ||
46 | _ => None, | ||
47 | }).for_each(&mut process_fn), | ||
48 | _ => continue, | ||
49 | } | ||
50 | }; | ||
51 | } | ||
52 | |||
53 | params | ||
54 | .into_iter() | ||
55 | .filter_map(|(label, param)| { | ||
56 | let lookup = param.pat()?.syntax().text().to_string(); | ||
57 | Some((label, lookup)) | ||
58 | }) | ||
59 | .for_each(|(label, lookup)| { | ||
60 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), label) | ||
61 | .kind(crate::CompletionItemKind::Binding) | ||
62 | .lookup_by(lookup) | ||
63 | .add_to(acc) | ||
64 | }); | ||
65 | } | ||
66 | |||
67 | #[cfg(test)] | ||
68 | mod tests { | ||
69 | use expect::{expect, Expect}; | ||
70 | |||
71 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
72 | |||
73 | fn check(ra_fixture: &str, expect: Expect) { | ||
74 | let actual = completion_list(ra_fixture, CompletionKind::Magic); | ||
75 | expect.assert_eq(&actual); | ||
76 | } | ||
77 | |||
78 | #[test] | ||
79 | fn test_param_completion_last_param() { | ||
80 | check( | ||
81 | r#" | ||
82 | fn foo(file_id: FileId) {} | ||
83 | fn bar(file_id: FileId) {} | ||
84 | fn baz(file<|>) {} | ||
85 | "#, | ||
86 | expect![[r#" | ||
87 | bn file_id: FileId | ||
88 | "#]], | ||
89 | ); | ||
90 | } | ||
91 | |||
92 | #[test] | ||
93 | fn test_param_completion_nth_param() { | ||
94 | check( | ||
95 | r#" | ||
96 | fn foo(file_id: FileId) {} | ||
97 | fn baz(file<|>, x: i32) {} | ||
98 | "#, | ||
99 | expect![[r#" | ||
100 | bn file_id: FileId | ||
101 | "#]], | ||
102 | ); | ||
103 | } | ||
104 | |||
105 | #[test] | ||
106 | fn test_param_completion_trait_param() { | ||
107 | check( | ||
108 | r#" | ||
109 | pub(crate) trait SourceRoot { | ||
110 | pub fn contains(&self, file_id: FileId) -> bool; | ||
111 | pub fn module_map(&self) -> &ModuleMap; | ||
112 | pub fn lines(&self, file_id: FileId) -> &LineIndex; | ||
113 | pub fn syntax(&self, file<|>) | ||
114 | } | ||
115 | "#, | ||
116 | expect![[r#" | ||
117 | bn file_id: FileId | ||
118 | "#]], | ||
119 | ); | ||
120 | } | ||
121 | |||
122 | #[test] | ||
123 | fn completes_param_in_inner_function() { | ||
124 | check( | ||
125 | r#" | ||
126 | fn outer(text: String) { | ||
127 | fn inner(<|>) | ||
128 | } | ||
129 | "#, | ||
130 | expect![[r#" | ||
131 | bn text: String | ||
132 | "#]], | ||
133 | ) | ||
134 | } | ||
135 | } | ||
diff --git a/crates/ide/src/completion/complete_keyword.rs b/crates/ide/src/completion/complete_keyword.rs new file mode 100644 index 000000000..a80708935 --- /dev/null +++ b/crates/ide/src/completion/complete_keyword.rs | |||
@@ -0,0 +1,536 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use syntax::{ast, SyntaxKind}; | ||
4 | use test_utils::mark; | ||
5 | |||
6 | use crate::completion::{ | ||
7 | CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, | ||
8 | }; | ||
9 | |||
10 | pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { | ||
11 | // complete keyword "crate" in use stmt | ||
12 | let source_range = ctx.source_range(); | ||
13 | match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { | ||
14 | (Some(_), None) => { | ||
15 | CompletionItem::new(CompletionKind::Keyword, source_range, "crate::") | ||
16 | .kind(CompletionItemKind::Keyword) | ||
17 | .insert_text("crate::") | ||
18 | .add_to(acc); | ||
19 | CompletionItem::new(CompletionKind::Keyword, source_range, "self") | ||
20 | .kind(CompletionItemKind::Keyword) | ||
21 | .add_to(acc); | ||
22 | CompletionItem::new(CompletionKind::Keyword, source_range, "super::") | ||
23 | .kind(CompletionItemKind::Keyword) | ||
24 | .insert_text("super::") | ||
25 | .add_to(acc); | ||
26 | } | ||
27 | (Some(_), Some(_)) => { | ||
28 | CompletionItem::new(CompletionKind::Keyword, source_range, "self") | ||
29 | .kind(CompletionItemKind::Keyword) | ||
30 | .add_to(acc); | ||
31 | CompletionItem::new(CompletionKind::Keyword, source_range, "super::") | ||
32 | .kind(CompletionItemKind::Keyword) | ||
33 | .insert_text("super::") | ||
34 | .add_to(acc); | ||
35 | } | ||
36 | _ => {} | ||
37 | } | ||
38 | |||
39 | // Suggest .await syntax for types that implement Future trait | ||
40 | if let Some(receiver) = &ctx.dot_receiver { | ||
41 | if let Some(ty) = ctx.sema.type_of_expr(receiver) { | ||
42 | if ty.impls_future(ctx.db) { | ||
43 | CompletionItem::new(CompletionKind::Keyword, ctx.source_range(), "await") | ||
44 | .kind(CompletionItemKind::Keyword) | ||
45 | .detail("expr.await") | ||
46 | .insert_text("await") | ||
47 | .add_to(acc); | ||
48 | } | ||
49 | }; | ||
50 | } | ||
51 | } | ||
52 | |||
53 | pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { | ||
54 | if ctx.token.kind() == SyntaxKind::COMMENT { | ||
55 | mark::hit!(no_keyword_completion_in_comments); | ||
56 | return; | ||
57 | } | ||
58 | |||
59 | let has_trait_or_impl_parent = ctx.has_impl_parent || ctx.has_trait_parent; | ||
60 | if ctx.trait_as_prev_sibling || ctx.impl_as_prev_sibling { | ||
61 | add_keyword(ctx, acc, "where", "where "); | ||
62 | return; | ||
63 | } | ||
64 | if ctx.unsafe_is_prev { | ||
65 | if ctx.has_item_list_or_source_file_parent || ctx.block_expr_parent { | ||
66 | add_keyword(ctx, acc, "fn", "fn $0() {}") | ||
67 | } | ||
68 | |||
69 | if (ctx.has_item_list_or_source_file_parent) || ctx.block_expr_parent { | ||
70 | add_keyword(ctx, acc, "trait", "trait $0 {}"); | ||
71 | add_keyword(ctx, acc, "impl", "impl $0 {}"); | ||
72 | } | ||
73 | |||
74 | return; | ||
75 | } | ||
76 | if ctx.has_item_list_or_source_file_parent || has_trait_or_impl_parent || ctx.block_expr_parent | ||
77 | { | ||
78 | add_keyword(ctx, acc, "fn", "fn $0() {}"); | ||
79 | } | ||
80 | if (ctx.has_item_list_or_source_file_parent) || ctx.block_expr_parent { | ||
81 | add_keyword(ctx, acc, "use", "use "); | ||
82 | add_keyword(ctx, acc, "impl", "impl $0 {}"); | ||
83 | add_keyword(ctx, acc, "trait", "trait $0 {}"); | ||
84 | } | ||
85 | |||
86 | if ctx.has_item_list_or_source_file_parent { | ||
87 | add_keyword(ctx, acc, "enum", "enum $0 {}"); | ||
88 | add_keyword(ctx, acc, "struct", "struct $0"); | ||
89 | add_keyword(ctx, acc, "union", "union $0 {}"); | ||
90 | } | ||
91 | |||
92 | if ctx.is_expr { | ||
93 | add_keyword(ctx, acc, "match", "match $0 {}"); | ||
94 | add_keyword(ctx, acc, "while", "while $0 {}"); | ||
95 | add_keyword(ctx, acc, "loop", "loop {$0}"); | ||
96 | add_keyword(ctx, acc, "if", "if "); | ||
97 | add_keyword(ctx, acc, "if let", "if let "); | ||
98 | } | ||
99 | |||
100 | if ctx.if_is_prev || ctx.block_expr_parent { | ||
101 | add_keyword(ctx, acc, "let", "let "); | ||
102 | } | ||
103 | |||
104 | if ctx.after_if { | ||
105 | add_keyword(ctx, acc, "else", "else {$0}"); | ||
106 | add_keyword(ctx, acc, "else if", "else if $0 {}"); | ||
107 | } | ||
108 | if (ctx.has_item_list_or_source_file_parent) || ctx.block_expr_parent { | ||
109 | add_keyword(ctx, acc, "mod", "mod $0 {}"); | ||
110 | } | ||
111 | if ctx.bind_pat_parent || ctx.ref_pat_parent { | ||
112 | add_keyword(ctx, acc, "mut", "mut "); | ||
113 | } | ||
114 | if ctx.has_item_list_or_source_file_parent || has_trait_or_impl_parent || ctx.block_expr_parent | ||
115 | { | ||
116 | add_keyword(ctx, acc, "const", "const "); | ||
117 | add_keyword(ctx, acc, "type", "type "); | ||
118 | } | ||
119 | if (ctx.has_item_list_or_source_file_parent) || ctx.block_expr_parent { | ||
120 | add_keyword(ctx, acc, "static", "static "); | ||
121 | }; | ||
122 | if (ctx.has_item_list_or_source_file_parent) || ctx.block_expr_parent { | ||
123 | add_keyword(ctx, acc, "extern", "extern "); | ||
124 | } | ||
125 | if ctx.has_item_list_or_source_file_parent | ||
126 | || has_trait_or_impl_parent | ||
127 | || ctx.block_expr_parent | ||
128 | || ctx.is_match_arm | ||
129 | { | ||
130 | add_keyword(ctx, acc, "unsafe", "unsafe "); | ||
131 | } | ||
132 | if ctx.in_loop_body { | ||
133 | if ctx.can_be_stmt { | ||
134 | add_keyword(ctx, acc, "continue", "continue;"); | ||
135 | add_keyword(ctx, acc, "break", "break;"); | ||
136 | } else { | ||
137 | add_keyword(ctx, acc, "continue", "continue"); | ||
138 | add_keyword(ctx, acc, "break", "break"); | ||
139 | } | ||
140 | } | ||
141 | if ctx.has_item_list_or_source_file_parent || ctx.has_impl_parent { | ||
142 | add_keyword(ctx, acc, "pub", "pub ") | ||
143 | } | ||
144 | |||
145 | if !ctx.is_trivial_path { | ||
146 | return; | ||
147 | } | ||
148 | let fn_def = match &ctx.function_syntax { | ||
149 | Some(it) => it, | ||
150 | None => return, | ||
151 | }; | ||
152 | acc.add_all(complete_return(ctx, &fn_def, ctx.can_be_stmt)); | ||
153 | } | ||
154 | |||
155 | fn keyword(ctx: &CompletionContext, kw: &str, snippet: &str) -> CompletionItem { | ||
156 | let res = CompletionItem::new(CompletionKind::Keyword, ctx.source_range(), kw) | ||
157 | .kind(CompletionItemKind::Keyword); | ||
158 | |||
159 | match ctx.config.snippet_cap { | ||
160 | Some(cap) => res.insert_snippet(cap, snippet), | ||
161 | _ => res.insert_text(if snippet.contains('$') { kw } else { snippet }), | ||
162 | } | ||
163 | .build() | ||
164 | } | ||
165 | |||
166 | fn add_keyword(ctx: &CompletionContext, acc: &mut Completions, kw: &str, snippet: &str) { | ||
167 | acc.add(keyword(ctx, kw, snippet)); | ||
168 | } | ||
169 | |||
170 | fn complete_return( | ||
171 | ctx: &CompletionContext, | ||
172 | fn_def: &ast::Fn, | ||
173 | can_be_stmt: bool, | ||
174 | ) -> Option<CompletionItem> { | ||
175 | let snip = match (can_be_stmt, fn_def.ret_type().is_some()) { | ||
176 | (true, true) => "return $0;", | ||
177 | (true, false) => "return;", | ||
178 | (false, true) => "return $0", | ||
179 | (false, false) => "return", | ||
180 | }; | ||
181 | Some(keyword(ctx, "return", snip)) | ||
182 | } | ||
183 | |||
184 | #[cfg(test)] | ||
185 | mod tests { | ||
186 | use expect::{expect, Expect}; | ||
187 | |||
188 | use crate::completion::{ | ||
189 | test_utils::{check_edit, completion_list}, | ||
190 | CompletionKind, | ||
191 | }; | ||
192 | use test_utils::mark; | ||
193 | |||
194 | fn check(ra_fixture: &str, expect: Expect) { | ||
195 | let actual = completion_list(ra_fixture, CompletionKind::Keyword); | ||
196 | expect.assert_eq(&actual) | ||
197 | } | ||
198 | |||
199 | #[test] | ||
200 | fn test_keywords_in_use_stmt() { | ||
201 | check( | ||
202 | r"use <|>", | ||
203 | expect![[r#" | ||
204 | kw crate:: | ||
205 | kw self | ||
206 | kw super:: | ||
207 | "#]], | ||
208 | ); | ||
209 | |||
210 | check( | ||
211 | r"use a::<|>", | ||
212 | expect![[r#" | ||
213 | kw self | ||
214 | kw super:: | ||
215 | "#]], | ||
216 | ); | ||
217 | |||
218 | check( | ||
219 | r"use a::{b, <|>}", | ||
220 | expect![[r#" | ||
221 | kw self | ||
222 | kw super:: | ||
223 | "#]], | ||
224 | ); | ||
225 | } | ||
226 | |||
227 | #[test] | ||
228 | fn test_keywords_at_source_file_level() { | ||
229 | check( | ||
230 | r"m<|>", | ||
231 | expect![[r#" | ||
232 | kw const | ||
233 | kw enum | ||
234 | kw extern | ||
235 | kw fn | ||
236 | kw impl | ||
237 | kw mod | ||
238 | kw pub | ||
239 | kw static | ||
240 | kw struct | ||
241 | kw trait | ||
242 | kw type | ||
243 | kw union | ||
244 | kw unsafe | ||
245 | kw use | ||
246 | "#]], | ||
247 | ); | ||
248 | } | ||
249 | |||
250 | #[test] | ||
251 | fn test_keywords_in_function() { | ||
252 | check( | ||
253 | r"fn quux() { <|> }", | ||
254 | expect![[r#" | ||
255 | kw const | ||
256 | kw extern | ||
257 | kw fn | ||
258 | kw if | ||
259 | kw if let | ||
260 | kw impl | ||
261 | kw let | ||
262 | kw loop | ||
263 | kw match | ||
264 | kw mod | ||
265 | kw return | ||
266 | kw static | ||
267 | kw trait | ||
268 | kw type | ||
269 | kw unsafe | ||
270 | kw use | ||
271 | kw while | ||
272 | "#]], | ||
273 | ); | ||
274 | } | ||
275 | |||
276 | #[test] | ||
277 | fn test_keywords_inside_block() { | ||
278 | check( | ||
279 | r"fn quux() { if true { <|> } }", | ||
280 | expect![[r#" | ||
281 | kw const | ||
282 | kw extern | ||
283 | kw fn | ||
284 | kw if | ||
285 | kw if let | ||
286 | kw impl | ||
287 | kw let | ||
288 | kw loop | ||
289 | kw match | ||
290 | kw mod | ||
291 | kw return | ||
292 | kw static | ||
293 | kw trait | ||
294 | kw type | ||
295 | kw unsafe | ||
296 | kw use | ||
297 | kw while | ||
298 | "#]], | ||
299 | ); | ||
300 | } | ||
301 | |||
302 | #[test] | ||
303 | fn test_keywords_after_if() { | ||
304 | check( | ||
305 | r#"fn quux() { if true { () } <|> }"#, | ||
306 | expect![[r#" | ||
307 | kw const | ||
308 | kw else | ||
309 | kw else if | ||
310 | kw extern | ||
311 | kw fn | ||
312 | kw if | ||
313 | kw if let | ||
314 | kw impl | ||
315 | kw let | ||
316 | kw loop | ||
317 | kw match | ||
318 | kw mod | ||
319 | kw return | ||
320 | kw static | ||
321 | kw trait | ||
322 | kw type | ||
323 | kw unsafe | ||
324 | kw use | ||
325 | kw while | ||
326 | "#]], | ||
327 | ); | ||
328 | check_edit( | ||
329 | "else", | ||
330 | r#"fn quux() { if true { () } <|> }"#, | ||
331 | r#"fn quux() { if true { () } else {$0} }"#, | ||
332 | ); | ||
333 | } | ||
334 | |||
335 | #[test] | ||
336 | fn test_keywords_in_match_arm() { | ||
337 | check( | ||
338 | r#" | ||
339 | fn quux() -> i32 { | ||
340 | match () { () => <|> } | ||
341 | } | ||
342 | "#, | ||
343 | expect![[r#" | ||
344 | kw if | ||
345 | kw if let | ||
346 | kw loop | ||
347 | kw match | ||
348 | kw return | ||
349 | kw unsafe | ||
350 | kw while | ||
351 | "#]], | ||
352 | ); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn test_keywords_in_trait_def() { | ||
357 | check( | ||
358 | r"trait My { <|> }", | ||
359 | expect![[r#" | ||
360 | kw const | ||
361 | kw fn | ||
362 | kw type | ||
363 | kw unsafe | ||
364 | "#]], | ||
365 | ); | ||
366 | } | ||
367 | |||
368 | #[test] | ||
369 | fn test_keywords_in_impl_def() { | ||
370 | check( | ||
371 | r"impl My { <|> }", | ||
372 | expect![[r#" | ||
373 | kw const | ||
374 | kw fn | ||
375 | kw pub | ||
376 | kw type | ||
377 | kw unsafe | ||
378 | "#]], | ||
379 | ); | ||
380 | } | ||
381 | |||
382 | #[test] | ||
383 | fn test_keywords_in_loop() { | ||
384 | check( | ||
385 | r"fn my() { loop { <|> } }", | ||
386 | expect![[r#" | ||
387 | kw break | ||
388 | kw const | ||
389 | kw continue | ||
390 | kw extern | ||
391 | kw fn | ||
392 | kw if | ||
393 | kw if let | ||
394 | kw impl | ||
395 | kw let | ||
396 | kw loop | ||
397 | kw match | ||
398 | kw mod | ||
399 | kw return | ||
400 | kw static | ||
401 | kw trait | ||
402 | kw type | ||
403 | kw unsafe | ||
404 | kw use | ||
405 | kw while | ||
406 | "#]], | ||
407 | ); | ||
408 | } | ||
409 | |||
410 | #[test] | ||
411 | fn test_keywords_after_unsafe_in_item_list() { | ||
412 | check( | ||
413 | r"unsafe <|>", | ||
414 | expect![[r#" | ||
415 | kw fn | ||
416 | kw impl | ||
417 | kw trait | ||
418 | "#]], | ||
419 | ); | ||
420 | } | ||
421 | |||
422 | #[test] | ||
423 | fn test_keywords_after_unsafe_in_block_expr() { | ||
424 | check( | ||
425 | r"fn my_fn() { unsafe <|> }", | ||
426 | expect![[r#" | ||
427 | kw fn | ||
428 | kw impl | ||
429 | kw trait | ||
430 | "#]], | ||
431 | ); | ||
432 | } | ||
433 | |||
434 | #[test] | ||
435 | fn test_mut_in_ref_and_in_fn_parameters_list() { | ||
436 | check( | ||
437 | r"fn my_fn(&<|>) {}", | ||
438 | expect![[r#" | ||
439 | kw mut | ||
440 | "#]], | ||
441 | ); | ||
442 | check( | ||
443 | r"fn my_fn(<|>) {}", | ||
444 | expect![[r#" | ||
445 | kw mut | ||
446 | "#]], | ||
447 | ); | ||
448 | check( | ||
449 | r"fn my_fn() { let &<|> }", | ||
450 | expect![[r#" | ||
451 | kw mut | ||
452 | "#]], | ||
453 | ); | ||
454 | } | ||
455 | |||
456 | #[test] | ||
457 | fn test_where_keyword() { | ||
458 | check( | ||
459 | r"trait A <|>", | ||
460 | expect![[r#" | ||
461 | kw where | ||
462 | "#]], | ||
463 | ); | ||
464 | check( | ||
465 | r"impl A <|>", | ||
466 | expect![[r#" | ||
467 | kw where | ||
468 | "#]], | ||
469 | ); | ||
470 | } | ||
471 | |||
472 | #[test] | ||
473 | fn no_keyword_completion_in_comments() { | ||
474 | mark::check!(no_keyword_completion_in_comments); | ||
475 | check( | ||
476 | r#" | ||
477 | fn test() { | ||
478 | let x = 2; // A comment<|> | ||
479 | } | ||
480 | "#, | ||
481 | expect![[""]], | ||
482 | ); | ||
483 | check( | ||
484 | r#" | ||
485 | /* | ||
486 | Some multi-line comment<|> | ||
487 | */ | ||
488 | "#, | ||
489 | expect![[""]], | ||
490 | ); | ||
491 | check( | ||
492 | r#" | ||
493 | /// Some doc comment | ||
494 | /// let test<|> = 1 | ||
495 | "#, | ||
496 | expect![[""]], | ||
497 | ); | ||
498 | } | ||
499 | |||
500 | #[test] | ||
501 | fn test_completion_await_impls_future() { | ||
502 | check( | ||
503 | r#" | ||
504 | //- /main.rs | ||
505 | use std::future::*; | ||
506 | struct A {} | ||
507 | impl Future for A {} | ||
508 | fn foo(a: A) { a.<|> } | ||
509 | |||
510 | //- /std/lib.rs | ||
511 | pub mod future { | ||
512 | #[lang = "future_trait"] | ||
513 | pub trait Future {} | ||
514 | } | ||
515 | "#, | ||
516 | expect![[r#" | ||
517 | kw await expr.await | ||
518 | "#]], | ||
519 | ) | ||
520 | } | ||
521 | |||
522 | #[test] | ||
523 | fn after_let() { | ||
524 | check( | ||
525 | r#"fn main() { let _ = <|> }"#, | ||
526 | expect![[r#" | ||
527 | kw if | ||
528 | kw if let | ||
529 | kw loop | ||
530 | kw match | ||
531 | kw return | ||
532 | kw while | ||
533 | "#]], | ||
534 | ) | ||
535 | } | ||
536 | } | ||
diff --git a/crates/ide/src/completion/complete_macro_in_item_position.rs b/crates/ide/src/completion/complete_macro_in_item_position.rs new file mode 100644 index 000000000..0447f0511 --- /dev/null +++ b/crates/ide/src/completion/complete_macro_in_item_position.rs | |||
@@ -0,0 +1,41 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{CompletionContext, Completions}; | ||
4 | |||
5 | pub(super) fn complete_macro_in_item_position(acc: &mut Completions, ctx: &CompletionContext) { | ||
6 | // Show only macros in top level. | ||
7 | if ctx.is_new_item { | ||
8 | ctx.scope.process_all_names(&mut |name, res| { | ||
9 | if let hir::ScopeDef::MacroDef(mac) = res { | ||
10 | acc.add_macro(ctx, Some(name.to_string()), mac); | ||
11 | } | ||
12 | }) | ||
13 | } | ||
14 | } | ||
15 | |||
16 | #[cfg(test)] | ||
17 | mod tests { | ||
18 | use expect::{expect, Expect}; | ||
19 | |||
20 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
21 | |||
22 | fn check(ra_fixture: &str, expect: Expect) { | ||
23 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
24 | expect.assert_eq(&actual) | ||
25 | } | ||
26 | |||
27 | #[test] | ||
28 | fn completes_macros_as_item() { | ||
29 | check( | ||
30 | r#" | ||
31 | macro_rules! foo { () => {} } | ||
32 | fn foo() {} | ||
33 | |||
34 | <|> | ||
35 | "#, | ||
36 | expect![[r#" | ||
37 | ma foo!(…) macro_rules! foo | ||
38 | "#]], | ||
39 | ) | ||
40 | } | ||
41 | } | ||
diff --git a/crates/ide/src/completion/complete_pattern.rs b/crates/ide/src/completion/complete_pattern.rs new file mode 100644 index 000000000..aceb77cb5 --- /dev/null +++ b/crates/ide/src/completion/complete_pattern.rs | |||
@@ -0,0 +1,88 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{CompletionContext, Completions}; | ||
4 | |||
5 | /// Completes constats and paths in patterns. | ||
6 | pub(super) fn complete_pattern(acc: &mut Completions, ctx: &CompletionContext) { | ||
7 | if !ctx.is_pat_binding_or_const { | ||
8 | return; | ||
9 | } | ||
10 | if ctx.record_pat_syntax.is_some() { | ||
11 | return; | ||
12 | } | ||
13 | |||
14 | // FIXME: ideally, we should look at the type we are matching against and | ||
15 | // suggest variants + auto-imports | ||
16 | ctx.scope.process_all_names(&mut |name, res| { | ||
17 | match &res { | ||
18 | hir::ScopeDef::ModuleDef(def) => match def { | ||
19 | hir::ModuleDef::Adt(hir::Adt::Enum(..)) | ||
20 | | hir::ModuleDef::Adt(hir::Adt::Struct(..)) | ||
21 | | hir::ModuleDef::EnumVariant(..) | ||
22 | | hir::ModuleDef::Const(..) | ||
23 | | hir::ModuleDef::Module(..) => (), | ||
24 | _ => return, | ||
25 | }, | ||
26 | hir::ScopeDef::MacroDef(_) => (), | ||
27 | _ => return, | ||
28 | }; | ||
29 | |||
30 | acc.add_resolution(ctx, name.to_string(), &res) | ||
31 | }); | ||
32 | } | ||
33 | |||
34 | #[cfg(test)] | ||
35 | mod tests { | ||
36 | use expect::{expect, Expect}; | ||
37 | |||
38 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
39 | |||
40 | fn check(ra_fixture: &str, expect: Expect) { | ||
41 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
42 | expect.assert_eq(&actual) | ||
43 | } | ||
44 | |||
45 | #[test] | ||
46 | fn completes_enum_variants_and_modules() { | ||
47 | check( | ||
48 | r#" | ||
49 | enum E { X } | ||
50 | use self::E::X; | ||
51 | const Z: E = E::X; | ||
52 | mod m {} | ||
53 | |||
54 | static FOO: E = E::X; | ||
55 | struct Bar { f: u32 } | ||
56 | |||
57 | fn foo() { | ||
58 | match E::X { <|> } | ||
59 | } | ||
60 | "#, | ||
61 | expect![[r#" | ||
62 | st Bar | ||
63 | en E | ||
64 | ev X () | ||
65 | ct Z | ||
66 | md m | ||
67 | "#]], | ||
68 | ); | ||
69 | } | ||
70 | |||
71 | #[test] | ||
72 | fn completes_in_simple_macro_call() { | ||
73 | check( | ||
74 | r#" | ||
75 | macro_rules! m { ($e:expr) => { $e } } | ||
76 | enum E { X } | ||
77 | |||
78 | fn foo() { | ||
79 | m!(match E::X { <|> }) | ||
80 | } | ||
81 | "#, | ||
82 | expect![[r#" | ||
83 | en E | ||
84 | ma m!(…) macro_rules! m | ||
85 | "#]], | ||
86 | ); | ||
87 | } | ||
88 | } | ||
diff --git a/crates/ide/src/completion/complete_postfix.rs b/crates/ide/src/completion/complete_postfix.rs new file mode 100644 index 000000000..d50b13c52 --- /dev/null +++ b/crates/ide/src/completion/complete_postfix.rs | |||
@@ -0,0 +1,378 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | use assists::utils::TryEnum; | ||
3 | use syntax::{ | ||
4 | ast::{self, AstNode}, | ||
5 | TextRange, TextSize, | ||
6 | }; | ||
7 | use text_edit::TextEdit; | ||
8 | |||
9 | use crate::{ | ||
10 | completion::{ | ||
11 | completion_config::SnippetCap, | ||
12 | completion_context::CompletionContext, | ||
13 | completion_item::{Builder, CompletionKind, Completions}, | ||
14 | }, | ||
15 | CompletionItem, CompletionItemKind, | ||
16 | }; | ||
17 | |||
18 | pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { | ||
19 | if !ctx.config.enable_postfix_completions { | ||
20 | return; | ||
21 | } | ||
22 | |||
23 | let dot_receiver = match &ctx.dot_receiver { | ||
24 | Some(it) => it, | ||
25 | None => return, | ||
26 | }; | ||
27 | |||
28 | let receiver_text = | ||
29 | get_receiver_text(dot_receiver, ctx.dot_receiver_is_ambiguous_float_literal); | ||
30 | |||
31 | let receiver_ty = match ctx.sema.type_of_expr(&dot_receiver) { | ||
32 | Some(it) => it, | ||
33 | None => return, | ||
34 | }; | ||
35 | |||
36 | let cap = match ctx.config.snippet_cap { | ||
37 | Some(it) => it, | ||
38 | None => return, | ||
39 | }; | ||
40 | let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty); | ||
41 | if let Some(try_enum) = &try_enum { | ||
42 | match try_enum { | ||
43 | TryEnum::Result => { | ||
44 | postfix_snippet( | ||
45 | ctx, | ||
46 | cap, | ||
47 | &dot_receiver, | ||
48 | "ifl", | ||
49 | "if let Ok {}", | ||
50 | &format!("if let Ok($1) = {} {{\n $0\n}}", receiver_text), | ||
51 | ) | ||
52 | .add_to(acc); | ||
53 | |||
54 | postfix_snippet( | ||
55 | ctx, | ||
56 | cap, | ||
57 | &dot_receiver, | ||
58 | "while", | ||
59 | "while let Ok {}", | ||
60 | &format!("while let Ok($1) = {} {{\n $0\n}}", receiver_text), | ||
61 | ) | ||
62 | .add_to(acc); | ||
63 | } | ||
64 | TryEnum::Option => { | ||
65 | postfix_snippet( | ||
66 | ctx, | ||
67 | cap, | ||
68 | &dot_receiver, | ||
69 | "ifl", | ||
70 | "if let Some {}", | ||
71 | &format!("if let Some($1) = {} {{\n $0\n}}", receiver_text), | ||
72 | ) | ||
73 | .add_to(acc); | ||
74 | |||
75 | postfix_snippet( | ||
76 | ctx, | ||
77 | cap, | ||
78 | &dot_receiver, | ||
79 | "while", | ||
80 | "while let Some {}", | ||
81 | &format!("while let Some($1) = {} {{\n $0\n}}", receiver_text), | ||
82 | ) | ||
83 | .add_to(acc); | ||
84 | } | ||
85 | } | ||
86 | } else if receiver_ty.is_bool() || receiver_ty.is_unknown() { | ||
87 | postfix_snippet( | ||
88 | ctx, | ||
89 | cap, | ||
90 | &dot_receiver, | ||
91 | "if", | ||
92 | "if expr {}", | ||
93 | &format!("if {} {{\n $0\n}}", receiver_text), | ||
94 | ) | ||
95 | .add_to(acc); | ||
96 | postfix_snippet( | ||
97 | ctx, | ||
98 | cap, | ||
99 | &dot_receiver, | ||
100 | "while", | ||
101 | "while expr {}", | ||
102 | &format!("while {} {{\n $0\n}}", receiver_text), | ||
103 | ) | ||
104 | .add_to(acc); | ||
105 | postfix_snippet(ctx, cap, &dot_receiver, "not", "!expr", &format!("!{}", receiver_text)) | ||
106 | .add_to(acc); | ||
107 | } | ||
108 | |||
109 | postfix_snippet(ctx, cap, &dot_receiver, "ref", "&expr", &format!("&{}", receiver_text)) | ||
110 | .add_to(acc); | ||
111 | postfix_snippet( | ||
112 | ctx, | ||
113 | cap, | ||
114 | &dot_receiver, | ||
115 | "refm", | ||
116 | "&mut expr", | ||
117 | &format!("&mut {}", receiver_text), | ||
118 | ) | ||
119 | .add_to(acc); | ||
120 | |||
121 | // The rest of the postfix completions create an expression that moves an argument, | ||
122 | // so it's better to consider references now to avoid breaking the compilation | ||
123 | let dot_receiver = include_references(dot_receiver); | ||
124 | let receiver_text = | ||
125 | get_receiver_text(&dot_receiver, ctx.dot_receiver_is_ambiguous_float_literal); | ||
126 | |||
127 | match try_enum { | ||
128 | Some(try_enum) => match try_enum { | ||
129 | TryEnum::Result => { | ||
130 | postfix_snippet( | ||
131 | ctx, | ||
132 | cap, | ||
133 | &dot_receiver, | ||
134 | "match", | ||
135 | "match expr {}", | ||
136 | &format!("match {} {{\n Ok(${{1:_}}) => {{$2}},\n Err(${{3:_}}) => {{$0}},\n}}", receiver_text), | ||
137 | ) | ||
138 | .add_to(acc); | ||
139 | } | ||
140 | TryEnum::Option => { | ||
141 | postfix_snippet( | ||
142 | ctx, | ||
143 | cap, | ||
144 | &dot_receiver, | ||
145 | "match", | ||
146 | "match expr {}", | ||
147 | &format!( | ||
148 | "match {} {{\n Some(${{1:_}}) => {{$2}},\n None => {{$0}},\n}}", | ||
149 | receiver_text | ||
150 | ), | ||
151 | ) | ||
152 | .add_to(acc); | ||
153 | } | ||
154 | }, | ||
155 | None => { | ||
156 | postfix_snippet( | ||
157 | ctx, | ||
158 | cap, | ||
159 | &dot_receiver, | ||
160 | "match", | ||
161 | "match expr {}", | ||
162 | &format!("match {} {{\n ${{1:_}} => {{$0}},\n}}", receiver_text), | ||
163 | ) | ||
164 | .add_to(acc); | ||
165 | } | ||
166 | } | ||
167 | |||
168 | postfix_snippet( | ||
169 | ctx, | ||
170 | cap, | ||
171 | &dot_receiver, | ||
172 | "box", | ||
173 | "Box::new(expr)", | ||
174 | &format!("Box::new({})", receiver_text), | ||
175 | ) | ||
176 | .add_to(acc); | ||
177 | |||
178 | postfix_snippet( | ||
179 | ctx, | ||
180 | cap, | ||
181 | &dot_receiver, | ||
182 | "dbg", | ||
183 | "dbg!(expr)", | ||
184 | &format!("dbg!({})", receiver_text), | ||
185 | ) | ||
186 | .add_to(acc); | ||
187 | |||
188 | postfix_snippet( | ||
189 | ctx, | ||
190 | cap, | ||
191 | &dot_receiver, | ||
192 | "call", | ||
193 | "function(expr)", | ||
194 | &format!("${{1}}({})", receiver_text), | ||
195 | ) | ||
196 | .add_to(acc); | ||
197 | } | ||
198 | |||
199 | fn get_receiver_text(receiver: &ast::Expr, receiver_is_ambiguous_float_literal: bool) -> String { | ||
200 | if receiver_is_ambiguous_float_literal { | ||
201 | let text = receiver.syntax().text(); | ||
202 | let without_dot = ..text.len() - TextSize::of('.'); | ||
203 | text.slice(without_dot).to_string() | ||
204 | } else { | ||
205 | receiver.to_string() | ||
206 | } | ||
207 | } | ||
208 | |||
209 | fn include_references(initial_element: &ast::Expr) -> ast::Expr { | ||
210 | let mut resulting_element = initial_element.clone(); | ||
211 | while let Some(parent_ref_element) = | ||
212 | resulting_element.syntax().parent().and_then(ast::RefExpr::cast) | ||
213 | { | ||
214 | resulting_element = ast::Expr::from(parent_ref_element); | ||
215 | } | ||
216 | resulting_element | ||
217 | } | ||
218 | |||
219 | fn postfix_snippet( | ||
220 | ctx: &CompletionContext, | ||
221 | cap: SnippetCap, | ||
222 | receiver: &ast::Expr, | ||
223 | label: &str, | ||
224 | detail: &str, | ||
225 | snippet: &str, | ||
226 | ) -> Builder { | ||
227 | let edit = { | ||
228 | let receiver_syntax = receiver.syntax(); | ||
229 | let receiver_range = ctx.sema.original_range(receiver_syntax).range; | ||
230 | let delete_range = TextRange::new(receiver_range.start(), ctx.source_range().end()); | ||
231 | TextEdit::replace(delete_range, snippet.to_string()) | ||
232 | }; | ||
233 | CompletionItem::new(CompletionKind::Postfix, ctx.source_range(), label) | ||
234 | .detail(detail) | ||
235 | .kind(CompletionItemKind::Snippet) | ||
236 | .snippet_edit(cap, edit) | ||
237 | } | ||
238 | |||
239 | #[cfg(test)] | ||
240 | mod tests { | ||
241 | use expect::{expect, Expect}; | ||
242 | |||
243 | use crate::completion::{ | ||
244 | test_utils::{check_edit, completion_list}, | ||
245 | CompletionKind, | ||
246 | }; | ||
247 | |||
248 | fn check(ra_fixture: &str, expect: Expect) { | ||
249 | let actual = completion_list(ra_fixture, CompletionKind::Postfix); | ||
250 | expect.assert_eq(&actual) | ||
251 | } | ||
252 | |||
253 | #[test] | ||
254 | fn postfix_completion_works_for_trivial_path_expression() { | ||
255 | check( | ||
256 | r#" | ||
257 | fn main() { | ||
258 | let bar = true; | ||
259 | bar.<|> | ||
260 | } | ||
261 | "#, | ||
262 | expect![[r#" | ||
263 | sn box Box::new(expr) | ||
264 | sn call function(expr) | ||
265 | sn dbg dbg!(expr) | ||
266 | sn if if expr {} | ||
267 | sn match match expr {} | ||
268 | sn not !expr | ||
269 | sn ref &expr | ||
270 | sn refm &mut expr | ||
271 | sn while while expr {} | ||
272 | "#]], | ||
273 | ); | ||
274 | } | ||
275 | |||
276 | #[test] | ||
277 | fn postfix_type_filtering() { | ||
278 | check( | ||
279 | r#" | ||
280 | fn main() { | ||
281 | let bar: u8 = 12; | ||
282 | bar.<|> | ||
283 | } | ||
284 | "#, | ||
285 | expect![[r#" | ||
286 | sn box Box::new(expr) | ||
287 | sn call function(expr) | ||
288 | sn dbg dbg!(expr) | ||
289 | sn match match expr {} | ||
290 | sn ref &expr | ||
291 | sn refm &mut expr | ||
292 | "#]], | ||
293 | ) | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn option_iflet() { | ||
298 | check_edit( | ||
299 | "ifl", | ||
300 | r#" | ||
301 | enum Option<T> { Some(T), None } | ||
302 | |||
303 | fn main() { | ||
304 | let bar = Option::Some(true); | ||
305 | bar.<|> | ||
306 | } | ||
307 | "#, | ||
308 | r#" | ||
309 | enum Option<T> { Some(T), None } | ||
310 | |||
311 | fn main() { | ||
312 | let bar = Option::Some(true); | ||
313 | if let Some($1) = bar { | ||
314 | $0 | ||
315 | } | ||
316 | } | ||
317 | "#, | ||
318 | ); | ||
319 | } | ||
320 | |||
321 | #[test] | ||
322 | fn result_match() { | ||
323 | check_edit( | ||
324 | "match", | ||
325 | r#" | ||
326 | enum Result<T, E> { Ok(T), Err(E) } | ||
327 | |||
328 | fn main() { | ||
329 | let bar = Result::Ok(true); | ||
330 | bar.<|> | ||
331 | } | ||
332 | "#, | ||
333 | r#" | ||
334 | enum Result<T, E> { Ok(T), Err(E) } | ||
335 | |||
336 | fn main() { | ||
337 | let bar = Result::Ok(true); | ||
338 | match bar { | ||
339 | Ok(${1:_}) => {$2}, | ||
340 | Err(${3:_}) => {$0}, | ||
341 | } | ||
342 | } | ||
343 | "#, | ||
344 | ); | ||
345 | } | ||
346 | |||
347 | #[test] | ||
348 | fn postfix_completion_works_for_ambiguous_float_literal() { | ||
349 | check_edit("refm", r#"fn main() { 42.<|> }"#, r#"fn main() { &mut 42 }"#) | ||
350 | } | ||
351 | |||
352 | #[test] | ||
353 | fn works_in_simple_macro() { | ||
354 | check_edit( | ||
355 | "dbg", | ||
356 | r#" | ||
357 | macro_rules! m { ($e:expr) => { $e } } | ||
358 | fn main() { | ||
359 | let bar: u8 = 12; | ||
360 | m!(bar.d<|>) | ||
361 | } | ||
362 | "#, | ||
363 | r#" | ||
364 | macro_rules! m { ($e:expr) => { $e } } | ||
365 | fn main() { | ||
366 | let bar: u8 = 12; | ||
367 | m!(dbg!(bar)) | ||
368 | } | ||
369 | "#, | ||
370 | ); | ||
371 | } | ||
372 | |||
373 | #[test] | ||
374 | fn postfix_completion_for_references() { | ||
375 | check_edit("dbg", r#"fn main() { &&42.<|> }"#, r#"fn main() { dbg!(&&42) }"#); | ||
376 | check_edit("refm", r#"fn main() { &&42.<|> }"#, r#"fn main() { &&&mut 42 }"#); | ||
377 | } | ||
378 | } | ||
diff --git a/crates/ide/src/completion/complete_qualified_path.rs b/crates/ide/src/completion/complete_qualified_path.rs new file mode 100644 index 000000000..cb7dd23c1 --- /dev/null +++ b/crates/ide/src/completion/complete_qualified_path.rs | |||
@@ -0,0 +1,733 @@ | |||
1 | //! Completion of paths, i.e. `some::prefix::<|>`. | ||
2 | |||
3 | use hir::{Adt, HasVisibility, PathResolution, ScopeDef}; | ||
4 | use rustc_hash::FxHashSet; | ||
5 | use syntax::AstNode; | ||
6 | use test_utils::mark; | ||
7 | |||
8 | use crate::completion::{CompletionContext, Completions}; | ||
9 | |||
10 | pub(super) fn complete_qualified_path(acc: &mut Completions, ctx: &CompletionContext) { | ||
11 | let path = match &ctx.path_prefix { | ||
12 | Some(path) => path.clone(), | ||
13 | None => return, | ||
14 | }; | ||
15 | |||
16 | if ctx.attribute_under_caret.is_some() { | ||
17 | return; | ||
18 | } | ||
19 | |||
20 | let context_module = ctx.scope.module(); | ||
21 | |||
22 | let resolution = match ctx.scope.resolve_hir_path_qualifier(&path) { | ||
23 | Some(res) => res, | ||
24 | None => return, | ||
25 | }; | ||
26 | |||
27 | // Add associated types on type parameters and `Self`. | ||
28 | resolution.assoc_type_shorthand_candidates(ctx.db, |alias| { | ||
29 | acc.add_type_alias(ctx, alias); | ||
30 | None::<()> | ||
31 | }); | ||
32 | |||
33 | match resolution { | ||
34 | PathResolution::Def(hir::ModuleDef::Module(module)) => { | ||
35 | let module_scope = module.scope(ctx.db, context_module); | ||
36 | for (name, def) in module_scope { | ||
37 | if ctx.use_item_syntax.is_some() { | ||
38 | if let ScopeDef::Unknown = def { | ||
39 | if let Some(name_ref) = ctx.name_ref_syntax.as_ref() { | ||
40 | if name_ref.syntax().text() == name.to_string().as_str() { | ||
41 | // for `use self::foo<|>`, don't suggest `foo` as a completion | ||
42 | mark::hit!(dont_complete_current_use); | ||
43 | continue; | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | acc.add_resolution(ctx, name.to_string(), &def); | ||
50 | } | ||
51 | } | ||
52 | PathResolution::Def(def @ hir::ModuleDef::Adt(_)) | ||
53 | | PathResolution::Def(def @ hir::ModuleDef::TypeAlias(_)) => { | ||
54 | if let hir::ModuleDef::Adt(Adt::Enum(e)) = def { | ||
55 | for variant in e.variants(ctx.db) { | ||
56 | acc.add_enum_variant(ctx, variant, None); | ||
57 | } | ||
58 | } | ||
59 | let ty = match def { | ||
60 | hir::ModuleDef::Adt(adt) => adt.ty(ctx.db), | ||
61 | hir::ModuleDef::TypeAlias(a) => a.ty(ctx.db), | ||
62 | _ => unreachable!(), | ||
63 | }; | ||
64 | |||
65 | // XXX: For parity with Rust bug #22519, this does not complete Ty::AssocType. | ||
66 | // (where AssocType is defined on a trait, not an inherent impl) | ||
67 | |||
68 | let krate = ctx.krate; | ||
69 | if let Some(krate) = krate { | ||
70 | let traits_in_scope = ctx.scope.traits_in_scope(); | ||
71 | ty.iterate_path_candidates(ctx.db, krate, &traits_in_scope, None, |_ty, item| { | ||
72 | if context_module.map_or(false, |m| !item.is_visible_from(ctx.db, m)) { | ||
73 | return None; | ||
74 | } | ||
75 | match item { | ||
76 | hir::AssocItem::Function(func) => { | ||
77 | acc.add_function(ctx, func, None); | ||
78 | } | ||
79 | hir::AssocItem::Const(ct) => acc.add_const(ctx, ct), | ||
80 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
81 | } | ||
82 | None::<()> | ||
83 | }); | ||
84 | |||
85 | // Iterate assoc types separately | ||
86 | ty.iterate_assoc_items(ctx.db, krate, |item| { | ||
87 | if context_module.map_or(false, |m| !item.is_visible_from(ctx.db, m)) { | ||
88 | return None; | ||
89 | } | ||
90 | match item { | ||
91 | hir::AssocItem::Function(_) | hir::AssocItem::Const(_) => {} | ||
92 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
93 | } | ||
94 | None::<()> | ||
95 | }); | ||
96 | } | ||
97 | } | ||
98 | PathResolution::Def(hir::ModuleDef::Trait(t)) => { | ||
99 | // Handles `Trait::assoc` as well as `<Ty as Trait>::assoc`. | ||
100 | for item in t.items(ctx.db) { | ||
101 | if context_module.map_or(false, |m| !item.is_visible_from(ctx.db, m)) { | ||
102 | continue; | ||
103 | } | ||
104 | match item { | ||
105 | hir::AssocItem::Function(func) => { | ||
106 | acc.add_function(ctx, func, None); | ||
107 | } | ||
108 | hir::AssocItem::Const(ct) => acc.add_const(ctx, ct), | ||
109 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
110 | } | ||
111 | } | ||
112 | } | ||
113 | PathResolution::TypeParam(_) | PathResolution::SelfType(_) => { | ||
114 | if let Some(krate) = ctx.krate { | ||
115 | let ty = match resolution { | ||
116 | PathResolution::TypeParam(param) => param.ty(ctx.db), | ||
117 | PathResolution::SelfType(impl_def) => impl_def.target_ty(ctx.db), | ||
118 | _ => return, | ||
119 | }; | ||
120 | |||
121 | let traits_in_scope = ctx.scope.traits_in_scope(); | ||
122 | let mut seen = FxHashSet::default(); | ||
123 | ty.iterate_path_candidates(ctx.db, krate, &traits_in_scope, None, |_ty, item| { | ||
124 | if context_module.map_or(false, |m| !item.is_visible_from(ctx.db, m)) { | ||
125 | return None; | ||
126 | } | ||
127 | |||
128 | // We might iterate candidates of a trait multiple times here, so deduplicate | ||
129 | // them. | ||
130 | if seen.insert(item) { | ||
131 | match item { | ||
132 | hir::AssocItem::Function(func) => { | ||
133 | acc.add_function(ctx, func, None); | ||
134 | } | ||
135 | hir::AssocItem::Const(ct) => acc.add_const(ctx, ct), | ||
136 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
137 | } | ||
138 | } | ||
139 | None::<()> | ||
140 | }); | ||
141 | } | ||
142 | } | ||
143 | _ => {} | ||
144 | } | ||
145 | } | ||
146 | |||
147 | #[cfg(test)] | ||
148 | mod tests { | ||
149 | use expect::{expect, Expect}; | ||
150 | use test_utils::mark; | ||
151 | |||
152 | use crate::completion::{ | ||
153 | test_utils::{check_edit, completion_list}, | ||
154 | CompletionKind, | ||
155 | }; | ||
156 | |||
157 | fn check(ra_fixture: &str, expect: Expect) { | ||
158 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
159 | expect.assert_eq(&actual); | ||
160 | } | ||
161 | |||
162 | fn check_builtin(ra_fixture: &str, expect: Expect) { | ||
163 | let actual = completion_list(ra_fixture, CompletionKind::BuiltinType); | ||
164 | expect.assert_eq(&actual); | ||
165 | } | ||
166 | |||
167 | #[test] | ||
168 | fn dont_complete_current_use() { | ||
169 | mark::check!(dont_complete_current_use); | ||
170 | check(r#"use self::foo<|>;"#, expect![[""]]); | ||
171 | } | ||
172 | |||
173 | #[test] | ||
174 | fn dont_complete_current_use_in_braces_with_glob() { | ||
175 | check( | ||
176 | r#" | ||
177 | mod foo { pub struct S; } | ||
178 | use self::{foo::*, bar<|>}; | ||
179 | "#, | ||
180 | expect![[r#" | ||
181 | st S | ||
182 | md foo | ||
183 | "#]], | ||
184 | ); | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn dont_complete_primitive_in_use() { | ||
189 | check_builtin(r#"use self::<|>;"#, expect![[""]]); | ||
190 | } | ||
191 | |||
192 | #[test] | ||
193 | fn dont_complete_primitive_in_module_scope() { | ||
194 | check_builtin(r#"fn foo() { self::<|> }"#, expect![[""]]); | ||
195 | } | ||
196 | |||
197 | #[test] | ||
198 | fn completes_primitives() { | ||
199 | check_builtin( | ||
200 | r#"fn main() { let _: <|> = 92; }"#, | ||
201 | expect![[r#" | ||
202 | bt bool | ||
203 | bt char | ||
204 | bt f32 | ||
205 | bt f64 | ||
206 | bt i128 | ||
207 | bt i16 | ||
208 | bt i32 | ||
209 | bt i64 | ||
210 | bt i8 | ||
211 | bt isize | ||
212 | bt str | ||
213 | bt u128 | ||
214 | bt u16 | ||
215 | bt u32 | ||
216 | bt u64 | ||
217 | bt u8 | ||
218 | bt usize | ||
219 | "#]], | ||
220 | ); | ||
221 | } | ||
222 | |||
223 | #[test] | ||
224 | fn completes_mod_with_same_name_as_function() { | ||
225 | check( | ||
226 | r#" | ||
227 | use self::my::<|>; | ||
228 | |||
229 | mod my { pub struct Bar; } | ||
230 | fn my() {} | ||
231 | "#, | ||
232 | expect![[r#" | ||
233 | st Bar | ||
234 | "#]], | ||
235 | ); | ||
236 | } | ||
237 | |||
238 | #[test] | ||
239 | fn filters_visibility() { | ||
240 | check( | ||
241 | r#" | ||
242 | use self::my::<|>; | ||
243 | |||
244 | mod my { | ||
245 | struct Bar; | ||
246 | pub struct Foo; | ||
247 | pub use Bar as PublicBar; | ||
248 | } | ||
249 | "#, | ||
250 | expect![[r#" | ||
251 | st Foo | ||
252 | st PublicBar | ||
253 | "#]], | ||
254 | ); | ||
255 | } | ||
256 | |||
257 | #[test] | ||
258 | fn completes_use_item_starting_with_self() { | ||
259 | check( | ||
260 | r#" | ||
261 | use self::m::<|>; | ||
262 | |||
263 | mod m { pub struct Bar; } | ||
264 | "#, | ||
265 | expect![[r#" | ||
266 | st Bar | ||
267 | "#]], | ||
268 | ); | ||
269 | } | ||
270 | |||
271 | #[test] | ||
272 | fn completes_use_item_starting_with_crate() { | ||
273 | check( | ||
274 | r#" | ||
275 | //- /lib.rs | ||
276 | mod foo; | ||
277 | struct Spam; | ||
278 | //- /foo.rs | ||
279 | use crate::Sp<|> | ||
280 | "#, | ||
281 | expect![[r#" | ||
282 | st Spam | ||
283 | md foo | ||
284 | "#]], | ||
285 | ); | ||
286 | } | ||
287 | |||
288 | #[test] | ||
289 | fn completes_nested_use_tree() { | ||
290 | check( | ||
291 | r#" | ||
292 | //- /lib.rs | ||
293 | mod foo; | ||
294 | struct Spam; | ||
295 | //- /foo.rs | ||
296 | use crate::{Sp<|>}; | ||
297 | "#, | ||
298 | expect![[r#" | ||
299 | st Spam | ||
300 | md foo | ||
301 | "#]], | ||
302 | ); | ||
303 | } | ||
304 | |||
305 | #[test] | ||
306 | fn completes_deeply_nested_use_tree() { | ||
307 | check( | ||
308 | r#" | ||
309 | //- /lib.rs | ||
310 | mod foo; | ||
311 | pub mod bar { | ||
312 | pub mod baz { | ||
313 | pub struct Spam; | ||
314 | } | ||
315 | } | ||
316 | //- /foo.rs | ||
317 | use crate::{bar::{baz::Sp<|>}}; | ||
318 | "#, | ||
319 | expect![[r#" | ||
320 | st Spam | ||
321 | "#]], | ||
322 | ); | ||
323 | } | ||
324 | |||
325 | #[test] | ||
326 | fn completes_enum_variant() { | ||
327 | check( | ||
328 | r#" | ||
329 | enum E { Foo, Bar(i32) } | ||
330 | fn foo() { let _ = E::<|> } | ||
331 | "#, | ||
332 | expect![[r#" | ||
333 | ev Bar(…) (i32) | ||
334 | ev Foo () | ||
335 | "#]], | ||
336 | ); | ||
337 | } | ||
338 | |||
339 | #[test] | ||
340 | fn completes_struct_associated_items() { | ||
341 | check( | ||
342 | r#" | ||
343 | //- /lib.rs | ||
344 | struct S; | ||
345 | |||
346 | impl S { | ||
347 | fn a() {} | ||
348 | fn b(&self) {} | ||
349 | const C: i32 = 42; | ||
350 | type T = i32; | ||
351 | } | ||
352 | |||
353 | fn foo() { let _ = S::<|> } | ||
354 | "#, | ||
355 | expect![[r#" | ||
356 | ct C const C: i32 = 42; | ||
357 | ta T type T = i32; | ||
358 | fn a() fn a() | ||
359 | me b() fn b(&self) | ||
360 | "#]], | ||
361 | ); | ||
362 | } | ||
363 | |||
364 | #[test] | ||
365 | fn associated_item_visibility() { | ||
366 | check( | ||
367 | r#" | ||
368 | struct S; | ||
369 | |||
370 | mod m { | ||
371 | impl super::S { | ||
372 | pub(super) fn public_method() { } | ||
373 | fn private_method() { } | ||
374 | pub(super) type PublicType = u32; | ||
375 | type PrivateType = u32; | ||
376 | pub(super) const PUBLIC_CONST: u32 = 1; | ||
377 | const PRIVATE_CONST: u32 = 1; | ||
378 | } | ||
379 | } | ||
380 | |||
381 | fn foo() { let _ = S::<|> } | ||
382 | "#, | ||
383 | expect![[r#" | ||
384 | ct PUBLIC_CONST pub(super) const PUBLIC_CONST: u32 = 1; | ||
385 | ta PublicType pub(super) type PublicType = u32; | ||
386 | fn public_method() pub(super) fn public_method() | ||
387 | "#]], | ||
388 | ); | ||
389 | } | ||
390 | |||
391 | #[test] | ||
392 | fn completes_enum_associated_method() { | ||
393 | check( | ||
394 | r#" | ||
395 | enum E {}; | ||
396 | impl E { fn m() { } } | ||
397 | |||
398 | fn foo() { let _ = E::<|> } | ||
399 | "#, | ||
400 | expect![[r#" | ||
401 | fn m() fn m() | ||
402 | "#]], | ||
403 | ); | ||
404 | } | ||
405 | |||
406 | #[test] | ||
407 | fn completes_union_associated_method() { | ||
408 | check( | ||
409 | r#" | ||
410 | union U {}; | ||
411 | impl U { fn m() { } } | ||
412 | |||
413 | fn foo() { let _ = U::<|> } | ||
414 | "#, | ||
415 | expect![[r#" | ||
416 | fn m() fn m() | ||
417 | "#]], | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn completes_use_paths_across_crates() { | ||
423 | check( | ||
424 | r#" | ||
425 | //- /main.rs | ||
426 | use foo::<|>; | ||
427 | |||
428 | //- /foo/lib.rs | ||
429 | pub mod bar { pub struct S; } | ||
430 | "#, | ||
431 | expect![[r#" | ||
432 | md bar | ||
433 | "#]], | ||
434 | ); | ||
435 | } | ||
436 | |||
437 | #[test] | ||
438 | fn completes_trait_associated_method_1() { | ||
439 | check( | ||
440 | r#" | ||
441 | trait Trait { fn m(); } | ||
442 | |||
443 | fn foo() { let _ = Trait::<|> } | ||
444 | "#, | ||
445 | expect![[r#" | ||
446 | fn m() fn m() | ||
447 | "#]], | ||
448 | ); | ||
449 | } | ||
450 | |||
451 | #[test] | ||
452 | fn completes_trait_associated_method_2() { | ||
453 | check( | ||
454 | r#" | ||
455 | trait Trait { fn m(); } | ||
456 | |||
457 | struct S; | ||
458 | impl Trait for S {} | ||
459 | |||
460 | fn foo() { let _ = S::<|> } | ||
461 | "#, | ||
462 | expect![[r#" | ||
463 | fn m() fn m() | ||
464 | "#]], | ||
465 | ); | ||
466 | } | ||
467 | |||
468 | #[test] | ||
469 | fn completes_trait_associated_method_3() { | ||
470 | check( | ||
471 | r#" | ||
472 | trait Trait { fn m(); } | ||
473 | |||
474 | struct S; | ||
475 | impl Trait for S {} | ||
476 | |||
477 | fn foo() { let _ = <S as Trait>::<|> } | ||
478 | "#, | ||
479 | expect![[r#" | ||
480 | fn m() fn m() | ||
481 | "#]], | ||
482 | ); | ||
483 | } | ||
484 | |||
485 | #[test] | ||
486 | fn completes_ty_param_assoc_ty() { | ||
487 | check( | ||
488 | r#" | ||
489 | trait Super { | ||
490 | type Ty; | ||
491 | const CONST: u8; | ||
492 | fn func() {} | ||
493 | fn method(&self) {} | ||
494 | } | ||
495 | |||
496 | trait Sub: Super { | ||
497 | type SubTy; | ||
498 | const C2: (); | ||
499 | fn subfunc() {} | ||
500 | fn submethod(&self) {} | ||
501 | } | ||
502 | |||
503 | fn foo<T: Sub>() { T::<|> } | ||
504 | "#, | ||
505 | expect![[r#" | ||
506 | ct C2 const C2: (); | ||
507 | ct CONST const CONST: u8; | ||
508 | ta SubTy type SubTy; | ||
509 | ta Ty type Ty; | ||
510 | fn func() fn func() | ||
511 | me method() fn method(&self) | ||
512 | fn subfunc() fn subfunc() | ||
513 | me submethod() fn submethod(&self) | ||
514 | "#]], | ||
515 | ); | ||
516 | } | ||
517 | |||
518 | #[test] | ||
519 | fn completes_self_param_assoc_ty() { | ||
520 | check( | ||
521 | r#" | ||
522 | trait Super { | ||
523 | type Ty; | ||
524 | const CONST: u8 = 0; | ||
525 | fn func() {} | ||
526 | fn method(&self) {} | ||
527 | } | ||
528 | |||
529 | trait Sub: Super { | ||
530 | type SubTy; | ||
531 | const C2: () = (); | ||
532 | fn subfunc() {} | ||
533 | fn submethod(&self) {} | ||
534 | } | ||
535 | |||
536 | struct Wrap<T>(T); | ||
537 | impl<T> Super for Wrap<T> {} | ||
538 | impl<T> Sub for Wrap<T> { | ||
539 | fn subfunc() { | ||
540 | // Should be able to assume `Self: Sub + Super` | ||
541 | Self::<|> | ||
542 | } | ||
543 | } | ||
544 | "#, | ||
545 | expect![[r#" | ||
546 | ct C2 const C2: () = (); | ||
547 | ct CONST const CONST: u8 = 0; | ||
548 | ta SubTy type SubTy; | ||
549 | ta Ty type Ty; | ||
550 | fn func() fn func() | ||
551 | me method() fn method(&self) | ||
552 | fn subfunc() fn subfunc() | ||
553 | me submethod() fn submethod(&self) | ||
554 | "#]], | ||
555 | ); | ||
556 | } | ||
557 | |||
558 | #[test] | ||
559 | fn completes_type_alias() { | ||
560 | check( | ||
561 | r#" | ||
562 | struct S; | ||
563 | impl S { fn foo() {} } | ||
564 | type T = S; | ||
565 | impl T { fn bar() {} } | ||
566 | |||
567 | fn main() { T::<|>; } | ||
568 | "#, | ||
569 | expect![[r#" | ||
570 | fn bar() fn bar() | ||
571 | fn foo() fn foo() | ||
572 | "#]], | ||
573 | ); | ||
574 | } | ||
575 | |||
576 | #[test] | ||
577 | fn completes_qualified_macros() { | ||
578 | check( | ||
579 | r#" | ||
580 | #[macro_export] | ||
581 | macro_rules! foo { () => {} } | ||
582 | |||
583 | fn main() { let _ = crate::<|> } | ||
584 | "#, | ||
585 | expect![[r##" | ||
586 | ma foo!(…) #[macro_export] | ||
587 | macro_rules! foo | ||
588 | fn main() fn main() | ||
589 | "##]], | ||
590 | ); | ||
591 | } | ||
592 | |||
593 | #[test] | ||
594 | fn test_super_super_completion() { | ||
595 | check( | ||
596 | r#" | ||
597 | mod a { | ||
598 | const A: usize = 0; | ||
599 | mod b { | ||
600 | const B: usize = 0; | ||
601 | mod c { use super::super::<|> } | ||
602 | } | ||
603 | } | ||
604 | "#, | ||
605 | expect![[r#" | ||
606 | ct A | ||
607 | md b | ||
608 | "#]], | ||
609 | ); | ||
610 | } | ||
611 | |||
612 | #[test] | ||
613 | fn completes_reexported_items_under_correct_name() { | ||
614 | check( | ||
615 | r#" | ||
616 | fn foo() { self::m::<|> } | ||
617 | |||
618 | mod m { | ||
619 | pub use super::p::wrong_fn as right_fn; | ||
620 | pub use super::p::WRONG_CONST as RIGHT_CONST; | ||
621 | pub use super::p::WrongType as RightType; | ||
622 | } | ||
623 | mod p { | ||
624 | fn wrong_fn() {} | ||
625 | const WRONG_CONST: u32 = 1; | ||
626 | struct WrongType {}; | ||
627 | } | ||
628 | "#, | ||
629 | expect![[r#" | ||
630 | ct RIGHT_CONST | ||
631 | st RightType | ||
632 | fn right_fn() fn wrong_fn() | ||
633 | "#]], | ||
634 | ); | ||
635 | |||
636 | check_edit( | ||
637 | "RightType", | ||
638 | r#" | ||
639 | fn foo() { self::m::<|> } | ||
640 | |||
641 | mod m { | ||
642 | pub use super::p::wrong_fn as right_fn; | ||
643 | pub use super::p::WRONG_CONST as RIGHT_CONST; | ||
644 | pub use super::p::WrongType as RightType; | ||
645 | } | ||
646 | mod p { | ||
647 | fn wrong_fn() {} | ||
648 | const WRONG_CONST: u32 = 1; | ||
649 | struct WrongType {}; | ||
650 | } | ||
651 | "#, | ||
652 | r#" | ||
653 | fn foo() { self::m::RightType } | ||
654 | |||
655 | mod m { | ||
656 | pub use super::p::wrong_fn as right_fn; | ||
657 | pub use super::p::WRONG_CONST as RIGHT_CONST; | ||
658 | pub use super::p::WrongType as RightType; | ||
659 | } | ||
660 | mod p { | ||
661 | fn wrong_fn() {} | ||
662 | const WRONG_CONST: u32 = 1; | ||
663 | struct WrongType {}; | ||
664 | } | ||
665 | "#, | ||
666 | ); | ||
667 | } | ||
668 | |||
669 | #[test] | ||
670 | fn completes_in_simple_macro_call() { | ||
671 | check( | ||
672 | r#" | ||
673 | macro_rules! m { ($e:expr) => { $e } } | ||
674 | fn main() { m!(self::f<|>); } | ||
675 | fn foo() {} | ||
676 | "#, | ||
677 | expect![[r#" | ||
678 | fn foo() fn foo() | ||
679 | fn main() fn main() | ||
680 | "#]], | ||
681 | ); | ||
682 | } | ||
683 | |||
684 | #[test] | ||
685 | fn function_mod_share_name() { | ||
686 | check( | ||
687 | r#" | ||
688 | fn foo() { self::m::<|> } | ||
689 | |||
690 | mod m { | ||
691 | pub mod z {} | ||
692 | pub fn z() {} | ||
693 | } | ||
694 | "#, | ||
695 | expect![[r#" | ||
696 | md z | ||
697 | fn z() pub fn z() | ||
698 | "#]], | ||
699 | ); | ||
700 | } | ||
701 | |||
702 | #[test] | ||
703 | fn completes_hashmap_new() { | ||
704 | check( | ||
705 | r#" | ||
706 | struct RandomState; | ||
707 | struct HashMap<K, V, S = RandomState> {} | ||
708 | |||
709 | impl<K, V> HashMap<K, V, RandomState> { | ||
710 | pub fn new() -> HashMap<K, V, RandomState> { } | ||
711 | } | ||
712 | fn foo() { | ||
713 | HashMap::<|> | ||
714 | } | ||
715 | "#, | ||
716 | expect![[r#" | ||
717 | fn new() pub fn new() -> HashMap<K, V, RandomState> | ||
718 | "#]], | ||
719 | ); | ||
720 | } | ||
721 | |||
722 | #[test] | ||
723 | fn dont_complete_attr() { | ||
724 | check( | ||
725 | r#" | ||
726 | mod foo { pub struct Foo; } | ||
727 | #[foo::<|>] | ||
728 | fn f() {} | ||
729 | "#, | ||
730 | expect![[""]], | ||
731 | ); | ||
732 | } | ||
733 | } | ||
diff --git a/crates/ide/src/completion/complete_record.rs b/crates/ide/src/completion/complete_record.rs new file mode 100644 index 000000000..74b94594d --- /dev/null +++ b/crates/ide/src/completion/complete_record.rs | |||
@@ -0,0 +1,226 @@ | |||
1 | //! Complete fields in record literals and patterns. | ||
2 | use crate::completion::{CompletionContext, Completions}; | ||
3 | |||
4 | pub(super) fn complete_record(acc: &mut Completions, ctx: &CompletionContext) -> Option<()> { | ||
5 | let missing_fields = match (ctx.record_pat_syntax.as_ref(), ctx.record_lit_syntax.as_ref()) { | ||
6 | (None, None) => return None, | ||
7 | (Some(_), Some(_)) => unreachable!("A record cannot be both a literal and a pattern"), | ||
8 | (Some(record_pat), _) => ctx.sema.record_pattern_missing_fields(record_pat), | ||
9 | (_, Some(record_lit)) => ctx.sema.record_literal_missing_fields(record_lit), | ||
10 | }; | ||
11 | |||
12 | for (field, ty) in missing_fields { | ||
13 | acc.add_field(ctx, field, &ty) | ||
14 | } | ||
15 | |||
16 | Some(()) | ||
17 | } | ||
18 | |||
19 | #[cfg(test)] | ||
20 | mod tests { | ||
21 | use expect::{expect, Expect}; | ||
22 | |||
23 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
24 | |||
25 | fn check(ra_fixture: &str, expect: Expect) { | ||
26 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
27 | expect.assert_eq(&actual); | ||
28 | } | ||
29 | |||
30 | #[test] | ||
31 | fn test_record_pattern_field() { | ||
32 | check( | ||
33 | r#" | ||
34 | struct S { foo: u32 } | ||
35 | |||
36 | fn process(f: S) { | ||
37 | match f { | ||
38 | S { f<|>: 92 } => (), | ||
39 | } | ||
40 | } | ||
41 | "#, | ||
42 | expect![[r#" | ||
43 | fd foo u32 | ||
44 | "#]], | ||
45 | ); | ||
46 | } | ||
47 | |||
48 | #[test] | ||
49 | fn test_record_pattern_enum_variant() { | ||
50 | check( | ||
51 | r#" | ||
52 | enum E { S { foo: u32, bar: () } } | ||
53 | |||
54 | fn process(e: E) { | ||
55 | match e { | ||
56 | E::S { <|> } => (), | ||
57 | } | ||
58 | } | ||
59 | "#, | ||
60 | expect![[r#" | ||
61 | fd bar () | ||
62 | fd foo u32 | ||
63 | "#]], | ||
64 | ); | ||
65 | } | ||
66 | |||
67 | #[test] | ||
68 | fn test_record_pattern_field_in_simple_macro() { | ||
69 | check( | ||
70 | r" | ||
71 | macro_rules! m { ($e:expr) => { $e } } | ||
72 | struct S { foo: u32 } | ||
73 | |||
74 | fn process(f: S) { | ||
75 | m!(match f { | ||
76 | S { f<|>: 92 } => (), | ||
77 | }) | ||
78 | } | ||
79 | ", | ||
80 | expect![[r#" | ||
81 | fd foo u32 | ||
82 | "#]], | ||
83 | ); | ||
84 | } | ||
85 | |||
86 | #[test] | ||
87 | fn only_missing_fields_are_completed_in_destruct_pats() { | ||
88 | check( | ||
89 | r#" | ||
90 | struct S { | ||
91 | foo1: u32, foo2: u32, | ||
92 | bar: u32, baz: u32, | ||
93 | } | ||
94 | |||
95 | fn main() { | ||
96 | let s = S { | ||
97 | foo1: 1, foo2: 2, | ||
98 | bar: 3, baz: 4, | ||
99 | }; | ||
100 | if let S { foo1, foo2: a, <|> } = s {} | ||
101 | } | ||
102 | "#, | ||
103 | expect![[r#" | ||
104 | fd bar u32 | ||
105 | fd baz u32 | ||
106 | "#]], | ||
107 | ); | ||
108 | } | ||
109 | |||
110 | #[test] | ||
111 | fn test_record_literal_field() { | ||
112 | check( | ||
113 | r#" | ||
114 | struct A { the_field: u32 } | ||
115 | fn foo() { | ||
116 | A { the<|> } | ||
117 | } | ||
118 | "#, | ||
119 | expect![[r#" | ||
120 | fd the_field u32 | ||
121 | "#]], | ||
122 | ); | ||
123 | } | ||
124 | |||
125 | #[test] | ||
126 | fn test_record_literal_enum_variant() { | ||
127 | check( | ||
128 | r#" | ||
129 | enum E { A { a: u32 } } | ||
130 | fn foo() { | ||
131 | let _ = E::A { <|> } | ||
132 | } | ||
133 | "#, | ||
134 | expect![[r#" | ||
135 | fd a u32 | ||
136 | "#]], | ||
137 | ); | ||
138 | } | ||
139 | |||
140 | #[test] | ||
141 | fn test_record_literal_two_structs() { | ||
142 | check( | ||
143 | r#" | ||
144 | struct A { a: u32 } | ||
145 | struct B { b: u32 } | ||
146 | |||
147 | fn foo() { | ||
148 | let _: A = B { <|> } | ||
149 | } | ||
150 | "#, | ||
151 | expect![[r#" | ||
152 | fd b u32 | ||
153 | "#]], | ||
154 | ); | ||
155 | } | ||
156 | |||
157 | #[test] | ||
158 | fn test_record_literal_generic_struct() { | ||
159 | check( | ||
160 | r#" | ||
161 | struct A<T> { a: T } | ||
162 | |||
163 | fn foo() { | ||
164 | let _: A<u32> = A { <|> } | ||
165 | } | ||
166 | "#, | ||
167 | expect![[r#" | ||
168 | fd a u32 | ||
169 | "#]], | ||
170 | ); | ||
171 | } | ||
172 | |||
173 | #[test] | ||
174 | fn test_record_literal_field_in_simple_macro() { | ||
175 | check( | ||
176 | r#" | ||
177 | macro_rules! m { ($e:expr) => { $e } } | ||
178 | struct A { the_field: u32 } | ||
179 | fn foo() { | ||
180 | m!(A { the<|> }) | ||
181 | } | ||
182 | "#, | ||
183 | expect![[r#" | ||
184 | fd the_field u32 | ||
185 | "#]], | ||
186 | ); | ||
187 | } | ||
188 | |||
189 | #[test] | ||
190 | fn only_missing_fields_are_completed() { | ||
191 | check( | ||
192 | r#" | ||
193 | struct S { | ||
194 | foo1: u32, foo2: u32, | ||
195 | bar: u32, baz: u32, | ||
196 | } | ||
197 | |||
198 | fn main() { | ||
199 | let foo1 = 1; | ||
200 | let s = S { foo1, foo2: 5, <|> } | ||
201 | } | ||
202 | "#, | ||
203 | expect![[r#" | ||
204 | fd bar u32 | ||
205 | fd baz u32 | ||
206 | "#]], | ||
207 | ); | ||
208 | } | ||
209 | |||
210 | #[test] | ||
211 | fn completes_functional_update() { | ||
212 | check( | ||
213 | r#" | ||
214 | struct S { foo1: u32, foo2: u32 } | ||
215 | |||
216 | fn main() { | ||
217 | let foo1 = 1; | ||
218 | let s = S { foo1, <|> .. loop {} } | ||
219 | } | ||
220 | "#, | ||
221 | expect![[r#" | ||
222 | fd foo2 u32 | ||
223 | "#]], | ||
224 | ); | ||
225 | } | ||
226 | } | ||
diff --git a/crates/ide/src/completion/complete_snippet.rs b/crates/ide/src/completion/complete_snippet.rs new file mode 100644 index 000000000..4368e4eec --- /dev/null +++ b/crates/ide/src/completion/complete_snippet.rs | |||
@@ -0,0 +1,116 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{ | ||
4 | completion_config::SnippetCap, completion_item::Builder, CompletionContext, CompletionItem, | ||
5 | CompletionItemKind, CompletionKind, Completions, | ||
6 | }; | ||
7 | |||
8 | fn snippet(ctx: &CompletionContext, cap: SnippetCap, label: &str, snippet: &str) -> Builder { | ||
9 | CompletionItem::new(CompletionKind::Snippet, ctx.source_range(), label) | ||
10 | .insert_snippet(cap, snippet) | ||
11 | .kind(CompletionItemKind::Snippet) | ||
12 | } | ||
13 | |||
14 | pub(super) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionContext) { | ||
15 | if !(ctx.is_trivial_path && ctx.function_syntax.is_some()) { | ||
16 | return; | ||
17 | } | ||
18 | let cap = match ctx.config.snippet_cap { | ||
19 | Some(it) => it, | ||
20 | None => return, | ||
21 | }; | ||
22 | |||
23 | snippet(ctx, cap, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); | ||
24 | snippet(ctx, cap, "ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); | ||
25 | } | ||
26 | |||
27 | pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { | ||
28 | if !ctx.is_new_item { | ||
29 | return; | ||
30 | } | ||
31 | let cap = match ctx.config.snippet_cap { | ||
32 | Some(it) => it, | ||
33 | None => return, | ||
34 | }; | ||
35 | |||
36 | snippet( | ||
37 | ctx, | ||
38 | cap, | ||
39 | "tmod (Test module)", | ||
40 | "\ | ||
41 | #[cfg(test)] | ||
42 | mod tests { | ||
43 | use super::*; | ||
44 | |||
45 | #[test] | ||
46 | fn ${1:test_name}() { | ||
47 | $0 | ||
48 | } | ||
49 | }", | ||
50 | ) | ||
51 | .lookup_by("tmod") | ||
52 | .add_to(acc); | ||
53 | |||
54 | snippet( | ||
55 | ctx, | ||
56 | cap, | ||
57 | "tfn (Test function)", | ||
58 | "\ | ||
59 | #[test] | ||
60 | fn ${1:feature}() { | ||
61 | $0 | ||
62 | }", | ||
63 | ) | ||
64 | .lookup_by("tfn") | ||
65 | .add_to(acc); | ||
66 | |||
67 | snippet(ctx, cap, "macro_rules", "macro_rules! $1 {\n\t($2) => {\n\t\t$0\n\t};\n}").add_to(acc); | ||
68 | snippet(ctx, cap, "pub(crate)", "pub(crate) $0").add_to(acc); | ||
69 | } | ||
70 | |||
71 | #[cfg(test)] | ||
72 | mod tests { | ||
73 | use expect::{expect, Expect}; | ||
74 | |||
75 | use crate::completion::{test_utils::completion_list, CompletionKind}; | ||
76 | |||
77 | fn check(ra_fixture: &str, expect: Expect) { | ||
78 | let actual = completion_list(ra_fixture, CompletionKind::Snippet); | ||
79 | expect.assert_eq(&actual) | ||
80 | } | ||
81 | |||
82 | #[test] | ||
83 | fn completes_snippets_in_expressions() { | ||
84 | check( | ||
85 | r#"fn foo(x: i32) { <|> }"#, | ||
86 | expect![[r#" | ||
87 | sn pd | ||
88 | sn ppd | ||
89 | "#]], | ||
90 | ); | ||
91 | } | ||
92 | |||
93 | #[test] | ||
94 | fn should_not_complete_snippets_in_path() { | ||
95 | check(r#"fn foo(x: i32) { ::foo<|> }"#, expect![[""]]); | ||
96 | check(r#"fn foo(x: i32) { ::<|> }"#, expect![[""]]); | ||
97 | } | ||
98 | |||
99 | #[test] | ||
100 | fn completes_snippets_in_items() { | ||
101 | check( | ||
102 | r#" | ||
103 | #[cfg(test)] | ||
104 | mod tests { | ||
105 | <|> | ||
106 | } | ||
107 | "#, | ||
108 | expect![[r#" | ||
109 | sn macro_rules | ||
110 | sn pub(crate) | ||
111 | sn tfn (Test function) | ||
112 | sn tmod (Test module) | ||
113 | "#]], | ||
114 | ) | ||
115 | } | ||
116 | } | ||
diff --git a/crates/ide/src/completion/complete_trait_impl.rs b/crates/ide/src/completion/complete_trait_impl.rs new file mode 100644 index 000000000..478e31262 --- /dev/null +++ b/crates/ide/src/completion/complete_trait_impl.rs | |||
@@ -0,0 +1,488 @@ | |||
1 | //! Completion for associated items in a trait implementation. | ||
2 | //! | ||
3 | //! This module adds the completion items related to implementing associated | ||
4 | //! items within a `impl Trait for Struct` block. The current context node | ||
5 | //! must be within either a `FN`, `TYPE_ALIAS`, or `CONST` node | ||
6 | //! and an direct child of an `IMPL`. | ||
7 | //! | ||
8 | //! # Examples | ||
9 | //! | ||
10 | //! Considering the following trait `impl`: | ||
11 | //! | ||
12 | //! ```ignore | ||
13 | //! trait SomeTrait { | ||
14 | //! fn foo(); | ||
15 | //! } | ||
16 | //! | ||
17 | //! impl SomeTrait for () { | ||
18 | //! fn f<|> | ||
19 | //! } | ||
20 | //! ``` | ||
21 | //! | ||
22 | //! may result in the completion of the following method: | ||
23 | //! | ||
24 | //! ```ignore | ||
25 | //! # trait SomeTrait { | ||
26 | //! # fn foo(); | ||
27 | //! # } | ||
28 | //! | ||
29 | //! impl SomeTrait for () { | ||
30 | //! fn foo() {}<|> | ||
31 | //! } | ||
32 | //! ``` | ||
33 | |||
34 | use assists::utils::get_missing_assoc_items; | ||
35 | use hir::{self, Docs, HasSource}; | ||
36 | use syntax::{ | ||
37 | ast::{self, edit, Impl}, | ||
38 | AstNode, SyntaxKind, SyntaxNode, TextRange, T, | ||
39 | }; | ||
40 | use text_edit::TextEdit; | ||
41 | |||
42 | use crate::{ | ||
43 | completion::{ | ||
44 | CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, | ||
45 | }, | ||
46 | display::function_declaration, | ||
47 | }; | ||
48 | |||
49 | pub(crate) fn complete_trait_impl(acc: &mut Completions, ctx: &CompletionContext) { | ||
50 | if let Some((trigger, impl_def)) = completion_match(ctx) { | ||
51 | match trigger.kind() { | ||
52 | SyntaxKind::NAME_REF => get_missing_assoc_items(&ctx.sema, &impl_def) | ||
53 | .into_iter() | ||
54 | .for_each(|item| match item { | ||
55 | hir::AssocItem::Function(fn_item) => { | ||
56 | add_function_impl(&trigger, acc, ctx, fn_item) | ||
57 | } | ||
58 | hir::AssocItem::TypeAlias(type_item) => { | ||
59 | add_type_alias_impl(&trigger, acc, ctx, type_item) | ||
60 | } | ||
61 | hir::AssocItem::Const(const_item) => { | ||
62 | add_const_impl(&trigger, acc, ctx, const_item) | ||
63 | } | ||
64 | }), | ||
65 | |||
66 | SyntaxKind::FN => { | ||
67 | for missing_fn in get_missing_assoc_items(&ctx.sema, &impl_def) | ||
68 | .into_iter() | ||
69 | .filter_map(|item| match item { | ||
70 | hir::AssocItem::Function(fn_item) => Some(fn_item), | ||
71 | _ => None, | ||
72 | }) | ||
73 | { | ||
74 | add_function_impl(&trigger, acc, ctx, missing_fn); | ||
75 | } | ||
76 | } | ||
77 | |||
78 | SyntaxKind::TYPE_ALIAS => { | ||
79 | for missing_fn in get_missing_assoc_items(&ctx.sema, &impl_def) | ||
80 | .into_iter() | ||
81 | .filter_map(|item| match item { | ||
82 | hir::AssocItem::TypeAlias(type_item) => Some(type_item), | ||
83 | _ => None, | ||
84 | }) | ||
85 | { | ||
86 | add_type_alias_impl(&trigger, acc, ctx, missing_fn); | ||
87 | } | ||
88 | } | ||
89 | |||
90 | SyntaxKind::CONST => { | ||
91 | for missing_fn in get_missing_assoc_items(&ctx.sema, &impl_def) | ||
92 | .into_iter() | ||
93 | .filter_map(|item| match item { | ||
94 | hir::AssocItem::Const(const_item) => Some(const_item), | ||
95 | _ => None, | ||
96 | }) | ||
97 | { | ||
98 | add_const_impl(&trigger, acc, ctx, missing_fn); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | _ => {} | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | fn completion_match(ctx: &CompletionContext) -> Option<(SyntaxNode, Impl)> { | ||
108 | let (trigger, impl_def_offset) = ctx.token.ancestors().find_map(|p| match p.kind() { | ||
109 | SyntaxKind::FN | SyntaxKind::TYPE_ALIAS | SyntaxKind::CONST | SyntaxKind::BLOCK_EXPR => { | ||
110 | Some((p, 2)) | ||
111 | } | ||
112 | SyntaxKind::NAME_REF => Some((p, 5)), | ||
113 | _ => None, | ||
114 | })?; | ||
115 | let impl_def = (0..impl_def_offset - 1) | ||
116 | .try_fold(trigger.parent()?, |t, _| t.parent()) | ||
117 | .and_then(ast::Impl::cast)?; | ||
118 | Some((trigger, impl_def)) | ||
119 | } | ||
120 | |||
121 | fn add_function_impl( | ||
122 | fn_def_node: &SyntaxNode, | ||
123 | acc: &mut Completions, | ||
124 | ctx: &CompletionContext, | ||
125 | func: hir::Function, | ||
126 | ) { | ||
127 | let fn_name = func.name(ctx.db).to_string(); | ||
128 | |||
129 | let label = if !func.params(ctx.db).is_empty() { | ||
130 | format!("fn {}(..)", fn_name) | ||
131 | } else { | ||
132 | format!("fn {}()", fn_name) | ||
133 | }; | ||
134 | |||
135 | let builder = CompletionItem::new(CompletionKind::Magic, ctx.source_range(), label) | ||
136 | .lookup_by(fn_name) | ||
137 | .set_documentation(func.docs(ctx.db)); | ||
138 | |||
139 | let completion_kind = if func.has_self_param(ctx.db) { | ||
140 | CompletionItemKind::Method | ||
141 | } else { | ||
142 | CompletionItemKind::Function | ||
143 | }; | ||
144 | let range = TextRange::new(fn_def_node.text_range().start(), ctx.source_range().end()); | ||
145 | |||
146 | let function_decl = function_declaration(&func.source(ctx.db).value); | ||
147 | match ctx.config.snippet_cap { | ||
148 | Some(cap) => { | ||
149 | let snippet = format!("{} {{\n $0\n}}", function_decl); | ||
150 | builder.snippet_edit(cap, TextEdit::replace(range, snippet)) | ||
151 | } | ||
152 | None => { | ||
153 | let header = format!("{} {{", function_decl); | ||
154 | builder.text_edit(TextEdit::replace(range, header)) | ||
155 | } | ||
156 | } | ||
157 | .kind(completion_kind) | ||
158 | .add_to(acc); | ||
159 | } | ||
160 | |||
161 | fn add_type_alias_impl( | ||
162 | type_def_node: &SyntaxNode, | ||
163 | acc: &mut Completions, | ||
164 | ctx: &CompletionContext, | ||
165 | type_alias: hir::TypeAlias, | ||
166 | ) { | ||
167 | let alias_name = type_alias.name(ctx.db).to_string(); | ||
168 | |||
169 | let snippet = format!("type {} = ", alias_name); | ||
170 | |||
171 | let range = TextRange::new(type_def_node.text_range().start(), ctx.source_range().end()); | ||
172 | |||
173 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), snippet.clone()) | ||
174 | .text_edit(TextEdit::replace(range, snippet)) | ||
175 | .lookup_by(alias_name) | ||
176 | .kind(CompletionItemKind::TypeAlias) | ||
177 | .set_documentation(type_alias.docs(ctx.db)) | ||
178 | .add_to(acc); | ||
179 | } | ||
180 | |||
181 | fn add_const_impl( | ||
182 | const_def_node: &SyntaxNode, | ||
183 | acc: &mut Completions, | ||
184 | ctx: &CompletionContext, | ||
185 | const_: hir::Const, | ||
186 | ) { | ||
187 | let const_name = const_.name(ctx.db).map(|n| n.to_string()); | ||
188 | |||
189 | if let Some(const_name) = const_name { | ||
190 | let snippet = make_const_compl_syntax(&const_.source(ctx.db).value); | ||
191 | |||
192 | let range = TextRange::new(const_def_node.text_range().start(), ctx.source_range().end()); | ||
193 | |||
194 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), snippet.clone()) | ||
195 | .text_edit(TextEdit::replace(range, snippet)) | ||
196 | .lookup_by(const_name) | ||
197 | .kind(CompletionItemKind::Const) | ||
198 | .set_documentation(const_.docs(ctx.db)) | ||
199 | .add_to(acc); | ||
200 | } | ||
201 | } | ||
202 | |||
203 | fn make_const_compl_syntax(const_: &ast::Const) -> String { | ||
204 | let const_ = edit::remove_attrs_and_docs(const_); | ||
205 | |||
206 | let const_start = const_.syntax().text_range().start(); | ||
207 | let const_end = const_.syntax().text_range().end(); | ||
208 | |||
209 | let start = | ||
210 | const_.syntax().first_child_or_token().map_or(const_start, |f| f.text_range().start()); | ||
211 | |||
212 | let end = const_ | ||
213 | .syntax() | ||
214 | .children_with_tokens() | ||
215 | .find(|s| s.kind() == T![;] || s.kind() == T![=]) | ||
216 | .map_or(const_end, |f| f.text_range().start()); | ||
217 | |||
218 | let len = end - start; | ||
219 | let range = TextRange::new(0.into(), len); | ||
220 | |||
221 | let syntax = const_.syntax().text().slice(range).to_string(); | ||
222 | |||
223 | format!("{} = ", syntax.trim_end()) | ||
224 | } | ||
225 | |||
226 | #[cfg(test)] | ||
227 | mod tests { | ||
228 | use expect::{expect, Expect}; | ||
229 | |||
230 | use crate::completion::{ | ||
231 | test_utils::{check_edit, completion_list}, | ||
232 | CompletionKind, | ||
233 | }; | ||
234 | |||
235 | fn check(ra_fixture: &str, expect: Expect) { | ||
236 | let actual = completion_list(ra_fixture, CompletionKind::Magic); | ||
237 | expect.assert_eq(&actual) | ||
238 | } | ||
239 | |||
240 | #[test] | ||
241 | fn name_ref_function_type_const() { | ||
242 | check( | ||
243 | r#" | ||
244 | trait Test { | ||
245 | type TestType; | ||
246 | const TEST_CONST: u16; | ||
247 | fn test(); | ||
248 | } | ||
249 | struct T; | ||
250 | |||
251 | impl Test for T { | ||
252 | t<|> | ||
253 | } | ||
254 | "#, | ||
255 | expect![[" | ||
256 | ct const TEST_CONST: u16 = \n\ | ||
257 | fn fn test() | ||
258 | ta type TestType = \n\ | ||
259 | "]], | ||
260 | ); | ||
261 | } | ||
262 | |||
263 | #[test] | ||
264 | fn no_nested_fn_completions() { | ||
265 | check( | ||
266 | r" | ||
267 | trait Test { | ||
268 | fn test(); | ||
269 | fn test2(); | ||
270 | } | ||
271 | struct T; | ||
272 | |||
273 | impl Test for T { | ||
274 | fn test() { | ||
275 | t<|> | ||
276 | } | ||
277 | } | ||
278 | ", | ||
279 | expect![[""]], | ||
280 | ); | ||
281 | } | ||
282 | |||
283 | #[test] | ||
284 | fn name_ref_single_function() { | ||
285 | check_edit( | ||
286 | "test", | ||
287 | r#" | ||
288 | trait Test { | ||
289 | fn test(); | ||
290 | } | ||
291 | struct T; | ||
292 | |||
293 | impl Test for T { | ||
294 | t<|> | ||
295 | } | ||
296 | "#, | ||
297 | r#" | ||
298 | trait Test { | ||
299 | fn test(); | ||
300 | } | ||
301 | struct T; | ||
302 | |||
303 | impl Test for T { | ||
304 | fn test() { | ||
305 | $0 | ||
306 | } | ||
307 | } | ||
308 | "#, | ||
309 | ); | ||
310 | } | ||
311 | |||
312 | #[test] | ||
313 | fn single_function() { | ||
314 | check_edit( | ||
315 | "test", | ||
316 | r#" | ||
317 | trait Test { | ||
318 | fn test(); | ||
319 | } | ||
320 | struct T; | ||
321 | |||
322 | impl Test for T { | ||
323 | fn t<|> | ||
324 | } | ||
325 | "#, | ||
326 | r#" | ||
327 | trait Test { | ||
328 | fn test(); | ||
329 | } | ||
330 | struct T; | ||
331 | |||
332 | impl Test for T { | ||
333 | fn test() { | ||
334 | $0 | ||
335 | } | ||
336 | } | ||
337 | "#, | ||
338 | ); | ||
339 | } | ||
340 | |||
341 | #[test] | ||
342 | fn hide_implemented_fn() { | ||
343 | check( | ||
344 | r#" | ||
345 | trait Test { | ||
346 | fn foo(); | ||
347 | fn foo_bar(); | ||
348 | } | ||
349 | struct T; | ||
350 | |||
351 | impl Test for T { | ||
352 | fn foo() {} | ||
353 | fn f<|> | ||
354 | } | ||
355 | "#, | ||
356 | expect![[r#" | ||
357 | fn fn foo_bar() | ||
358 | "#]], | ||
359 | ); | ||
360 | } | ||
361 | |||
362 | #[test] | ||
363 | fn generic_fn() { | ||
364 | check_edit( | ||
365 | "foo", | ||
366 | r#" | ||
367 | trait Test { | ||
368 | fn foo<T>(); | ||
369 | } | ||
370 | struct T; | ||
371 | |||
372 | impl Test for T { | ||
373 | fn f<|> | ||
374 | } | ||
375 | "#, | ||
376 | r#" | ||
377 | trait Test { | ||
378 | fn foo<T>(); | ||
379 | } | ||
380 | struct T; | ||
381 | |||
382 | impl Test for T { | ||
383 | fn foo<T>() { | ||
384 | $0 | ||
385 | } | ||
386 | } | ||
387 | "#, | ||
388 | ); | ||
389 | check_edit( | ||
390 | "foo", | ||
391 | r#" | ||
392 | trait Test { | ||
393 | fn foo<T>() where T: Into<String>; | ||
394 | } | ||
395 | struct T; | ||
396 | |||
397 | impl Test for T { | ||
398 | fn f<|> | ||
399 | } | ||
400 | "#, | ||
401 | r#" | ||
402 | trait Test { | ||
403 | fn foo<T>() where T: Into<String>; | ||
404 | } | ||
405 | struct T; | ||
406 | |||
407 | impl Test for T { | ||
408 | fn foo<T>() | ||
409 | where T: Into<String> { | ||
410 | $0 | ||
411 | } | ||
412 | } | ||
413 | "#, | ||
414 | ); | ||
415 | } | ||
416 | |||
417 | #[test] | ||
418 | fn associated_type() { | ||
419 | check_edit( | ||
420 | "SomeType", | ||
421 | r#" | ||
422 | trait Test { | ||
423 | type SomeType; | ||
424 | } | ||
425 | |||
426 | impl Test for () { | ||
427 | type S<|> | ||
428 | } | ||
429 | "#, | ||
430 | " | ||
431 | trait Test { | ||
432 | type SomeType; | ||
433 | } | ||
434 | |||
435 | impl Test for () { | ||
436 | type SomeType = \n\ | ||
437 | } | ||
438 | ", | ||
439 | ); | ||
440 | } | ||
441 | |||
442 | #[test] | ||
443 | fn associated_const() { | ||
444 | check_edit( | ||
445 | "SOME_CONST", | ||
446 | r#" | ||
447 | trait Test { | ||
448 | const SOME_CONST: u16; | ||
449 | } | ||
450 | |||
451 | impl Test for () { | ||
452 | const S<|> | ||
453 | } | ||
454 | "#, | ||
455 | " | ||
456 | trait Test { | ||
457 | const SOME_CONST: u16; | ||
458 | } | ||
459 | |||
460 | impl Test for () { | ||
461 | const SOME_CONST: u16 = \n\ | ||
462 | } | ||
463 | ", | ||
464 | ); | ||
465 | |||
466 | check_edit( | ||
467 | "SOME_CONST", | ||
468 | r#" | ||
469 | trait Test { | ||
470 | const SOME_CONST: u16 = 92; | ||
471 | } | ||
472 | |||
473 | impl Test for () { | ||
474 | const S<|> | ||
475 | } | ||
476 | "#, | ||
477 | " | ||
478 | trait Test { | ||
479 | const SOME_CONST: u16 = 92; | ||
480 | } | ||
481 | |||
482 | impl Test for () { | ||
483 | const SOME_CONST: u16 = \n\ | ||
484 | } | ||
485 | ", | ||
486 | ); | ||
487 | } | ||
488 | } | ||
diff --git a/crates/ide/src/completion/complete_unqualified_path.rs b/crates/ide/src/completion/complete_unqualified_path.rs new file mode 100644 index 000000000..824227f31 --- /dev/null +++ b/crates/ide/src/completion/complete_unqualified_path.rs | |||
@@ -0,0 +1,658 @@ | |||
1 | //! Completion of names from the current scope, e.g. locals and imported items. | ||
2 | |||
3 | use hir::{Adt, ModuleDef, ScopeDef, Type}; | ||
4 | use syntax::AstNode; | ||
5 | use test_utils::mark; | ||
6 | |||
7 | use crate::completion::{CompletionContext, Completions}; | ||
8 | |||
9 | pub(super) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionContext) { | ||
10 | if !(ctx.is_trivial_path || ctx.is_pat_binding_or_const) { | ||
11 | return; | ||
12 | } | ||
13 | if ctx.record_lit_syntax.is_some() | ||
14 | || ctx.record_pat_syntax.is_some() | ||
15 | || ctx.attribute_under_caret.is_some() | ||
16 | { | ||
17 | return; | ||
18 | } | ||
19 | |||
20 | if let Some(ty) = &ctx.expected_type { | ||
21 | complete_enum_variants(acc, ctx, ty); | ||
22 | } | ||
23 | |||
24 | if ctx.is_pat_binding_or_const { | ||
25 | return; | ||
26 | } | ||
27 | |||
28 | ctx.scope.process_all_names(&mut |name, res| { | ||
29 | if ctx.use_item_syntax.is_some() { | ||
30 | if let (ScopeDef::Unknown, Some(name_ref)) = (&res, &ctx.name_ref_syntax) { | ||
31 | if name_ref.syntax().text() == name.to_string().as_str() { | ||
32 | mark::hit!(self_fulfilling_completion); | ||
33 | return; | ||
34 | } | ||
35 | } | ||
36 | } | ||
37 | acc.add_resolution(ctx, name.to_string(), &res) | ||
38 | }); | ||
39 | } | ||
40 | |||
41 | fn complete_enum_variants(acc: &mut Completions, ctx: &CompletionContext, ty: &Type) { | ||
42 | if let Some(Adt::Enum(enum_data)) = ty.as_adt() { | ||
43 | let variants = enum_data.variants(ctx.db); | ||
44 | |||
45 | let module = if let Some(module) = ctx.scope.module() { | ||
46 | // Compute path from the completion site if available. | ||
47 | module | ||
48 | } else { | ||
49 | // Otherwise fall back to the enum's definition site. | ||
50 | enum_data.module(ctx.db) | ||
51 | }; | ||
52 | |||
53 | for variant in variants { | ||
54 | if let Some(path) = module.find_use_path(ctx.db, ModuleDef::from(variant)) { | ||
55 | // Variants with trivial paths are already added by the existing completion logic, | ||
56 | // so we should avoid adding these twice | ||
57 | if path.segments.len() > 1 { | ||
58 | acc.add_qualified_enum_variant(ctx, variant, path); | ||
59 | } | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
65 | #[cfg(test)] | ||
66 | mod tests { | ||
67 | use expect::{expect, Expect}; | ||
68 | use test_utils::mark; | ||
69 | |||
70 | use crate::completion::{ | ||
71 | test_utils::{check_edit, completion_list}, | ||
72 | CompletionKind, | ||
73 | }; | ||
74 | |||
75 | fn check(ra_fixture: &str, expect: Expect) { | ||
76 | let actual = completion_list(ra_fixture, CompletionKind::Reference); | ||
77 | expect.assert_eq(&actual) | ||
78 | } | ||
79 | |||
80 | #[test] | ||
81 | fn self_fulfilling_completion() { | ||
82 | mark::check!(self_fulfilling_completion); | ||
83 | check( | ||
84 | r#" | ||
85 | use foo<|> | ||
86 | use std::collections; | ||
87 | "#, | ||
88 | expect![[r#" | ||
89 | ?? collections | ||
90 | "#]], | ||
91 | ); | ||
92 | } | ||
93 | |||
94 | #[test] | ||
95 | fn bind_pat_and_path_ignore_at() { | ||
96 | check( | ||
97 | r#" | ||
98 | enum Enum { A, B } | ||
99 | fn quux(x: Option<Enum>) { | ||
100 | match x { | ||
101 | None => (), | ||
102 | Some(en<|> @ Enum::A) => (), | ||
103 | } | ||
104 | } | ||
105 | "#, | ||
106 | expect![[""]], | ||
107 | ); | ||
108 | } | ||
109 | |||
110 | #[test] | ||
111 | fn bind_pat_and_path_ignore_ref() { | ||
112 | check( | ||
113 | r#" | ||
114 | enum Enum { A, B } | ||
115 | fn quux(x: Option<Enum>) { | ||
116 | match x { | ||
117 | None => (), | ||
118 | Some(ref en<|>) => (), | ||
119 | } | ||
120 | } | ||
121 | "#, | ||
122 | expect![[""]], | ||
123 | ); | ||
124 | } | ||
125 | |||
126 | #[test] | ||
127 | fn bind_pat_and_path() { | ||
128 | check( | ||
129 | r#" | ||
130 | enum Enum { A, B } | ||
131 | fn quux(x: Option<Enum>) { | ||
132 | match x { | ||
133 | None => (), | ||
134 | Some(En<|>) => (), | ||
135 | } | ||
136 | } | ||
137 | "#, | ||
138 | expect![[r#" | ||
139 | en Enum | ||
140 | "#]], | ||
141 | ); | ||
142 | } | ||
143 | |||
144 | #[test] | ||
145 | fn completes_bindings_from_let() { | ||
146 | check( | ||
147 | r#" | ||
148 | fn quux(x: i32) { | ||
149 | let y = 92; | ||
150 | 1 + <|>; | ||
151 | let z = (); | ||
152 | } | ||
153 | "#, | ||
154 | expect![[r#" | ||
155 | fn quux(…) fn quux(x: i32) | ||
156 | bn x i32 | ||
157 | bn y i32 | ||
158 | "#]], | ||
159 | ); | ||
160 | } | ||
161 | |||
162 | #[test] | ||
163 | fn completes_bindings_from_if_let() { | ||
164 | check( | ||
165 | r#" | ||
166 | fn quux() { | ||
167 | if let Some(x) = foo() { | ||
168 | let y = 92; | ||
169 | }; | ||
170 | if let Some(a) = bar() { | ||
171 | let b = 62; | ||
172 | 1 + <|> | ||
173 | } | ||
174 | } | ||
175 | "#, | ||
176 | expect![[r#" | ||
177 | bn a | ||
178 | bn b i32 | ||
179 | fn quux() fn quux() | ||
180 | "#]], | ||
181 | ); | ||
182 | } | ||
183 | |||
184 | #[test] | ||
185 | fn completes_bindings_from_for() { | ||
186 | check( | ||
187 | r#" | ||
188 | fn quux() { | ||
189 | for x in &[1, 2, 3] { <|> } | ||
190 | } | ||
191 | "#, | ||
192 | expect![[r#" | ||
193 | fn quux() fn quux() | ||
194 | bn x | ||
195 | "#]], | ||
196 | ); | ||
197 | } | ||
198 | |||
199 | #[test] | ||
200 | fn completes_if_prefix_is_keyword() { | ||
201 | mark::check!(completes_if_prefix_is_keyword); | ||
202 | check_edit( | ||
203 | "wherewolf", | ||
204 | r#" | ||
205 | fn main() { | ||
206 | let wherewolf = 92; | ||
207 | drop(where<|>) | ||
208 | } | ||
209 | "#, | ||
210 | r#" | ||
211 | fn main() { | ||
212 | let wherewolf = 92; | ||
213 | drop(wherewolf) | ||
214 | } | ||
215 | "#, | ||
216 | ) | ||
217 | } | ||
218 | |||
219 | #[test] | ||
220 | fn completes_generic_params() { | ||
221 | check( | ||
222 | r#"fn quux<T>() { <|> }"#, | ||
223 | expect![[r#" | ||
224 | tp T | ||
225 | fn quux() fn quux<T>() | ||
226 | "#]], | ||
227 | ); | ||
228 | } | ||
229 | |||
230 | #[test] | ||
231 | fn completes_generic_params_in_struct() { | ||
232 | check( | ||
233 | r#"struct S<T> { x: <|>}"#, | ||
234 | expect![[r#" | ||
235 | st S<…> | ||
236 | tp Self | ||
237 | tp T | ||
238 | "#]], | ||
239 | ); | ||
240 | } | ||
241 | |||
242 | #[test] | ||
243 | fn completes_self_in_enum() { | ||
244 | check( | ||
245 | r#"enum X { Y(<|>) }"#, | ||
246 | expect![[r#" | ||
247 | tp Self | ||
248 | en X | ||
249 | "#]], | ||
250 | ); | ||
251 | } | ||
252 | |||
253 | #[test] | ||
254 | fn completes_module_items() { | ||
255 | check( | ||
256 | r#" | ||
257 | struct S; | ||
258 | enum E {} | ||
259 | fn quux() { <|> } | ||
260 | "#, | ||
261 | expect![[r#" | ||
262 | en E | ||
263 | st S | ||
264 | fn quux() fn quux() | ||
265 | "#]], | ||
266 | ); | ||
267 | } | ||
268 | |||
269 | #[test] | ||
270 | fn completes_extern_prelude() { | ||
271 | check( | ||
272 | r#" | ||
273 | //- /lib.rs | ||
274 | use <|>; | ||
275 | |||
276 | //- /other_crate/lib.rs | ||
277 | // nothing here | ||
278 | "#, | ||
279 | expect![[r#" | ||
280 | md other_crate | ||
281 | "#]], | ||
282 | ); | ||
283 | } | ||
284 | |||
285 | #[test] | ||
286 | fn completes_module_items_in_nested_modules() { | ||
287 | check( | ||
288 | r#" | ||
289 | struct Foo; | ||
290 | mod m { | ||
291 | struct Bar; | ||
292 | fn quux() { <|> } | ||
293 | } | ||
294 | "#, | ||
295 | expect![[r#" | ||
296 | st Bar | ||
297 | fn quux() fn quux() | ||
298 | "#]], | ||
299 | ); | ||
300 | } | ||
301 | |||
302 | #[test] | ||
303 | fn completes_return_type() { | ||
304 | check( | ||
305 | r#" | ||
306 | struct Foo; | ||
307 | fn x() -> <|> | ||
308 | "#, | ||
309 | expect![[r#" | ||
310 | st Foo | ||
311 | fn x() fn x() | ||
312 | "#]], | ||
313 | ); | ||
314 | } | ||
315 | |||
316 | #[test] | ||
317 | fn dont_show_both_completions_for_shadowing() { | ||
318 | check( | ||
319 | r#" | ||
320 | fn foo() { | ||
321 | let bar = 92; | ||
322 | { | ||
323 | let bar = 62; | ||
324 | drop(<|>) | ||
325 | } | ||
326 | } | ||
327 | "#, | ||
328 | // FIXME: should be only one bar here | ||
329 | expect![[r#" | ||
330 | bn bar i32 | ||
331 | bn bar i32 | ||
332 | fn foo() fn foo() | ||
333 | "#]], | ||
334 | ); | ||
335 | } | ||
336 | |||
337 | #[test] | ||
338 | fn completes_self_in_methods() { | ||
339 | check( | ||
340 | r#"impl S { fn foo(&self) { <|> } }"#, | ||
341 | expect![[r#" | ||
342 | tp Self | ||
343 | bn self &{unknown} | ||
344 | "#]], | ||
345 | ); | ||
346 | } | ||
347 | |||
348 | #[test] | ||
349 | fn completes_prelude() { | ||
350 | check( | ||
351 | r#" | ||
352 | //- /main.rs | ||
353 | fn foo() { let x: <|> } | ||
354 | |||
355 | //- /std/lib.rs | ||
356 | #[prelude_import] | ||
357 | use prelude::*; | ||
358 | |||
359 | mod prelude { struct Option; } | ||
360 | "#, | ||
361 | expect![[r#" | ||
362 | st Option | ||
363 | fn foo() fn foo() | ||
364 | md std | ||
365 | "#]], | ||
366 | ); | ||
367 | } | ||
368 | |||
369 | #[test] | ||
370 | fn completes_std_prelude_if_core_is_defined() { | ||
371 | check( | ||
372 | r#" | ||
373 | //- /main.rs | ||
374 | fn foo() { let x: <|> } | ||
375 | |||
376 | //- /core/lib.rs | ||
377 | #[prelude_import] | ||
378 | use prelude::*; | ||
379 | |||
380 | mod prelude { struct Option; } | ||
381 | |||
382 | //- /std/lib.rs | ||
383 | #[prelude_import] | ||
384 | use prelude::*; | ||
385 | |||
386 | mod prelude { struct String; } | ||
387 | "#, | ||
388 | expect![[r#" | ||
389 | st String | ||
390 | md core | ||
391 | fn foo() fn foo() | ||
392 | md std | ||
393 | "#]], | ||
394 | ); | ||
395 | } | ||
396 | |||
397 | #[test] | ||
398 | fn completes_macros_as_value() { | ||
399 | check( | ||
400 | r#" | ||
401 | macro_rules! foo { () => {} } | ||
402 | |||
403 | #[macro_use] | ||
404 | mod m1 { | ||
405 | macro_rules! bar { () => {} } | ||
406 | } | ||
407 | |||
408 | mod m2 { | ||
409 | macro_rules! nope { () => {} } | ||
410 | |||
411 | #[macro_export] | ||
412 | macro_rules! baz { () => {} } | ||
413 | } | ||
414 | |||
415 | fn main() { let v = <|> } | ||
416 | "#, | ||
417 | expect![[r##" | ||
418 | ma bar!(…) macro_rules! bar | ||
419 | ma baz!(…) #[macro_export] | ||
420 | macro_rules! baz | ||
421 | ma foo!(…) macro_rules! foo | ||
422 | md m1 | ||
423 | md m2 | ||
424 | fn main() fn main() | ||
425 | "##]], | ||
426 | ); | ||
427 | } | ||
428 | |||
429 | #[test] | ||
430 | fn completes_both_macro_and_value() { | ||
431 | check( | ||
432 | r#" | ||
433 | macro_rules! foo { () => {} } | ||
434 | fn foo() { <|> } | ||
435 | "#, | ||
436 | expect![[r#" | ||
437 | ma foo!(…) macro_rules! foo | ||
438 | fn foo() fn foo() | ||
439 | "#]], | ||
440 | ); | ||
441 | } | ||
442 | |||
443 | #[test] | ||
444 | fn completes_macros_as_type() { | ||
445 | check( | ||
446 | r#" | ||
447 | macro_rules! foo { () => {} } | ||
448 | fn main() { let x: <|> } | ||
449 | "#, | ||
450 | expect![[r#" | ||
451 | ma foo!(…) macro_rules! foo | ||
452 | fn main() fn main() | ||
453 | "#]], | ||
454 | ); | ||
455 | } | ||
456 | |||
457 | #[test] | ||
458 | fn completes_macros_as_stmt() { | ||
459 | check( | ||
460 | r#" | ||
461 | macro_rules! foo { () => {} } | ||
462 | fn main() { <|> } | ||
463 | "#, | ||
464 | expect![[r#" | ||
465 | ma foo!(…) macro_rules! foo | ||
466 | fn main() fn main() | ||
467 | "#]], | ||
468 | ); | ||
469 | } | ||
470 | |||
471 | #[test] | ||
472 | fn completes_local_item() { | ||
473 | check( | ||
474 | r#" | ||
475 | fn main() { | ||
476 | return f<|>; | ||
477 | fn frobnicate() {} | ||
478 | } | ||
479 | "#, | ||
480 | expect![[r#" | ||
481 | fn frobnicate() fn frobnicate() | ||
482 | fn main() fn main() | ||
483 | "#]], | ||
484 | ); | ||
485 | } | ||
486 | |||
487 | #[test] | ||
488 | fn completes_in_simple_macro_1() { | ||
489 | check( | ||
490 | r#" | ||
491 | macro_rules! m { ($e:expr) => { $e } } | ||
492 | fn quux(x: i32) { | ||
493 | let y = 92; | ||
494 | m!(<|>); | ||
495 | } | ||
496 | "#, | ||
497 | expect![[r#" | ||
498 | ma m!(…) macro_rules! m | ||
499 | fn quux(…) fn quux(x: i32) | ||
500 | bn x i32 | ||
501 | bn y i32 | ||
502 | "#]], | ||
503 | ); | ||
504 | } | ||
505 | |||
506 | #[test] | ||
507 | fn completes_in_simple_macro_2() { | ||
508 | check( | ||
509 | r" | ||
510 | macro_rules! m { ($e:expr) => { $e } } | ||
511 | fn quux(x: i32) { | ||
512 | let y = 92; | ||
513 | m!(x<|>); | ||
514 | } | ||
515 | ", | ||
516 | expect![[r#" | ||
517 | ma m!(…) macro_rules! m | ||
518 | fn quux(…) fn quux(x: i32) | ||
519 | bn x i32 | ||
520 | bn y i32 | ||
521 | "#]], | ||
522 | ); | ||
523 | } | ||
524 | |||
525 | #[test] | ||
526 | fn completes_in_simple_macro_without_closing_parens() { | ||
527 | check( | ||
528 | r#" | ||
529 | macro_rules! m { ($e:expr) => { $e } } | ||
530 | fn quux(x: i32) { | ||
531 | let y = 92; | ||
532 | m!(x<|> | ||
533 | } | ||
534 | "#, | ||
535 | expect![[r#" | ||
536 | ma m!(…) macro_rules! m | ||
537 | fn quux(…) fn quux(x: i32) | ||
538 | bn x i32 | ||
539 | bn y i32 | ||
540 | "#]], | ||
541 | ); | ||
542 | } | ||
543 | |||
544 | #[test] | ||
545 | fn completes_unresolved_uses() { | ||
546 | check( | ||
547 | r#" | ||
548 | use spam::Quux; | ||
549 | |||
550 | fn main() { <|> } | ||
551 | "#, | ||
552 | expect![[r#" | ||
553 | ?? Quux | ||
554 | fn main() fn main() | ||
555 | "#]], | ||
556 | ); | ||
557 | } | ||
558 | #[test] | ||
559 | fn completes_enum_variant_matcharm() { | ||
560 | check( | ||
561 | r#" | ||
562 | enum Foo { Bar, Baz, Quux } | ||
563 | |||
564 | fn main() { | ||
565 | let foo = Foo::Quux; | ||
566 | match foo { Qu<|> } | ||
567 | } | ||
568 | "#, | ||
569 | expect![[r#" | ||
570 | en Foo | ||
571 | ev Foo::Bar () | ||
572 | ev Foo::Baz () | ||
573 | ev Foo::Quux () | ||
574 | "#]], | ||
575 | ) | ||
576 | } | ||
577 | |||
578 | #[test] | ||
579 | fn completes_enum_variant_iflet() { | ||
580 | check( | ||
581 | r#" | ||
582 | enum Foo { Bar, Baz, Quux } | ||
583 | |||
584 | fn main() { | ||
585 | let foo = Foo::Quux; | ||
586 | if let Qu<|> = foo { } | ||
587 | } | ||
588 | "#, | ||
589 | expect![[r#" | ||
590 | en Foo | ||
591 | ev Foo::Bar () | ||
592 | ev Foo::Baz () | ||
593 | ev Foo::Quux () | ||
594 | "#]], | ||
595 | ) | ||
596 | } | ||
597 | |||
598 | #[test] | ||
599 | fn completes_enum_variant_basic_expr() { | ||
600 | check( | ||
601 | r#" | ||
602 | enum Foo { Bar, Baz, Quux } | ||
603 | fn main() { let foo: Foo = Q<|> } | ||
604 | "#, | ||
605 | expect![[r#" | ||
606 | en Foo | ||
607 | ev Foo::Bar () | ||
608 | ev Foo::Baz () | ||
609 | ev Foo::Quux () | ||
610 | fn main() fn main() | ||
611 | "#]], | ||
612 | ) | ||
613 | } | ||
614 | |||
615 | #[test] | ||
616 | fn completes_enum_variant_from_module() { | ||
617 | check( | ||
618 | r#" | ||
619 | mod m { pub enum E { V } } | ||
620 | fn f() -> m::E { V<|> } | ||
621 | "#, | ||
622 | expect![[r#" | ||
623 | fn f() fn f() -> m::E | ||
624 | md m | ||
625 | ev m::E::V () | ||
626 | "#]], | ||
627 | ) | ||
628 | } | ||
629 | |||
630 | #[test] | ||
631 | fn dont_complete_attr() { | ||
632 | check( | ||
633 | r#" | ||
634 | struct Foo; | ||
635 | #[<|>] | ||
636 | fn f() {} | ||
637 | "#, | ||
638 | expect![[""]], | ||
639 | ) | ||
640 | } | ||
641 | |||
642 | #[test] | ||
643 | fn completes_type_or_trait_in_impl_block() { | ||
644 | check( | ||
645 | r#" | ||
646 | trait MyTrait {} | ||
647 | struct MyStruct {} | ||
648 | |||
649 | impl My<|> | ||
650 | "#, | ||
651 | expect![[r#" | ||
652 | st MyStruct | ||
653 | tt MyTrait | ||
654 | tp Self | ||
655 | "#]], | ||
656 | ) | ||
657 | } | ||
658 | } | ||
diff --git a/crates/ide/src/completion/completion_config.rs b/crates/ide/src/completion/completion_config.rs new file mode 100644 index 000000000..71b49ace8 --- /dev/null +++ b/crates/ide/src/completion/completion_config.rs | |||
@@ -0,0 +1,35 @@ | |||
1 | //! Settings for tweaking completion. | ||
2 | //! | ||
3 | //! The fun thing here is `SnippetCap` -- this type can only be created in this | ||
4 | //! module, and we use to statically check that we only produce snippet | ||
5 | //! completions if we are allowed to. | ||
6 | |||
7 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
8 | pub struct CompletionConfig { | ||
9 | pub enable_postfix_completions: bool, | ||
10 | pub add_call_parenthesis: bool, | ||
11 | pub add_call_argument_snippets: bool, | ||
12 | pub snippet_cap: Option<SnippetCap>, | ||
13 | } | ||
14 | |||
15 | impl CompletionConfig { | ||
16 | pub fn allow_snippets(&mut self, yes: bool) { | ||
17 | self.snippet_cap = if yes { Some(SnippetCap { _private: () }) } else { None } | ||
18 | } | ||
19 | } | ||
20 | |||
21 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | ||
22 | pub struct SnippetCap { | ||
23 | _private: (), | ||
24 | } | ||
25 | |||
26 | impl Default for CompletionConfig { | ||
27 | fn default() -> Self { | ||
28 | CompletionConfig { | ||
29 | enable_postfix_completions: true, | ||
30 | add_call_parenthesis: true, | ||
31 | add_call_argument_snippets: true, | ||
32 | snippet_cap: Some(SnippetCap { _private: () }), | ||
33 | } | ||
34 | } | ||
35 | } | ||
diff --git a/crates/ide/src/completion/completion_context.rs b/crates/ide/src/completion/completion_context.rs new file mode 100644 index 000000000..047ecd9d7 --- /dev/null +++ b/crates/ide/src/completion/completion_context.rs | |||
@@ -0,0 +1,465 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use base_db::SourceDatabase; | ||
4 | use hir::{Semantics, SemanticsScope, Type}; | ||
5 | use ide_db::RootDatabase; | ||
6 | use syntax::{ | ||
7 | algo::{find_covering_element, find_node_at_offset}, | ||
8 | ast, match_ast, AstNode, NodeOrToken, | ||
9 | SyntaxKind::*, | ||
10 | SyntaxNode, SyntaxToken, TextRange, TextSize, | ||
11 | }; | ||
12 | use text_edit::Indel; | ||
13 | |||
14 | use super::patterns::{ | ||
15 | has_bind_pat_parent, has_block_expr_parent, has_impl_as_prev_sibling, has_impl_parent, | ||
16 | has_item_list_or_source_file_parent, has_ref_parent, has_trait_as_prev_sibling, | ||
17 | has_trait_parent, if_is_prev, is_in_loop_body, is_match_arm, unsafe_is_prev, | ||
18 | }; | ||
19 | use crate::{call_info::ActiveParameter, completion::CompletionConfig, FilePosition}; | ||
20 | use test_utils::mark; | ||
21 | |||
22 | /// `CompletionContext` is created early during completion to figure out, where | ||
23 | /// exactly is the cursor, syntax-wise. | ||
24 | #[derive(Debug)] | ||
25 | pub(crate) struct CompletionContext<'a> { | ||
26 | pub(super) sema: Semantics<'a, RootDatabase>, | ||
27 | pub(super) scope: SemanticsScope<'a>, | ||
28 | pub(super) db: &'a RootDatabase, | ||
29 | pub(super) config: &'a CompletionConfig, | ||
30 | pub(super) position: FilePosition, | ||
31 | /// The token before the cursor, in the original file. | ||
32 | pub(super) original_token: SyntaxToken, | ||
33 | /// The token before the cursor, in the macro-expanded file. | ||
34 | pub(super) token: SyntaxToken, | ||
35 | pub(super) krate: Option<hir::Crate>, | ||
36 | pub(super) expected_type: Option<Type>, | ||
37 | pub(super) name_ref_syntax: Option<ast::NameRef>, | ||
38 | pub(super) function_syntax: Option<ast::Fn>, | ||
39 | pub(super) use_item_syntax: Option<ast::Use>, | ||
40 | pub(super) record_lit_syntax: Option<ast::RecordExpr>, | ||
41 | pub(super) record_pat_syntax: Option<ast::RecordPat>, | ||
42 | pub(super) record_field_syntax: Option<ast::RecordExprField>, | ||
43 | pub(super) impl_def: Option<ast::Impl>, | ||
44 | /// FIXME: `ActiveParameter` is string-based, which is very very wrong | ||
45 | pub(super) active_parameter: Option<ActiveParameter>, | ||
46 | pub(super) is_param: bool, | ||
47 | /// If a name-binding or reference to a const in a pattern. | ||
48 | /// Irrefutable patterns (like let) are excluded. | ||
49 | pub(super) is_pat_binding_or_const: bool, | ||
50 | /// A single-indent path, like `foo`. `::foo` should not be considered a trivial path. | ||
51 | pub(super) is_trivial_path: bool, | ||
52 | /// If not a trivial path, the prefix (qualifier). | ||
53 | pub(super) path_prefix: Option<hir::Path>, | ||
54 | pub(super) after_if: bool, | ||
55 | /// `true` if we are a statement or a last expr in the block. | ||
56 | pub(super) can_be_stmt: bool, | ||
57 | /// `true` if we expect an expression at the cursor position. | ||
58 | pub(super) is_expr: bool, | ||
59 | /// Something is typed at the "top" level, in module or impl/trait. | ||
60 | pub(super) is_new_item: bool, | ||
61 | /// The receiver if this is a field or method access, i.e. writing something.<|> | ||
62 | pub(super) dot_receiver: Option<ast::Expr>, | ||
63 | pub(super) dot_receiver_is_ambiguous_float_literal: bool, | ||
64 | /// If this is a call (method or function) in particular, i.e. the () are already there. | ||
65 | pub(super) is_call: bool, | ||
66 | /// Like `is_call`, but for tuple patterns. | ||
67 | pub(super) is_pattern_call: bool, | ||
68 | /// If this is a macro call, i.e. the () are already there. | ||
69 | pub(super) is_macro_call: bool, | ||
70 | pub(super) is_path_type: bool, | ||
71 | pub(super) has_type_args: bool, | ||
72 | pub(super) attribute_under_caret: Option<ast::Attr>, | ||
73 | pub(super) unsafe_is_prev: bool, | ||
74 | pub(super) if_is_prev: bool, | ||
75 | pub(super) block_expr_parent: bool, | ||
76 | pub(super) bind_pat_parent: bool, | ||
77 | pub(super) ref_pat_parent: bool, | ||
78 | pub(super) in_loop_body: bool, | ||
79 | pub(super) has_trait_parent: bool, | ||
80 | pub(super) has_impl_parent: bool, | ||
81 | pub(super) trait_as_prev_sibling: bool, | ||
82 | pub(super) impl_as_prev_sibling: bool, | ||
83 | pub(super) is_match_arm: bool, | ||
84 | pub(super) has_item_list_or_source_file_parent: bool, | ||
85 | } | ||
86 | |||
87 | impl<'a> CompletionContext<'a> { | ||
88 | pub(super) fn new( | ||
89 | db: &'a RootDatabase, | ||
90 | position: FilePosition, | ||
91 | config: &'a CompletionConfig, | ||
92 | ) -> Option<CompletionContext<'a>> { | ||
93 | let sema = Semantics::new(db); | ||
94 | |||
95 | let original_file = sema.parse(position.file_id); | ||
96 | |||
97 | // Insert a fake ident to get a valid parse tree. We will use this file | ||
98 | // to determine context, though the original_file will be used for | ||
99 | // actual completion. | ||
100 | let file_with_fake_ident = { | ||
101 | let parse = db.parse(position.file_id); | ||
102 | let edit = Indel::insert(position.offset, "intellijRulezz".to_string()); | ||
103 | parse.reparse(&edit).tree() | ||
104 | }; | ||
105 | let fake_ident_token = | ||
106 | file_with_fake_ident.syntax().token_at_offset(position.offset).right_biased().unwrap(); | ||
107 | |||
108 | let krate = sema.to_module_def(position.file_id).map(|m| m.krate()); | ||
109 | let original_token = | ||
110 | original_file.syntax().token_at_offset(position.offset).left_biased()?; | ||
111 | let token = sema.descend_into_macros(original_token.clone()); | ||
112 | let scope = sema.scope_at_offset(&token.parent(), position.offset); | ||
113 | let mut ctx = CompletionContext { | ||
114 | sema, | ||
115 | scope, | ||
116 | db, | ||
117 | config, | ||
118 | original_token, | ||
119 | token, | ||
120 | position, | ||
121 | krate, | ||
122 | expected_type: None, | ||
123 | name_ref_syntax: None, | ||
124 | function_syntax: None, | ||
125 | use_item_syntax: None, | ||
126 | record_lit_syntax: None, | ||
127 | record_pat_syntax: None, | ||
128 | record_field_syntax: None, | ||
129 | impl_def: None, | ||
130 | active_parameter: ActiveParameter::at(db, position), | ||
131 | is_param: false, | ||
132 | is_pat_binding_or_const: false, | ||
133 | is_trivial_path: false, | ||
134 | path_prefix: None, | ||
135 | after_if: false, | ||
136 | can_be_stmt: false, | ||
137 | is_expr: false, | ||
138 | is_new_item: false, | ||
139 | dot_receiver: None, | ||
140 | is_call: false, | ||
141 | is_pattern_call: false, | ||
142 | is_macro_call: false, | ||
143 | is_path_type: false, | ||
144 | has_type_args: false, | ||
145 | dot_receiver_is_ambiguous_float_literal: false, | ||
146 | attribute_under_caret: None, | ||
147 | unsafe_is_prev: false, | ||
148 | in_loop_body: false, | ||
149 | ref_pat_parent: false, | ||
150 | bind_pat_parent: false, | ||
151 | block_expr_parent: false, | ||
152 | has_trait_parent: false, | ||
153 | has_impl_parent: false, | ||
154 | trait_as_prev_sibling: false, | ||
155 | impl_as_prev_sibling: false, | ||
156 | if_is_prev: false, | ||
157 | is_match_arm: false, | ||
158 | has_item_list_or_source_file_parent: false, | ||
159 | }; | ||
160 | |||
161 | let mut original_file = original_file.syntax().clone(); | ||
162 | let mut hypothetical_file = file_with_fake_ident.syntax().clone(); | ||
163 | let mut offset = position.offset; | ||
164 | let mut fake_ident_token = fake_ident_token; | ||
165 | |||
166 | // Are we inside a macro call? | ||
167 | while let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = ( | ||
168 | find_node_at_offset::<ast::MacroCall>(&original_file, offset), | ||
169 | find_node_at_offset::<ast::MacroCall>(&hypothetical_file, offset), | ||
170 | ) { | ||
171 | if actual_macro_call.path().as_ref().map(|s| s.syntax().text()) | ||
172 | != macro_call_with_fake_ident.path().as_ref().map(|s| s.syntax().text()) | ||
173 | { | ||
174 | break; | ||
175 | } | ||
176 | let hypothetical_args = match macro_call_with_fake_ident.token_tree() { | ||
177 | Some(tt) => tt, | ||
178 | None => break, | ||
179 | }; | ||
180 | if let (Some(actual_expansion), Some(hypothetical_expansion)) = ( | ||
181 | ctx.sema.expand(&actual_macro_call), | ||
182 | ctx.sema.expand_hypothetical( | ||
183 | &actual_macro_call, | ||
184 | &hypothetical_args, | ||
185 | fake_ident_token, | ||
186 | ), | ||
187 | ) { | ||
188 | let new_offset = hypothetical_expansion.1.text_range().start(); | ||
189 | if new_offset > actual_expansion.text_range().end() { | ||
190 | break; | ||
191 | } | ||
192 | original_file = actual_expansion; | ||
193 | hypothetical_file = hypothetical_expansion.0; | ||
194 | fake_ident_token = hypothetical_expansion.1; | ||
195 | offset = new_offset; | ||
196 | } else { | ||
197 | break; | ||
198 | } | ||
199 | } | ||
200 | ctx.fill_keyword_patterns(&hypothetical_file, offset); | ||
201 | ctx.fill(&original_file, hypothetical_file, offset); | ||
202 | Some(ctx) | ||
203 | } | ||
204 | |||
205 | // The range of the identifier that is being completed. | ||
206 | pub(crate) fn source_range(&self) -> TextRange { | ||
207 | // check kind of macro-expanded token, but use range of original token | ||
208 | if self.token.kind() == IDENT || self.token.kind().is_keyword() { | ||
209 | mark::hit!(completes_if_prefix_is_keyword); | ||
210 | self.original_token.text_range() | ||
211 | } else { | ||
212 | TextRange::empty(self.position.offset) | ||
213 | } | ||
214 | } | ||
215 | |||
216 | fn fill_keyword_patterns(&mut self, file_with_fake_ident: &SyntaxNode, offset: TextSize) { | ||
217 | let fake_ident_token = file_with_fake_ident.token_at_offset(offset).right_biased().unwrap(); | ||
218 | let syntax_element = NodeOrToken::Token(fake_ident_token); | ||
219 | self.block_expr_parent = has_block_expr_parent(syntax_element.clone()); | ||
220 | self.unsafe_is_prev = unsafe_is_prev(syntax_element.clone()); | ||
221 | self.if_is_prev = if_is_prev(syntax_element.clone()); | ||
222 | self.bind_pat_parent = has_bind_pat_parent(syntax_element.clone()); | ||
223 | self.ref_pat_parent = has_ref_parent(syntax_element.clone()); | ||
224 | self.in_loop_body = is_in_loop_body(syntax_element.clone()); | ||
225 | self.has_trait_parent = has_trait_parent(syntax_element.clone()); | ||
226 | self.has_impl_parent = has_impl_parent(syntax_element.clone()); | ||
227 | self.impl_as_prev_sibling = has_impl_as_prev_sibling(syntax_element.clone()); | ||
228 | self.trait_as_prev_sibling = has_trait_as_prev_sibling(syntax_element.clone()); | ||
229 | self.is_match_arm = is_match_arm(syntax_element.clone()); | ||
230 | self.has_item_list_or_source_file_parent = | ||
231 | has_item_list_or_source_file_parent(syntax_element); | ||
232 | } | ||
233 | |||
234 | fn fill( | ||
235 | &mut self, | ||
236 | original_file: &SyntaxNode, | ||
237 | file_with_fake_ident: SyntaxNode, | ||
238 | offset: TextSize, | ||
239 | ) { | ||
240 | // FIXME: this is wrong in at least two cases: | ||
241 | // * when there's no token `foo(<|>)` | ||
242 | // * when there is a token, but it happens to have type of it's own | ||
243 | self.expected_type = self | ||
244 | .token | ||
245 | .ancestors() | ||
246 | .find_map(|node| { | ||
247 | let ty = match_ast! { | ||
248 | match node { | ||
249 | ast::Pat(it) => self.sema.type_of_pat(&it), | ||
250 | ast::Expr(it) => self.sema.type_of_expr(&it), | ||
251 | _ => return None, | ||
252 | } | ||
253 | }; | ||
254 | Some(ty) | ||
255 | }) | ||
256 | .flatten(); | ||
257 | self.attribute_under_caret = find_node_at_offset(&file_with_fake_ident, offset); | ||
258 | |||
259 | // First, let's try to complete a reference to some declaration. | ||
260 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(&file_with_fake_ident, offset) { | ||
261 | // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. | ||
262 | // See RFC#1685. | ||
263 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
264 | self.is_param = true; | ||
265 | return; | ||
266 | } | ||
267 | // FIXME: remove this (V) duplication and make the check more precise | ||
268 | if name_ref.syntax().ancestors().find_map(ast::RecordPatFieldList::cast).is_some() { | ||
269 | self.record_pat_syntax = | ||
270 | self.sema.find_node_at_offset_with_macros(&original_file, offset); | ||
271 | } | ||
272 | self.classify_name_ref(original_file, name_ref, offset); | ||
273 | } | ||
274 | |||
275 | // Otherwise, see if this is a declaration. We can use heuristics to | ||
276 | // suggest declaration names, see `CompletionKind::Magic`. | ||
277 | if let Some(name) = find_node_at_offset::<ast::Name>(&file_with_fake_ident, offset) { | ||
278 | if let Some(bind_pat) = name.syntax().ancestors().find_map(ast::IdentPat::cast) { | ||
279 | self.is_pat_binding_or_const = true; | ||
280 | if bind_pat.at_token().is_some() | ||
281 | || bind_pat.ref_token().is_some() | ||
282 | || bind_pat.mut_token().is_some() | ||
283 | { | ||
284 | self.is_pat_binding_or_const = false; | ||
285 | } | ||
286 | if bind_pat.syntax().parent().and_then(ast::RecordPatFieldList::cast).is_some() { | ||
287 | self.is_pat_binding_or_const = false; | ||
288 | } | ||
289 | if let Some(let_stmt) = bind_pat.syntax().ancestors().find_map(ast::LetStmt::cast) { | ||
290 | if let Some(pat) = let_stmt.pat() { | ||
291 | if pat.syntax().text_range().contains_range(bind_pat.syntax().text_range()) | ||
292 | { | ||
293 | self.is_pat_binding_or_const = false; | ||
294 | } | ||
295 | } | ||
296 | } | ||
297 | } | ||
298 | if is_node::<ast::Param>(name.syntax()) { | ||
299 | self.is_param = true; | ||
300 | return; | ||
301 | } | ||
302 | // FIXME: remove this (^) duplication and make the check more precise | ||
303 | if name.syntax().ancestors().find_map(ast::RecordPatFieldList::cast).is_some() { | ||
304 | self.record_pat_syntax = | ||
305 | self.sema.find_node_at_offset_with_macros(&original_file, offset); | ||
306 | } | ||
307 | } | ||
308 | } | ||
309 | |||
310 | fn classify_name_ref( | ||
311 | &mut self, | ||
312 | original_file: &SyntaxNode, | ||
313 | name_ref: ast::NameRef, | ||
314 | offset: TextSize, | ||
315 | ) { | ||
316 | self.name_ref_syntax = | ||
317 | find_node_at_offset(&original_file, name_ref.syntax().text_range().start()); | ||
318 | let name_range = name_ref.syntax().text_range(); | ||
319 | if ast::RecordExprField::for_field_name(&name_ref).is_some() { | ||
320 | self.record_lit_syntax = | ||
321 | self.sema.find_node_at_offset_with_macros(&original_file, offset); | ||
322 | } | ||
323 | |||
324 | self.impl_def = self | ||
325 | .sema | ||
326 | .ancestors_with_macros(self.token.parent()) | ||
327 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
328 | .find_map(ast::Impl::cast); | ||
329 | |||
330 | let top_node = name_ref | ||
331 | .syntax() | ||
332 | .ancestors() | ||
333 | .take_while(|it| it.text_range() == name_range) | ||
334 | .last() | ||
335 | .unwrap(); | ||
336 | |||
337 | match top_node.parent().map(|it| it.kind()) { | ||
338 | Some(SOURCE_FILE) | Some(ITEM_LIST) => { | ||
339 | self.is_new_item = true; | ||
340 | return; | ||
341 | } | ||
342 | _ => (), | ||
343 | } | ||
344 | |||
345 | self.use_item_syntax = | ||
346 | self.sema.ancestors_with_macros(self.token.parent()).find_map(ast::Use::cast); | ||
347 | |||
348 | self.function_syntax = self | ||
349 | .sema | ||
350 | .ancestors_with_macros(self.token.parent()) | ||
351 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
352 | .find_map(ast::Fn::cast); | ||
353 | |||
354 | self.record_field_syntax = self | ||
355 | .sema | ||
356 | .ancestors_with_macros(self.token.parent()) | ||
357 | .take_while(|it| { | ||
358 | it.kind() != SOURCE_FILE && it.kind() != MODULE && it.kind() != CALL_EXPR | ||
359 | }) | ||
360 | .find_map(ast::RecordExprField::cast); | ||
361 | |||
362 | let parent = match name_ref.syntax().parent() { | ||
363 | Some(it) => it, | ||
364 | None => return, | ||
365 | }; | ||
366 | |||
367 | if let Some(segment) = ast::PathSegment::cast(parent.clone()) { | ||
368 | let path = segment.parent_path(); | ||
369 | self.is_call = path | ||
370 | .syntax() | ||
371 | .parent() | ||
372 | .and_then(ast::PathExpr::cast) | ||
373 | .and_then(|it| it.syntax().parent().and_then(ast::CallExpr::cast)) | ||
374 | .is_some(); | ||
375 | self.is_macro_call = path.syntax().parent().and_then(ast::MacroCall::cast).is_some(); | ||
376 | self.is_pattern_call = | ||
377 | path.syntax().parent().and_then(ast::TupleStructPat::cast).is_some(); | ||
378 | |||
379 | self.is_path_type = path.syntax().parent().and_then(ast::PathType::cast).is_some(); | ||
380 | self.has_type_args = segment.generic_arg_list().is_some(); | ||
381 | |||
382 | let hygiene = hir::Hygiene::new(self.db, self.position.file_id.into()); | ||
383 | if let Some(path) = hir::Path::from_src(path.clone(), &hygiene) { | ||
384 | if let Some(path_prefix) = path.qualifier() { | ||
385 | self.path_prefix = Some(path_prefix); | ||
386 | return; | ||
387 | } | ||
388 | } | ||
389 | |||
390 | if path.qualifier().is_none() { | ||
391 | self.is_trivial_path = true; | ||
392 | |||
393 | // Find either enclosing expr statement (thing with `;`) or a | ||
394 | // block. If block, check that we are the last expr. | ||
395 | self.can_be_stmt = name_ref | ||
396 | .syntax() | ||
397 | .ancestors() | ||
398 | .find_map(|node| { | ||
399 | if let Some(stmt) = ast::ExprStmt::cast(node.clone()) { | ||
400 | return Some( | ||
401 | stmt.syntax().text_range() == name_ref.syntax().text_range(), | ||
402 | ); | ||
403 | } | ||
404 | if let Some(block) = ast::BlockExpr::cast(node) { | ||
405 | return Some( | ||
406 | block.expr().map(|e| e.syntax().text_range()) | ||
407 | == Some(name_ref.syntax().text_range()), | ||
408 | ); | ||
409 | } | ||
410 | None | ||
411 | }) | ||
412 | .unwrap_or(false); | ||
413 | self.is_expr = path.syntax().parent().and_then(ast::PathExpr::cast).is_some(); | ||
414 | |||
415 | if let Some(off) = name_ref.syntax().text_range().start().checked_sub(2.into()) { | ||
416 | if let Some(if_expr) = | ||
417 | self.sema.find_node_at_offset_with_macros::<ast::IfExpr>(original_file, off) | ||
418 | { | ||
419 | if if_expr.syntax().text_range().end() | ||
420 | < name_ref.syntax().text_range().start() | ||
421 | { | ||
422 | self.after_if = true; | ||
423 | } | ||
424 | } | ||
425 | } | ||
426 | } | ||
427 | } | ||
428 | if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) { | ||
429 | // The receiver comes before the point of insertion of the fake | ||
430 | // ident, so it should have the same range in the non-modified file | ||
431 | self.dot_receiver = field_expr | ||
432 | .expr() | ||
433 | .map(|e| e.syntax().text_range()) | ||
434 | .and_then(|r| find_node_with_range(original_file, r)); | ||
435 | self.dot_receiver_is_ambiguous_float_literal = | ||
436 | if let Some(ast::Expr::Literal(l)) = &self.dot_receiver { | ||
437 | match l.kind() { | ||
438 | ast::LiteralKind::FloatNumber { .. } => l.token().text().ends_with('.'), | ||
439 | _ => false, | ||
440 | } | ||
441 | } else { | ||
442 | false | ||
443 | } | ||
444 | } | ||
445 | if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { | ||
446 | // As above | ||
447 | self.dot_receiver = method_call_expr | ||
448 | .expr() | ||
449 | .map(|e| e.syntax().text_range()) | ||
450 | .and_then(|r| find_node_with_range(original_file, r)); | ||
451 | self.is_call = true; | ||
452 | } | ||
453 | } | ||
454 | } | ||
455 | |||
456 | fn find_node_with_range<N: AstNode>(syntax: &SyntaxNode, range: TextRange) -> Option<N> { | ||
457 | find_covering_element(syntax, range).ancestors().find_map(N::cast) | ||
458 | } | ||
459 | |||
460 | fn is_node<N: AstNode>(node: &SyntaxNode) -> bool { | ||
461 | match node.ancestors().find_map(N::cast) { | ||
462 | None => false, | ||
463 | Some(n) => n.syntax().text_range() == node.text_range(), | ||
464 | } | ||
465 | } | ||
diff --git a/crates/ide/src/completion/completion_item.rs b/crates/ide/src/completion/completion_item.rs new file mode 100644 index 000000000..9377cdc57 --- /dev/null +++ b/crates/ide/src/completion/completion_item.rs | |||
@@ -0,0 +1,384 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::fmt; | ||
4 | |||
5 | use hir::Documentation; | ||
6 | use syntax::TextRange; | ||
7 | use text_edit::TextEdit; | ||
8 | |||
9 | use crate::completion::completion_config::SnippetCap; | ||
10 | |||
11 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | ||
12 | /// It is basically a POD with various properties. To construct a | ||
13 | /// `CompletionItem`, use `new` method and the `Builder` struct. | ||
14 | pub struct CompletionItem { | ||
15 | /// Used only internally in tests, to check only specific kind of | ||
16 | /// completion (postfix, keyword, reference, etc). | ||
17 | #[allow(unused)] | ||
18 | pub(crate) completion_kind: CompletionKind, | ||
19 | /// Label in the completion pop up which identifies completion. | ||
20 | label: String, | ||
21 | /// Range of identifier that is being completed. | ||
22 | /// | ||
23 | /// It should be used primarily for UI, but we also use this to convert | ||
24 | /// genetic TextEdit into LSP's completion edit (see conv.rs). | ||
25 | /// | ||
26 | /// `source_range` must contain the completion offset. `insert_text` should | ||
27 | /// start with what `source_range` points to, or VSCode will filter out the | ||
28 | /// completion silently. | ||
29 | source_range: TextRange, | ||
30 | /// What happens when user selects this item. | ||
31 | /// | ||
32 | /// Typically, replaces `source_range` with new identifier. | ||
33 | text_edit: TextEdit, | ||
34 | insert_text_format: InsertTextFormat, | ||
35 | |||
36 | /// What item (struct, function, etc) are we completing. | ||
37 | kind: Option<CompletionItemKind>, | ||
38 | |||
39 | /// Lookup is used to check if completion item indeed can complete current | ||
40 | /// ident. | ||
41 | /// | ||
42 | /// That is, in `foo.bar<|>` lookup of `abracadabra` will be accepted (it | ||
43 | /// contains `bar` sub sequence), and `quux` will rejected. | ||
44 | lookup: Option<String>, | ||
45 | |||
46 | /// Additional info to show in the UI pop up. | ||
47 | detail: Option<String>, | ||
48 | documentation: Option<Documentation>, | ||
49 | |||
50 | /// Whether this item is marked as deprecated | ||
51 | deprecated: bool, | ||
52 | |||
53 | /// If completing a function call, ask the editor to show parameter popup | ||
54 | /// after completion. | ||
55 | trigger_call_info: bool, | ||
56 | |||
57 | /// Score is useful to pre select or display in better order completion items | ||
58 | score: Option<CompletionScore>, | ||
59 | } | ||
60 | |||
61 | // We use custom debug for CompletionItem to make snapshot tests more readable. | ||
62 | impl fmt::Debug for CompletionItem { | ||
63 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
64 | let mut s = f.debug_struct("CompletionItem"); | ||
65 | s.field("label", &self.label()).field("source_range", &self.source_range()); | ||
66 | if self.text_edit().len() == 1 { | ||
67 | let atom = &self.text_edit().iter().next().unwrap(); | ||
68 | s.field("delete", &atom.delete); | ||
69 | s.field("insert", &atom.insert); | ||
70 | } else { | ||
71 | s.field("text_edit", &self.text_edit); | ||
72 | } | ||
73 | if let Some(kind) = self.kind().as_ref() { | ||
74 | s.field("kind", kind); | ||
75 | } | ||
76 | if self.lookup() != self.label() { | ||
77 | s.field("lookup", &self.lookup()); | ||
78 | } | ||
79 | if let Some(detail) = self.detail() { | ||
80 | s.field("detail", &detail); | ||
81 | } | ||
82 | if let Some(documentation) = self.documentation() { | ||
83 | s.field("documentation", &documentation); | ||
84 | } | ||
85 | if self.deprecated { | ||
86 | s.field("deprecated", &true); | ||
87 | } | ||
88 | if let Some(score) = &self.score { | ||
89 | s.field("score", score); | ||
90 | } | ||
91 | if self.trigger_call_info { | ||
92 | s.field("trigger_call_info", &true); | ||
93 | } | ||
94 | s.finish() | ||
95 | } | ||
96 | } | ||
97 | |||
98 | #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] | ||
99 | pub enum CompletionScore { | ||
100 | /// If only type match | ||
101 | TypeMatch, | ||
102 | /// If type and name match | ||
103 | TypeAndNameMatch, | ||
104 | } | ||
105 | |||
106 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
107 | pub enum CompletionItemKind { | ||
108 | Snippet, | ||
109 | Keyword, | ||
110 | Module, | ||
111 | Function, | ||
112 | BuiltinType, | ||
113 | Struct, | ||
114 | Enum, | ||
115 | EnumVariant, | ||
116 | Binding, | ||
117 | Field, | ||
118 | Static, | ||
119 | Const, | ||
120 | Trait, | ||
121 | TypeAlias, | ||
122 | Method, | ||
123 | TypeParam, | ||
124 | Macro, | ||
125 | Attribute, | ||
126 | UnresolvedReference, | ||
127 | } | ||
128 | |||
129 | impl CompletionItemKind { | ||
130 | #[cfg(test)] | ||
131 | pub(crate) fn tag(&self) -> &'static str { | ||
132 | match self { | ||
133 | CompletionItemKind::Attribute => "at", | ||
134 | CompletionItemKind::Binding => "bn", | ||
135 | CompletionItemKind::BuiltinType => "bt", | ||
136 | CompletionItemKind::Const => "ct", | ||
137 | CompletionItemKind::Enum => "en", | ||
138 | CompletionItemKind::EnumVariant => "ev", | ||
139 | CompletionItemKind::Field => "fd", | ||
140 | CompletionItemKind::Function => "fn", | ||
141 | CompletionItemKind::Keyword => "kw", | ||
142 | CompletionItemKind::Macro => "ma", | ||
143 | CompletionItemKind::Method => "me", | ||
144 | CompletionItemKind::Module => "md", | ||
145 | CompletionItemKind::Snippet => "sn", | ||
146 | CompletionItemKind::Static => "sc", | ||
147 | CompletionItemKind::Struct => "st", | ||
148 | CompletionItemKind::Trait => "tt", | ||
149 | CompletionItemKind::TypeAlias => "ta", | ||
150 | CompletionItemKind::TypeParam => "tp", | ||
151 | CompletionItemKind::UnresolvedReference => "??", | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | |||
156 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
157 | pub(crate) enum CompletionKind { | ||
158 | /// Parser-based keyword completion. | ||
159 | Keyword, | ||
160 | /// Your usual "complete all valid identifiers". | ||
161 | Reference, | ||
162 | /// "Secret sauce" completions. | ||
163 | Magic, | ||
164 | Snippet, | ||
165 | Postfix, | ||
166 | BuiltinType, | ||
167 | Attribute, | ||
168 | } | ||
169 | |||
170 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
171 | pub enum InsertTextFormat { | ||
172 | PlainText, | ||
173 | Snippet, | ||
174 | } | ||
175 | |||
176 | impl CompletionItem { | ||
177 | pub(crate) fn new( | ||
178 | completion_kind: CompletionKind, | ||
179 | source_range: TextRange, | ||
180 | label: impl Into<String>, | ||
181 | ) -> Builder { | ||
182 | let label = label.into(); | ||
183 | Builder { | ||
184 | source_range, | ||
185 | completion_kind, | ||
186 | label, | ||
187 | insert_text: None, | ||
188 | insert_text_format: InsertTextFormat::PlainText, | ||
189 | detail: None, | ||
190 | documentation: None, | ||
191 | lookup: None, | ||
192 | kind: None, | ||
193 | text_edit: None, | ||
194 | deprecated: None, | ||
195 | trigger_call_info: None, | ||
196 | score: None, | ||
197 | } | ||
198 | } | ||
199 | /// What user sees in pop-up in the UI. | ||
200 | pub fn label(&self) -> &str { | ||
201 | &self.label | ||
202 | } | ||
203 | pub fn source_range(&self) -> TextRange { | ||
204 | self.source_range | ||
205 | } | ||
206 | |||
207 | pub fn insert_text_format(&self) -> InsertTextFormat { | ||
208 | self.insert_text_format | ||
209 | } | ||
210 | |||
211 | pub fn text_edit(&self) -> &TextEdit { | ||
212 | &self.text_edit | ||
213 | } | ||
214 | |||
215 | /// Short one-line additional information, like a type | ||
216 | pub fn detail(&self) -> Option<&str> { | ||
217 | self.detail.as_deref() | ||
218 | } | ||
219 | /// A doc-comment | ||
220 | pub fn documentation(&self) -> Option<Documentation> { | ||
221 | self.documentation.clone() | ||
222 | } | ||
223 | /// What string is used for filtering. | ||
224 | pub fn lookup(&self) -> &str { | ||
225 | self.lookup.as_deref().unwrap_or(&self.label) | ||
226 | } | ||
227 | |||
228 | pub fn kind(&self) -> Option<CompletionItemKind> { | ||
229 | self.kind | ||
230 | } | ||
231 | |||
232 | pub fn deprecated(&self) -> bool { | ||
233 | self.deprecated | ||
234 | } | ||
235 | |||
236 | pub fn score(&self) -> Option<CompletionScore> { | ||
237 | self.score | ||
238 | } | ||
239 | |||
240 | pub fn trigger_call_info(&self) -> bool { | ||
241 | self.trigger_call_info | ||
242 | } | ||
243 | } | ||
244 | |||
245 | /// A helper to make `CompletionItem`s. | ||
246 | #[must_use] | ||
247 | pub(crate) struct Builder { | ||
248 | source_range: TextRange, | ||
249 | completion_kind: CompletionKind, | ||
250 | label: String, | ||
251 | insert_text: Option<String>, | ||
252 | insert_text_format: InsertTextFormat, | ||
253 | detail: Option<String>, | ||
254 | documentation: Option<Documentation>, | ||
255 | lookup: Option<String>, | ||
256 | kind: Option<CompletionItemKind>, | ||
257 | text_edit: Option<TextEdit>, | ||
258 | deprecated: Option<bool>, | ||
259 | trigger_call_info: Option<bool>, | ||
260 | score: Option<CompletionScore>, | ||
261 | } | ||
262 | |||
263 | impl Builder { | ||
264 | pub(crate) fn add_to(self, acc: &mut Completions) { | ||
265 | acc.add(self.build()) | ||
266 | } | ||
267 | |||
268 | pub(crate) fn build(self) -> CompletionItem { | ||
269 | let label = self.label; | ||
270 | let text_edit = match self.text_edit { | ||
271 | Some(it) => it, | ||
272 | None => TextEdit::replace( | ||
273 | self.source_range, | ||
274 | self.insert_text.unwrap_or_else(|| label.clone()), | ||
275 | ), | ||
276 | }; | ||
277 | |||
278 | CompletionItem { | ||
279 | source_range: self.source_range, | ||
280 | label, | ||
281 | insert_text_format: self.insert_text_format, | ||
282 | text_edit, | ||
283 | detail: self.detail, | ||
284 | documentation: self.documentation, | ||
285 | lookup: self.lookup, | ||
286 | kind: self.kind, | ||
287 | completion_kind: self.completion_kind, | ||
288 | deprecated: self.deprecated.unwrap_or(false), | ||
289 | trigger_call_info: self.trigger_call_info.unwrap_or(false), | ||
290 | score: self.score, | ||
291 | } | ||
292 | } | ||
293 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { | ||
294 | self.lookup = Some(lookup.into()); | ||
295 | self | ||
296 | } | ||
297 | pub(crate) fn label(mut self, label: impl Into<String>) -> Builder { | ||
298 | self.label = label.into(); | ||
299 | self | ||
300 | } | ||
301 | pub(crate) fn insert_text(mut self, insert_text: impl Into<String>) -> Builder { | ||
302 | self.insert_text = Some(insert_text.into()); | ||
303 | self | ||
304 | } | ||
305 | pub(crate) fn insert_snippet( | ||
306 | mut self, | ||
307 | _cap: SnippetCap, | ||
308 | snippet: impl Into<String>, | ||
309 | ) -> Builder { | ||
310 | self.insert_text_format = InsertTextFormat::Snippet; | ||
311 | self.insert_text(snippet) | ||
312 | } | ||
313 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { | ||
314 | self.kind = Some(kind); | ||
315 | self | ||
316 | } | ||
317 | pub(crate) fn text_edit(mut self, edit: TextEdit) -> Builder { | ||
318 | self.text_edit = Some(edit); | ||
319 | self | ||
320 | } | ||
321 | pub(crate) fn snippet_edit(mut self, _cap: SnippetCap, edit: TextEdit) -> Builder { | ||
322 | self.insert_text_format = InsertTextFormat::Snippet; | ||
323 | self.text_edit(edit) | ||
324 | } | ||
325 | #[allow(unused)] | ||
326 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { | ||
327 | self.set_detail(Some(detail)) | ||
328 | } | ||
329 | pub(crate) fn set_detail(mut self, detail: Option<impl Into<String>>) -> Builder { | ||
330 | self.detail = detail.map(Into::into); | ||
331 | self | ||
332 | } | ||
333 | #[allow(unused)] | ||
334 | pub(crate) fn documentation(self, docs: Documentation) -> Builder { | ||
335 | self.set_documentation(Some(docs)) | ||
336 | } | ||
337 | pub(crate) fn set_documentation(mut self, docs: Option<Documentation>) -> Builder { | ||
338 | self.documentation = docs.map(Into::into); | ||
339 | self | ||
340 | } | ||
341 | pub(crate) fn set_deprecated(mut self, deprecated: bool) -> Builder { | ||
342 | self.deprecated = Some(deprecated); | ||
343 | self | ||
344 | } | ||
345 | pub(crate) fn set_score(mut self, score: CompletionScore) -> Builder { | ||
346 | self.score = Some(score); | ||
347 | self | ||
348 | } | ||
349 | pub(crate) fn trigger_call_info(mut self) -> Builder { | ||
350 | self.trigger_call_info = Some(true); | ||
351 | self | ||
352 | } | ||
353 | } | ||
354 | |||
355 | impl<'a> Into<CompletionItem> for Builder { | ||
356 | fn into(self) -> CompletionItem { | ||
357 | self.build() | ||
358 | } | ||
359 | } | ||
360 | |||
361 | /// Represents an in-progress set of completions being built. | ||
362 | #[derive(Debug, Default)] | ||
363 | pub(crate) struct Completions { | ||
364 | buf: Vec<CompletionItem>, | ||
365 | } | ||
366 | |||
367 | impl Completions { | ||
368 | pub(crate) fn add(&mut self, item: impl Into<CompletionItem>) { | ||
369 | self.buf.push(item.into()) | ||
370 | } | ||
371 | pub(crate) fn add_all<I>(&mut self, items: I) | ||
372 | where | ||
373 | I: IntoIterator, | ||
374 | I::Item: Into<CompletionItem>, | ||
375 | { | ||
376 | items.into_iter().for_each(|item| self.add(item.into())) | ||
377 | } | ||
378 | } | ||
379 | |||
380 | impl Into<Vec<CompletionItem>> for Completions { | ||
381 | fn into(self) -> Vec<CompletionItem> { | ||
382 | self.buf | ||
383 | } | ||
384 | } | ||
diff --git a/crates/ide/src/completion/patterns.rs b/crates/ide/src/completion/patterns.rs new file mode 100644 index 000000000..ffc97c076 --- /dev/null +++ b/crates/ide/src/completion/patterns.rs | |||
@@ -0,0 +1,194 @@ | |||
1 | //! Patterns telling us certain facts about current syntax element, they are used in completion context | ||
2 | |||
3 | use syntax::{ | ||
4 | algo::non_trivia_sibling, | ||
5 | ast::{self, LoopBodyOwner}, | ||
6 | match_ast, AstNode, Direction, NodeOrToken, SyntaxElement, | ||
7 | SyntaxKind::*, | ||
8 | SyntaxNode, SyntaxToken, | ||
9 | }; | ||
10 | |||
11 | #[cfg(test)] | ||
12 | use crate::completion::test_utils::check_pattern_is_applicable; | ||
13 | |||
14 | pub(crate) fn has_trait_parent(element: SyntaxElement) -> bool { | ||
15 | not_same_range_ancestor(element) | ||
16 | .filter(|it| it.kind() == ASSOC_ITEM_LIST) | ||
17 | .and_then(|it| it.parent()) | ||
18 | .filter(|it| it.kind() == TRAIT) | ||
19 | .is_some() | ||
20 | } | ||
21 | #[test] | ||
22 | fn test_has_trait_parent() { | ||
23 | check_pattern_is_applicable(r"trait A { f<|> }", has_trait_parent); | ||
24 | } | ||
25 | |||
26 | pub(crate) fn has_impl_parent(element: SyntaxElement) -> bool { | ||
27 | not_same_range_ancestor(element) | ||
28 | .filter(|it| it.kind() == ASSOC_ITEM_LIST) | ||
29 | .and_then(|it| it.parent()) | ||
30 | .filter(|it| it.kind() == IMPL) | ||
31 | .is_some() | ||
32 | } | ||
33 | #[test] | ||
34 | fn test_has_impl_parent() { | ||
35 | check_pattern_is_applicable(r"impl A { f<|> }", has_impl_parent); | ||
36 | } | ||
37 | |||
38 | pub(crate) fn has_block_expr_parent(element: SyntaxElement) -> bool { | ||
39 | not_same_range_ancestor(element).filter(|it| it.kind() == BLOCK_EXPR).is_some() | ||
40 | } | ||
41 | #[test] | ||
42 | fn test_has_block_expr_parent() { | ||
43 | check_pattern_is_applicable(r"fn my_fn() { let a = 2; f<|> }", has_block_expr_parent); | ||
44 | } | ||
45 | |||
46 | pub(crate) fn has_bind_pat_parent(element: SyntaxElement) -> bool { | ||
47 | element.ancestors().find(|it| it.kind() == IDENT_PAT).is_some() | ||
48 | } | ||
49 | #[test] | ||
50 | fn test_has_bind_pat_parent() { | ||
51 | check_pattern_is_applicable(r"fn my_fn(m<|>) {}", has_bind_pat_parent); | ||
52 | check_pattern_is_applicable(r"fn my_fn() { let m<|> }", has_bind_pat_parent); | ||
53 | } | ||
54 | |||
55 | pub(crate) fn has_ref_parent(element: SyntaxElement) -> bool { | ||
56 | not_same_range_ancestor(element) | ||
57 | .filter(|it| it.kind() == REF_PAT || it.kind() == REF_EXPR) | ||
58 | .is_some() | ||
59 | } | ||
60 | #[test] | ||
61 | fn test_has_ref_parent() { | ||
62 | check_pattern_is_applicable(r"fn my_fn(&m<|>) {}", has_ref_parent); | ||
63 | check_pattern_is_applicable(r"fn my() { let &m<|> }", has_ref_parent); | ||
64 | } | ||
65 | |||
66 | pub(crate) fn has_item_list_or_source_file_parent(element: SyntaxElement) -> bool { | ||
67 | let ancestor = not_same_range_ancestor(element); | ||
68 | if !ancestor.is_some() { | ||
69 | return true; | ||
70 | } | ||
71 | ancestor.filter(|it| it.kind() == SOURCE_FILE || it.kind() == ITEM_LIST).is_some() | ||
72 | } | ||
73 | #[test] | ||
74 | fn test_has_item_list_or_source_file_parent() { | ||
75 | check_pattern_is_applicable(r"i<|>", has_item_list_or_source_file_parent); | ||
76 | check_pattern_is_applicable(r"mod foo { f<|> }", has_item_list_or_source_file_parent); | ||
77 | } | ||
78 | |||
79 | pub(crate) fn is_match_arm(element: SyntaxElement) -> bool { | ||
80 | not_same_range_ancestor(element.clone()).filter(|it| it.kind() == MATCH_ARM).is_some() | ||
81 | && previous_sibling_or_ancestor_sibling(element) | ||
82 | .and_then(|it| it.into_token()) | ||
83 | .filter(|it| it.kind() == FAT_ARROW) | ||
84 | .is_some() | ||
85 | } | ||
86 | #[test] | ||
87 | fn test_is_match_arm() { | ||
88 | check_pattern_is_applicable(r"fn my_fn() { match () { () => m<|> } }", is_match_arm); | ||
89 | } | ||
90 | |||
91 | pub(crate) fn unsafe_is_prev(element: SyntaxElement) -> bool { | ||
92 | element | ||
93 | .into_token() | ||
94 | .and_then(|it| previous_non_trivia_token(it)) | ||
95 | .filter(|it| it.kind() == UNSAFE_KW) | ||
96 | .is_some() | ||
97 | } | ||
98 | #[test] | ||
99 | fn test_unsafe_is_prev() { | ||
100 | check_pattern_is_applicable(r"unsafe i<|>", unsafe_is_prev); | ||
101 | } | ||
102 | |||
103 | pub(crate) fn if_is_prev(element: SyntaxElement) -> bool { | ||
104 | element | ||
105 | .into_token() | ||
106 | .and_then(|it| previous_non_trivia_token(it)) | ||
107 | .filter(|it| it.kind() == IF_KW) | ||
108 | .is_some() | ||
109 | } | ||
110 | #[test] | ||
111 | fn test_if_is_prev() { | ||
112 | check_pattern_is_applicable(r"if l<|>", if_is_prev); | ||
113 | } | ||
114 | |||
115 | pub(crate) fn has_trait_as_prev_sibling(element: SyntaxElement) -> bool { | ||
116 | previous_sibling_or_ancestor_sibling(element).filter(|it| it.kind() == TRAIT).is_some() | ||
117 | } | ||
118 | #[test] | ||
119 | fn test_has_trait_as_prev_sibling() { | ||
120 | check_pattern_is_applicable(r"trait A w<|> {}", has_trait_as_prev_sibling); | ||
121 | } | ||
122 | |||
123 | pub(crate) fn has_impl_as_prev_sibling(element: SyntaxElement) -> bool { | ||
124 | previous_sibling_or_ancestor_sibling(element).filter(|it| it.kind() == IMPL).is_some() | ||
125 | } | ||
126 | #[test] | ||
127 | fn test_has_impl_as_prev_sibling() { | ||
128 | check_pattern_is_applicable(r"impl A w<|> {}", has_impl_as_prev_sibling); | ||
129 | } | ||
130 | |||
131 | pub(crate) fn is_in_loop_body(element: SyntaxElement) -> bool { | ||
132 | let leaf = match element { | ||
133 | NodeOrToken::Node(node) => node, | ||
134 | NodeOrToken::Token(token) => token.parent(), | ||
135 | }; | ||
136 | for node in leaf.ancestors() { | ||
137 | if node.kind() == FN || node.kind() == CLOSURE_EXPR { | ||
138 | break; | ||
139 | } | ||
140 | let loop_body = match_ast! { | ||
141 | match node { | ||
142 | ast::ForExpr(it) => it.loop_body(), | ||
143 | ast::WhileExpr(it) => it.loop_body(), | ||
144 | ast::LoopExpr(it) => it.loop_body(), | ||
145 | _ => None, | ||
146 | } | ||
147 | }; | ||
148 | if let Some(body) = loop_body { | ||
149 | if body.syntax().text_range().contains_range(leaf.text_range()) { | ||
150 | return true; | ||
151 | } | ||
152 | } | ||
153 | } | ||
154 | false | ||
155 | } | ||
156 | |||
157 | fn not_same_range_ancestor(element: SyntaxElement) -> Option<SyntaxNode> { | ||
158 | element | ||
159 | .ancestors() | ||
160 | .take_while(|it| it.text_range() == element.text_range()) | ||
161 | .last() | ||
162 | .and_then(|it| it.parent()) | ||
163 | } | ||
164 | |||
165 | fn previous_non_trivia_token(token: SyntaxToken) -> Option<SyntaxToken> { | ||
166 | let mut token = token.prev_token(); | ||
167 | while let Some(inner) = token.clone() { | ||
168 | if !inner.kind().is_trivia() { | ||
169 | return Some(inner); | ||
170 | } else { | ||
171 | token = inner.prev_token(); | ||
172 | } | ||
173 | } | ||
174 | None | ||
175 | } | ||
176 | |||
177 | fn previous_sibling_or_ancestor_sibling(element: SyntaxElement) -> Option<SyntaxElement> { | ||
178 | let token_sibling = non_trivia_sibling(element.clone(), Direction::Prev); | ||
179 | if let Some(sibling) = token_sibling { | ||
180 | Some(sibling) | ||
181 | } else { | ||
182 | // if not trying to find first ancestor which has such a sibling | ||
183 | let node = match element { | ||
184 | NodeOrToken::Node(node) => node, | ||
185 | NodeOrToken::Token(token) => token.parent(), | ||
186 | }; | ||
187 | let range = node.text_range(); | ||
188 | let top_node = node.ancestors().take_while(|it| it.text_range() == range).last()?; | ||
189 | let prev_sibling_node = top_node.ancestors().find(|it| { | ||
190 | non_trivia_sibling(NodeOrToken::Node(it.to_owned()), Direction::Prev).is_some() | ||
191 | })?; | ||
192 | non_trivia_sibling(NodeOrToken::Node(prev_sibling_node), Direction::Prev) | ||
193 | } | ||
194 | } | ||
diff --git a/crates/ide/src/completion/presentation.rs b/crates/ide/src/completion/presentation.rs new file mode 100644 index 000000000..e1b1ea4ce --- /dev/null +++ b/crates/ide/src/completion/presentation.rs | |||
@@ -0,0 +1,1229 @@ | |||
1 | //! This modules takes care of rendering various definitions as completion items. | ||
2 | //! It also handles scoring (sorting) completions. | ||
3 | |||
4 | use hir::{Docs, HasAttrs, HasSource, HirDisplay, ModPath, ScopeDef, StructKind, Type}; | ||
5 | use itertools::Itertools; | ||
6 | use syntax::ast::NameOwner; | ||
7 | use test_utils::mark; | ||
8 | |||
9 | use crate::{ | ||
10 | completion::{ | ||
11 | completion_item::Builder, CompletionContext, CompletionItem, CompletionItemKind, | ||
12 | CompletionKind, Completions, | ||
13 | }, | ||
14 | display::{const_label, function_declaration, macro_label, type_label}, | ||
15 | CompletionScore, RootDatabase, | ||
16 | }; | ||
17 | |||
18 | impl Completions { | ||
19 | pub(crate) fn add_field(&mut self, ctx: &CompletionContext, field: hir::Field, ty: &Type) { | ||
20 | let is_deprecated = is_deprecated(field, ctx.db); | ||
21 | let name = field.name(ctx.db); | ||
22 | let mut completion_item = | ||
23 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.to_string()) | ||
24 | .kind(CompletionItemKind::Field) | ||
25 | .detail(ty.display(ctx.db).to_string()) | ||
26 | .set_documentation(field.docs(ctx.db)) | ||
27 | .set_deprecated(is_deprecated); | ||
28 | |||
29 | if let Some(score) = compute_score(ctx, &ty, &name.to_string()) { | ||
30 | completion_item = completion_item.set_score(score); | ||
31 | } | ||
32 | |||
33 | completion_item.add_to(self); | ||
34 | } | ||
35 | |||
36 | pub(crate) fn add_tuple_field(&mut self, ctx: &CompletionContext, field: usize, ty: &Type) { | ||
37 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), field.to_string()) | ||
38 | .kind(CompletionItemKind::Field) | ||
39 | .detail(ty.display(ctx.db).to_string()) | ||
40 | .add_to(self); | ||
41 | } | ||
42 | |||
43 | pub(crate) fn add_resolution( | ||
44 | &mut self, | ||
45 | ctx: &CompletionContext, | ||
46 | local_name: String, | ||
47 | resolution: &ScopeDef, | ||
48 | ) { | ||
49 | use hir::ModuleDef::*; | ||
50 | |||
51 | let completion_kind = match resolution { | ||
52 | ScopeDef::ModuleDef(BuiltinType(..)) => CompletionKind::BuiltinType, | ||
53 | _ => CompletionKind::Reference, | ||
54 | }; | ||
55 | |||
56 | let kind = match resolution { | ||
57 | ScopeDef::ModuleDef(Module(..)) => CompletionItemKind::Module, | ||
58 | ScopeDef::ModuleDef(Function(func)) => { | ||
59 | return self.add_function(ctx, *func, Some(local_name)); | ||
60 | } | ||
61 | ScopeDef::ModuleDef(Adt(hir::Adt::Struct(_))) => CompletionItemKind::Struct, | ||
62 | // FIXME: add CompletionItemKind::Union | ||
63 | ScopeDef::ModuleDef(Adt(hir::Adt::Union(_))) => CompletionItemKind::Struct, | ||
64 | ScopeDef::ModuleDef(Adt(hir::Adt::Enum(_))) => CompletionItemKind::Enum, | ||
65 | |||
66 | ScopeDef::ModuleDef(EnumVariant(var)) => { | ||
67 | return self.add_enum_variant(ctx, *var, Some(local_name)); | ||
68 | } | ||
69 | ScopeDef::ModuleDef(Const(..)) => CompletionItemKind::Const, | ||
70 | ScopeDef::ModuleDef(Static(..)) => CompletionItemKind::Static, | ||
71 | ScopeDef::ModuleDef(Trait(..)) => CompletionItemKind::Trait, | ||
72 | ScopeDef::ModuleDef(TypeAlias(..)) => CompletionItemKind::TypeAlias, | ||
73 | ScopeDef::ModuleDef(BuiltinType(..)) => CompletionItemKind::BuiltinType, | ||
74 | ScopeDef::GenericParam(..) => CompletionItemKind::TypeParam, | ||
75 | ScopeDef::Local(..) => CompletionItemKind::Binding, | ||
76 | // (does this need its own kind?) | ||
77 | ScopeDef::AdtSelfType(..) | ScopeDef::ImplSelfType(..) => CompletionItemKind::TypeParam, | ||
78 | ScopeDef::MacroDef(mac) => { | ||
79 | return self.add_macro(ctx, Some(local_name), *mac); | ||
80 | } | ||
81 | ScopeDef::Unknown => { | ||
82 | return self.add( | ||
83 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), local_name) | ||
84 | .kind(CompletionItemKind::UnresolvedReference), | ||
85 | ); | ||
86 | } | ||
87 | }; | ||
88 | |||
89 | let docs = match resolution { | ||
90 | ScopeDef::ModuleDef(Module(it)) => it.docs(ctx.db), | ||
91 | ScopeDef::ModuleDef(Adt(it)) => it.docs(ctx.db), | ||
92 | ScopeDef::ModuleDef(EnumVariant(it)) => it.docs(ctx.db), | ||
93 | ScopeDef::ModuleDef(Const(it)) => it.docs(ctx.db), | ||
94 | ScopeDef::ModuleDef(Static(it)) => it.docs(ctx.db), | ||
95 | ScopeDef::ModuleDef(Trait(it)) => it.docs(ctx.db), | ||
96 | ScopeDef::ModuleDef(TypeAlias(it)) => it.docs(ctx.db), | ||
97 | _ => None, | ||
98 | }; | ||
99 | |||
100 | let mut completion_item = | ||
101 | CompletionItem::new(completion_kind, ctx.source_range(), local_name.clone()); | ||
102 | if let ScopeDef::Local(local) = resolution { | ||
103 | let ty = local.ty(ctx.db); | ||
104 | if !ty.is_unknown() { | ||
105 | completion_item = completion_item.detail(ty.display(ctx.db).to_string()); | ||
106 | } | ||
107 | }; | ||
108 | |||
109 | if let ScopeDef::Local(local) = resolution { | ||
110 | if let Some(score) = compute_score(ctx, &local.ty(ctx.db), &local_name) { | ||
111 | completion_item = completion_item.set_score(score); | ||
112 | } | ||
113 | } | ||
114 | |||
115 | // Add `<>` for generic types | ||
116 | if ctx.is_path_type && !ctx.has_type_args && ctx.config.add_call_parenthesis { | ||
117 | if let Some(cap) = ctx.config.snippet_cap { | ||
118 | let has_non_default_type_params = match resolution { | ||
119 | ScopeDef::ModuleDef(Adt(it)) => it.has_non_default_type_params(ctx.db), | ||
120 | ScopeDef::ModuleDef(TypeAlias(it)) => it.has_non_default_type_params(ctx.db), | ||
121 | _ => false, | ||
122 | }; | ||
123 | if has_non_default_type_params { | ||
124 | mark::hit!(inserts_angle_brackets_for_generics); | ||
125 | completion_item = completion_item | ||
126 | .lookup_by(local_name.clone()) | ||
127 | .label(format!("{}<…>", local_name)) | ||
128 | .insert_snippet(cap, format!("{}<$0>", local_name)); | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
133 | completion_item.kind(kind).set_documentation(docs).add_to(self) | ||
134 | } | ||
135 | |||
136 | pub(crate) fn add_macro( | ||
137 | &mut self, | ||
138 | ctx: &CompletionContext, | ||
139 | name: Option<String>, | ||
140 | macro_: hir::MacroDef, | ||
141 | ) { | ||
142 | // FIXME: Currently proc-macro do not have ast-node, | ||
143 | // such that it does not have source | ||
144 | if macro_.is_proc_macro() { | ||
145 | return; | ||
146 | } | ||
147 | |||
148 | let name = match name { | ||
149 | Some(it) => it, | ||
150 | None => return, | ||
151 | }; | ||
152 | |||
153 | let ast_node = macro_.source(ctx.db).value; | ||
154 | let detail = macro_label(&ast_node); | ||
155 | |||
156 | let docs = macro_.docs(ctx.db); | ||
157 | |||
158 | let mut builder = CompletionItem::new( | ||
159 | CompletionKind::Reference, | ||
160 | ctx.source_range(), | ||
161 | &format!("{}!", name), | ||
162 | ) | ||
163 | .kind(CompletionItemKind::Macro) | ||
164 | .set_documentation(docs.clone()) | ||
165 | .set_deprecated(is_deprecated(macro_, ctx.db)) | ||
166 | .detail(detail); | ||
167 | |||
168 | let needs_bang = ctx.use_item_syntax.is_none() && !ctx.is_macro_call; | ||
169 | builder = match ctx.config.snippet_cap { | ||
170 | Some(cap) if needs_bang => { | ||
171 | let docs = docs.as_ref().map_or("", |s| s.as_str()); | ||
172 | let (bra, ket) = guess_macro_braces(&name, docs); | ||
173 | builder | ||
174 | .insert_snippet(cap, format!("{}!{}$0{}", name, bra, ket)) | ||
175 | .label(format!("{}!{}…{}", name, bra, ket)) | ||
176 | .lookup_by(format!("{}!", name)) | ||
177 | } | ||
178 | None if needs_bang => builder.insert_text(format!("{}!", name)), | ||
179 | _ => { | ||
180 | mark::hit!(dont_insert_macro_call_parens_unncessary); | ||
181 | builder.insert_text(name) | ||
182 | } | ||
183 | }; | ||
184 | |||
185 | self.add(builder); | ||
186 | } | ||
187 | |||
188 | pub(crate) fn add_function( | ||
189 | &mut self, | ||
190 | ctx: &CompletionContext, | ||
191 | func: hir::Function, | ||
192 | local_name: Option<String>, | ||
193 | ) { | ||
194 | let has_self_param = func.has_self_param(ctx.db); | ||
195 | |||
196 | let name = local_name.unwrap_or_else(|| func.name(ctx.db).to_string()); | ||
197 | let ast_node = func.source(ctx.db).value; | ||
198 | |||
199 | let mut builder = | ||
200 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.clone()) | ||
201 | .kind(if has_self_param { | ||
202 | CompletionItemKind::Method | ||
203 | } else { | ||
204 | CompletionItemKind::Function | ||
205 | }) | ||
206 | .set_documentation(func.docs(ctx.db)) | ||
207 | .set_deprecated(is_deprecated(func, ctx.db)) | ||
208 | .detail(function_declaration(&ast_node)); | ||
209 | |||
210 | let params = ast_node | ||
211 | .param_list() | ||
212 | .into_iter() | ||
213 | .flat_map(|it| it.params()) | ||
214 | .flat_map(|it| it.pat()) | ||
215 | .map(|pat| pat.to_string().trim_start_matches('_').into()) | ||
216 | .collect(); | ||
217 | |||
218 | builder = builder.add_call_parens(ctx, name, Params::Named(params)); | ||
219 | |||
220 | self.add(builder) | ||
221 | } | ||
222 | |||
223 | pub(crate) fn add_const(&mut self, ctx: &CompletionContext, constant: hir::Const) { | ||
224 | let ast_node = constant.source(ctx.db).value; | ||
225 | let name = match ast_node.name() { | ||
226 | Some(name) => name, | ||
227 | _ => return, | ||
228 | }; | ||
229 | let detail = const_label(&ast_node); | ||
230 | |||
231 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.text().to_string()) | ||
232 | .kind(CompletionItemKind::Const) | ||
233 | .set_documentation(constant.docs(ctx.db)) | ||
234 | .set_deprecated(is_deprecated(constant, ctx.db)) | ||
235 | .detail(detail) | ||
236 | .add_to(self); | ||
237 | } | ||
238 | |||
239 | pub(crate) fn add_type_alias(&mut self, ctx: &CompletionContext, type_alias: hir::TypeAlias) { | ||
240 | let type_def = type_alias.source(ctx.db).value; | ||
241 | let name = match type_def.name() { | ||
242 | Some(name) => name, | ||
243 | _ => return, | ||
244 | }; | ||
245 | let detail = type_label(&type_def); | ||
246 | |||
247 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.text().to_string()) | ||
248 | .kind(CompletionItemKind::TypeAlias) | ||
249 | .set_documentation(type_alias.docs(ctx.db)) | ||
250 | .set_deprecated(is_deprecated(type_alias, ctx.db)) | ||
251 | .detail(detail) | ||
252 | .add_to(self); | ||
253 | } | ||
254 | |||
255 | pub(crate) fn add_qualified_enum_variant( | ||
256 | &mut self, | ||
257 | ctx: &CompletionContext, | ||
258 | variant: hir::EnumVariant, | ||
259 | path: ModPath, | ||
260 | ) { | ||
261 | self.add_enum_variant_impl(ctx, variant, None, Some(path)) | ||
262 | } | ||
263 | |||
264 | pub(crate) fn add_enum_variant( | ||
265 | &mut self, | ||
266 | ctx: &CompletionContext, | ||
267 | variant: hir::EnumVariant, | ||
268 | local_name: Option<String>, | ||
269 | ) { | ||
270 | self.add_enum_variant_impl(ctx, variant, local_name, None) | ||
271 | } | ||
272 | |||
273 | fn add_enum_variant_impl( | ||
274 | &mut self, | ||
275 | ctx: &CompletionContext, | ||
276 | variant: hir::EnumVariant, | ||
277 | local_name: Option<String>, | ||
278 | path: Option<ModPath>, | ||
279 | ) { | ||
280 | let is_deprecated = is_deprecated(variant, ctx.db); | ||
281 | let name = local_name.unwrap_or_else(|| variant.name(ctx.db).to_string()); | ||
282 | let qualified_name = match &path { | ||
283 | Some(it) => it.to_string(), | ||
284 | None => name.to_string(), | ||
285 | }; | ||
286 | let detail_types = variant | ||
287 | .fields(ctx.db) | ||
288 | .into_iter() | ||
289 | .map(|field| (field.name(ctx.db), field.signature_ty(ctx.db))); | ||
290 | let variant_kind = variant.kind(ctx.db); | ||
291 | let detail = match variant_kind { | ||
292 | StructKind::Tuple | StructKind::Unit => format!( | ||
293 | "({})", | ||
294 | detail_types.map(|(_, t)| t.display(ctx.db).to_string()).format(", ") | ||
295 | ), | ||
296 | StructKind::Record => format!( | ||
297 | "{{ {} }}", | ||
298 | detail_types | ||
299 | .map(|(n, t)| format!("{}: {}", n, t.display(ctx.db).to_string())) | ||
300 | .format(", ") | ||
301 | ), | ||
302 | }; | ||
303 | let mut res = CompletionItem::new( | ||
304 | CompletionKind::Reference, | ||
305 | ctx.source_range(), | ||
306 | qualified_name.clone(), | ||
307 | ) | ||
308 | .kind(CompletionItemKind::EnumVariant) | ||
309 | .set_documentation(variant.docs(ctx.db)) | ||
310 | .set_deprecated(is_deprecated) | ||
311 | .detail(detail); | ||
312 | |||
313 | if path.is_some() { | ||
314 | res = res.lookup_by(name); | ||
315 | } | ||
316 | |||
317 | if variant_kind == StructKind::Tuple { | ||
318 | mark::hit!(inserts_parens_for_tuple_enums); | ||
319 | let params = Params::Anonymous(variant.fields(ctx.db).len()); | ||
320 | res = res.add_call_parens(ctx, qualified_name, params) | ||
321 | } | ||
322 | |||
323 | res.add_to(self); | ||
324 | } | ||
325 | } | ||
326 | |||
327 | pub(crate) fn compute_score( | ||
328 | ctx: &CompletionContext, | ||
329 | ty: &Type, | ||
330 | name: &str, | ||
331 | ) -> Option<CompletionScore> { | ||
332 | let (active_name, active_type) = if let Some(record_field) = &ctx.record_field_syntax { | ||
333 | mark::hit!(record_field_type_match); | ||
334 | let (struct_field, _local) = ctx.sema.resolve_record_field(record_field)?; | ||
335 | (struct_field.name(ctx.db).to_string(), struct_field.signature_ty(ctx.db)) | ||
336 | } else if let Some(active_parameter) = &ctx.active_parameter { | ||
337 | mark::hit!(active_param_type_match); | ||
338 | (active_parameter.name.clone(), active_parameter.ty.clone()) | ||
339 | } else { | ||
340 | return None; | ||
341 | }; | ||
342 | |||
343 | // Compute score | ||
344 | // For the same type | ||
345 | if &active_type != ty { | ||
346 | return None; | ||
347 | } | ||
348 | |||
349 | let mut res = CompletionScore::TypeMatch; | ||
350 | |||
351 | // If same type + same name then go top position | ||
352 | if active_name == name { | ||
353 | res = CompletionScore::TypeAndNameMatch | ||
354 | } | ||
355 | |||
356 | Some(res) | ||
357 | } | ||
358 | |||
359 | enum Params { | ||
360 | Named(Vec<String>), | ||
361 | Anonymous(usize), | ||
362 | } | ||
363 | |||
364 | impl Params { | ||
365 | fn len(&self) -> usize { | ||
366 | match self { | ||
367 | Params::Named(xs) => xs.len(), | ||
368 | Params::Anonymous(len) => *len, | ||
369 | } | ||
370 | } | ||
371 | |||
372 | fn is_empty(&self) -> bool { | ||
373 | self.len() == 0 | ||
374 | } | ||
375 | } | ||
376 | |||
377 | impl Builder { | ||
378 | fn add_call_parens(mut self, ctx: &CompletionContext, name: String, params: Params) -> Builder { | ||
379 | if !ctx.config.add_call_parenthesis { | ||
380 | return self; | ||
381 | } | ||
382 | if ctx.use_item_syntax.is_some() { | ||
383 | mark::hit!(no_parens_in_use_item); | ||
384 | return self; | ||
385 | } | ||
386 | if ctx.is_pattern_call { | ||
387 | mark::hit!(dont_duplicate_pattern_parens); | ||
388 | return self; | ||
389 | } | ||
390 | if ctx.is_call { | ||
391 | return self; | ||
392 | } | ||
393 | |||
394 | // Don't add parentheses if the expected type is some function reference. | ||
395 | if let Some(ty) = &ctx.expected_type { | ||
396 | if ty.is_fn() { | ||
397 | mark::hit!(no_call_parens_if_fn_ptr_needed); | ||
398 | return self; | ||
399 | } | ||
400 | } | ||
401 | |||
402 | let cap = match ctx.config.snippet_cap { | ||
403 | Some(it) => it, | ||
404 | None => return self, | ||
405 | }; | ||
406 | // If not an import, add parenthesis automatically. | ||
407 | mark::hit!(inserts_parens_for_function_calls); | ||
408 | |||
409 | let (snippet, label) = if params.is_empty() { | ||
410 | (format!("{}()$0", name), format!("{}()", name)) | ||
411 | } else { | ||
412 | self = self.trigger_call_info(); | ||
413 | let snippet = match (ctx.config.add_call_argument_snippets, params) { | ||
414 | (true, Params::Named(params)) => { | ||
415 | let function_params_snippet = | ||
416 | params.iter().enumerate().format_with(", ", |(index, param_name), f| { | ||
417 | f(&format_args!("${{{}:{}}}", index + 1, param_name)) | ||
418 | }); | ||
419 | format!("{}({})$0", name, function_params_snippet) | ||
420 | } | ||
421 | _ => { | ||
422 | mark::hit!(suppress_arg_snippets); | ||
423 | format!("{}($0)", name) | ||
424 | } | ||
425 | }; | ||
426 | |||
427 | (snippet, format!("{}(…)", name)) | ||
428 | }; | ||
429 | self.lookup_by(name).label(label).insert_snippet(cap, snippet) | ||
430 | } | ||
431 | } | ||
432 | |||
433 | fn is_deprecated(node: impl HasAttrs, db: &RootDatabase) -> bool { | ||
434 | node.attrs(db).by_key("deprecated").exists() | ||
435 | } | ||
436 | |||
437 | fn guess_macro_braces(macro_name: &str, docs: &str) -> (&'static str, &'static str) { | ||
438 | let mut votes = [0, 0, 0]; | ||
439 | for (idx, s) in docs.match_indices(¯o_name) { | ||
440 | let (before, after) = (&docs[..idx], &docs[idx + s.len()..]); | ||
441 | // Ensure to match the full word | ||
442 | if after.starts_with('!') | ||
443 | && !before.ends_with(|c: char| c == '_' || c.is_ascii_alphanumeric()) | ||
444 | { | ||
445 | // It may have spaces before the braces like `foo! {}` | ||
446 | match after[1..].chars().find(|&c| !c.is_whitespace()) { | ||
447 | Some('{') => votes[0] += 1, | ||
448 | Some('[') => votes[1] += 1, | ||
449 | Some('(') => votes[2] += 1, | ||
450 | _ => {} | ||
451 | } | ||
452 | } | ||
453 | } | ||
454 | |||
455 | // Insert a space before `{}`. | ||
456 | // We prefer the last one when some votes equal. | ||
457 | let (_vote, (bra, ket)) = votes | ||
458 | .iter() | ||
459 | .zip(&[(" {", "}"), ("[", "]"), ("(", ")")]) | ||
460 | .max_by_key(|&(&vote, _)| vote) | ||
461 | .unwrap(); | ||
462 | (*bra, *ket) | ||
463 | } | ||
464 | |||
465 | #[cfg(test)] | ||
466 | mod tests { | ||
467 | use std::cmp::Reverse; | ||
468 | |||
469 | use expect::{expect, Expect}; | ||
470 | use test_utils::mark; | ||
471 | |||
472 | use crate::{ | ||
473 | completion::{ | ||
474 | test_utils::{ | ||
475 | check_edit, check_edit_with_config, do_completion, get_all_completion_items, | ||
476 | }, | ||
477 | CompletionConfig, CompletionKind, | ||
478 | }, | ||
479 | CompletionScore, | ||
480 | }; | ||
481 | |||
482 | fn check(ra_fixture: &str, expect: Expect) { | ||
483 | let actual = do_completion(ra_fixture, CompletionKind::Reference); | ||
484 | expect.assert_debug_eq(&actual); | ||
485 | } | ||
486 | |||
487 | fn check_scores(ra_fixture: &str, expect: Expect) { | ||
488 | fn display_score(score: Option<CompletionScore>) -> &'static str { | ||
489 | match score { | ||
490 | Some(CompletionScore::TypeMatch) => "[type]", | ||
491 | Some(CompletionScore::TypeAndNameMatch) => "[type+name]", | ||
492 | None => "[]".into(), | ||
493 | } | ||
494 | } | ||
495 | |||
496 | let mut completions = get_all_completion_items(CompletionConfig::default(), ra_fixture); | ||
497 | completions.sort_by_key(|it| (Reverse(it.score()), it.label().to_string())); | ||
498 | let actual = completions | ||
499 | .into_iter() | ||
500 | .filter(|it| it.completion_kind == CompletionKind::Reference) | ||
501 | .map(|it| { | ||
502 | let tag = it.kind().unwrap().tag(); | ||
503 | let score = display_score(it.score()); | ||
504 | format!("{} {} {}\n", tag, it.label(), score) | ||
505 | }) | ||
506 | .collect::<String>(); | ||
507 | expect.assert_eq(&actual); | ||
508 | } | ||
509 | |||
510 | #[test] | ||
511 | fn enum_detail_includes_record_fields() { | ||
512 | check( | ||
513 | r#" | ||
514 | enum Foo { Foo { x: i32, y: i32 } } | ||
515 | |||
516 | fn main() { Foo::Fo<|> } | ||
517 | "#, | ||
518 | expect![[r#" | ||
519 | [ | ||
520 | CompletionItem { | ||
521 | label: "Foo", | ||
522 | source_range: 54..56, | ||
523 | delete: 54..56, | ||
524 | insert: "Foo", | ||
525 | kind: EnumVariant, | ||
526 | detail: "{ x: i32, y: i32 }", | ||
527 | }, | ||
528 | ] | ||
529 | "#]], | ||
530 | ); | ||
531 | } | ||
532 | |||
533 | #[test] | ||
534 | fn enum_detail_doesnt_include_tuple_fields() { | ||
535 | check( | ||
536 | r#" | ||
537 | enum Foo { Foo (i32, i32) } | ||
538 | |||
539 | fn main() { Foo::Fo<|> } | ||
540 | "#, | ||
541 | expect![[r#" | ||
542 | [ | ||
543 | CompletionItem { | ||
544 | label: "Foo(…)", | ||
545 | source_range: 46..48, | ||
546 | delete: 46..48, | ||
547 | insert: "Foo($0)", | ||
548 | kind: EnumVariant, | ||
549 | lookup: "Foo", | ||
550 | detail: "(i32, i32)", | ||
551 | trigger_call_info: true, | ||
552 | }, | ||
553 | ] | ||
554 | "#]], | ||
555 | ); | ||
556 | } | ||
557 | |||
558 | #[test] | ||
559 | fn enum_detail_just_parentheses_for_unit() { | ||
560 | check( | ||
561 | r#" | ||
562 | enum Foo { Foo } | ||
563 | |||
564 | fn main() { Foo::Fo<|> } | ||
565 | "#, | ||
566 | expect![[r#" | ||
567 | [ | ||
568 | CompletionItem { | ||
569 | label: "Foo", | ||
570 | source_range: 35..37, | ||
571 | delete: 35..37, | ||
572 | insert: "Foo", | ||
573 | kind: EnumVariant, | ||
574 | detail: "()", | ||
575 | }, | ||
576 | ] | ||
577 | "#]], | ||
578 | ); | ||
579 | } | ||
580 | |||
581 | #[test] | ||
582 | fn sets_deprecated_flag_in_completion_items() { | ||
583 | check( | ||
584 | r#" | ||
585 | #[deprecated] | ||
586 | fn something_deprecated() {} | ||
587 | #[deprecated(since = "1.0.0")] | ||
588 | fn something_else_deprecated() {} | ||
589 | |||
590 | fn main() { som<|> } | ||
591 | "#, | ||
592 | expect![[r#" | ||
593 | [ | ||
594 | CompletionItem { | ||
595 | label: "main()", | ||
596 | source_range: 121..124, | ||
597 | delete: 121..124, | ||
598 | insert: "main()$0", | ||
599 | kind: Function, | ||
600 | lookup: "main", | ||
601 | detail: "fn main()", | ||
602 | }, | ||
603 | CompletionItem { | ||
604 | label: "something_deprecated()", | ||
605 | source_range: 121..124, | ||
606 | delete: 121..124, | ||
607 | insert: "something_deprecated()$0", | ||
608 | kind: Function, | ||
609 | lookup: "something_deprecated", | ||
610 | detail: "fn something_deprecated()", | ||
611 | deprecated: true, | ||
612 | }, | ||
613 | CompletionItem { | ||
614 | label: "something_else_deprecated()", | ||
615 | source_range: 121..124, | ||
616 | delete: 121..124, | ||
617 | insert: "something_else_deprecated()$0", | ||
618 | kind: Function, | ||
619 | lookup: "something_else_deprecated", | ||
620 | detail: "fn something_else_deprecated()", | ||
621 | deprecated: true, | ||
622 | }, | ||
623 | ] | ||
624 | "#]], | ||
625 | ); | ||
626 | |||
627 | check( | ||
628 | r#" | ||
629 | struct A { #[deprecated] the_field: u32 } | ||
630 | fn foo() { A { the<|> } } | ||
631 | "#, | ||
632 | expect![[r#" | ||
633 | [ | ||
634 | CompletionItem { | ||
635 | label: "the_field", | ||
636 | source_range: 57..60, | ||
637 | delete: 57..60, | ||
638 | insert: "the_field", | ||
639 | kind: Field, | ||
640 | detail: "u32", | ||
641 | deprecated: true, | ||
642 | }, | ||
643 | ] | ||
644 | "#]], | ||
645 | ); | ||
646 | } | ||
647 | |||
648 | #[test] | ||
649 | fn renders_docs() { | ||
650 | check( | ||
651 | r#" | ||
652 | struct S { | ||
653 | /// Field docs | ||
654 | foo: | ||
655 | } | ||
656 | impl S { | ||
657 | /// Method docs | ||
658 | fn bar(self) { self.<|> } | ||
659 | }"#, | ||
660 | expect![[r#" | ||
661 | [ | ||
662 | CompletionItem { | ||
663 | label: "bar()", | ||
664 | source_range: 94..94, | ||
665 | delete: 94..94, | ||
666 | insert: "bar()$0", | ||
667 | kind: Method, | ||
668 | lookup: "bar", | ||
669 | detail: "fn bar(self)", | ||
670 | documentation: Documentation( | ||
671 | "Method docs", | ||
672 | ), | ||
673 | }, | ||
674 | CompletionItem { | ||
675 | label: "foo", | ||
676 | source_range: 94..94, | ||
677 | delete: 94..94, | ||
678 | insert: "foo", | ||
679 | kind: Field, | ||
680 | detail: "{unknown}", | ||
681 | documentation: Documentation( | ||
682 | "Field docs", | ||
683 | ), | ||
684 | }, | ||
685 | ] | ||
686 | "#]], | ||
687 | ); | ||
688 | |||
689 | check( | ||
690 | r#" | ||
691 | use self::my<|>; | ||
692 | |||
693 | /// mod docs | ||
694 | mod my { } | ||
695 | |||
696 | /// enum docs | ||
697 | enum E { | ||
698 | /// variant docs | ||
699 | V | ||
700 | } | ||
701 | use self::E::*; | ||
702 | "#, | ||
703 | expect![[r#" | ||
704 | [ | ||
705 | CompletionItem { | ||
706 | label: "E", | ||
707 | source_range: 10..12, | ||
708 | delete: 10..12, | ||
709 | insert: "E", | ||
710 | kind: Enum, | ||
711 | documentation: Documentation( | ||
712 | "enum docs", | ||
713 | ), | ||
714 | }, | ||
715 | CompletionItem { | ||
716 | label: "V", | ||
717 | source_range: 10..12, | ||
718 | delete: 10..12, | ||
719 | insert: "V", | ||
720 | kind: EnumVariant, | ||
721 | detail: "()", | ||
722 | documentation: Documentation( | ||
723 | "variant docs", | ||
724 | ), | ||
725 | }, | ||
726 | CompletionItem { | ||
727 | label: "my", | ||
728 | source_range: 10..12, | ||
729 | delete: 10..12, | ||
730 | insert: "my", | ||
731 | kind: Module, | ||
732 | documentation: Documentation( | ||
733 | "mod docs", | ||
734 | ), | ||
735 | }, | ||
736 | ] | ||
737 | "#]], | ||
738 | ) | ||
739 | } | ||
740 | |||
741 | #[test] | ||
742 | fn dont_render_attrs() { | ||
743 | check( | ||
744 | r#" | ||
745 | struct S; | ||
746 | impl S { | ||
747 | #[inline] | ||
748 | fn the_method(&self) { } | ||
749 | } | ||
750 | fn foo(s: S) { s.<|> } | ||
751 | "#, | ||
752 | expect![[r#" | ||
753 | [ | ||
754 | CompletionItem { | ||
755 | label: "the_method()", | ||
756 | source_range: 81..81, | ||
757 | delete: 81..81, | ||
758 | insert: "the_method()$0", | ||
759 | kind: Method, | ||
760 | lookup: "the_method", | ||
761 | detail: "fn the_method(&self)", | ||
762 | }, | ||
763 | ] | ||
764 | "#]], | ||
765 | ) | ||
766 | } | ||
767 | |||
768 | #[test] | ||
769 | fn inserts_parens_for_function_calls() { | ||
770 | mark::check!(inserts_parens_for_function_calls); | ||
771 | check_edit( | ||
772 | "no_args", | ||
773 | r#" | ||
774 | fn no_args() {} | ||
775 | fn main() { no_<|> } | ||
776 | "#, | ||
777 | r#" | ||
778 | fn no_args() {} | ||
779 | fn main() { no_args()$0 } | ||
780 | "#, | ||
781 | ); | ||
782 | |||
783 | check_edit( | ||
784 | "with_args", | ||
785 | r#" | ||
786 | fn with_args(x: i32, y: String) {} | ||
787 | fn main() { with_<|> } | ||
788 | "#, | ||
789 | r#" | ||
790 | fn with_args(x: i32, y: String) {} | ||
791 | fn main() { with_args(${1:x}, ${2:y})$0 } | ||
792 | "#, | ||
793 | ); | ||
794 | |||
795 | check_edit( | ||
796 | "foo", | ||
797 | r#" | ||
798 | struct S; | ||
799 | impl S { | ||
800 | fn foo(&self) {} | ||
801 | } | ||
802 | fn bar(s: &S) { s.f<|> } | ||
803 | "#, | ||
804 | r#" | ||
805 | struct S; | ||
806 | impl S { | ||
807 | fn foo(&self) {} | ||
808 | } | ||
809 | fn bar(s: &S) { s.foo()$0 } | ||
810 | "#, | ||
811 | ); | ||
812 | |||
813 | check_edit( | ||
814 | "foo", | ||
815 | r#" | ||
816 | struct S {} | ||
817 | impl S { | ||
818 | fn foo(&self, x: i32) {} | ||
819 | } | ||
820 | fn bar(s: &S) { | ||
821 | s.f<|> | ||
822 | } | ||
823 | "#, | ||
824 | r#" | ||
825 | struct S {} | ||
826 | impl S { | ||
827 | fn foo(&self, x: i32) {} | ||
828 | } | ||
829 | fn bar(s: &S) { | ||
830 | s.foo(${1:x})$0 | ||
831 | } | ||
832 | "#, | ||
833 | ); | ||
834 | } | ||
835 | |||
836 | #[test] | ||
837 | fn suppress_arg_snippets() { | ||
838 | mark::check!(suppress_arg_snippets); | ||
839 | check_edit_with_config( | ||
840 | CompletionConfig { add_call_argument_snippets: false, ..CompletionConfig::default() }, | ||
841 | "with_args", | ||
842 | r#" | ||
843 | fn with_args(x: i32, y: String) {} | ||
844 | fn main() { with_<|> } | ||
845 | "#, | ||
846 | r#" | ||
847 | fn with_args(x: i32, y: String) {} | ||
848 | fn main() { with_args($0) } | ||
849 | "#, | ||
850 | ); | ||
851 | } | ||
852 | |||
853 | #[test] | ||
854 | fn strips_underscores_from_args() { | ||
855 | check_edit( | ||
856 | "foo", | ||
857 | r#" | ||
858 | fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {} | ||
859 | fn main() { f<|> } | ||
860 | "#, | ||
861 | r#" | ||
862 | fn foo(_foo: i32, ___bar: bool, ho_ge_: String) {} | ||
863 | fn main() { foo(${1:foo}, ${2:bar}, ${3:ho_ge_})$0 } | ||
864 | "#, | ||
865 | ); | ||
866 | } | ||
867 | |||
868 | #[test] | ||
869 | fn inserts_parens_for_tuple_enums() { | ||
870 | mark::check!(inserts_parens_for_tuple_enums); | ||
871 | check_edit( | ||
872 | "Some", | ||
873 | r#" | ||
874 | enum Option<T> { Some(T), None } | ||
875 | use Option::*; | ||
876 | fn main() -> Option<i32> { | ||
877 | Som<|> | ||
878 | } | ||
879 | "#, | ||
880 | r#" | ||
881 | enum Option<T> { Some(T), None } | ||
882 | use Option::*; | ||
883 | fn main() -> Option<i32> { | ||
884 | Some($0) | ||
885 | } | ||
886 | "#, | ||
887 | ); | ||
888 | check_edit( | ||
889 | "Some", | ||
890 | r#" | ||
891 | enum Option<T> { Some(T), None } | ||
892 | use Option::*; | ||
893 | fn main(value: Option<i32>) { | ||
894 | match value { | ||
895 | Som<|> | ||
896 | } | ||
897 | } | ||
898 | "#, | ||
899 | r#" | ||
900 | enum Option<T> { Some(T), None } | ||
901 | use Option::*; | ||
902 | fn main(value: Option<i32>) { | ||
903 | match value { | ||
904 | Some($0) | ||
905 | } | ||
906 | } | ||
907 | "#, | ||
908 | ); | ||
909 | } | ||
910 | |||
911 | #[test] | ||
912 | fn dont_duplicate_pattern_parens() { | ||
913 | mark::check!(dont_duplicate_pattern_parens); | ||
914 | check_edit( | ||
915 | "Var", | ||
916 | r#" | ||
917 | enum E { Var(i32) } | ||
918 | fn main() { | ||
919 | match E::Var(92) { | ||
920 | E::<|>(92) => (), | ||
921 | } | ||
922 | } | ||
923 | "#, | ||
924 | r#" | ||
925 | enum E { Var(i32) } | ||
926 | fn main() { | ||
927 | match E::Var(92) { | ||
928 | E::Var(92) => (), | ||
929 | } | ||
930 | } | ||
931 | "#, | ||
932 | ); | ||
933 | } | ||
934 | |||
935 | #[test] | ||
936 | fn no_call_parens_if_fn_ptr_needed() { | ||
937 | mark::check!(no_call_parens_if_fn_ptr_needed); | ||
938 | check_edit( | ||
939 | "foo", | ||
940 | r#" | ||
941 | fn foo(foo: u8, bar: u8) {} | ||
942 | struct ManualVtable { f: fn(u8, u8) } | ||
943 | |||
944 | fn main() -> ManualVtable { | ||
945 | ManualVtable { f: f<|> } | ||
946 | } | ||
947 | "#, | ||
948 | r#" | ||
949 | fn foo(foo: u8, bar: u8) {} | ||
950 | struct ManualVtable { f: fn(u8, u8) } | ||
951 | |||
952 | fn main() -> ManualVtable { | ||
953 | ManualVtable { f: foo } | ||
954 | } | ||
955 | "#, | ||
956 | ); | ||
957 | } | ||
958 | |||
959 | #[test] | ||
960 | fn no_parens_in_use_item() { | ||
961 | mark::check!(no_parens_in_use_item); | ||
962 | check_edit( | ||
963 | "foo", | ||
964 | r#" | ||
965 | mod m { pub fn foo() {} } | ||
966 | use crate::m::f<|>; | ||
967 | "#, | ||
968 | r#" | ||
969 | mod m { pub fn foo() {} } | ||
970 | use crate::m::foo; | ||
971 | "#, | ||
972 | ); | ||
973 | } | ||
974 | |||
975 | #[test] | ||
976 | fn no_parens_in_call() { | ||
977 | check_edit( | ||
978 | "foo", | ||
979 | r#" | ||
980 | fn foo(x: i32) {} | ||
981 | fn main() { f<|>(); } | ||
982 | "#, | ||
983 | r#" | ||
984 | fn foo(x: i32) {} | ||
985 | fn main() { foo(); } | ||
986 | "#, | ||
987 | ); | ||
988 | check_edit( | ||
989 | "foo", | ||
990 | r#" | ||
991 | struct Foo; | ||
992 | impl Foo { fn foo(&self){} } | ||
993 | fn f(foo: &Foo) { foo.f<|>(); } | ||
994 | "#, | ||
995 | r#" | ||
996 | struct Foo; | ||
997 | impl Foo { fn foo(&self){} } | ||
998 | fn f(foo: &Foo) { foo.foo(); } | ||
999 | "#, | ||
1000 | ); | ||
1001 | } | ||
1002 | |||
1003 | #[test] | ||
1004 | fn inserts_angle_brackets_for_generics() { | ||
1005 | mark::check!(inserts_angle_brackets_for_generics); | ||
1006 | check_edit( | ||
1007 | "Vec", | ||
1008 | r#" | ||
1009 | struct Vec<T> {} | ||
1010 | fn foo(xs: Ve<|>) | ||
1011 | "#, | ||
1012 | r#" | ||
1013 | struct Vec<T> {} | ||
1014 | fn foo(xs: Vec<$0>) | ||
1015 | "#, | ||
1016 | ); | ||
1017 | check_edit( | ||
1018 | "Vec", | ||
1019 | r#" | ||
1020 | type Vec<T> = (T,); | ||
1021 | fn foo(xs: Ve<|>) | ||
1022 | "#, | ||
1023 | r#" | ||
1024 | type Vec<T> = (T,); | ||
1025 | fn foo(xs: Vec<$0>) | ||
1026 | "#, | ||
1027 | ); | ||
1028 | check_edit( | ||
1029 | "Vec", | ||
1030 | r#" | ||
1031 | struct Vec<T = i128> {} | ||
1032 | fn foo(xs: Ve<|>) | ||
1033 | "#, | ||
1034 | r#" | ||
1035 | struct Vec<T = i128> {} | ||
1036 | fn foo(xs: Vec) | ||
1037 | "#, | ||
1038 | ); | ||
1039 | check_edit( | ||
1040 | "Vec", | ||
1041 | r#" | ||
1042 | struct Vec<T> {} | ||
1043 | fn foo(xs: Ve<|><i128>) | ||
1044 | "#, | ||
1045 | r#" | ||
1046 | struct Vec<T> {} | ||
1047 | fn foo(xs: Vec<i128>) | ||
1048 | "#, | ||
1049 | ); | ||
1050 | } | ||
1051 | |||
1052 | #[test] | ||
1053 | fn dont_insert_macro_call_parens_unncessary() { | ||
1054 | mark::check!(dont_insert_macro_call_parens_unncessary); | ||
1055 | check_edit( | ||
1056 | "frobnicate!", | ||
1057 | r#" | ||
1058 | //- /main.rs | ||
1059 | use foo::<|>; | ||
1060 | //- /foo/lib.rs | ||
1061 | #[macro_export] | ||
1062 | macro_rules frobnicate { () => () } | ||
1063 | "#, | ||
1064 | r#" | ||
1065 | use foo::frobnicate; | ||
1066 | "#, | ||
1067 | ); | ||
1068 | |||
1069 | check_edit( | ||
1070 | "frobnicate!", | ||
1071 | r#" | ||
1072 | macro_rules frobnicate { () => () } | ||
1073 | fn main() { frob<|>!(); } | ||
1074 | "#, | ||
1075 | r#" | ||
1076 | macro_rules frobnicate { () => () } | ||
1077 | fn main() { frobnicate!(); } | ||
1078 | "#, | ||
1079 | ); | ||
1080 | } | ||
1081 | |||
1082 | #[test] | ||
1083 | fn active_param_score() { | ||
1084 | mark::check!(active_param_type_match); | ||
1085 | check_scores( | ||
1086 | r#" | ||
1087 | struct S { foo: i64, bar: u32, baz: u32 } | ||
1088 | fn test(bar: u32) { } | ||
1089 | fn foo(s: S) { test(s.<|>) } | ||
1090 | "#, | ||
1091 | expect![[r#" | ||
1092 | fd bar [type+name] | ||
1093 | fd baz [type] | ||
1094 | fd foo [] | ||
1095 | "#]], | ||
1096 | ); | ||
1097 | } | ||
1098 | |||
1099 | #[test] | ||
1100 | fn record_field_scores() { | ||
1101 | mark::check!(record_field_type_match); | ||
1102 | check_scores( | ||
1103 | r#" | ||
1104 | struct A { foo: i64, bar: u32, baz: u32 } | ||
1105 | struct B { x: (), y: f32, bar: u32 } | ||
1106 | fn foo(a: A) { B { bar: a.<|> }; } | ||
1107 | "#, | ||
1108 | expect![[r#" | ||
1109 | fd bar [type+name] | ||
1110 | fd baz [type] | ||
1111 | fd foo [] | ||
1112 | "#]], | ||
1113 | ) | ||
1114 | } | ||
1115 | |||
1116 | #[test] | ||
1117 | fn record_field_and_call_scores() { | ||
1118 | check_scores( | ||
1119 | r#" | ||
1120 | struct A { foo: i64, bar: u32, baz: u32 } | ||
1121 | struct B { x: (), y: f32, bar: u32 } | ||
1122 | fn f(foo: i64) { } | ||
1123 | fn foo(a: A) { B { bar: f(a.<|>) }; } | ||
1124 | "#, | ||
1125 | expect![[r#" | ||
1126 | fd foo [type+name] | ||
1127 | fd bar [] | ||
1128 | fd baz [] | ||
1129 | "#]], | ||
1130 | ); | ||
1131 | check_scores( | ||
1132 | r#" | ||
1133 | struct A { foo: i64, bar: u32, baz: u32 } | ||
1134 | struct B { x: (), y: f32, bar: u32 } | ||
1135 | fn f(foo: i64) { } | ||
1136 | fn foo(a: A) { f(B { bar: a.<|> }); } | ||
1137 | "#, | ||
1138 | expect![[r#" | ||
1139 | fd bar [type+name] | ||
1140 | fd baz [type] | ||
1141 | fd foo [] | ||
1142 | "#]], | ||
1143 | ); | ||
1144 | } | ||
1145 | |||
1146 | #[test] | ||
1147 | fn prioritize_exact_ref_match() { | ||
1148 | check_scores( | ||
1149 | r#" | ||
1150 | struct WorldSnapshot { _f: () }; | ||
1151 | fn go(world: &WorldSnapshot) { go(w<|>) } | ||
1152 | "#, | ||
1153 | expect![[r#" | ||
1154 | bn world [type+name] | ||
1155 | st WorldSnapshot [] | ||
1156 | fn go(…) [] | ||
1157 | "#]], | ||
1158 | ); | ||
1159 | } | ||
1160 | |||
1161 | #[test] | ||
1162 | fn too_many_arguments() { | ||
1163 | mark::check!(too_many_arguments); | ||
1164 | check_scores( | ||
1165 | r#" | ||
1166 | struct Foo; | ||
1167 | fn f(foo: &Foo) { f(foo, w<|>) } | ||
1168 | "#, | ||
1169 | expect![[r#" | ||
1170 | st Foo [] | ||
1171 | fn f(…) [] | ||
1172 | bn foo [] | ||
1173 | "#]], | ||
1174 | ); | ||
1175 | } | ||
1176 | |||
1177 | #[test] | ||
1178 | fn guesses_macro_braces() { | ||
1179 | check_edit( | ||
1180 | "vec!", | ||
1181 | r#" | ||
1182 | /// Creates a [`Vec`] containing the arguments. | ||
1183 | /// | ||
1184 | /// ``` | ||
1185 | /// let v = vec![1, 2, 3]; | ||
1186 | /// assert_eq!(v[0], 1); | ||
1187 | /// assert_eq!(v[1], 2); | ||
1188 | /// assert_eq!(v[2], 3); | ||
1189 | /// ``` | ||
1190 | macro_rules! vec { () => {} } | ||
1191 | |||
1192 | fn fn main() { v<|> } | ||
1193 | "#, | ||
1194 | r#" | ||
1195 | /// Creates a [`Vec`] containing the arguments. | ||
1196 | /// | ||
1197 | /// ``` | ||
1198 | /// let v = vec![1, 2, 3]; | ||
1199 | /// assert_eq!(v[0], 1); | ||
1200 | /// assert_eq!(v[1], 2); | ||
1201 | /// assert_eq!(v[2], 3); | ||
1202 | /// ``` | ||
1203 | macro_rules! vec { () => {} } | ||
1204 | |||
1205 | fn fn main() { vec![$0] } | ||
1206 | "#, | ||
1207 | ); | ||
1208 | |||
1209 | check_edit( | ||
1210 | "foo!", | ||
1211 | r#" | ||
1212 | /// Foo | ||
1213 | /// | ||
1214 | /// Don't call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`, | ||
1215 | /// call as `let _=foo! { hello world };` | ||
1216 | macro_rules! foo { () => {} } | ||
1217 | fn main() { <|> } | ||
1218 | "#, | ||
1219 | r#" | ||
1220 | /// Foo | ||
1221 | /// | ||
1222 | /// Don't call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`, | ||
1223 | /// call as `let _=foo! { hello world };` | ||
1224 | macro_rules! foo { () => {} } | ||
1225 | fn main() { foo! {$0} } | ||
1226 | "#, | ||
1227 | ) | ||
1228 | } | ||
1229 | } | ||
diff --git a/crates/ide/src/completion/test_utils.rs b/crates/ide/src/completion/test_utils.rs new file mode 100644 index 000000000..1452d7e9e --- /dev/null +++ b/crates/ide/src/completion/test_utils.rs | |||
@@ -0,0 +1,114 @@ | |||
1 | //! Runs completion for testing purposes. | ||
2 | |||
3 | use hir::Semantics; | ||
4 | use itertools::Itertools; | ||
5 | use stdx::{format_to, trim_indent}; | ||
6 | use syntax::{AstNode, NodeOrToken, SyntaxElement}; | ||
7 | use test_utils::assert_eq_text; | ||
8 | |||
9 | use crate::{ | ||
10 | completion::{completion_item::CompletionKind, CompletionConfig}, | ||
11 | mock_analysis::analysis_and_position, | ||
12 | CompletionItem, | ||
13 | }; | ||
14 | |||
15 | pub(crate) fn do_completion(code: &str, kind: CompletionKind) -> Vec<CompletionItem> { | ||
16 | do_completion_with_config(CompletionConfig::default(), code, kind) | ||
17 | } | ||
18 | |||
19 | pub(crate) fn do_completion_with_config( | ||
20 | config: CompletionConfig, | ||
21 | code: &str, | ||
22 | kind: CompletionKind, | ||
23 | ) -> Vec<CompletionItem> { | ||
24 | let mut kind_completions: Vec<CompletionItem> = get_all_completion_items(config, code) | ||
25 | .into_iter() | ||
26 | .filter(|c| c.completion_kind == kind) | ||
27 | .collect(); | ||
28 | kind_completions.sort_by(|l, r| l.label().cmp(r.label())); | ||
29 | kind_completions | ||
30 | } | ||
31 | |||
32 | pub(crate) fn completion_list(code: &str, kind: CompletionKind) -> String { | ||
33 | completion_list_with_config(CompletionConfig::default(), code, kind) | ||
34 | } | ||
35 | |||
36 | pub(crate) fn completion_list_with_config( | ||
37 | config: CompletionConfig, | ||
38 | code: &str, | ||
39 | kind: CompletionKind, | ||
40 | ) -> String { | ||
41 | let mut kind_completions: Vec<CompletionItem> = get_all_completion_items(config, code) | ||
42 | .into_iter() | ||
43 | .filter(|c| c.completion_kind == kind) | ||
44 | .collect(); | ||
45 | kind_completions.sort_by_key(|c| c.label().to_owned()); | ||
46 | let label_width = kind_completions | ||
47 | .iter() | ||
48 | .map(|it| monospace_width(it.label())) | ||
49 | .max() | ||
50 | .unwrap_or_default() | ||
51 | .min(16); | ||
52 | kind_completions | ||
53 | .into_iter() | ||
54 | .map(|it| { | ||
55 | let tag = it.kind().unwrap().tag(); | ||
56 | let var_name = format!("{} {}", tag, it.label()); | ||
57 | let mut buf = var_name; | ||
58 | if let Some(detail) = it.detail() { | ||
59 | let width = label_width.saturating_sub(monospace_width(it.label())); | ||
60 | format_to!(buf, "{:width$} {}", "", detail, width = width); | ||
61 | } | ||
62 | format_to!(buf, "\n"); | ||
63 | buf | ||
64 | }) | ||
65 | .collect() | ||
66 | } | ||
67 | |||
68 | fn monospace_width(s: &str) -> usize { | ||
69 | s.chars().count() | ||
70 | } | ||
71 | |||
72 | pub(crate) fn check_edit(what: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
73 | check_edit_with_config(CompletionConfig::default(), what, ra_fixture_before, ra_fixture_after) | ||
74 | } | ||
75 | |||
76 | pub(crate) fn check_edit_with_config( | ||
77 | config: CompletionConfig, | ||
78 | what: &str, | ||
79 | ra_fixture_before: &str, | ||
80 | ra_fixture_after: &str, | ||
81 | ) { | ||
82 | let ra_fixture_after = trim_indent(ra_fixture_after); | ||
83 | let (analysis, position) = analysis_and_position(ra_fixture_before); | ||
84 | let completions: Vec<CompletionItem> = | ||
85 | analysis.completions(&config, position).unwrap().unwrap().into(); | ||
86 | let (completion,) = completions | ||
87 | .iter() | ||
88 | .filter(|it| it.lookup() == what) | ||
89 | .collect_tuple() | ||
90 | .unwrap_or_else(|| panic!("can't find {:?} completion in {:#?}", what, completions)); | ||
91 | let mut actual = analysis.file_text(position.file_id).unwrap().to_string(); | ||
92 | completion.text_edit().apply(&mut actual); | ||
93 | assert_eq_text!(&ra_fixture_after, &actual) | ||
94 | } | ||
95 | |||
96 | pub(crate) fn check_pattern_is_applicable(code: &str, check: fn(SyntaxElement) -> bool) { | ||
97 | let (analysis, pos) = analysis_and_position(code); | ||
98 | analysis | ||
99 | .with_db(|db| { | ||
100 | let sema = Semantics::new(db); | ||
101 | let original_file = sema.parse(pos.file_id); | ||
102 | let token = original_file.syntax().token_at_offset(pos.offset).left_biased().unwrap(); | ||
103 | assert!(check(NodeOrToken::Token(token))); | ||
104 | }) | ||
105 | .unwrap(); | ||
106 | } | ||
107 | |||
108 | pub(crate) fn get_all_completion_items( | ||
109 | config: CompletionConfig, | ||
110 | code: &str, | ||
111 | ) -> Vec<CompletionItem> { | ||
112 | let (analysis, position) = analysis_and_position(code); | ||
113 | analysis.completions(&config, position).unwrap().unwrap().into() | ||
114 | } | ||
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs new file mode 100644 index 000000000..a3ec98178 --- /dev/null +++ b/crates/ide/src/diagnostics.rs | |||
@@ -0,0 +1,678 @@ | |||
1 | //! Collects diagnostics & fixits for a single file. | ||
2 | //! | ||
3 | //! The tricky bit here is that diagnostics are produced by hir in terms of | ||
4 | //! macro-expanded files, but we need to present them to the users in terms of | ||
5 | //! original files. So we need to map the ranges. | ||
6 | |||
7 | use std::cell::RefCell; | ||
8 | |||
9 | use base_db::SourceDatabase; | ||
10 | use hir::{diagnostics::DiagnosticSinkBuilder, Semantics}; | ||
11 | use ide_db::RootDatabase; | ||
12 | use itertools::Itertools; | ||
13 | use syntax::{ | ||
14 | ast::{self, AstNode}, | ||
15 | SyntaxNode, TextRange, T, | ||
16 | }; | ||
17 | use text_edit::TextEdit; | ||
18 | |||
19 | use crate::{Diagnostic, FileId, Fix, SourceFileEdit}; | ||
20 | |||
21 | mod diagnostics_with_fix; | ||
22 | use diagnostics_with_fix::DiagnosticWithFix; | ||
23 | |||
24 | #[derive(Debug, Copy, Clone)] | ||
25 | pub enum Severity { | ||
26 | Error, | ||
27 | WeakWarning, | ||
28 | } | ||
29 | |||
30 | pub(crate) fn diagnostics( | ||
31 | db: &RootDatabase, | ||
32 | file_id: FileId, | ||
33 | enable_experimental: bool, | ||
34 | ) -> Vec<Diagnostic> { | ||
35 | let _p = profile::span("diagnostics"); | ||
36 | let sema = Semantics::new(db); | ||
37 | let parse = db.parse(file_id); | ||
38 | let mut res = Vec::new(); | ||
39 | |||
40 | // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily. | ||
41 | res.extend(parse.errors().iter().take(128).map(|err| Diagnostic { | ||
42 | range: err.range(), | ||
43 | message: format!("Syntax Error: {}", err), | ||
44 | severity: Severity::Error, | ||
45 | fix: None, | ||
46 | })); | ||
47 | |||
48 | for node in parse.tree().syntax().descendants() { | ||
49 | check_unnecessary_braces_in_use_statement(&mut res, file_id, &node); | ||
50 | check_struct_shorthand_initialization(&mut res, file_id, &node); | ||
51 | } | ||
52 | let res = RefCell::new(res); | ||
53 | let mut sink = DiagnosticSinkBuilder::new() | ||
54 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { | ||
55 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); | ||
56 | }) | ||
57 | .on::<hir::diagnostics::MissingFields, _>(|d| { | ||
58 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); | ||
59 | }) | ||
60 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | ||
61 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); | ||
62 | }) | ||
63 | .on::<hir::diagnostics::NoSuchField, _>(|d| { | ||
64 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); | ||
65 | }) | ||
66 | // Only collect experimental diagnostics when they're enabled. | ||
67 | .filter(|diag| !diag.is_experimental() || enable_experimental) | ||
68 | // Diagnostics not handled above get no fix and default treatment. | ||
69 | .build(|d| { | ||
70 | res.borrow_mut().push(Diagnostic { | ||
71 | message: d.message(), | ||
72 | range: sema.diagnostics_display_range(d).range, | ||
73 | severity: Severity::Error, | ||
74 | fix: None, | ||
75 | }) | ||
76 | }); | ||
77 | |||
78 | if let Some(m) = sema.to_module_def(file_id) { | ||
79 | m.diagnostics(db, &mut sink); | ||
80 | }; | ||
81 | drop(sink); | ||
82 | res.into_inner() | ||
83 | } | ||
84 | |||
85 | fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { | ||
86 | Diagnostic { | ||
87 | range: sema.diagnostics_display_range(d).range, | ||
88 | message: d.message(), | ||
89 | severity: Severity::Error, | ||
90 | fix: d.fix(&sema), | ||
91 | } | ||
92 | } | ||
93 | |||
94 | fn check_unnecessary_braces_in_use_statement( | ||
95 | acc: &mut Vec<Diagnostic>, | ||
96 | file_id: FileId, | ||
97 | node: &SyntaxNode, | ||
98 | ) -> Option<()> { | ||
99 | let use_tree_list = ast::UseTreeList::cast(node.clone())?; | ||
100 | if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { | ||
101 | let use_range = use_tree_list.syntax().text_range(); | ||
102 | let edit = | ||
103 | text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) | ||
104 | .unwrap_or_else(|| { | ||
105 | let to_replace = single_use_tree.syntax().text().to_string(); | ||
106 | let mut edit_builder = TextEdit::builder(); | ||
107 | edit_builder.delete(use_range); | ||
108 | edit_builder.insert(use_range.start(), to_replace); | ||
109 | edit_builder.finish() | ||
110 | }); | ||
111 | |||
112 | acc.push(Diagnostic { | ||
113 | range: use_range, | ||
114 | message: "Unnecessary braces in use statement".to_string(), | ||
115 | severity: Severity::WeakWarning, | ||
116 | fix: Some(Fix::new( | ||
117 | "Remove unnecessary braces", | ||
118 | SourceFileEdit { file_id, edit }.into(), | ||
119 | use_range, | ||
120 | )), | ||
121 | }); | ||
122 | } | ||
123 | |||
124 | Some(()) | ||
125 | } | ||
126 | |||
127 | fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( | ||
128 | single_use_tree: &ast::UseTree, | ||
129 | ) -> Option<TextEdit> { | ||
130 | let use_tree_list_node = single_use_tree.syntax().parent()?; | ||
131 | if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { | ||
132 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); | ||
133 | let end = use_tree_list_node.text_range().end(); | ||
134 | return Some(TextEdit::delete(TextRange::new(start, end))); | ||
135 | } | ||
136 | None | ||
137 | } | ||
138 | |||
139 | fn check_struct_shorthand_initialization( | ||
140 | acc: &mut Vec<Diagnostic>, | ||
141 | file_id: FileId, | ||
142 | node: &SyntaxNode, | ||
143 | ) -> Option<()> { | ||
144 | let record_lit = ast::RecordExpr::cast(node.clone())?; | ||
145 | let record_field_list = record_lit.record_expr_field_list()?; | ||
146 | for record_field in record_field_list.fields() { | ||
147 | if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) { | ||
148 | let field_name = name_ref.syntax().text().to_string(); | ||
149 | let field_expr = expr.syntax().text().to_string(); | ||
150 | let field_name_is_tup_index = name_ref.as_tuple_field().is_some(); | ||
151 | if field_name == field_expr && !field_name_is_tup_index { | ||
152 | let mut edit_builder = TextEdit::builder(); | ||
153 | edit_builder.delete(record_field.syntax().text_range()); | ||
154 | edit_builder.insert(record_field.syntax().text_range().start(), field_name); | ||
155 | let edit = edit_builder.finish(); | ||
156 | |||
157 | let field_range = record_field.syntax().text_range(); | ||
158 | acc.push(Diagnostic { | ||
159 | range: field_range, | ||
160 | message: "Shorthand struct initialization".to_string(), | ||
161 | severity: Severity::WeakWarning, | ||
162 | fix: Some(Fix::new( | ||
163 | "Use struct shorthand initialization", | ||
164 | SourceFileEdit { file_id, edit }.into(), | ||
165 | field_range, | ||
166 | )), | ||
167 | }); | ||
168 | } | ||
169 | } | ||
170 | } | ||
171 | Some(()) | ||
172 | } | ||
173 | |||
174 | #[cfg(test)] | ||
175 | mod tests { | ||
176 | use stdx::trim_indent; | ||
177 | use test_utils::assert_eq_text; | ||
178 | |||
179 | use crate::mock_analysis::{analysis_and_position, single_file, MockAnalysis}; | ||
180 | use expect::{expect, Expect}; | ||
181 | |||
182 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
183 | /// and checks that: | ||
184 | /// * a diagnostic is produced | ||
185 | /// * this diagnostic fix trigger range touches the input cursor position | ||
186 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | ||
187 | fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { | ||
188 | let after = trim_indent(ra_fixture_after); | ||
189 | |||
190 | let (analysis, file_position) = analysis_and_position(ra_fixture_before); | ||
191 | let diagnostic = analysis.diagnostics(file_position.file_id, true).unwrap().pop().unwrap(); | ||
192 | let mut fix = diagnostic.fix.unwrap(); | ||
193 | let edit = fix.source_change.source_file_edits.pop().unwrap().edit; | ||
194 | let target_file_contents = analysis.file_text(file_position.file_id).unwrap(); | ||
195 | let actual = { | ||
196 | let mut actual = target_file_contents.to_string(); | ||
197 | edit.apply(&mut actual); | ||
198 | actual | ||
199 | }; | ||
200 | |||
201 | assert_eq_text!(&after, &actual); | ||
202 | assert!( | ||
203 | fix.fix_trigger_range.start() <= file_position.offset | ||
204 | && fix.fix_trigger_range.end() >= file_position.offset, | ||
205 | "diagnostic fix range {:?} does not touch cursor position {:?}", | ||
206 | fix.fix_trigger_range, | ||
207 | file_position.offset | ||
208 | ); | ||
209 | } | ||
210 | |||
211 | /// Checks that a diagnostic applies to the file containing the `<|>` cursor marker | ||
212 | /// which has a fix that can apply to other files. | ||
213 | fn check_apply_diagnostic_fix_in_other_file(ra_fixture_before: &str, ra_fixture_after: &str) { | ||
214 | let ra_fixture_after = &trim_indent(ra_fixture_after); | ||
215 | let (analysis, file_pos) = analysis_and_position(ra_fixture_before); | ||
216 | let current_file_id = file_pos.file_id; | ||
217 | let diagnostic = analysis.diagnostics(current_file_id, true).unwrap().pop().unwrap(); | ||
218 | let mut fix = diagnostic.fix.unwrap(); | ||
219 | let edit = fix.source_change.source_file_edits.pop().unwrap(); | ||
220 | let changed_file_id = edit.file_id; | ||
221 | let before = analysis.file_text(changed_file_id).unwrap(); | ||
222 | let actual = { | ||
223 | let mut actual = before.to_string(); | ||
224 | edit.edit.apply(&mut actual); | ||
225 | actual | ||
226 | }; | ||
227 | assert_eq_text!(ra_fixture_after, &actual); | ||
228 | } | ||
229 | |||
230 | /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics | ||
231 | /// apply to the file containing the cursor. | ||
232 | fn check_no_diagnostics(ra_fixture: &str) { | ||
233 | let mock = MockAnalysis::with_files(ra_fixture); | ||
234 | let files = mock.files().map(|(it, _)| it).collect::<Vec<_>>(); | ||
235 | let analysis = mock.analysis(); | ||
236 | let diagnostics = files | ||
237 | .into_iter() | ||
238 | .flat_map(|file_id| analysis.diagnostics(file_id, true).unwrap()) | ||
239 | .collect::<Vec<_>>(); | ||
240 | assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); | ||
241 | } | ||
242 | |||
243 | fn check_expect(ra_fixture: &str, expect: Expect) { | ||
244 | let (analysis, file_id) = single_file(ra_fixture); | ||
245 | let diagnostics = analysis.diagnostics(file_id, true).unwrap(); | ||
246 | expect.assert_debug_eq(&diagnostics) | ||
247 | } | ||
248 | |||
249 | #[test] | ||
250 | fn test_wrap_return_type() { | ||
251 | check_fix( | ||
252 | r#" | ||
253 | //- /main.rs | ||
254 | use core::result::Result::{self, Ok, Err}; | ||
255 | |||
256 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
257 | if y == 0 { | ||
258 | return Err(()); | ||
259 | } | ||
260 | x / y<|> | ||
261 | } | ||
262 | //- /core/lib.rs | ||
263 | pub mod result { | ||
264 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
265 | } | ||
266 | "#, | ||
267 | r#" | ||
268 | use core::result::Result::{self, Ok, Err}; | ||
269 | |||
270 | fn div(x: i32, y: i32) -> Result<i32, ()> { | ||
271 | if y == 0 { | ||
272 | return Err(()); | ||
273 | } | ||
274 | Ok(x / y) | ||
275 | } | ||
276 | "#, | ||
277 | ); | ||
278 | } | ||
279 | |||
280 | #[test] | ||
281 | fn test_wrap_return_type_handles_generic_functions() { | ||
282 | check_fix( | ||
283 | r#" | ||
284 | //- /main.rs | ||
285 | use core::result::Result::{self, Ok, Err}; | ||
286 | |||
287 | fn div<T>(x: T) -> Result<T, i32> { | ||
288 | if x == 0 { | ||
289 | return Err(7); | ||
290 | } | ||
291 | <|>x | ||
292 | } | ||
293 | //- /core/lib.rs | ||
294 | pub mod result { | ||
295 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
296 | } | ||
297 | "#, | ||
298 | r#" | ||
299 | use core::result::Result::{self, Ok, Err}; | ||
300 | |||
301 | fn div<T>(x: T) -> Result<T, i32> { | ||
302 | if x == 0 { | ||
303 | return Err(7); | ||
304 | } | ||
305 | Ok(x) | ||
306 | } | ||
307 | "#, | ||
308 | ); | ||
309 | } | ||
310 | |||
311 | #[test] | ||
312 | fn test_wrap_return_type_handles_type_aliases() { | ||
313 | check_fix( | ||
314 | r#" | ||
315 | //- /main.rs | ||
316 | use core::result::Result::{self, Ok, Err}; | ||
317 | |||
318 | type MyResult<T> = Result<T, ()>; | ||
319 | |||
320 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
321 | if y == 0 { | ||
322 | return Err(()); | ||
323 | } | ||
324 | x <|>/ y | ||
325 | } | ||
326 | //- /core/lib.rs | ||
327 | pub mod result { | ||
328 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
329 | } | ||
330 | "#, | ||
331 | r#" | ||
332 | use core::result::Result::{self, Ok, Err}; | ||
333 | |||
334 | type MyResult<T> = Result<T, ()>; | ||
335 | |||
336 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
337 | if y == 0 { | ||
338 | return Err(()); | ||
339 | } | ||
340 | Ok(x / y) | ||
341 | } | ||
342 | "#, | ||
343 | ); | ||
344 | } | ||
345 | |||
346 | #[test] | ||
347 | fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { | ||
348 | check_no_diagnostics( | ||
349 | r#" | ||
350 | //- /main.rs | ||
351 | use core::result::Result::{self, Ok, Err}; | ||
352 | |||
353 | fn foo() -> Result<(), i32> { 0 } | ||
354 | |||
355 | //- /core/lib.rs | ||
356 | pub mod result { | ||
357 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
358 | } | ||
359 | "#, | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() { | ||
365 | check_no_diagnostics( | ||
366 | r#" | ||
367 | //- /main.rs | ||
368 | use core::result::Result::{self, Ok, Err}; | ||
369 | |||
370 | enum SomeOtherEnum { Ok(i32), Err(String) } | ||
371 | |||
372 | fn foo() -> SomeOtherEnum { 0 } | ||
373 | |||
374 | //- /core/lib.rs | ||
375 | pub mod result { | ||
376 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
377 | } | ||
378 | "#, | ||
379 | ); | ||
380 | } | ||
381 | |||
382 | #[test] | ||
383 | fn test_fill_struct_fields_empty() { | ||
384 | check_fix( | ||
385 | r#" | ||
386 | struct TestStruct { one: i32, two: i64 } | ||
387 | |||
388 | fn test_fn() { | ||
389 | let s = TestStruct {<|>}; | ||
390 | } | ||
391 | "#, | ||
392 | r#" | ||
393 | struct TestStruct { one: i32, two: i64 } | ||
394 | |||
395 | fn test_fn() { | ||
396 | let s = TestStruct { one: (), two: ()}; | ||
397 | } | ||
398 | "#, | ||
399 | ); | ||
400 | } | ||
401 | |||
402 | #[test] | ||
403 | fn test_fill_struct_fields_self() { | ||
404 | check_fix( | ||
405 | r#" | ||
406 | struct TestStruct { one: i32 } | ||
407 | |||
408 | impl TestStruct { | ||
409 | fn test_fn() { let s = Self {<|>}; } | ||
410 | } | ||
411 | "#, | ||
412 | r#" | ||
413 | struct TestStruct { one: i32 } | ||
414 | |||
415 | impl TestStruct { | ||
416 | fn test_fn() { let s = Self { one: ()}; } | ||
417 | } | ||
418 | "#, | ||
419 | ); | ||
420 | } | ||
421 | |||
422 | #[test] | ||
423 | fn test_fill_struct_fields_enum() { | ||
424 | check_fix( | ||
425 | r#" | ||
426 | enum Expr { | ||
427 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
428 | } | ||
429 | |||
430 | impl Expr { | ||
431 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
432 | Expr::Bin {<|> } | ||
433 | } | ||
434 | } | ||
435 | "#, | ||
436 | r#" | ||
437 | enum Expr { | ||
438 | Bin { lhs: Box<Expr>, rhs: Box<Expr> } | ||
439 | } | ||
440 | |||
441 | impl Expr { | ||
442 | fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr { | ||
443 | Expr::Bin { lhs: (), rhs: () } | ||
444 | } | ||
445 | } | ||
446 | "#, | ||
447 | ); | ||
448 | } | ||
449 | |||
450 | #[test] | ||
451 | fn test_fill_struct_fields_partial() { | ||
452 | check_fix( | ||
453 | r#" | ||
454 | struct TestStruct { one: i32, two: i64 } | ||
455 | |||
456 | fn test_fn() { | ||
457 | let s = TestStruct{ two: 2<|> }; | ||
458 | } | ||
459 | "#, | ||
460 | r" | ||
461 | struct TestStruct { one: i32, two: i64 } | ||
462 | |||
463 | fn test_fn() { | ||
464 | let s = TestStruct{ two: 2, one: () }; | ||
465 | } | ||
466 | ", | ||
467 | ); | ||
468 | } | ||
469 | |||
470 | #[test] | ||
471 | fn test_fill_struct_fields_no_diagnostic() { | ||
472 | check_no_diagnostics( | ||
473 | r" | ||
474 | struct TestStruct { one: i32, two: i64 } | ||
475 | |||
476 | fn test_fn() { | ||
477 | let one = 1; | ||
478 | let s = TestStruct{ one, two: 2 }; | ||
479 | } | ||
480 | ", | ||
481 | ); | ||
482 | } | ||
483 | |||
484 | #[test] | ||
485 | fn test_fill_struct_fields_no_diagnostic_on_spread() { | ||
486 | check_no_diagnostics( | ||
487 | r" | ||
488 | struct TestStruct { one: i32, two: i64 } | ||
489 | |||
490 | fn test_fn() { | ||
491 | let one = 1; | ||
492 | let s = TestStruct{ ..a }; | ||
493 | } | ||
494 | ", | ||
495 | ); | ||
496 | } | ||
497 | |||
498 | #[test] | ||
499 | fn test_unresolved_module_diagnostic() { | ||
500 | check_expect( | ||
501 | r#"mod foo;"#, | ||
502 | expect![[r#" | ||
503 | [ | ||
504 | Diagnostic { | ||
505 | message: "unresolved module", | ||
506 | range: 0..8, | ||
507 | severity: Error, | ||
508 | fix: Some( | ||
509 | Fix { | ||
510 | label: "Create module", | ||
511 | source_change: SourceChange { | ||
512 | source_file_edits: [], | ||
513 | file_system_edits: [ | ||
514 | CreateFile { | ||
515 | anchor: FileId( | ||
516 | 1, | ||
517 | ), | ||
518 | dst: "foo.rs", | ||
519 | }, | ||
520 | ], | ||
521 | is_snippet: false, | ||
522 | }, | ||
523 | fix_trigger_range: 0..8, | ||
524 | }, | ||
525 | ), | ||
526 | }, | ||
527 | ] | ||
528 | "#]], | ||
529 | ); | ||
530 | } | ||
531 | |||
532 | #[test] | ||
533 | fn range_mapping_out_of_macros() { | ||
534 | // FIXME: this is very wrong, but somewhat tricky to fix. | ||
535 | check_fix( | ||
536 | r#" | ||
537 | fn some() {} | ||
538 | fn items() {} | ||
539 | fn here() {} | ||
540 | |||
541 | macro_rules! id { ($($tt:tt)*) => { $($tt)*}; } | ||
542 | |||
543 | fn main() { | ||
544 | let _x = id![Foo { a: <|>42 }]; | ||
545 | } | ||
546 | |||
547 | pub struct Foo { pub a: i32, pub b: i32 } | ||
548 | "#, | ||
549 | r#" | ||
550 | fn {a:42, b: ()} {} | ||
551 | fn items() {} | ||
552 | fn here() {} | ||
553 | |||
554 | macro_rules! id { ($($tt:tt)*) => { $($tt)*}; } | ||
555 | |||
556 | fn main() { | ||
557 | let _x = id![Foo { a: 42 }]; | ||
558 | } | ||
559 | |||
560 | pub struct Foo { pub a: i32, pub b: i32 } | ||
561 | "#, | ||
562 | ); | ||
563 | } | ||
564 | |||
565 | #[test] | ||
566 | fn test_check_unnecessary_braces_in_use_statement() { | ||
567 | check_no_diagnostics( | ||
568 | r#" | ||
569 | use a; | ||
570 | use a::{c, d::e}; | ||
571 | "#, | ||
572 | ); | ||
573 | check_fix(r#"use {<|>b};"#, r#"use b;"#); | ||
574 | check_fix(r#"use {b<|>};"#, r#"use b;"#); | ||
575 | check_fix(r#"use a::{c<|>};"#, r#"use a::c;"#); | ||
576 | check_fix(r#"use a::{self<|>};"#, r#"use a;"#); | ||
577 | check_fix(r#"use a::{c, d::{e<|>}};"#, r#"use a::{c, d::e};"#); | ||
578 | } | ||
579 | |||
580 | #[test] | ||
581 | fn test_check_struct_shorthand_initialization() { | ||
582 | check_no_diagnostics( | ||
583 | r#" | ||
584 | struct A { a: &'static str } | ||
585 | fn main() { A { a: "hello" } } | ||
586 | "#, | ||
587 | ); | ||
588 | check_no_diagnostics( | ||
589 | r#" | ||
590 | struct A(usize); | ||
591 | fn main() { A { 0: 0 } } | ||
592 | "#, | ||
593 | ); | ||
594 | |||
595 | check_fix( | ||
596 | r#" | ||
597 | struct A { a: &'static str } | ||
598 | fn main() { | ||
599 | let a = "haha"; | ||
600 | A { a<|>: a } | ||
601 | } | ||
602 | "#, | ||
603 | r#" | ||
604 | struct A { a: &'static str } | ||
605 | fn main() { | ||
606 | let a = "haha"; | ||
607 | A { a } | ||
608 | } | ||
609 | "#, | ||
610 | ); | ||
611 | |||
612 | check_fix( | ||
613 | r#" | ||
614 | struct A { a: &'static str, b: &'static str } | ||
615 | fn main() { | ||
616 | let a = "haha"; | ||
617 | let b = "bb"; | ||
618 | A { a<|>: a, b } | ||
619 | } | ||
620 | "#, | ||
621 | r#" | ||
622 | struct A { a: &'static str, b: &'static str } | ||
623 | fn main() { | ||
624 | let a = "haha"; | ||
625 | let b = "bb"; | ||
626 | A { a, b } | ||
627 | } | ||
628 | "#, | ||
629 | ); | ||
630 | } | ||
631 | |||
632 | #[test] | ||
633 | fn test_add_field_from_usage() { | ||
634 | check_fix( | ||
635 | r" | ||
636 | fn main() { | ||
637 | Foo { bar: 3, baz<|>: false}; | ||
638 | } | ||
639 | struct Foo { | ||
640 | bar: i32 | ||
641 | } | ||
642 | ", | ||
643 | r" | ||
644 | fn main() { | ||
645 | Foo { bar: 3, baz: false}; | ||
646 | } | ||
647 | struct Foo { | ||
648 | bar: i32, | ||
649 | baz: bool | ||
650 | } | ||
651 | ", | ||
652 | ) | ||
653 | } | ||
654 | |||
655 | #[test] | ||
656 | fn test_add_field_in_other_file_from_usage() { | ||
657 | check_apply_diagnostic_fix_in_other_file( | ||
658 | r" | ||
659 | //- /main.rs | ||
660 | mod foo; | ||
661 | |||
662 | fn main() { | ||
663 | <|>foo::Foo { bar: 3, baz: false}; | ||
664 | } | ||
665 | //- /foo.rs | ||
666 | struct Foo { | ||
667 | bar: i32 | ||
668 | } | ||
669 | ", | ||
670 | r" | ||
671 | struct Foo { | ||
672 | bar: i32, | ||
673 | pub(crate) baz: bool | ||
674 | } | ||
675 | ", | ||
676 | ) | ||
677 | } | ||
678 | } | ||
diff --git a/crates/ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ide/src/diagnostics/diagnostics_with_fix.rs new file mode 100644 index 000000000..85b46c995 --- /dev/null +++ b/crates/ide/src/diagnostics/diagnostics_with_fix.rs | |||
@@ -0,0 +1,171 @@ | |||
1 | //! Provides a way to attach fixes to the diagnostics. | ||
2 | //! The same module also has all curret custom fixes for the diagnostics implemented. | ||
3 | use crate::Fix; | ||
4 | use ast::{edit::IndentLevel, make}; | ||
5 | use base_db::FileId; | ||
6 | use hir::{ | ||
7 | db::AstDatabase, | ||
8 | diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | ||
9 | HasSource, HirDisplay, Semantics, VariantDef, | ||
10 | }; | ||
11 | use ide_db::{ | ||
12 | source_change::{FileSystemEdit, SourceFileEdit}, | ||
13 | RootDatabase, | ||
14 | }; | ||
15 | use syntax::{algo, ast, AstNode}; | ||
16 | use text_edit::TextEdit; | ||
17 | |||
18 | /// A [Diagnostic] that potentially has a fix available. | ||
19 | /// | ||
20 | /// [Diagnostic]: hir::diagnostics::Diagnostic | ||
21 | pub trait DiagnosticWithFix: Diagnostic { | ||
22 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>; | ||
23 | } | ||
24 | |||
25 | impl DiagnosticWithFix for UnresolvedModule { | ||
26 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
27 | let root = sema.db.parse_or_expand(self.file)?; | ||
28 | let unresolved_module = self.decl.to_node(&root); | ||
29 | Some(Fix::new( | ||
30 | "Create module", | ||
31 | FileSystemEdit::CreateFile { | ||
32 | anchor: self.file.original_file(sema.db), | ||
33 | dst: self.candidate.clone(), | ||
34 | } | ||
35 | .into(), | ||
36 | unresolved_module.syntax().text_range(), | ||
37 | )) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | impl DiagnosticWithFix for NoSuchField { | ||
42 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
43 | let root = sema.db.parse_or_expand(self.file)?; | ||
44 | missing_record_expr_field_fix( | ||
45 | &sema, | ||
46 | self.file.original_file(sema.db), | ||
47 | &self.field.to_node(&root), | ||
48 | ) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl DiagnosticWithFix for MissingFields { | ||
53 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
54 | // Note that although we could add a diagnostics to | ||
55 | // fill the missing tuple field, e.g : | ||
56 | // `struct A(usize);` | ||
57 | // `let a = A { 0: () }` | ||
58 | // but it is uncommon usage and it should not be encouraged. | ||
59 | if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) { | ||
60 | return None; | ||
61 | } | ||
62 | |||
63 | let root = sema.db.parse_or_expand(self.file)?; | ||
64 | let old_field_list = self.field_list_parent.to_node(&root).record_expr_field_list()?; | ||
65 | let mut new_field_list = old_field_list.clone(); | ||
66 | for f in self.missed_fields.iter() { | ||
67 | let field = | ||
68 | make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
69 | new_field_list = new_field_list.append_field(&field); | ||
70 | } | ||
71 | |||
72 | let edit = { | ||
73 | let mut builder = TextEdit::builder(); | ||
74 | algo::diff(&old_field_list.syntax(), &new_field_list.syntax()) | ||
75 | .into_text_edit(&mut builder); | ||
76 | builder.finish() | ||
77 | }; | ||
78 | Some(Fix::new( | ||
79 | "Fill struct fields", | ||
80 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), | ||
81 | sema.original_range(&old_field_list.syntax()).range, | ||
82 | )) | ||
83 | } | ||
84 | } | ||
85 | |||
86 | impl DiagnosticWithFix for MissingOkInTailExpr { | ||
87 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
88 | let root = sema.db.parse_or_expand(self.file)?; | ||
89 | let tail_expr = self.expr.to_node(&root); | ||
90 | let tail_expr_range = tail_expr.syntax().text_range(); | ||
91 | let edit = TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax())); | ||
92 | let source_change = | ||
93 | SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); | ||
94 | Some(Fix::new("Wrap with ok", source_change, tail_expr_range)) | ||
95 | } | ||
96 | } | ||
97 | |||
98 | fn missing_record_expr_field_fix( | ||
99 | sema: &Semantics<RootDatabase>, | ||
100 | usage_file_id: FileId, | ||
101 | record_expr_field: &ast::RecordExprField, | ||
102 | ) -> Option<Fix> { | ||
103 | let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; | ||
104 | let def_id = sema.resolve_variant(record_lit)?; | ||
105 | let module; | ||
106 | let def_file_id; | ||
107 | let record_fields = match VariantDef::from(def_id) { | ||
108 | VariantDef::Struct(s) => { | ||
109 | module = s.module(sema.db); | ||
110 | let source = s.source(sema.db); | ||
111 | def_file_id = source.file_id; | ||
112 | let fields = source.value.field_list()?; | ||
113 | record_field_list(fields)? | ||
114 | } | ||
115 | VariantDef::Union(u) => { | ||
116 | module = u.module(sema.db); | ||
117 | let source = u.source(sema.db); | ||
118 | def_file_id = source.file_id; | ||
119 | source.value.record_field_list()? | ||
120 | } | ||
121 | VariantDef::EnumVariant(e) => { | ||
122 | module = e.module(sema.db); | ||
123 | let source = e.source(sema.db); | ||
124 | def_file_id = source.file_id; | ||
125 | let fields = source.value.field_list()?; | ||
126 | record_field_list(fields)? | ||
127 | } | ||
128 | }; | ||
129 | let def_file_id = def_file_id.original_file(sema.db); | ||
130 | |||
131 | let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; | ||
132 | if new_field_type.is_unknown() { | ||
133 | return None; | ||
134 | } | ||
135 | let new_field = make::record_field( | ||
136 | record_expr_field.field_name()?, | ||
137 | make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), | ||
138 | ); | ||
139 | |||
140 | let last_field = record_fields.fields().last()?; | ||
141 | let last_field_syntax = last_field.syntax(); | ||
142 | let indent = IndentLevel::from_node(last_field_syntax); | ||
143 | |||
144 | let mut new_field = new_field.to_string(); | ||
145 | if usage_file_id != def_file_id { | ||
146 | new_field = format!("pub(crate) {}", new_field); | ||
147 | } | ||
148 | new_field = format!("\n{}{}", indent, new_field); | ||
149 | |||
150 | let needs_comma = !last_field_syntax.to_string().ends_with(','); | ||
151 | if needs_comma { | ||
152 | new_field = format!(",{}", new_field); | ||
153 | } | ||
154 | |||
155 | let source_change = SourceFileEdit { | ||
156 | file_id: def_file_id, | ||
157 | edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), | ||
158 | }; | ||
159 | return Some(Fix::new( | ||
160 | "Create field", | ||
161 | source_change.into(), | ||
162 | record_expr_field.syntax().text_range(), | ||
163 | )); | ||
164 | |||
165 | fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { | ||
166 | match field_def_list { | ||
167 | ast::FieldList::RecordFieldList(it) => Some(it), | ||
168 | ast::FieldList::TupleFieldList(_) => None, | ||
169 | } | ||
170 | } | ||
171 | } | ||
diff --git a/crates/ide/src/display.rs b/crates/ide/src/display.rs new file mode 100644 index 000000000..41b5bdc49 --- /dev/null +++ b/crates/ide/src/display.rs | |||
@@ -0,0 +1,83 @@ | |||
1 | //! This module contains utilities for turning SyntaxNodes and HIR types | ||
2 | //! into types that may be used to render in a UI. | ||
3 | |||
4 | mod navigation_target; | ||
5 | mod short_label; | ||
6 | |||
7 | use syntax::{ | ||
8 | ast::{self, AstNode, AttrsOwner, GenericParamsOwner, NameOwner}, | ||
9 | SyntaxKind::{ATTR, COMMENT}, | ||
10 | }; | ||
11 | |||
12 | use ast::VisibilityOwner; | ||
13 | use stdx::format_to; | ||
14 | |||
15 | pub use navigation_target::NavigationTarget; | ||
16 | pub(crate) use navigation_target::{ToNav, TryToNav}; | ||
17 | pub(crate) use short_label::ShortLabel; | ||
18 | |||
19 | pub(crate) fn function_declaration(node: &ast::Fn) -> String { | ||
20 | let mut buf = String::new(); | ||
21 | if let Some(vis) = node.visibility() { | ||
22 | format_to!(buf, "{} ", vis); | ||
23 | } | ||
24 | if node.async_token().is_some() { | ||
25 | format_to!(buf, "async "); | ||
26 | } | ||
27 | if node.const_token().is_some() { | ||
28 | format_to!(buf, "const "); | ||
29 | } | ||
30 | if node.unsafe_token().is_some() { | ||
31 | format_to!(buf, "unsafe "); | ||
32 | } | ||
33 | if let Some(abi) = node.abi() { | ||
34 | // Keyword `extern` is included in the string. | ||
35 | format_to!(buf, "{} ", abi); | ||
36 | } | ||
37 | if let Some(name) = node.name() { | ||
38 | format_to!(buf, "fn {}", name) | ||
39 | } | ||
40 | if let Some(type_params) = node.generic_param_list() { | ||
41 | format_to!(buf, "{}", type_params); | ||
42 | } | ||
43 | if let Some(param_list) = node.param_list() { | ||
44 | format_to!(buf, "{}", param_list); | ||
45 | } | ||
46 | if let Some(ret_type) = node.ret_type() { | ||
47 | if ret_type.ty().is_some() { | ||
48 | format_to!(buf, " {}", ret_type); | ||
49 | } | ||
50 | } | ||
51 | if let Some(where_clause) = node.where_clause() { | ||
52 | format_to!(buf, "\n{}", where_clause); | ||
53 | } | ||
54 | buf | ||
55 | } | ||
56 | |||
57 | pub(crate) fn const_label(node: &ast::Const) -> String { | ||
58 | let label: String = node | ||
59 | .syntax() | ||
60 | .children_with_tokens() | ||
61 | .filter(|child| !(child.kind() == COMMENT || child.kind() == ATTR)) | ||
62 | .map(|node| node.to_string()) | ||
63 | .collect(); | ||
64 | |||
65 | label.trim().to_owned() | ||
66 | } | ||
67 | |||
68 | pub(crate) fn type_label(node: &ast::TypeAlias) -> String { | ||
69 | let label: String = node | ||
70 | .syntax() | ||
71 | .children_with_tokens() | ||
72 | .filter(|child| !(child.kind() == COMMENT || child.kind() == ATTR)) | ||
73 | .map(|node| node.to_string()) | ||
74 | .collect(); | ||
75 | |||
76 | label.trim().to_owned() | ||
77 | } | ||
78 | |||
79 | pub(crate) fn macro_label(node: &ast::MacroCall) -> String { | ||
80 | let name = node.name().map(|name| name.syntax().text().to_string()).unwrap_or_default(); | ||
81 | let vis = if node.has_atom_attr("macro_export") { "#[macro_export]\n" } else { "" }; | ||
82 | format!("{}macro_rules! {}", vis, name) | ||
83 | } | ||
diff --git a/crates/ide/src/display/navigation_target.rs b/crates/ide/src/display/navigation_target.rs new file mode 100644 index 000000000..e77106177 --- /dev/null +++ b/crates/ide/src/display/navigation_target.rs | |||
@@ -0,0 +1,491 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use base_db::{FileId, SourceDatabase}; | ||
4 | use either::Either; | ||
5 | use hir::{original_range, AssocItem, FieldSource, HasSource, InFile, ModuleSource}; | ||
6 | use ide_db::{defs::Definition, RootDatabase}; | ||
7 | use syntax::{ | ||
8 | ast::{self, DocCommentsOwner, NameOwner}, | ||
9 | match_ast, AstNode, SmolStr, | ||
10 | SyntaxKind::{self, IDENT_PAT, TYPE_PARAM}, | ||
11 | TextRange, | ||
12 | }; | ||
13 | |||
14 | use crate::FileSymbol; | ||
15 | |||
16 | use super::short_label::ShortLabel; | ||
17 | |||
18 | /// `NavigationTarget` represents and element in the editor's UI which you can | ||
19 | /// click on to navigate to a particular piece of code. | ||
20 | /// | ||
21 | /// Typically, a `NavigationTarget` corresponds to some element in the source | ||
22 | /// code, like a function or a struct, but this is not strictly required. | ||
23 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
24 | pub struct NavigationTarget { | ||
25 | pub file_id: FileId, | ||
26 | /// Range which encompasses the whole element. | ||
27 | /// | ||
28 | /// Should include body, doc comments, attributes, etc. | ||
29 | /// | ||
30 | /// Clients should use this range to answer "is the cursor inside the | ||
31 | /// element?" question. | ||
32 | pub full_range: TextRange, | ||
33 | /// A "most interesting" range withing the `full_range`. | ||
34 | /// | ||
35 | /// Typically, `full_range` is the whole syntax node, including doc | ||
36 | /// comments, and `focus_range` is the range of the identifier. "Most | ||
37 | /// interesting" range within the full range, typically the range of | ||
38 | /// identifier. | ||
39 | /// | ||
40 | /// Clients should place the cursor on this range when navigating to this target. | ||
41 | pub focus_range: Option<TextRange>, | ||
42 | pub name: SmolStr, | ||
43 | pub kind: SyntaxKind, | ||
44 | pub container_name: Option<SmolStr>, | ||
45 | pub description: Option<String>, | ||
46 | pub docs: Option<String>, | ||
47 | } | ||
48 | |||
49 | pub(crate) trait ToNav { | ||
50 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget; | ||
51 | } | ||
52 | |||
53 | pub(crate) trait TryToNav { | ||
54 | fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget>; | ||
55 | } | ||
56 | |||
57 | impl NavigationTarget { | ||
58 | pub fn focus_or_full_range(&self) -> TextRange { | ||
59 | self.focus_range.unwrap_or(self.full_range) | ||
60 | } | ||
61 | |||
62 | pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget { | ||
63 | let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default(); | ||
64 | if let Some(src) = module.declaration_source(db) { | ||
65 | let frange = original_range(db, src.as_ref().map(|it| it.syntax())); | ||
66 | let mut res = NavigationTarget::from_syntax( | ||
67 | frange.file_id, | ||
68 | name, | ||
69 | None, | ||
70 | frange.range, | ||
71 | src.value.syntax().kind(), | ||
72 | ); | ||
73 | res.docs = src.value.doc_comment_text(); | ||
74 | res.description = src.value.short_label(); | ||
75 | return res; | ||
76 | } | ||
77 | module.to_nav(db) | ||
78 | } | ||
79 | |||
80 | #[cfg(test)] | ||
81 | pub(crate) fn assert_match(&self, expected: &str) { | ||
82 | let actual = self.debug_render(); | ||
83 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
84 | } | ||
85 | |||
86 | #[cfg(test)] | ||
87 | pub(crate) fn debug_render(&self) -> String { | ||
88 | let mut buf = | ||
89 | format!("{} {:?} {:?} {:?}", self.name, self.kind, self.file_id, self.full_range); | ||
90 | if let Some(focus_range) = self.focus_range { | ||
91 | buf.push_str(&format!(" {:?}", focus_range)) | ||
92 | } | ||
93 | if let Some(container_name) = &self.container_name { | ||
94 | buf.push_str(&format!(" {}", container_name)) | ||
95 | } | ||
96 | buf | ||
97 | } | ||
98 | |||
99 | /// Allows `NavigationTarget` to be created from a `NameOwner` | ||
100 | pub(crate) fn from_named( | ||
101 | db: &RootDatabase, | ||
102 | node: InFile<&dyn ast::NameOwner>, | ||
103 | ) -> NavigationTarget { | ||
104 | let name = | ||
105 | node.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_")); | ||
106 | let focus_range = | ||
107 | node.value.name().map(|it| original_range(db, node.with_value(it.syntax())).range); | ||
108 | let frange = original_range(db, node.map(|it| it.syntax())); | ||
109 | |||
110 | NavigationTarget::from_syntax( | ||
111 | frange.file_id, | ||
112 | name, | ||
113 | focus_range, | ||
114 | frange.range, | ||
115 | node.value.syntax().kind(), | ||
116 | ) | ||
117 | } | ||
118 | |||
119 | /// Allows `NavigationTarget` to be created from a `DocCommentsOwner` and a `NameOwner` | ||
120 | pub(crate) fn from_doc_commented( | ||
121 | db: &RootDatabase, | ||
122 | named: InFile<&dyn ast::NameOwner>, | ||
123 | node: InFile<&dyn ast::DocCommentsOwner>, | ||
124 | ) -> NavigationTarget { | ||
125 | let name = | ||
126 | named.value.name().map(|it| it.text().clone()).unwrap_or_else(|| SmolStr::new("_")); | ||
127 | let frange = original_range(db, node.map(|it| it.syntax())); | ||
128 | |||
129 | NavigationTarget::from_syntax( | ||
130 | frange.file_id, | ||
131 | name, | ||
132 | None, | ||
133 | frange.range, | ||
134 | node.value.syntax().kind(), | ||
135 | ) | ||
136 | } | ||
137 | |||
138 | fn from_syntax( | ||
139 | file_id: FileId, | ||
140 | name: SmolStr, | ||
141 | focus_range: Option<TextRange>, | ||
142 | full_range: TextRange, | ||
143 | kind: SyntaxKind, | ||
144 | ) -> NavigationTarget { | ||
145 | NavigationTarget { | ||
146 | file_id, | ||
147 | name, | ||
148 | kind, | ||
149 | full_range, | ||
150 | focus_range, | ||
151 | container_name: None, | ||
152 | description: None, | ||
153 | docs: None, | ||
154 | } | ||
155 | } | ||
156 | } | ||
157 | |||
158 | impl ToNav for FileSymbol { | ||
159 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
160 | NavigationTarget { | ||
161 | file_id: self.file_id, | ||
162 | name: self.name.clone(), | ||
163 | kind: self.kind, | ||
164 | full_range: self.range, | ||
165 | focus_range: self.name_range, | ||
166 | container_name: self.container_name.clone(), | ||
167 | description: description_from_symbol(db, self), | ||
168 | docs: docs_from_symbol(db, self), | ||
169 | } | ||
170 | } | ||
171 | } | ||
172 | |||
173 | impl TryToNav for Definition { | ||
174 | fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { | ||
175 | match self { | ||
176 | Definition::Macro(it) => Some(it.to_nav(db)), | ||
177 | Definition::Field(it) => Some(it.to_nav(db)), | ||
178 | Definition::ModuleDef(it) => it.try_to_nav(db), | ||
179 | Definition::SelfType(it) => Some(it.to_nav(db)), | ||
180 | Definition::Local(it) => Some(it.to_nav(db)), | ||
181 | Definition::TypeParam(it) => Some(it.to_nav(db)), | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | |||
186 | impl TryToNav for hir::ModuleDef { | ||
187 | fn try_to_nav(&self, db: &RootDatabase) -> Option<NavigationTarget> { | ||
188 | let res = match self { | ||
189 | hir::ModuleDef::Module(it) => it.to_nav(db), | ||
190 | hir::ModuleDef::Function(it) => it.to_nav(db), | ||
191 | hir::ModuleDef::Adt(it) => it.to_nav(db), | ||
192 | hir::ModuleDef::EnumVariant(it) => it.to_nav(db), | ||
193 | hir::ModuleDef::Const(it) => it.to_nav(db), | ||
194 | hir::ModuleDef::Static(it) => it.to_nav(db), | ||
195 | hir::ModuleDef::Trait(it) => it.to_nav(db), | ||
196 | hir::ModuleDef::TypeAlias(it) => it.to_nav(db), | ||
197 | hir::ModuleDef::BuiltinType(_) => return None, | ||
198 | }; | ||
199 | Some(res) | ||
200 | } | ||
201 | } | ||
202 | |||
203 | pub(crate) trait ToNavFromAst {} | ||
204 | impl ToNavFromAst for hir::Function {} | ||
205 | impl ToNavFromAst for hir::Const {} | ||
206 | impl ToNavFromAst for hir::Static {} | ||
207 | impl ToNavFromAst for hir::Struct {} | ||
208 | impl ToNavFromAst for hir::Enum {} | ||
209 | impl ToNavFromAst for hir::EnumVariant {} | ||
210 | impl ToNavFromAst for hir::Union {} | ||
211 | impl ToNavFromAst for hir::TypeAlias {} | ||
212 | impl ToNavFromAst for hir::Trait {} | ||
213 | |||
214 | impl<D> ToNav for D | ||
215 | where | ||
216 | D: HasSource + ToNavFromAst + Copy, | ||
217 | D::Ast: ast::DocCommentsOwner + ast::NameOwner + ShortLabel, | ||
218 | { | ||
219 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
220 | let src = self.source(db); | ||
221 | let mut res = | ||
222 | NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner)); | ||
223 | res.docs = src.value.doc_comment_text(); | ||
224 | res.description = src.value.short_label(); | ||
225 | res | ||
226 | } | ||
227 | } | ||
228 | |||
229 | impl ToNav for hir::Module { | ||
230 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
231 | let src = self.definition_source(db); | ||
232 | let name = self.name(db).map(|it| it.to_string().into()).unwrap_or_default(); | ||
233 | let (syntax, focus) = match &src.value { | ||
234 | ModuleSource::SourceFile(node) => (node.syntax(), None), | ||
235 | ModuleSource::Module(node) => { | ||
236 | (node.syntax(), node.name().map(|it| it.syntax().text_range())) | ||
237 | } | ||
238 | }; | ||
239 | let frange = original_range(db, src.with_value(syntax)); | ||
240 | NavigationTarget::from_syntax(frange.file_id, name, focus, frange.range, syntax.kind()) | ||
241 | } | ||
242 | } | ||
243 | |||
244 | impl ToNav for hir::ImplDef { | ||
245 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
246 | let src = self.source(db); | ||
247 | let derive_attr = self.is_builtin_derive(db); | ||
248 | let frange = if let Some(item) = &derive_attr { | ||
249 | original_range(db, item.syntax()) | ||
250 | } else { | ||
251 | original_range(db, src.as_ref().map(|it| it.syntax())) | ||
252 | }; | ||
253 | let focus_range = if derive_attr.is_some() { | ||
254 | None | ||
255 | } else { | ||
256 | src.value.self_ty().map(|ty| original_range(db, src.with_value(ty.syntax())).range) | ||
257 | }; | ||
258 | |||
259 | NavigationTarget::from_syntax( | ||
260 | frange.file_id, | ||
261 | "impl".into(), | ||
262 | focus_range, | ||
263 | frange.range, | ||
264 | src.value.syntax().kind(), | ||
265 | ) | ||
266 | } | ||
267 | } | ||
268 | |||
269 | impl ToNav for hir::Field { | ||
270 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
271 | let src = self.source(db); | ||
272 | |||
273 | match &src.value { | ||
274 | FieldSource::Named(it) => { | ||
275 | let mut res = NavigationTarget::from_named(db, src.with_value(it)); | ||
276 | res.docs = it.doc_comment_text(); | ||
277 | res.description = it.short_label(); | ||
278 | res | ||
279 | } | ||
280 | FieldSource::Pos(it) => { | ||
281 | let frange = original_range(db, src.with_value(it.syntax())); | ||
282 | NavigationTarget::from_syntax( | ||
283 | frange.file_id, | ||
284 | "".into(), | ||
285 | None, | ||
286 | frange.range, | ||
287 | it.syntax().kind(), | ||
288 | ) | ||
289 | } | ||
290 | } | ||
291 | } | ||
292 | } | ||
293 | |||
294 | impl ToNav for hir::MacroDef { | ||
295 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
296 | let src = self.source(db); | ||
297 | log::debug!("nav target {:#?}", src.value.syntax()); | ||
298 | let mut res = | ||
299 | NavigationTarget::from_named(db, src.as_ref().map(|it| it as &dyn ast::NameOwner)); | ||
300 | res.docs = src.value.doc_comment_text(); | ||
301 | res | ||
302 | } | ||
303 | } | ||
304 | |||
305 | impl ToNav for hir::Adt { | ||
306 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
307 | match self { | ||
308 | hir::Adt::Struct(it) => it.to_nav(db), | ||
309 | hir::Adt::Union(it) => it.to_nav(db), | ||
310 | hir::Adt::Enum(it) => it.to_nav(db), | ||
311 | } | ||
312 | } | ||
313 | } | ||
314 | |||
315 | impl ToNav for hir::AssocItem { | ||
316 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
317 | match self { | ||
318 | AssocItem::Function(it) => it.to_nav(db), | ||
319 | AssocItem::Const(it) => it.to_nav(db), | ||
320 | AssocItem::TypeAlias(it) => it.to_nav(db), | ||
321 | } | ||
322 | } | ||
323 | } | ||
324 | |||
325 | impl ToNav for hir::Local { | ||
326 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
327 | let src = self.source(db); | ||
328 | let node = match &src.value { | ||
329 | Either::Left(bind_pat) => { | ||
330 | bind_pat.name().map_or_else(|| bind_pat.syntax().clone(), |it| it.syntax().clone()) | ||
331 | } | ||
332 | Either::Right(it) => it.syntax().clone(), | ||
333 | }; | ||
334 | let full_range = original_range(db, src.with_value(&node)); | ||
335 | let name = match self.name(db) { | ||
336 | Some(it) => it.to_string().into(), | ||
337 | None => "".into(), | ||
338 | }; | ||
339 | NavigationTarget { | ||
340 | file_id: full_range.file_id, | ||
341 | name, | ||
342 | kind: IDENT_PAT, | ||
343 | full_range: full_range.range, | ||
344 | focus_range: None, | ||
345 | container_name: None, | ||
346 | description: None, | ||
347 | docs: None, | ||
348 | } | ||
349 | } | ||
350 | } | ||
351 | |||
352 | impl ToNav for hir::TypeParam { | ||
353 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
354 | let src = self.source(db); | ||
355 | let full_range = match &src.value { | ||
356 | Either::Left(it) => it.syntax().text_range(), | ||
357 | Either::Right(it) => it.syntax().text_range(), | ||
358 | }; | ||
359 | let focus_range = match &src.value { | ||
360 | Either::Left(_) => None, | ||
361 | Either::Right(it) => it.name().map(|it| it.syntax().text_range()), | ||
362 | }; | ||
363 | NavigationTarget { | ||
364 | file_id: src.file_id.original_file(db), | ||
365 | name: self.name(db).to_string().into(), | ||
366 | kind: TYPE_PARAM, | ||
367 | full_range, | ||
368 | focus_range, | ||
369 | container_name: None, | ||
370 | description: None, | ||
371 | docs: None, | ||
372 | } | ||
373 | } | ||
374 | } | ||
375 | |||
376 | pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> { | ||
377 | let parse = db.parse(symbol.file_id); | ||
378 | let node = symbol.ptr.to_node(parse.tree().syntax()); | ||
379 | |||
380 | match_ast! { | ||
381 | match node { | ||
382 | ast::Fn(it) => it.doc_comment_text(), | ||
383 | ast::Struct(it) => it.doc_comment_text(), | ||
384 | ast::Enum(it) => it.doc_comment_text(), | ||
385 | ast::Trait(it) => it.doc_comment_text(), | ||
386 | ast::Module(it) => it.doc_comment_text(), | ||
387 | ast::TypeAlias(it) => it.doc_comment_text(), | ||
388 | ast::Const(it) => it.doc_comment_text(), | ||
389 | ast::Static(it) => it.doc_comment_text(), | ||
390 | ast::RecordField(it) => it.doc_comment_text(), | ||
391 | ast::Variant(it) => it.doc_comment_text(), | ||
392 | ast::MacroCall(it) => it.doc_comment_text(), | ||
393 | _ => None, | ||
394 | } | ||
395 | } | ||
396 | } | ||
397 | |||
398 | /// Get a description of a symbol. | ||
399 | /// | ||
400 | /// e.g. `struct Name`, `enum Name`, `fn Name` | ||
401 | pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> { | ||
402 | let parse = db.parse(symbol.file_id); | ||
403 | let node = symbol.ptr.to_node(parse.tree().syntax()); | ||
404 | |||
405 | match_ast! { | ||
406 | match node { | ||
407 | ast::Fn(it) => it.short_label(), | ||
408 | ast::Struct(it) => it.short_label(), | ||
409 | ast::Enum(it) => it.short_label(), | ||
410 | ast::Trait(it) => it.short_label(), | ||
411 | ast::Module(it) => it.short_label(), | ||
412 | ast::TypeAlias(it) => it.short_label(), | ||
413 | ast::Const(it) => it.short_label(), | ||
414 | ast::Static(it) => it.short_label(), | ||
415 | ast::RecordField(it) => it.short_label(), | ||
416 | ast::Variant(it) => it.short_label(), | ||
417 | _ => None, | ||
418 | } | ||
419 | } | ||
420 | } | ||
421 | |||
422 | #[cfg(test)] | ||
423 | mod tests { | ||
424 | use expect::expect; | ||
425 | |||
426 | use crate::{mock_analysis::single_file, Query}; | ||
427 | |||
428 | #[test] | ||
429 | fn test_nav_for_symbol() { | ||
430 | let (analysis, _) = single_file( | ||
431 | r#" | ||
432 | enum FooInner { } | ||
433 | fn foo() { enum FooInner { } } | ||
434 | "#, | ||
435 | ); | ||
436 | |||
437 | let navs = analysis.symbol_search(Query::new("FooInner".to_string())).unwrap(); | ||
438 | expect![[r#" | ||
439 | [ | ||
440 | NavigationTarget { | ||
441 | file_id: FileId( | ||
442 | 1, | ||
443 | ), | ||
444 | full_range: 0..17, | ||
445 | focus_range: Some( | ||
446 | 5..13, | ||
447 | ), | ||
448 | name: "FooInner", | ||
449 | kind: ENUM, | ||
450 | container_name: None, | ||
451 | description: Some( | ||
452 | "enum FooInner", | ||
453 | ), | ||
454 | docs: None, | ||
455 | }, | ||
456 | NavigationTarget { | ||
457 | file_id: FileId( | ||
458 | 1, | ||
459 | ), | ||
460 | full_range: 29..46, | ||
461 | focus_range: Some( | ||
462 | 34..42, | ||
463 | ), | ||
464 | name: "FooInner", | ||
465 | kind: ENUM, | ||
466 | container_name: Some( | ||
467 | "foo", | ||
468 | ), | ||
469 | description: Some( | ||
470 | "enum FooInner", | ||
471 | ), | ||
472 | docs: None, | ||
473 | }, | ||
474 | ] | ||
475 | "#]] | ||
476 | .assert_debug_eq(&navs); | ||
477 | } | ||
478 | |||
479 | #[test] | ||
480 | fn test_world_symbols_are_case_sensitive() { | ||
481 | let (analysis, _) = single_file( | ||
482 | r#" | ||
483 | fn foo() {} | ||
484 | struct Foo; | ||
485 | "#, | ||
486 | ); | ||
487 | |||
488 | let navs = analysis.symbol_search(Query::new("foo".to_string())).unwrap(); | ||
489 | assert_eq!(navs.len(), 2) | ||
490 | } | ||
491 | } | ||
diff --git a/crates/ide/src/display/short_label.rs b/crates/ide/src/display/short_label.rs new file mode 100644 index 000000000..ea49d9f97 --- /dev/null +++ b/crates/ide/src/display/short_label.rs | |||
@@ -0,0 +1,111 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use stdx::format_to; | ||
4 | use syntax::ast::{self, AstNode, NameOwner, VisibilityOwner}; | ||
5 | |||
6 | pub(crate) trait ShortLabel { | ||
7 | fn short_label(&self) -> Option<String>; | ||
8 | } | ||
9 | |||
10 | impl ShortLabel for ast::Fn { | ||
11 | fn short_label(&self) -> Option<String> { | ||
12 | Some(crate::display::function_declaration(self)) | ||
13 | } | ||
14 | } | ||
15 | |||
16 | impl ShortLabel for ast::Struct { | ||
17 | fn short_label(&self) -> Option<String> { | ||
18 | short_label_from_node(self, "struct ") | ||
19 | } | ||
20 | } | ||
21 | |||
22 | impl ShortLabel for ast::Union { | ||
23 | fn short_label(&self) -> Option<String> { | ||
24 | short_label_from_node(self, "union ") | ||
25 | } | ||
26 | } | ||
27 | |||
28 | impl ShortLabel for ast::Enum { | ||
29 | fn short_label(&self) -> Option<String> { | ||
30 | short_label_from_node(self, "enum ") | ||
31 | } | ||
32 | } | ||
33 | |||
34 | impl ShortLabel for ast::Trait { | ||
35 | fn short_label(&self) -> Option<String> { | ||
36 | if self.unsafe_token().is_some() { | ||
37 | short_label_from_node(self, "unsafe trait ") | ||
38 | } else { | ||
39 | short_label_from_node(self, "trait ") | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | |||
44 | impl ShortLabel for ast::Module { | ||
45 | fn short_label(&self) -> Option<String> { | ||
46 | short_label_from_node(self, "mod ") | ||
47 | } | ||
48 | } | ||
49 | |||
50 | impl ShortLabel for ast::SourceFile { | ||
51 | fn short_label(&self) -> Option<String> { | ||
52 | None | ||
53 | } | ||
54 | } | ||
55 | |||
56 | impl ShortLabel for ast::TypeAlias { | ||
57 | fn short_label(&self) -> Option<String> { | ||
58 | short_label_from_node(self, "type ") | ||
59 | } | ||
60 | } | ||
61 | |||
62 | impl ShortLabel for ast::Const { | ||
63 | fn short_label(&self) -> Option<String> { | ||
64 | let mut new_buf = short_label_from_ty(self, self.ty(), "const ")?; | ||
65 | if let Some(expr) = self.body() { | ||
66 | format_to!(new_buf, " = {}", expr.syntax()); | ||
67 | } | ||
68 | Some(new_buf) | ||
69 | } | ||
70 | } | ||
71 | |||
72 | impl ShortLabel for ast::Static { | ||
73 | fn short_label(&self) -> Option<String> { | ||
74 | short_label_from_ty(self, self.ty(), "static ") | ||
75 | } | ||
76 | } | ||
77 | |||
78 | impl ShortLabel for ast::RecordField { | ||
79 | fn short_label(&self) -> Option<String> { | ||
80 | short_label_from_ty(self, self.ty(), "") | ||
81 | } | ||
82 | } | ||
83 | |||
84 | impl ShortLabel for ast::Variant { | ||
85 | fn short_label(&self) -> Option<String> { | ||
86 | Some(self.name()?.text().to_string()) | ||
87 | } | ||
88 | } | ||
89 | |||
90 | fn short_label_from_ty<T>(node: &T, ty: Option<ast::Type>, prefix: &str) -> Option<String> | ||
91 | where | ||
92 | T: NameOwner + VisibilityOwner, | ||
93 | { | ||
94 | let mut buf = short_label_from_node(node, prefix)?; | ||
95 | |||
96 | if let Some(type_ref) = ty { | ||
97 | format_to!(buf, ": {}", type_ref.syntax()); | ||
98 | } | ||
99 | |||
100 | Some(buf) | ||
101 | } | ||
102 | |||
103 | fn short_label_from_node<T>(node: &T, label: &str) -> Option<String> | ||
104 | where | ||
105 | T: NameOwner + VisibilityOwner, | ||
106 | { | ||
107 | let mut buf = node.visibility().map(|v| format!("{} ", v.syntax())).unwrap_or_default(); | ||
108 | buf.push_str(label); | ||
109 | buf.push_str(node.name()?.text().as_str()); | ||
110 | Some(buf) | ||
111 | } | ||
diff --git a/crates/ide/src/expand_macro.rs b/crates/ide/src/expand_macro.rs new file mode 100644 index 000000000..31455709d --- /dev/null +++ b/crates/ide/src/expand_macro.rs | |||
@@ -0,0 +1,283 @@ | |||
1 | use hir::Semantics; | ||
2 | use ide_db::RootDatabase; | ||
3 | use syntax::{ | ||
4 | algo::{find_node_at_offset, SyntaxRewriter}, | ||
5 | ast, AstNode, NodeOrToken, SyntaxKind, | ||
6 | SyntaxKind::*, | ||
7 | SyntaxNode, WalkEvent, T, | ||
8 | }; | ||
9 | |||
10 | use crate::FilePosition; | ||
11 | |||
12 | pub struct ExpandedMacro { | ||
13 | pub name: String, | ||
14 | pub expansion: String, | ||
15 | } | ||
16 | |||
17 | // Feature: Expand Macro Recursively | ||
18 | // | ||
19 | // Shows the full macro expansion of the macro at current cursor. | ||
20 | // | ||
21 | // |=== | ||
22 | // | Editor | Action Name | ||
23 | // | ||
24 | // | VS Code | **Rust Analyzer: Expand macro recursively** | ||
25 | // |=== | ||
26 | pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option<ExpandedMacro> { | ||
27 | let sema = Semantics::new(db); | ||
28 | let file = sema.parse(position.file_id); | ||
29 | let name_ref = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset)?; | ||
30 | let mac = name_ref.syntax().ancestors().find_map(ast::MacroCall::cast)?; | ||
31 | |||
32 | let expanded = expand_macro_recur(&sema, &mac)?; | ||
33 | |||
34 | // FIXME: | ||
35 | // macro expansion may lose all white space information | ||
36 | // But we hope someday we can use ra_fmt for that | ||
37 | let expansion = insert_whitespaces(expanded); | ||
38 | Some(ExpandedMacro { name: name_ref.text().to_string(), expansion }) | ||
39 | } | ||
40 | |||
41 | fn expand_macro_recur( | ||
42 | sema: &Semantics<RootDatabase>, | ||
43 | macro_call: &ast::MacroCall, | ||
44 | ) -> Option<SyntaxNode> { | ||
45 | let mut expanded = sema.expand(macro_call)?; | ||
46 | |||
47 | let children = expanded.descendants().filter_map(ast::MacroCall::cast); | ||
48 | let mut rewriter = SyntaxRewriter::default(); | ||
49 | |||
50 | for child in children.into_iter() { | ||
51 | if let Some(new_node) = expand_macro_recur(sema, &child) { | ||
52 | // Replace the whole node if it is root | ||
53 | // `replace_descendants` will not replace the parent node | ||
54 | // but `SyntaxNode::descendants include itself | ||
55 | if expanded == *child.syntax() { | ||
56 | expanded = new_node; | ||
57 | } else { | ||
58 | rewriter.replace(child.syntax(), &new_node) | ||
59 | } | ||
60 | } | ||
61 | } | ||
62 | |||
63 | let res = rewriter.rewrite(&expanded); | ||
64 | Some(res) | ||
65 | } | ||
66 | |||
67 | // FIXME: It would also be cool to share logic here and in the mbe tests, | ||
68 | // which are pretty unreadable at the moment. | ||
69 | fn insert_whitespaces(syn: SyntaxNode) -> String { | ||
70 | let mut res = String::new(); | ||
71 | let mut token_iter = syn | ||
72 | .preorder_with_tokens() | ||
73 | .filter_map(|event| { | ||
74 | if let WalkEvent::Enter(NodeOrToken::Token(token)) = event { | ||
75 | Some(token) | ||
76 | } else { | ||
77 | None | ||
78 | } | ||
79 | }) | ||
80 | .peekable(); | ||
81 | |||
82 | let mut indent = 0; | ||
83 | let mut last: Option<SyntaxKind> = None; | ||
84 | |||
85 | while let Some(token) = token_iter.next() { | ||
86 | let mut is_next = |f: fn(SyntaxKind) -> bool, default| -> bool { | ||
87 | token_iter.peek().map(|it| f(it.kind())).unwrap_or(default) | ||
88 | }; | ||
89 | let is_last = | ||
90 | |f: fn(SyntaxKind) -> bool, default| -> bool { last.map(f).unwrap_or(default) }; | ||
91 | |||
92 | res += &match token.kind() { | ||
93 | k if is_text(k) && is_next(|it| !it.is_punct(), true) => token.text().to_string() + " ", | ||
94 | L_CURLY if is_next(|it| it != R_CURLY, true) => { | ||
95 | indent += 1; | ||
96 | let leading_space = if is_last(is_text, false) { " " } else { "" }; | ||
97 | format!("{}{{\n{}", leading_space, " ".repeat(indent)) | ||
98 | } | ||
99 | R_CURLY if is_last(|it| it != L_CURLY, true) => { | ||
100 | indent = indent.saturating_sub(1); | ||
101 | format!("\n{}}}", " ".repeat(indent)) | ||
102 | } | ||
103 | R_CURLY => format!("}}\n{}", " ".repeat(indent)), | ||
104 | T![;] => format!(";\n{}", " ".repeat(indent)), | ||
105 | T![->] => " -> ".to_string(), | ||
106 | T![=] => " = ".to_string(), | ||
107 | T![=>] => " => ".to_string(), | ||
108 | _ => token.text().to_string(), | ||
109 | }; | ||
110 | |||
111 | last = Some(token.kind()); | ||
112 | } | ||
113 | |||
114 | return res; | ||
115 | |||
116 | fn is_text(k: SyntaxKind) -> bool { | ||
117 | k.is_keyword() || k.is_literal() || k == IDENT | ||
118 | } | ||
119 | } | ||
120 | |||
121 | #[cfg(test)] | ||
122 | mod tests { | ||
123 | use expect::{expect, Expect}; | ||
124 | |||
125 | use crate::mock_analysis::analysis_and_position; | ||
126 | |||
127 | fn check(ra_fixture: &str, expect: Expect) { | ||
128 | let (analysis, pos) = analysis_and_position(ra_fixture); | ||
129 | let expansion = analysis.expand_macro(pos).unwrap().unwrap(); | ||
130 | let actual = format!("{}\n{}", expansion.name, expansion.expansion); | ||
131 | expect.assert_eq(&actual); | ||
132 | } | ||
133 | |||
134 | #[test] | ||
135 | fn macro_expand_recursive_expansion() { | ||
136 | check( | ||
137 | r#" | ||
138 | macro_rules! bar { | ||
139 | () => { fn b() {} } | ||
140 | } | ||
141 | macro_rules! foo { | ||
142 | () => { bar!(); } | ||
143 | } | ||
144 | macro_rules! baz { | ||
145 | () => { foo!(); } | ||
146 | } | ||
147 | f<|>oo!(); | ||
148 | "#, | ||
149 | expect![[r#" | ||
150 | foo | ||
151 | fn b(){} | ||
152 | "#]], | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | #[test] | ||
157 | fn macro_expand_multiple_lines() { | ||
158 | check( | ||
159 | r#" | ||
160 | macro_rules! foo { | ||
161 | () => { | ||
162 | fn some_thing() -> u32 { | ||
163 | let a = 0; | ||
164 | a + 10 | ||
165 | } | ||
166 | } | ||
167 | } | ||
168 | f<|>oo!(); | ||
169 | "#, | ||
170 | expect![[r#" | ||
171 | foo | ||
172 | fn some_thing() -> u32 { | ||
173 | let a = 0; | ||
174 | a+10 | ||
175 | }"#]], | ||
176 | ); | ||
177 | } | ||
178 | |||
179 | #[test] | ||
180 | fn macro_expand_match_ast() { | ||
181 | check( | ||
182 | r#" | ||
183 | macro_rules! match_ast { | ||
184 | (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; | ||
185 | (match ($node:expr) { | ||
186 | $( ast::$ast:ident($it:ident) => $res:block, )* | ||
187 | _ => $catch_all:expr $(,)? | ||
188 | }) => {{ | ||
189 | $( if let Some($it) = ast::$ast::cast($node.clone()) $res else )* | ||
190 | { $catch_all } | ||
191 | }}; | ||
192 | } | ||
193 | |||
194 | fn main() { | ||
195 | mat<|>ch_ast! { | ||
196 | match container { | ||
197 | ast::TraitDef(it) => {}, | ||
198 | ast::ImplDef(it) => {}, | ||
199 | _ => { continue }, | ||
200 | } | ||
201 | } | ||
202 | } | ||
203 | "#, | ||
204 | expect![[r#" | ||
205 | match_ast | ||
206 | { | ||
207 | if let Some(it) = ast::TraitDef::cast(container.clone()){} | ||
208 | else if let Some(it) = ast::ImplDef::cast(container.clone()){} | ||
209 | else { | ||
210 | { | ||
211 | continue | ||
212 | } | ||
213 | } | ||
214 | }"#]], | ||
215 | ); | ||
216 | } | ||
217 | |||
218 | #[test] | ||
219 | fn macro_expand_match_ast_inside_let_statement() { | ||
220 | check( | ||
221 | r#" | ||
222 | macro_rules! match_ast { | ||
223 | (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; | ||
224 | (match ($node:expr) {}) => {{}}; | ||
225 | } | ||
226 | |||
227 | fn main() { | ||
228 | let p = f(|it| { | ||
229 | let res = mat<|>ch_ast! { match c {}}; | ||
230 | Some(res) | ||
231 | })?; | ||
232 | } | ||
233 | "#, | ||
234 | expect![[r#" | ||
235 | match_ast | ||
236 | {} | ||
237 | "#]], | ||
238 | ); | ||
239 | } | ||
240 | |||
241 | #[test] | ||
242 | fn macro_expand_inner_macro_fail_to_expand() { | ||
243 | check( | ||
244 | r#" | ||
245 | macro_rules! bar { | ||
246 | (BAD) => {}; | ||
247 | } | ||
248 | macro_rules! foo { | ||
249 | () => {bar!()}; | ||
250 | } | ||
251 | |||
252 | fn main() { | ||
253 | let res = fo<|>o!(); | ||
254 | } | ||
255 | "#, | ||
256 | expect![[r#" | ||
257 | foo | ||
258 | "#]], | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | #[test] | ||
263 | fn macro_expand_with_dollar_crate() { | ||
264 | check( | ||
265 | r#" | ||
266 | #[macro_export] | ||
267 | macro_rules! bar { | ||
268 | () => {0}; | ||
269 | } | ||
270 | macro_rules! foo { | ||
271 | () => {$crate::bar!()}; | ||
272 | } | ||
273 | |||
274 | fn main() { | ||
275 | let res = fo<|>o!(); | ||
276 | } | ||
277 | "#, | ||
278 | expect![[r#" | ||
279 | foo | ||
280 | 0 "#]], | ||
281 | ); | ||
282 | } | ||
283 | } | ||
diff --git a/crates/ide/src/extend_selection.rs b/crates/ide/src/extend_selection.rs new file mode 100644 index 000000000..34563a026 --- /dev/null +++ b/crates/ide/src/extend_selection.rs | |||
@@ -0,0 +1,654 @@ | |||
1 | use std::iter::successors; | ||
2 | |||
3 | use hir::Semantics; | ||
4 | use ide_db::RootDatabase; | ||
5 | use syntax::{ | ||
6 | algo::{self, find_covering_element, skip_trivia_token}, | ||
7 | ast::{self, AstNode, AstToken}, | ||
8 | Direction, NodeOrToken, | ||
9 | SyntaxKind::{self, *}, | ||
10 | SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, T, | ||
11 | }; | ||
12 | |||
13 | use crate::FileRange; | ||
14 | |||
15 | // Feature: Extend Selection | ||
16 | // | ||
17 | // Extends the current selection to the encompassing syntactic construct | ||
18 | // (expression, statement, item, module, etc). It works with multiple cursors. | ||
19 | // | ||
20 | // |=== | ||
21 | // | Editor | Shortcut | ||
22 | // | ||
23 | // | VS Code | kbd:[Ctrl+Shift+→] | ||
24 | // |=== | ||
25 | pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { | ||
26 | let sema = Semantics::new(db); | ||
27 | let src = sema.parse(frange.file_id); | ||
28 | try_extend_selection(&sema, src.syntax(), frange).unwrap_or(frange.range) | ||
29 | } | ||
30 | |||
31 | fn try_extend_selection( | ||
32 | sema: &Semantics<RootDatabase>, | ||
33 | root: &SyntaxNode, | ||
34 | frange: FileRange, | ||
35 | ) -> Option<TextRange> { | ||
36 | let range = frange.range; | ||
37 | |||
38 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; | ||
39 | let list_kinds = [ | ||
40 | RECORD_PAT_FIELD_LIST, | ||
41 | MATCH_ARM_LIST, | ||
42 | RECORD_FIELD_LIST, | ||
43 | TUPLE_FIELD_LIST, | ||
44 | RECORD_EXPR_FIELD_LIST, | ||
45 | VARIANT_LIST, | ||
46 | USE_TREE_LIST, | ||
47 | GENERIC_PARAM_LIST, | ||
48 | GENERIC_ARG_LIST, | ||
49 | TYPE_BOUND_LIST, | ||
50 | PARAM_LIST, | ||
51 | ARG_LIST, | ||
52 | ARRAY_EXPR, | ||
53 | TUPLE_EXPR, | ||
54 | TUPLE_TYPE, | ||
55 | TUPLE_PAT, | ||
56 | WHERE_CLAUSE, | ||
57 | ]; | ||
58 | |||
59 | if range.is_empty() { | ||
60 | let offset = range.start(); | ||
61 | let mut leaves = root.token_at_offset(offset); | ||
62 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | ||
63 | return Some(extend_ws(root, leaves.next()?, offset)); | ||
64 | } | ||
65 | let leaf_range = match leaves { | ||
66 | TokenAtOffset::None => return None, | ||
67 | TokenAtOffset::Single(l) => { | ||
68 | if string_kinds.contains(&l.kind()) { | ||
69 | extend_single_word_in_comment_or_string(&l, offset) | ||
70 | .unwrap_or_else(|| l.text_range()) | ||
71 | } else { | ||
72 | l.text_range() | ||
73 | } | ||
74 | } | ||
75 | TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(), | ||
76 | }; | ||
77 | return Some(leaf_range); | ||
78 | }; | ||
79 | let node = match find_covering_element(root, range) { | ||
80 | NodeOrToken::Token(token) => { | ||
81 | if token.text_range() != range { | ||
82 | return Some(token.text_range()); | ||
83 | } | ||
84 | if let Some(comment) = ast::Comment::cast(token.clone()) { | ||
85 | if let Some(range) = extend_comments(comment) { | ||
86 | return Some(range); | ||
87 | } | ||
88 | } | ||
89 | token.parent() | ||
90 | } | ||
91 | NodeOrToken::Node(node) => node, | ||
92 | }; | ||
93 | |||
94 | // if we are in single token_tree, we maybe live in macro or attr | ||
95 | if node.kind() == TOKEN_TREE { | ||
96 | if let Some(macro_call) = node.ancestors().find_map(ast::MacroCall::cast) { | ||
97 | if let Some(range) = extend_tokens_from_range(sema, macro_call, range) { | ||
98 | return Some(range); | ||
99 | } | ||
100 | } | ||
101 | } | ||
102 | |||
103 | if node.text_range() != range { | ||
104 | return Some(node.text_range()); | ||
105 | } | ||
106 | |||
107 | let node = shallowest_node(&node); | ||
108 | |||
109 | if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) { | ||
110 | if let Some(range) = extend_list_item(&node) { | ||
111 | return Some(range); | ||
112 | } | ||
113 | } | ||
114 | |||
115 | node.parent().map(|it| it.text_range()) | ||
116 | } | ||
117 | |||
118 | fn extend_tokens_from_range( | ||
119 | sema: &Semantics<RootDatabase>, | ||
120 | macro_call: ast::MacroCall, | ||
121 | original_range: TextRange, | ||
122 | ) -> Option<TextRange> { | ||
123 | let src = find_covering_element(¯o_call.syntax(), original_range); | ||
124 | let (first_token, last_token) = match src { | ||
125 | NodeOrToken::Node(it) => (it.first_token()?, it.last_token()?), | ||
126 | NodeOrToken::Token(it) => (it.clone(), it), | ||
127 | }; | ||
128 | |||
129 | let mut first_token = skip_trivia_token(first_token, Direction::Next)?; | ||
130 | let mut last_token = skip_trivia_token(last_token, Direction::Prev)?; | ||
131 | |||
132 | while !original_range.contains_range(first_token.text_range()) { | ||
133 | first_token = skip_trivia_token(first_token.next_token()?, Direction::Next)?; | ||
134 | } | ||
135 | while !original_range.contains_range(last_token.text_range()) { | ||
136 | last_token = skip_trivia_token(last_token.prev_token()?, Direction::Prev)?; | ||
137 | } | ||
138 | |||
139 | // compute original mapped token range | ||
140 | let extended = { | ||
141 | let fst_expanded = sema.descend_into_macros(first_token.clone()); | ||
142 | let lst_expanded = sema.descend_into_macros(last_token.clone()); | ||
143 | let mut lca = algo::least_common_ancestor(&fst_expanded.parent(), &lst_expanded.parent())?; | ||
144 | lca = shallowest_node(&lca); | ||
145 | if lca.first_token() == Some(fst_expanded) && lca.last_token() == Some(lst_expanded) { | ||
146 | lca = lca.parent()?; | ||
147 | } | ||
148 | lca | ||
149 | }; | ||
150 | |||
151 | // Compute parent node range | ||
152 | let validate = |token: &SyntaxToken| { | ||
153 | let expanded = sema.descend_into_macros(token.clone()); | ||
154 | algo::least_common_ancestor(&extended, &expanded.parent()).as_ref() == Some(&extended) | ||
155 | }; | ||
156 | |||
157 | // Find the first and last text range under expanded parent | ||
158 | let first = successors(Some(first_token), |token| { | ||
159 | let token = token.prev_token()?; | ||
160 | skip_trivia_token(token, Direction::Prev) | ||
161 | }) | ||
162 | .take_while(validate) | ||
163 | .last()?; | ||
164 | |||
165 | let last = successors(Some(last_token), |token| { | ||
166 | let token = token.next_token()?; | ||
167 | skip_trivia_token(token, Direction::Next) | ||
168 | }) | ||
169 | .take_while(validate) | ||
170 | .last()?; | ||
171 | |||
172 | let range = first.text_range().cover(last.text_range()); | ||
173 | if range.contains_range(original_range) && original_range != range { | ||
174 | Some(range) | ||
175 | } else { | ||
176 | None | ||
177 | } | ||
178 | } | ||
179 | |||
180 | /// Find the shallowest node with same range, which allows us to traverse siblings. | ||
181 | fn shallowest_node(node: &SyntaxNode) -> SyntaxNode { | ||
182 | node.ancestors().take_while(|n| n.text_range() == node.text_range()).last().unwrap() | ||
183 | } | ||
184 | |||
185 | fn extend_single_word_in_comment_or_string( | ||
186 | leaf: &SyntaxToken, | ||
187 | offset: TextSize, | ||
188 | ) -> Option<TextRange> { | ||
189 | let text: &str = leaf.text(); | ||
190 | let cursor_position: u32 = (offset - leaf.text_range().start()).into(); | ||
191 | |||
192 | let (before, after) = text.split_at(cursor_position as usize); | ||
193 | |||
194 | fn non_word_char(c: char) -> bool { | ||
195 | !(c.is_alphanumeric() || c == '_') | ||
196 | } | ||
197 | |||
198 | let start_idx = before.rfind(non_word_char)? as u32; | ||
199 | let end_idx = after.find(non_word_char).unwrap_or_else(|| after.len()) as u32; | ||
200 | |||
201 | let from: TextSize = (start_idx + 1).into(); | ||
202 | let to: TextSize = (cursor_position + end_idx).into(); | ||
203 | |||
204 | let range = TextRange::new(from, to); | ||
205 | if range.is_empty() { | ||
206 | None | ||
207 | } else { | ||
208 | Some(range + leaf.text_range().start()) | ||
209 | } | ||
210 | } | ||
211 | |||
212 | fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextSize) -> TextRange { | ||
213 | let ws_text = ws.text(); | ||
214 | let suffix = TextRange::new(offset, ws.text_range().end()) - ws.text_range().start(); | ||
215 | let prefix = TextRange::new(ws.text_range().start(), offset) - ws.text_range().start(); | ||
216 | let ws_suffix = &ws_text.as_str()[suffix]; | ||
217 | let ws_prefix = &ws_text.as_str()[prefix]; | ||
218 | if ws_text.contains('\n') && !ws_suffix.contains('\n') { | ||
219 | if let Some(node) = ws.next_sibling_or_token() { | ||
220 | let start = match ws_prefix.rfind('\n') { | ||
221 | Some(idx) => ws.text_range().start() + TextSize::from((idx + 1) as u32), | ||
222 | None => node.text_range().start(), | ||
223 | }; | ||
224 | let end = if root.text().char_at(node.text_range().end()) == Some('\n') { | ||
225 | node.text_range().end() + TextSize::of('\n') | ||
226 | } else { | ||
227 | node.text_range().end() | ||
228 | }; | ||
229 | return TextRange::new(start, end); | ||
230 | } | ||
231 | } | ||
232 | ws.text_range() | ||
233 | } | ||
234 | |||
235 | fn pick_best(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { | ||
236 | return if priority(&r) > priority(&l) { r } else { l }; | ||
237 | fn priority(n: &SyntaxToken) -> usize { | ||
238 | match n.kind() { | ||
239 | WHITESPACE => 0, | ||
240 | IDENT | T![self] | T![super] | T![crate] | LIFETIME => 2, | ||
241 | _ => 1, | ||
242 | } | ||
243 | } | ||
244 | } | ||
245 | |||
246 | /// Extend list item selection to include nearby delimiter and whitespace. | ||
247 | fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { | ||
248 | fn is_single_line_ws(node: &SyntaxToken) -> bool { | ||
249 | node.kind() == WHITESPACE && !node.text().contains('\n') | ||
250 | } | ||
251 | |||
252 | fn nearby_delimiter( | ||
253 | delimiter_kind: SyntaxKind, | ||
254 | node: &SyntaxNode, | ||
255 | dir: Direction, | ||
256 | ) -> Option<SyntaxToken> { | ||
257 | node.siblings_with_tokens(dir) | ||
258 | .skip(1) | ||
259 | .skip_while(|node| match node { | ||
260 | NodeOrToken::Node(_) => false, | ||
261 | NodeOrToken::Token(it) => is_single_line_ws(it), | ||
262 | }) | ||
263 | .next() | ||
264 | .and_then(|it| it.into_token()) | ||
265 | .filter(|node| node.kind() == delimiter_kind) | ||
266 | } | ||
267 | |||
268 | let delimiter = match node.kind() { | ||
269 | TYPE_BOUND => T![+], | ||
270 | _ => T![,], | ||
271 | }; | ||
272 | |||
273 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { | ||
274 | // Include any following whitespace when delimiter is after list item. | ||
275 | let final_node = delimiter_node | ||
276 | .next_sibling_or_token() | ||
277 | .and_then(|it| it.into_token()) | ||
278 | .filter(|node| is_single_line_ws(node)) | ||
279 | .unwrap_or(delimiter_node); | ||
280 | |||
281 | return Some(TextRange::new(node.text_range().start(), final_node.text_range().end())); | ||
282 | } | ||
283 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { | ||
284 | return Some(TextRange::new(delimiter_node.text_range().start(), node.text_range().end())); | ||
285 | } | ||
286 | |||
287 | None | ||
288 | } | ||
289 | |||
290 | fn extend_comments(comment: ast::Comment) -> Option<TextRange> { | ||
291 | let prev = adj_comments(&comment, Direction::Prev); | ||
292 | let next = adj_comments(&comment, Direction::Next); | ||
293 | if prev != next { | ||
294 | Some(TextRange::new(prev.syntax().text_range().start(), next.syntax().text_range().end())) | ||
295 | } else { | ||
296 | None | ||
297 | } | ||
298 | } | ||
299 | |||
300 | fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment { | ||
301 | let mut res = comment.clone(); | ||
302 | for element in comment.syntax().siblings_with_tokens(dir) { | ||
303 | let token = match element.as_token() { | ||
304 | None => break, | ||
305 | Some(token) => token, | ||
306 | }; | ||
307 | if let Some(c) = ast::Comment::cast(token.clone()) { | ||
308 | res = c | ||
309 | } else if token.kind() != WHITESPACE || token.text().contains("\n\n") { | ||
310 | break; | ||
311 | } | ||
312 | } | ||
313 | res | ||
314 | } | ||
315 | |||
316 | #[cfg(test)] | ||
317 | mod tests { | ||
318 | use crate::mock_analysis::analysis_and_position; | ||
319 | |||
320 | use super::*; | ||
321 | |||
322 | fn do_check(before: &str, afters: &[&str]) { | ||
323 | let (analysis, position) = analysis_and_position(&before); | ||
324 | let before = analysis.file_text(position.file_id).unwrap(); | ||
325 | let range = TextRange::empty(position.offset); | ||
326 | let mut frange = FileRange { file_id: position.file_id, range }; | ||
327 | |||
328 | for &after in afters { | ||
329 | frange.range = analysis.extend_selection(frange).unwrap(); | ||
330 | let actual = &before[frange.range]; | ||
331 | assert_eq!(after, actual); | ||
332 | } | ||
333 | } | ||
334 | |||
335 | #[test] | ||
336 | fn test_extend_selection_arith() { | ||
337 | do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]); | ||
338 | } | ||
339 | |||
340 | #[test] | ||
341 | fn test_extend_selection_list() { | ||
342 | do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]); | ||
343 | do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]); | ||
344 | do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,", "(x: i32,y: i32)"]); | ||
345 | do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]); | ||
346 | do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", "y: i32, "]); | ||
347 | do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]); | ||
348 | |||
349 | do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]); | ||
350 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]); | ||
351 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", "33 ,", "[ 22 , 33 ,]"]); | ||
352 | |||
353 | do_check(r#"fn main() { (1, 2<|>) }"#, &["2", ", 2", "(1, 2)"]); | ||
354 | |||
355 | do_check( | ||
356 | r#" | ||
357 | const FOO: [usize; 2] = [ | ||
358 | 22, | ||
359 | <|>33, | ||
360 | ]"#, | ||
361 | &["33", "33,"], | ||
362 | ); | ||
363 | |||
364 | do_check( | ||
365 | r#" | ||
366 | const FOO: [usize; 2] = [ | ||
367 | 22 | ||
368 | , 33<|>, | ||
369 | ]"#, | ||
370 | &["33", "33,"], | ||
371 | ); | ||
372 | } | ||
373 | |||
374 | #[test] | ||
375 | fn test_extend_selection_start_of_the_line() { | ||
376 | do_check( | ||
377 | r#" | ||
378 | impl S { | ||
379 | <|> fn foo() { | ||
380 | |||
381 | } | ||
382 | }"#, | ||
383 | &[" fn foo() {\n\n }\n"], | ||
384 | ); | ||
385 | } | ||
386 | |||
387 | #[test] | ||
388 | fn test_extend_selection_doc_comments() { | ||
389 | do_check( | ||
390 | r#" | ||
391 | struct A; | ||
392 | |||
393 | /// bla | ||
394 | /// bla | ||
395 | struct B { | ||
396 | <|> | ||
397 | } | ||
398 | "#, | ||
399 | &["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"], | ||
400 | ) | ||
401 | } | ||
402 | |||
403 | #[test] | ||
404 | fn test_extend_selection_comments() { | ||
405 | do_check( | ||
406 | r#" | ||
407 | fn bar(){} | ||
408 | |||
409 | // fn foo() { | ||
410 | // 1 + <|>1 | ||
411 | // } | ||
412 | |||
413 | // fn foo(){} | ||
414 | "#, | ||
415 | &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"], | ||
416 | ); | ||
417 | |||
418 | do_check( | ||
419 | r#" | ||
420 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
421 | // pub enum Direction { | ||
422 | // <|> Next, | ||
423 | // Prev | ||
424 | // } | ||
425 | "#, | ||
426 | &[ | ||
427 | "// Next,", | ||
428 | "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }", | ||
429 | ], | ||
430 | ); | ||
431 | |||
432 | do_check( | ||
433 | r#" | ||
434 | /* | ||
435 | foo | ||
436 | _bar1<|>*/ | ||
437 | "#, | ||
438 | &["_bar1", "/*\nfoo\n_bar1*/"], | ||
439 | ); | ||
440 | |||
441 | do_check(r#"//!<|>foo_2 bar"#, &["foo_2", "//!foo_2 bar"]); | ||
442 | |||
443 | do_check(r#"/<|>/foo bar"#, &["//foo bar"]); | ||
444 | } | ||
445 | |||
446 | #[test] | ||
447 | fn test_extend_selection_prefer_idents() { | ||
448 | do_check( | ||
449 | r#" | ||
450 | fn main() { foo<|>+bar;} | ||
451 | "#, | ||
452 | &["foo", "foo+bar"], | ||
453 | ); | ||
454 | do_check( | ||
455 | r#" | ||
456 | fn main() { foo+<|>bar;} | ||
457 | "#, | ||
458 | &["bar", "foo+bar"], | ||
459 | ); | ||
460 | } | ||
461 | |||
462 | #[test] | ||
463 | fn test_extend_selection_prefer_lifetimes() { | ||
464 | do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]); | ||
465 | do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]); | ||
466 | } | ||
467 | |||
468 | #[test] | ||
469 | fn test_extend_selection_select_first_word() { | ||
470 | do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]); | ||
471 | do_check( | ||
472 | r#" | ||
473 | impl S { | ||
474 | fn foo() { | ||
475 | // hel<|>lo world | ||
476 | } | ||
477 | } | ||
478 | "#, | ||
479 | &["hello", "// hello world"], | ||
480 | ); | ||
481 | } | ||
482 | |||
483 | #[test] | ||
484 | fn test_extend_selection_string() { | ||
485 | do_check( | ||
486 | r#" | ||
487 | fn bar(){} | ||
488 | |||
489 | " fn f<|>oo() {" | ||
490 | "#, | ||
491 | &["foo", "\" fn foo() {\""], | ||
492 | ); | ||
493 | } | ||
494 | |||
495 | #[test] | ||
496 | fn test_extend_trait_bounds_list_in_where_clause() { | ||
497 | do_check( | ||
498 | r#" | ||
499 | fn foo<R>() | ||
500 | where | ||
501 | R: req::Request + 'static, | ||
502 | R::Params: DeserializeOwned<|> + panic::UnwindSafe + 'static, | ||
503 | R::Result: Serialize + 'static, | ||
504 | "#, | ||
505 | &[ | ||
506 | "DeserializeOwned", | ||
507 | "DeserializeOwned + ", | ||
508 | "DeserializeOwned + panic::UnwindSafe + 'static", | ||
509 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static", | ||
510 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,", | ||
511 | ], | ||
512 | ); | ||
513 | do_check(r#"fn foo<T>() where T: <|>Copy"#, &["Copy"]); | ||
514 | do_check(r#"fn foo<T>() where T: <|>Copy + Display"#, &["Copy", "Copy + "]); | ||
515 | do_check(r#"fn foo<T>() where T: <|>Copy +Display"#, &["Copy", "Copy +"]); | ||
516 | do_check(r#"fn foo<T>() where T: <|>Copy+Display"#, &["Copy", "Copy+"]); | ||
517 | do_check(r#"fn foo<T>() where T: Copy + <|>Display"#, &["Display", "+ Display"]); | ||
518 | do_check(r#"fn foo<T>() where T: Copy + <|>Display + Sync"#, &["Display", "Display + "]); | ||
519 | do_check(r#"fn foo<T>() where T: Copy +<|>Display"#, &["Display", "+Display"]); | ||
520 | } | ||
521 | |||
522 | #[test] | ||
523 | fn test_extend_trait_bounds_list_inline() { | ||
524 | do_check(r#"fn foo<T: <|>Copy>() {}"#, &["Copy"]); | ||
525 | do_check(r#"fn foo<T: <|>Copy + Display>() {}"#, &["Copy", "Copy + "]); | ||
526 | do_check(r#"fn foo<T: <|>Copy +Display>() {}"#, &["Copy", "Copy +"]); | ||
527 | do_check(r#"fn foo<T: <|>Copy+Display>() {}"#, &["Copy", "Copy+"]); | ||
528 | do_check(r#"fn foo<T: Copy + <|>Display>() {}"#, &["Display", "+ Display"]); | ||
529 | do_check(r#"fn foo<T: Copy + <|>Display + Sync>() {}"#, &["Display", "Display + "]); | ||
530 | do_check(r#"fn foo<T: Copy +<|>Display>() {}"#, &["Display", "+Display"]); | ||
531 | do_check( | ||
532 | r#"fn foo<T: Copy<|> + Display, U: Copy>() {}"#, | ||
533 | &[ | ||
534 | "Copy", | ||
535 | "Copy + ", | ||
536 | "Copy + Display", | ||
537 | "T: Copy + Display", | ||
538 | "T: Copy + Display, ", | ||
539 | "<T: Copy + Display, U: Copy>", | ||
540 | ], | ||
541 | ); | ||
542 | } | ||
543 | |||
544 | #[test] | ||
545 | fn test_extend_selection_on_tuple_in_type() { | ||
546 | do_check( | ||
547 | r#"fn main() { let _: (krate, <|>_crate_def_map, module_id) = (); }"#, | ||
548 | &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], | ||
549 | ); | ||
550 | // white space variations | ||
551 | do_check( | ||
552 | r#"fn main() { let _: (krate,<|>_crate_def_map,module_id) = (); }"#, | ||
553 | &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], | ||
554 | ); | ||
555 | do_check( | ||
556 | r#" | ||
557 | fn main() { let _: ( | ||
558 | krate, | ||
559 | _crate<|>_def_map, | ||
560 | module_id | ||
561 | ) = (); }"#, | ||
562 | &[ | ||
563 | "_crate_def_map", | ||
564 | "_crate_def_map,", | ||
565 | "(\n krate,\n _crate_def_map,\n module_id\n)", | ||
566 | ], | ||
567 | ); | ||
568 | } | ||
569 | |||
570 | #[test] | ||
571 | fn test_extend_selection_on_tuple_in_rvalue() { | ||
572 | do_check( | ||
573 | r#"fn main() { let var = (krate, _crate_def_map<|>, module_id); }"#, | ||
574 | &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], | ||
575 | ); | ||
576 | // white space variations | ||
577 | do_check( | ||
578 | r#"fn main() { let var = (krate,_crate<|>_def_map,module_id); }"#, | ||
579 | &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], | ||
580 | ); | ||
581 | do_check( | ||
582 | r#" | ||
583 | fn main() { let var = ( | ||
584 | krate, | ||
585 | _crate_def_map<|>, | ||
586 | module_id | ||
587 | ); }"#, | ||
588 | &[ | ||
589 | "_crate_def_map", | ||
590 | "_crate_def_map,", | ||
591 | "(\n krate,\n _crate_def_map,\n module_id\n)", | ||
592 | ], | ||
593 | ); | ||
594 | } | ||
595 | |||
596 | #[test] | ||
597 | fn test_extend_selection_on_tuple_pat() { | ||
598 | do_check( | ||
599 | r#"fn main() { let (krate, _crate_def_map<|>, module_id) = var; }"#, | ||
600 | &["_crate_def_map", "_crate_def_map, ", "(krate, _crate_def_map, module_id)"], | ||
601 | ); | ||
602 | // white space variations | ||
603 | do_check( | ||
604 | r#"fn main() { let (krate,_crate<|>_def_map,module_id) = var; }"#, | ||
605 | &["_crate_def_map", "_crate_def_map,", "(krate,_crate_def_map,module_id)"], | ||
606 | ); | ||
607 | do_check( | ||
608 | r#" | ||
609 | fn main() { let ( | ||
610 | krate, | ||
611 | _crate_def_map<|>, | ||
612 | module_id | ||
613 | ) = var; }"#, | ||
614 | &[ | ||
615 | "_crate_def_map", | ||
616 | "_crate_def_map,", | ||
617 | "(\n krate,\n _crate_def_map,\n module_id\n)", | ||
618 | ], | ||
619 | ); | ||
620 | } | ||
621 | |||
622 | #[test] | ||
623 | fn extend_selection_inside_macros() { | ||
624 | do_check( | ||
625 | r#"macro_rules! foo { ($item:item) => {$item} } | ||
626 | foo!{fn hello(na<|>me:usize){}}"#, | ||
627 | &[ | ||
628 | "name", | ||
629 | "name:usize", | ||
630 | "(name:usize)", | ||
631 | "fn hello(name:usize){}", | ||
632 | "{fn hello(name:usize){}}", | ||
633 | "foo!{fn hello(name:usize){}}", | ||
634 | ], | ||
635 | ); | ||
636 | } | ||
637 | |||
638 | #[test] | ||
639 | fn extend_selection_inside_recur_macros() { | ||
640 | do_check( | ||
641 | r#" macro_rules! foo2 { ($item:item) => {$item} } | ||
642 | macro_rules! foo { ($item:item) => {foo2!($item);} } | ||
643 | foo!{fn hello(na<|>me:usize){}}"#, | ||
644 | &[ | ||
645 | "name", | ||
646 | "name:usize", | ||
647 | "(name:usize)", | ||
648 | "fn hello(name:usize){}", | ||
649 | "{fn hello(name:usize){}}", | ||
650 | "foo!{fn hello(name:usize){}}", | ||
651 | ], | ||
652 | ); | ||
653 | } | ||
654 | } | ||
diff --git a/crates/ide/src/file_structure.rs b/crates/ide/src/file_structure.rs new file mode 100644 index 000000000..c90247ba6 --- /dev/null +++ b/crates/ide/src/file_structure.rs | |||
@@ -0,0 +1,431 @@ | |||
1 | use syntax::{ | ||
2 | ast::{self, AttrsOwner, GenericParamsOwner, NameOwner}, | ||
3 | match_ast, AstNode, SourceFile, SyntaxKind, SyntaxNode, TextRange, WalkEvent, | ||
4 | }; | ||
5 | |||
6 | #[derive(Debug, Clone)] | ||
7 | pub struct StructureNode { | ||
8 | pub parent: Option<usize>, | ||
9 | pub label: String, | ||
10 | pub navigation_range: TextRange, | ||
11 | pub node_range: TextRange, | ||
12 | pub kind: SyntaxKind, | ||
13 | pub detail: Option<String>, | ||
14 | pub deprecated: bool, | ||
15 | } | ||
16 | |||
17 | // Feature: File Structure | ||
18 | // | ||
19 | // Provides a tree of the symbols defined in the file. Can be used to | ||
20 | // | ||
21 | // * fuzzy search symbol in a file (super useful) | ||
22 | // * draw breadcrumbs to describe the context around the cursor | ||
23 | // * draw outline of the file | ||
24 | // | ||
25 | // |=== | ||
26 | // | Editor | Shortcut | ||
27 | // | ||
28 | // | VS Code | kbd:[Ctrl+Shift+O] | ||
29 | // |=== | ||
30 | pub fn file_structure(file: &SourceFile) -> Vec<StructureNode> { | ||
31 | let mut res = Vec::new(); | ||
32 | let mut stack = Vec::new(); | ||
33 | |||
34 | for event in file.syntax().preorder() { | ||
35 | match event { | ||
36 | WalkEvent::Enter(node) => { | ||
37 | if let Some(mut symbol) = structure_node(&node) { | ||
38 | symbol.parent = stack.last().copied(); | ||
39 | stack.push(res.len()); | ||
40 | res.push(symbol); | ||
41 | } | ||
42 | } | ||
43 | WalkEvent::Leave(node) => { | ||
44 | if structure_node(&node).is_some() { | ||
45 | stack.pop().unwrap(); | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | } | ||
50 | res | ||
51 | } | ||
52 | |||
53 | fn structure_node(node: &SyntaxNode) -> Option<StructureNode> { | ||
54 | fn decl<N: NameOwner + AttrsOwner>(node: N) -> Option<StructureNode> { | ||
55 | decl_with_detail(&node, None) | ||
56 | } | ||
57 | |||
58 | fn decl_with_type_ref<N: NameOwner + AttrsOwner>( | ||
59 | node: &N, | ||
60 | type_ref: Option<ast::Type>, | ||
61 | ) -> Option<StructureNode> { | ||
62 | let detail = type_ref.map(|type_ref| { | ||
63 | let mut detail = String::new(); | ||
64 | collapse_ws(type_ref.syntax(), &mut detail); | ||
65 | detail | ||
66 | }); | ||
67 | decl_with_detail(node, detail) | ||
68 | } | ||
69 | |||
70 | fn decl_with_detail<N: NameOwner + AttrsOwner>( | ||
71 | node: &N, | ||
72 | detail: Option<String>, | ||
73 | ) -> Option<StructureNode> { | ||
74 | let name = node.name()?; | ||
75 | |||
76 | Some(StructureNode { | ||
77 | parent: None, | ||
78 | label: name.text().to_string(), | ||
79 | navigation_range: name.syntax().text_range(), | ||
80 | node_range: node.syntax().text_range(), | ||
81 | kind: node.syntax().kind(), | ||
82 | detail, | ||
83 | deprecated: node.attrs().filter_map(|x| x.simple_name()).any(|x| x == "deprecated"), | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | fn collapse_ws(node: &SyntaxNode, output: &mut String) { | ||
88 | let mut can_insert_ws = false; | ||
89 | node.text().for_each_chunk(|chunk| { | ||
90 | for line in chunk.lines() { | ||
91 | let line = line.trim(); | ||
92 | if line.is_empty() { | ||
93 | if can_insert_ws { | ||
94 | output.push(' '); | ||
95 | can_insert_ws = false; | ||
96 | } | ||
97 | } else { | ||
98 | output.push_str(line); | ||
99 | can_insert_ws = true; | ||
100 | } | ||
101 | } | ||
102 | }) | ||
103 | } | ||
104 | |||
105 | match_ast! { | ||
106 | match node { | ||
107 | ast::Fn(it) => { | ||
108 | let mut detail = String::from("fn"); | ||
109 | if let Some(type_param_list) = it.generic_param_list() { | ||
110 | collapse_ws(type_param_list.syntax(), &mut detail); | ||
111 | } | ||
112 | if let Some(param_list) = it.param_list() { | ||
113 | collapse_ws(param_list.syntax(), &mut detail); | ||
114 | } | ||
115 | if let Some(ret_type) = it.ret_type() { | ||
116 | detail.push_str(" "); | ||
117 | collapse_ws(ret_type.syntax(), &mut detail); | ||
118 | } | ||
119 | |||
120 | decl_with_detail(&it, Some(detail)) | ||
121 | }, | ||
122 | ast::Struct(it) => decl(it), | ||
123 | ast::Union(it) => decl(it), | ||
124 | ast::Enum(it) => decl(it), | ||
125 | ast::Variant(it) => decl(it), | ||
126 | ast::Trait(it) => decl(it), | ||
127 | ast::Module(it) => decl(it), | ||
128 | ast::TypeAlias(it) => decl_with_type_ref(&it, it.ty()), | ||
129 | ast::RecordField(it) => decl_with_type_ref(&it, it.ty()), | ||
130 | ast::Const(it) => decl_with_type_ref(&it, it.ty()), | ||
131 | ast::Static(it) => decl_with_type_ref(&it, it.ty()), | ||
132 | ast::Impl(it) => { | ||
133 | let target_type = it.self_ty()?; | ||
134 | let target_trait = it.trait_(); | ||
135 | let label = match target_trait { | ||
136 | None => format!("impl {}", target_type.syntax().text()), | ||
137 | Some(t) => { | ||
138 | format!("impl {} for {}", t.syntax().text(), target_type.syntax().text(),) | ||
139 | } | ||
140 | }; | ||
141 | |||
142 | let node = StructureNode { | ||
143 | parent: None, | ||
144 | label, | ||
145 | navigation_range: target_type.syntax().text_range(), | ||
146 | node_range: it.syntax().text_range(), | ||
147 | kind: it.syntax().kind(), | ||
148 | detail: None, | ||
149 | deprecated: false, | ||
150 | }; | ||
151 | Some(node) | ||
152 | }, | ||
153 | ast::MacroCall(it) => { | ||
154 | match it.path().and_then(|it| it.segment()).and_then(|it| it.name_ref()) { | ||
155 | Some(path_segment) if path_segment.text() == "macro_rules" | ||
156 | => decl(it), | ||
157 | _ => None, | ||
158 | } | ||
159 | }, | ||
160 | _ => None, | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | |||
165 | #[cfg(test)] | ||
166 | mod tests { | ||
167 | use expect::{expect, Expect}; | ||
168 | |||
169 | use super::*; | ||
170 | |||
171 | fn check(ra_fixture: &str, expect: Expect) { | ||
172 | let file = SourceFile::parse(ra_fixture).ok().unwrap(); | ||
173 | let structure = file_structure(&file); | ||
174 | expect.assert_debug_eq(&structure) | ||
175 | } | ||
176 | |||
177 | #[test] | ||
178 | fn test_file_structure() { | ||
179 | check( | ||
180 | r#" | ||
181 | struct Foo { | ||
182 | x: i32 | ||
183 | } | ||
184 | |||
185 | mod m { | ||
186 | fn bar1() {} | ||
187 | fn bar2<T>(t: T) -> T {} | ||
188 | fn bar3<A, | ||
189 | B>(a: A, | ||
190 | b: B) -> Vec< | ||
191 | u32 | ||
192 | > {} | ||
193 | } | ||
194 | |||
195 | enum E { X, Y(i32) } | ||
196 | type T = (); | ||
197 | static S: i32 = 92; | ||
198 | const C: i32 = 92; | ||
199 | |||
200 | impl E {} | ||
201 | |||
202 | impl fmt::Debug for E {} | ||
203 | |||
204 | macro_rules! mc { | ||
205 | () => {} | ||
206 | } | ||
207 | |||
208 | #[macro_export] | ||
209 | macro_rules! mcexp { | ||
210 | () => {} | ||
211 | } | ||
212 | |||
213 | /// Doc comment | ||
214 | macro_rules! mcexp { | ||
215 | () => {} | ||
216 | } | ||
217 | |||
218 | #[deprecated] | ||
219 | fn obsolete() {} | ||
220 | |||
221 | #[deprecated(note = "for awhile")] | ||
222 | fn very_obsolete() {} | ||
223 | "#, | ||
224 | expect![[r#" | ||
225 | [ | ||
226 | StructureNode { | ||
227 | parent: None, | ||
228 | label: "Foo", | ||
229 | navigation_range: 8..11, | ||
230 | node_range: 1..26, | ||
231 | kind: STRUCT, | ||
232 | detail: None, | ||
233 | deprecated: false, | ||
234 | }, | ||
235 | StructureNode { | ||
236 | parent: Some( | ||
237 | 0, | ||
238 | ), | ||
239 | label: "x", | ||
240 | navigation_range: 18..19, | ||
241 | node_range: 18..24, | ||
242 | kind: RECORD_FIELD, | ||
243 | detail: Some( | ||
244 | "i32", | ||
245 | ), | ||
246 | deprecated: false, | ||
247 | }, | ||
248 | StructureNode { | ||
249 | parent: None, | ||
250 | label: "m", | ||
251 | navigation_range: 32..33, | ||
252 | node_range: 28..158, | ||
253 | kind: MODULE, | ||
254 | detail: None, | ||
255 | deprecated: false, | ||
256 | }, | ||
257 | StructureNode { | ||
258 | parent: Some( | ||
259 | 2, | ||
260 | ), | ||
261 | label: "bar1", | ||
262 | navigation_range: 43..47, | ||
263 | node_range: 40..52, | ||
264 | kind: FN, | ||
265 | detail: Some( | ||
266 | "fn()", | ||
267 | ), | ||
268 | deprecated: false, | ||
269 | }, | ||
270 | StructureNode { | ||
271 | parent: Some( | ||
272 | 2, | ||
273 | ), | ||
274 | label: "bar2", | ||
275 | navigation_range: 60..64, | ||
276 | node_range: 57..81, | ||
277 | kind: FN, | ||
278 | detail: Some( | ||
279 | "fn<T>(t: T) -> T", | ||
280 | ), | ||
281 | deprecated: false, | ||
282 | }, | ||
283 | StructureNode { | ||
284 | parent: Some( | ||
285 | 2, | ||
286 | ), | ||
287 | label: "bar3", | ||
288 | navigation_range: 89..93, | ||
289 | node_range: 86..156, | ||
290 | kind: FN, | ||
291 | detail: Some( | ||
292 | "fn<A, B>(a: A, b: B) -> Vec< u32 >", | ||
293 | ), | ||
294 | deprecated: false, | ||
295 | }, | ||
296 | StructureNode { | ||
297 | parent: None, | ||
298 | label: "E", | ||
299 | navigation_range: 165..166, | ||
300 | node_range: 160..180, | ||
301 | kind: ENUM, | ||
302 | detail: None, | ||
303 | deprecated: false, | ||
304 | }, | ||
305 | StructureNode { | ||
306 | parent: Some( | ||
307 | 6, | ||
308 | ), | ||
309 | label: "X", | ||
310 | navigation_range: 169..170, | ||
311 | node_range: 169..170, | ||
312 | kind: VARIANT, | ||
313 | detail: None, | ||
314 | deprecated: false, | ||
315 | }, | ||
316 | StructureNode { | ||
317 | parent: Some( | ||
318 | 6, | ||
319 | ), | ||
320 | label: "Y", | ||
321 | navigation_range: 172..173, | ||
322 | node_range: 172..178, | ||
323 | kind: VARIANT, | ||
324 | detail: None, | ||
325 | deprecated: false, | ||
326 | }, | ||
327 | StructureNode { | ||
328 | parent: None, | ||
329 | label: "T", | ||
330 | navigation_range: 186..187, | ||
331 | node_range: 181..193, | ||
332 | kind: TYPE_ALIAS, | ||
333 | detail: Some( | ||
334 | "()", | ||
335 | ), | ||
336 | deprecated: false, | ||
337 | }, | ||
338 | StructureNode { | ||
339 | parent: None, | ||
340 | label: "S", | ||
341 | navigation_range: 201..202, | ||
342 | node_range: 194..213, | ||
343 | kind: STATIC, | ||
344 | detail: Some( | ||
345 | "i32", | ||
346 | ), | ||
347 | deprecated: false, | ||
348 | }, | ||
349 | StructureNode { | ||
350 | parent: None, | ||
351 | label: "C", | ||
352 | navigation_range: 220..221, | ||
353 | node_range: 214..232, | ||
354 | kind: CONST, | ||
355 | detail: Some( | ||
356 | "i32", | ||
357 | ), | ||
358 | deprecated: false, | ||
359 | }, | ||
360 | StructureNode { | ||
361 | parent: None, | ||
362 | label: "impl E", | ||
363 | navigation_range: 239..240, | ||
364 | node_range: 234..243, | ||
365 | kind: IMPL, | ||
366 | detail: None, | ||
367 | deprecated: false, | ||
368 | }, | ||
369 | StructureNode { | ||
370 | parent: None, | ||
371 | label: "impl fmt::Debug for E", | ||
372 | navigation_range: 265..266, | ||
373 | node_range: 245..269, | ||
374 | kind: IMPL, | ||
375 | detail: None, | ||
376 | deprecated: false, | ||
377 | }, | ||
378 | StructureNode { | ||
379 | parent: None, | ||
380 | label: "mc", | ||
381 | navigation_range: 284..286, | ||
382 | node_range: 271..303, | ||
383 | kind: MACRO_CALL, | ||
384 | detail: None, | ||
385 | deprecated: false, | ||
386 | }, | ||
387 | StructureNode { | ||
388 | parent: None, | ||
389 | label: "mcexp", | ||
390 | navigation_range: 334..339, | ||
391 | node_range: 305..356, | ||
392 | kind: MACRO_CALL, | ||
393 | detail: None, | ||
394 | deprecated: false, | ||
395 | }, | ||
396 | StructureNode { | ||
397 | parent: None, | ||
398 | label: "mcexp", | ||
399 | navigation_range: 387..392, | ||
400 | node_range: 358..409, | ||
401 | kind: MACRO_CALL, | ||
402 | detail: None, | ||
403 | deprecated: false, | ||
404 | }, | ||
405 | StructureNode { | ||
406 | parent: None, | ||
407 | label: "obsolete", | ||
408 | navigation_range: 428..436, | ||
409 | node_range: 411..441, | ||
410 | kind: FN, | ||
411 | detail: Some( | ||
412 | "fn()", | ||
413 | ), | ||
414 | deprecated: true, | ||
415 | }, | ||
416 | StructureNode { | ||
417 | parent: None, | ||
418 | label: "very_obsolete", | ||
419 | navigation_range: 481..494, | ||
420 | node_range: 443..499, | ||
421 | kind: FN, | ||
422 | detail: Some( | ||
423 | "fn()", | ||
424 | ), | ||
425 | deprecated: true, | ||
426 | }, | ||
427 | ] | ||
428 | "#]], | ||
429 | ); | ||
430 | } | ||
431 | } | ||
diff --git a/crates/ide/src/folding_ranges.rs b/crates/ide/src/folding_ranges.rs new file mode 100644 index 000000000..7523aec55 --- /dev/null +++ b/crates/ide/src/folding_ranges.rs | |||
@@ -0,0 +1,422 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use rustc_hash::FxHashSet; | ||
4 | |||
5 | use syntax::{ | ||
6 | ast::{self, AstNode, AstToken, VisibilityOwner}, | ||
7 | Direction, NodeOrToken, SourceFile, | ||
8 | SyntaxKind::{self, *}, | ||
9 | SyntaxNode, TextRange, | ||
10 | }; | ||
11 | |||
12 | #[derive(Debug, PartialEq, Eq)] | ||
13 | pub enum FoldKind { | ||
14 | Comment, | ||
15 | Imports, | ||
16 | Mods, | ||
17 | Block, | ||
18 | ArgList, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug)] | ||
22 | pub struct Fold { | ||
23 | pub range: TextRange, | ||
24 | pub kind: FoldKind, | ||
25 | } | ||
26 | |||
27 | pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { | ||
28 | let mut res = vec![]; | ||
29 | let mut visited_comments = FxHashSet::default(); | ||
30 | let mut visited_imports = FxHashSet::default(); | ||
31 | let mut visited_mods = FxHashSet::default(); | ||
32 | |||
33 | for element in file.syntax().descendants_with_tokens() { | ||
34 | // Fold items that span multiple lines | ||
35 | if let Some(kind) = fold_kind(element.kind()) { | ||
36 | let is_multiline = match &element { | ||
37 | NodeOrToken::Node(node) => node.text().contains_char('\n'), | ||
38 | NodeOrToken::Token(token) => token.text().contains('\n'), | ||
39 | }; | ||
40 | if is_multiline { | ||
41 | res.push(Fold { range: element.text_range(), kind }); | ||
42 | continue; | ||
43 | } | ||
44 | } | ||
45 | |||
46 | match element { | ||
47 | NodeOrToken::Token(token) => { | ||
48 | // Fold groups of comments | ||
49 | if let Some(comment) = ast::Comment::cast(token) { | ||
50 | if !visited_comments.contains(&comment) { | ||
51 | if let Some(range) = | ||
52 | contiguous_range_for_comment(comment, &mut visited_comments) | ||
53 | { | ||
54 | res.push(Fold { range, kind: FoldKind::Comment }) | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | } | ||
59 | NodeOrToken::Node(node) => { | ||
60 | // Fold groups of imports | ||
61 | if node.kind() == USE && !visited_imports.contains(&node) { | ||
62 | if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) { | ||
63 | res.push(Fold { range, kind: FoldKind::Imports }) | ||
64 | } | ||
65 | } | ||
66 | |||
67 | // Fold groups of mods | ||
68 | if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node) | ||
69 | { | ||
70 | if let Some(range) = | ||
71 | contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods) | ||
72 | { | ||
73 | res.push(Fold { range, kind: FoldKind::Mods }) | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | res | ||
81 | } | ||
82 | |||
83 | fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | ||
84 | match kind { | ||
85 | COMMENT => Some(FoldKind::Comment), | ||
86 | USE => Some(FoldKind::Imports), | ||
87 | ARG_LIST | PARAM_LIST => Some(FoldKind::ArgList), | ||
88 | ASSOC_ITEM_LIST | ||
89 | | RECORD_FIELD_LIST | ||
90 | | RECORD_PAT_FIELD_LIST | ||
91 | | RECORD_EXPR_FIELD_LIST | ||
92 | | ITEM_LIST | ||
93 | | EXTERN_ITEM_LIST | ||
94 | | USE_TREE_LIST | ||
95 | | BLOCK_EXPR | ||
96 | | MATCH_ARM_LIST | ||
97 | | VARIANT_LIST | ||
98 | | TOKEN_TREE => Some(FoldKind::Block), | ||
99 | _ => None, | ||
100 | } | ||
101 | } | ||
102 | |||
103 | fn has_visibility(node: &SyntaxNode) -> bool { | ||
104 | ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some() | ||
105 | } | ||
106 | |||
107 | fn contiguous_range_for_group( | ||
108 | first: &SyntaxNode, | ||
109 | visited: &mut FxHashSet<SyntaxNode>, | ||
110 | ) -> Option<TextRange> { | ||
111 | contiguous_range_for_group_unless(first, |_| false, visited) | ||
112 | } | ||
113 | |||
114 | fn contiguous_range_for_group_unless( | ||
115 | first: &SyntaxNode, | ||
116 | unless: impl Fn(&SyntaxNode) -> bool, | ||
117 | visited: &mut FxHashSet<SyntaxNode>, | ||
118 | ) -> Option<TextRange> { | ||
119 | visited.insert(first.clone()); | ||
120 | |||
121 | let mut last = first.clone(); | ||
122 | for element in first.siblings_with_tokens(Direction::Next) { | ||
123 | let node = match element { | ||
124 | NodeOrToken::Token(token) => { | ||
125 | if let Some(ws) = ast::Whitespace::cast(token) { | ||
126 | if !ws.spans_multiple_lines() { | ||
127 | // Ignore whitespace without blank lines | ||
128 | continue; | ||
129 | } | ||
130 | } | ||
131 | // There is a blank line or another token, which means that the | ||
132 | // group ends here | ||
133 | break; | ||
134 | } | ||
135 | NodeOrToken::Node(node) => node, | ||
136 | }; | ||
137 | |||
138 | // Stop if we find a node that doesn't belong to the group | ||
139 | if node.kind() != first.kind() || unless(&node) { | ||
140 | break; | ||
141 | } | ||
142 | |||
143 | visited.insert(node.clone()); | ||
144 | last = node; | ||
145 | } | ||
146 | |||
147 | if first != &last { | ||
148 | Some(TextRange::new(first.text_range().start(), last.text_range().end())) | ||
149 | } else { | ||
150 | // The group consists of only one element, therefore it cannot be folded | ||
151 | None | ||
152 | } | ||
153 | } | ||
154 | |||
155 | fn contiguous_range_for_comment( | ||
156 | first: ast::Comment, | ||
157 | visited: &mut FxHashSet<ast::Comment>, | ||
158 | ) -> Option<TextRange> { | ||
159 | visited.insert(first.clone()); | ||
160 | |||
161 | // Only fold comments of the same flavor | ||
162 | let group_kind = first.kind(); | ||
163 | if !group_kind.shape.is_line() { | ||
164 | return None; | ||
165 | } | ||
166 | |||
167 | let mut last = first.clone(); | ||
168 | for element in first.syntax().siblings_with_tokens(Direction::Next) { | ||
169 | match element { | ||
170 | NodeOrToken::Token(token) => { | ||
171 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | ||
172 | if !ws.spans_multiple_lines() { | ||
173 | // Ignore whitespace without blank lines | ||
174 | continue; | ||
175 | } | ||
176 | } | ||
177 | if let Some(c) = ast::Comment::cast(token) { | ||
178 | if c.kind() == group_kind { | ||
179 | visited.insert(c.clone()); | ||
180 | last = c; | ||
181 | continue; | ||
182 | } | ||
183 | } | ||
184 | // The comment group ends because either: | ||
185 | // * An element of a different kind was reached | ||
186 | // * A comment of a different flavor was reached | ||
187 | break; | ||
188 | } | ||
189 | NodeOrToken::Node(_) => break, | ||
190 | }; | ||
191 | } | ||
192 | |||
193 | if first != last { | ||
194 | Some(TextRange::new(first.syntax().text_range().start(), last.syntax().text_range().end())) | ||
195 | } else { | ||
196 | // The group consists of only one element, therefore it cannot be folded | ||
197 | None | ||
198 | } | ||
199 | } | ||
200 | |||
201 | #[cfg(test)] | ||
202 | mod tests { | ||
203 | use test_utils::extract_tags; | ||
204 | |||
205 | use super::*; | ||
206 | |||
207 | fn check(ra_fixture: &str) { | ||
208 | let (ranges, text) = extract_tags(ra_fixture, "fold"); | ||
209 | |||
210 | let parse = SourceFile::parse(&text); | ||
211 | let folds = folding_ranges(&parse.tree()); | ||
212 | assert_eq!( | ||
213 | folds.len(), | ||
214 | ranges.len(), | ||
215 | "The amount of folds is different than the expected amount" | ||
216 | ); | ||
217 | |||
218 | for (fold, (range, attr)) in folds.iter().zip(ranges.into_iter()) { | ||
219 | assert_eq!(fold.range.start(), range.start()); | ||
220 | assert_eq!(fold.range.end(), range.end()); | ||
221 | |||
222 | let kind = match fold.kind { | ||
223 | FoldKind::Comment => "comment", | ||
224 | FoldKind::Imports => "imports", | ||
225 | FoldKind::Mods => "mods", | ||
226 | FoldKind::Block => "block", | ||
227 | FoldKind::ArgList => "arglist", | ||
228 | }; | ||
229 | assert_eq!(kind, &attr.unwrap()); | ||
230 | } | ||
231 | } | ||
232 | |||
233 | #[test] | ||
234 | fn test_fold_comments() { | ||
235 | check( | ||
236 | r#" | ||
237 | <fold comment>// Hello | ||
238 | // this is a multiline | ||
239 | // comment | ||
240 | //</fold> | ||
241 | |||
242 | // But this is not | ||
243 | |||
244 | fn main() <fold block>{ | ||
245 | <fold comment>// We should | ||
246 | // also | ||
247 | // fold | ||
248 | // this one.</fold> | ||
249 | <fold comment>//! But this one is different | ||
250 | //! because it has another flavor</fold> | ||
251 | <fold comment>/* As does this | ||
252 | multiline comment */</fold> | ||
253 | }</fold>"#, | ||
254 | ); | ||
255 | } | ||
256 | |||
257 | #[test] | ||
258 | fn test_fold_imports() { | ||
259 | check( | ||
260 | r#" | ||
261 | <fold imports>use std::<fold block>{ | ||
262 | str, | ||
263 | vec, | ||
264 | io as iop | ||
265 | }</fold>;</fold> | ||
266 | |||
267 | fn main() <fold block>{ | ||
268 | }</fold>"#, | ||
269 | ); | ||
270 | } | ||
271 | |||
272 | #[test] | ||
273 | fn test_fold_mods() { | ||
274 | check( | ||
275 | r#" | ||
276 | |||
277 | pub mod foo; | ||
278 | <fold mods>mod after_pub; | ||
279 | mod after_pub_next;</fold> | ||
280 | |||
281 | <fold mods>mod before_pub; | ||
282 | mod before_pub_next;</fold> | ||
283 | pub mod bar; | ||
284 | |||
285 | mod not_folding_single; | ||
286 | pub mod foobar; | ||
287 | pub not_folding_single_next; | ||
288 | |||
289 | <fold mods>#[cfg(test)] | ||
290 | mod with_attribute; | ||
291 | mod with_attribute_next;</fold> | ||
292 | |||
293 | fn main() <fold block>{ | ||
294 | }</fold>"#, | ||
295 | ); | ||
296 | } | ||
297 | |||
298 | #[test] | ||
299 | fn test_fold_import_groups() { | ||
300 | check( | ||
301 | r#" | ||
302 | <fold imports>use std::str; | ||
303 | use std::vec; | ||
304 | use std::io as iop;</fold> | ||
305 | |||
306 | <fold imports>use std::mem; | ||
307 | use std::f64;</fold> | ||
308 | |||
309 | use std::collections::HashMap; | ||
310 | // Some random comment | ||
311 | use std::collections::VecDeque; | ||
312 | |||
313 | fn main() <fold block>{ | ||
314 | }</fold>"#, | ||
315 | ); | ||
316 | } | ||
317 | |||
318 | #[test] | ||
319 | fn test_fold_import_and_groups() { | ||
320 | check( | ||
321 | r#" | ||
322 | <fold imports>use std::str; | ||
323 | use std::vec; | ||
324 | use std::io as iop;</fold> | ||
325 | |||
326 | <fold imports>use std::mem; | ||
327 | use std::f64;</fold> | ||
328 | |||
329 | <fold imports>use std::collections::<fold block>{ | ||
330 | HashMap, | ||
331 | VecDeque, | ||
332 | }</fold>;</fold> | ||
333 | // Some random comment | ||
334 | |||
335 | fn main() <fold block>{ | ||
336 | }</fold>"#, | ||
337 | ); | ||
338 | } | ||
339 | |||
340 | #[test] | ||
341 | fn test_folds_structs() { | ||
342 | check( | ||
343 | r#" | ||
344 | struct Foo <fold block>{ | ||
345 | }</fold> | ||
346 | "#, | ||
347 | ); | ||
348 | } | ||
349 | |||
350 | #[test] | ||
351 | fn test_folds_traits() { | ||
352 | check( | ||
353 | r#" | ||
354 | trait Foo <fold block>{ | ||
355 | }</fold> | ||
356 | "#, | ||
357 | ); | ||
358 | } | ||
359 | |||
360 | #[test] | ||
361 | fn test_folds_macros() { | ||
362 | check( | ||
363 | r#" | ||
364 | macro_rules! foo <fold block>{ | ||
365 | ($($tt:tt)*) => { $($tt)* } | ||
366 | }</fold> | ||
367 | "#, | ||
368 | ); | ||
369 | } | ||
370 | |||
371 | #[test] | ||
372 | fn test_fold_match_arms() { | ||
373 | check( | ||
374 | r#" | ||
375 | fn main() <fold block>{ | ||
376 | match 0 <fold block>{ | ||
377 | 0 => 0, | ||
378 | _ => 1, | ||
379 | }</fold> | ||
380 | }</fold> | ||
381 | "#, | ||
382 | ); | ||
383 | } | ||
384 | |||
385 | #[test] | ||
386 | fn fold_big_calls() { | ||
387 | check( | ||
388 | r#" | ||
389 | fn main() <fold block>{ | ||
390 | frobnicate<fold arglist>( | ||
391 | 1, | ||
392 | 2, | ||
393 | 3, | ||
394 | )</fold> | ||
395 | }</fold> | ||
396 | "#, | ||
397 | ) | ||
398 | } | ||
399 | |||
400 | #[test] | ||
401 | fn fold_record_literals() { | ||
402 | check( | ||
403 | r#" | ||
404 | const _: S = S <fold block>{ | ||
405 | |||
406 | }</fold>; | ||
407 | "#, | ||
408 | ) | ||
409 | } | ||
410 | |||
411 | #[test] | ||
412 | fn fold_multiline_params() { | ||
413 | check( | ||
414 | r#" | ||
415 | fn foo<fold arglist>( | ||
416 | x: i32, | ||
417 | y: String, | ||
418 | )</fold> {} | ||
419 | "#, | ||
420 | ) | ||
421 | } | ||
422 | } | ||
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs new file mode 100644 index 000000000..15e9b7fad --- /dev/null +++ b/crates/ide/src/goto_definition.rs | |||
@@ -0,0 +1,989 @@ | |||
1 | use hir::Semantics; | ||
2 | use ide_db::{ | ||
3 | defs::{classify_name, classify_name_ref}, | ||
4 | symbol_index, RootDatabase, | ||
5 | }; | ||
6 | use syntax::{ | ||
7 | ast::{self}, | ||
8 | match_ast, AstNode, | ||
9 | SyntaxKind::*, | ||
10 | SyntaxToken, TokenAtOffset, T, | ||
11 | }; | ||
12 | |||
13 | use crate::{ | ||
14 | display::{ToNav, TryToNav}, | ||
15 | FilePosition, NavigationTarget, RangeInfo, | ||
16 | }; | ||
17 | |||
18 | // Feature: Go to Definition | ||
19 | // | ||
20 | // Navigates to the definition of an identifier. | ||
21 | // | ||
22 | // |=== | ||
23 | // | Editor | Shortcut | ||
24 | // | ||
25 | // | VS Code | kbd:[F12] | ||
26 | // |=== | ||
27 | pub(crate) fn goto_definition( | ||
28 | db: &RootDatabase, | ||
29 | position: FilePosition, | ||
30 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
31 | let sema = Semantics::new(db); | ||
32 | let file = sema.parse(position.file_id).syntax().clone(); | ||
33 | let original_token = pick_best(file.token_at_offset(position.offset))?; | ||
34 | let token = sema.descend_into_macros(original_token.clone()); | ||
35 | let parent = token.parent(); | ||
36 | |||
37 | let nav_targets = match_ast! { | ||
38 | match parent { | ||
39 | ast::NameRef(name_ref) => { | ||
40 | reference_definition(&sema, &name_ref).to_vec() | ||
41 | }, | ||
42 | ast::Name(name) => { | ||
43 | let def = classify_name(&sema, &name)?.definition(sema.db); | ||
44 | let nav = def.try_to_nav(sema.db)?; | ||
45 | vec![nav] | ||
46 | }, | ||
47 | _ => return None, | ||
48 | } | ||
49 | }; | ||
50 | |||
51 | Some(RangeInfo::new(original_token.text_range(), nav_targets)) | ||
52 | } | ||
53 | |||
54 | fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { | ||
55 | return tokens.max_by_key(priority); | ||
56 | fn priority(n: &SyntaxToken) -> usize { | ||
57 | match n.kind() { | ||
58 | IDENT | INT_NUMBER | T![self] => 2, | ||
59 | kind if kind.is_trivia() => 0, | ||
60 | _ => 1, | ||
61 | } | ||
62 | } | ||
63 | } | ||
64 | |||
65 | #[derive(Debug)] | ||
66 | pub(crate) enum ReferenceResult { | ||
67 | Exact(NavigationTarget), | ||
68 | Approximate(Vec<NavigationTarget>), | ||
69 | } | ||
70 | |||
71 | impl ReferenceResult { | ||
72 | fn to_vec(self) -> Vec<NavigationTarget> { | ||
73 | match self { | ||
74 | ReferenceResult::Exact(target) => vec![target], | ||
75 | ReferenceResult::Approximate(vec) => vec, | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | pub(crate) fn reference_definition( | ||
81 | sema: &Semantics<RootDatabase>, | ||
82 | name_ref: &ast::NameRef, | ||
83 | ) -> ReferenceResult { | ||
84 | let name_kind = classify_name_ref(sema, name_ref); | ||
85 | if let Some(def) = name_kind { | ||
86 | let def = def.definition(sema.db); | ||
87 | return match def.try_to_nav(sema.db) { | ||
88 | Some(nav) => ReferenceResult::Exact(nav), | ||
89 | None => ReferenceResult::Approximate(Vec::new()), | ||
90 | }; | ||
91 | } | ||
92 | |||
93 | // Fallback index based approach: | ||
94 | let navs = symbol_index::index_resolve(sema.db, name_ref) | ||
95 | .into_iter() | ||
96 | .map(|s| s.to_nav(sema.db)) | ||
97 | .collect(); | ||
98 | ReferenceResult::Approximate(navs) | ||
99 | } | ||
100 | |||
101 | #[cfg(test)] | ||
102 | mod tests { | ||
103 | use base_db::FileRange; | ||
104 | use syntax::{TextRange, TextSize}; | ||
105 | |||
106 | use crate::mock_analysis::MockAnalysis; | ||
107 | |||
108 | fn check(ra_fixture: &str) { | ||
109 | let (mock, position) = MockAnalysis::with_files_and_position(ra_fixture); | ||
110 | let (mut expected, data) = mock.annotation(); | ||
111 | let analysis = mock.analysis(); | ||
112 | match data.as_str() { | ||
113 | "" => (), | ||
114 | "file" => { | ||
115 | expected.range = | ||
116 | TextRange::up_to(TextSize::of(&*analysis.file_text(expected.file_id).unwrap())) | ||
117 | } | ||
118 | data => panic!("bad data: {}", data), | ||
119 | } | ||
120 | |||
121 | let mut navs = | ||
122 | analysis.goto_definition(position).unwrap().expect("no definition found").info; | ||
123 | if navs.len() == 0 { | ||
124 | panic!("unresolved reference") | ||
125 | } | ||
126 | assert_eq!(navs.len(), 1); | ||
127 | |||
128 | let nav = navs.pop().unwrap(); | ||
129 | assert_eq!(expected, FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }); | ||
130 | } | ||
131 | |||
132 | #[test] | ||
133 | fn goto_def_for_extern_crate() { | ||
134 | check( | ||
135 | r#" | ||
136 | //- /main.rs | ||
137 | extern crate std<|>; | ||
138 | //- /std/lib.rs | ||
139 | // empty | ||
140 | //^ file | ||
141 | "#, | ||
142 | ) | ||
143 | } | ||
144 | |||
145 | #[test] | ||
146 | fn goto_def_for_renamed_extern_crate() { | ||
147 | check( | ||
148 | r#" | ||
149 | //- /main.rs | ||
150 | extern crate std as abc<|>; | ||
151 | //- /std/lib.rs | ||
152 | // empty | ||
153 | //^ file | ||
154 | "#, | ||
155 | ) | ||
156 | } | ||
157 | |||
158 | #[test] | ||
159 | fn goto_def_in_items() { | ||
160 | check( | ||
161 | r#" | ||
162 | struct Foo; | ||
163 | //^^^ | ||
164 | enum E { X(Foo<|>) } | ||
165 | "#, | ||
166 | ); | ||
167 | } | ||
168 | |||
169 | #[test] | ||
170 | fn goto_def_at_start_of_item() { | ||
171 | check( | ||
172 | r#" | ||
173 | struct Foo; | ||
174 | //^^^ | ||
175 | enum E { X(<|>Foo) } | ||
176 | "#, | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | #[test] | ||
181 | fn goto_definition_resolves_correct_name() { | ||
182 | check( | ||
183 | r#" | ||
184 | //- /lib.rs | ||
185 | use a::Foo; | ||
186 | mod a; | ||
187 | mod b; | ||
188 | enum E { X(Foo<|>) } | ||
189 | |||
190 | //- /a.rs | ||
191 | struct Foo; | ||
192 | //^^^ | ||
193 | //- /b.rs | ||
194 | struct Foo; | ||
195 | "#, | ||
196 | ); | ||
197 | } | ||
198 | |||
199 | #[test] | ||
200 | fn goto_def_for_module_declaration() { | ||
201 | check( | ||
202 | r#" | ||
203 | //- /lib.rs | ||
204 | mod <|>foo; | ||
205 | |||
206 | //- /foo.rs | ||
207 | // empty | ||
208 | //^ file | ||
209 | "#, | ||
210 | ); | ||
211 | |||
212 | check( | ||
213 | r#" | ||
214 | //- /lib.rs | ||
215 | mod <|>foo; | ||
216 | |||
217 | //- /foo/mod.rs | ||
218 | // empty | ||
219 | //^ file | ||
220 | "#, | ||
221 | ); | ||
222 | } | ||
223 | |||
224 | #[test] | ||
225 | fn goto_def_for_macros() { | ||
226 | check( | ||
227 | r#" | ||
228 | macro_rules! foo { () => { () } } | ||
229 | //^^^ | ||
230 | fn bar() { | ||
231 | <|>foo!(); | ||
232 | } | ||
233 | "#, | ||
234 | ); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn goto_def_for_macros_from_other_crates() { | ||
239 | check( | ||
240 | r#" | ||
241 | //- /lib.rs | ||
242 | use foo::foo; | ||
243 | fn bar() { | ||
244 | <|>foo!(); | ||
245 | } | ||
246 | |||
247 | //- /foo/lib.rs | ||
248 | #[macro_export] | ||
249 | macro_rules! foo { () => { () } } | ||
250 | //^^^ | ||
251 | "#, | ||
252 | ); | ||
253 | } | ||
254 | |||
255 | #[test] | ||
256 | fn goto_def_for_macros_in_use_tree() { | ||
257 | check( | ||
258 | r#" | ||
259 | //- /lib.rs | ||
260 | use foo::foo<|>; | ||
261 | |||
262 | //- /foo/lib.rs | ||
263 | #[macro_export] | ||
264 | macro_rules! foo { () => { () } } | ||
265 | //^^^ | ||
266 | "#, | ||
267 | ); | ||
268 | } | ||
269 | |||
270 | #[test] | ||
271 | fn goto_def_for_macro_defined_fn_with_arg() { | ||
272 | check( | ||
273 | r#" | ||
274 | //- /lib.rs | ||
275 | macro_rules! define_fn { | ||
276 | ($name:ident) => (fn $name() {}) | ||
277 | } | ||
278 | |||
279 | define_fn!(foo); | ||
280 | //^^^ | ||
281 | |||
282 | fn bar() { | ||
283 | <|>foo(); | ||
284 | } | ||
285 | "#, | ||
286 | ); | ||
287 | } | ||
288 | |||
289 | #[test] | ||
290 | fn goto_def_for_macro_defined_fn_no_arg() { | ||
291 | check( | ||
292 | r#" | ||
293 | //- /lib.rs | ||
294 | macro_rules! define_fn { | ||
295 | () => (fn foo() {}) | ||
296 | } | ||
297 | |||
298 | define_fn!(); | ||
299 | //^^^^^^^^^^^^^ | ||
300 | |||
301 | fn bar() { | ||
302 | <|>foo(); | ||
303 | } | ||
304 | "#, | ||
305 | ); | ||
306 | } | ||
307 | |||
308 | #[test] | ||
309 | fn goto_definition_works_for_macro_inside_pattern() { | ||
310 | check( | ||
311 | r#" | ||
312 | //- /lib.rs | ||
313 | macro_rules! foo {() => {0}} | ||
314 | //^^^ | ||
315 | |||
316 | fn bar() { | ||
317 | match (0,1) { | ||
318 | (<|>foo!(), _) => {} | ||
319 | } | ||
320 | } | ||
321 | "#, | ||
322 | ); | ||
323 | } | ||
324 | |||
325 | #[test] | ||
326 | fn goto_definition_works_for_macro_inside_match_arm_lhs() { | ||
327 | check( | ||
328 | r#" | ||
329 | //- /lib.rs | ||
330 | macro_rules! foo {() => {0}} | ||
331 | //^^^ | ||
332 | fn bar() { | ||
333 | match 0 { | ||
334 | <|>foo!() => {} | ||
335 | } | ||
336 | } | ||
337 | "#, | ||
338 | ); | ||
339 | } | ||
340 | |||
341 | #[test] | ||
342 | fn goto_def_for_use_alias() { | ||
343 | check( | ||
344 | r#" | ||
345 | //- /lib.rs | ||
346 | use foo as bar<|>; | ||
347 | |||
348 | //- /foo/lib.rs | ||
349 | // empty | ||
350 | //^ file | ||
351 | "#, | ||
352 | ); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn goto_def_for_use_alias_foo_macro() { | ||
357 | check( | ||
358 | r#" | ||
359 | //- /lib.rs | ||
360 | use foo::foo as bar<|>; | ||
361 | |||
362 | //- /foo/lib.rs | ||
363 | #[macro_export] | ||
364 | macro_rules! foo { () => { () } } | ||
365 | //^^^ | ||
366 | "#, | ||
367 | ); | ||
368 | } | ||
369 | |||
370 | #[test] | ||
371 | fn goto_def_for_methods() { | ||
372 | check( | ||
373 | r#" | ||
374 | //- /lib.rs | ||
375 | struct Foo; | ||
376 | impl Foo { | ||
377 | fn frobnicate(&self) { } | ||
378 | //^^^^^^^^^^ | ||
379 | } | ||
380 | |||
381 | fn bar(foo: &Foo) { | ||
382 | foo.frobnicate<|>(); | ||
383 | } | ||
384 | "#, | ||
385 | ); | ||
386 | } | ||
387 | |||
388 | #[test] | ||
389 | fn goto_def_for_fields() { | ||
390 | check( | ||
391 | r#" | ||
392 | struct Foo { | ||
393 | spam: u32, | ||
394 | } //^^^^ | ||
395 | |||
396 | fn bar(foo: &Foo) { | ||
397 | foo.spam<|>; | ||
398 | } | ||
399 | "#, | ||
400 | ); | ||
401 | } | ||
402 | |||
403 | #[test] | ||
404 | fn goto_def_for_record_fields() { | ||
405 | check( | ||
406 | r#" | ||
407 | //- /lib.rs | ||
408 | struct Foo { | ||
409 | spam: u32, | ||
410 | } //^^^^ | ||
411 | |||
412 | fn bar() -> Foo { | ||
413 | Foo { | ||
414 | spam<|>: 0, | ||
415 | } | ||
416 | } | ||
417 | "#, | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn goto_def_for_record_pat_fields() { | ||
423 | check( | ||
424 | r#" | ||
425 | //- /lib.rs | ||
426 | struct Foo { | ||
427 | spam: u32, | ||
428 | } //^^^^ | ||
429 | |||
430 | fn bar(foo: Foo) -> Foo { | ||
431 | let Foo { spam<|>: _, } = foo | ||
432 | } | ||
433 | "#, | ||
434 | ); | ||
435 | } | ||
436 | |||
437 | #[test] | ||
438 | fn goto_def_for_record_fields_macros() { | ||
439 | check( | ||
440 | r" | ||
441 | macro_rules! m { () => { 92 };} | ||
442 | struct Foo { spam: u32 } | ||
443 | //^^^^ | ||
444 | |||
445 | fn bar() -> Foo { | ||
446 | Foo { spam<|>: m!() } | ||
447 | } | ||
448 | ", | ||
449 | ); | ||
450 | } | ||
451 | |||
452 | #[test] | ||
453 | fn goto_for_tuple_fields() { | ||
454 | check( | ||
455 | r#" | ||
456 | struct Foo(u32); | ||
457 | //^^^ | ||
458 | |||
459 | fn bar() { | ||
460 | let foo = Foo(0); | ||
461 | foo.<|>0; | ||
462 | } | ||
463 | "#, | ||
464 | ); | ||
465 | } | ||
466 | |||
467 | #[test] | ||
468 | fn goto_def_for_ufcs_inherent_methods() { | ||
469 | check( | ||
470 | r#" | ||
471 | struct Foo; | ||
472 | impl Foo { | ||
473 | fn frobnicate() { } | ||
474 | } //^^^^^^^^^^ | ||
475 | |||
476 | fn bar(foo: &Foo) { | ||
477 | Foo::frobnicate<|>(); | ||
478 | } | ||
479 | "#, | ||
480 | ); | ||
481 | } | ||
482 | |||
483 | #[test] | ||
484 | fn goto_def_for_ufcs_trait_methods_through_traits() { | ||
485 | check( | ||
486 | r#" | ||
487 | trait Foo { | ||
488 | fn frobnicate(); | ||
489 | } //^^^^^^^^^^ | ||
490 | |||
491 | fn bar() { | ||
492 | Foo::frobnicate<|>(); | ||
493 | } | ||
494 | "#, | ||
495 | ); | ||
496 | } | ||
497 | |||
498 | #[test] | ||
499 | fn goto_def_for_ufcs_trait_methods_through_self() { | ||
500 | check( | ||
501 | r#" | ||
502 | struct Foo; | ||
503 | trait Trait { | ||
504 | fn frobnicate(); | ||
505 | } //^^^^^^^^^^ | ||
506 | impl Trait for Foo {} | ||
507 | |||
508 | fn bar() { | ||
509 | Foo::frobnicate<|>(); | ||
510 | } | ||
511 | "#, | ||
512 | ); | ||
513 | } | ||
514 | |||
515 | #[test] | ||
516 | fn goto_definition_on_self() { | ||
517 | check( | ||
518 | r#" | ||
519 | struct Foo; | ||
520 | impl Foo { | ||
521 | //^^^ | ||
522 | pub fn new() -> Self { | ||
523 | Self<|> {} | ||
524 | } | ||
525 | } | ||
526 | "#, | ||
527 | ); | ||
528 | check( | ||
529 | r#" | ||
530 | struct Foo; | ||
531 | impl Foo { | ||
532 | //^^^ | ||
533 | pub fn new() -> Self<|> { | ||
534 | Self {} | ||
535 | } | ||
536 | } | ||
537 | "#, | ||
538 | ); | ||
539 | |||
540 | check( | ||
541 | r#" | ||
542 | enum Foo { A } | ||
543 | impl Foo { | ||
544 | //^^^ | ||
545 | pub fn new() -> Self<|> { | ||
546 | Foo::A | ||
547 | } | ||
548 | } | ||
549 | "#, | ||
550 | ); | ||
551 | |||
552 | check( | ||
553 | r#" | ||
554 | enum Foo { A } | ||
555 | impl Foo { | ||
556 | //^^^ | ||
557 | pub fn thing(a: &Self<|>) { | ||
558 | } | ||
559 | } | ||
560 | "#, | ||
561 | ); | ||
562 | } | ||
563 | |||
564 | #[test] | ||
565 | fn goto_definition_on_self_in_trait_impl() { | ||
566 | check( | ||
567 | r#" | ||
568 | struct Foo; | ||
569 | trait Make { | ||
570 | fn new() -> Self; | ||
571 | } | ||
572 | impl Make for Foo { | ||
573 | //^^^ | ||
574 | fn new() -> Self { | ||
575 | Self<|> {} | ||
576 | } | ||
577 | } | ||
578 | "#, | ||
579 | ); | ||
580 | |||
581 | check( | ||
582 | r#" | ||
583 | struct Foo; | ||
584 | trait Make { | ||
585 | fn new() -> Self; | ||
586 | } | ||
587 | impl Make for Foo { | ||
588 | //^^^ | ||
589 | fn new() -> Self<|> { | ||
590 | Self {} | ||
591 | } | ||
592 | } | ||
593 | "#, | ||
594 | ); | ||
595 | } | ||
596 | |||
597 | #[test] | ||
598 | fn goto_def_when_used_on_definition_name_itself() { | ||
599 | check( | ||
600 | r#" | ||
601 | struct Foo<|> { value: u32 } | ||
602 | //^^^ | ||
603 | "#, | ||
604 | ); | ||
605 | |||
606 | check( | ||
607 | r#" | ||
608 | struct Foo { | ||
609 | field<|>: string, | ||
610 | } //^^^^^ | ||
611 | "#, | ||
612 | ); | ||
613 | |||
614 | check( | ||
615 | r#" | ||
616 | fn foo_test<|>() { } | ||
617 | //^^^^^^^^ | ||
618 | "#, | ||
619 | ); | ||
620 | |||
621 | check( | ||
622 | r#" | ||
623 | enum Foo<|> { Variant } | ||
624 | //^^^ | ||
625 | "#, | ||
626 | ); | ||
627 | |||
628 | check( | ||
629 | r#" | ||
630 | enum Foo { | ||
631 | Variant1, | ||
632 | Variant2<|>, | ||
633 | //^^^^^^^^ | ||
634 | Variant3, | ||
635 | } | ||
636 | "#, | ||
637 | ); | ||
638 | |||
639 | check( | ||
640 | r#" | ||
641 | static INNER<|>: &str = ""; | ||
642 | //^^^^^ | ||
643 | "#, | ||
644 | ); | ||
645 | |||
646 | check( | ||
647 | r#" | ||
648 | const INNER<|>: &str = ""; | ||
649 | //^^^^^ | ||
650 | "#, | ||
651 | ); | ||
652 | |||
653 | check( | ||
654 | r#" | ||
655 | type Thing<|> = Option<()>; | ||
656 | //^^^^^ | ||
657 | "#, | ||
658 | ); | ||
659 | |||
660 | check( | ||
661 | r#" | ||
662 | trait Foo<|> { } | ||
663 | //^^^ | ||
664 | "#, | ||
665 | ); | ||
666 | |||
667 | check( | ||
668 | r#" | ||
669 | mod bar<|> { } | ||
670 | //^^^ | ||
671 | "#, | ||
672 | ); | ||
673 | } | ||
674 | |||
675 | #[test] | ||
676 | fn goto_from_macro() { | ||
677 | check( | ||
678 | r#" | ||
679 | macro_rules! id { | ||
680 | ($($tt:tt)*) => { $($tt)* } | ||
681 | } | ||
682 | fn foo() {} | ||
683 | //^^^ | ||
684 | id! { | ||
685 | fn bar() { | ||
686 | fo<|>o(); | ||
687 | } | ||
688 | } | ||
689 | mod confuse_index { fn foo(); } | ||
690 | "#, | ||
691 | ); | ||
692 | } | ||
693 | |||
694 | #[test] | ||
695 | fn goto_through_format() { | ||
696 | check( | ||
697 | r#" | ||
698 | #[macro_export] | ||
699 | macro_rules! format { | ||
700 | ($($arg:tt)*) => ($crate::fmt::format($crate::__export::format_args!($($arg)*))) | ||
701 | } | ||
702 | #[rustc_builtin_macro] | ||
703 | #[macro_export] | ||
704 | macro_rules! format_args { | ||
705 | ($fmt:expr) => ({ /* compiler built-in */ }); | ||
706 | ($fmt:expr, $($args:tt)*) => ({ /* compiler built-in */ }) | ||
707 | } | ||
708 | pub mod __export { | ||
709 | pub use crate::format_args; | ||
710 | fn foo() {} // for index confusion | ||
711 | } | ||
712 | fn foo() -> i8 {} | ||
713 | //^^^ | ||
714 | fn test() { | ||
715 | format!("{}", fo<|>o()) | ||
716 | } | ||
717 | "#, | ||
718 | ); | ||
719 | } | ||
720 | |||
721 | #[test] | ||
722 | fn goto_for_type_param() { | ||
723 | check( | ||
724 | r#" | ||
725 | struct Foo<T: Clone> { t: <|>T } | ||
726 | //^ | ||
727 | "#, | ||
728 | ); | ||
729 | } | ||
730 | |||
731 | #[test] | ||
732 | fn goto_within_macro() { | ||
733 | check( | ||
734 | r#" | ||
735 | macro_rules! id { | ||
736 | ($($tt:tt)*) => ($($tt)*) | ||
737 | } | ||
738 | |||
739 | fn foo() { | ||
740 | let x = 1; | ||
741 | //^ | ||
742 | id!({ | ||
743 | let y = <|>x; | ||
744 | let z = y; | ||
745 | }); | ||
746 | } | ||
747 | "#, | ||
748 | ); | ||
749 | |||
750 | check( | ||
751 | r#" | ||
752 | macro_rules! id { | ||
753 | ($($tt:tt)*) => ($($tt)*) | ||
754 | } | ||
755 | |||
756 | fn foo() { | ||
757 | let x = 1; | ||
758 | id!({ | ||
759 | let y = x; | ||
760 | //^ | ||
761 | let z = <|>y; | ||
762 | }); | ||
763 | } | ||
764 | "#, | ||
765 | ); | ||
766 | } | ||
767 | |||
768 | #[test] | ||
769 | fn goto_def_in_local_fn() { | ||
770 | check( | ||
771 | r#" | ||
772 | fn main() { | ||
773 | fn foo() { | ||
774 | let x = 92; | ||
775 | //^ | ||
776 | <|>x; | ||
777 | } | ||
778 | } | ||
779 | "#, | ||
780 | ); | ||
781 | } | ||
782 | |||
783 | #[test] | ||
784 | fn goto_def_in_local_macro() { | ||
785 | check( | ||
786 | r#" | ||
787 | fn bar() { | ||
788 | macro_rules! foo { () => { () } } | ||
789 | //^^^ | ||
790 | <|>foo!(); | ||
791 | } | ||
792 | "#, | ||
793 | ); | ||
794 | } | ||
795 | |||
796 | #[test] | ||
797 | fn goto_def_for_field_init_shorthand() { | ||
798 | check( | ||
799 | r#" | ||
800 | struct Foo { x: i32 } | ||
801 | fn main() { | ||
802 | let x = 92; | ||
803 | //^ | ||
804 | Foo { x<|> }; | ||
805 | } | ||
806 | "#, | ||
807 | ) | ||
808 | } | ||
809 | |||
810 | #[test] | ||
811 | fn goto_def_for_enum_variant_field() { | ||
812 | check( | ||
813 | r#" | ||
814 | enum Foo { | ||
815 | Bar { x: i32 } | ||
816 | } //^ | ||
817 | fn baz(foo: Foo) { | ||
818 | match foo { | ||
819 | Foo::Bar { x<|> } => x | ||
820 | }; | ||
821 | } | ||
822 | "#, | ||
823 | ); | ||
824 | } | ||
825 | |||
826 | #[test] | ||
827 | fn goto_def_for_enum_variant_self_pattern_const() { | ||
828 | check( | ||
829 | r#" | ||
830 | enum Foo { Bar } | ||
831 | //^^^ | ||
832 | impl Foo { | ||
833 | fn baz(self) { | ||
834 | match self { Self::Bar<|> => {} } | ||
835 | } | ||
836 | } | ||
837 | "#, | ||
838 | ); | ||
839 | } | ||
840 | |||
841 | #[test] | ||
842 | fn goto_def_for_enum_variant_self_pattern_record() { | ||
843 | check( | ||
844 | r#" | ||
845 | enum Foo { Bar { val: i32 } } | ||
846 | //^^^ | ||
847 | impl Foo { | ||
848 | fn baz(self) -> i32 { | ||
849 | match self { Self::Bar<|> { val } => {} } | ||
850 | } | ||
851 | } | ||
852 | "#, | ||
853 | ); | ||
854 | } | ||
855 | |||
856 | #[test] | ||
857 | fn goto_def_for_enum_variant_self_expr_const() { | ||
858 | check( | ||
859 | r#" | ||
860 | enum Foo { Bar } | ||
861 | //^^^ | ||
862 | impl Foo { | ||
863 | fn baz(self) { Self::Bar<|>; } | ||
864 | } | ||
865 | "#, | ||
866 | ); | ||
867 | } | ||
868 | |||
869 | #[test] | ||
870 | fn goto_def_for_enum_variant_self_expr_record() { | ||
871 | check( | ||
872 | r#" | ||
873 | enum Foo { Bar { val: i32 } } | ||
874 | //^^^ | ||
875 | impl Foo { | ||
876 | fn baz(self) { Self::Bar<|> {val: 4}; } | ||
877 | } | ||
878 | "#, | ||
879 | ); | ||
880 | } | ||
881 | |||
882 | #[test] | ||
883 | fn goto_def_for_type_alias_generic_parameter() { | ||
884 | check( | ||
885 | r#" | ||
886 | type Alias<T> = T<|>; | ||
887 | //^ | ||
888 | "#, | ||
889 | ) | ||
890 | } | ||
891 | |||
892 | #[test] | ||
893 | fn goto_def_for_macro_container() { | ||
894 | check( | ||
895 | r#" | ||
896 | //- /lib.rs | ||
897 | foo::module<|>::mac!(); | ||
898 | |||
899 | //- /foo/lib.rs | ||
900 | pub mod module { | ||
901 | //^^^^^^ | ||
902 | #[macro_export] | ||
903 | macro_rules! _mac { () => { () } } | ||
904 | pub use crate::_mac as mac; | ||
905 | } | ||
906 | "#, | ||
907 | ); | ||
908 | } | ||
909 | |||
910 | #[test] | ||
911 | fn goto_def_for_assoc_ty_in_path() { | ||
912 | check( | ||
913 | r#" | ||
914 | trait Iterator { | ||
915 | type Item; | ||
916 | //^^^^ | ||
917 | } | ||
918 | |||
919 | fn f() -> impl Iterator<Item<|> = u8> {} | ||
920 | "#, | ||
921 | ); | ||
922 | } | ||
923 | |||
924 | #[test] | ||
925 | fn goto_def_for_assoc_ty_in_path_multiple() { | ||
926 | check( | ||
927 | r#" | ||
928 | trait Iterator { | ||
929 | type A; | ||
930 | //^ | ||
931 | type B; | ||
932 | } | ||
933 | |||
934 | fn f() -> impl Iterator<A<|> = u8, B = ()> {} | ||
935 | "#, | ||
936 | ); | ||
937 | check( | ||
938 | r#" | ||
939 | trait Iterator { | ||
940 | type A; | ||
941 | type B; | ||
942 | //^ | ||
943 | } | ||
944 | |||
945 | fn f() -> impl Iterator<A = u8, B<|> = ()> {} | ||
946 | "#, | ||
947 | ); | ||
948 | } | ||
949 | |||
950 | #[test] | ||
951 | fn goto_def_for_assoc_ty_ufcs() { | ||
952 | check( | ||
953 | r#" | ||
954 | trait Iterator { | ||
955 | type Item; | ||
956 | //^^^^ | ||
957 | } | ||
958 | |||
959 | fn g() -> <() as Iterator<Item<|> = ()>>::Item {} | ||
960 | "#, | ||
961 | ); | ||
962 | } | ||
963 | |||
964 | #[test] | ||
965 | fn goto_def_for_assoc_ty_ufcs_multiple() { | ||
966 | check( | ||
967 | r#" | ||
968 | trait Iterator { | ||
969 | type A; | ||
970 | //^ | ||
971 | type B; | ||
972 | } | ||
973 | |||
974 | fn g() -> <() as Iterator<A<|> = (), B = u8>>::B {} | ||
975 | "#, | ||
976 | ); | ||
977 | check( | ||
978 | r#" | ||
979 | trait Iterator { | ||
980 | type A; | ||
981 | type B; | ||
982 | //^ | ||
983 | } | ||
984 | |||
985 | fn g() -> <() as Iterator<A = (), B<|> = u8>>::A {} | ||
986 | "#, | ||
987 | ); | ||
988 | } | ||
989 | } | ||
diff --git a/crates/ide/src/goto_implementation.rs b/crates/ide/src/goto_implementation.rs new file mode 100644 index 000000000..f503f4ec5 --- /dev/null +++ b/crates/ide/src/goto_implementation.rs | |||
@@ -0,0 +1,229 @@ | |||
1 | use hir::{Crate, ImplDef, Semantics}; | ||
2 | use ide_db::RootDatabase; | ||
3 | use syntax::{algo::find_node_at_offset, ast, AstNode}; | ||
4 | |||
5 | use crate::{display::ToNav, FilePosition, NavigationTarget, RangeInfo}; | ||
6 | |||
7 | // Feature: Go to Implementation | ||
8 | // | ||
9 | // Navigates to the impl block of structs, enums or traits. Also implemented as a code lens. | ||
10 | // | ||
11 | // |=== | ||
12 | // | Editor | Shortcut | ||
13 | // | ||
14 | // | VS Code | kbd:[Ctrl+F12] | ||
15 | // |=== | ||
16 | pub(crate) fn goto_implementation( | ||
17 | db: &RootDatabase, | ||
18 | position: FilePosition, | ||
19 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
20 | let sema = Semantics::new(db); | ||
21 | let source_file = sema.parse(position.file_id); | ||
22 | let syntax = source_file.syntax().clone(); | ||
23 | |||
24 | let krate = sema.to_module_def(position.file_id)?.krate(); | ||
25 | |||
26 | if let Some(nominal_def) = find_node_at_offset::<ast::AdtDef>(&syntax, position.offset) { | ||
27 | return Some(RangeInfo::new( | ||
28 | nominal_def.syntax().text_range(), | ||
29 | impls_for_def(&sema, &nominal_def, krate)?, | ||
30 | )); | ||
31 | } else if let Some(trait_def) = find_node_at_offset::<ast::Trait>(&syntax, position.offset) { | ||
32 | return Some(RangeInfo::new( | ||
33 | trait_def.syntax().text_range(), | ||
34 | impls_for_trait(&sema, &trait_def, krate)?, | ||
35 | )); | ||
36 | } | ||
37 | |||
38 | None | ||
39 | } | ||
40 | |||
41 | fn impls_for_def( | ||
42 | sema: &Semantics<RootDatabase>, | ||
43 | node: &ast::AdtDef, | ||
44 | krate: Crate, | ||
45 | ) -> Option<Vec<NavigationTarget>> { | ||
46 | let ty = match node { | ||
47 | ast::AdtDef::Struct(def) => sema.to_def(def)?.ty(sema.db), | ||
48 | ast::AdtDef::Enum(def) => sema.to_def(def)?.ty(sema.db), | ||
49 | ast::AdtDef::Union(def) => sema.to_def(def)?.ty(sema.db), | ||
50 | }; | ||
51 | |||
52 | let impls = ImplDef::all_in_crate(sema.db, krate); | ||
53 | |||
54 | Some( | ||
55 | impls | ||
56 | .into_iter() | ||
57 | .filter(|impl_def| ty.is_equal_for_find_impls(&impl_def.target_ty(sema.db))) | ||
58 | .map(|imp| imp.to_nav(sema.db)) | ||
59 | .collect(), | ||
60 | ) | ||
61 | } | ||
62 | |||
63 | fn impls_for_trait( | ||
64 | sema: &Semantics<RootDatabase>, | ||
65 | node: &ast::Trait, | ||
66 | krate: Crate, | ||
67 | ) -> Option<Vec<NavigationTarget>> { | ||
68 | let tr = sema.to_def(node)?; | ||
69 | |||
70 | let impls = ImplDef::for_trait(sema.db, krate, tr); | ||
71 | |||
72 | Some(impls.into_iter().map(|imp| imp.to_nav(sema.db)).collect()) | ||
73 | } | ||
74 | |||
75 | #[cfg(test)] | ||
76 | mod tests { | ||
77 | use base_db::FileRange; | ||
78 | |||
79 | use crate::mock_analysis::MockAnalysis; | ||
80 | |||
81 | fn check(ra_fixture: &str) { | ||
82 | let (mock, position) = MockAnalysis::with_files_and_position(ra_fixture); | ||
83 | let annotations = mock.annotations(); | ||
84 | let analysis = mock.analysis(); | ||
85 | |||
86 | let navs = analysis.goto_implementation(position).unwrap().unwrap().info; | ||
87 | |||
88 | let key = |frange: &FileRange| (frange.file_id, frange.range.start()); | ||
89 | |||
90 | let mut expected = annotations | ||
91 | .into_iter() | ||
92 | .map(|(range, data)| { | ||
93 | assert!(data.is_empty()); | ||
94 | range | ||
95 | }) | ||
96 | .collect::<Vec<_>>(); | ||
97 | expected.sort_by_key(key); | ||
98 | |||
99 | let mut actual = navs | ||
100 | .into_iter() | ||
101 | .map(|nav| FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }) | ||
102 | .collect::<Vec<_>>(); | ||
103 | actual.sort_by_key(key); | ||
104 | |||
105 | assert_eq!(expected, actual); | ||
106 | } | ||
107 | |||
108 | #[test] | ||
109 | fn goto_implementation_works() { | ||
110 | check( | ||
111 | r#" | ||
112 | struct Foo<|>; | ||
113 | impl Foo {} | ||
114 | //^^^ | ||
115 | "#, | ||
116 | ); | ||
117 | } | ||
118 | |||
119 | #[test] | ||
120 | fn goto_implementation_works_multiple_blocks() { | ||
121 | check( | ||
122 | r#" | ||
123 | struct Foo<|>; | ||
124 | impl Foo {} | ||
125 | //^^^ | ||
126 | impl Foo {} | ||
127 | //^^^ | ||
128 | "#, | ||
129 | ); | ||
130 | } | ||
131 | |||
132 | #[test] | ||
133 | fn goto_implementation_works_multiple_mods() { | ||
134 | check( | ||
135 | r#" | ||
136 | struct Foo<|>; | ||
137 | mod a { | ||
138 | impl super::Foo {} | ||
139 | //^^^^^^^^^^ | ||
140 | } | ||
141 | mod b { | ||
142 | impl super::Foo {} | ||
143 | //^^^^^^^^^^ | ||
144 | } | ||
145 | "#, | ||
146 | ); | ||
147 | } | ||
148 | |||
149 | #[test] | ||
150 | fn goto_implementation_works_multiple_files() { | ||
151 | check( | ||
152 | r#" | ||
153 | //- /lib.rs | ||
154 | struct Foo<|>; | ||
155 | mod a; | ||
156 | mod b; | ||
157 | //- /a.rs | ||
158 | impl crate::Foo {} | ||
159 | //^^^^^^^^^^ | ||
160 | //- /b.rs | ||
161 | impl crate::Foo {} | ||
162 | //^^^^^^^^^^ | ||
163 | "#, | ||
164 | ); | ||
165 | } | ||
166 | |||
167 | #[test] | ||
168 | fn goto_implementation_for_trait() { | ||
169 | check( | ||
170 | r#" | ||
171 | trait T<|> {} | ||
172 | struct Foo; | ||
173 | impl T for Foo {} | ||
174 | //^^^ | ||
175 | "#, | ||
176 | ); | ||
177 | } | ||
178 | |||
179 | #[test] | ||
180 | fn goto_implementation_for_trait_multiple_files() { | ||
181 | check( | ||
182 | r#" | ||
183 | //- /lib.rs | ||
184 | trait T<|> {}; | ||
185 | struct Foo; | ||
186 | mod a; | ||
187 | mod b; | ||
188 | //- /a.rs | ||
189 | impl crate::T for crate::Foo {} | ||
190 | //^^^^^^^^^^ | ||
191 | //- /b.rs | ||
192 | impl crate::T for crate::Foo {} | ||
193 | //^^^^^^^^^^ | ||
194 | "#, | ||
195 | ); | ||
196 | } | ||
197 | |||
198 | #[test] | ||
199 | fn goto_implementation_all_impls() { | ||
200 | check( | ||
201 | r#" | ||
202 | //- /lib.rs | ||
203 | trait T {} | ||
204 | struct Foo<|>; | ||
205 | impl Foo {} | ||
206 | //^^^ | ||
207 | impl T for Foo {} | ||
208 | //^^^ | ||
209 | impl T for &Foo {} | ||
210 | //^^^^ | ||
211 | "#, | ||
212 | ); | ||
213 | } | ||
214 | |||
215 | #[test] | ||
216 | fn goto_implementation_to_builtin_derive() { | ||
217 | check( | ||
218 | r#" | ||
219 | #[derive(Copy)] | ||
220 | //^^^^^^^^^^^^^^^ | ||
221 | struct Foo<|>; | ||
222 | |||
223 | mod marker { | ||
224 | trait Copy {} | ||
225 | } | ||
226 | "#, | ||
227 | ); | ||
228 | } | ||
229 | } | ||
diff --git a/crates/ide/src/goto_type_definition.rs b/crates/ide/src/goto_type_definition.rs new file mode 100644 index 000000000..4a151b150 --- /dev/null +++ b/crates/ide/src/goto_type_definition.rs | |||
@@ -0,0 +1,151 @@ | |||
1 | use ide_db::RootDatabase; | ||
2 | use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; | ||
3 | |||
4 | use crate::{display::ToNav, FilePosition, NavigationTarget, RangeInfo}; | ||
5 | |||
6 | // Feature: Go to Type Definition | ||
7 | // | ||
8 | // Navigates to the type of an identifier. | ||
9 | // | ||
10 | // |=== | ||
11 | // | Editor | Action Name | ||
12 | // | ||
13 | // | VS Code | **Go to Type Definition* | ||
14 | // |=== | ||
15 | pub(crate) fn goto_type_definition( | ||
16 | db: &RootDatabase, | ||
17 | position: FilePosition, | ||
18 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
19 | let sema = hir::Semantics::new(db); | ||
20 | |||
21 | let file: ast::SourceFile = sema.parse(position.file_id); | ||
22 | let token: SyntaxToken = pick_best(file.syntax().token_at_offset(position.offset))?; | ||
23 | let token: SyntaxToken = sema.descend_into_macros(token); | ||
24 | |||
25 | let (ty, node) = sema.ancestors_with_macros(token.parent()).find_map(|node| { | ||
26 | let ty = match_ast! { | ||
27 | match node { | ||
28 | ast::Expr(it) => sema.type_of_expr(&it)?, | ||
29 | ast::Pat(it) => sema.type_of_pat(&it)?, | ||
30 | ast::SelfParam(it) => sema.type_of_self(&it)?, | ||
31 | _ => return None, | ||
32 | } | ||
33 | }; | ||
34 | |||
35 | Some((ty, node)) | ||
36 | })?; | ||
37 | |||
38 | let adt_def = ty.autoderef(db).filter_map(|ty| ty.as_adt()).last()?; | ||
39 | |||
40 | let nav = adt_def.to_nav(db); | ||
41 | Some(RangeInfo::new(node.text_range(), vec![nav])) | ||
42 | } | ||
43 | |||
44 | fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { | ||
45 | return tokens.max_by_key(priority); | ||
46 | fn priority(n: &SyntaxToken) -> usize { | ||
47 | match n.kind() { | ||
48 | IDENT | INT_NUMBER | T![self] => 2, | ||
49 | kind if kind.is_trivia() => 0, | ||
50 | _ => 1, | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | |||
55 | #[cfg(test)] | ||
56 | mod tests { | ||
57 | use base_db::FileRange; | ||
58 | |||
59 | use crate::mock_analysis::MockAnalysis; | ||
60 | |||
61 | fn check(ra_fixture: &str) { | ||
62 | let (mock, position) = MockAnalysis::with_files_and_position(ra_fixture); | ||
63 | let (expected, data) = mock.annotation(); | ||
64 | assert!(data.is_empty()); | ||
65 | let analysis = mock.analysis(); | ||
66 | |||
67 | let mut navs = analysis.goto_type_definition(position).unwrap().unwrap().info; | ||
68 | assert_eq!(navs.len(), 1); | ||
69 | let nav = navs.pop().unwrap(); | ||
70 | assert_eq!(expected, FileRange { file_id: nav.file_id, range: nav.focus_or_full_range() }); | ||
71 | } | ||
72 | |||
73 | #[test] | ||
74 | fn goto_type_definition_works_simple() { | ||
75 | check( | ||
76 | r#" | ||
77 | struct Foo; | ||
78 | //^^^ | ||
79 | fn foo() { | ||
80 | let f: Foo; f<|> | ||
81 | } | ||
82 | "#, | ||
83 | ); | ||
84 | } | ||
85 | |||
86 | #[test] | ||
87 | fn goto_type_definition_works_simple_ref() { | ||
88 | check( | ||
89 | r#" | ||
90 | struct Foo; | ||
91 | //^^^ | ||
92 | fn foo() { | ||
93 | let f: &Foo; f<|> | ||
94 | } | ||
95 | "#, | ||
96 | ); | ||
97 | } | ||
98 | |||
99 | #[test] | ||
100 | fn goto_type_definition_works_through_macro() { | ||
101 | check( | ||
102 | r#" | ||
103 | macro_rules! id { ($($tt:tt)*) => { $($tt)* } } | ||
104 | struct Foo {} | ||
105 | //^^^ | ||
106 | id! { | ||
107 | fn bar() { let f<|> = Foo {}; } | ||
108 | } | ||
109 | "#, | ||
110 | ); | ||
111 | } | ||
112 | |||
113 | #[test] | ||
114 | fn goto_type_definition_for_param() { | ||
115 | check( | ||
116 | r#" | ||
117 | struct Foo; | ||
118 | //^^^ | ||
119 | fn foo(<|>f: Foo) {} | ||
120 | "#, | ||
121 | ); | ||
122 | } | ||
123 | |||
124 | #[test] | ||
125 | fn goto_type_definition_for_tuple_field() { | ||
126 | check( | ||
127 | r#" | ||
128 | struct Foo; | ||
129 | //^^^ | ||
130 | struct Bar(Foo); | ||
131 | fn foo() { | ||
132 | let bar = Bar(Foo); | ||
133 | bar.<|>0; | ||
134 | } | ||
135 | "#, | ||
136 | ); | ||
137 | } | ||
138 | |||
139 | #[test] | ||
140 | fn goto_def_for_self_param() { | ||
141 | check( | ||
142 | r#" | ||
143 | struct Foo; | ||
144 | //^^^ | ||
145 | impl Foo { | ||
146 | fn f(&self<|>) {} | ||
147 | } | ||
148 | "#, | ||
149 | ) | ||
150 | } | ||
151 | } | ||
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs new file mode 100644 index 000000000..331aa4db0 --- /dev/null +++ b/crates/ide/src/hover.rs | |||
@@ -0,0 +1,2461 @@ | |||
1 | use base_db::SourceDatabase; | ||
2 | use hir::{ | ||
3 | Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay, | ||
4 | Module, ModuleDef, ModuleSource, Semantics, | ||
5 | }; | ||
6 | use ide_db::{ | ||
7 | defs::{classify_name, classify_name_ref, Definition}, | ||
8 | RootDatabase, | ||
9 | }; | ||
10 | use itertools::Itertools; | ||
11 | use stdx::format_to; | ||
12 | use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; | ||
13 | use test_utils::mark; | ||
14 | |||
15 | use crate::{ | ||
16 | display::{macro_label, ShortLabel, ToNav, TryToNav}, | ||
17 | markup::Markup, | ||
18 | runnables::runnable, | ||
19 | FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, | ||
20 | }; | ||
21 | |||
22 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
23 | pub struct HoverConfig { | ||
24 | pub implementations: bool, | ||
25 | pub run: bool, | ||
26 | pub debug: bool, | ||
27 | pub goto_type_def: bool, | ||
28 | } | ||
29 | |||
30 | impl Default for HoverConfig { | ||
31 | fn default() -> Self { | ||
32 | Self { implementations: true, run: true, debug: true, goto_type_def: true } | ||
33 | } | ||
34 | } | ||
35 | |||
36 | impl HoverConfig { | ||
37 | pub const NO_ACTIONS: Self = | ||
38 | Self { implementations: false, run: false, debug: false, goto_type_def: false }; | ||
39 | |||
40 | pub fn any(&self) -> bool { | ||
41 | self.implementations || self.runnable() || self.goto_type_def | ||
42 | } | ||
43 | |||
44 | pub fn none(&self) -> bool { | ||
45 | !self.any() | ||
46 | } | ||
47 | |||
48 | pub fn runnable(&self) -> bool { | ||
49 | self.run || self.debug | ||
50 | } | ||
51 | } | ||
52 | |||
53 | #[derive(Debug, Clone)] | ||
54 | pub enum HoverAction { | ||
55 | Runnable(Runnable), | ||
56 | Implementaion(FilePosition), | ||
57 | GoToType(Vec<HoverGotoTypeData>), | ||
58 | } | ||
59 | |||
60 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
61 | pub struct HoverGotoTypeData { | ||
62 | pub mod_path: String, | ||
63 | pub nav: NavigationTarget, | ||
64 | } | ||
65 | |||
66 | /// Contains the results when hovering over an item | ||
67 | #[derive(Debug, Default)] | ||
68 | pub struct HoverResult { | ||
69 | pub markup: Markup, | ||
70 | pub actions: Vec<HoverAction>, | ||
71 | } | ||
72 | |||
73 | // Feature: Hover | ||
74 | // | ||
75 | // Shows additional information, like type of an expression or documentation for definition when "focusing" code. | ||
76 | // Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. | ||
77 | pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeInfo<HoverResult>> { | ||
78 | let sema = Semantics::new(db); | ||
79 | let file = sema.parse(position.file_id).syntax().clone(); | ||
80 | let token = pick_best(file.token_at_offset(position.offset))?; | ||
81 | let token = sema.descend_into_macros(token); | ||
82 | |||
83 | let mut res = HoverResult::default(); | ||
84 | |||
85 | let node = token.parent(); | ||
86 | let definition = match_ast! { | ||
87 | match node { | ||
88 | ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)), | ||
89 | ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)), | ||
90 | _ => None, | ||
91 | } | ||
92 | }; | ||
93 | if let Some(definition) = definition { | ||
94 | if let Some(markup) = hover_for_definition(db, definition) { | ||
95 | res.markup = markup; | ||
96 | if let Some(action) = show_implementations_action(db, definition) { | ||
97 | res.actions.push(action); | ||
98 | } | ||
99 | |||
100 | if let Some(action) = runnable_action(&sema, definition, position.file_id) { | ||
101 | res.actions.push(action); | ||
102 | } | ||
103 | |||
104 | if let Some(action) = goto_type_action(db, definition) { | ||
105 | res.actions.push(action); | ||
106 | } | ||
107 | |||
108 | let range = sema.original_range(&node).range; | ||
109 | return Some(RangeInfo::new(range, res)); | ||
110 | } | ||
111 | } | ||
112 | |||
113 | let node = token | ||
114 | .ancestors() | ||
115 | .find(|n| ast::Expr::cast(n.clone()).is_some() || ast::Pat::cast(n.clone()).is_some())?; | ||
116 | |||
117 | let ty = match_ast! { | ||
118 | match node { | ||
119 | ast::Expr(it) => sema.type_of_expr(&it)?, | ||
120 | ast::Pat(it) => sema.type_of_pat(&it)?, | ||
121 | // If this node is a MACRO_CALL, it means that `descend_into_macros` failed to resolve. | ||
122 | // (e.g expanding a builtin macro). So we give up here. | ||
123 | ast::MacroCall(_it) => return None, | ||
124 | _ => return None, | ||
125 | } | ||
126 | }; | ||
127 | |||
128 | res.markup = Markup::fenced_block(&ty.display(db)); | ||
129 | let range = sema.original_range(&node).range; | ||
130 | Some(RangeInfo::new(range, res)) | ||
131 | } | ||
132 | |||
133 | fn show_implementations_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { | ||
134 | fn to_action(nav_target: NavigationTarget) -> HoverAction { | ||
135 | HoverAction::Implementaion(FilePosition { | ||
136 | file_id: nav_target.file_id, | ||
137 | offset: nav_target.focus_or_full_range().start(), | ||
138 | }) | ||
139 | } | ||
140 | |||
141 | match def { | ||
142 | Definition::ModuleDef(it) => match it { | ||
143 | ModuleDef::Adt(Adt::Struct(it)) => Some(to_action(it.to_nav(db))), | ||
144 | ModuleDef::Adt(Adt::Union(it)) => Some(to_action(it.to_nav(db))), | ||
145 | ModuleDef::Adt(Adt::Enum(it)) => Some(to_action(it.to_nav(db))), | ||
146 | ModuleDef::Trait(it) => Some(to_action(it.to_nav(db))), | ||
147 | _ => None, | ||
148 | }, | ||
149 | _ => None, | ||
150 | } | ||
151 | } | ||
152 | |||
153 | fn runnable_action( | ||
154 | sema: &Semantics<RootDatabase>, | ||
155 | def: Definition, | ||
156 | file_id: FileId, | ||
157 | ) -> Option<HoverAction> { | ||
158 | match def { | ||
159 | Definition::ModuleDef(it) => match it { | ||
160 | ModuleDef::Module(it) => match it.definition_source(sema.db).value { | ||
161 | ModuleSource::Module(it) => runnable(&sema, it.syntax().clone(), file_id) | ||
162 | .map(|it| HoverAction::Runnable(it)), | ||
163 | _ => None, | ||
164 | }, | ||
165 | ModuleDef::Function(it) => { | ||
166 | let src = it.source(sema.db); | ||
167 | if src.file_id != file_id.into() { | ||
168 | mark::hit!(hover_macro_generated_struct_fn_doc_comment); | ||
169 | mark::hit!(hover_macro_generated_struct_fn_doc_attr); | ||
170 | |||
171 | return None; | ||
172 | } | ||
173 | |||
174 | runnable(&sema, src.value.syntax().clone(), file_id) | ||
175 | .map(|it| HoverAction::Runnable(it)) | ||
176 | } | ||
177 | _ => None, | ||
178 | }, | ||
179 | _ => None, | ||
180 | } | ||
181 | } | ||
182 | |||
183 | fn goto_type_action(db: &RootDatabase, def: Definition) -> Option<HoverAction> { | ||
184 | match def { | ||
185 | Definition::Local(it) => { | ||
186 | let mut targets: Vec<ModuleDef> = Vec::new(); | ||
187 | let mut push_new_def = |item: ModuleDef| { | ||
188 | if !targets.contains(&item) { | ||
189 | targets.push(item); | ||
190 | } | ||
191 | }; | ||
192 | |||
193 | it.ty(db).walk(db, |t| { | ||
194 | if let Some(adt) = t.as_adt() { | ||
195 | push_new_def(adt.into()); | ||
196 | } else if let Some(trait_) = t.as_dyn_trait() { | ||
197 | push_new_def(trait_.into()); | ||
198 | } else if let Some(traits) = t.as_impl_traits(db) { | ||
199 | traits.into_iter().for_each(|it| push_new_def(it.into())); | ||
200 | } else if let Some(trait_) = t.as_associated_type_parent_trait(db) { | ||
201 | push_new_def(trait_.into()); | ||
202 | } | ||
203 | }); | ||
204 | |||
205 | let targets = targets | ||
206 | .into_iter() | ||
207 | .filter_map(|it| { | ||
208 | Some(HoverGotoTypeData { | ||
209 | mod_path: render_path( | ||
210 | db, | ||
211 | it.module(db)?, | ||
212 | it.name(db).map(|name| name.to_string()), | ||
213 | ), | ||
214 | nav: it.try_to_nav(db)?, | ||
215 | }) | ||
216 | }) | ||
217 | .collect(); | ||
218 | |||
219 | Some(HoverAction::GoToType(targets)) | ||
220 | } | ||
221 | _ => None, | ||
222 | } | ||
223 | } | ||
224 | |||
225 | fn hover_markup( | ||
226 | docs: Option<String>, | ||
227 | desc: Option<String>, | ||
228 | mod_path: Option<String>, | ||
229 | ) -> Option<Markup> { | ||
230 | match desc { | ||
231 | Some(desc) => { | ||
232 | let mut buf = String::new(); | ||
233 | |||
234 | if let Some(mod_path) = mod_path { | ||
235 | if !mod_path.is_empty() { | ||
236 | format_to!(buf, "```rust\n{}\n```\n\n", mod_path); | ||
237 | } | ||
238 | } | ||
239 | format_to!(buf, "```rust\n{}\n```", desc); | ||
240 | |||
241 | if let Some(doc) = docs { | ||
242 | format_to!(buf, "\n___\n\n{}", doc); | ||
243 | } | ||
244 | Some(buf.into()) | ||
245 | } | ||
246 | None => docs.map(Markup::from), | ||
247 | } | ||
248 | } | ||
249 | |||
250 | fn definition_owner_name(db: &RootDatabase, def: &Definition) -> Option<String> { | ||
251 | match def { | ||
252 | Definition::Field(f) => Some(f.parent_def(db).name(db)), | ||
253 | Definition::Local(l) => l.parent(db).name(db), | ||
254 | Definition::ModuleDef(md) => match md { | ||
255 | ModuleDef::Function(f) => match f.as_assoc_item(db)?.container(db) { | ||
256 | AssocItemContainer::Trait(t) => Some(t.name(db)), | ||
257 | AssocItemContainer::ImplDef(i) => i.target_ty(db).as_adt().map(|adt| adt.name(db)), | ||
258 | }, | ||
259 | ModuleDef::EnumVariant(e) => Some(e.parent_enum(db).name(db)), | ||
260 | _ => None, | ||
261 | }, | ||
262 | Definition::SelfType(i) => i.target_ty(db).as_adt().map(|adt| adt.name(db)), | ||
263 | _ => None, | ||
264 | } | ||
265 | .map(|name| name.to_string()) | ||
266 | } | ||
267 | |||
268 | fn render_path(db: &RootDatabase, module: Module, item_name: Option<String>) -> String { | ||
269 | let crate_name = | ||
270 | db.crate_graph()[module.krate().into()].display_name.as_ref().map(ToString::to_string); | ||
271 | let module_path = module | ||
272 | .path_to_root(db) | ||
273 | .into_iter() | ||
274 | .rev() | ||
275 | .flat_map(|it| it.name(db).map(|name| name.to_string())); | ||
276 | crate_name.into_iter().chain(module_path).chain(item_name).join("::") | ||
277 | } | ||
278 | |||
279 | fn definition_mod_path(db: &RootDatabase, def: &Definition) -> Option<String> { | ||
280 | def.module(db).map(|module| render_path(db, module, definition_owner_name(db, def))) | ||
281 | } | ||
282 | |||
283 | fn hover_for_definition(db: &RootDatabase, def: Definition) -> Option<Markup> { | ||
284 | let mod_path = definition_mod_path(db, &def); | ||
285 | return match def { | ||
286 | Definition::Macro(it) => { | ||
287 | let src = it.source(db); | ||
288 | let docs = Documentation::from_ast(&src.value).map(Into::into); | ||
289 | hover_markup(docs, Some(macro_label(&src.value)), mod_path) | ||
290 | } | ||
291 | Definition::Field(it) => { | ||
292 | let src = it.source(db); | ||
293 | match src.value { | ||
294 | FieldSource::Named(it) => { | ||
295 | let docs = Documentation::from_ast(&it).map(Into::into); | ||
296 | hover_markup(docs, it.short_label(), mod_path) | ||
297 | } | ||
298 | _ => None, | ||
299 | } | ||
300 | } | ||
301 | Definition::ModuleDef(it) => match it { | ||
302 | ModuleDef::Module(it) => match it.definition_source(db).value { | ||
303 | ModuleSource::Module(it) => { | ||
304 | let docs = Documentation::from_ast(&it).map(Into::into); | ||
305 | hover_markup(docs, it.short_label(), mod_path) | ||
306 | } | ||
307 | ModuleSource::SourceFile(it) => { | ||
308 | let docs = Documentation::from_ast(&it).map(Into::into); | ||
309 | hover_markup(docs, it.short_label(), mod_path) | ||
310 | } | ||
311 | }, | ||
312 | ModuleDef::Function(it) => from_def_source(db, it, mod_path), | ||
313 | ModuleDef::Adt(Adt::Struct(it)) => from_def_source(db, it, mod_path), | ||
314 | ModuleDef::Adt(Adt::Union(it)) => from_def_source(db, it, mod_path), | ||
315 | ModuleDef::Adt(Adt::Enum(it)) => from_def_source(db, it, mod_path), | ||
316 | ModuleDef::EnumVariant(it) => from_def_source(db, it, mod_path), | ||
317 | ModuleDef::Const(it) => from_def_source(db, it, mod_path), | ||
318 | ModuleDef::Static(it) => from_def_source(db, it, mod_path), | ||
319 | ModuleDef::Trait(it) => from_def_source(db, it, mod_path), | ||
320 | ModuleDef::TypeAlias(it) => from_def_source(db, it, mod_path), | ||
321 | ModuleDef::BuiltinType(it) => return Some(it.to_string().into()), | ||
322 | }, | ||
323 | Definition::Local(it) => return Some(Markup::fenced_block(&it.ty(db).display(db))), | ||
324 | Definition::TypeParam(_) | Definition::SelfType(_) => { | ||
325 | // FIXME: Hover for generic param | ||
326 | None | ||
327 | } | ||
328 | }; | ||
329 | |||
330 | fn from_def_source<A, D>(db: &RootDatabase, def: D, mod_path: Option<String>) -> Option<Markup> | ||
331 | where | ||
332 | D: HasSource<Ast = A>, | ||
333 | A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel + ast::AttrsOwner, | ||
334 | { | ||
335 | let src = def.source(db); | ||
336 | let docs = Documentation::from_ast(&src.value).map(Into::into); | ||
337 | hover_markup(docs, src.value.short_label(), mod_path) | ||
338 | } | ||
339 | } | ||
340 | |||
341 | fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> { | ||
342 | return tokens.max_by_key(priority); | ||
343 | fn priority(n: &SyntaxToken) -> usize { | ||
344 | match n.kind() { | ||
345 | IDENT | INT_NUMBER => 3, | ||
346 | T!['('] | T![')'] => 2, | ||
347 | kind if kind.is_trivia() => 0, | ||
348 | _ => 1, | ||
349 | } | ||
350 | } | ||
351 | } | ||
352 | |||
353 | #[cfg(test)] | ||
354 | mod tests { | ||
355 | use base_db::FileLoader; | ||
356 | use expect::{expect, Expect}; | ||
357 | |||
358 | use crate::mock_analysis::analysis_and_position; | ||
359 | |||
360 | use super::*; | ||
361 | |||
362 | fn check_hover_no_result(ra_fixture: &str) { | ||
363 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
364 | assert!(analysis.hover(position).unwrap().is_none()); | ||
365 | } | ||
366 | |||
367 | fn check(ra_fixture: &str, expect: Expect) { | ||
368 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
369 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
370 | |||
371 | let content = analysis.db.file_text(position.file_id); | ||
372 | let hovered_element = &content[hover.range]; | ||
373 | |||
374 | let actual = format!("*{}*\n{}\n", hovered_element, hover.info.markup); | ||
375 | expect.assert_eq(&actual) | ||
376 | } | ||
377 | |||
378 | fn check_actions(ra_fixture: &str, expect: Expect) { | ||
379 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
380 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
381 | expect.assert_debug_eq(&hover.info.actions) | ||
382 | } | ||
383 | |||
384 | #[test] | ||
385 | fn hover_shows_type_of_an_expression() { | ||
386 | check( | ||
387 | r#" | ||
388 | pub fn foo() -> u32 { 1 } | ||
389 | |||
390 | fn main() { | ||
391 | let foo_test = foo()<|>; | ||
392 | } | ||
393 | "#, | ||
394 | expect![[r#" | ||
395 | *foo()* | ||
396 | ```rust | ||
397 | u32 | ||
398 | ``` | ||
399 | "#]], | ||
400 | ); | ||
401 | } | ||
402 | |||
403 | #[test] | ||
404 | fn hover_shows_long_type_of_an_expression() { | ||
405 | check( | ||
406 | r#" | ||
407 | struct Scan<A, B, C> { a: A, b: B, c: C } | ||
408 | struct Iter<I> { inner: I } | ||
409 | enum Option<T> { Some(T), None } | ||
410 | |||
411 | struct OtherStruct<T> { i: T } | ||
412 | |||
413 | fn scan<A, B, C>(a: A, b: B, c: C) -> Iter<Scan<OtherStruct<A>, B, C>> { | ||
414 | Iter { inner: Scan { a, b, c } } | ||
415 | } | ||
416 | |||
417 | fn main() { | ||
418 | let num: i32 = 55; | ||
419 | let closure = |memo: &mut u32, value: &u32, _another: &mut u32| -> Option<u32> { | ||
420 | Option::Some(*memo + value) | ||
421 | }; | ||
422 | let number = 5u32; | ||
423 | let mut iter<|> = scan(OtherStruct { i: num }, closure, number); | ||
424 | } | ||
425 | "#, | ||
426 | expect![[r#" | ||
427 | *iter* | ||
428 | ```rust | ||
429 | Iter<Scan<OtherStruct<OtherStruct<i32>>, |&mut u32, &u32, &mut u32| -> Option<u32>, u32>> | ||
430 | ``` | ||
431 | "#]], | ||
432 | ); | ||
433 | } | ||
434 | |||
435 | #[test] | ||
436 | fn hover_shows_fn_signature() { | ||
437 | // Single file with result | ||
438 | check( | ||
439 | r#" | ||
440 | pub fn foo() -> u32 { 1 } | ||
441 | |||
442 | fn main() { let foo_test = fo<|>o(); } | ||
443 | "#, | ||
444 | expect![[r#" | ||
445 | *foo* | ||
446 | ```rust | ||
447 | pub fn foo() -> u32 | ||
448 | ``` | ||
449 | "#]], | ||
450 | ); | ||
451 | |||
452 | // Multiple candidates but results are ambiguous. | ||
453 | check( | ||
454 | r#" | ||
455 | //- /a.rs | ||
456 | pub fn foo() -> u32 { 1 } | ||
457 | |||
458 | //- /b.rs | ||
459 | pub fn foo() -> &str { "" } | ||
460 | |||
461 | //- /c.rs | ||
462 | pub fn foo(a: u32, b: u32) {} | ||
463 | |||
464 | //- /main.rs | ||
465 | mod a; | ||
466 | mod b; | ||
467 | mod c; | ||
468 | |||
469 | fn main() { let foo_test = fo<|>o(); } | ||
470 | "#, | ||
471 | expect![[r#" | ||
472 | *foo* | ||
473 | ```rust | ||
474 | {unknown} | ||
475 | ``` | ||
476 | "#]], | ||
477 | ); | ||
478 | } | ||
479 | |||
480 | #[test] | ||
481 | fn hover_shows_fn_signature_with_type_params() { | ||
482 | check( | ||
483 | r#" | ||
484 | pub fn foo<'a, T: AsRef<str>>(b: &'a T) -> &'a str { } | ||
485 | |||
486 | fn main() { let foo_test = fo<|>o(); } | ||
487 | "#, | ||
488 | expect![[r#" | ||
489 | *foo* | ||
490 | ```rust | ||
491 | pub fn foo<'a, T: AsRef<str>>(b: &'a T) -> &'a str | ||
492 | ``` | ||
493 | "#]], | ||
494 | ); | ||
495 | } | ||
496 | |||
497 | #[test] | ||
498 | fn hover_shows_fn_signature_on_fn_name() { | ||
499 | check( | ||
500 | r#" | ||
501 | pub fn foo<|>(a: u32, b: u32) -> u32 {} | ||
502 | |||
503 | fn main() { } | ||
504 | "#, | ||
505 | expect![[r#" | ||
506 | *foo* | ||
507 | ```rust | ||
508 | pub fn foo(a: u32, b: u32) -> u32 | ||
509 | ``` | ||
510 | "#]], | ||
511 | ); | ||
512 | } | ||
513 | |||
514 | #[test] | ||
515 | fn hover_shows_fn_doc() { | ||
516 | check( | ||
517 | r#" | ||
518 | /// # Example | ||
519 | /// ``` | ||
520 | /// # use std::path::Path; | ||
521 | /// # | ||
522 | /// foo(Path::new("hello, world!")) | ||
523 | /// ``` | ||
524 | pub fn foo<|>(_: &Path) {} | ||
525 | |||
526 | fn main() { } | ||
527 | "#, | ||
528 | expect![[r#" | ||
529 | *foo* | ||
530 | ```rust | ||
531 | pub fn foo(_: &Path) | ||
532 | ``` | ||
533 | ___ | ||
534 | |||
535 | # Example | ||
536 | ``` | ||
537 | # use std::path::Path; | ||
538 | # | ||
539 | foo(Path::new("hello, world!")) | ||
540 | ``` | ||
541 | "#]], | ||
542 | ); | ||
543 | } | ||
544 | |||
545 | #[test] | ||
546 | fn hover_shows_struct_field_info() { | ||
547 | // Hovering over the field when instantiating | ||
548 | check( | ||
549 | r#" | ||
550 | struct Foo { field_a: u32 } | ||
551 | |||
552 | fn main() { | ||
553 | let foo = Foo { field_a<|>: 0, }; | ||
554 | } | ||
555 | "#, | ||
556 | expect![[r#" | ||
557 | *field_a* | ||
558 | ```rust | ||
559 | Foo | ||
560 | ``` | ||
561 | |||
562 | ```rust | ||
563 | field_a: u32 | ||
564 | ``` | ||
565 | "#]], | ||
566 | ); | ||
567 | |||
568 | // Hovering over the field in the definition | ||
569 | check( | ||
570 | r#" | ||
571 | struct Foo { field_a<|>: u32 } | ||
572 | |||
573 | fn main() { | ||
574 | let foo = Foo { field_a: 0 }; | ||
575 | } | ||
576 | "#, | ||
577 | expect![[r#" | ||
578 | *field_a* | ||
579 | ```rust | ||
580 | Foo | ||
581 | ``` | ||
582 | |||
583 | ```rust | ||
584 | field_a: u32 | ||
585 | ``` | ||
586 | "#]], | ||
587 | ); | ||
588 | } | ||
589 | |||
590 | #[test] | ||
591 | fn hover_const_static() { | ||
592 | check( | ||
593 | r#"const foo<|>: u32 = 123;"#, | ||
594 | expect![[r#" | ||
595 | *foo* | ||
596 | ```rust | ||
597 | const foo: u32 = 123 | ||
598 | ``` | ||
599 | "#]], | ||
600 | ); | ||
601 | check( | ||
602 | r#"static foo<|>: u32 = 456;"#, | ||
603 | expect![[r#" | ||
604 | *foo* | ||
605 | ```rust | ||
606 | static foo: u32 | ||
607 | ``` | ||
608 | "#]], | ||
609 | ); | ||
610 | } | ||
611 | |||
612 | #[test] | ||
613 | fn hover_default_generic_types() { | ||
614 | check( | ||
615 | r#" | ||
616 | struct Test<K, T = u8> { k: K, t: T } | ||
617 | |||
618 | fn main() { | ||
619 | let zz<|> = Test { t: 23u8, k: 33 }; | ||
620 | }"#, | ||
621 | expect![[r#" | ||
622 | *zz* | ||
623 | ```rust | ||
624 | Test<i32, u8> | ||
625 | ``` | ||
626 | "#]], | ||
627 | ); | ||
628 | } | ||
629 | |||
630 | #[test] | ||
631 | fn hover_some() { | ||
632 | check( | ||
633 | r#" | ||
634 | enum Option<T> { Some(T) } | ||
635 | use Option::Some; | ||
636 | |||
637 | fn main() { So<|>me(12); } | ||
638 | "#, | ||
639 | expect![[r#" | ||
640 | *Some* | ||
641 | ```rust | ||
642 | Option | ||
643 | ``` | ||
644 | |||
645 | ```rust | ||
646 | Some | ||
647 | ``` | ||
648 | "#]], | ||
649 | ); | ||
650 | |||
651 | check( | ||
652 | r#" | ||
653 | enum Option<T> { Some(T) } | ||
654 | use Option::Some; | ||
655 | |||
656 | fn main() { let b<|>ar = Some(12); } | ||
657 | "#, | ||
658 | expect![[r#" | ||
659 | *bar* | ||
660 | ```rust | ||
661 | Option<i32> | ||
662 | ``` | ||
663 | "#]], | ||
664 | ); | ||
665 | } | ||
666 | |||
667 | #[test] | ||
668 | fn hover_enum_variant() { | ||
669 | check( | ||
670 | r#" | ||
671 | enum Option<T> { | ||
672 | /// The None variant | ||
673 | Non<|>e | ||
674 | } | ||
675 | "#, | ||
676 | expect![[r#" | ||
677 | *None* | ||
678 | ```rust | ||
679 | Option | ||
680 | ``` | ||
681 | |||
682 | ```rust | ||
683 | None | ||
684 | ``` | ||
685 | ___ | ||
686 | |||
687 | The None variant | ||
688 | "#]], | ||
689 | ); | ||
690 | |||
691 | check( | ||
692 | r#" | ||
693 | enum Option<T> { | ||
694 | /// The Some variant | ||
695 | Some(T) | ||
696 | } | ||
697 | fn main() { | ||
698 | let s = Option::Som<|>e(12); | ||
699 | } | ||
700 | "#, | ||
701 | expect![[r#" | ||
702 | *Some* | ||
703 | ```rust | ||
704 | Option | ||
705 | ``` | ||
706 | |||
707 | ```rust | ||
708 | Some | ||
709 | ``` | ||
710 | ___ | ||
711 | |||
712 | The Some variant | ||
713 | "#]], | ||
714 | ); | ||
715 | } | ||
716 | |||
717 | #[test] | ||
718 | fn hover_for_local_variable() { | ||
719 | check( | ||
720 | r#"fn func(foo: i32) { fo<|>o; }"#, | ||
721 | expect![[r#" | ||
722 | *foo* | ||
723 | ```rust | ||
724 | i32 | ||
725 | ``` | ||
726 | "#]], | ||
727 | ) | ||
728 | } | ||
729 | |||
730 | #[test] | ||
731 | fn hover_for_local_variable_pat() { | ||
732 | check( | ||
733 | r#"fn func(fo<|>o: i32) {}"#, | ||
734 | expect![[r#" | ||
735 | *foo* | ||
736 | ```rust | ||
737 | i32 | ||
738 | ``` | ||
739 | "#]], | ||
740 | ) | ||
741 | } | ||
742 | |||
743 | #[test] | ||
744 | fn hover_local_var_edge() { | ||
745 | check( | ||
746 | r#"fn func(foo: i32) { if true { <|>foo; }; }"#, | ||
747 | expect![[r#" | ||
748 | *foo* | ||
749 | ```rust | ||
750 | i32 | ||
751 | ``` | ||
752 | "#]], | ||
753 | ) | ||
754 | } | ||
755 | |||
756 | #[test] | ||
757 | fn hover_for_param_edge() { | ||
758 | check( | ||
759 | r#"fn func(<|>foo: i32) {}"#, | ||
760 | expect![[r#" | ||
761 | *foo* | ||
762 | ```rust | ||
763 | i32 | ||
764 | ``` | ||
765 | "#]], | ||
766 | ) | ||
767 | } | ||
768 | |||
769 | #[test] | ||
770 | fn test_hover_infer_associated_method_result() { | ||
771 | check( | ||
772 | r#" | ||
773 | struct Thing { x: u32 } | ||
774 | |||
775 | impl Thing { | ||
776 | fn new() -> Thing { Thing { x: 0 } } | ||
777 | } | ||
778 | |||
779 | fn main() { let foo_<|>test = Thing::new(); } | ||
780 | "#, | ||
781 | expect![[r#" | ||
782 | *foo_test* | ||
783 | ```rust | ||
784 | Thing | ||
785 | ``` | ||
786 | "#]], | ||
787 | ) | ||
788 | } | ||
789 | |||
790 | #[test] | ||
791 | fn test_hover_infer_associated_method_exact() { | ||
792 | check( | ||
793 | r#" | ||
794 | mod wrapper { | ||
795 | struct Thing { x: u32 } | ||
796 | |||
797 | impl Thing { | ||
798 | fn new() -> Thing { Thing { x: 0 } } | ||
799 | } | ||
800 | } | ||
801 | |||
802 | fn main() { let foo_test = wrapper::Thing::new<|>(); } | ||
803 | "#, | ||
804 | expect![[r#" | ||
805 | *new* | ||
806 | ```rust | ||
807 | wrapper::Thing | ||
808 | ``` | ||
809 | |||
810 | ```rust | ||
811 | fn new() -> Thing | ||
812 | ``` | ||
813 | "#]], | ||
814 | ) | ||
815 | } | ||
816 | |||
817 | #[test] | ||
818 | fn test_hover_infer_associated_const_in_pattern() { | ||
819 | check( | ||
820 | r#" | ||
821 | struct X; | ||
822 | impl X { | ||
823 | const C: u32 = 1; | ||
824 | } | ||
825 | |||
826 | fn main() { | ||
827 | match 1 { | ||
828 | X::C<|> => {}, | ||
829 | 2 => {}, | ||
830 | _ => {} | ||
831 | }; | ||
832 | } | ||
833 | "#, | ||
834 | expect![[r#" | ||
835 | *C* | ||
836 | ```rust | ||
837 | const C: u32 = 1 | ||
838 | ``` | ||
839 | "#]], | ||
840 | ) | ||
841 | } | ||
842 | |||
843 | #[test] | ||
844 | fn test_hover_self() { | ||
845 | check( | ||
846 | r#" | ||
847 | struct Thing { x: u32 } | ||
848 | impl Thing { | ||
849 | fn new() -> Self { Self<|> { x: 0 } } | ||
850 | } | ||
851 | "#, | ||
852 | expect![[r#" | ||
853 | *Self { x: 0 }* | ||
854 | ```rust | ||
855 | Thing | ||
856 | ``` | ||
857 | "#]], | ||
858 | ) | ||
859 | } /* FIXME: revive these tests | ||
860 | let (analysis, position) = analysis_and_position( | ||
861 | " | ||
862 | struct Thing { x: u32 } | ||
863 | impl Thing { | ||
864 | fn new() -> Self<|> { | ||
865 | Self { x: 0 } | ||
866 | } | ||
867 | } | ||
868 | ", | ||
869 | ); | ||
870 | |||
871 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
872 | assert_eq!(trim_markup(&hover.info.markup.as_str()), ("Thing")); | ||
873 | |||
874 | let (analysis, position) = analysis_and_position( | ||
875 | " | ||
876 | enum Thing { A } | ||
877 | impl Thing { | ||
878 | pub fn new() -> Self<|> { | ||
879 | Thing::A | ||
880 | } | ||
881 | } | ||
882 | ", | ||
883 | ); | ||
884 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
885 | assert_eq!(trim_markup(&hover.info.markup.as_str()), ("enum Thing")); | ||
886 | |||
887 | let (analysis, position) = analysis_and_position( | ||
888 | " | ||
889 | enum Thing { A } | ||
890 | impl Thing { | ||
891 | pub fn thing(a: Self<|>) { | ||
892 | } | ||
893 | } | ||
894 | ", | ||
895 | ); | ||
896 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
897 | assert_eq!(trim_markup(&hover.info.markup.as_str()), ("enum Thing")); | ||
898 | */ | ||
899 | |||
900 | #[test] | ||
901 | fn test_hover_shadowing_pat() { | ||
902 | check( | ||
903 | r#" | ||
904 | fn x() {} | ||
905 | |||
906 | fn y() { | ||
907 | let x = 0i32; | ||
908 | x<|>; | ||
909 | } | ||
910 | "#, | ||
911 | expect![[r#" | ||
912 | *x* | ||
913 | ```rust | ||
914 | i32 | ||
915 | ``` | ||
916 | "#]], | ||
917 | ) | ||
918 | } | ||
919 | |||
920 | #[test] | ||
921 | fn test_hover_macro_invocation() { | ||
922 | check( | ||
923 | r#" | ||
924 | macro_rules! foo { () => {} } | ||
925 | |||
926 | fn f() { fo<|>o!(); } | ||
927 | "#, | ||
928 | expect![[r#" | ||
929 | *foo* | ||
930 | ```rust | ||
931 | macro_rules! foo | ||
932 | ``` | ||
933 | "#]], | ||
934 | ) | ||
935 | } | ||
936 | |||
937 | #[test] | ||
938 | fn test_hover_tuple_field() { | ||
939 | check( | ||
940 | r#"struct TS(String, i32<|>);"#, | ||
941 | expect![[r#" | ||
942 | *i32* | ||
943 | i32 | ||
944 | "#]], | ||
945 | ) | ||
946 | } | ||
947 | |||
948 | #[test] | ||
949 | fn test_hover_through_macro() { | ||
950 | check( | ||
951 | r#" | ||
952 | macro_rules! id { ($($tt:tt)*) => { $($tt)* } } | ||
953 | fn foo() {} | ||
954 | id! { | ||
955 | fn bar() { fo<|>o(); } | ||
956 | } | ||
957 | "#, | ||
958 | expect![[r#" | ||
959 | *foo* | ||
960 | ```rust | ||
961 | fn foo() | ||
962 | ``` | ||
963 | "#]], | ||
964 | ); | ||
965 | } | ||
966 | |||
967 | #[test] | ||
968 | fn test_hover_through_expr_in_macro() { | ||
969 | check( | ||
970 | r#" | ||
971 | macro_rules! id { ($($tt:tt)*) => { $($tt)* } } | ||
972 | fn foo(bar:u32) { let a = id!(ba<|>r); } | ||
973 | "#, | ||
974 | expect![[r#" | ||
975 | *bar* | ||
976 | ```rust | ||
977 | u32 | ||
978 | ``` | ||
979 | "#]], | ||
980 | ); | ||
981 | } | ||
982 | |||
983 | #[test] | ||
984 | fn test_hover_through_expr_in_macro_recursive() { | ||
985 | check( | ||
986 | r#" | ||
987 | macro_rules! id_deep { ($($tt:tt)*) => { $($tt)* } } | ||
988 | macro_rules! id { ($($tt:tt)*) => { id_deep!($($tt)*) } } | ||
989 | fn foo(bar:u32) { let a = id!(ba<|>r); } | ||
990 | "#, | ||
991 | expect![[r#" | ||
992 | *bar* | ||
993 | ```rust | ||
994 | u32 | ||
995 | ``` | ||
996 | "#]], | ||
997 | ); | ||
998 | } | ||
999 | |||
1000 | #[test] | ||
1001 | fn test_hover_through_func_in_macro_recursive() { | ||
1002 | check( | ||
1003 | r#" | ||
1004 | macro_rules! id_deep { ($($tt:tt)*) => { $($tt)* } } | ||
1005 | macro_rules! id { ($($tt:tt)*) => { id_deep!($($tt)*) } } | ||
1006 | fn bar() -> u32 { 0 } | ||
1007 | fn foo() { let a = id!([0u32, bar(<|>)] ); } | ||
1008 | "#, | ||
1009 | expect![[r#" | ||
1010 | *bar()* | ||
1011 | ```rust | ||
1012 | u32 | ||
1013 | ``` | ||
1014 | "#]], | ||
1015 | ); | ||
1016 | } | ||
1017 | |||
1018 | #[test] | ||
1019 | fn test_hover_through_literal_string_in_macro() { | ||
1020 | check( | ||
1021 | r#" | ||
1022 | macro_rules! arr { ($($tt:tt)*) => { [$($tt)*)] } } | ||
1023 | fn foo() { | ||
1024 | let mastered_for_itunes = ""; | ||
1025 | let _ = arr!("Tr<|>acks", &mastered_for_itunes); | ||
1026 | } | ||
1027 | "#, | ||
1028 | expect![[r#" | ||
1029 | *"Tracks"* | ||
1030 | ```rust | ||
1031 | &str | ||
1032 | ``` | ||
1033 | "#]], | ||
1034 | ); | ||
1035 | } | ||
1036 | |||
1037 | #[test] | ||
1038 | fn test_hover_through_assert_macro() { | ||
1039 | check( | ||
1040 | r#" | ||
1041 | #[rustc_builtin_macro] | ||
1042 | macro_rules! assert {} | ||
1043 | |||
1044 | fn bar() -> bool { true } | ||
1045 | fn foo() { | ||
1046 | assert!(ba<|>r()); | ||
1047 | } | ||
1048 | "#, | ||
1049 | expect![[r#" | ||
1050 | *bar* | ||
1051 | ```rust | ||
1052 | fn bar() -> bool | ||
1053 | ``` | ||
1054 | "#]], | ||
1055 | ); | ||
1056 | } | ||
1057 | |||
1058 | #[test] | ||
1059 | fn test_hover_through_literal_string_in_builtin_macro() { | ||
1060 | check_hover_no_result( | ||
1061 | r#" | ||
1062 | #[rustc_builtin_macro] | ||
1063 | macro_rules! format {} | ||
1064 | |||
1065 | fn foo() { | ||
1066 | format!("hel<|>lo {}", 0); | ||
1067 | } | ||
1068 | "#, | ||
1069 | ); | ||
1070 | } | ||
1071 | |||
1072 | #[test] | ||
1073 | fn test_hover_non_ascii_space_doc() { | ||
1074 | check( | ||
1075 | " | ||
1076 | /// <- `\u{3000}` here | ||
1077 | fn foo() { } | ||
1078 | |||
1079 | fn bar() { fo<|>o(); } | ||
1080 | ", | ||
1081 | expect![[r#" | ||
1082 | *foo* | ||
1083 | ```rust | ||
1084 | fn foo() | ||
1085 | ``` | ||
1086 | ___ | ||
1087 | |||
1088 | <- ` ` here | ||
1089 | "#]], | ||
1090 | ); | ||
1091 | } | ||
1092 | |||
1093 | #[test] | ||
1094 | fn test_hover_function_show_qualifiers() { | ||
1095 | check( | ||
1096 | r#"async fn foo<|>() {}"#, | ||
1097 | expect![[r#" | ||
1098 | *foo* | ||
1099 | ```rust | ||
1100 | async fn foo() | ||
1101 | ``` | ||
1102 | "#]], | ||
1103 | ); | ||
1104 | check( | ||
1105 | r#"pub const unsafe fn foo<|>() {}"#, | ||
1106 | expect![[r#" | ||
1107 | *foo* | ||
1108 | ```rust | ||
1109 | pub const unsafe fn foo() | ||
1110 | ``` | ||
1111 | "#]], | ||
1112 | ); | ||
1113 | check( | ||
1114 | r#"pub(crate) async unsafe extern "C" fn foo<|>() {}"#, | ||
1115 | expect![[r#" | ||
1116 | *foo* | ||
1117 | ```rust | ||
1118 | pub(crate) async unsafe extern "C" fn foo() | ||
1119 | ``` | ||
1120 | "#]], | ||
1121 | ); | ||
1122 | } | ||
1123 | |||
1124 | #[test] | ||
1125 | fn test_hover_trait_show_qualifiers() { | ||
1126 | check_actions( | ||
1127 | r"unsafe trait foo<|>() {}", | ||
1128 | expect![[r#" | ||
1129 | [ | ||
1130 | Implementaion( | ||
1131 | FilePosition { | ||
1132 | file_id: FileId( | ||
1133 | 1, | ||
1134 | ), | ||
1135 | offset: 13, | ||
1136 | }, | ||
1137 | ), | ||
1138 | ] | ||
1139 | "#]], | ||
1140 | ); | ||
1141 | } | ||
1142 | |||
1143 | #[test] | ||
1144 | fn test_hover_extern_crate() { | ||
1145 | check( | ||
1146 | r#" | ||
1147 | //- /main.rs | ||
1148 | extern crate st<|>d; | ||
1149 | //- /std/lib.rs | ||
1150 | //! Standard library for this test | ||
1151 | //! | ||
1152 | //! Printed? | ||
1153 | //! abc123 | ||
1154 | "#, | ||
1155 | expect![[r#" | ||
1156 | *std* | ||
1157 | Standard library for this test | ||
1158 | |||
1159 | Printed? | ||
1160 | abc123 | ||
1161 | "#]], | ||
1162 | ); | ||
1163 | check( | ||
1164 | r#" | ||
1165 | //- /main.rs | ||
1166 | extern crate std as ab<|>c; | ||
1167 | //- /std/lib.rs | ||
1168 | //! Standard library for this test | ||
1169 | //! | ||
1170 | //! Printed? | ||
1171 | //! abc123 | ||
1172 | "#, | ||
1173 | expect![[r#" | ||
1174 | *abc* | ||
1175 | Standard library for this test | ||
1176 | |||
1177 | Printed? | ||
1178 | abc123 | ||
1179 | "#]], | ||
1180 | ); | ||
1181 | } | ||
1182 | |||
1183 | #[test] | ||
1184 | fn test_hover_mod_with_same_name_as_function() { | ||
1185 | check( | ||
1186 | r#" | ||
1187 | use self::m<|>y::Bar; | ||
1188 | mod my { pub struct Bar; } | ||
1189 | |||
1190 | fn my() {} | ||
1191 | "#, | ||
1192 | expect![[r#" | ||
1193 | *my* | ||
1194 | ```rust | ||
1195 | mod my | ||
1196 | ``` | ||
1197 | "#]], | ||
1198 | ); | ||
1199 | } | ||
1200 | |||
1201 | #[test] | ||
1202 | fn test_hover_struct_doc_comment() { | ||
1203 | check( | ||
1204 | r#" | ||
1205 | /// bar docs | ||
1206 | struct Bar; | ||
1207 | |||
1208 | fn foo() { let bar = Ba<|>r; } | ||
1209 | "#, | ||
1210 | expect![[r#" | ||
1211 | *Bar* | ||
1212 | ```rust | ||
1213 | struct Bar | ||
1214 | ``` | ||
1215 | ___ | ||
1216 | |||
1217 | bar docs | ||
1218 | "#]], | ||
1219 | ); | ||
1220 | } | ||
1221 | |||
1222 | #[test] | ||
1223 | fn test_hover_struct_doc_attr() { | ||
1224 | check( | ||
1225 | r#" | ||
1226 | #[doc = "bar docs"] | ||
1227 | struct Bar; | ||
1228 | |||
1229 | fn foo() { let bar = Ba<|>r; } | ||
1230 | "#, | ||
1231 | expect![[r#" | ||
1232 | *Bar* | ||
1233 | ```rust | ||
1234 | struct Bar | ||
1235 | ``` | ||
1236 | ___ | ||
1237 | |||
1238 | bar docs | ||
1239 | "#]], | ||
1240 | ); | ||
1241 | } | ||
1242 | |||
1243 | #[test] | ||
1244 | fn test_hover_struct_doc_attr_multiple_and_mixed() { | ||
1245 | check( | ||
1246 | r#" | ||
1247 | /// bar docs 0 | ||
1248 | #[doc = "bar docs 1"] | ||
1249 | #[doc = "bar docs 2"] | ||
1250 | struct Bar; | ||
1251 | |||
1252 | fn foo() { let bar = Ba<|>r; } | ||
1253 | "#, | ||
1254 | expect![[r#" | ||
1255 | *Bar* | ||
1256 | ```rust | ||
1257 | struct Bar | ||
1258 | ``` | ||
1259 | ___ | ||
1260 | |||
1261 | bar docs 0 | ||
1262 | |||
1263 | bar docs 1 | ||
1264 | |||
1265 | bar docs 2 | ||
1266 | "#]], | ||
1267 | ); | ||
1268 | } | ||
1269 | |||
1270 | #[test] | ||
1271 | fn test_hover_macro_generated_struct_fn_doc_comment() { | ||
1272 | mark::check!(hover_macro_generated_struct_fn_doc_comment); | ||
1273 | |||
1274 | check( | ||
1275 | r#" | ||
1276 | macro_rules! bar { | ||
1277 | () => { | ||
1278 | struct Bar; | ||
1279 | impl Bar { | ||
1280 | /// Do the foo | ||
1281 | fn foo(&self) {} | ||
1282 | } | ||
1283 | } | ||
1284 | } | ||
1285 | |||
1286 | bar!(); | ||
1287 | |||
1288 | fn foo() { let bar = Bar; bar.fo<|>o(); } | ||
1289 | "#, | ||
1290 | expect![[r#" | ||
1291 | *foo* | ||
1292 | ```rust | ||
1293 | Bar | ||
1294 | ``` | ||
1295 | |||
1296 | ```rust | ||
1297 | fn foo(&self) | ||
1298 | ``` | ||
1299 | ___ | ||
1300 | |||
1301 | Do the foo | ||
1302 | "#]], | ||
1303 | ); | ||
1304 | } | ||
1305 | |||
1306 | #[test] | ||
1307 | fn test_hover_macro_generated_struct_fn_doc_attr() { | ||
1308 | mark::check!(hover_macro_generated_struct_fn_doc_attr); | ||
1309 | |||
1310 | check( | ||
1311 | r#" | ||
1312 | macro_rules! bar { | ||
1313 | () => { | ||
1314 | struct Bar; | ||
1315 | impl Bar { | ||
1316 | #[doc = "Do the foo"] | ||
1317 | fn foo(&self) {} | ||
1318 | } | ||
1319 | } | ||
1320 | } | ||
1321 | |||
1322 | bar!(); | ||
1323 | |||
1324 | fn foo() { let bar = Bar; bar.fo<|>o(); } | ||
1325 | "#, | ||
1326 | expect![[r#" | ||
1327 | *foo* | ||
1328 | ```rust | ||
1329 | Bar | ||
1330 | ``` | ||
1331 | |||
1332 | ```rust | ||
1333 | fn foo(&self) | ||
1334 | ``` | ||
1335 | ___ | ||
1336 | |||
1337 | Do the foo | ||
1338 | "#]], | ||
1339 | ); | ||
1340 | } | ||
1341 | |||
1342 | #[test] | ||
1343 | fn test_hover_trait_has_impl_action() { | ||
1344 | check_actions( | ||
1345 | r#"trait foo<|>() {}"#, | ||
1346 | expect![[r#" | ||
1347 | [ | ||
1348 | Implementaion( | ||
1349 | FilePosition { | ||
1350 | file_id: FileId( | ||
1351 | 1, | ||
1352 | ), | ||
1353 | offset: 6, | ||
1354 | }, | ||
1355 | ), | ||
1356 | ] | ||
1357 | "#]], | ||
1358 | ); | ||
1359 | } | ||
1360 | |||
1361 | #[test] | ||
1362 | fn test_hover_struct_has_impl_action() { | ||
1363 | check_actions( | ||
1364 | r"struct foo<|>() {}", | ||
1365 | expect![[r#" | ||
1366 | [ | ||
1367 | Implementaion( | ||
1368 | FilePosition { | ||
1369 | file_id: FileId( | ||
1370 | 1, | ||
1371 | ), | ||
1372 | offset: 7, | ||
1373 | }, | ||
1374 | ), | ||
1375 | ] | ||
1376 | "#]], | ||
1377 | ); | ||
1378 | } | ||
1379 | |||
1380 | #[test] | ||
1381 | fn test_hover_union_has_impl_action() { | ||
1382 | check_actions( | ||
1383 | r#"union foo<|>() {}"#, | ||
1384 | expect![[r#" | ||
1385 | [ | ||
1386 | Implementaion( | ||
1387 | FilePosition { | ||
1388 | file_id: FileId( | ||
1389 | 1, | ||
1390 | ), | ||
1391 | offset: 6, | ||
1392 | }, | ||
1393 | ), | ||
1394 | ] | ||
1395 | "#]], | ||
1396 | ); | ||
1397 | } | ||
1398 | |||
1399 | #[test] | ||
1400 | fn test_hover_enum_has_impl_action() { | ||
1401 | check_actions( | ||
1402 | r"enum foo<|>() { A, B }", | ||
1403 | expect![[r#" | ||
1404 | [ | ||
1405 | Implementaion( | ||
1406 | FilePosition { | ||
1407 | file_id: FileId( | ||
1408 | 1, | ||
1409 | ), | ||
1410 | offset: 5, | ||
1411 | }, | ||
1412 | ), | ||
1413 | ] | ||
1414 | "#]], | ||
1415 | ); | ||
1416 | } | ||
1417 | |||
1418 | #[test] | ||
1419 | fn test_hover_test_has_action() { | ||
1420 | check_actions( | ||
1421 | r#" | ||
1422 | #[test] | ||
1423 | fn foo_<|>test() {} | ||
1424 | "#, | ||
1425 | expect![[r#" | ||
1426 | [ | ||
1427 | Runnable( | ||
1428 | Runnable { | ||
1429 | nav: NavigationTarget { | ||
1430 | file_id: FileId( | ||
1431 | 1, | ||
1432 | ), | ||
1433 | full_range: 0..24, | ||
1434 | focus_range: Some( | ||
1435 | 11..19, | ||
1436 | ), | ||
1437 | name: "foo_test", | ||
1438 | kind: FN, | ||
1439 | container_name: None, | ||
1440 | description: None, | ||
1441 | docs: None, | ||
1442 | }, | ||
1443 | kind: Test { | ||
1444 | test_id: Path( | ||
1445 | "foo_test", | ||
1446 | ), | ||
1447 | attr: TestAttr { | ||
1448 | ignore: false, | ||
1449 | }, | ||
1450 | }, | ||
1451 | cfg_exprs: [], | ||
1452 | }, | ||
1453 | ), | ||
1454 | ] | ||
1455 | "#]], | ||
1456 | ); | ||
1457 | } | ||
1458 | |||
1459 | #[test] | ||
1460 | fn test_hover_test_mod_has_action() { | ||
1461 | check_actions( | ||
1462 | r#" | ||
1463 | mod tests<|> { | ||
1464 | #[test] | ||
1465 | fn foo_test() {} | ||
1466 | } | ||
1467 | "#, | ||
1468 | expect![[r#" | ||
1469 | [ | ||
1470 | Runnable( | ||
1471 | Runnable { | ||
1472 | nav: NavigationTarget { | ||
1473 | file_id: FileId( | ||
1474 | 1, | ||
1475 | ), | ||
1476 | full_range: 0..46, | ||
1477 | focus_range: Some( | ||
1478 | 4..9, | ||
1479 | ), | ||
1480 | name: "tests", | ||
1481 | kind: MODULE, | ||
1482 | container_name: None, | ||
1483 | description: None, | ||
1484 | docs: None, | ||
1485 | }, | ||
1486 | kind: TestMod { | ||
1487 | path: "tests", | ||
1488 | }, | ||
1489 | cfg_exprs: [], | ||
1490 | }, | ||
1491 | ), | ||
1492 | ] | ||
1493 | "#]], | ||
1494 | ); | ||
1495 | } | ||
1496 | |||
1497 | #[test] | ||
1498 | fn test_hover_struct_has_goto_type_action() { | ||
1499 | check_actions( | ||
1500 | r#" | ||
1501 | struct S{ f1: u32 } | ||
1502 | |||
1503 | fn main() { let s<|>t = S{ f1:0 }; } | ||
1504 | "#, | ||
1505 | expect![[r#" | ||
1506 | [ | ||
1507 | GoToType( | ||
1508 | [ | ||
1509 | HoverGotoTypeData { | ||
1510 | mod_path: "S", | ||
1511 | nav: NavigationTarget { | ||
1512 | file_id: FileId( | ||
1513 | 1, | ||
1514 | ), | ||
1515 | full_range: 0..19, | ||
1516 | focus_range: Some( | ||
1517 | 7..8, | ||
1518 | ), | ||
1519 | name: "S", | ||
1520 | kind: STRUCT, | ||
1521 | container_name: None, | ||
1522 | description: Some( | ||
1523 | "struct S", | ||
1524 | ), | ||
1525 | docs: None, | ||
1526 | }, | ||
1527 | }, | ||
1528 | ], | ||
1529 | ), | ||
1530 | ] | ||
1531 | "#]], | ||
1532 | ); | ||
1533 | } | ||
1534 | |||
1535 | #[test] | ||
1536 | fn test_hover_generic_struct_has_goto_type_actions() { | ||
1537 | check_actions( | ||
1538 | r#" | ||
1539 | struct Arg(u32); | ||
1540 | struct S<T>{ f1: T } | ||
1541 | |||
1542 | fn main() { let s<|>t = S{ f1:Arg(0) }; } | ||
1543 | "#, | ||
1544 | expect![[r#" | ||
1545 | [ | ||
1546 | GoToType( | ||
1547 | [ | ||
1548 | HoverGotoTypeData { | ||
1549 | mod_path: "S", | ||
1550 | nav: NavigationTarget { | ||
1551 | file_id: FileId( | ||
1552 | 1, | ||
1553 | ), | ||
1554 | full_range: 17..37, | ||
1555 | focus_range: Some( | ||
1556 | 24..25, | ||
1557 | ), | ||
1558 | name: "S", | ||
1559 | kind: STRUCT, | ||
1560 | container_name: None, | ||
1561 | description: Some( | ||
1562 | "struct S", | ||
1563 | ), | ||
1564 | docs: None, | ||
1565 | }, | ||
1566 | }, | ||
1567 | HoverGotoTypeData { | ||
1568 | mod_path: "Arg", | ||
1569 | nav: NavigationTarget { | ||
1570 | file_id: FileId( | ||
1571 | 1, | ||
1572 | ), | ||
1573 | full_range: 0..16, | ||
1574 | focus_range: Some( | ||
1575 | 7..10, | ||
1576 | ), | ||
1577 | name: "Arg", | ||
1578 | kind: STRUCT, | ||
1579 | container_name: None, | ||
1580 | description: Some( | ||
1581 | "struct Arg", | ||
1582 | ), | ||
1583 | docs: None, | ||
1584 | }, | ||
1585 | }, | ||
1586 | ], | ||
1587 | ), | ||
1588 | ] | ||
1589 | "#]], | ||
1590 | ); | ||
1591 | } | ||
1592 | |||
1593 | #[test] | ||
1594 | fn test_hover_generic_struct_has_flattened_goto_type_actions() { | ||
1595 | check_actions( | ||
1596 | r#" | ||
1597 | struct Arg(u32); | ||
1598 | struct S<T>{ f1: T } | ||
1599 | |||
1600 | fn main() { let s<|>t = S{ f1: S{ f1: Arg(0) } }; } | ||
1601 | "#, | ||
1602 | expect![[r#" | ||
1603 | [ | ||
1604 | GoToType( | ||
1605 | [ | ||
1606 | HoverGotoTypeData { | ||
1607 | mod_path: "S", | ||
1608 | nav: NavigationTarget { | ||
1609 | file_id: FileId( | ||
1610 | 1, | ||
1611 | ), | ||
1612 | full_range: 17..37, | ||
1613 | focus_range: Some( | ||
1614 | 24..25, | ||
1615 | ), | ||
1616 | name: "S", | ||
1617 | kind: STRUCT, | ||
1618 | container_name: None, | ||
1619 | description: Some( | ||
1620 | "struct S", | ||
1621 | ), | ||
1622 | docs: None, | ||
1623 | }, | ||
1624 | }, | ||
1625 | HoverGotoTypeData { | ||
1626 | mod_path: "Arg", | ||
1627 | nav: NavigationTarget { | ||
1628 | file_id: FileId( | ||
1629 | 1, | ||
1630 | ), | ||
1631 | full_range: 0..16, | ||
1632 | focus_range: Some( | ||
1633 | 7..10, | ||
1634 | ), | ||
1635 | name: "Arg", | ||
1636 | kind: STRUCT, | ||
1637 | container_name: None, | ||
1638 | description: Some( | ||
1639 | "struct Arg", | ||
1640 | ), | ||
1641 | docs: None, | ||
1642 | }, | ||
1643 | }, | ||
1644 | ], | ||
1645 | ), | ||
1646 | ] | ||
1647 | "#]], | ||
1648 | ); | ||
1649 | } | ||
1650 | |||
1651 | #[test] | ||
1652 | fn test_hover_tuple_has_goto_type_actions() { | ||
1653 | check_actions( | ||
1654 | r#" | ||
1655 | struct A(u32); | ||
1656 | struct B(u32); | ||
1657 | mod M { | ||
1658 | pub struct C(u32); | ||
1659 | } | ||
1660 | |||
1661 | fn main() { let s<|>t = (A(1), B(2), M::C(3) ); } | ||
1662 | "#, | ||
1663 | expect![[r#" | ||
1664 | [ | ||
1665 | GoToType( | ||
1666 | [ | ||
1667 | HoverGotoTypeData { | ||
1668 | mod_path: "A", | ||
1669 | nav: NavigationTarget { | ||
1670 | file_id: FileId( | ||
1671 | 1, | ||
1672 | ), | ||
1673 | full_range: 0..14, | ||
1674 | focus_range: Some( | ||
1675 | 7..8, | ||
1676 | ), | ||
1677 | name: "A", | ||
1678 | kind: STRUCT, | ||
1679 | container_name: None, | ||
1680 | description: Some( | ||
1681 | "struct A", | ||
1682 | ), | ||
1683 | docs: None, | ||
1684 | }, | ||
1685 | }, | ||
1686 | HoverGotoTypeData { | ||
1687 | mod_path: "B", | ||
1688 | nav: NavigationTarget { | ||
1689 | file_id: FileId( | ||
1690 | 1, | ||
1691 | ), | ||
1692 | full_range: 15..29, | ||
1693 | focus_range: Some( | ||
1694 | 22..23, | ||
1695 | ), | ||
1696 | name: "B", | ||
1697 | kind: STRUCT, | ||
1698 | container_name: None, | ||
1699 | description: Some( | ||
1700 | "struct B", | ||
1701 | ), | ||
1702 | docs: None, | ||
1703 | }, | ||
1704 | }, | ||
1705 | HoverGotoTypeData { | ||
1706 | mod_path: "M::C", | ||
1707 | nav: NavigationTarget { | ||
1708 | file_id: FileId( | ||
1709 | 1, | ||
1710 | ), | ||
1711 | full_range: 42..60, | ||
1712 | focus_range: Some( | ||
1713 | 53..54, | ||
1714 | ), | ||
1715 | name: "C", | ||
1716 | kind: STRUCT, | ||
1717 | container_name: None, | ||
1718 | description: Some( | ||
1719 | "pub struct C", | ||
1720 | ), | ||
1721 | docs: None, | ||
1722 | }, | ||
1723 | }, | ||
1724 | ], | ||
1725 | ), | ||
1726 | ] | ||
1727 | "#]], | ||
1728 | ); | ||
1729 | } | ||
1730 | |||
1731 | #[test] | ||
1732 | fn test_hover_return_impl_trait_has_goto_type_action() { | ||
1733 | check_actions( | ||
1734 | r#" | ||
1735 | trait Foo {} | ||
1736 | fn foo() -> impl Foo {} | ||
1737 | |||
1738 | fn main() { let s<|>t = foo(); } | ||
1739 | "#, | ||
1740 | expect![[r#" | ||
1741 | [ | ||
1742 | GoToType( | ||
1743 | [ | ||
1744 | HoverGotoTypeData { | ||
1745 | mod_path: "Foo", | ||
1746 | nav: NavigationTarget { | ||
1747 | file_id: FileId( | ||
1748 | 1, | ||
1749 | ), | ||
1750 | full_range: 0..12, | ||
1751 | focus_range: Some( | ||
1752 | 6..9, | ||
1753 | ), | ||
1754 | name: "Foo", | ||
1755 | kind: TRAIT, | ||
1756 | container_name: None, | ||
1757 | description: Some( | ||
1758 | "trait Foo", | ||
1759 | ), | ||
1760 | docs: None, | ||
1761 | }, | ||
1762 | }, | ||
1763 | ], | ||
1764 | ), | ||
1765 | ] | ||
1766 | "#]], | ||
1767 | ); | ||
1768 | } | ||
1769 | |||
1770 | #[test] | ||
1771 | fn test_hover_generic_return_impl_trait_has_goto_type_action() { | ||
1772 | check_actions( | ||
1773 | r#" | ||
1774 | trait Foo<T> {} | ||
1775 | struct S; | ||
1776 | fn foo() -> impl Foo<S> {} | ||
1777 | |||
1778 | fn main() { let s<|>t = foo(); } | ||
1779 | "#, | ||
1780 | expect![[r#" | ||
1781 | [ | ||
1782 | GoToType( | ||
1783 | [ | ||
1784 | HoverGotoTypeData { | ||
1785 | mod_path: "Foo", | ||
1786 | nav: NavigationTarget { | ||
1787 | file_id: FileId( | ||
1788 | 1, | ||
1789 | ), | ||
1790 | full_range: 0..15, | ||
1791 | focus_range: Some( | ||
1792 | 6..9, | ||
1793 | ), | ||
1794 | name: "Foo", | ||
1795 | kind: TRAIT, | ||
1796 | container_name: None, | ||
1797 | description: Some( | ||
1798 | "trait Foo", | ||
1799 | ), | ||
1800 | docs: None, | ||
1801 | }, | ||
1802 | }, | ||
1803 | HoverGotoTypeData { | ||
1804 | mod_path: "S", | ||
1805 | nav: NavigationTarget { | ||
1806 | file_id: FileId( | ||
1807 | 1, | ||
1808 | ), | ||
1809 | full_range: 16..25, | ||
1810 | focus_range: Some( | ||
1811 | 23..24, | ||
1812 | ), | ||
1813 | name: "S", | ||
1814 | kind: STRUCT, | ||
1815 | container_name: None, | ||
1816 | description: Some( | ||
1817 | "struct S", | ||
1818 | ), | ||
1819 | docs: None, | ||
1820 | }, | ||
1821 | }, | ||
1822 | ], | ||
1823 | ), | ||
1824 | ] | ||
1825 | "#]], | ||
1826 | ); | ||
1827 | } | ||
1828 | |||
1829 | #[test] | ||
1830 | fn test_hover_return_impl_traits_has_goto_type_action() { | ||
1831 | check_actions( | ||
1832 | r#" | ||
1833 | trait Foo {} | ||
1834 | trait Bar {} | ||
1835 | fn foo() -> impl Foo + Bar {} | ||
1836 | |||
1837 | fn main() { let s<|>t = foo(); } | ||
1838 | "#, | ||
1839 | expect![[r#" | ||
1840 | [ | ||
1841 | GoToType( | ||
1842 | [ | ||
1843 | HoverGotoTypeData { | ||
1844 | mod_path: "Foo", | ||
1845 | nav: NavigationTarget { | ||
1846 | file_id: FileId( | ||
1847 | 1, | ||
1848 | ), | ||
1849 | full_range: 0..12, | ||
1850 | focus_range: Some( | ||
1851 | 6..9, | ||
1852 | ), | ||
1853 | name: "Foo", | ||
1854 | kind: TRAIT, | ||
1855 | container_name: None, | ||
1856 | description: Some( | ||
1857 | "trait Foo", | ||
1858 | ), | ||
1859 | docs: None, | ||
1860 | }, | ||
1861 | }, | ||
1862 | HoverGotoTypeData { | ||
1863 | mod_path: "Bar", | ||
1864 | nav: NavigationTarget { | ||
1865 | file_id: FileId( | ||
1866 | 1, | ||
1867 | ), | ||
1868 | full_range: 13..25, | ||
1869 | focus_range: Some( | ||
1870 | 19..22, | ||
1871 | ), | ||
1872 | name: "Bar", | ||
1873 | kind: TRAIT, | ||
1874 | container_name: None, | ||
1875 | description: Some( | ||
1876 | "trait Bar", | ||
1877 | ), | ||
1878 | docs: None, | ||
1879 | }, | ||
1880 | }, | ||
1881 | ], | ||
1882 | ), | ||
1883 | ] | ||
1884 | "#]], | ||
1885 | ); | ||
1886 | } | ||
1887 | |||
1888 | #[test] | ||
1889 | fn test_hover_generic_return_impl_traits_has_goto_type_action() { | ||
1890 | check_actions( | ||
1891 | r#" | ||
1892 | trait Foo<T> {} | ||
1893 | trait Bar<T> {} | ||
1894 | struct S1 {} | ||
1895 | struct S2 {} | ||
1896 | |||
1897 | fn foo() -> impl Foo<S1> + Bar<S2> {} | ||
1898 | |||
1899 | fn main() { let s<|>t = foo(); } | ||
1900 | "#, | ||
1901 | expect![[r#" | ||
1902 | [ | ||
1903 | GoToType( | ||
1904 | [ | ||
1905 | HoverGotoTypeData { | ||
1906 | mod_path: "Foo", | ||
1907 | nav: NavigationTarget { | ||
1908 | file_id: FileId( | ||
1909 | 1, | ||
1910 | ), | ||
1911 | full_range: 0..15, | ||
1912 | focus_range: Some( | ||
1913 | 6..9, | ||
1914 | ), | ||
1915 | name: "Foo", | ||
1916 | kind: TRAIT, | ||
1917 | container_name: None, | ||
1918 | description: Some( | ||
1919 | "trait Foo", | ||
1920 | ), | ||
1921 | docs: None, | ||
1922 | }, | ||
1923 | }, | ||
1924 | HoverGotoTypeData { | ||
1925 | mod_path: "Bar", | ||
1926 | nav: NavigationTarget { | ||
1927 | file_id: FileId( | ||
1928 | 1, | ||
1929 | ), | ||
1930 | full_range: 16..31, | ||
1931 | focus_range: Some( | ||
1932 | 22..25, | ||
1933 | ), | ||
1934 | name: "Bar", | ||
1935 | kind: TRAIT, | ||
1936 | container_name: None, | ||
1937 | description: Some( | ||
1938 | "trait Bar", | ||
1939 | ), | ||
1940 | docs: None, | ||
1941 | }, | ||
1942 | }, | ||
1943 | HoverGotoTypeData { | ||
1944 | mod_path: "S1", | ||
1945 | nav: NavigationTarget { | ||
1946 | file_id: FileId( | ||
1947 | 1, | ||
1948 | ), | ||
1949 | full_range: 32..44, | ||
1950 | focus_range: Some( | ||
1951 | 39..41, | ||
1952 | ), | ||
1953 | name: "S1", | ||
1954 | kind: STRUCT, | ||
1955 | container_name: None, | ||
1956 | description: Some( | ||
1957 | "struct S1", | ||
1958 | ), | ||
1959 | docs: None, | ||
1960 | }, | ||
1961 | }, | ||
1962 | HoverGotoTypeData { | ||
1963 | mod_path: "S2", | ||
1964 | nav: NavigationTarget { | ||
1965 | file_id: FileId( | ||
1966 | 1, | ||
1967 | ), | ||
1968 | full_range: 45..57, | ||
1969 | focus_range: Some( | ||
1970 | 52..54, | ||
1971 | ), | ||
1972 | name: "S2", | ||
1973 | kind: STRUCT, | ||
1974 | container_name: None, | ||
1975 | description: Some( | ||
1976 | "struct S2", | ||
1977 | ), | ||
1978 | docs: None, | ||
1979 | }, | ||
1980 | }, | ||
1981 | ], | ||
1982 | ), | ||
1983 | ] | ||
1984 | "#]], | ||
1985 | ); | ||
1986 | } | ||
1987 | |||
1988 | #[test] | ||
1989 | fn test_hover_arg_impl_trait_has_goto_type_action() { | ||
1990 | check_actions( | ||
1991 | r#" | ||
1992 | trait Foo {} | ||
1993 | fn foo(ar<|>g: &impl Foo) {} | ||
1994 | "#, | ||
1995 | expect![[r#" | ||
1996 | [ | ||
1997 | GoToType( | ||
1998 | [ | ||
1999 | HoverGotoTypeData { | ||
2000 | mod_path: "Foo", | ||
2001 | nav: NavigationTarget { | ||
2002 | file_id: FileId( | ||
2003 | 1, | ||
2004 | ), | ||
2005 | full_range: 0..12, | ||
2006 | focus_range: Some( | ||
2007 | 6..9, | ||
2008 | ), | ||
2009 | name: "Foo", | ||
2010 | kind: TRAIT, | ||
2011 | container_name: None, | ||
2012 | description: Some( | ||
2013 | "trait Foo", | ||
2014 | ), | ||
2015 | docs: None, | ||
2016 | }, | ||
2017 | }, | ||
2018 | ], | ||
2019 | ), | ||
2020 | ] | ||
2021 | "#]], | ||
2022 | ); | ||
2023 | } | ||
2024 | |||
2025 | #[test] | ||
2026 | fn test_hover_arg_impl_traits_has_goto_type_action() { | ||
2027 | check_actions( | ||
2028 | r#" | ||
2029 | trait Foo {} | ||
2030 | trait Bar<T> {} | ||
2031 | struct S{} | ||
2032 | |||
2033 | fn foo(ar<|>g: &impl Foo + Bar<S>) {} | ||
2034 | "#, | ||
2035 | expect![[r#" | ||
2036 | [ | ||
2037 | GoToType( | ||
2038 | [ | ||
2039 | HoverGotoTypeData { | ||
2040 | mod_path: "Foo", | ||
2041 | nav: NavigationTarget { | ||
2042 | file_id: FileId( | ||
2043 | 1, | ||
2044 | ), | ||
2045 | full_range: 0..12, | ||
2046 | focus_range: Some( | ||
2047 | 6..9, | ||
2048 | ), | ||
2049 | name: "Foo", | ||
2050 | kind: TRAIT, | ||
2051 | container_name: None, | ||
2052 | description: Some( | ||
2053 | "trait Foo", | ||
2054 | ), | ||
2055 | docs: None, | ||
2056 | }, | ||
2057 | }, | ||
2058 | HoverGotoTypeData { | ||
2059 | mod_path: "Bar", | ||
2060 | nav: NavigationTarget { | ||
2061 | file_id: FileId( | ||
2062 | 1, | ||
2063 | ), | ||
2064 | full_range: 13..28, | ||
2065 | focus_range: Some( | ||
2066 | 19..22, | ||
2067 | ), | ||
2068 | name: "Bar", | ||
2069 | kind: TRAIT, | ||
2070 | container_name: None, | ||
2071 | description: Some( | ||
2072 | "trait Bar", | ||
2073 | ), | ||
2074 | docs: None, | ||
2075 | }, | ||
2076 | }, | ||
2077 | HoverGotoTypeData { | ||
2078 | mod_path: "S", | ||
2079 | nav: NavigationTarget { | ||
2080 | file_id: FileId( | ||
2081 | 1, | ||
2082 | ), | ||
2083 | full_range: 29..39, | ||
2084 | focus_range: Some( | ||
2085 | 36..37, | ||
2086 | ), | ||
2087 | name: "S", | ||
2088 | kind: STRUCT, | ||
2089 | container_name: None, | ||
2090 | description: Some( | ||
2091 | "struct S", | ||
2092 | ), | ||
2093 | docs: None, | ||
2094 | }, | ||
2095 | }, | ||
2096 | ], | ||
2097 | ), | ||
2098 | ] | ||
2099 | "#]], | ||
2100 | ); | ||
2101 | } | ||
2102 | |||
2103 | #[test] | ||
2104 | fn test_hover_arg_generic_impl_trait_has_goto_type_action() { | ||
2105 | check_actions( | ||
2106 | r#" | ||
2107 | trait Foo<T> {} | ||
2108 | struct S {} | ||
2109 | fn foo(ar<|>g: &impl Foo<S>) {} | ||
2110 | "#, | ||
2111 | expect![[r#" | ||
2112 | [ | ||
2113 | GoToType( | ||
2114 | [ | ||
2115 | HoverGotoTypeData { | ||
2116 | mod_path: "Foo", | ||
2117 | nav: NavigationTarget { | ||
2118 | file_id: FileId( | ||
2119 | 1, | ||
2120 | ), | ||
2121 | full_range: 0..15, | ||
2122 | focus_range: Some( | ||
2123 | 6..9, | ||
2124 | ), | ||
2125 | name: "Foo", | ||
2126 | kind: TRAIT, | ||
2127 | container_name: None, | ||
2128 | description: Some( | ||
2129 | "trait Foo", | ||
2130 | ), | ||
2131 | docs: None, | ||
2132 | }, | ||
2133 | }, | ||
2134 | HoverGotoTypeData { | ||
2135 | mod_path: "S", | ||
2136 | nav: NavigationTarget { | ||
2137 | file_id: FileId( | ||
2138 | 1, | ||
2139 | ), | ||
2140 | full_range: 16..27, | ||
2141 | focus_range: Some( | ||
2142 | 23..24, | ||
2143 | ), | ||
2144 | name: "S", | ||
2145 | kind: STRUCT, | ||
2146 | container_name: None, | ||
2147 | description: Some( | ||
2148 | "struct S", | ||
2149 | ), | ||
2150 | docs: None, | ||
2151 | }, | ||
2152 | }, | ||
2153 | ], | ||
2154 | ), | ||
2155 | ] | ||
2156 | "#]], | ||
2157 | ); | ||
2158 | } | ||
2159 | |||
2160 | #[test] | ||
2161 | fn test_hover_dyn_return_has_goto_type_action() { | ||
2162 | check_actions( | ||
2163 | r#" | ||
2164 | trait Foo {} | ||
2165 | struct S; | ||
2166 | impl Foo for S {} | ||
2167 | |||
2168 | struct B<T>{} | ||
2169 | fn foo() -> B<dyn Foo> {} | ||
2170 | |||
2171 | fn main() { let s<|>t = foo(); } | ||
2172 | "#, | ||
2173 | expect![[r#" | ||
2174 | [ | ||
2175 | GoToType( | ||
2176 | [ | ||
2177 | HoverGotoTypeData { | ||
2178 | mod_path: "B", | ||
2179 | nav: NavigationTarget { | ||
2180 | file_id: FileId( | ||
2181 | 1, | ||
2182 | ), | ||
2183 | full_range: 42..55, | ||
2184 | focus_range: Some( | ||
2185 | 49..50, | ||
2186 | ), | ||
2187 | name: "B", | ||
2188 | kind: STRUCT, | ||
2189 | container_name: None, | ||
2190 | description: Some( | ||
2191 | "struct B", | ||
2192 | ), | ||
2193 | docs: None, | ||
2194 | }, | ||
2195 | }, | ||
2196 | HoverGotoTypeData { | ||
2197 | mod_path: "Foo", | ||
2198 | nav: NavigationTarget { | ||
2199 | file_id: FileId( | ||
2200 | 1, | ||
2201 | ), | ||
2202 | full_range: 0..12, | ||
2203 | focus_range: Some( | ||
2204 | 6..9, | ||
2205 | ), | ||
2206 | name: "Foo", | ||
2207 | kind: TRAIT, | ||
2208 | container_name: None, | ||
2209 | description: Some( | ||
2210 | "trait Foo", | ||
2211 | ), | ||
2212 | docs: None, | ||
2213 | }, | ||
2214 | }, | ||
2215 | ], | ||
2216 | ), | ||
2217 | ] | ||
2218 | "#]], | ||
2219 | ); | ||
2220 | } | ||
2221 | |||
2222 | #[test] | ||
2223 | fn test_hover_dyn_arg_has_goto_type_action() { | ||
2224 | check_actions( | ||
2225 | r#" | ||
2226 | trait Foo {} | ||
2227 | fn foo(ar<|>g: &dyn Foo) {} | ||
2228 | "#, | ||
2229 | expect![[r#" | ||
2230 | [ | ||
2231 | GoToType( | ||
2232 | [ | ||
2233 | HoverGotoTypeData { | ||
2234 | mod_path: "Foo", | ||
2235 | nav: NavigationTarget { | ||
2236 | file_id: FileId( | ||
2237 | 1, | ||
2238 | ), | ||
2239 | full_range: 0..12, | ||
2240 | focus_range: Some( | ||
2241 | 6..9, | ||
2242 | ), | ||
2243 | name: "Foo", | ||
2244 | kind: TRAIT, | ||
2245 | container_name: None, | ||
2246 | description: Some( | ||
2247 | "trait Foo", | ||
2248 | ), | ||
2249 | docs: None, | ||
2250 | }, | ||
2251 | }, | ||
2252 | ], | ||
2253 | ), | ||
2254 | ] | ||
2255 | "#]], | ||
2256 | ); | ||
2257 | } | ||
2258 | |||
2259 | #[test] | ||
2260 | fn test_hover_generic_dyn_arg_has_goto_type_action() { | ||
2261 | check_actions( | ||
2262 | r#" | ||
2263 | trait Foo<T> {} | ||
2264 | struct S {} | ||
2265 | fn foo(ar<|>g: &dyn Foo<S>) {} | ||
2266 | "#, | ||
2267 | expect![[r#" | ||
2268 | [ | ||
2269 | GoToType( | ||
2270 | [ | ||
2271 | HoverGotoTypeData { | ||
2272 | mod_path: "Foo", | ||
2273 | nav: NavigationTarget { | ||
2274 | file_id: FileId( | ||
2275 | 1, | ||
2276 | ), | ||
2277 | full_range: 0..15, | ||
2278 | focus_range: Some( | ||
2279 | 6..9, | ||
2280 | ), | ||
2281 | name: "Foo", | ||
2282 | kind: TRAIT, | ||
2283 | container_name: None, | ||
2284 | description: Some( | ||
2285 | "trait Foo", | ||
2286 | ), | ||
2287 | docs: None, | ||
2288 | }, | ||
2289 | }, | ||
2290 | HoverGotoTypeData { | ||
2291 | mod_path: "S", | ||
2292 | nav: NavigationTarget { | ||
2293 | file_id: FileId( | ||
2294 | 1, | ||
2295 | ), | ||
2296 | full_range: 16..27, | ||
2297 | focus_range: Some( | ||
2298 | 23..24, | ||
2299 | ), | ||
2300 | name: "S", | ||
2301 | kind: STRUCT, | ||
2302 | container_name: None, | ||
2303 | description: Some( | ||
2304 | "struct S", | ||
2305 | ), | ||
2306 | docs: None, | ||
2307 | }, | ||
2308 | }, | ||
2309 | ], | ||
2310 | ), | ||
2311 | ] | ||
2312 | "#]], | ||
2313 | ); | ||
2314 | } | ||
2315 | |||
2316 | #[test] | ||
2317 | fn test_hover_goto_type_action_links_order() { | ||
2318 | check_actions( | ||
2319 | r#" | ||
2320 | trait ImplTrait<T> {} | ||
2321 | trait DynTrait<T> {} | ||
2322 | struct B<T> {} | ||
2323 | struct S {} | ||
2324 | |||
2325 | fn foo(a<|>rg: &impl ImplTrait<B<dyn DynTrait<B<S>>>>) {} | ||
2326 | "#, | ||
2327 | expect![[r#" | ||
2328 | [ | ||
2329 | GoToType( | ||
2330 | [ | ||
2331 | HoverGotoTypeData { | ||
2332 | mod_path: "ImplTrait", | ||
2333 | nav: NavigationTarget { | ||
2334 | file_id: FileId( | ||
2335 | 1, | ||
2336 | ), | ||
2337 | full_range: 0..21, | ||
2338 | focus_range: Some( | ||
2339 | 6..15, | ||
2340 | ), | ||
2341 | name: "ImplTrait", | ||
2342 | kind: TRAIT, | ||
2343 | container_name: None, | ||
2344 | description: Some( | ||
2345 | "trait ImplTrait", | ||
2346 | ), | ||
2347 | docs: None, | ||
2348 | }, | ||
2349 | }, | ||
2350 | HoverGotoTypeData { | ||
2351 | mod_path: "B", | ||
2352 | nav: NavigationTarget { | ||
2353 | file_id: FileId( | ||
2354 | 1, | ||
2355 | ), | ||
2356 | full_range: 43..57, | ||
2357 | focus_range: Some( | ||
2358 | 50..51, | ||
2359 | ), | ||
2360 | name: "B", | ||
2361 | kind: STRUCT, | ||
2362 | container_name: None, | ||
2363 | description: Some( | ||
2364 | "struct B", | ||
2365 | ), | ||
2366 | docs: None, | ||
2367 | }, | ||
2368 | }, | ||
2369 | HoverGotoTypeData { | ||
2370 | mod_path: "DynTrait", | ||
2371 | nav: NavigationTarget { | ||
2372 | file_id: FileId( | ||
2373 | 1, | ||
2374 | ), | ||
2375 | full_range: 22..42, | ||
2376 | focus_range: Some( | ||
2377 | 28..36, | ||
2378 | ), | ||
2379 | name: "DynTrait", | ||
2380 | kind: TRAIT, | ||
2381 | container_name: None, | ||
2382 | description: Some( | ||
2383 | "trait DynTrait", | ||
2384 | ), | ||
2385 | docs: None, | ||
2386 | }, | ||
2387 | }, | ||
2388 | HoverGotoTypeData { | ||
2389 | mod_path: "S", | ||
2390 | nav: NavigationTarget { | ||
2391 | file_id: FileId( | ||
2392 | 1, | ||
2393 | ), | ||
2394 | full_range: 58..69, | ||
2395 | focus_range: Some( | ||
2396 | 65..66, | ||
2397 | ), | ||
2398 | name: "S", | ||
2399 | kind: STRUCT, | ||
2400 | container_name: None, | ||
2401 | description: Some( | ||
2402 | "struct S", | ||
2403 | ), | ||
2404 | docs: None, | ||
2405 | }, | ||
2406 | }, | ||
2407 | ], | ||
2408 | ), | ||
2409 | ] | ||
2410 | "#]], | ||
2411 | ); | ||
2412 | } | ||
2413 | |||
2414 | #[test] | ||
2415 | fn test_hover_associated_type_has_goto_type_action() { | ||
2416 | check_actions( | ||
2417 | r#" | ||
2418 | trait Foo { | ||
2419 | type Item; | ||
2420 | fn get(self) -> Self::Item {} | ||
2421 | } | ||
2422 | |||
2423 | struct Bar{} | ||
2424 | struct S{} | ||
2425 | |||
2426 | impl Foo for S { type Item = Bar; } | ||
2427 | |||
2428 | fn test() -> impl Foo { S {} } | ||
2429 | |||
2430 | fn main() { let s<|>t = test().get(); } | ||
2431 | "#, | ||
2432 | expect![[r#" | ||
2433 | [ | ||
2434 | GoToType( | ||
2435 | [ | ||
2436 | HoverGotoTypeData { | ||
2437 | mod_path: "Foo", | ||
2438 | nav: NavigationTarget { | ||
2439 | file_id: FileId( | ||
2440 | 1, | ||
2441 | ), | ||
2442 | full_range: 0..62, | ||
2443 | focus_range: Some( | ||
2444 | 6..9, | ||
2445 | ), | ||
2446 | name: "Foo", | ||
2447 | kind: TRAIT, | ||
2448 | container_name: None, | ||
2449 | description: Some( | ||
2450 | "trait Foo", | ||
2451 | ), | ||
2452 | docs: None, | ||
2453 | }, | ||
2454 | }, | ||
2455 | ], | ||
2456 | ), | ||
2457 | ] | ||
2458 | "#]], | ||
2459 | ); | ||
2460 | } | ||
2461 | } | ||
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs new file mode 100644 index 000000000..002adf915 --- /dev/null +++ b/crates/ide/src/inlay_hints.rs | |||
@@ -0,0 +1,927 @@ | |||
1 | use hir::{Adt, Callable, HirDisplay, Semantics, Type}; | ||
2 | use ide_db::RootDatabase; | ||
3 | use stdx::to_lower_snake_case; | ||
4 | use syntax::{ | ||
5 | ast::{self, ArgListOwner, AstNode}, | ||
6 | match_ast, Direction, NodeOrToken, SmolStr, SyntaxKind, TextRange, T, | ||
7 | }; | ||
8 | |||
9 | use crate::FileId; | ||
10 | use ast::NameOwner; | ||
11 | use either::Either; | ||
12 | |||
13 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
14 | pub struct InlayHintsConfig { | ||
15 | pub type_hints: bool, | ||
16 | pub parameter_hints: bool, | ||
17 | pub chaining_hints: bool, | ||
18 | pub max_length: Option<usize>, | ||
19 | } | ||
20 | |||
21 | impl Default for InlayHintsConfig { | ||
22 | fn default() -> Self { | ||
23 | Self { type_hints: true, parameter_hints: true, chaining_hints: true, max_length: None } | ||
24 | } | ||
25 | } | ||
26 | |||
27 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
28 | pub enum InlayKind { | ||
29 | TypeHint, | ||
30 | ParameterHint, | ||
31 | ChainingHint, | ||
32 | } | ||
33 | |||
34 | #[derive(Debug)] | ||
35 | pub struct InlayHint { | ||
36 | pub range: TextRange, | ||
37 | pub kind: InlayKind, | ||
38 | pub label: SmolStr, | ||
39 | } | ||
40 | |||
41 | // Feature: Inlay Hints | ||
42 | // | ||
43 | // rust-analyzer shows additional information inline with the source code. | ||
44 | // Editors usually render this using read-only virtual text snippets interspersed with code. | ||
45 | // | ||
46 | // rust-analyzer shows hits for | ||
47 | // | ||
48 | // * types of local variables | ||
49 | // * names of function arguments | ||
50 | // * types of chained expressions | ||
51 | // | ||
52 | // **Note:** VS Code does not have native support for inlay hints https://github.com/microsoft/vscode/issues/16221[yet] and the hints are implemented using decorations. | ||
53 | // This approach has limitations, the caret movement and bracket highlighting near the edges of the hint may be weird: | ||
54 | // https://github.com/rust-analyzer/rust-analyzer/issues/1623[1], https://github.com/rust-analyzer/rust-analyzer/issues/3453[2]. | ||
55 | // | ||
56 | // |=== | ||
57 | // | Editor | Action Name | ||
58 | // | ||
59 | // | VS Code | **Rust Analyzer: Toggle inlay hints* | ||
60 | // |=== | ||
61 | pub(crate) fn inlay_hints( | ||
62 | db: &RootDatabase, | ||
63 | file_id: FileId, | ||
64 | config: &InlayHintsConfig, | ||
65 | ) -> Vec<InlayHint> { | ||
66 | let _p = profile::span("inlay_hints"); | ||
67 | let sema = Semantics::new(db); | ||
68 | let file = sema.parse(file_id); | ||
69 | |||
70 | let mut res = Vec::new(); | ||
71 | for node in file.syntax().descendants() { | ||
72 | if let Some(expr) = ast::Expr::cast(node.clone()) { | ||
73 | get_chaining_hints(&mut res, &sema, config, expr); | ||
74 | } | ||
75 | |||
76 | match_ast! { | ||
77 | match node { | ||
78 | ast::CallExpr(it) => { get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it)); }, | ||
79 | ast::MethodCallExpr(it) => { get_param_name_hints(&mut res, &sema, config, ast::Expr::from(it)); }, | ||
80 | ast::IdentPat(it) => { get_bind_pat_hints(&mut res, &sema, config, it); }, | ||
81 | _ => (), | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | res | ||
86 | } | ||
87 | |||
88 | fn get_chaining_hints( | ||
89 | acc: &mut Vec<InlayHint>, | ||
90 | sema: &Semantics<RootDatabase>, | ||
91 | config: &InlayHintsConfig, | ||
92 | expr: ast::Expr, | ||
93 | ) -> Option<()> { | ||
94 | if !config.chaining_hints { | ||
95 | return None; | ||
96 | } | ||
97 | |||
98 | if matches!(expr, ast::Expr::RecordExpr(_)) { | ||
99 | return None; | ||
100 | } | ||
101 | |||
102 | let mut tokens = expr | ||
103 | .syntax() | ||
104 | .siblings_with_tokens(Direction::Next) | ||
105 | .filter_map(NodeOrToken::into_token) | ||
106 | .filter(|t| match t.kind() { | ||
107 | SyntaxKind::WHITESPACE if !t.text().contains('\n') => false, | ||
108 | SyntaxKind::COMMENT => false, | ||
109 | _ => true, | ||
110 | }); | ||
111 | |||
112 | // Chaining can be defined as an expression whose next sibling tokens are newline and dot | ||
113 | // Ignoring extra whitespace and comments | ||
114 | let next = tokens.next()?.kind(); | ||
115 | let next_next = tokens.next()?.kind(); | ||
116 | if next == SyntaxKind::WHITESPACE && next_next == T![.] { | ||
117 | let ty = sema.type_of_expr(&expr)?; | ||
118 | if ty.is_unknown() { | ||
119 | return None; | ||
120 | } | ||
121 | if matches!(expr, ast::Expr::PathExpr(_)) { | ||
122 | if let Some(Adt::Struct(st)) = ty.as_adt() { | ||
123 | if st.fields(sema.db).is_empty() { | ||
124 | return None; | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | let label = ty.display_truncated(sema.db, config.max_length).to_string(); | ||
129 | acc.push(InlayHint { | ||
130 | range: expr.syntax().text_range(), | ||
131 | kind: InlayKind::ChainingHint, | ||
132 | label: label.into(), | ||
133 | }); | ||
134 | } | ||
135 | Some(()) | ||
136 | } | ||
137 | |||
138 | fn get_param_name_hints( | ||
139 | acc: &mut Vec<InlayHint>, | ||
140 | sema: &Semantics<RootDatabase>, | ||
141 | config: &InlayHintsConfig, | ||
142 | expr: ast::Expr, | ||
143 | ) -> Option<()> { | ||
144 | if !config.parameter_hints { | ||
145 | return None; | ||
146 | } | ||
147 | |||
148 | let args = match &expr { | ||
149 | ast::Expr::CallExpr(expr) => expr.arg_list()?.args(), | ||
150 | ast::Expr::MethodCallExpr(expr) => expr.arg_list()?.args(), | ||
151 | _ => return None, | ||
152 | }; | ||
153 | |||
154 | let callable = get_callable(sema, &expr)?; | ||
155 | let hints = callable | ||
156 | .params(sema.db) | ||
157 | .into_iter() | ||
158 | .zip(args) | ||
159 | .filter_map(|((param, _ty), arg)| { | ||
160 | let param_name = match param? { | ||
161 | Either::Left(self_param) => self_param.to_string(), | ||
162 | Either::Right(pat) => match pat { | ||
163 | ast::Pat::IdentPat(it) => it.name()?.to_string(), | ||
164 | _ => return None, | ||
165 | }, | ||
166 | }; | ||
167 | Some((param_name, arg)) | ||
168 | }) | ||
169 | .filter(|(param_name, arg)| should_show_param_name_hint(sema, &callable, ¶m_name, &arg)) | ||
170 | .map(|(param_name, arg)| InlayHint { | ||
171 | range: arg.syntax().text_range(), | ||
172 | kind: InlayKind::ParameterHint, | ||
173 | label: param_name.into(), | ||
174 | }); | ||
175 | |||
176 | acc.extend(hints); | ||
177 | Some(()) | ||
178 | } | ||
179 | |||
180 | fn get_bind_pat_hints( | ||
181 | acc: &mut Vec<InlayHint>, | ||
182 | sema: &Semantics<RootDatabase>, | ||
183 | config: &InlayHintsConfig, | ||
184 | pat: ast::IdentPat, | ||
185 | ) -> Option<()> { | ||
186 | if !config.type_hints { | ||
187 | return None; | ||
188 | } | ||
189 | |||
190 | let ty = sema.type_of_pat(&pat.clone().into())?; | ||
191 | |||
192 | if should_not_display_type_hint(sema.db, &pat, &ty) { | ||
193 | return None; | ||
194 | } | ||
195 | |||
196 | acc.push(InlayHint { | ||
197 | range: pat.syntax().text_range(), | ||
198 | kind: InlayKind::TypeHint, | ||
199 | label: ty.display_truncated(sema.db, config.max_length).to_string().into(), | ||
200 | }); | ||
201 | Some(()) | ||
202 | } | ||
203 | |||
204 | fn pat_is_enum_variant(db: &RootDatabase, bind_pat: &ast::IdentPat, pat_ty: &Type) -> bool { | ||
205 | if let Some(Adt::Enum(enum_data)) = pat_ty.as_adt() { | ||
206 | let pat_text = bind_pat.to_string(); | ||
207 | enum_data | ||
208 | .variants(db) | ||
209 | .into_iter() | ||
210 | .map(|variant| variant.name(db).to_string()) | ||
211 | .any(|enum_name| enum_name == pat_text) | ||
212 | } else { | ||
213 | false | ||
214 | } | ||
215 | } | ||
216 | |||
217 | fn should_not_display_type_hint( | ||
218 | db: &RootDatabase, | ||
219 | bind_pat: &ast::IdentPat, | ||
220 | pat_ty: &Type, | ||
221 | ) -> bool { | ||
222 | if pat_ty.is_unknown() { | ||
223 | return true; | ||
224 | } | ||
225 | |||
226 | if let Some(Adt::Struct(s)) = pat_ty.as_adt() { | ||
227 | if s.fields(db).is_empty() && s.name(db).to_string() == bind_pat.to_string() { | ||
228 | return true; | ||
229 | } | ||
230 | } | ||
231 | |||
232 | for node in bind_pat.syntax().ancestors() { | ||
233 | match_ast! { | ||
234 | match node { | ||
235 | ast::LetStmt(it) => { | ||
236 | return it.ty().is_some() | ||
237 | }, | ||
238 | ast::Param(it) => { | ||
239 | return it.ty().is_some() | ||
240 | }, | ||
241 | ast::MatchArm(_it) => { | ||
242 | return pat_is_enum_variant(db, bind_pat, pat_ty); | ||
243 | }, | ||
244 | ast::IfExpr(it) => { | ||
245 | return it.condition().and_then(|condition| condition.pat()).is_some() | ||
246 | && pat_is_enum_variant(db, bind_pat, pat_ty); | ||
247 | }, | ||
248 | ast::WhileExpr(it) => { | ||
249 | return it.condition().and_then(|condition| condition.pat()).is_some() | ||
250 | && pat_is_enum_variant(db, bind_pat, pat_ty); | ||
251 | }, | ||
252 | _ => (), | ||
253 | } | ||
254 | } | ||
255 | } | ||
256 | false | ||
257 | } | ||
258 | |||
259 | fn should_show_param_name_hint( | ||
260 | sema: &Semantics<RootDatabase>, | ||
261 | callable: &Callable, | ||
262 | param_name: &str, | ||
263 | argument: &ast::Expr, | ||
264 | ) -> bool { | ||
265 | let param_name = param_name.trim_start_matches('_'); | ||
266 | let fn_name = match callable.kind() { | ||
267 | hir::CallableKind::Function(it) => Some(it.name(sema.db).to_string()), | ||
268 | hir::CallableKind::TupleStruct(_) | ||
269 | | hir::CallableKind::TupleEnumVariant(_) | ||
270 | | hir::CallableKind::Closure => None, | ||
271 | }; | ||
272 | if param_name.is_empty() | ||
273 | || Some(param_name) == fn_name.as_ref().map(|s| s.trim_start_matches('_')) | ||
274 | || is_argument_similar_to_param_name(sema, argument, param_name) | ||
275 | || param_name.starts_with("ra_fixture") | ||
276 | { | ||
277 | return false; | ||
278 | } | ||
279 | |||
280 | // avoid displaying hints for common functions like map, filter, etc. | ||
281 | // or other obvious words used in std | ||
282 | !(callable.n_params() == 1 && is_obvious_param(param_name)) | ||
283 | } | ||
284 | |||
285 | fn is_argument_similar_to_param_name( | ||
286 | sema: &Semantics<RootDatabase>, | ||
287 | argument: &ast::Expr, | ||
288 | param_name: &str, | ||
289 | ) -> bool { | ||
290 | if is_enum_name_similar_to_param_name(sema, argument, param_name) { | ||
291 | return true; | ||
292 | } | ||
293 | match get_string_representation(argument) { | ||
294 | None => false, | ||
295 | Some(repr) => { | ||
296 | let argument_string = repr.trim_start_matches('_'); | ||
297 | argument_string.starts_with(param_name) || argument_string.ends_with(param_name) | ||
298 | } | ||
299 | } | ||
300 | } | ||
301 | |||
302 | fn is_enum_name_similar_to_param_name( | ||
303 | sema: &Semantics<RootDatabase>, | ||
304 | argument: &ast::Expr, | ||
305 | param_name: &str, | ||
306 | ) -> bool { | ||
307 | match sema.type_of_expr(argument).and_then(|t| t.as_adt()) { | ||
308 | Some(Adt::Enum(e)) => to_lower_snake_case(&e.name(sema.db).to_string()) == param_name, | ||
309 | _ => false, | ||
310 | } | ||
311 | } | ||
312 | |||
313 | fn get_string_representation(expr: &ast::Expr) -> Option<String> { | ||
314 | match expr { | ||
315 | ast::Expr::MethodCallExpr(method_call_expr) => { | ||
316 | Some(method_call_expr.name_ref()?.to_string()) | ||
317 | } | ||
318 | ast::Expr::RefExpr(ref_expr) => get_string_representation(&ref_expr.expr()?), | ||
319 | _ => Some(expr.to_string()), | ||
320 | } | ||
321 | } | ||
322 | |||
323 | fn is_obvious_param(param_name: &str) -> bool { | ||
324 | let is_obvious_param_name = | ||
325 | matches!(param_name, "predicate" | "value" | "pat" | "rhs" | "other"); | ||
326 | param_name.len() == 1 || is_obvious_param_name | ||
327 | } | ||
328 | |||
329 | fn get_callable(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> Option<Callable> { | ||
330 | match expr { | ||
331 | ast::Expr::CallExpr(expr) => sema.type_of_expr(&expr.expr()?)?.as_callable(sema.db), | ||
332 | ast::Expr::MethodCallExpr(expr) => sema.resolve_method_call_as_callable(expr), | ||
333 | _ => None, | ||
334 | } | ||
335 | } | ||
336 | |||
337 | #[cfg(test)] | ||
338 | mod tests { | ||
339 | use expect::{expect, Expect}; | ||
340 | use test_utils::extract_annotations; | ||
341 | |||
342 | use crate::{inlay_hints::InlayHintsConfig, mock_analysis::single_file}; | ||
343 | |||
344 | fn check(ra_fixture: &str) { | ||
345 | check_with_config(InlayHintsConfig::default(), ra_fixture); | ||
346 | } | ||
347 | |||
348 | fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) { | ||
349 | let (analysis, file_id) = single_file(ra_fixture); | ||
350 | let expected = extract_annotations(&*analysis.file_text(file_id).unwrap()); | ||
351 | let inlay_hints = analysis.inlay_hints(file_id, &config).unwrap(); | ||
352 | let actual = | ||
353 | inlay_hints.into_iter().map(|it| (it.range, it.label.to_string())).collect::<Vec<_>>(); | ||
354 | assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual); | ||
355 | } | ||
356 | |||
357 | fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) { | ||
358 | let (analysis, file_id) = single_file(ra_fixture); | ||
359 | let inlay_hints = analysis.inlay_hints(file_id, &config).unwrap(); | ||
360 | expect.assert_debug_eq(&inlay_hints) | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn param_hints_only() { | ||
365 | check_with_config( | ||
366 | InlayHintsConfig { | ||
367 | parameter_hints: true, | ||
368 | type_hints: false, | ||
369 | chaining_hints: false, | ||
370 | max_length: None, | ||
371 | }, | ||
372 | r#" | ||
373 | fn foo(a: i32, b: i32) -> i32 { a + b } | ||
374 | fn main() { | ||
375 | let _x = foo( | ||
376 | 4, | ||
377 | //^ a | ||
378 | 4, | ||
379 | //^ b | ||
380 | ); | ||
381 | }"#, | ||
382 | ); | ||
383 | } | ||
384 | |||
385 | #[test] | ||
386 | fn hints_disabled() { | ||
387 | check_with_config( | ||
388 | InlayHintsConfig { | ||
389 | type_hints: false, | ||
390 | parameter_hints: false, | ||
391 | chaining_hints: false, | ||
392 | max_length: None, | ||
393 | }, | ||
394 | r#" | ||
395 | fn foo(a: i32, b: i32) -> i32 { a + b } | ||
396 | fn main() { | ||
397 | let _x = foo(4, 4); | ||
398 | }"#, | ||
399 | ); | ||
400 | } | ||
401 | |||
402 | #[test] | ||
403 | fn type_hints_only() { | ||
404 | check_with_config( | ||
405 | InlayHintsConfig { | ||
406 | type_hints: true, | ||
407 | parameter_hints: false, | ||
408 | chaining_hints: false, | ||
409 | max_length: None, | ||
410 | }, | ||
411 | r#" | ||
412 | fn foo(a: i32, b: i32) -> i32 { a + b } | ||
413 | fn main() { | ||
414 | let _x = foo(4, 4); | ||
415 | //^^ i32 | ||
416 | }"#, | ||
417 | ); | ||
418 | } | ||
419 | |||
420 | #[test] | ||
421 | fn default_generic_types_should_not_be_displayed() { | ||
422 | check( | ||
423 | r#" | ||
424 | struct Test<K, T = u8> { k: K, t: T } | ||
425 | |||
426 | fn main() { | ||
427 | let zz = Test { t: 23u8, k: 33 }; | ||
428 | //^^ Test<i32> | ||
429 | let zz_ref = &zz; | ||
430 | //^^^^^^ &Test<i32> | ||
431 | let test = || zz; | ||
432 | //^^^^ || -> Test<i32> | ||
433 | }"#, | ||
434 | ); | ||
435 | } | ||
436 | |||
437 | #[test] | ||
438 | fn let_statement() { | ||
439 | check( | ||
440 | r#" | ||
441 | #[derive(PartialEq)] | ||
442 | enum Option<T> { None, Some(T) } | ||
443 | |||
444 | #[derive(PartialEq)] | ||
445 | struct Test { a: Option<u32>, b: u8 } | ||
446 | |||
447 | fn main() { | ||
448 | struct InnerStruct {} | ||
449 | |||
450 | let test = 54; | ||
451 | //^^^^ i32 | ||
452 | let test: i32 = 33; | ||
453 | let mut test = 33; | ||
454 | //^^^^^^^^ i32 | ||
455 | let _ = 22; | ||
456 | let test = "test"; | ||
457 | //^^^^ &str | ||
458 | let test = InnerStruct {}; | ||
459 | |||
460 | let test = unresolved(); | ||
461 | |||
462 | let test = (42, 'a'); | ||
463 | //^^^^ (i32, char) | ||
464 | let (a, (b, (c,)) = (2, (3, (9.2,)); | ||
465 | //^ i32 ^ i32 ^ f64 | ||
466 | let &x = &92; | ||
467 | //^ i32 | ||
468 | }"#, | ||
469 | ); | ||
470 | } | ||
471 | |||
472 | #[test] | ||
473 | fn closure_parameters() { | ||
474 | check( | ||
475 | r#" | ||
476 | fn main() { | ||
477 | let mut start = 0; | ||
478 | //^^^^^^^^^ i32 | ||
479 | (0..2).for_each(|increment| { start += increment; }); | ||
480 | //^^^^^^^^^ i32 | ||
481 | |||
482 | let multiply = | ||
483 | //^^^^^^^^ |…| -> i32 | ||
484 | | a, b| a * b | ||
485 | //^ i32 ^ i32 | ||
486 | ; | ||
487 | |||
488 | let _: i32 = multiply(1, 2); | ||
489 | let multiply_ref = &multiply; | ||
490 | //^^^^^^^^^^^^ &|…| -> i32 | ||
491 | |||
492 | let return_42 = || 42; | ||
493 | //^^^^^^^^^ || -> i32 | ||
494 | }"#, | ||
495 | ); | ||
496 | } | ||
497 | |||
498 | #[test] | ||
499 | fn for_expression() { | ||
500 | check( | ||
501 | r#" | ||
502 | fn main() { | ||
503 | let mut start = 0; | ||
504 | //^^^^^^^^^ i32 | ||
505 | for increment in 0..2 { start += increment; } | ||
506 | //^^^^^^^^^ i32 | ||
507 | }"#, | ||
508 | ); | ||
509 | } | ||
510 | |||
511 | #[test] | ||
512 | fn if_expr() { | ||
513 | check( | ||
514 | r#" | ||
515 | enum Option<T> { None, Some(T) } | ||
516 | use Option::*; | ||
517 | |||
518 | struct Test { a: Option<u32>, b: u8 } | ||
519 | |||
520 | fn main() { | ||
521 | let test = Some(Test { a: Some(3), b: 1 }); | ||
522 | //^^^^ Option<Test> | ||
523 | if let None = &test {}; | ||
524 | if let test = &test {}; | ||
525 | //^^^^ &Option<Test> | ||
526 | if let Some(test) = &test {}; | ||
527 | //^^^^ &Test | ||
528 | if let Some(Test { a, b }) = &test {}; | ||
529 | //^ &Option<u32> ^ &u8 | ||
530 | if let Some(Test { a: x, b: y }) = &test {}; | ||
531 | //^ &Option<u32> ^ &u8 | ||
532 | if let Some(Test { a: Some(x), b: y }) = &test {}; | ||
533 | //^ &u32 ^ &u8 | ||
534 | if let Some(Test { a: None, b: y }) = &test {}; | ||
535 | //^ &u8 | ||
536 | if let Some(Test { b: y, .. }) = &test {}; | ||
537 | //^ &u8 | ||
538 | if test == None {} | ||
539 | }"#, | ||
540 | ); | ||
541 | } | ||
542 | |||
543 | #[test] | ||
544 | fn while_expr() { | ||
545 | check( | ||
546 | r#" | ||
547 | enum Option<T> { None, Some(T) } | ||
548 | use Option::*; | ||
549 | |||
550 | struct Test { a: Option<u32>, b: u8 } | ||
551 | |||
552 | fn main() { | ||
553 | let test = Some(Test { a: Some(3), b: 1 }); | ||
554 | //^^^^ Option<Test> | ||
555 | while let Some(Test { a: Some(x), b: y }) = &test {}; | ||
556 | //^ &u32 ^ &u8 | ||
557 | }"#, | ||
558 | ); | ||
559 | } | ||
560 | |||
561 | #[test] | ||
562 | fn match_arm_list() { | ||
563 | check( | ||
564 | r#" | ||
565 | enum Option<T> { None, Some(T) } | ||
566 | use Option::*; | ||
567 | |||
568 | struct Test { a: Option<u32>, b: u8 } | ||
569 | |||
570 | fn main() { | ||
571 | match Some(Test { a: Some(3), b: 1 }) { | ||
572 | None => (), | ||
573 | test => (), | ||
574 | //^^^^ Option<Test> | ||
575 | Some(Test { a: Some(x), b: y }) => (), | ||
576 | //^ u32 ^ u8 | ||
577 | _ => {} | ||
578 | } | ||
579 | }"#, | ||
580 | ); | ||
581 | } | ||
582 | |||
583 | #[test] | ||
584 | fn hint_truncation() { | ||
585 | check_with_config( | ||
586 | InlayHintsConfig { max_length: Some(8), ..Default::default() }, | ||
587 | r#" | ||
588 | struct Smol<T>(T); | ||
589 | |||
590 | struct VeryLongOuterName<T>(T); | ||
591 | |||
592 | fn main() { | ||
593 | let a = Smol(0u32); | ||
594 | //^ Smol<u32> | ||
595 | let b = VeryLongOuterName(0usize); | ||
596 | //^ VeryLongOuterName<…> | ||
597 | let c = Smol(Smol(0u32)) | ||
598 | //^ Smol<Smol<…>> | ||
599 | }"#, | ||
600 | ); | ||
601 | } | ||
602 | |||
603 | #[test] | ||
604 | fn function_call_parameter_hint() { | ||
605 | check( | ||
606 | r#" | ||
607 | enum Option<T> { None, Some(T) } | ||
608 | use Option::*; | ||
609 | |||
610 | struct FileId {} | ||
611 | struct SmolStr {} | ||
612 | |||
613 | struct TextRange {} | ||
614 | struct SyntaxKind {} | ||
615 | struct NavigationTarget {} | ||
616 | |||
617 | struct Test {} | ||
618 | |||
619 | impl Test { | ||
620 | fn method(&self, mut param: i32) -> i32 { param * 2 } | ||
621 | |||
622 | fn from_syntax( | ||
623 | file_id: FileId, | ||
624 | name: SmolStr, | ||
625 | focus_range: Option<TextRange>, | ||
626 | full_range: TextRange, | ||
627 | kind: SyntaxKind, | ||
628 | docs: Option<String>, | ||
629 | ) -> NavigationTarget { | ||
630 | NavigationTarget {} | ||
631 | } | ||
632 | } | ||
633 | |||
634 | fn test_func(mut foo: i32, bar: i32, msg: &str, _: i32, last: i32) -> i32 { | ||
635 | foo + bar | ||
636 | } | ||
637 | |||
638 | fn main() { | ||
639 | let not_literal = 1; | ||
640 | //^^^^^^^^^^^ i32 | ||
641 | let _: i32 = test_func(1, 2, "hello", 3, not_literal); | ||
642 | //^ foo ^ bar ^^^^^^^ msg ^^^^^^^^^^^ last | ||
643 | let t: Test = Test {}; | ||
644 | t.method(123); | ||
645 | //^^^ param | ||
646 | Test::method(&t, 3456); | ||
647 | //^^ &self ^^^^ param | ||
648 | Test::from_syntax( | ||
649 | FileId {}, | ||
650 | //^^^^^^^^^ file_id | ||
651 | "impl".into(), | ||
652 | //^^^^^^^^^^^^^ name | ||
653 | None, | ||
654 | //^^^^ focus_range | ||
655 | TextRange {}, | ||
656 | //^^^^^^^^^^^^ full_range | ||
657 | SyntaxKind {}, | ||
658 | //^^^^^^^^^^^^^ kind | ||
659 | None, | ||
660 | //^^^^ docs | ||
661 | ); | ||
662 | }"#, | ||
663 | ); | ||
664 | } | ||
665 | |||
666 | #[test] | ||
667 | fn omitted_parameters_hints_heuristics() { | ||
668 | check_with_config( | ||
669 | InlayHintsConfig { max_length: Some(8), ..Default::default() }, | ||
670 | r#" | ||
671 | fn map(f: i32) {} | ||
672 | fn filter(predicate: i32) {} | ||
673 | |||
674 | struct TestVarContainer { | ||
675 | test_var: i32, | ||
676 | } | ||
677 | |||
678 | impl TestVarContainer { | ||
679 | fn test_var(&self) -> i32 { | ||
680 | self.test_var | ||
681 | } | ||
682 | } | ||
683 | |||
684 | struct Test {} | ||
685 | |||
686 | impl Test { | ||
687 | fn map(self, f: i32) -> Self { | ||
688 | self | ||
689 | } | ||
690 | |||
691 | fn filter(self, predicate: i32) -> Self { | ||
692 | self | ||
693 | } | ||
694 | |||
695 | fn field(self, value: i32) -> Self { | ||
696 | self | ||
697 | } | ||
698 | |||
699 | fn no_hints_expected(&self, _: i32, test_var: i32) {} | ||
700 | |||
701 | fn frob(&self, frob: bool) {} | ||
702 | } | ||
703 | |||
704 | struct Param {} | ||
705 | |||
706 | fn different_order(param: &Param) {} | ||
707 | fn different_order_mut(param: &mut Param) {} | ||
708 | fn has_underscore(_param: bool) {} | ||
709 | fn enum_matches_param_name(completion_kind: CompletionKind) {} | ||
710 | fn param_destructuring_omitted_1((a, b): (u32, u32)) {} | ||
711 | fn param_destructuring_omitted_2(TestVarContainer { test_var: _ }: TestVarContainer) {} | ||
712 | |||
713 | fn twiddle(twiddle: bool) {} | ||
714 | fn doo(_doo: bool) {} | ||
715 | |||
716 | enum CompletionKind { | ||
717 | Keyword, | ||
718 | } | ||
719 | |||
720 | fn main() { | ||
721 | let container: TestVarContainer = TestVarContainer { test_var: 42 }; | ||
722 | let test: Test = Test {}; | ||
723 | |||
724 | map(22); | ||
725 | filter(33); | ||
726 | |||
727 | let test_processed: Test = test.map(1).filter(2).field(3); | ||
728 | |||
729 | let test_var: i32 = 55; | ||
730 | test_processed.no_hints_expected(22, test_var); | ||
731 | test_processed.no_hints_expected(33, container.test_var); | ||
732 | test_processed.no_hints_expected(44, container.test_var()); | ||
733 | test_processed.frob(false); | ||
734 | |||
735 | twiddle(true); | ||
736 | doo(true); | ||
737 | |||
738 | let mut param_begin: Param = Param {}; | ||
739 | different_order(¶m_begin); | ||
740 | different_order(&mut param_begin); | ||
741 | |||
742 | let param: bool = true; | ||
743 | has_underscore(param); | ||
744 | |||
745 | enum_matches_param_name(CompletionKind::Keyword); | ||
746 | |||
747 | let a: f64 = 7.0; | ||
748 | let b: f64 = 4.0; | ||
749 | let _: f64 = a.div_euclid(b); | ||
750 | let _: f64 = a.abs_sub(b); | ||
751 | |||
752 | let range: (u32, u32) = (3, 5); | ||
753 | param_destructuring_omitted_1(range); | ||
754 | param_destructuring_omitted_2(container); | ||
755 | }"#, | ||
756 | ); | ||
757 | } | ||
758 | |||
759 | #[test] | ||
760 | fn unit_structs_have_no_type_hints() { | ||
761 | check_with_config( | ||
762 | InlayHintsConfig { max_length: Some(8), ..Default::default() }, | ||
763 | r#" | ||
764 | enum Result<T, E> { Ok(T), Err(E) } | ||
765 | use Result::*; | ||
766 | |||
767 | struct SyntheticSyntax; | ||
768 | |||
769 | fn main() { | ||
770 | match Ok(()) { | ||
771 | Ok(_) => (), | ||
772 | Err(SyntheticSyntax) => (), | ||
773 | } | ||
774 | }"#, | ||
775 | ); | ||
776 | } | ||
777 | |||
778 | #[test] | ||
779 | fn chaining_hints_ignore_comments() { | ||
780 | check_expect( | ||
781 | InlayHintsConfig { | ||
782 | parameter_hints: false, | ||
783 | type_hints: false, | ||
784 | chaining_hints: true, | ||
785 | max_length: None, | ||
786 | }, | ||
787 | r#" | ||
788 | struct A(B); | ||
789 | impl A { fn into_b(self) -> B { self.0 } } | ||
790 | struct B(C); | ||
791 | impl B { fn into_c(self) -> C { self.0 } } | ||
792 | struct C; | ||
793 | |||
794 | fn main() { | ||
795 | let c = A(B(C)) | ||
796 | .into_b() // This is a comment | ||
797 | .into_c(); | ||
798 | } | ||
799 | "#, | ||
800 | expect![[r#" | ||
801 | [ | ||
802 | InlayHint { | ||
803 | range: 147..172, | ||
804 | kind: ChainingHint, | ||
805 | label: "B", | ||
806 | }, | ||
807 | InlayHint { | ||
808 | range: 147..154, | ||
809 | kind: ChainingHint, | ||
810 | label: "A", | ||
811 | }, | ||
812 | ] | ||
813 | "#]], | ||
814 | ); | ||
815 | } | ||
816 | |||
817 | #[test] | ||
818 | fn chaining_hints_without_newlines() { | ||
819 | check_with_config( | ||
820 | InlayHintsConfig { | ||
821 | parameter_hints: false, | ||
822 | type_hints: false, | ||
823 | chaining_hints: true, | ||
824 | max_length: None, | ||
825 | }, | ||
826 | r#" | ||
827 | struct A(B); | ||
828 | impl A { fn into_b(self) -> B { self.0 } } | ||
829 | struct B(C); | ||
830 | impl B { fn into_c(self) -> C { self.0 } } | ||
831 | struct C; | ||
832 | |||
833 | fn main() { | ||
834 | let c = A(B(C)).into_b().into_c(); | ||
835 | }"#, | ||
836 | ); | ||
837 | } | ||
838 | |||
839 | #[test] | ||
840 | fn struct_access_chaining_hints() { | ||
841 | check_expect( | ||
842 | InlayHintsConfig { | ||
843 | parameter_hints: false, | ||
844 | type_hints: false, | ||
845 | chaining_hints: true, | ||
846 | max_length: None, | ||
847 | }, | ||
848 | r#" | ||
849 | struct A { pub b: B } | ||
850 | struct B { pub c: C } | ||
851 | struct C(pub bool); | ||
852 | struct D; | ||
853 | |||
854 | impl D { | ||
855 | fn foo(&self) -> i32 { 42 } | ||
856 | } | ||
857 | |||
858 | fn main() { | ||
859 | let x = A { b: B { c: C(true) } } | ||
860 | .b | ||
861 | .c | ||
862 | .0; | ||
863 | let x = D | ||
864 | .foo(); | ||
865 | }"#, | ||
866 | expect![[r#" | ||
867 | [ | ||
868 | InlayHint { | ||
869 | range: 143..190, | ||
870 | kind: ChainingHint, | ||
871 | label: "C", | ||
872 | }, | ||
873 | InlayHint { | ||
874 | range: 143..179, | ||
875 | kind: ChainingHint, | ||
876 | label: "B", | ||
877 | }, | ||
878 | ] | ||
879 | "#]], | ||
880 | ); | ||
881 | } | ||
882 | |||
883 | #[test] | ||
884 | fn generic_chaining_hints() { | ||
885 | check_expect( | ||
886 | InlayHintsConfig { | ||
887 | parameter_hints: false, | ||
888 | type_hints: false, | ||
889 | chaining_hints: true, | ||
890 | max_length: None, | ||
891 | }, | ||
892 | r#" | ||
893 | struct A<T>(T); | ||
894 | struct B<T>(T); | ||
895 | struct C<T>(T); | ||
896 | struct X<T,R>(T, R); | ||
897 | |||
898 | impl<T> A<T> { | ||
899 | fn new(t: T) -> Self { A(t) } | ||
900 | fn into_b(self) -> B<T> { B(self.0) } | ||
901 | } | ||
902 | impl<T> B<T> { | ||
903 | fn into_c(self) -> C<T> { C(self.0) } | ||
904 | } | ||
905 | fn main() { | ||
906 | let c = A::new(X(42, true)) | ||
907 | .into_b() | ||
908 | .into_c(); | ||
909 | } | ||
910 | "#, | ||
911 | expect![[r#" | ||
912 | [ | ||
913 | InlayHint { | ||
914 | range: 246..283, | ||
915 | kind: ChainingHint, | ||
916 | label: "B<X<i32, bool>>", | ||
917 | }, | ||
918 | InlayHint { | ||
919 | range: 246..265, | ||
920 | kind: ChainingHint, | ||
921 | label: "A<X<i32, bool>>", | ||
922 | }, | ||
923 | ] | ||
924 | "#]], | ||
925 | ); | ||
926 | } | ||
927 | } | ||
diff --git a/crates/ide/src/join_lines.rs b/crates/ide/src/join_lines.rs new file mode 100644 index 000000000..e37702acd --- /dev/null +++ b/crates/ide/src/join_lines.rs | |||
@@ -0,0 +1,773 @@ | |||
1 | use assists::utils::extract_trivial_expression; | ||
2 | use itertools::Itertools; | ||
3 | use syntax::{ | ||
4 | algo::{find_covering_element, non_trivia_sibling}, | ||
5 | ast::{self, AstNode, AstToken}, | ||
6 | Direction, NodeOrToken, SourceFile, | ||
7 | SyntaxKind::{self, USE_TREE, WHITESPACE}, | ||
8 | SyntaxNode, SyntaxToken, TextRange, TextSize, T, | ||
9 | }; | ||
10 | use text_edit::{TextEdit, TextEditBuilder}; | ||
11 | |||
12 | // Feature: Join Lines | ||
13 | // | ||
14 | // Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces. | ||
15 | // | ||
16 | // |=== | ||
17 | // | Editor | Action Name | ||
18 | // | ||
19 | // | VS Code | **Rust Analyzer: Join lines** | ||
20 | // |=== | ||
21 | pub fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit { | ||
22 | let range = if range.is_empty() { | ||
23 | let syntax = file.syntax(); | ||
24 | let text = syntax.text().slice(range.start()..); | ||
25 | let pos = match text.find_char('\n') { | ||
26 | None => return TextEdit::builder().finish(), | ||
27 | Some(pos) => pos, | ||
28 | }; | ||
29 | TextRange::at(range.start() + pos, TextSize::of('\n')) | ||
30 | } else { | ||
31 | range | ||
32 | }; | ||
33 | |||
34 | let node = match find_covering_element(file.syntax(), range) { | ||
35 | NodeOrToken::Node(node) => node, | ||
36 | NodeOrToken::Token(token) => token.parent(), | ||
37 | }; | ||
38 | let mut edit = TextEdit::builder(); | ||
39 | for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) { | ||
40 | let range = match range.intersect(token.text_range()) { | ||
41 | Some(range) => range, | ||
42 | None => continue, | ||
43 | } - token.text_range().start(); | ||
44 | let text = token.text(); | ||
45 | for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { | ||
46 | let pos: TextSize = (pos as u32).into(); | ||
47 | let off = token.text_range().start() + range.start() + pos; | ||
48 | if !edit.invalidates_offset(off) { | ||
49 | remove_newline(&mut edit, &token, off); | ||
50 | } | ||
51 | } | ||
52 | } | ||
53 | |||
54 | edit.finish() | ||
55 | } | ||
56 | |||
57 | fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) { | ||
58 | if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { | ||
59 | // The node is either the first or the last in the file | ||
60 | let suff = &token.text()[TextRange::new( | ||
61 | offset - token.text_range().start() + TextSize::of('\n'), | ||
62 | TextSize::of(token.text().as_str()), | ||
63 | )]; | ||
64 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); | ||
65 | |||
66 | edit.replace(TextRange::at(offset, ((spaces + 1) as u32).into()), " ".to_string()); | ||
67 | return; | ||
68 | } | ||
69 | |||
70 | // The node is between two other nodes | ||
71 | let prev = token.prev_sibling_or_token().unwrap(); | ||
72 | let next = token.next_sibling_or_token().unwrap(); | ||
73 | if is_trailing_comma(prev.kind(), next.kind()) { | ||
74 | // Removes: trailing comma, newline (incl. surrounding whitespace) | ||
75 | edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end())); | ||
76 | return; | ||
77 | } | ||
78 | if prev.kind() == T![,] && next.kind() == T!['}'] { | ||
79 | // Removes: comma, newline (incl. surrounding whitespace) | ||
80 | let space = if let Some(left) = prev.prev_sibling_or_token() { | ||
81 | compute_ws(left.kind(), next.kind()) | ||
82 | } else { | ||
83 | " " | ||
84 | }; | ||
85 | edit.replace( | ||
86 | TextRange::new(prev.text_range().start(), token.text_range().end()), | ||
87 | space.to_string(), | ||
88 | ); | ||
89 | return; | ||
90 | } | ||
91 | |||
92 | if let (Some(_), Some(next)) = ( | ||
93 | prev.as_token().cloned().and_then(ast::Comment::cast), | ||
94 | next.as_token().cloned().and_then(ast::Comment::cast), | ||
95 | ) { | ||
96 | // Removes: newline (incl. surrounding whitespace), start of the next comment | ||
97 | edit.delete(TextRange::new( | ||
98 | token.text_range().start(), | ||
99 | next.syntax().text_range().start() + TextSize::of(next.prefix()), | ||
100 | )); | ||
101 | return; | ||
102 | } | ||
103 | |||
104 | // Special case that turns something like: | ||
105 | // | ||
106 | // ``` | ||
107 | // my_function({<|> | ||
108 | // <some-expr> | ||
109 | // }) | ||
110 | // ``` | ||
111 | // | ||
112 | // into `my_function(<some-expr>)` | ||
113 | if join_single_expr_block(edit, token).is_some() { | ||
114 | return; | ||
115 | } | ||
116 | // ditto for | ||
117 | // | ||
118 | // ``` | ||
119 | // use foo::{<|> | ||
120 | // bar | ||
121 | // }; | ||
122 | // ``` | ||
123 | if join_single_use_tree(edit, token).is_some() { | ||
124 | return; | ||
125 | } | ||
126 | |||
127 | // Remove newline but add a computed amount of whitespace characters | ||
128 | edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string()); | ||
129 | } | ||
130 | |||
131 | fn has_comma_after(node: &SyntaxNode) -> bool { | ||
132 | match non_trivia_sibling(node.clone().into(), Direction::Next) { | ||
133 | Some(n) => n.kind() == T![,], | ||
134 | _ => false, | ||
135 | } | ||
136 | } | ||
137 | |||
138 | fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { | ||
139 | let block_expr = ast::BlockExpr::cast(token.parent())?; | ||
140 | if !block_expr.is_standalone() { | ||
141 | return None; | ||
142 | } | ||
143 | let expr = extract_trivial_expression(&block_expr)?; | ||
144 | |||
145 | let block_range = block_expr.syntax().text_range(); | ||
146 | let mut buf = expr.syntax().text().to_string(); | ||
147 | |||
148 | // Match block needs to have a comma after the block | ||
149 | if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) { | ||
150 | if !has_comma_after(match_arm.syntax()) { | ||
151 | buf.push(','); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | edit.replace(block_range, buf); | ||
156 | |||
157 | Some(()) | ||
158 | } | ||
159 | |||
160 | fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { | ||
161 | let use_tree_list = ast::UseTreeList::cast(token.parent())?; | ||
162 | let (tree,) = use_tree_list.use_trees().collect_tuple()?; | ||
163 | edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string()); | ||
164 | Some(()) | ||
165 | } | ||
166 | |||
167 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
168 | matches!((left, right), (T![,], T![')']) | (T![,], T![']'])) | ||
169 | } | ||
170 | |||
171 | fn compute_ws(left: SyntaxKind, right: SyntaxKind) -> &'static str { | ||
172 | match left { | ||
173 | T!['('] | T!['['] => return "", | ||
174 | T!['{'] => { | ||
175 | if let USE_TREE = right { | ||
176 | return ""; | ||
177 | } | ||
178 | } | ||
179 | _ => (), | ||
180 | } | ||
181 | match right { | ||
182 | T![')'] | T![']'] => return "", | ||
183 | T!['}'] => { | ||
184 | if let USE_TREE = left { | ||
185 | return ""; | ||
186 | } | ||
187 | } | ||
188 | T![.] => return "", | ||
189 | _ => (), | ||
190 | } | ||
191 | " " | ||
192 | } | ||
193 | |||
194 | #[cfg(test)] | ||
195 | mod tests { | ||
196 | use syntax::SourceFile; | ||
197 | use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; | ||
198 | |||
199 | use super::*; | ||
200 | |||
201 | fn check_join_lines(before: &str, after: &str) { | ||
202 | let (before_cursor_pos, before) = extract_offset(before); | ||
203 | let file = SourceFile::parse(&before).ok().unwrap(); | ||
204 | |||
205 | let range = TextRange::empty(before_cursor_pos); | ||
206 | let result = join_lines(&file, range); | ||
207 | |||
208 | let actual = { | ||
209 | let mut actual = before.to_string(); | ||
210 | result.apply(&mut actual); | ||
211 | actual | ||
212 | }; | ||
213 | let actual_cursor_pos = result | ||
214 | .apply_to_offset(before_cursor_pos) | ||
215 | .expect("cursor position is affected by the edit"); | ||
216 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
217 | assert_eq_text!(after, &actual); | ||
218 | } | ||
219 | |||
220 | #[test] | ||
221 | fn test_join_lines_comma() { | ||
222 | check_join_lines( | ||
223 | r" | ||
224 | fn foo() { | ||
225 | <|>foo(1, | ||
226 | ) | ||
227 | } | ||
228 | ", | ||
229 | r" | ||
230 | fn foo() { | ||
231 | <|>foo(1) | ||
232 | } | ||
233 | ", | ||
234 | ); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn test_join_lines_lambda_block() { | ||
239 | check_join_lines( | ||
240 | r" | ||
241 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
242 | <|>self.incremental_reparse(edit).unwrap_or_else(|| { | ||
243 | self.full_reparse(edit) | ||
244 | }) | ||
245 | } | ||
246 | ", | ||
247 | r" | ||
248 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
249 | <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) | ||
250 | } | ||
251 | ", | ||
252 | ); | ||
253 | } | ||
254 | |||
255 | #[test] | ||
256 | fn test_join_lines_block() { | ||
257 | check_join_lines( | ||
258 | r" | ||
259 | fn foo() { | ||
260 | foo(<|>{ | ||
261 | 92 | ||
262 | }) | ||
263 | }", | ||
264 | r" | ||
265 | fn foo() { | ||
266 | foo(<|>92) | ||
267 | }", | ||
268 | ); | ||
269 | } | ||
270 | |||
271 | #[test] | ||
272 | fn test_join_lines_diverging_block() { | ||
273 | let before = r" | ||
274 | fn foo() { | ||
275 | loop { | ||
276 | match x { | ||
277 | 92 => <|>{ | ||
278 | continue; | ||
279 | } | ||
280 | } | ||
281 | } | ||
282 | } | ||
283 | "; | ||
284 | let after = r" | ||
285 | fn foo() { | ||
286 | loop { | ||
287 | match x { | ||
288 | 92 => <|>continue, | ||
289 | } | ||
290 | } | ||
291 | } | ||
292 | "; | ||
293 | check_join_lines(before, after); | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn join_lines_adds_comma_for_block_in_match_arm() { | ||
298 | check_join_lines( | ||
299 | r" | ||
300 | fn foo(e: Result<U, V>) { | ||
301 | match e { | ||
302 | Ok(u) => <|>{ | ||
303 | u.foo() | ||
304 | } | ||
305 | Err(v) => v, | ||
306 | } | ||
307 | }", | ||
308 | r" | ||
309 | fn foo(e: Result<U, V>) { | ||
310 | match e { | ||
311 | Ok(u) => <|>u.foo(), | ||
312 | Err(v) => v, | ||
313 | } | ||
314 | }", | ||
315 | ); | ||
316 | } | ||
317 | |||
318 | #[test] | ||
319 | fn join_lines_multiline_in_block() { | ||
320 | check_join_lines( | ||
321 | r" | ||
322 | fn foo() { | ||
323 | match ty { | ||
324 | <|> Some(ty) => { | ||
325 | match ty { | ||
326 | _ => false, | ||
327 | } | ||
328 | } | ||
329 | _ => true, | ||
330 | } | ||
331 | } | ||
332 | ", | ||
333 | r" | ||
334 | fn foo() { | ||
335 | match ty { | ||
336 | <|> Some(ty) => match ty { | ||
337 | _ => false, | ||
338 | }, | ||
339 | _ => true, | ||
340 | } | ||
341 | } | ||
342 | ", | ||
343 | ); | ||
344 | } | ||
345 | |||
346 | #[test] | ||
347 | fn join_lines_keeps_comma_for_block_in_match_arm() { | ||
348 | // We already have a comma | ||
349 | check_join_lines( | ||
350 | r" | ||
351 | fn foo(e: Result<U, V>) { | ||
352 | match e { | ||
353 | Ok(u) => <|>{ | ||
354 | u.foo() | ||
355 | }, | ||
356 | Err(v) => v, | ||
357 | } | ||
358 | }", | ||
359 | r" | ||
360 | fn foo(e: Result<U, V>) { | ||
361 | match e { | ||
362 | Ok(u) => <|>u.foo(), | ||
363 | Err(v) => v, | ||
364 | } | ||
365 | }", | ||
366 | ); | ||
367 | |||
368 | // comma with whitespace between brace and , | ||
369 | check_join_lines( | ||
370 | r" | ||
371 | fn foo(e: Result<U, V>) { | ||
372 | match e { | ||
373 | Ok(u) => <|>{ | ||
374 | u.foo() | ||
375 | } , | ||
376 | Err(v) => v, | ||
377 | } | ||
378 | }", | ||
379 | r" | ||
380 | fn foo(e: Result<U, V>) { | ||
381 | match e { | ||
382 | Ok(u) => <|>u.foo() , | ||
383 | Err(v) => v, | ||
384 | } | ||
385 | }", | ||
386 | ); | ||
387 | |||
388 | // comma with newline between brace and , | ||
389 | check_join_lines( | ||
390 | r" | ||
391 | fn foo(e: Result<U, V>) { | ||
392 | match e { | ||
393 | Ok(u) => <|>{ | ||
394 | u.foo() | ||
395 | } | ||
396 | , | ||
397 | Err(v) => v, | ||
398 | } | ||
399 | }", | ||
400 | r" | ||
401 | fn foo(e: Result<U, V>) { | ||
402 | match e { | ||
403 | Ok(u) => <|>u.foo() | ||
404 | , | ||
405 | Err(v) => v, | ||
406 | } | ||
407 | }", | ||
408 | ); | ||
409 | } | ||
410 | |||
411 | #[test] | ||
412 | fn join_lines_keeps_comma_with_single_arg_tuple() { | ||
413 | // A single arg tuple | ||
414 | check_join_lines( | ||
415 | r" | ||
416 | fn foo() { | ||
417 | let x = (<|>{ | ||
418 | 4 | ||
419 | },); | ||
420 | }", | ||
421 | r" | ||
422 | fn foo() { | ||
423 | let x = (<|>4,); | ||
424 | }", | ||
425 | ); | ||
426 | |||
427 | // single arg tuple with whitespace between brace and comma | ||
428 | check_join_lines( | ||
429 | r" | ||
430 | fn foo() { | ||
431 | let x = (<|>{ | ||
432 | 4 | ||
433 | } ,); | ||
434 | }", | ||
435 | r" | ||
436 | fn foo() { | ||
437 | let x = (<|>4 ,); | ||
438 | }", | ||
439 | ); | ||
440 | |||
441 | // single arg tuple with newline between brace and comma | ||
442 | check_join_lines( | ||
443 | r" | ||
444 | fn foo() { | ||
445 | let x = (<|>{ | ||
446 | 4 | ||
447 | } | ||
448 | ,); | ||
449 | }", | ||
450 | r" | ||
451 | fn foo() { | ||
452 | let x = (<|>4 | ||
453 | ,); | ||
454 | }", | ||
455 | ); | ||
456 | } | ||
457 | |||
458 | #[test] | ||
459 | fn test_join_lines_use_items_left() { | ||
460 | // No space after the '{' | ||
461 | check_join_lines( | ||
462 | r" | ||
463 | <|>use syntax::{ | ||
464 | TextSize, TextRange, | ||
465 | };", | ||
466 | r" | ||
467 | <|>use syntax::{TextSize, TextRange, | ||
468 | };", | ||
469 | ); | ||
470 | } | ||
471 | |||
472 | #[test] | ||
473 | fn test_join_lines_use_items_right() { | ||
474 | // No space after the '}' | ||
475 | check_join_lines( | ||
476 | r" | ||
477 | use syntax::{ | ||
478 | <|> TextSize, TextRange | ||
479 | };", | ||
480 | r" | ||
481 | use syntax::{ | ||
482 | <|> TextSize, TextRange};", | ||
483 | ); | ||
484 | } | ||
485 | |||
486 | #[test] | ||
487 | fn test_join_lines_use_items_right_comma() { | ||
488 | // No space after the '}' | ||
489 | check_join_lines( | ||
490 | r" | ||
491 | use syntax::{ | ||
492 | <|> TextSize, TextRange, | ||
493 | };", | ||
494 | r" | ||
495 | use syntax::{ | ||
496 | <|> TextSize, TextRange};", | ||
497 | ); | ||
498 | } | ||
499 | |||
500 | #[test] | ||
501 | fn test_join_lines_use_tree() { | ||
502 | check_join_lines( | ||
503 | r" | ||
504 | use syntax::{ | ||
505 | algo::<|>{ | ||
506 | find_token_at_offset, | ||
507 | }, | ||
508 | ast, | ||
509 | };", | ||
510 | r" | ||
511 | use syntax::{ | ||
512 | algo::<|>find_token_at_offset, | ||
513 | ast, | ||
514 | };", | ||
515 | ); | ||
516 | } | ||
517 | |||
518 | #[test] | ||
519 | fn test_join_lines_normal_comments() { | ||
520 | check_join_lines( | ||
521 | r" | ||
522 | fn foo() { | ||
523 | // Hello<|> | ||
524 | // world! | ||
525 | } | ||
526 | ", | ||
527 | r" | ||
528 | fn foo() { | ||
529 | // Hello<|> world! | ||
530 | } | ||
531 | ", | ||
532 | ); | ||
533 | } | ||
534 | |||
535 | #[test] | ||
536 | fn test_join_lines_doc_comments() { | ||
537 | check_join_lines( | ||
538 | r" | ||
539 | fn foo() { | ||
540 | /// Hello<|> | ||
541 | /// world! | ||
542 | } | ||
543 | ", | ||
544 | r" | ||
545 | fn foo() { | ||
546 | /// Hello<|> world! | ||
547 | } | ||
548 | ", | ||
549 | ); | ||
550 | } | ||
551 | |||
552 | #[test] | ||
553 | fn test_join_lines_mod_comments() { | ||
554 | check_join_lines( | ||
555 | r" | ||
556 | fn foo() { | ||
557 | //! Hello<|> | ||
558 | //! world! | ||
559 | } | ||
560 | ", | ||
561 | r" | ||
562 | fn foo() { | ||
563 | //! Hello<|> world! | ||
564 | } | ||
565 | ", | ||
566 | ); | ||
567 | } | ||
568 | |||
569 | #[test] | ||
570 | fn test_join_lines_multiline_comments_1() { | ||
571 | check_join_lines( | ||
572 | r" | ||
573 | fn foo() { | ||
574 | // Hello<|> | ||
575 | /* world! */ | ||
576 | } | ||
577 | ", | ||
578 | r" | ||
579 | fn foo() { | ||
580 | // Hello<|> world! */ | ||
581 | } | ||
582 | ", | ||
583 | ); | ||
584 | } | ||
585 | |||
586 | #[test] | ||
587 | fn test_join_lines_multiline_comments_2() { | ||
588 | check_join_lines( | ||
589 | r" | ||
590 | fn foo() { | ||
591 | // The<|> | ||
592 | /* quick | ||
593 | brown | ||
594 | fox! */ | ||
595 | } | ||
596 | ", | ||
597 | r" | ||
598 | fn foo() { | ||
599 | // The<|> quick | ||
600 | brown | ||
601 | fox! */ | ||
602 | } | ||
603 | ", | ||
604 | ); | ||
605 | } | ||
606 | |||
607 | fn check_join_lines_sel(before: &str, after: &str) { | ||
608 | let (sel, before) = extract_range(before); | ||
609 | let parse = SourceFile::parse(&before); | ||
610 | let result = join_lines(&parse.tree(), sel); | ||
611 | let actual = { | ||
612 | let mut actual = before.to_string(); | ||
613 | result.apply(&mut actual); | ||
614 | actual | ||
615 | }; | ||
616 | assert_eq_text!(after, &actual); | ||
617 | } | ||
618 | |||
619 | #[test] | ||
620 | fn test_join_lines_selection_fn_args() { | ||
621 | check_join_lines_sel( | ||
622 | r" | ||
623 | fn foo() { | ||
624 | <|>foo(1, | ||
625 | 2, | ||
626 | 3, | ||
627 | <|>) | ||
628 | } | ||
629 | ", | ||
630 | r" | ||
631 | fn foo() { | ||
632 | foo(1, 2, 3) | ||
633 | } | ||
634 | ", | ||
635 | ); | ||
636 | } | ||
637 | |||
638 | #[test] | ||
639 | fn test_join_lines_selection_struct() { | ||
640 | check_join_lines_sel( | ||
641 | r" | ||
642 | struct Foo <|>{ | ||
643 | f: u32, | ||
644 | }<|> | ||
645 | ", | ||
646 | r" | ||
647 | struct Foo { f: u32 } | ||
648 | ", | ||
649 | ); | ||
650 | } | ||
651 | |||
652 | #[test] | ||
653 | fn test_join_lines_selection_dot_chain() { | ||
654 | check_join_lines_sel( | ||
655 | r" | ||
656 | fn foo() { | ||
657 | join(<|>type_params.type_params() | ||
658 | .filter_map(|it| it.name()) | ||
659 | .map(|it| it.text())<|>) | ||
660 | }", | ||
661 | r" | ||
662 | fn foo() { | ||
663 | join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) | ||
664 | }", | ||
665 | ); | ||
666 | } | ||
667 | |||
668 | #[test] | ||
669 | fn test_join_lines_selection_lambda_block_body() { | ||
670 | check_join_lines_sel( | ||
671 | r" | ||
672 | pub fn handle_find_matching_brace() { | ||
673 | params.offsets | ||
674 | .map(|offset| <|>{ | ||
675 | world.analysis().matching_brace(&file, offset).unwrap_or(offset) | ||
676 | }<|>) | ||
677 | .collect(); | ||
678 | }", | ||
679 | r" | ||
680 | pub fn handle_find_matching_brace() { | ||
681 | params.offsets | ||
682 | .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) | ||
683 | .collect(); | ||
684 | }", | ||
685 | ); | ||
686 | } | ||
687 | |||
688 | #[test] | ||
689 | fn test_join_lines_commented_block() { | ||
690 | check_join_lines( | ||
691 | r" | ||
692 | fn main() { | ||
693 | let _ = { | ||
694 | // <|>foo | ||
695 | // bar | ||
696 | 92 | ||
697 | }; | ||
698 | } | ||
699 | ", | ||
700 | r" | ||
701 | fn main() { | ||
702 | let _ = { | ||
703 | // <|>foo bar | ||
704 | 92 | ||
705 | }; | ||
706 | } | ||
707 | ", | ||
708 | ) | ||
709 | } | ||
710 | |||
711 | #[test] | ||
712 | fn join_lines_mandatory_blocks_block() { | ||
713 | check_join_lines( | ||
714 | r" | ||
715 | <|>fn foo() { | ||
716 | 92 | ||
717 | } | ||
718 | ", | ||
719 | r" | ||
720 | <|>fn foo() { 92 | ||
721 | } | ||
722 | ", | ||
723 | ); | ||
724 | |||
725 | check_join_lines( | ||
726 | r" | ||
727 | fn foo() { | ||
728 | <|>if true { | ||
729 | 92 | ||
730 | } | ||
731 | } | ||
732 | ", | ||
733 | r" | ||
734 | fn foo() { | ||
735 | <|>if true { 92 | ||
736 | } | ||
737 | } | ||
738 | ", | ||
739 | ); | ||
740 | |||
741 | check_join_lines( | ||
742 | r" | ||
743 | fn foo() { | ||
744 | <|>loop { | ||
745 | 92 | ||
746 | } | ||
747 | } | ||
748 | ", | ||
749 | r" | ||
750 | fn foo() { | ||
751 | <|>loop { 92 | ||
752 | } | ||
753 | } | ||
754 | ", | ||
755 | ); | ||
756 | |||
757 | check_join_lines( | ||
758 | r" | ||
759 | fn foo() { | ||
760 | <|>unsafe { | ||
761 | 92 | ||
762 | } | ||
763 | } | ||
764 | ", | ||
765 | r" | ||
766 | fn foo() { | ||
767 | <|>unsafe { 92 | ||
768 | } | ||
769 | } | ||
770 | ", | ||
771 | ); | ||
772 | } | ||
773 | } | ||
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs new file mode 100644 index 000000000..eb6389529 --- /dev/null +++ b/crates/ide/src/lib.rs | |||
@@ -0,0 +1,542 @@ | |||
1 | //! ide crate provides "ide-centric" APIs for the rust-analyzer. That is, | ||
2 | //! it generally operates with files and text ranges, and returns results as | ||
3 | //! Strings, suitable for displaying to the human. | ||
4 | //! | ||
5 | //! What powers this API are the `RootDatabase` struct, which defines a `salsa` | ||
6 | //! database, and the `hir` crate, where majority of the analysis happens. | ||
7 | //! However, IDE specific bits of the analysis (most notably completion) happen | ||
8 | //! in this crate. | ||
9 | |||
10 | // For proving that RootDatabase is RefUnwindSafe. | ||
11 | #![recursion_limit = "128"] | ||
12 | |||
13 | #[allow(unused)] | ||
14 | macro_rules! eprintln { | ||
15 | ($($tt:tt)*) => { stdx::eprintln!($($tt)*) }; | ||
16 | } | ||
17 | |||
18 | pub mod mock_analysis; | ||
19 | |||
20 | mod markup; | ||
21 | mod prime_caches; | ||
22 | mod display; | ||
23 | |||
24 | mod call_hierarchy; | ||
25 | mod call_info; | ||
26 | mod completion; | ||
27 | mod diagnostics; | ||
28 | mod expand_macro; | ||
29 | mod extend_selection; | ||
30 | mod file_structure; | ||
31 | mod folding_ranges; | ||
32 | mod goto_definition; | ||
33 | mod goto_implementation; | ||
34 | mod goto_type_definition; | ||
35 | mod hover; | ||
36 | mod inlay_hints; | ||
37 | mod join_lines; | ||
38 | mod matching_brace; | ||
39 | mod parent_module; | ||
40 | mod references; | ||
41 | mod runnables; | ||
42 | mod status; | ||
43 | mod syntax_highlighting; | ||
44 | mod syntax_tree; | ||
45 | mod typing; | ||
46 | |||
47 | use std::sync::Arc; | ||
48 | |||
49 | use base_db::{ | ||
50 | salsa::{self, ParallelDatabase}, | ||
51 | CheckCanceled, Env, FileLoader, FileSet, SourceDatabase, VfsPath, | ||
52 | }; | ||
53 | use cfg::CfgOptions; | ||
54 | use ide_db::{ | ||
55 | symbol_index::{self, FileSymbol}, | ||
56 | LineIndexDatabase, | ||
57 | }; | ||
58 | use syntax::{SourceFile, TextRange, TextSize}; | ||
59 | |||
60 | use crate::display::ToNav; | ||
61 | |||
62 | pub use crate::{ | ||
63 | call_hierarchy::CallItem, | ||
64 | call_info::CallInfo, | ||
65 | completion::{ | ||
66 | CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat, | ||
67 | }, | ||
68 | diagnostics::Severity, | ||
69 | display::NavigationTarget, | ||
70 | expand_macro::ExpandedMacro, | ||
71 | file_structure::StructureNode, | ||
72 | folding_ranges::{Fold, FoldKind}, | ||
73 | hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, | ||
74 | inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, | ||
75 | markup::Markup, | ||
76 | references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult}, | ||
77 | runnables::{Runnable, RunnableKind, TestId}, | ||
78 | syntax_highlighting::{ | ||
79 | Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange, | ||
80 | }, | ||
81 | }; | ||
82 | |||
83 | pub use assists::{Assist, AssistConfig, AssistId, AssistKind, ResolvedAssist}; | ||
84 | pub use base_db::{ | ||
85 | Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRoot, | ||
86 | SourceRootId, | ||
87 | }; | ||
88 | pub use hir::{Documentation, Semantics}; | ||
89 | pub use ide_db::{ | ||
90 | change::AnalysisChange, | ||
91 | line_index::{LineCol, LineIndex}, | ||
92 | search::SearchScope, | ||
93 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, | ||
94 | symbol_index::Query, | ||
95 | RootDatabase, | ||
96 | }; | ||
97 | pub use ssr::SsrError; | ||
98 | pub use text_edit::{Indel, TextEdit}; | ||
99 | |||
100 | pub type Cancelable<T> = Result<T, Canceled>; | ||
101 | |||
102 | #[derive(Debug)] | ||
103 | pub struct Diagnostic { | ||
104 | pub message: String, | ||
105 | pub range: TextRange, | ||
106 | pub severity: Severity, | ||
107 | pub fix: Option<Fix>, | ||
108 | } | ||
109 | |||
110 | #[derive(Debug)] | ||
111 | pub struct Fix { | ||
112 | pub label: String, | ||
113 | pub source_change: SourceChange, | ||
114 | /// Allows to trigger the fix only when the caret is in the range given | ||
115 | pub fix_trigger_range: TextRange, | ||
116 | } | ||
117 | |||
118 | impl Fix { | ||
119 | pub fn new( | ||
120 | label: impl Into<String>, | ||
121 | source_change: SourceChange, | ||
122 | fix_trigger_range: TextRange, | ||
123 | ) -> Self { | ||
124 | let label = label.into(); | ||
125 | assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.')); | ||
126 | Self { label, source_change, fix_trigger_range } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | /// Info associated with a text range. | ||
131 | #[derive(Debug)] | ||
132 | pub struct RangeInfo<T> { | ||
133 | pub range: TextRange, | ||
134 | pub info: T, | ||
135 | } | ||
136 | |||
137 | impl<T> RangeInfo<T> { | ||
138 | pub fn new(range: TextRange, info: T) -> RangeInfo<T> { | ||
139 | RangeInfo { range, info } | ||
140 | } | ||
141 | } | ||
142 | |||
143 | /// `AnalysisHost` stores the current state of the world. | ||
144 | #[derive(Debug)] | ||
145 | pub struct AnalysisHost { | ||
146 | db: RootDatabase, | ||
147 | } | ||
148 | |||
149 | impl AnalysisHost { | ||
150 | pub fn new(lru_capacity: Option<usize>) -> AnalysisHost { | ||
151 | AnalysisHost { db: RootDatabase::new(lru_capacity) } | ||
152 | } | ||
153 | |||
154 | pub fn update_lru_capacity(&mut self, lru_capacity: Option<usize>) { | ||
155 | self.db.update_lru_capacity(lru_capacity); | ||
156 | } | ||
157 | |||
158 | /// Returns a snapshot of the current state, which you can query for | ||
159 | /// semantic information. | ||
160 | pub fn analysis(&self) -> Analysis { | ||
161 | Analysis { db: self.db.snapshot() } | ||
162 | } | ||
163 | |||
164 | /// Applies changes to the current state of the world. If there are | ||
165 | /// outstanding snapshots, they will be canceled. | ||
166 | pub fn apply_change(&mut self, change: AnalysisChange) { | ||
167 | self.db.apply_change(change) | ||
168 | } | ||
169 | |||
170 | pub fn maybe_collect_garbage(&mut self) { | ||
171 | self.db.maybe_collect_garbage(); | ||
172 | } | ||
173 | |||
174 | pub fn collect_garbage(&mut self) { | ||
175 | self.db.collect_garbage(); | ||
176 | } | ||
177 | /// NB: this clears the database | ||
178 | pub fn per_query_memory_usage(&mut self) -> Vec<(String, profile::Bytes)> { | ||
179 | self.db.per_query_memory_usage() | ||
180 | } | ||
181 | pub fn request_cancellation(&mut self) { | ||
182 | self.db.request_cancellation(); | ||
183 | } | ||
184 | pub fn raw_database(&self) -> &RootDatabase { | ||
185 | &self.db | ||
186 | } | ||
187 | pub fn raw_database_mut(&mut self) -> &mut RootDatabase { | ||
188 | &mut self.db | ||
189 | } | ||
190 | } | ||
191 | |||
192 | impl Default for AnalysisHost { | ||
193 | fn default() -> AnalysisHost { | ||
194 | AnalysisHost::new(None) | ||
195 | } | ||
196 | } | ||
197 | |||
198 | /// Analysis is a snapshot of a world state at a moment in time. It is the main | ||
199 | /// entry point for asking semantic information about the world. When the world | ||
200 | /// state is advanced using `AnalysisHost::apply_change` method, all existing | ||
201 | /// `Analysis` are canceled (most method return `Err(Canceled)`). | ||
202 | #[derive(Debug)] | ||
203 | pub struct Analysis { | ||
204 | db: salsa::Snapshot<RootDatabase>, | ||
205 | } | ||
206 | |||
207 | // As a general design guideline, `Analysis` API are intended to be independent | ||
208 | // from the language server protocol. That is, when exposing some functionality | ||
209 | // we should think in terms of "what API makes most sense" and not in terms of | ||
210 | // "what types LSP uses". Although currently LSP is the only consumer of the | ||
211 | // API, the API should in theory be usable as a library, or via a different | ||
212 | // protocol. | ||
213 | impl Analysis { | ||
214 | // Creates an analysis instance for a single file, without any extenal | ||
215 | // dependencies, stdlib support or ability to apply changes. See | ||
216 | // `AnalysisHost` for creating a fully-featured analysis. | ||
217 | pub fn from_single_file(text: String) -> (Analysis, FileId) { | ||
218 | let mut host = AnalysisHost::default(); | ||
219 | let file_id = FileId(0); | ||
220 | let mut file_set = FileSet::default(); | ||
221 | file_set.insert(file_id, VfsPath::new_virtual_path("/main.rs".to_string())); | ||
222 | let source_root = SourceRoot::new_local(file_set); | ||
223 | |||
224 | let mut change = AnalysisChange::new(); | ||
225 | change.set_roots(vec![source_root]); | ||
226 | let mut crate_graph = CrateGraph::default(); | ||
227 | // FIXME: cfg options | ||
228 | // Default to enable test for single file. | ||
229 | let mut cfg_options = CfgOptions::default(); | ||
230 | cfg_options.insert_atom("test".into()); | ||
231 | crate_graph.add_crate_root( | ||
232 | file_id, | ||
233 | Edition::Edition2018, | ||
234 | None, | ||
235 | cfg_options, | ||
236 | Env::default(), | ||
237 | Default::default(), | ||
238 | ); | ||
239 | change.change_file(file_id, Some(Arc::new(text))); | ||
240 | change.set_crate_graph(crate_graph); | ||
241 | host.apply_change(change); | ||
242 | (host.analysis(), file_id) | ||
243 | } | ||
244 | |||
245 | /// Debug info about the current state of the analysis. | ||
246 | pub fn status(&self) -> Cancelable<String> { | ||
247 | self.with_db(|db| status::status(&*db)) | ||
248 | } | ||
249 | |||
250 | pub fn prime_caches(&self, files: Vec<FileId>) -> Cancelable<()> { | ||
251 | self.with_db(|db| prime_caches::prime_caches(db, files)) | ||
252 | } | ||
253 | |||
254 | /// Gets the text of the source file. | ||
255 | pub fn file_text(&self, file_id: FileId) -> Cancelable<Arc<String>> { | ||
256 | self.with_db(|db| db.file_text(file_id)) | ||
257 | } | ||
258 | |||
259 | /// Gets the syntax tree of the file. | ||
260 | pub fn parse(&self, file_id: FileId) -> Cancelable<SourceFile> { | ||
261 | self.with_db(|db| db.parse(file_id).tree()) | ||
262 | } | ||
263 | |||
264 | /// Gets the file's `LineIndex`: data structure to convert between absolute | ||
265 | /// offsets and line/column representation. | ||
266 | pub fn file_line_index(&self, file_id: FileId) -> Cancelable<Arc<LineIndex>> { | ||
267 | self.with_db(|db| db.line_index(file_id)) | ||
268 | } | ||
269 | |||
270 | /// Selects the next syntactic nodes encompassing the range. | ||
271 | pub fn extend_selection(&self, frange: FileRange) -> Cancelable<TextRange> { | ||
272 | self.with_db(|db| extend_selection::extend_selection(db, frange)) | ||
273 | } | ||
274 | |||
275 | /// Returns position of the matching brace (all types of braces are | ||
276 | /// supported). | ||
277 | pub fn matching_brace(&self, position: FilePosition) -> Cancelable<Option<TextSize>> { | ||
278 | self.with_db(|db| { | ||
279 | let parse = db.parse(position.file_id); | ||
280 | let file = parse.tree(); | ||
281 | matching_brace::matching_brace(&file, position.offset) | ||
282 | }) | ||
283 | } | ||
284 | |||
285 | /// Returns a syntax tree represented as `String`, for debug purposes. | ||
286 | // FIXME: use a better name here. | ||
287 | pub fn syntax_tree( | ||
288 | &self, | ||
289 | file_id: FileId, | ||
290 | text_range: Option<TextRange>, | ||
291 | ) -> Cancelable<String> { | ||
292 | self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) | ||
293 | } | ||
294 | |||
295 | pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> { | ||
296 | self.with_db(|db| expand_macro::expand_macro(db, position)) | ||
297 | } | ||
298 | |||
299 | /// Returns an edit to remove all newlines in the range, cleaning up minor | ||
300 | /// stuff like trailing commas. | ||
301 | pub fn join_lines(&self, frange: FileRange) -> Cancelable<TextEdit> { | ||
302 | self.with_db(|db| { | ||
303 | let parse = db.parse(frange.file_id); | ||
304 | join_lines::join_lines(&parse.tree(), frange.range) | ||
305 | }) | ||
306 | } | ||
307 | |||
308 | /// Returns an edit which should be applied when opening a new line, fixing | ||
309 | /// up minor stuff like continuing the comment. | ||
310 | /// The edit will be a snippet (with `$0`). | ||
311 | pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<TextEdit>> { | ||
312 | self.with_db(|db| typing::on_enter(&db, position)) | ||
313 | } | ||
314 | |||
315 | /// Returns an edit which should be applied after a character was typed. | ||
316 | /// | ||
317 | /// This is useful for some on-the-fly fixups, like adding `;` to `let =` | ||
318 | /// automatically. | ||
319 | pub fn on_char_typed( | ||
320 | &self, | ||
321 | position: FilePosition, | ||
322 | char_typed: char, | ||
323 | ) -> Cancelable<Option<SourceChange>> { | ||
324 | // Fast path to not even parse the file. | ||
325 | if !typing::TRIGGER_CHARS.contains(char_typed) { | ||
326 | return Ok(None); | ||
327 | } | ||
328 | self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) | ||
329 | } | ||
330 | |||
331 | /// Returns a tree representation of symbols in the file. Useful to draw a | ||
332 | /// file outline. | ||
333 | pub fn file_structure(&self, file_id: FileId) -> Cancelable<Vec<StructureNode>> { | ||
334 | self.with_db(|db| file_structure::file_structure(&db.parse(file_id).tree())) | ||
335 | } | ||
336 | |||
337 | /// Returns a list of the places in the file where type hints can be displayed. | ||
338 | pub fn inlay_hints( | ||
339 | &self, | ||
340 | file_id: FileId, | ||
341 | config: &InlayHintsConfig, | ||
342 | ) -> Cancelable<Vec<InlayHint>> { | ||
343 | self.with_db(|db| inlay_hints::inlay_hints(db, file_id, config)) | ||
344 | } | ||
345 | |||
346 | /// Returns the set of folding ranges. | ||
347 | pub fn folding_ranges(&self, file_id: FileId) -> Cancelable<Vec<Fold>> { | ||
348 | self.with_db(|db| folding_ranges::folding_ranges(&db.parse(file_id).tree())) | ||
349 | } | ||
350 | |||
351 | /// Fuzzy searches for a symbol. | ||
352 | pub fn symbol_search(&self, query: Query) -> Cancelable<Vec<NavigationTarget>> { | ||
353 | self.with_db(|db| { | ||
354 | symbol_index::world_symbols(db, query) | ||
355 | .into_iter() | ||
356 | .map(|s| s.to_nav(db)) | ||
357 | .collect::<Vec<_>>() | ||
358 | }) | ||
359 | } | ||
360 | |||
361 | /// Returns the definitions from the symbol at `position`. | ||
362 | pub fn goto_definition( | ||
363 | &self, | ||
364 | position: FilePosition, | ||
365 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
366 | self.with_db(|db| goto_definition::goto_definition(db, position)) | ||
367 | } | ||
368 | |||
369 | /// Returns the impls from the symbol at `position`. | ||
370 | pub fn goto_implementation( | ||
371 | &self, | ||
372 | position: FilePosition, | ||
373 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
374 | self.with_db(|db| goto_implementation::goto_implementation(db, position)) | ||
375 | } | ||
376 | |||
377 | /// Returns the type definitions for the symbol at `position`. | ||
378 | pub fn goto_type_definition( | ||
379 | &self, | ||
380 | position: FilePosition, | ||
381 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
382 | self.with_db(|db| goto_type_definition::goto_type_definition(db, position)) | ||
383 | } | ||
384 | |||
385 | /// Finds all usages of the reference at point. | ||
386 | pub fn find_all_refs( | ||
387 | &self, | ||
388 | position: FilePosition, | ||
389 | search_scope: Option<SearchScope>, | ||
390 | ) -> Cancelable<Option<ReferenceSearchResult>> { | ||
391 | self.with_db(|db| { | ||
392 | references::find_all_refs(&Semantics::new(db), position, search_scope).map(|it| it.info) | ||
393 | }) | ||
394 | } | ||
395 | |||
396 | /// Returns a short text describing element at position. | ||
397 | pub fn hover(&self, position: FilePosition) -> Cancelable<Option<RangeInfo<HoverResult>>> { | ||
398 | self.with_db(|db| hover::hover(db, position)) | ||
399 | } | ||
400 | |||
401 | /// Computes parameter information for the given call expression. | ||
402 | pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> { | ||
403 | self.with_db(|db| call_info::call_info(db, position)) | ||
404 | } | ||
405 | |||
406 | /// Computes call hierarchy candidates for the given file position. | ||
407 | pub fn call_hierarchy( | ||
408 | &self, | ||
409 | position: FilePosition, | ||
410 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
411 | self.with_db(|db| call_hierarchy::call_hierarchy(db, position)) | ||
412 | } | ||
413 | |||
414 | /// Computes incoming calls for the given file position. | ||
415 | pub fn incoming_calls(&self, position: FilePosition) -> Cancelable<Option<Vec<CallItem>>> { | ||
416 | self.with_db(|db| call_hierarchy::incoming_calls(db, position)) | ||
417 | } | ||
418 | |||
419 | /// Computes incoming calls for the given file position. | ||
420 | pub fn outgoing_calls(&self, position: FilePosition) -> Cancelable<Option<Vec<CallItem>>> { | ||
421 | self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) | ||
422 | } | ||
423 | |||
424 | /// Returns a `mod name;` declaration which created the current module. | ||
425 | pub fn parent_module(&self, position: FilePosition) -> Cancelable<Vec<NavigationTarget>> { | ||
426 | self.with_db(|db| parent_module::parent_module(db, position)) | ||
427 | } | ||
428 | |||
429 | /// Returns crates this file belongs too. | ||
430 | pub fn crate_for(&self, file_id: FileId) -> Cancelable<Vec<CrateId>> { | ||
431 | self.with_db(|db| parent_module::crate_for(db, file_id)) | ||
432 | } | ||
433 | |||
434 | /// Returns the edition of the given crate. | ||
435 | pub fn crate_edition(&self, crate_id: CrateId) -> Cancelable<Edition> { | ||
436 | self.with_db(|db| db.crate_graph()[crate_id].edition) | ||
437 | } | ||
438 | |||
439 | /// Returns the root file of the given crate. | ||
440 | pub fn crate_root(&self, crate_id: CrateId) -> Cancelable<FileId> { | ||
441 | self.with_db(|db| db.crate_graph()[crate_id].root_file_id) | ||
442 | } | ||
443 | |||
444 | /// Returns the set of possible targets to run for the current file. | ||
445 | pub fn runnables(&self, file_id: FileId) -> Cancelable<Vec<Runnable>> { | ||
446 | self.with_db(|db| runnables::runnables(db, file_id)) | ||
447 | } | ||
448 | |||
449 | /// Computes syntax highlighting for the given file | ||
450 | pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HighlightedRange>> { | ||
451 | self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) | ||
452 | } | ||
453 | |||
454 | /// Computes syntax highlighting for the given file range. | ||
455 | pub fn highlight_range(&self, frange: FileRange) -> Cancelable<Vec<HighlightedRange>> { | ||
456 | self.with_db(|db| { | ||
457 | syntax_highlighting::highlight(db, frange.file_id, Some(frange.range), false) | ||
458 | }) | ||
459 | } | ||
460 | |||
461 | /// Computes syntax highlighting for the given file. | ||
462 | pub fn highlight_as_html(&self, file_id: FileId, rainbow: bool) -> Cancelable<String> { | ||
463 | self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id, rainbow)) | ||
464 | } | ||
465 | |||
466 | /// Computes completions at the given position. | ||
467 | pub fn completions( | ||
468 | &self, | ||
469 | config: &CompletionConfig, | ||
470 | position: FilePosition, | ||
471 | ) -> Cancelable<Option<Vec<CompletionItem>>> { | ||
472 | self.with_db(|db| completion::completions(db, config, position).map(Into::into)) | ||
473 | } | ||
474 | |||
475 | /// Computes resolved assists with source changes for the given position. | ||
476 | pub fn resolved_assists( | ||
477 | &self, | ||
478 | config: &AssistConfig, | ||
479 | frange: FileRange, | ||
480 | ) -> Cancelable<Vec<ResolvedAssist>> { | ||
481 | self.with_db(|db| assists::Assist::resolved(db, config, frange)) | ||
482 | } | ||
483 | |||
484 | /// Computes unresolved assists (aka code actions aka intentions) for the given | ||
485 | /// position. | ||
486 | pub fn unresolved_assists( | ||
487 | &self, | ||
488 | config: &AssistConfig, | ||
489 | frange: FileRange, | ||
490 | ) -> Cancelable<Vec<Assist>> { | ||
491 | self.with_db(|db| Assist::unresolved(db, config, frange)) | ||
492 | } | ||
493 | |||
494 | /// Computes the set of diagnostics for the given file. | ||
495 | pub fn diagnostics( | ||
496 | &self, | ||
497 | file_id: FileId, | ||
498 | enable_experimental: bool, | ||
499 | ) -> Cancelable<Vec<Diagnostic>> { | ||
500 | self.with_db(|db| diagnostics::diagnostics(db, file_id, enable_experimental)) | ||
501 | } | ||
502 | |||
503 | /// Returns the edit required to rename reference at the position to the new | ||
504 | /// name. | ||
505 | pub fn rename( | ||
506 | &self, | ||
507 | position: FilePosition, | ||
508 | new_name: &str, | ||
509 | ) -> Cancelable<Option<RangeInfo<SourceChange>>> { | ||
510 | self.with_db(|db| references::rename(db, position, new_name)) | ||
511 | } | ||
512 | |||
513 | pub fn structural_search_replace( | ||
514 | &self, | ||
515 | query: &str, | ||
516 | parse_only: bool, | ||
517 | resolve_context: FilePosition, | ||
518 | selections: Vec<FileRange>, | ||
519 | ) -> Cancelable<Result<SourceChange, SsrError>> { | ||
520 | self.with_db(|db| { | ||
521 | let rule: ssr::SsrRule = query.parse()?; | ||
522 | let mut match_finder = ssr::MatchFinder::in_context(db, resolve_context, selections); | ||
523 | match_finder.add_rule(rule)?; | ||
524 | let edits = if parse_only { Vec::new() } else { match_finder.edits() }; | ||
525 | Ok(SourceChange::from(edits)) | ||
526 | }) | ||
527 | } | ||
528 | |||
529 | /// Performs an operation on that may be Canceled. | ||
530 | fn with_db<F, T>(&self, f: F) -> Cancelable<T> | ||
531 | where | ||
532 | F: FnOnce(&RootDatabase) -> T + std::panic::UnwindSafe, | ||
533 | { | ||
534 | self.db.catch_canceled(f) | ||
535 | } | ||
536 | } | ||
537 | |||
538 | #[test] | ||
539 | fn analysis_is_send() { | ||
540 | fn is_send<T: Send>() {} | ||
541 | is_send::<Analysis>(); | ||
542 | } | ||
diff --git a/crates/ide/src/markup.rs b/crates/ide/src/markup.rs new file mode 100644 index 000000000..60c193c40 --- /dev/null +++ b/crates/ide/src/markup.rs | |||
@@ -0,0 +1,38 @@ | |||
1 | //! Markdown formatting. | ||
2 | //! | ||
3 | //! Sometimes, we want to display a "rich text" in the UI. At the moment, we use | ||
4 | //! markdown for this purpose. It doesn't feel like a right option, but that's | ||
5 | //! what is used by LSP, so let's keep it simple. | ||
6 | use std::fmt; | ||
7 | |||
8 | #[derive(Default, Debug)] | ||
9 | pub struct Markup { | ||
10 | text: String, | ||
11 | } | ||
12 | |||
13 | impl From<Markup> for String { | ||
14 | fn from(markup: Markup) -> Self { | ||
15 | markup.text | ||
16 | } | ||
17 | } | ||
18 | |||
19 | impl From<String> for Markup { | ||
20 | fn from(text: String) -> Self { | ||
21 | Markup { text } | ||
22 | } | ||
23 | } | ||
24 | |||
25 | impl fmt::Display for Markup { | ||
26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
27 | fmt::Display::fmt(&self.text, f) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | impl Markup { | ||
32 | pub fn as_str(&self) -> &str { | ||
33 | self.text.as_str() | ||
34 | } | ||
35 | pub fn fenced_block(contents: &impl fmt::Display) -> Markup { | ||
36 | format!("```rust\n{}\n```", contents).into() | ||
37 | } | ||
38 | } | ||
diff --git a/crates/ide/src/matching_brace.rs b/crates/ide/src/matching_brace.rs new file mode 100644 index 000000000..cb6abb0db --- /dev/null +++ b/crates/ide/src/matching_brace.rs | |||
@@ -0,0 +1,73 @@ | |||
1 | use syntax::{ | ||
2 | ast::{self, AstNode}, | ||
3 | SourceFile, SyntaxKind, TextSize, T, | ||
4 | }; | ||
5 | use test_utils::mark; | ||
6 | |||
7 | // Feature: Matching Brace | ||
8 | // | ||
9 | // If the cursor is on any brace (`<>(){}[]||`) which is a part of a brace-pair, | ||
10 | // moves cursor to the matching brace. It uses the actual parser to determine | ||
11 | // braces, so it won't confuse generics with comparisons. | ||
12 | // | ||
13 | // |=== | ||
14 | // | Editor | Action Name | ||
15 | // | ||
16 | // | VS Code | **Rust Analyzer: Find matching brace** | ||
17 | // |=== | ||
18 | pub fn matching_brace(file: &SourceFile, offset: TextSize) -> Option<TextSize> { | ||
19 | const BRACES: &[SyntaxKind] = | ||
20 | &[T!['{'], T!['}'], T!['['], T![']'], T!['('], T![')'], T![<], T![>], T![|], T![|]]; | ||
21 | let (brace_token, brace_idx) = file | ||
22 | .syntax() | ||
23 | .token_at_offset(offset) | ||
24 | .filter_map(|node| { | ||
25 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; | ||
26 | Some((node, idx)) | ||
27 | }) | ||
28 | .next()?; | ||
29 | let parent = brace_token.parent(); | ||
30 | if brace_token.kind() == T![|] && !ast::ParamList::can_cast(parent.kind()) { | ||
31 | mark::hit!(pipes_not_braces); | ||
32 | return None; | ||
33 | } | ||
34 | let matching_kind = BRACES[brace_idx ^ 1]; | ||
35 | let matching_node = parent | ||
36 | .children_with_tokens() | ||
37 | .filter_map(|it| it.into_token()) | ||
38 | .find(|node| node.kind() == matching_kind && node != &brace_token)?; | ||
39 | Some(matching_node.text_range().start()) | ||
40 | } | ||
41 | |||
42 | #[cfg(test)] | ||
43 | mod tests { | ||
44 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | ||
45 | |||
46 | use super::*; | ||
47 | |||
48 | #[test] | ||
49 | fn test_matching_brace() { | ||
50 | fn do_check(before: &str, after: &str) { | ||
51 | let (pos, before) = extract_offset(before); | ||
52 | let parse = SourceFile::parse(&before); | ||
53 | let new_pos = match matching_brace(&parse.tree(), pos) { | ||
54 | None => pos, | ||
55 | Some(pos) => pos, | ||
56 | }; | ||
57 | let actual = add_cursor(&before, new_pos); | ||
58 | assert_eq_text!(after, &actual); | ||
59 | } | ||
60 | |||
61 | do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }"); | ||
62 | do_check("fn main() { |x: i32|<|> x * 2;}", "fn main() { <|>|x: i32| x * 2;}"); | ||
63 | do_check("fn main() { <|>|x: i32| x * 2;}", "fn main() { |x: i32<|>| x * 2;}"); | ||
64 | |||
65 | { | ||
66 | mark::check!(pipes_not_braces); | ||
67 | do_check( | ||
68 | "fn main() { match 92 { 1 | 2 |<|> 3 => 92 } }", | ||
69 | "fn main() { match 92 { 1 | 2 |<|> 3 => 92 } }", | ||
70 | ); | ||
71 | } | ||
72 | } | ||
73 | } | ||
diff --git a/crates/ide/src/mock_analysis.rs b/crates/ide/src/mock_analysis.rs new file mode 100644 index 000000000..363e6d27e --- /dev/null +++ b/crates/ide/src/mock_analysis.rs | |||
@@ -0,0 +1,176 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | use std::sync::Arc; | ||
3 | |||
4 | use base_db::{CrateName, FileSet, SourceRoot, VfsPath}; | ||
5 | use cfg::CfgOptions; | ||
6 | use test_utils::{ | ||
7 | extract_annotations, extract_range_or_offset, Fixture, RangeOrOffset, CURSOR_MARKER, | ||
8 | }; | ||
9 | |||
10 | use crate::{ | ||
11 | Analysis, AnalysisChange, AnalysisHost, CrateGraph, Edition, FileId, FilePosition, FileRange, | ||
12 | }; | ||
13 | |||
14 | /// Mock analysis is used in test to bootstrap an AnalysisHost/Analysis | ||
15 | /// from a set of in-memory files. | ||
16 | #[derive(Debug, Default)] | ||
17 | pub struct MockAnalysis { | ||
18 | files: Vec<Fixture>, | ||
19 | } | ||
20 | |||
21 | impl MockAnalysis { | ||
22 | /// Creates `MockAnalysis` using a fixture data in the following format: | ||
23 | /// | ||
24 | /// ```not_rust | ||
25 | /// //- /main.rs | ||
26 | /// mod foo; | ||
27 | /// fn main() {} | ||
28 | /// | ||
29 | /// //- /foo.rs | ||
30 | /// struct Baz; | ||
31 | /// ``` | ||
32 | pub fn with_files(ra_fixture: &str) -> MockAnalysis { | ||
33 | let (res, pos) = MockAnalysis::with_fixture(ra_fixture); | ||
34 | assert!(pos.is_none()); | ||
35 | res | ||
36 | } | ||
37 | |||
38 | /// Same as `with_files`, but requires that a single file contains a `<|>` marker, | ||
39 | /// whose position is also returned. | ||
40 | pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) { | ||
41 | let (res, position) = MockAnalysis::with_fixture(fixture); | ||
42 | let (file_id, range_or_offset) = position.expect("expected a marker (<|>)"); | ||
43 | let offset = match range_or_offset { | ||
44 | RangeOrOffset::Range(_) => panic!(), | ||
45 | RangeOrOffset::Offset(it) => it, | ||
46 | }; | ||
47 | (res, FilePosition { file_id, offset }) | ||
48 | } | ||
49 | |||
50 | fn with_fixture(fixture: &str) -> (MockAnalysis, Option<(FileId, RangeOrOffset)>) { | ||
51 | let mut position = None; | ||
52 | let mut res = MockAnalysis::default(); | ||
53 | for mut entry in Fixture::parse(fixture) { | ||
54 | if entry.text.contains(CURSOR_MARKER) { | ||
55 | assert!(position.is_none(), "only one marker (<|>) per fixture is allowed"); | ||
56 | let (range_or_offset, text) = extract_range_or_offset(&entry.text); | ||
57 | entry.text = text; | ||
58 | let file_id = res.add_file_fixture(entry); | ||
59 | position = Some((file_id, range_or_offset)); | ||
60 | } else { | ||
61 | res.add_file_fixture(entry); | ||
62 | } | ||
63 | } | ||
64 | (res, position) | ||
65 | } | ||
66 | |||
67 | fn add_file_fixture(&mut self, fixture: Fixture) -> FileId { | ||
68 | let file_id = FileId((self.files.len() + 1) as u32); | ||
69 | self.files.push(fixture); | ||
70 | file_id | ||
71 | } | ||
72 | |||
73 | pub fn id_of(&self, path: &str) -> FileId { | ||
74 | let (file_id, _) = | ||
75 | self.files().find(|(_, data)| path == data.path).expect("no file in this mock"); | ||
76 | file_id | ||
77 | } | ||
78 | pub fn annotations(&self) -> Vec<(FileRange, String)> { | ||
79 | self.files() | ||
80 | .flat_map(|(file_id, fixture)| { | ||
81 | let annotations = extract_annotations(&fixture.text); | ||
82 | annotations | ||
83 | .into_iter() | ||
84 | .map(move |(range, data)| (FileRange { file_id, range }, data)) | ||
85 | }) | ||
86 | .collect() | ||
87 | } | ||
88 | pub fn files(&self) -> impl Iterator<Item = (FileId, &Fixture)> + '_ { | ||
89 | self.files.iter().enumerate().map(|(idx, fixture)| (FileId(idx as u32 + 1), fixture)) | ||
90 | } | ||
91 | pub fn annotation(&self) -> (FileRange, String) { | ||
92 | let mut all = self.annotations(); | ||
93 | assert_eq!(all.len(), 1); | ||
94 | all.pop().unwrap() | ||
95 | } | ||
96 | pub fn analysis_host(self) -> AnalysisHost { | ||
97 | let mut host = AnalysisHost::default(); | ||
98 | let mut change = AnalysisChange::new(); | ||
99 | let mut file_set = FileSet::default(); | ||
100 | let mut crate_graph = CrateGraph::default(); | ||
101 | let mut root_crate = None; | ||
102 | for (i, data) in self.files.into_iter().enumerate() { | ||
103 | let path = data.path; | ||
104 | assert!(path.starts_with('/')); | ||
105 | |||
106 | let mut cfg = CfgOptions::default(); | ||
107 | data.cfg_atoms.iter().for_each(|it| cfg.insert_atom(it.into())); | ||
108 | data.cfg_key_values.iter().for_each(|(k, v)| cfg.insert_key_value(k.into(), v.into())); | ||
109 | let edition: Edition = | ||
110 | data.edition.and_then(|it| it.parse().ok()).unwrap_or(Edition::Edition2018); | ||
111 | |||
112 | let file_id = FileId(i as u32 + 1); | ||
113 | let env = data.env.into_iter().collect(); | ||
114 | if path == "/lib.rs" || path == "/main.rs" { | ||
115 | root_crate = Some(crate_graph.add_crate_root( | ||
116 | file_id, | ||
117 | edition, | ||
118 | None, | ||
119 | cfg, | ||
120 | env, | ||
121 | Default::default(), | ||
122 | )); | ||
123 | } else if path.ends_with("/lib.rs") { | ||
124 | let base = &path[..path.len() - "/lib.rs".len()]; | ||
125 | let crate_name = &base[base.rfind('/').unwrap() + '/'.len_utf8()..]; | ||
126 | let other_crate = crate_graph.add_crate_root( | ||
127 | file_id, | ||
128 | edition, | ||
129 | Some(crate_name.to_string()), | ||
130 | cfg, | ||
131 | env, | ||
132 | Default::default(), | ||
133 | ); | ||
134 | if let Some(root_crate) = root_crate { | ||
135 | crate_graph | ||
136 | .add_dep(root_crate, CrateName::new(crate_name).unwrap(), other_crate) | ||
137 | .unwrap(); | ||
138 | } | ||
139 | } | ||
140 | let path = VfsPath::new_virtual_path(path.to_string()); | ||
141 | file_set.insert(file_id, path); | ||
142 | change.change_file(file_id, Some(Arc::new(data.text).to_owned())); | ||
143 | } | ||
144 | change.set_crate_graph(crate_graph); | ||
145 | change.set_roots(vec![SourceRoot::new_local(file_set)]); | ||
146 | host.apply_change(change); | ||
147 | host | ||
148 | } | ||
149 | pub fn analysis(self) -> Analysis { | ||
150 | self.analysis_host().analysis() | ||
151 | } | ||
152 | } | ||
153 | |||
154 | /// Creates analysis from a multi-file fixture, returns positions marked with <|>. | ||
155 | pub fn analysis_and_position(ra_fixture: &str) -> (Analysis, FilePosition) { | ||
156 | let (mock, position) = MockAnalysis::with_files_and_position(ra_fixture); | ||
157 | (mock.analysis(), position) | ||
158 | } | ||
159 | |||
160 | /// Creates analysis for a single file. | ||
161 | pub fn single_file(ra_fixture: &str) -> (Analysis, FileId) { | ||
162 | let mock = MockAnalysis::with_files(ra_fixture); | ||
163 | let file_id = mock.id_of("/main.rs"); | ||
164 | (mock.analysis(), file_id) | ||
165 | } | ||
166 | |||
167 | /// Creates analysis for a single file, returns range marked with a pair of <|>. | ||
168 | pub fn analysis_and_range(ra_fixture: &str) -> (Analysis, FileRange) { | ||
169 | let (res, position) = MockAnalysis::with_fixture(ra_fixture); | ||
170 | let (file_id, range_or_offset) = position.expect("expected a marker (<|>)"); | ||
171 | let range = match range_or_offset { | ||
172 | RangeOrOffset::Range(it) => it, | ||
173 | RangeOrOffset::Offset(_) => panic!(), | ||
174 | }; | ||
175 | (res.analysis(), FileRange { file_id, range }) | ||
176 | } | ||
diff --git a/crates/ide/src/parent_module.rs b/crates/ide/src/parent_module.rs new file mode 100644 index 000000000..59ed2967c --- /dev/null +++ b/crates/ide/src/parent_module.rs | |||
@@ -0,0 +1,155 @@ | |||
1 | use base_db::{CrateId, FileId, FilePosition}; | ||
2 | use hir::Semantics; | ||
3 | use ide_db::RootDatabase; | ||
4 | use syntax::{ | ||
5 | algo::find_node_at_offset, | ||
6 | ast::{self, AstNode}, | ||
7 | }; | ||
8 | use test_utils::mark; | ||
9 | |||
10 | use crate::NavigationTarget; | ||
11 | |||
12 | // Feature: Parent Module | ||
13 | // | ||
14 | // Navigates to the parent module of the current module. | ||
15 | // | ||
16 | // |=== | ||
17 | // | Editor | Action Name | ||
18 | // | ||
19 | // | VS Code | **Rust Analyzer: Locate parent module** | ||
20 | // |=== | ||
21 | |||
22 | /// This returns `Vec` because a module may be included from several places. We | ||
23 | /// don't handle this case yet though, so the Vec has length at most one. | ||
24 | pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec<NavigationTarget> { | ||
25 | let sema = Semantics::new(db); | ||
26 | let source_file = sema.parse(position.file_id); | ||
27 | |||
28 | let mut module = find_node_at_offset::<ast::Module>(source_file.syntax(), position.offset); | ||
29 | |||
30 | // If cursor is literally on `mod foo`, go to the grandpa. | ||
31 | if let Some(m) = &module { | ||
32 | if !m | ||
33 | .item_list() | ||
34 | .map_or(false, |it| it.syntax().text_range().contains_inclusive(position.offset)) | ||
35 | { | ||
36 | mark::hit!(test_resolve_parent_module_on_module_decl); | ||
37 | module = m.syntax().ancestors().skip(1).find_map(ast::Module::cast); | ||
38 | } | ||
39 | } | ||
40 | |||
41 | let module = match module { | ||
42 | Some(module) => sema.to_def(&module), | ||
43 | None => sema.to_module_def(position.file_id), | ||
44 | }; | ||
45 | let module = match module { | ||
46 | None => return Vec::new(), | ||
47 | Some(it) => it, | ||
48 | }; | ||
49 | let nav = NavigationTarget::from_module_to_decl(db, module); | ||
50 | vec![nav] | ||
51 | } | ||
52 | |||
53 | /// Returns `Vec` for the same reason as `parent_module` | ||
54 | pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> { | ||
55 | let sema = Semantics::new(db); | ||
56 | let module = match sema.to_module_def(file_id) { | ||
57 | Some(it) => it, | ||
58 | None => return Vec::new(), | ||
59 | }; | ||
60 | let krate = module.krate(); | ||
61 | vec![krate.into()] | ||
62 | } | ||
63 | |||
64 | #[cfg(test)] | ||
65 | mod tests { | ||
66 | use base_db::Env; | ||
67 | use cfg::CfgOptions; | ||
68 | use test_utils::mark; | ||
69 | |||
70 | use crate::{ | ||
71 | mock_analysis::{analysis_and_position, MockAnalysis}, | ||
72 | AnalysisChange, CrateGraph, | ||
73 | Edition::Edition2018, | ||
74 | }; | ||
75 | |||
76 | #[test] | ||
77 | fn test_resolve_parent_module() { | ||
78 | let (analysis, pos) = analysis_and_position( | ||
79 | " | ||
80 | //- /lib.rs | ||
81 | mod foo; | ||
82 | //- /foo.rs | ||
83 | <|>// empty | ||
84 | ", | ||
85 | ); | ||
86 | let nav = analysis.parent_module(pos).unwrap().pop().unwrap(); | ||
87 | nav.assert_match("foo MODULE FileId(1) 0..8"); | ||
88 | } | ||
89 | |||
90 | #[test] | ||
91 | fn test_resolve_parent_module_on_module_decl() { | ||
92 | mark::check!(test_resolve_parent_module_on_module_decl); | ||
93 | let (analysis, pos) = analysis_and_position( | ||
94 | " | ||
95 | //- /lib.rs | ||
96 | mod foo; | ||
97 | |||
98 | //- /foo.rs | ||
99 | mod <|>bar; | ||
100 | |||
101 | //- /foo/bar.rs | ||
102 | // empty | ||
103 | ", | ||
104 | ); | ||
105 | let nav = analysis.parent_module(pos).unwrap().pop().unwrap(); | ||
106 | nav.assert_match("foo MODULE FileId(1) 0..8"); | ||
107 | } | ||
108 | |||
109 | #[test] | ||
110 | fn test_resolve_parent_module_for_inline() { | ||
111 | let (analysis, pos) = analysis_and_position( | ||
112 | " | ||
113 | //- /lib.rs | ||
114 | mod foo { | ||
115 | mod bar { | ||
116 | mod baz { <|> } | ||
117 | } | ||
118 | } | ||
119 | ", | ||
120 | ); | ||
121 | let nav = analysis.parent_module(pos).unwrap().pop().unwrap(); | ||
122 | nav.assert_match("baz MODULE FileId(1) 32..44"); | ||
123 | } | ||
124 | |||
125 | #[test] | ||
126 | fn test_resolve_crate_root() { | ||
127 | let mock = MockAnalysis::with_files( | ||
128 | r#" | ||
129 | //- /bar.rs | ||
130 | mod foo; | ||
131 | //- /foo.rs | ||
132 | // empty | ||
133 | "#, | ||
134 | ); | ||
135 | let root_file = mock.id_of("/bar.rs"); | ||
136 | let mod_file = mock.id_of("/foo.rs"); | ||
137 | let mut host = mock.analysis_host(); | ||
138 | assert!(host.analysis().crate_for(mod_file).unwrap().is_empty()); | ||
139 | |||
140 | let mut crate_graph = CrateGraph::default(); | ||
141 | let crate_id = crate_graph.add_crate_root( | ||
142 | root_file, | ||
143 | Edition2018, | ||
144 | None, | ||
145 | CfgOptions::default(), | ||
146 | Env::default(), | ||
147 | Default::default(), | ||
148 | ); | ||
149 | let mut change = AnalysisChange::new(); | ||
150 | change.set_crate_graph(crate_graph); | ||
151 | host.apply_change(change); | ||
152 | |||
153 | assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]); | ||
154 | } | ||
155 | } | ||
diff --git a/crates/ide/src/prime_caches.rs b/crates/ide/src/prime_caches.rs new file mode 100644 index 000000000..c5ab5a1d8 --- /dev/null +++ b/crates/ide/src/prime_caches.rs | |||
@@ -0,0 +1,12 @@ | |||
1 | //! rust-analyzer is lazy and doesn't not compute anything unless asked. This | ||
2 | //! sometimes is counter productive when, for example, the first goto definition | ||
3 | //! request takes longer to compute. This modules implemented prepopulating of | ||
4 | //! various caches, it's not really advanced at the moment. | ||
5 | |||
6 | use crate::{FileId, RootDatabase}; | ||
7 | |||
8 | pub(crate) fn prime_caches(db: &RootDatabase, files: Vec<FileId>) { | ||
9 | for file in files { | ||
10 | let _ = crate::syntax_highlighting::highlight(db, file, None, false); | ||
11 | } | ||
12 | } | ||
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs new file mode 100644 index 000000000..0a76ec6b4 --- /dev/null +++ b/crates/ide/src/references.rs | |||
@@ -0,0 +1,694 @@ | |||
1 | //! This module implements a reference search. | ||
2 | //! First, the element at the cursor position must be either an `ast::Name` | ||
3 | //! or `ast::NameRef`. If it's a `ast::NameRef`, at the classification step we | ||
4 | //! try to resolve the direct tree parent of this element, otherwise we | ||
5 | //! already have a definition and just need to get its HIR together with | ||
6 | //! some information that is needed for futher steps of searching. | ||
7 | //! After that, we collect files that might contain references and look | ||
8 | //! for text occurrences of the identifier. If there's an `ast::NameRef` | ||
9 | //! at the index that the match starts at and its tree parent is | ||
10 | //! resolved to the search element definition, we get a reference. | ||
11 | |||
12 | mod rename; | ||
13 | |||
14 | use hir::Semantics; | ||
15 | use ide_db::{ | ||
16 | defs::{classify_name, classify_name_ref, Definition}, | ||
17 | search::SearchScope, | ||
18 | RootDatabase, | ||
19 | }; | ||
20 | use syntax::{ | ||
21 | algo::find_node_at_offset, | ||
22 | ast::{self, NameOwner}, | ||
23 | AstNode, SyntaxKind, SyntaxNode, TextRange, TokenAtOffset, | ||
24 | }; | ||
25 | |||
26 | use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo}; | ||
27 | |||
28 | pub(crate) use self::rename::rename; | ||
29 | |||
30 | pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind}; | ||
31 | |||
32 | #[derive(Debug, Clone)] | ||
33 | pub struct ReferenceSearchResult { | ||
34 | declaration: Declaration, | ||
35 | references: Vec<Reference>, | ||
36 | } | ||
37 | |||
38 | #[derive(Debug, Clone)] | ||
39 | pub struct Declaration { | ||
40 | pub nav: NavigationTarget, | ||
41 | pub kind: ReferenceKind, | ||
42 | pub access: Option<ReferenceAccess>, | ||
43 | } | ||
44 | |||
45 | impl ReferenceSearchResult { | ||
46 | pub fn declaration(&self) -> &Declaration { | ||
47 | &self.declaration | ||
48 | } | ||
49 | |||
50 | pub fn decl_target(&self) -> &NavigationTarget { | ||
51 | &self.declaration.nav | ||
52 | } | ||
53 | |||
54 | pub fn references(&self) -> &[Reference] { | ||
55 | &self.references | ||
56 | } | ||
57 | |||
58 | /// Total number of references | ||
59 | /// At least 1 since all valid references should | ||
60 | /// Have a declaration | ||
61 | pub fn len(&self) -> usize { | ||
62 | self.references.len() + 1 | ||
63 | } | ||
64 | } | ||
65 | |||
66 | // allow turning ReferenceSearchResult into an iterator | ||
67 | // over References | ||
68 | impl IntoIterator for ReferenceSearchResult { | ||
69 | type Item = Reference; | ||
70 | type IntoIter = std::vec::IntoIter<Reference>; | ||
71 | |||
72 | fn into_iter(mut self) -> Self::IntoIter { | ||
73 | let mut v = Vec::with_capacity(self.len()); | ||
74 | v.push(Reference { | ||
75 | file_range: FileRange { | ||
76 | file_id: self.declaration.nav.file_id, | ||
77 | range: self.declaration.nav.focus_or_full_range(), | ||
78 | }, | ||
79 | kind: self.declaration.kind, | ||
80 | access: self.declaration.access, | ||
81 | }); | ||
82 | v.append(&mut self.references); | ||
83 | v.into_iter() | ||
84 | } | ||
85 | } | ||
86 | |||
87 | pub(crate) fn find_all_refs( | ||
88 | sema: &Semantics<RootDatabase>, | ||
89 | position: FilePosition, | ||
90 | search_scope: Option<SearchScope>, | ||
91 | ) -> Option<RangeInfo<ReferenceSearchResult>> { | ||
92 | let _p = profile::span("find_all_refs"); | ||
93 | let syntax = sema.parse(position.file_id).syntax().clone(); | ||
94 | |||
95 | let (opt_name, search_kind) = if let Some(name) = | ||
96 | get_struct_def_name_for_struct_literal_search(&sema, &syntax, position) | ||
97 | { | ||
98 | (Some(name), ReferenceKind::StructLiteral) | ||
99 | } else { | ||
100 | ( | ||
101 | sema.find_node_at_offset_with_descend::<ast::Name>(&syntax, position.offset), | ||
102 | ReferenceKind::Other, | ||
103 | ) | ||
104 | }; | ||
105 | |||
106 | let RangeInfo { range, info: def } = find_name(&sema, &syntax, position, opt_name)?; | ||
107 | |||
108 | let references = def | ||
109 | .find_usages(sema, search_scope) | ||
110 | .into_iter() | ||
111 | .filter(|r| search_kind == ReferenceKind::Other || search_kind == r.kind) | ||
112 | .collect(); | ||
113 | |||
114 | let decl_range = def.try_to_nav(sema.db)?.focus_or_full_range(); | ||
115 | |||
116 | let declaration = Declaration { | ||
117 | nav: def.try_to_nav(sema.db)?, | ||
118 | kind: ReferenceKind::Other, | ||
119 | access: decl_access(&def, &syntax, decl_range), | ||
120 | }; | ||
121 | |||
122 | Some(RangeInfo::new(range, ReferenceSearchResult { declaration, references })) | ||
123 | } | ||
124 | |||
125 | fn find_name( | ||
126 | sema: &Semantics<RootDatabase>, | ||
127 | syntax: &SyntaxNode, | ||
128 | position: FilePosition, | ||
129 | opt_name: Option<ast::Name>, | ||
130 | ) -> Option<RangeInfo<Definition>> { | ||
131 | if let Some(name) = opt_name { | ||
132 | let def = classify_name(sema, &name)?.definition(sema.db); | ||
133 | let range = name.syntax().text_range(); | ||
134 | return Some(RangeInfo::new(range, def)); | ||
135 | } | ||
136 | let name_ref = | ||
137 | sema.find_node_at_offset_with_descend::<ast::NameRef>(&syntax, position.offset)?; | ||
138 | let def = classify_name_ref(sema, &name_ref)?.definition(sema.db); | ||
139 | let range = name_ref.syntax().text_range(); | ||
140 | Some(RangeInfo::new(range, def)) | ||
141 | } | ||
142 | |||
143 | fn decl_access(def: &Definition, syntax: &SyntaxNode, range: TextRange) -> Option<ReferenceAccess> { | ||
144 | match def { | ||
145 | Definition::Local(_) | Definition::Field(_) => {} | ||
146 | _ => return None, | ||
147 | }; | ||
148 | |||
149 | let stmt = find_node_at_offset::<ast::LetStmt>(syntax, range.start())?; | ||
150 | if stmt.initializer().is_some() { | ||
151 | let pat = stmt.pat()?; | ||
152 | if let ast::Pat::IdentPat(it) = pat { | ||
153 | if it.mut_token().is_some() { | ||
154 | return Some(ReferenceAccess::Write); | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | |||
159 | None | ||
160 | } | ||
161 | |||
162 | fn get_struct_def_name_for_struct_literal_search( | ||
163 | sema: &Semantics<RootDatabase>, | ||
164 | syntax: &SyntaxNode, | ||
165 | position: FilePosition, | ||
166 | ) -> Option<ast::Name> { | ||
167 | if let TokenAtOffset::Between(ref left, ref right) = syntax.token_at_offset(position.offset) { | ||
168 | if right.kind() != SyntaxKind::L_CURLY && right.kind() != SyntaxKind::L_PAREN { | ||
169 | return None; | ||
170 | } | ||
171 | if let Some(name) = | ||
172 | sema.find_node_at_offset_with_descend::<ast::Name>(&syntax, left.text_range().start()) | ||
173 | { | ||
174 | return name.syntax().ancestors().find_map(ast::Struct::cast).and_then(|l| l.name()); | ||
175 | } | ||
176 | if sema | ||
177 | .find_node_at_offset_with_descend::<ast::GenericParamList>( | ||
178 | &syntax, | ||
179 | left.text_range().start(), | ||
180 | ) | ||
181 | .is_some() | ||
182 | { | ||
183 | return left.ancestors().find_map(ast::Struct::cast).and_then(|l| l.name()); | ||
184 | } | ||
185 | } | ||
186 | None | ||
187 | } | ||
188 | |||
189 | #[cfg(test)] | ||
190 | mod tests { | ||
191 | use crate::{ | ||
192 | mock_analysis::{analysis_and_position, MockAnalysis}, | ||
193 | Declaration, Reference, ReferenceSearchResult, SearchScope, | ||
194 | }; | ||
195 | |||
196 | #[test] | ||
197 | fn test_struct_literal_after_space() { | ||
198 | let refs = get_all_refs( | ||
199 | r#" | ||
200 | struct Foo <|>{ | ||
201 | a: i32, | ||
202 | } | ||
203 | impl Foo { | ||
204 | fn f() -> i32 { 42 } | ||
205 | } | ||
206 | fn main() { | ||
207 | let f: Foo; | ||
208 | f = Foo {a: Foo::f()}; | ||
209 | } | ||
210 | "#, | ||
211 | ); | ||
212 | check_result( | ||
213 | refs, | ||
214 | "Foo STRUCT FileId(1) 0..26 7..10 Other", | ||
215 | &["FileId(1) 101..104 StructLiteral"], | ||
216 | ); | ||
217 | } | ||
218 | |||
219 | #[test] | ||
220 | fn test_struct_literal_before_space() { | ||
221 | let refs = get_all_refs( | ||
222 | r#" | ||
223 | struct Foo<|> {} | ||
224 | fn main() { | ||
225 | let f: Foo; | ||
226 | f = Foo {}; | ||
227 | } | ||
228 | "#, | ||
229 | ); | ||
230 | check_result( | ||
231 | refs, | ||
232 | "Foo STRUCT FileId(1) 0..13 7..10 Other", | ||
233 | &["FileId(1) 41..44 Other", "FileId(1) 54..57 StructLiteral"], | ||
234 | ); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn test_struct_literal_with_generic_type() { | ||
239 | let refs = get_all_refs( | ||
240 | r#" | ||
241 | struct Foo<T> <|>{} | ||
242 | fn main() { | ||
243 | let f: Foo::<i32>; | ||
244 | f = Foo {}; | ||
245 | } | ||
246 | "#, | ||
247 | ); | ||
248 | check_result( | ||
249 | refs, | ||
250 | "Foo STRUCT FileId(1) 0..16 7..10 Other", | ||
251 | &["FileId(1) 64..67 StructLiteral"], | ||
252 | ); | ||
253 | } | ||
254 | |||
255 | #[test] | ||
256 | fn test_struct_literal_for_tuple() { | ||
257 | let refs = get_all_refs( | ||
258 | r#" | ||
259 | struct Foo<|>(i32); | ||
260 | |||
261 | fn main() { | ||
262 | let f: Foo; | ||
263 | f = Foo(1); | ||
264 | } | ||
265 | "#, | ||
266 | ); | ||
267 | check_result( | ||
268 | refs, | ||
269 | "Foo STRUCT FileId(1) 0..16 7..10 Other", | ||
270 | &["FileId(1) 54..57 StructLiteral"], | ||
271 | ); | ||
272 | } | ||
273 | |||
274 | #[test] | ||
275 | fn test_find_all_refs_for_local() { | ||
276 | let refs = get_all_refs( | ||
277 | r#" | ||
278 | fn main() { | ||
279 | let mut i = 1; | ||
280 | let j = 1; | ||
281 | i = i<|> + j; | ||
282 | |||
283 | { | ||
284 | i = 0; | ||
285 | } | ||
286 | |||
287 | i = 5; | ||
288 | }"#, | ||
289 | ); | ||
290 | check_result( | ||
291 | refs, | ||
292 | "i IDENT_PAT FileId(1) 24..25 Other Write", | ||
293 | &[ | ||
294 | "FileId(1) 50..51 Other Write", | ||
295 | "FileId(1) 54..55 Other Read", | ||
296 | "FileId(1) 76..77 Other Write", | ||
297 | "FileId(1) 94..95 Other Write", | ||
298 | ], | ||
299 | ); | ||
300 | } | ||
301 | |||
302 | #[test] | ||
303 | fn search_filters_by_range() { | ||
304 | let refs = get_all_refs( | ||
305 | r#" | ||
306 | fn foo() { | ||
307 | let spam<|> = 92; | ||
308 | spam + spam | ||
309 | } | ||
310 | fn bar() { | ||
311 | let spam = 92; | ||
312 | spam + spam | ||
313 | } | ||
314 | "#, | ||
315 | ); | ||
316 | check_result( | ||
317 | refs, | ||
318 | "spam IDENT_PAT FileId(1) 19..23 Other", | ||
319 | &["FileId(1) 34..38 Other Read", "FileId(1) 41..45 Other Read"], | ||
320 | ); | ||
321 | } | ||
322 | |||
323 | #[test] | ||
324 | fn test_find_all_refs_for_param_inside() { | ||
325 | let refs = get_all_refs( | ||
326 | r#" | ||
327 | fn foo(i : u32) -> u32 { | ||
328 | i<|> | ||
329 | } | ||
330 | "#, | ||
331 | ); | ||
332 | check_result(refs, "i IDENT_PAT FileId(1) 7..8 Other", &["FileId(1) 29..30 Other Read"]); | ||
333 | } | ||
334 | |||
335 | #[test] | ||
336 | fn test_find_all_refs_for_fn_param() { | ||
337 | let refs = get_all_refs( | ||
338 | r#" | ||
339 | fn foo(i<|> : u32) -> u32 { | ||
340 | i | ||
341 | } | ||
342 | "#, | ||
343 | ); | ||
344 | check_result(refs, "i IDENT_PAT FileId(1) 7..8 Other", &["FileId(1) 29..30 Other Read"]); | ||
345 | } | ||
346 | |||
347 | #[test] | ||
348 | fn test_find_all_refs_field_name() { | ||
349 | let refs = get_all_refs( | ||
350 | r#" | ||
351 | //- /lib.rs | ||
352 | struct Foo { | ||
353 | pub spam<|>: u32, | ||
354 | } | ||
355 | |||
356 | fn main(s: Foo) { | ||
357 | let f = s.spam; | ||
358 | } | ||
359 | "#, | ||
360 | ); | ||
361 | check_result( | ||
362 | refs, | ||
363 | "spam RECORD_FIELD FileId(1) 17..30 21..25 Other", | ||
364 | &["FileId(1) 67..71 Other Read"], | ||
365 | ); | ||
366 | } | ||
367 | |||
368 | #[test] | ||
369 | fn test_find_all_refs_impl_item_name() { | ||
370 | let refs = get_all_refs( | ||
371 | r#" | ||
372 | struct Foo; | ||
373 | impl Foo { | ||
374 | fn f<|>(&self) { } | ||
375 | } | ||
376 | "#, | ||
377 | ); | ||
378 | check_result(refs, "f FN FileId(1) 27..43 30..31 Other", &[]); | ||
379 | } | ||
380 | |||
381 | #[test] | ||
382 | fn test_find_all_refs_enum_var_name() { | ||
383 | let refs = get_all_refs( | ||
384 | r#" | ||
385 | enum Foo { | ||
386 | A, | ||
387 | B<|>, | ||
388 | C, | ||
389 | } | ||
390 | "#, | ||
391 | ); | ||
392 | check_result(refs, "B VARIANT FileId(1) 22..23 22..23 Other", &[]); | ||
393 | } | ||
394 | |||
395 | #[test] | ||
396 | fn test_find_all_refs_two_modules() { | ||
397 | let (analysis, pos) = analysis_and_position( | ||
398 | r#" | ||
399 | //- /lib.rs | ||
400 | pub mod foo; | ||
401 | pub mod bar; | ||
402 | |||
403 | fn f() { | ||
404 | let i = foo::Foo { n: 5 }; | ||
405 | } | ||
406 | |||
407 | //- /foo.rs | ||
408 | use crate::bar; | ||
409 | |||
410 | pub struct Foo { | ||
411 | pub n: u32, | ||
412 | } | ||
413 | |||
414 | fn f() { | ||
415 | let i = bar::Bar { n: 5 }; | ||
416 | } | ||
417 | |||
418 | //- /bar.rs | ||
419 | use crate::foo; | ||
420 | |||
421 | pub struct Bar { | ||
422 | pub n: u32, | ||
423 | } | ||
424 | |||
425 | fn f() { | ||
426 | let i = foo::Foo<|> { n: 5 }; | ||
427 | } | ||
428 | "#, | ||
429 | ); | ||
430 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
431 | check_result( | ||
432 | refs, | ||
433 | "Foo STRUCT FileId(2) 17..51 28..31 Other", | ||
434 | &["FileId(1) 53..56 StructLiteral", "FileId(3) 79..82 StructLiteral"], | ||
435 | ); | ||
436 | } | ||
437 | |||
438 | // `mod foo;` is not in the results because `foo` is an `ast::Name`. | ||
439 | // So, there are two references: the first one is a definition of the `foo` module, | ||
440 | // which is the whole `foo.rs`, and the second one is in `use foo::Foo`. | ||
441 | #[test] | ||
442 | fn test_find_all_refs_decl_module() { | ||
443 | let (analysis, pos) = analysis_and_position( | ||
444 | r#" | ||
445 | //- /lib.rs | ||
446 | mod foo<|>; | ||
447 | |||
448 | use foo::Foo; | ||
449 | |||
450 | fn f() { | ||
451 | let i = Foo { n: 5 }; | ||
452 | } | ||
453 | |||
454 | //- /foo.rs | ||
455 | pub struct Foo { | ||
456 | pub n: u32, | ||
457 | } | ||
458 | "#, | ||
459 | ); | ||
460 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
461 | check_result(refs, "foo SOURCE_FILE FileId(2) 0..35 Other", &["FileId(1) 14..17 Other"]); | ||
462 | } | ||
463 | |||
464 | #[test] | ||
465 | fn test_find_all_refs_super_mod_vis() { | ||
466 | let (analysis, pos) = analysis_and_position( | ||
467 | r#" | ||
468 | //- /lib.rs | ||
469 | mod foo; | ||
470 | |||
471 | //- /foo.rs | ||
472 | mod some; | ||
473 | use some::Foo; | ||
474 | |||
475 | fn f() { | ||
476 | let i = Foo { n: 5 }; | ||
477 | } | ||
478 | |||
479 | //- /foo/some.rs | ||
480 | pub(super) struct Foo<|> { | ||
481 | pub n: u32, | ||
482 | } | ||
483 | "#, | ||
484 | ); | ||
485 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
486 | check_result( | ||
487 | refs, | ||
488 | "Foo STRUCT FileId(3) 0..41 18..21 Other", | ||
489 | &["FileId(2) 20..23 Other", "FileId(2) 47..50 StructLiteral"], | ||
490 | ); | ||
491 | } | ||
492 | |||
493 | #[test] | ||
494 | fn test_find_all_refs_with_scope() { | ||
495 | let code = r#" | ||
496 | //- /lib.rs | ||
497 | mod foo; | ||
498 | mod bar; | ||
499 | |||
500 | pub fn quux<|>() {} | ||
501 | |||
502 | //- /foo.rs | ||
503 | fn f() { super::quux(); } | ||
504 | |||
505 | //- /bar.rs | ||
506 | fn f() { super::quux(); } | ||
507 | "#; | ||
508 | |||
509 | let (mock, pos) = MockAnalysis::with_files_and_position(code); | ||
510 | let bar = mock.id_of("/bar.rs"); | ||
511 | let analysis = mock.analysis(); | ||
512 | |||
513 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
514 | check_result( | ||
515 | refs, | ||
516 | "quux FN FileId(1) 19..35 26..30 Other", | ||
517 | &["FileId(2) 16..20 StructLiteral", "FileId(3) 16..20 StructLiteral"], | ||
518 | ); | ||
519 | |||
520 | let refs = | ||
521 | analysis.find_all_refs(pos, Some(SearchScope::single_file(bar))).unwrap().unwrap(); | ||
522 | check_result( | ||
523 | refs, | ||
524 | "quux FN FileId(1) 19..35 26..30 Other", | ||
525 | &["FileId(3) 16..20 StructLiteral"], | ||
526 | ); | ||
527 | } | ||
528 | |||
529 | #[test] | ||
530 | fn test_find_all_refs_macro_def() { | ||
531 | let refs = get_all_refs( | ||
532 | r#" | ||
533 | #[macro_export] | ||
534 | macro_rules! m1<|> { () => (()) } | ||
535 | |||
536 | fn foo() { | ||
537 | m1(); | ||
538 | m1(); | ||
539 | } | ||
540 | "#, | ||
541 | ); | ||
542 | check_result( | ||
543 | refs, | ||
544 | "m1 MACRO_CALL FileId(1) 0..46 29..31 Other", | ||
545 | &["FileId(1) 63..65 StructLiteral", "FileId(1) 73..75 StructLiteral"], | ||
546 | ); | ||
547 | } | ||
548 | |||
549 | #[test] | ||
550 | fn test_basic_highlight_read_write() { | ||
551 | let refs = get_all_refs( | ||
552 | r#" | ||
553 | fn foo() { | ||
554 | let mut i<|> = 0; | ||
555 | i = i + 1; | ||
556 | } | ||
557 | "#, | ||
558 | ); | ||
559 | check_result( | ||
560 | refs, | ||
561 | "i IDENT_PAT FileId(1) 23..24 Other Write", | ||
562 | &["FileId(1) 34..35 Other Write", "FileId(1) 38..39 Other Read"], | ||
563 | ); | ||
564 | } | ||
565 | |||
566 | #[test] | ||
567 | fn test_basic_highlight_field_read_write() { | ||
568 | let refs = get_all_refs( | ||
569 | r#" | ||
570 | struct S { | ||
571 | f: u32, | ||
572 | } | ||
573 | |||
574 | fn foo() { | ||
575 | let mut s = S{f: 0}; | ||
576 | s.f<|> = 0; | ||
577 | } | ||
578 | "#, | ||
579 | ); | ||
580 | check_result( | ||
581 | refs, | ||
582 | "f RECORD_FIELD FileId(1) 15..21 15..16 Other", | ||
583 | &["FileId(1) 55..56 Other Read", "FileId(1) 68..69 Other Write"], | ||
584 | ); | ||
585 | } | ||
586 | |||
587 | #[test] | ||
588 | fn test_basic_highlight_decl_no_write() { | ||
589 | let refs = get_all_refs( | ||
590 | r#" | ||
591 | fn foo() { | ||
592 | let i<|>; | ||
593 | i = 1; | ||
594 | } | ||
595 | "#, | ||
596 | ); | ||
597 | check_result(refs, "i IDENT_PAT FileId(1) 19..20 Other", &["FileId(1) 26..27 Other Write"]); | ||
598 | } | ||
599 | |||
600 | #[test] | ||
601 | fn test_find_struct_function_refs_outside_module() { | ||
602 | let refs = get_all_refs( | ||
603 | r#" | ||
604 | mod foo { | ||
605 | pub struct Foo; | ||
606 | |||
607 | impl Foo { | ||
608 | pub fn new<|>() -> Foo { | ||
609 | Foo | ||
610 | } | ||
611 | } | ||
612 | } | ||
613 | |||
614 | fn main() { | ||
615 | let _f = foo::Foo::new(); | ||
616 | } | ||
617 | "#, | ||
618 | ); | ||
619 | check_result( | ||
620 | refs, | ||
621 | "new FN FileId(1) 54..101 61..64 Other", | ||
622 | &["FileId(1) 146..149 StructLiteral"], | ||
623 | ); | ||
624 | } | ||
625 | |||
626 | #[test] | ||
627 | fn test_find_all_refs_nested_module() { | ||
628 | let code = r#" | ||
629 | //- /lib.rs | ||
630 | mod foo { | ||
631 | mod bar; | ||
632 | } | ||
633 | |||
634 | fn f<|>() {} | ||
635 | |||
636 | //- /foo/bar.rs | ||
637 | use crate::f; | ||
638 | |||
639 | fn g() { | ||
640 | f(); | ||
641 | } | ||
642 | "#; | ||
643 | |||
644 | let (analysis, pos) = analysis_and_position(code); | ||
645 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
646 | check_result( | ||
647 | refs, | ||
648 | "f FN FileId(1) 26..35 29..30 Other", | ||
649 | &["FileId(2) 11..12 Other", "FileId(2) 28..29 StructLiteral"], | ||
650 | ); | ||
651 | } | ||
652 | |||
653 | fn get_all_refs(ra_fixture: &str) -> ReferenceSearchResult { | ||
654 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
655 | analysis.find_all_refs(position, None).unwrap().unwrap() | ||
656 | } | ||
657 | |||
658 | fn check_result(res: ReferenceSearchResult, expected_decl: &str, expected_refs: &[&str]) { | ||
659 | res.declaration().assert_match(expected_decl); | ||
660 | assert_eq!(res.references.len(), expected_refs.len()); | ||
661 | res.references() | ||
662 | .iter() | ||
663 | .enumerate() | ||
664 | .for_each(|(i, r)| ref_assert_match(r, expected_refs[i])); | ||
665 | } | ||
666 | |||
667 | impl Declaration { | ||
668 | fn debug_render(&self) -> String { | ||
669 | let mut s = format!("{} {:?}", self.nav.debug_render(), self.kind); | ||
670 | if let Some(access) = self.access { | ||
671 | s.push_str(&format!(" {:?}", access)); | ||
672 | } | ||
673 | s | ||
674 | } | ||
675 | |||
676 | fn assert_match(&self, expected: &str) { | ||
677 | let actual = self.debug_render(); | ||
678 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
679 | } | ||
680 | } | ||
681 | |||
682 | fn ref_debug_render(r: &Reference) -> String { | ||
683 | let mut s = format!("{:?} {:?} {:?}", r.file_range.file_id, r.file_range.range, r.kind); | ||
684 | if let Some(access) = r.access { | ||
685 | s.push_str(&format!(" {:?}", access)); | ||
686 | } | ||
687 | s | ||
688 | } | ||
689 | |||
690 | fn ref_assert_match(r: &Reference, expected: &str) { | ||
691 | let actual = ref_debug_render(r); | ||
692 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
693 | } | ||
694 | } | ||
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs new file mode 100644 index 000000000..d73dc9cd0 --- /dev/null +++ b/crates/ide/src/references/rename.rs | |||
@@ -0,0 +1,1010 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use base_db::SourceDatabaseExt; | ||
4 | use hir::{Module, ModuleDef, ModuleSource, Semantics}; | ||
5 | use ide_db::{ | ||
6 | defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, | ||
7 | RootDatabase, | ||
8 | }; | ||
9 | use std::convert::TryInto; | ||
10 | use syntax::{ | ||
11 | algo::find_node_at_offset, | ||
12 | ast::{self, NameOwner}, | ||
13 | lex_single_valid_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken, | ||
14 | }; | ||
15 | use test_utils::mark; | ||
16 | use text_edit::TextEdit; | ||
17 | |||
18 | use crate::{ | ||
19 | references::find_all_refs, FilePosition, FileSystemEdit, RangeInfo, Reference, ReferenceKind, | ||
20 | SourceChange, SourceFileEdit, TextRange, TextSize, | ||
21 | }; | ||
22 | |||
23 | pub(crate) fn rename( | ||
24 | db: &RootDatabase, | ||
25 | position: FilePosition, | ||
26 | new_name: &str, | ||
27 | ) -> Option<RangeInfo<SourceChange>> { | ||
28 | let sema = Semantics::new(db); | ||
29 | |||
30 | match lex_single_valid_syntax_kind(new_name)? { | ||
31 | SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), | ||
32 | SyntaxKind::SELF_KW => return rename_to_self(&sema, position), | ||
33 | _ => return None, | ||
34 | } | ||
35 | |||
36 | let source_file = sema.parse(position.file_id); | ||
37 | let syntax = source_file.syntax(); | ||
38 | if let Some(module) = find_module_at_offset(&sema, position, syntax) { | ||
39 | rename_mod(&sema, position, module, new_name) | ||
40 | } else if let Some(self_token) = | ||
41 | syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::SELF_KW) | ||
42 | { | ||
43 | rename_self_to_param(&sema, position, self_token, new_name) | ||
44 | } else { | ||
45 | rename_reference(&sema, position, new_name) | ||
46 | } | ||
47 | } | ||
48 | |||
49 | fn find_module_at_offset( | ||
50 | sema: &Semantics<RootDatabase>, | ||
51 | position: FilePosition, | ||
52 | syntax: &SyntaxNode, | ||
53 | ) -> Option<Module> { | ||
54 | let ident = syntax.token_at_offset(position.offset).find(|t| t.kind() == SyntaxKind::IDENT)?; | ||
55 | |||
56 | let module = match_ast! { | ||
57 | match (ident.parent()) { | ||
58 | ast::NameRef(name_ref) => { | ||
59 | match classify_name_ref(sema, &name_ref)? { | ||
60 | NameRefClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module, | ||
61 | _ => return None, | ||
62 | } | ||
63 | }, | ||
64 | ast::Name(name) => { | ||
65 | match classify_name(&sema, &name)? { | ||
66 | NameClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module, | ||
67 | _ => return None, | ||
68 | } | ||
69 | }, | ||
70 | _ => return None, | ||
71 | } | ||
72 | }; | ||
73 | |||
74 | Some(module) | ||
75 | } | ||
76 | |||
77 | fn source_edit_from_reference(reference: Reference, new_name: &str) -> SourceFileEdit { | ||
78 | let mut replacement_text = String::new(); | ||
79 | let file_id = reference.file_range.file_id; | ||
80 | let range = match reference.kind { | ||
81 | ReferenceKind::FieldShorthandForField => { | ||
82 | mark::hit!(test_rename_struct_field_for_shorthand); | ||
83 | replacement_text.push_str(new_name); | ||
84 | replacement_text.push_str(": "); | ||
85 | TextRange::new(reference.file_range.range.start(), reference.file_range.range.start()) | ||
86 | } | ||
87 | ReferenceKind::FieldShorthandForLocal => { | ||
88 | mark::hit!(test_rename_local_for_field_shorthand); | ||
89 | replacement_text.push_str(": "); | ||
90 | replacement_text.push_str(new_name); | ||
91 | TextRange::new(reference.file_range.range.end(), reference.file_range.range.end()) | ||
92 | } | ||
93 | _ => { | ||
94 | replacement_text.push_str(new_name); | ||
95 | reference.file_range.range | ||
96 | } | ||
97 | }; | ||
98 | SourceFileEdit { file_id, edit: TextEdit::replace(range, replacement_text) } | ||
99 | } | ||
100 | |||
101 | fn rename_mod( | ||
102 | sema: &Semantics<RootDatabase>, | ||
103 | position: FilePosition, | ||
104 | module: Module, | ||
105 | new_name: &str, | ||
106 | ) -> Option<RangeInfo<SourceChange>> { | ||
107 | let mut source_file_edits = Vec::new(); | ||
108 | let mut file_system_edits = Vec::new(); | ||
109 | |||
110 | let src = module.definition_source(sema.db); | ||
111 | let file_id = src.file_id.original_file(sema.db); | ||
112 | match src.value { | ||
113 | ModuleSource::SourceFile(..) => { | ||
114 | // mod is defined in path/to/dir/mod.rs | ||
115 | let dst = if module.is_mod_rs(sema.db) { | ||
116 | format!("../{}/mod.rs", new_name) | ||
117 | } else { | ||
118 | format!("{}.rs", new_name) | ||
119 | }; | ||
120 | let move_file = FileSystemEdit::MoveFile { src: file_id, anchor: file_id, dst }; | ||
121 | file_system_edits.push(move_file); | ||
122 | } | ||
123 | ModuleSource::Module(..) => {} | ||
124 | } | ||
125 | |||
126 | if let Some(src) = module.declaration_source(sema.db) { | ||
127 | let file_id = src.file_id.original_file(sema.db); | ||
128 | let name = src.value.name()?; | ||
129 | let edit = SourceFileEdit { | ||
130 | file_id, | ||
131 | edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), | ||
132 | }; | ||
133 | source_file_edits.push(edit); | ||
134 | } | ||
135 | |||
136 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | ||
137 | let ref_edits = refs | ||
138 | .references | ||
139 | .into_iter() | ||
140 | .map(|reference| source_edit_from_reference(reference, new_name)); | ||
141 | source_file_edits.extend(ref_edits); | ||
142 | |||
143 | Some(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) | ||
144 | } | ||
145 | |||
146 | fn rename_to_self( | ||
147 | sema: &Semantics<RootDatabase>, | ||
148 | position: FilePosition, | ||
149 | ) -> Option<RangeInfo<SourceChange>> { | ||
150 | let source_file = sema.parse(position.file_id); | ||
151 | let syn = source_file.syntax(); | ||
152 | |||
153 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; | ||
154 | let params = fn_def.param_list()?; | ||
155 | if params.self_param().is_some() { | ||
156 | return None; // method already has self param | ||
157 | } | ||
158 | let first_param = params.params().next()?; | ||
159 | let mutable = match first_param.ty() { | ||
160 | Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(), | ||
161 | _ => return None, // not renaming other types | ||
162 | }; | ||
163 | |||
164 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | ||
165 | |||
166 | let param_range = first_param.syntax().text_range(); | ||
167 | let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs | ||
168 | .into_iter() | ||
169 | .partition(|reference| param_range.intersect(reference.file_range.range).is_some()); | ||
170 | |||
171 | if param_ref.is_empty() { | ||
172 | return None; | ||
173 | } | ||
174 | |||
175 | let mut edits = usages | ||
176 | .into_iter() | ||
177 | .map(|reference| source_edit_from_reference(reference, "self")) | ||
178 | .collect::<Vec<_>>(); | ||
179 | |||
180 | edits.push(SourceFileEdit { | ||
181 | file_id: position.file_id, | ||
182 | edit: TextEdit::replace( | ||
183 | param_range, | ||
184 | String::from(if mutable { "&mut self" } else { "&self" }), | ||
185 | ), | ||
186 | }); | ||
187 | |||
188 | Some(RangeInfo::new(range, SourceChange::from(edits))) | ||
189 | } | ||
190 | |||
191 | fn text_edit_from_self_param( | ||
192 | syn: &SyntaxNode, | ||
193 | self_param: &ast::SelfParam, | ||
194 | new_name: &str, | ||
195 | ) -> Option<TextEdit> { | ||
196 | fn target_type_name(impl_def: &ast::Impl) -> Option<String> { | ||
197 | if let Some(ast::Type::PathType(p)) = impl_def.self_ty() { | ||
198 | return Some(p.path()?.segment()?.name_ref()?.text().to_string()); | ||
199 | } | ||
200 | None | ||
201 | } | ||
202 | |||
203 | let impl_def = find_node_at_offset::<ast::Impl>(syn, self_param.syntax().text_range().start())?; | ||
204 | let type_name = target_type_name(&impl_def)?; | ||
205 | |||
206 | let mut replacement_text = String::from(new_name); | ||
207 | replacement_text.push_str(": "); | ||
208 | replacement_text.push_str(self_param.mut_token().map_or("&", |_| "&mut ")); | ||
209 | replacement_text.push_str(type_name.as_str()); | ||
210 | |||
211 | Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text)) | ||
212 | } | ||
213 | |||
214 | fn rename_self_to_param( | ||
215 | sema: &Semantics<RootDatabase>, | ||
216 | position: FilePosition, | ||
217 | self_token: SyntaxToken, | ||
218 | new_name: &str, | ||
219 | ) -> Option<RangeInfo<SourceChange>> { | ||
220 | let source_file = sema.parse(position.file_id); | ||
221 | let syn = source_file.syntax(); | ||
222 | |||
223 | let text = sema.db.file_text(position.file_id); | ||
224 | let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; | ||
225 | let search_range = fn_def.syntax().text_range(); | ||
226 | |||
227 | let mut edits: Vec<SourceFileEdit> = vec![]; | ||
228 | |||
229 | for (idx, _) in text.match_indices("self") { | ||
230 | let offset: TextSize = idx.try_into().unwrap(); | ||
231 | if !search_range.contains_inclusive(offset) { | ||
232 | continue; | ||
233 | } | ||
234 | if let Some(ref usage) = | ||
235 | syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) | ||
236 | { | ||
237 | let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { | ||
238 | text_edit_from_self_param(syn, self_param, new_name)? | ||
239 | } else { | ||
240 | TextEdit::replace(usage.text_range(), String::from(new_name)) | ||
241 | }; | ||
242 | edits.push(SourceFileEdit { file_id: position.file_id, edit }); | ||
243 | } | ||
244 | } | ||
245 | |||
246 | let range = ast::SelfParam::cast(self_token.parent()) | ||
247 | .map_or(self_token.text_range(), |p| p.syntax().text_range()); | ||
248 | |||
249 | Some(RangeInfo::new(range, SourceChange::from(edits))) | ||
250 | } | ||
251 | |||
252 | fn rename_reference( | ||
253 | sema: &Semantics<RootDatabase>, | ||
254 | position: FilePosition, | ||
255 | new_name: &str, | ||
256 | ) -> Option<RangeInfo<SourceChange>> { | ||
257 | let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; | ||
258 | |||
259 | let edit = refs | ||
260 | .into_iter() | ||
261 | .map(|reference| source_edit_from_reference(reference, new_name)) | ||
262 | .collect::<Vec<_>>(); | ||
263 | |||
264 | if edit.is_empty() { | ||
265 | return None; | ||
266 | } | ||
267 | |||
268 | Some(RangeInfo::new(range, SourceChange::from(edit))) | ||
269 | } | ||
270 | |||
271 | #[cfg(test)] | ||
272 | mod tests { | ||
273 | use expect::{expect, Expect}; | ||
274 | use stdx::trim_indent; | ||
275 | use test_utils::{assert_eq_text, mark}; | ||
276 | use text_edit::TextEdit; | ||
277 | |||
278 | use crate::{mock_analysis::analysis_and_position, FileId}; | ||
279 | |||
280 | fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
281 | let ra_fixture_after = &trim_indent(ra_fixture_after); | ||
282 | let (analysis, position) = analysis_and_position(ra_fixture_before); | ||
283 | let source_change = analysis.rename(position, new_name).unwrap(); | ||
284 | let mut text_edit_builder = TextEdit::builder(); | ||
285 | let mut file_id: Option<FileId> = None; | ||
286 | if let Some(change) = source_change { | ||
287 | for edit in change.info.source_file_edits { | ||
288 | file_id = Some(edit.file_id); | ||
289 | for indel in edit.edit.into_iter() { | ||
290 | text_edit_builder.replace(indel.delete, indel.insert); | ||
291 | } | ||
292 | } | ||
293 | } | ||
294 | let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); | ||
295 | text_edit_builder.finish().apply(&mut result); | ||
296 | assert_eq_text!(ra_fixture_after, &*result); | ||
297 | } | ||
298 | |||
299 | fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { | ||
300 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
301 | let source_change = analysis.rename(position, new_name).unwrap().unwrap(); | ||
302 | expect.assert_debug_eq(&source_change) | ||
303 | } | ||
304 | |||
305 | #[test] | ||
306 | fn test_rename_to_underscore() { | ||
307 | check("_", r#"fn main() { let i<|> = 1; }"#, r#"fn main() { let _ = 1; }"#); | ||
308 | } | ||
309 | |||
310 | #[test] | ||
311 | fn test_rename_to_raw_identifier() { | ||
312 | check("r#fn", r#"fn main() { let i<|> = 1; }"#, r#"fn main() { let r#fn = 1; }"#); | ||
313 | } | ||
314 | |||
315 | #[test] | ||
316 | fn test_rename_to_invalid_identifier() { | ||
317 | let (analysis, position) = analysis_and_position(r#"fn main() { let i<|> = 1; }"#); | ||
318 | let new_name = "invalid!"; | ||
319 | let source_change = analysis.rename(position, new_name).unwrap(); | ||
320 | assert!(source_change.is_none()); | ||
321 | } | ||
322 | |||
323 | #[test] | ||
324 | fn test_rename_for_local() { | ||
325 | check( | ||
326 | "k", | ||
327 | r#" | ||
328 | fn main() { | ||
329 | let mut i = 1; | ||
330 | let j = 1; | ||
331 | i = i<|> + j; | ||
332 | |||
333 | { i = 0; } | ||
334 | |||
335 | i = 5; | ||
336 | } | ||
337 | "#, | ||
338 | r#" | ||
339 | fn main() { | ||
340 | let mut k = 1; | ||
341 | let j = 1; | ||
342 | k = k + j; | ||
343 | |||
344 | { k = 0; } | ||
345 | |||
346 | k = 5; | ||
347 | } | ||
348 | "#, | ||
349 | ); | ||
350 | } | ||
351 | |||
352 | #[test] | ||
353 | fn test_rename_for_macro_args() { | ||
354 | check( | ||
355 | "b", | ||
356 | r#" | ||
357 | macro_rules! foo {($i:ident) => {$i} } | ||
358 | fn main() { | ||
359 | let a<|> = "test"; | ||
360 | foo!(a); | ||
361 | } | ||
362 | "#, | ||
363 | r#" | ||
364 | macro_rules! foo {($i:ident) => {$i} } | ||
365 | fn main() { | ||
366 | let b = "test"; | ||
367 | foo!(b); | ||
368 | } | ||
369 | "#, | ||
370 | ); | ||
371 | } | ||
372 | |||
373 | #[test] | ||
374 | fn test_rename_for_macro_args_rev() { | ||
375 | check( | ||
376 | "b", | ||
377 | r#" | ||
378 | macro_rules! foo {($i:ident) => {$i} } | ||
379 | fn main() { | ||
380 | let a = "test"; | ||
381 | foo!(a<|>); | ||
382 | } | ||
383 | "#, | ||
384 | r#" | ||
385 | macro_rules! foo {($i:ident) => {$i} } | ||
386 | fn main() { | ||
387 | let b = "test"; | ||
388 | foo!(b); | ||
389 | } | ||
390 | "#, | ||
391 | ); | ||
392 | } | ||
393 | |||
394 | #[test] | ||
395 | fn test_rename_for_macro_define_fn() { | ||
396 | check( | ||
397 | "bar", | ||
398 | r#" | ||
399 | macro_rules! define_fn {($id:ident) => { fn $id{} }} | ||
400 | define_fn!(foo); | ||
401 | fn main() { | ||
402 | fo<|>o(); | ||
403 | } | ||
404 | "#, | ||
405 | r#" | ||
406 | macro_rules! define_fn {($id:ident) => { fn $id{} }} | ||
407 | define_fn!(bar); | ||
408 | fn main() { | ||
409 | bar(); | ||
410 | } | ||
411 | "#, | ||
412 | ); | ||
413 | } | ||
414 | |||
415 | #[test] | ||
416 | fn test_rename_for_macro_define_fn_rev() { | ||
417 | check( | ||
418 | "bar", | ||
419 | r#" | ||
420 | macro_rules! define_fn {($id:ident) => { fn $id{} }} | ||
421 | define_fn!(fo<|>o); | ||
422 | fn main() { | ||
423 | foo(); | ||
424 | } | ||
425 | "#, | ||
426 | r#" | ||
427 | macro_rules! define_fn {($id:ident) => { fn $id{} }} | ||
428 | define_fn!(bar); | ||
429 | fn main() { | ||
430 | bar(); | ||
431 | } | ||
432 | "#, | ||
433 | ); | ||
434 | } | ||
435 | |||
436 | #[test] | ||
437 | fn test_rename_for_param_inside() { | ||
438 | check("j", r#"fn foo(i : u32) -> u32 { i<|> }"#, r#"fn foo(j : u32) -> u32 { j }"#); | ||
439 | } | ||
440 | |||
441 | #[test] | ||
442 | fn test_rename_refs_for_fn_param() { | ||
443 | check("j", r#"fn foo(i<|> : u32) -> u32 { i }"#, r#"fn foo(j : u32) -> u32 { j }"#); | ||
444 | } | ||
445 | |||
446 | #[test] | ||
447 | fn test_rename_for_mut_param() { | ||
448 | check("j", r#"fn foo(mut i<|> : u32) -> u32 { i }"#, r#"fn foo(mut j : u32) -> u32 { j }"#); | ||
449 | } | ||
450 | |||
451 | #[test] | ||
452 | fn test_rename_struct_field() { | ||
453 | check( | ||
454 | "j", | ||
455 | r#" | ||
456 | struct Foo { i<|>: i32 } | ||
457 | |||
458 | impl Foo { | ||
459 | fn new(i: i32) -> Self { | ||
460 | Self { i: i } | ||
461 | } | ||
462 | } | ||
463 | "#, | ||
464 | r#" | ||
465 | struct Foo { j: i32 } | ||
466 | |||
467 | impl Foo { | ||
468 | fn new(i: i32) -> Self { | ||
469 | Self { j: i } | ||
470 | } | ||
471 | } | ||
472 | "#, | ||
473 | ); | ||
474 | } | ||
475 | |||
476 | #[test] | ||
477 | fn test_rename_struct_field_for_shorthand() { | ||
478 | mark::check!(test_rename_struct_field_for_shorthand); | ||
479 | check( | ||
480 | "j", | ||
481 | r#" | ||
482 | struct Foo { i<|>: i32 } | ||
483 | |||
484 | impl Foo { | ||
485 | fn new(i: i32) -> Self { | ||
486 | Self { i } | ||
487 | } | ||
488 | } | ||
489 | "#, | ||
490 | r#" | ||
491 | struct Foo { j: i32 } | ||
492 | |||
493 | impl Foo { | ||
494 | fn new(i: i32) -> Self { | ||
495 | Self { j: i } | ||
496 | } | ||
497 | } | ||
498 | "#, | ||
499 | ); | ||
500 | } | ||
501 | |||
502 | #[test] | ||
503 | fn test_rename_local_for_field_shorthand() { | ||
504 | mark::check!(test_rename_local_for_field_shorthand); | ||
505 | check( | ||
506 | "j", | ||
507 | r#" | ||
508 | struct Foo { i: i32 } | ||
509 | |||
510 | impl Foo { | ||
511 | fn new(i<|>: i32) -> Self { | ||
512 | Self { i } | ||
513 | } | ||
514 | } | ||
515 | "#, | ||
516 | r#" | ||
517 | struct Foo { i: i32 } | ||
518 | |||
519 | impl Foo { | ||
520 | fn new(j: i32) -> Self { | ||
521 | Self { i: j } | ||
522 | } | ||
523 | } | ||
524 | "#, | ||
525 | ); | ||
526 | } | ||
527 | |||
528 | #[test] | ||
529 | fn test_field_shorthand_correct_struct() { | ||
530 | check( | ||
531 | "j", | ||
532 | r#" | ||
533 | struct Foo { i<|>: i32 } | ||
534 | struct Bar { i: i32 } | ||
535 | |||
536 | impl Bar { | ||
537 | fn new(i: i32) -> Self { | ||
538 | Self { i } | ||
539 | } | ||
540 | } | ||
541 | "#, | ||
542 | r#" | ||
543 | struct Foo { j: i32 } | ||
544 | struct Bar { i: i32 } | ||
545 | |||
546 | impl Bar { | ||
547 | fn new(i: i32) -> Self { | ||
548 | Self { i } | ||
549 | } | ||
550 | } | ||
551 | "#, | ||
552 | ); | ||
553 | } | ||
554 | |||
555 | #[test] | ||
556 | fn test_shadow_local_for_struct_shorthand() { | ||
557 | check( | ||
558 | "j", | ||
559 | r#" | ||
560 | struct Foo { i: i32 } | ||
561 | |||
562 | fn baz(i<|>: i32) -> Self { | ||
563 | let x = Foo { i }; | ||
564 | { | ||
565 | let i = 0; | ||
566 | Foo { i } | ||
567 | } | ||
568 | } | ||
569 | "#, | ||
570 | r#" | ||
571 | struct Foo { i: i32 } | ||
572 | |||
573 | fn baz(j: i32) -> Self { | ||
574 | let x = Foo { i: j }; | ||
575 | { | ||
576 | let i = 0; | ||
577 | Foo { i } | ||
578 | } | ||
579 | } | ||
580 | "#, | ||
581 | ); | ||
582 | } | ||
583 | |||
584 | #[test] | ||
585 | fn test_rename_mod() { | ||
586 | check_expect( | ||
587 | "foo2", | ||
588 | r#" | ||
589 | //- /lib.rs | ||
590 | mod bar; | ||
591 | |||
592 | //- /bar.rs | ||
593 | mod foo<|>; | ||
594 | |||
595 | //- /bar/foo.rs | ||
596 | // empty | ||
597 | "#, | ||
598 | expect![[r#" | ||
599 | RangeInfo { | ||
600 | range: 4..7, | ||
601 | info: SourceChange { | ||
602 | source_file_edits: [ | ||
603 | SourceFileEdit { | ||
604 | file_id: FileId( | ||
605 | 2, | ||
606 | ), | ||
607 | edit: TextEdit { | ||
608 | indels: [ | ||
609 | Indel { | ||
610 | insert: "foo2", | ||
611 | delete: 4..7, | ||
612 | }, | ||
613 | ], | ||
614 | }, | ||
615 | }, | ||
616 | ], | ||
617 | file_system_edits: [ | ||
618 | MoveFile { | ||
619 | src: FileId( | ||
620 | 3, | ||
621 | ), | ||
622 | anchor: FileId( | ||
623 | 3, | ||
624 | ), | ||
625 | dst: "foo2.rs", | ||
626 | }, | ||
627 | ], | ||
628 | is_snippet: false, | ||
629 | }, | ||
630 | } | ||
631 | "#]], | ||
632 | ); | ||
633 | } | ||
634 | |||
635 | #[test] | ||
636 | fn test_rename_mod_in_use_tree() { | ||
637 | check_expect( | ||
638 | "quux", | ||
639 | r#" | ||
640 | //- /main.rs | ||
641 | pub mod foo; | ||
642 | pub mod bar; | ||
643 | fn main() {} | ||
644 | |||
645 | //- /foo.rs | ||
646 | pub struct FooContent; | ||
647 | |||
648 | //- /bar.rs | ||
649 | use crate::foo<|>::FooContent; | ||
650 | "#, | ||
651 | expect![[r#" | ||
652 | RangeInfo { | ||
653 | range: 11..14, | ||
654 | info: SourceChange { | ||
655 | source_file_edits: [ | ||
656 | SourceFileEdit { | ||
657 | file_id: FileId( | ||
658 | 1, | ||
659 | ), | ||
660 | edit: TextEdit { | ||
661 | indels: [ | ||
662 | Indel { | ||
663 | insert: "quux", | ||
664 | delete: 8..11, | ||
665 | }, | ||
666 | ], | ||
667 | }, | ||
668 | }, | ||
669 | SourceFileEdit { | ||
670 | file_id: FileId( | ||
671 | 3, | ||
672 | ), | ||
673 | edit: TextEdit { | ||
674 | indels: [ | ||
675 | Indel { | ||
676 | insert: "quux", | ||
677 | delete: 11..14, | ||
678 | }, | ||
679 | ], | ||
680 | }, | ||
681 | }, | ||
682 | ], | ||
683 | file_system_edits: [ | ||
684 | MoveFile { | ||
685 | src: FileId( | ||
686 | 2, | ||
687 | ), | ||
688 | anchor: FileId( | ||
689 | 2, | ||
690 | ), | ||
691 | dst: "quux.rs", | ||
692 | }, | ||
693 | ], | ||
694 | is_snippet: false, | ||
695 | }, | ||
696 | } | ||
697 | "#]], | ||
698 | ); | ||
699 | } | ||
700 | |||
701 | #[test] | ||
702 | fn test_rename_mod_in_dir() { | ||
703 | check_expect( | ||
704 | "foo2", | ||
705 | r#" | ||
706 | //- /lib.rs | ||
707 | mod fo<|>o; | ||
708 | //- /foo/mod.rs | ||
709 | // emtpy | ||
710 | "#, | ||
711 | expect![[r#" | ||
712 | RangeInfo { | ||
713 | range: 4..7, | ||
714 | info: SourceChange { | ||
715 | source_file_edits: [ | ||
716 | SourceFileEdit { | ||
717 | file_id: FileId( | ||
718 | 1, | ||
719 | ), | ||
720 | edit: TextEdit { | ||
721 | indels: [ | ||
722 | Indel { | ||
723 | insert: "foo2", | ||
724 | delete: 4..7, | ||
725 | }, | ||
726 | ], | ||
727 | }, | ||
728 | }, | ||
729 | ], | ||
730 | file_system_edits: [ | ||
731 | MoveFile { | ||
732 | src: FileId( | ||
733 | 2, | ||
734 | ), | ||
735 | anchor: FileId( | ||
736 | 2, | ||
737 | ), | ||
738 | dst: "../foo2/mod.rs", | ||
739 | }, | ||
740 | ], | ||
741 | is_snippet: false, | ||
742 | }, | ||
743 | } | ||
744 | "#]], | ||
745 | ); | ||
746 | } | ||
747 | |||
748 | #[test] | ||
749 | fn test_rename_unusually_nested_mod() { | ||
750 | check_expect( | ||
751 | "bar", | ||
752 | r#" | ||
753 | //- /lib.rs | ||
754 | mod outer { mod fo<|>o; } | ||
755 | |||
756 | //- /outer/foo.rs | ||
757 | // emtpy | ||
758 | "#, | ||
759 | expect![[r#" | ||
760 | RangeInfo { | ||
761 | range: 16..19, | ||
762 | info: SourceChange { | ||
763 | source_file_edits: [ | ||
764 | SourceFileEdit { | ||
765 | file_id: FileId( | ||
766 | 1, | ||
767 | ), | ||
768 | edit: TextEdit { | ||
769 | indels: [ | ||
770 | Indel { | ||
771 | insert: "bar", | ||
772 | delete: 16..19, | ||
773 | }, | ||
774 | ], | ||
775 | }, | ||
776 | }, | ||
777 | ], | ||
778 | file_system_edits: [ | ||
779 | MoveFile { | ||
780 | src: FileId( | ||
781 | 2, | ||
782 | ), | ||
783 | anchor: FileId( | ||
784 | 2, | ||
785 | ), | ||
786 | dst: "bar.rs", | ||
787 | }, | ||
788 | ], | ||
789 | is_snippet: false, | ||
790 | }, | ||
791 | } | ||
792 | "#]], | ||
793 | ); | ||
794 | } | ||
795 | |||
796 | #[test] | ||
797 | fn test_module_rename_in_path() { | ||
798 | check( | ||
799 | "baz", | ||
800 | r#" | ||
801 | mod <|>foo { pub fn bar() {} } | ||
802 | |||
803 | fn main() { foo::bar(); } | ||
804 | "#, | ||
805 | r#" | ||
806 | mod baz { pub fn bar() {} } | ||
807 | |||
808 | fn main() { baz::bar(); } | ||
809 | "#, | ||
810 | ); | ||
811 | } | ||
812 | |||
813 | #[test] | ||
814 | fn test_rename_mod_filename_and_path() { | ||
815 | check_expect( | ||
816 | "foo2", | ||
817 | r#" | ||
818 | //- /lib.rs | ||
819 | mod bar; | ||
820 | fn f() { | ||
821 | bar::foo::fun() | ||
822 | } | ||
823 | |||
824 | //- /bar.rs | ||
825 | pub mod foo<|>; | ||
826 | |||
827 | //- /bar/foo.rs | ||
828 | // pub fn fun() {} | ||
829 | "#, | ||
830 | expect![[r#" | ||
831 | RangeInfo { | ||
832 | range: 8..11, | ||
833 | info: SourceChange { | ||
834 | source_file_edits: [ | ||
835 | SourceFileEdit { | ||
836 | file_id: FileId( | ||
837 | 2, | ||
838 | ), | ||
839 | edit: TextEdit { | ||
840 | indels: [ | ||
841 | Indel { | ||
842 | insert: "foo2", | ||
843 | delete: 8..11, | ||
844 | }, | ||
845 | ], | ||
846 | }, | ||
847 | }, | ||
848 | SourceFileEdit { | ||
849 | file_id: FileId( | ||
850 | 1, | ||
851 | ), | ||
852 | edit: TextEdit { | ||
853 | indels: [ | ||
854 | Indel { | ||
855 | insert: "foo2", | ||
856 | delete: 27..30, | ||
857 | }, | ||
858 | ], | ||
859 | }, | ||
860 | }, | ||
861 | ], | ||
862 | file_system_edits: [ | ||
863 | MoveFile { | ||
864 | src: FileId( | ||
865 | 3, | ||
866 | ), | ||
867 | anchor: FileId( | ||
868 | 3, | ||
869 | ), | ||
870 | dst: "foo2.rs", | ||
871 | }, | ||
872 | ], | ||
873 | is_snippet: false, | ||
874 | }, | ||
875 | } | ||
876 | "#]], | ||
877 | ); | ||
878 | } | ||
879 | |||
880 | #[test] | ||
881 | fn test_enum_variant_from_module_1() { | ||
882 | check( | ||
883 | "Baz", | ||
884 | r#" | ||
885 | mod foo { | ||
886 | pub enum Foo { Bar<|> } | ||
887 | } | ||
888 | |||
889 | fn func(f: foo::Foo) { | ||
890 | match f { | ||
891 | foo::Foo::Bar => {} | ||
892 | } | ||
893 | } | ||
894 | "#, | ||
895 | r#" | ||
896 | mod foo { | ||
897 | pub enum Foo { Baz } | ||
898 | } | ||
899 | |||
900 | fn func(f: foo::Foo) { | ||
901 | match f { | ||
902 | foo::Foo::Baz => {} | ||
903 | } | ||
904 | } | ||
905 | "#, | ||
906 | ); | ||
907 | } | ||
908 | |||
909 | #[test] | ||
910 | fn test_enum_variant_from_module_2() { | ||
911 | check( | ||
912 | "baz", | ||
913 | r#" | ||
914 | mod foo { | ||
915 | pub struct Foo { pub bar<|>: uint } | ||
916 | } | ||
917 | |||
918 | fn foo(f: foo::Foo) { | ||
919 | let _ = f.bar; | ||
920 | } | ||
921 | "#, | ||
922 | r#" | ||
923 | mod foo { | ||
924 | pub struct Foo { pub baz: uint } | ||
925 | } | ||
926 | |||
927 | fn foo(f: foo::Foo) { | ||
928 | let _ = f.baz; | ||
929 | } | ||
930 | "#, | ||
931 | ); | ||
932 | } | ||
933 | |||
934 | #[test] | ||
935 | fn test_parameter_to_self() { | ||
936 | check( | ||
937 | "self", | ||
938 | r#" | ||
939 | struct Foo { i: i32 } | ||
940 | |||
941 | impl Foo { | ||
942 | fn f(foo<|>: &mut Foo) -> i32 { | ||
943 | foo.i | ||
944 | } | ||
945 | } | ||
946 | "#, | ||
947 | r#" | ||
948 | struct Foo { i: i32 } | ||
949 | |||
950 | impl Foo { | ||
951 | fn f(&mut self) -> i32 { | ||
952 | self.i | ||
953 | } | ||
954 | } | ||
955 | "#, | ||
956 | ); | ||
957 | } | ||
958 | |||
959 | #[test] | ||
960 | fn test_self_to_parameter() { | ||
961 | check( | ||
962 | "foo", | ||
963 | r#" | ||
964 | struct Foo { i: i32 } | ||
965 | |||
966 | impl Foo { | ||
967 | fn f(&mut <|>self) -> i32 { | ||
968 | self.i | ||
969 | } | ||
970 | } | ||
971 | "#, | ||
972 | r#" | ||
973 | struct Foo { i: i32 } | ||
974 | |||
975 | impl Foo { | ||
976 | fn f(foo: &mut Foo) -> i32 { | ||
977 | foo.i | ||
978 | } | ||
979 | } | ||
980 | "#, | ||
981 | ); | ||
982 | } | ||
983 | |||
984 | #[test] | ||
985 | fn test_self_in_path_to_parameter() { | ||
986 | check( | ||
987 | "foo", | ||
988 | r#" | ||
989 | struct Foo { i: i32 } | ||
990 | |||
991 | impl Foo { | ||
992 | fn f(&self) -> i32 { | ||
993 | let self_var = 1; | ||
994 | self<|>.i | ||
995 | } | ||
996 | } | ||
997 | "#, | ||
998 | r#" | ||
999 | struct Foo { i: i32 } | ||
1000 | |||
1001 | impl Foo { | ||
1002 | fn f(foo: &Foo) -> i32 { | ||
1003 | let self_var = 1; | ||
1004 | foo.i | ||
1005 | } | ||
1006 | } | ||
1007 | "#, | ||
1008 | ); | ||
1009 | } | ||
1010 | } | ||
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs new file mode 100644 index 000000000..c3e07c8de --- /dev/null +++ b/crates/ide/src/runnables.rs | |||
@@ -0,0 +1,883 @@ | |||
1 | use std::fmt; | ||
2 | |||
3 | use cfg::CfgExpr; | ||
4 | use hir::{AsAssocItem, Attrs, HirFileId, InFile, Semantics}; | ||
5 | use ide_db::RootDatabase; | ||
6 | use itertools::Itertools; | ||
7 | use syntax::{ | ||
8 | ast::{self, AstNode, AttrsOwner, DocCommentsOwner, ModuleItemOwner, NameOwner}, | ||
9 | match_ast, SyntaxNode, | ||
10 | }; | ||
11 | |||
12 | use crate::{display::ToNav, FileId, NavigationTarget}; | ||
13 | |||
14 | #[derive(Debug, Clone)] | ||
15 | pub struct Runnable { | ||
16 | pub nav: NavigationTarget, | ||
17 | pub kind: RunnableKind, | ||
18 | pub cfg_exprs: Vec<CfgExpr>, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug, Clone)] | ||
22 | pub enum TestId { | ||
23 | Name(String), | ||
24 | Path(String), | ||
25 | } | ||
26 | |||
27 | impl fmt::Display for TestId { | ||
28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
29 | match self { | ||
30 | TestId::Name(name) => write!(f, "{}", name), | ||
31 | TestId::Path(path) => write!(f, "{}", path), | ||
32 | } | ||
33 | } | ||
34 | } | ||
35 | |||
36 | #[derive(Debug, Clone)] | ||
37 | pub enum RunnableKind { | ||
38 | Test { test_id: TestId, attr: TestAttr }, | ||
39 | TestMod { path: String }, | ||
40 | Bench { test_id: TestId }, | ||
41 | DocTest { test_id: TestId }, | ||
42 | Bin, | ||
43 | } | ||
44 | |||
45 | #[derive(Debug, Eq, PartialEq)] | ||
46 | pub struct RunnableAction { | ||
47 | pub run_title: &'static str, | ||
48 | pub debugee: bool, | ||
49 | } | ||
50 | |||
51 | const TEST: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Test", debugee: true }; | ||
52 | const DOCTEST: RunnableAction = | ||
53 | RunnableAction { run_title: "▶\u{fe0e} Run Doctest", debugee: false }; | ||
54 | const BENCH: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Bench", debugee: true }; | ||
55 | const BIN: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run", debugee: true }; | ||
56 | |||
57 | impl Runnable { | ||
58 | // test package::module::testname | ||
59 | pub fn label(&self, target: Option<String>) -> String { | ||
60 | match &self.kind { | ||
61 | RunnableKind::Test { test_id, .. } => format!("test {}", test_id), | ||
62 | RunnableKind::TestMod { path } => format!("test-mod {}", path), | ||
63 | RunnableKind::Bench { test_id } => format!("bench {}", test_id), | ||
64 | RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id), | ||
65 | RunnableKind::Bin => { | ||
66 | target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t)) | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | pub fn action(&self) -> &'static RunnableAction { | ||
72 | match &self.kind { | ||
73 | RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => &TEST, | ||
74 | RunnableKind::DocTest { .. } => &DOCTEST, | ||
75 | RunnableKind::Bench { .. } => &BENCH, | ||
76 | RunnableKind::Bin => &BIN, | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | |||
81 | // Feature: Run | ||
82 | // | ||
83 | // Shows a popup suggesting to run a test/benchmark/binary **at the current cursor | ||
84 | // location**. Super useful for repeatedly running just a single test. Do bind this | ||
85 | // to a shortcut! | ||
86 | // | ||
87 | // |=== | ||
88 | // | Editor | Action Name | ||
89 | // | ||
90 | // | VS Code | **Rust Analyzer: Run** | ||
91 | // |=== | ||
92 | pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | ||
93 | let sema = Semantics::new(db); | ||
94 | let source_file = sema.parse(file_id); | ||
95 | source_file.syntax().descendants().filter_map(|i| runnable(&sema, i, file_id)).collect() | ||
96 | } | ||
97 | |||
98 | pub(crate) fn runnable( | ||
99 | sema: &Semantics<RootDatabase>, | ||
100 | item: SyntaxNode, | ||
101 | file_id: FileId, | ||
102 | ) -> Option<Runnable> { | ||
103 | match_ast! { | ||
104 | match item { | ||
105 | ast::Fn(it) => runnable_fn(sema, it, file_id), | ||
106 | ast::Module(it) => runnable_mod(sema, it, file_id), | ||
107 | _ => None, | ||
108 | } | ||
109 | } | ||
110 | } | ||
111 | |||
112 | fn runnable_fn( | ||
113 | sema: &Semantics<RootDatabase>, | ||
114 | fn_def: ast::Fn, | ||
115 | file_id: FileId, | ||
116 | ) -> Option<Runnable> { | ||
117 | let name_string = fn_def.name()?.text().to_string(); | ||
118 | |||
119 | let kind = if name_string == "main" { | ||
120 | RunnableKind::Bin | ||
121 | } else { | ||
122 | let test_id = match sema.to_def(&fn_def).map(|def| def.module(sema.db)) { | ||
123 | Some(module) => { | ||
124 | let def = sema.to_def(&fn_def)?; | ||
125 | let impl_trait_name = def.as_assoc_item(sema.db).and_then(|assoc_item| { | ||
126 | match assoc_item.container(sema.db) { | ||
127 | hir::AssocItemContainer::Trait(trait_item) => { | ||
128 | Some(trait_item.name(sema.db).to_string()) | ||
129 | } | ||
130 | hir::AssocItemContainer::ImplDef(impl_def) => impl_def | ||
131 | .target_ty(sema.db) | ||
132 | .as_adt() | ||
133 | .map(|adt| adt.name(sema.db).to_string()), | ||
134 | } | ||
135 | }); | ||
136 | |||
137 | let path_iter = module | ||
138 | .path_to_root(sema.db) | ||
139 | .into_iter() | ||
140 | .rev() | ||
141 | .filter_map(|it| it.name(sema.db)) | ||
142 | .map(|name| name.to_string()); | ||
143 | |||
144 | let path = if let Some(impl_trait_name) = impl_trait_name { | ||
145 | path_iter | ||
146 | .chain(std::iter::once(impl_trait_name)) | ||
147 | .chain(std::iter::once(name_string)) | ||
148 | .join("::") | ||
149 | } else { | ||
150 | path_iter.chain(std::iter::once(name_string)).join("::") | ||
151 | }; | ||
152 | |||
153 | TestId::Path(path) | ||
154 | } | ||
155 | None => TestId::Name(name_string), | ||
156 | }; | ||
157 | |||
158 | if has_test_related_attribute(&fn_def) { | ||
159 | let attr = TestAttr::from_fn(&fn_def); | ||
160 | RunnableKind::Test { test_id, attr } | ||
161 | } else if fn_def.has_atom_attr("bench") { | ||
162 | RunnableKind::Bench { test_id } | ||
163 | } else if has_doc_test(&fn_def) { | ||
164 | RunnableKind::DocTest { test_id } | ||
165 | } else { | ||
166 | return None; | ||
167 | } | ||
168 | }; | ||
169 | |||
170 | let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &fn_def)); | ||
171 | let cfg_exprs = attrs.cfg().collect(); | ||
172 | |||
173 | let nav = if let RunnableKind::DocTest { .. } = kind { | ||
174 | NavigationTarget::from_doc_commented( | ||
175 | sema.db, | ||
176 | InFile::new(file_id.into(), &fn_def), | ||
177 | InFile::new(file_id.into(), &fn_def), | ||
178 | ) | ||
179 | } else { | ||
180 | NavigationTarget::from_named(sema.db, InFile::new(file_id.into(), &fn_def)) | ||
181 | }; | ||
182 | Some(Runnable { nav, kind, cfg_exprs }) | ||
183 | } | ||
184 | |||
185 | #[derive(Debug, Copy, Clone)] | ||
186 | pub struct TestAttr { | ||
187 | pub ignore: bool, | ||
188 | } | ||
189 | |||
190 | impl TestAttr { | ||
191 | fn from_fn(fn_def: &ast::Fn) -> TestAttr { | ||
192 | let ignore = fn_def | ||
193 | .attrs() | ||
194 | .filter_map(|attr| attr.simple_name()) | ||
195 | .any(|attribute_text| attribute_text == "ignore"); | ||
196 | TestAttr { ignore } | ||
197 | } | ||
198 | } | ||
199 | |||
200 | /// This is a method with a heuristics to support test methods annotated with custom test annotations, such as | ||
201 | /// `#[test_case(...)]`, `#[tokio::test]` and similar. | ||
202 | /// Also a regular `#[test]` annotation is supported. | ||
203 | /// | ||
204 | /// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test, | ||
205 | /// but it's better than not to have the runnables for the tests at all. | ||
206 | fn has_test_related_attribute(fn_def: &ast::Fn) -> bool { | ||
207 | fn_def | ||
208 | .attrs() | ||
209 | .filter_map(|attr| attr.path()) | ||
210 | .map(|path| path.syntax().to_string().to_lowercase()) | ||
211 | .any(|attribute_text| attribute_text.contains("test")) | ||
212 | } | ||
213 | |||
214 | fn has_doc_test(fn_def: &ast::Fn) -> bool { | ||
215 | fn_def.doc_comment_text().map_or(false, |comment| comment.contains("```")) | ||
216 | } | ||
217 | |||
218 | fn runnable_mod( | ||
219 | sema: &Semantics<RootDatabase>, | ||
220 | module: ast::Module, | ||
221 | file_id: FileId, | ||
222 | ) -> Option<Runnable> { | ||
223 | if !has_test_function_or_multiple_test_submodules(&module) { | ||
224 | return None; | ||
225 | } | ||
226 | let module_def = sema.to_def(&module)?; | ||
227 | |||
228 | let path = module_def | ||
229 | .path_to_root(sema.db) | ||
230 | .into_iter() | ||
231 | .rev() | ||
232 | .filter_map(|it| it.name(sema.db)) | ||
233 | .join("::"); | ||
234 | |||
235 | let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &module)); | ||
236 | let cfg_exprs = attrs.cfg().collect(); | ||
237 | let nav = module_def.to_nav(sema.db); | ||
238 | Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg_exprs }) | ||
239 | } | ||
240 | |||
241 | // We could create runnables for modules with number_of_test_submodules > 0, | ||
242 | // but that bloats the runnables for no real benefit, since all tests can be run by the submodule already | ||
243 | fn has_test_function_or_multiple_test_submodules(module: &ast::Module) -> bool { | ||
244 | if let Some(item_list) = module.item_list() { | ||
245 | let mut number_of_test_submodules = 0; | ||
246 | |||
247 | for item in item_list.items() { | ||
248 | match item { | ||
249 | ast::Item::Fn(f) => { | ||
250 | if has_test_related_attribute(&f) { | ||
251 | return true; | ||
252 | } | ||
253 | } | ||
254 | ast::Item::Module(submodule) => { | ||
255 | if has_test_function_or_multiple_test_submodules(&submodule) { | ||
256 | number_of_test_submodules += 1; | ||
257 | } | ||
258 | } | ||
259 | _ => (), | ||
260 | } | ||
261 | } | ||
262 | |||
263 | number_of_test_submodules > 1 | ||
264 | } else { | ||
265 | false | ||
266 | } | ||
267 | } | ||
268 | |||
269 | #[cfg(test)] | ||
270 | mod tests { | ||
271 | use expect::{expect, Expect}; | ||
272 | |||
273 | use crate::mock_analysis::analysis_and_position; | ||
274 | |||
275 | use super::{RunnableAction, BENCH, BIN, DOCTEST, TEST}; | ||
276 | |||
277 | fn check( | ||
278 | ra_fixture: &str, | ||
279 | // FIXME: fold this into `expect` as well | ||
280 | actions: &[&RunnableAction], | ||
281 | expect: Expect, | ||
282 | ) { | ||
283 | let (analysis, position) = analysis_and_position(ra_fixture); | ||
284 | let runnables = analysis.runnables(position.file_id).unwrap(); | ||
285 | expect.assert_debug_eq(&runnables); | ||
286 | assert_eq!( | ||
287 | actions, | ||
288 | runnables.into_iter().map(|it| it.action()).collect::<Vec<_>>().as_slice() | ||
289 | ); | ||
290 | } | ||
291 | |||
292 | #[test] | ||
293 | fn test_runnables() { | ||
294 | check( | ||
295 | r#" | ||
296 | //- /lib.rs | ||
297 | <|> | ||
298 | fn main() {} | ||
299 | |||
300 | #[test] | ||
301 | fn test_foo() {} | ||
302 | |||
303 | #[test] | ||
304 | #[ignore] | ||
305 | fn test_foo() {} | ||
306 | |||
307 | #[bench] | ||
308 | fn bench() {} | ||
309 | "#, | ||
310 | &[&BIN, &TEST, &TEST, &BENCH], | ||
311 | expect![[r#" | ||
312 | [ | ||
313 | Runnable { | ||
314 | nav: NavigationTarget { | ||
315 | file_id: FileId( | ||
316 | 1, | ||
317 | ), | ||
318 | full_range: 1..13, | ||
319 | focus_range: Some( | ||
320 | 4..8, | ||
321 | ), | ||
322 | name: "main", | ||
323 | kind: FN, | ||
324 | container_name: None, | ||
325 | description: None, | ||
326 | docs: None, | ||
327 | }, | ||
328 | kind: Bin, | ||
329 | cfg_exprs: [], | ||
330 | }, | ||
331 | Runnable { | ||
332 | nav: NavigationTarget { | ||
333 | file_id: FileId( | ||
334 | 1, | ||
335 | ), | ||
336 | full_range: 15..39, | ||
337 | focus_range: Some( | ||
338 | 26..34, | ||
339 | ), | ||
340 | name: "test_foo", | ||
341 | kind: FN, | ||
342 | container_name: None, | ||
343 | description: None, | ||
344 | docs: None, | ||
345 | }, | ||
346 | kind: Test { | ||
347 | test_id: Path( | ||
348 | "test_foo", | ||
349 | ), | ||
350 | attr: TestAttr { | ||
351 | ignore: false, | ||
352 | }, | ||
353 | }, | ||
354 | cfg_exprs: [], | ||
355 | }, | ||
356 | Runnable { | ||
357 | nav: NavigationTarget { | ||
358 | file_id: FileId( | ||
359 | 1, | ||
360 | ), | ||
361 | full_range: 41..75, | ||
362 | focus_range: Some( | ||
363 | 62..70, | ||
364 | ), | ||
365 | name: "test_foo", | ||
366 | kind: FN, | ||
367 | container_name: None, | ||
368 | description: None, | ||
369 | docs: None, | ||
370 | }, | ||
371 | kind: Test { | ||
372 | test_id: Path( | ||
373 | "test_foo", | ||
374 | ), | ||
375 | attr: TestAttr { | ||
376 | ignore: true, | ||
377 | }, | ||
378 | }, | ||
379 | cfg_exprs: [], | ||
380 | }, | ||
381 | Runnable { | ||
382 | nav: NavigationTarget { | ||
383 | file_id: FileId( | ||
384 | 1, | ||
385 | ), | ||
386 | full_range: 77..99, | ||
387 | focus_range: Some( | ||
388 | 89..94, | ||
389 | ), | ||
390 | name: "bench", | ||
391 | kind: FN, | ||
392 | container_name: None, | ||
393 | description: None, | ||
394 | docs: None, | ||
395 | }, | ||
396 | kind: Bench { | ||
397 | test_id: Path( | ||
398 | "bench", | ||
399 | ), | ||
400 | }, | ||
401 | cfg_exprs: [], | ||
402 | }, | ||
403 | ] | ||
404 | "#]], | ||
405 | ); | ||
406 | } | ||
407 | |||
408 | #[test] | ||
409 | fn test_runnables_doc_test() { | ||
410 | check( | ||
411 | r#" | ||
412 | //- /lib.rs | ||
413 | <|> | ||
414 | fn main() {} | ||
415 | |||
416 | /// ``` | ||
417 | /// let x = 5; | ||
418 | /// ``` | ||
419 | fn foo() {} | ||
420 | "#, | ||
421 | &[&BIN, &DOCTEST], | ||
422 | expect![[r#" | ||
423 | [ | ||
424 | Runnable { | ||
425 | nav: NavigationTarget { | ||
426 | file_id: FileId( | ||
427 | 1, | ||
428 | ), | ||
429 | full_range: 1..13, | ||
430 | focus_range: Some( | ||
431 | 4..8, | ||
432 | ), | ||
433 | name: "main", | ||
434 | kind: FN, | ||
435 | container_name: None, | ||
436 | description: None, | ||
437 | docs: None, | ||
438 | }, | ||
439 | kind: Bin, | ||
440 | cfg_exprs: [], | ||
441 | }, | ||
442 | Runnable { | ||
443 | nav: NavigationTarget { | ||
444 | file_id: FileId( | ||
445 | 1, | ||
446 | ), | ||
447 | full_range: 15..57, | ||
448 | focus_range: None, | ||
449 | name: "foo", | ||
450 | kind: FN, | ||
451 | container_name: None, | ||
452 | description: None, | ||
453 | docs: None, | ||
454 | }, | ||
455 | kind: DocTest { | ||
456 | test_id: Path( | ||
457 | "foo", | ||
458 | ), | ||
459 | }, | ||
460 | cfg_exprs: [], | ||
461 | }, | ||
462 | ] | ||
463 | "#]], | ||
464 | ); | ||
465 | } | ||
466 | |||
467 | #[test] | ||
468 | fn test_runnables_doc_test_in_impl() { | ||
469 | check( | ||
470 | r#" | ||
471 | //- /lib.rs | ||
472 | <|> | ||
473 | fn main() {} | ||
474 | |||
475 | struct Data; | ||
476 | impl Data { | ||
477 | /// ``` | ||
478 | /// let x = 5; | ||
479 | /// ``` | ||
480 | fn foo() {} | ||
481 | } | ||
482 | "#, | ||
483 | &[&BIN, &DOCTEST], | ||
484 | expect![[r#" | ||
485 | [ | ||
486 | Runnable { | ||
487 | nav: NavigationTarget { | ||
488 | file_id: FileId( | ||
489 | 1, | ||
490 | ), | ||
491 | full_range: 1..13, | ||
492 | focus_range: Some( | ||
493 | 4..8, | ||
494 | ), | ||
495 | name: "main", | ||
496 | kind: FN, | ||
497 | container_name: None, | ||
498 | description: None, | ||
499 | docs: None, | ||
500 | }, | ||
501 | kind: Bin, | ||
502 | cfg_exprs: [], | ||
503 | }, | ||
504 | Runnable { | ||
505 | nav: NavigationTarget { | ||
506 | file_id: FileId( | ||
507 | 1, | ||
508 | ), | ||
509 | full_range: 44..98, | ||
510 | focus_range: None, | ||
511 | name: "foo", | ||
512 | kind: FN, | ||
513 | container_name: None, | ||
514 | description: None, | ||
515 | docs: None, | ||
516 | }, | ||
517 | kind: DocTest { | ||
518 | test_id: Path( | ||
519 | "Data::foo", | ||
520 | ), | ||
521 | }, | ||
522 | cfg_exprs: [], | ||
523 | }, | ||
524 | ] | ||
525 | "#]], | ||
526 | ); | ||
527 | } | ||
528 | |||
529 | #[test] | ||
530 | fn test_runnables_module() { | ||
531 | check( | ||
532 | r#" | ||
533 | //- /lib.rs | ||
534 | <|> | ||
535 | mod test_mod { | ||
536 | #[test] | ||
537 | fn test_foo1() {} | ||
538 | } | ||
539 | "#, | ||
540 | &[&TEST, &TEST], | ||
541 | expect![[r#" | ||
542 | [ | ||
543 | Runnable { | ||
544 | nav: NavigationTarget { | ||
545 | file_id: FileId( | ||
546 | 1, | ||
547 | ), | ||
548 | full_range: 1..51, | ||
549 | focus_range: Some( | ||
550 | 5..13, | ||
551 | ), | ||
552 | name: "test_mod", | ||
553 | kind: MODULE, | ||
554 | container_name: None, | ||
555 | description: None, | ||
556 | docs: None, | ||
557 | }, | ||
558 | kind: TestMod { | ||
559 | path: "test_mod", | ||
560 | }, | ||
561 | cfg_exprs: [], | ||
562 | }, | ||
563 | Runnable { | ||
564 | nav: NavigationTarget { | ||
565 | file_id: FileId( | ||
566 | 1, | ||
567 | ), | ||
568 | full_range: 20..49, | ||
569 | focus_range: Some( | ||
570 | 35..44, | ||
571 | ), | ||
572 | name: "test_foo1", | ||
573 | kind: FN, | ||
574 | container_name: None, | ||
575 | description: None, | ||
576 | docs: None, | ||
577 | }, | ||
578 | kind: Test { | ||
579 | test_id: Path( | ||
580 | "test_mod::test_foo1", | ||
581 | ), | ||
582 | attr: TestAttr { | ||
583 | ignore: false, | ||
584 | }, | ||
585 | }, | ||
586 | cfg_exprs: [], | ||
587 | }, | ||
588 | ] | ||
589 | "#]], | ||
590 | ); | ||
591 | } | ||
592 | |||
593 | #[test] | ||
594 | fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() { | ||
595 | check( | ||
596 | r#" | ||
597 | //- /lib.rs | ||
598 | <|> | ||
599 | mod root_tests { | ||
600 | mod nested_tests_0 { | ||
601 | mod nested_tests_1 { | ||
602 | #[test] | ||
603 | fn nested_test_11() {} | ||
604 | |||
605 | #[test] | ||
606 | fn nested_test_12() {} | ||
607 | } | ||
608 | |||
609 | mod nested_tests_2 { | ||
610 | #[test] | ||
611 | fn nested_test_2() {} | ||
612 | } | ||
613 | |||
614 | mod nested_tests_3 {} | ||
615 | } | ||
616 | |||
617 | mod nested_tests_4 {} | ||
618 | } | ||
619 | "#, | ||
620 | &[&TEST, &TEST, &TEST, &TEST, &TEST, &TEST], | ||
621 | expect![[r#" | ||
622 | [ | ||
623 | Runnable { | ||
624 | nav: NavigationTarget { | ||
625 | file_id: FileId( | ||
626 | 1, | ||
627 | ), | ||
628 | full_range: 22..323, | ||
629 | focus_range: Some( | ||
630 | 26..40, | ||
631 | ), | ||
632 | name: "nested_tests_0", | ||
633 | kind: MODULE, | ||
634 | container_name: None, | ||
635 | description: None, | ||
636 | docs: None, | ||
637 | }, | ||
638 | kind: TestMod { | ||
639 | path: "root_tests::nested_tests_0", | ||
640 | }, | ||
641 | cfg_exprs: [], | ||
642 | }, | ||
643 | Runnable { | ||
644 | nav: NavigationTarget { | ||
645 | file_id: FileId( | ||
646 | 1, | ||
647 | ), | ||
648 | full_range: 51..192, | ||
649 | focus_range: Some( | ||
650 | 55..69, | ||
651 | ), | ||
652 | name: "nested_tests_1", | ||
653 | kind: MODULE, | ||
654 | container_name: None, | ||
655 | description: None, | ||
656 | docs: None, | ||
657 | }, | ||
658 | kind: TestMod { | ||
659 | path: "root_tests::nested_tests_0::nested_tests_1", | ||
660 | }, | ||
661 | cfg_exprs: [], | ||
662 | }, | ||
663 | Runnable { | ||
664 | nav: NavigationTarget { | ||
665 | file_id: FileId( | ||
666 | 1, | ||
667 | ), | ||
668 | full_range: 84..126, | ||
669 | focus_range: Some( | ||
670 | 107..121, | ||
671 | ), | ||
672 | name: "nested_test_11", | ||
673 | kind: FN, | ||
674 | container_name: None, | ||
675 | description: None, | ||
676 | docs: None, | ||
677 | }, | ||
678 | kind: Test { | ||
679 | test_id: Path( | ||
680 | "root_tests::nested_tests_0::nested_tests_1::nested_test_11", | ||
681 | ), | ||
682 | attr: TestAttr { | ||
683 | ignore: false, | ||
684 | }, | ||
685 | }, | ||
686 | cfg_exprs: [], | ||
687 | }, | ||
688 | Runnable { | ||
689 | nav: NavigationTarget { | ||
690 | file_id: FileId( | ||
691 | 1, | ||
692 | ), | ||
693 | full_range: 140..182, | ||
694 | focus_range: Some( | ||
695 | 163..177, | ||
696 | ), | ||
697 | name: "nested_test_12", | ||
698 | kind: FN, | ||
699 | container_name: None, | ||
700 | description: None, | ||
701 | docs: None, | ||
702 | }, | ||
703 | kind: Test { | ||
704 | test_id: Path( | ||
705 | "root_tests::nested_tests_0::nested_tests_1::nested_test_12", | ||
706 | ), | ||
707 | attr: TestAttr { | ||
708 | ignore: false, | ||
709 | }, | ||
710 | }, | ||
711 | cfg_exprs: [], | ||
712 | }, | ||
713 | Runnable { | ||
714 | nav: NavigationTarget { | ||
715 | file_id: FileId( | ||
716 | 1, | ||
717 | ), | ||
718 | full_range: 202..286, | ||
719 | focus_range: Some( | ||
720 | 206..220, | ||
721 | ), | ||
722 | name: "nested_tests_2", | ||
723 | kind: MODULE, | ||
724 | container_name: None, | ||
725 | description: None, | ||
726 | docs: None, | ||
727 | }, | ||
728 | kind: TestMod { | ||
729 | path: "root_tests::nested_tests_0::nested_tests_2", | ||
730 | }, | ||
731 | cfg_exprs: [], | ||
732 | }, | ||
733 | Runnable { | ||
734 | nav: NavigationTarget { | ||
735 | file_id: FileId( | ||
736 | 1, | ||
737 | ), | ||
738 | full_range: 235..276, | ||
739 | focus_range: Some( | ||
740 | 258..271, | ||
741 | ), | ||
742 | name: "nested_test_2", | ||
743 | kind: FN, | ||
744 | container_name: None, | ||
745 | description: None, | ||
746 | docs: None, | ||
747 | }, | ||
748 | kind: Test { | ||
749 | test_id: Path( | ||
750 | "root_tests::nested_tests_0::nested_tests_2::nested_test_2", | ||
751 | ), | ||
752 | attr: TestAttr { | ||
753 | ignore: false, | ||
754 | }, | ||
755 | }, | ||
756 | cfg_exprs: [], | ||
757 | }, | ||
758 | ] | ||
759 | "#]], | ||
760 | ); | ||
761 | } | ||
762 | |||
763 | #[test] | ||
764 | fn test_runnables_with_feature() { | ||
765 | check( | ||
766 | r#" | ||
767 | //- /lib.rs crate:foo cfg:feature=foo | ||
768 | <|> | ||
769 | #[test] | ||
770 | #[cfg(feature = "foo")] | ||
771 | fn test_foo1() {} | ||
772 | "#, | ||
773 | &[&TEST], | ||
774 | expect![[r#" | ||
775 | [ | ||
776 | Runnable { | ||
777 | nav: NavigationTarget { | ||
778 | file_id: FileId( | ||
779 | 1, | ||
780 | ), | ||
781 | full_range: 1..50, | ||
782 | focus_range: Some( | ||
783 | 36..45, | ||
784 | ), | ||
785 | name: "test_foo1", | ||
786 | kind: FN, | ||
787 | container_name: None, | ||
788 | description: None, | ||
789 | docs: None, | ||
790 | }, | ||
791 | kind: Test { | ||
792 | test_id: Path( | ||
793 | "test_foo1", | ||
794 | ), | ||
795 | attr: TestAttr { | ||
796 | ignore: false, | ||
797 | }, | ||
798 | }, | ||
799 | cfg_exprs: [ | ||
800 | KeyValue { | ||
801 | key: "feature", | ||
802 | value: "foo", | ||
803 | }, | ||
804 | ], | ||
805 | }, | ||
806 | ] | ||
807 | "#]], | ||
808 | ); | ||
809 | } | ||
810 | |||
811 | #[test] | ||
812 | fn test_runnables_with_features() { | ||
813 | check( | ||
814 | r#" | ||
815 | //- /lib.rs crate:foo cfg:feature=foo,feature=bar | ||
816 | <|> | ||
817 | #[test] | ||
818 | #[cfg(all(feature = "foo", feature = "bar"))] | ||
819 | fn test_foo1() {} | ||
820 | "#, | ||
821 | &[&TEST], | ||
822 | expect![[r#" | ||
823 | [ | ||
824 | Runnable { | ||
825 | nav: NavigationTarget { | ||
826 | file_id: FileId( | ||
827 | 1, | ||
828 | ), | ||
829 | full_range: 1..72, | ||
830 | focus_range: Some( | ||
831 | 58..67, | ||
832 | ), | ||
833 | name: "test_foo1", | ||
834 | kind: FN, | ||
835 | container_name: None, | ||
836 | description: None, | ||
837 | docs: None, | ||
838 | }, | ||
839 | kind: Test { | ||
840 | test_id: Path( | ||
841 | "test_foo1", | ||
842 | ), | ||
843 | attr: TestAttr { | ||
844 | ignore: false, | ||
845 | }, | ||
846 | }, | ||
847 | cfg_exprs: [ | ||
848 | All( | ||
849 | [ | ||
850 | KeyValue { | ||
851 | key: "feature", | ||
852 | value: "foo", | ||
853 | }, | ||
854 | KeyValue { | ||
855 | key: "feature", | ||
856 | value: "bar", | ||
857 | }, | ||
858 | ], | ||
859 | ), | ||
860 | ], | ||
861 | }, | ||
862 | ] | ||
863 | "#]], | ||
864 | ); | ||
865 | } | ||
866 | |||
867 | #[test] | ||
868 | fn test_runnables_no_test_function_in_module() { | ||
869 | check( | ||
870 | r#" | ||
871 | //- /lib.rs | ||
872 | <|> | ||
873 | mod test_mod { | ||
874 | fn foo1() {} | ||
875 | } | ||
876 | "#, | ||
877 | &[], | ||
878 | expect![[r#" | ||
879 | [] | ||
880 | "#]], | ||
881 | ); | ||
882 | } | ||
883 | } | ||
diff --git a/crates/ide/src/status.rs b/crates/ide/src/status.rs new file mode 100644 index 000000000..c23708181 --- /dev/null +++ b/crates/ide/src/status.rs | |||
@@ -0,0 +1,145 @@ | |||
1 | use std::{fmt, iter::FromIterator, sync::Arc}; | ||
2 | |||
3 | use base_db::{ | ||
4 | salsa::debug::{DebugQueryTable, TableEntry}, | ||
5 | FileTextQuery, SourceRootId, | ||
6 | }; | ||
7 | use hir::MacroFile; | ||
8 | use ide_db::{ | ||
9 | symbol_index::{LibrarySymbolsQuery, SymbolIndex}, | ||
10 | RootDatabase, | ||
11 | }; | ||
12 | use profile::{memory_usage, Bytes}; | ||
13 | use rustc_hash::FxHashMap; | ||
14 | use syntax::{ast, Parse, SyntaxNode}; | ||
15 | |||
16 | use crate::FileId; | ||
17 | |||
18 | fn syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { | ||
19 | base_db::ParseQuery.in_db(db).entries::<SyntaxTreeStats>() | ||
20 | } | ||
21 | fn macro_syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { | ||
22 | hir::db::ParseMacroQuery.in_db(db).entries::<SyntaxTreeStats>() | ||
23 | } | ||
24 | |||
25 | // Feature: Status | ||
26 | // | ||
27 | // Shows internal statistic about memory usage of rust-analyzer. | ||
28 | // | ||
29 | // |=== | ||
30 | // | Editor | Action Name | ||
31 | // | ||
32 | // | VS Code | **Rust Analyzer: Status** | ||
33 | // |=== | ||
34 | pub(crate) fn status(db: &RootDatabase) -> String { | ||
35 | let files_stats = FileTextQuery.in_db(db).entries::<FilesStats>(); | ||
36 | let syntax_tree_stats = syntax_tree_stats(db); | ||
37 | let macro_syntax_tree_stats = macro_syntax_tree_stats(db); | ||
38 | let symbols_stats = LibrarySymbolsQuery.in_db(db).entries::<LibrarySymbolsStats>(); | ||
39 | format!( | ||
40 | "{}\n{}\n{}\n{} (macros)\n\n\nmemory:\n{}\ngc {:?} seconds ago", | ||
41 | files_stats, | ||
42 | symbols_stats, | ||
43 | syntax_tree_stats, | ||
44 | macro_syntax_tree_stats, | ||
45 | memory_usage(), | ||
46 | db.last_gc.elapsed().as_secs(), | ||
47 | ) | ||
48 | } | ||
49 | |||
50 | #[derive(Default)] | ||
51 | struct FilesStats { | ||
52 | total: usize, | ||
53 | size: Bytes, | ||
54 | } | ||
55 | |||
56 | impl fmt::Display for FilesStats { | ||
57 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
58 | write!(fmt, "{} ({}) files", self.total, self.size) | ||
59 | } | ||
60 | } | ||
61 | |||
62 | impl FromIterator<TableEntry<FileId, Arc<String>>> for FilesStats { | ||
63 | fn from_iter<T>(iter: T) -> FilesStats | ||
64 | where | ||
65 | T: IntoIterator<Item = TableEntry<FileId, Arc<String>>>, | ||
66 | { | ||
67 | let mut res = FilesStats::default(); | ||
68 | for entry in iter { | ||
69 | res.total += 1; | ||
70 | res.size += entry.value.unwrap().len(); | ||
71 | } | ||
72 | res | ||
73 | } | ||
74 | } | ||
75 | |||
76 | #[derive(Default)] | ||
77 | pub(crate) struct SyntaxTreeStats { | ||
78 | total: usize, | ||
79 | pub(crate) retained: usize, | ||
80 | } | ||
81 | |||
82 | impl fmt::Display for SyntaxTreeStats { | ||
83 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
84 | write!(fmt, "{} trees, {} retained", self.total, self.retained) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | impl FromIterator<TableEntry<FileId, Parse<ast::SourceFile>>> for SyntaxTreeStats { | ||
89 | fn from_iter<T>(iter: T) -> SyntaxTreeStats | ||
90 | where | ||
91 | T: IntoIterator<Item = TableEntry<FileId, Parse<ast::SourceFile>>>, | ||
92 | { | ||
93 | let mut res = SyntaxTreeStats::default(); | ||
94 | for entry in iter { | ||
95 | res.total += 1; | ||
96 | res.retained += entry.value.is_some() as usize; | ||
97 | } | ||
98 | res | ||
99 | } | ||
100 | } | ||
101 | |||
102 | impl<M> FromIterator<TableEntry<MacroFile, Option<(Parse<SyntaxNode>, M)>>> for SyntaxTreeStats { | ||
103 | fn from_iter<T>(iter: T) -> SyntaxTreeStats | ||
104 | where | ||
105 | T: IntoIterator<Item = TableEntry<MacroFile, Option<(Parse<SyntaxNode>, M)>>>, | ||
106 | { | ||
107 | let mut res = SyntaxTreeStats::default(); | ||
108 | for entry in iter { | ||
109 | res.total += 1; | ||
110 | res.retained += entry.value.is_some() as usize; | ||
111 | } | ||
112 | res | ||
113 | } | ||
114 | } | ||
115 | |||
116 | #[derive(Default)] | ||
117 | struct LibrarySymbolsStats { | ||
118 | total: usize, | ||
119 | size: Bytes, | ||
120 | } | ||
121 | |||
122 | impl fmt::Display for LibrarySymbolsStats { | ||
123 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
124 | write!(fmt, "{} ({}) symbols", self.total, self.size) | ||
125 | } | ||
126 | } | ||
127 | |||
128 | impl FromIterator<TableEntry<(), Arc<FxHashMap<SourceRootId, SymbolIndex>>>> | ||
129 | for LibrarySymbolsStats | ||
130 | { | ||
131 | fn from_iter<T>(iter: T) -> LibrarySymbolsStats | ||
132 | where | ||
133 | T: IntoIterator<Item = TableEntry<(), Arc<FxHashMap<SourceRootId, SymbolIndex>>>>, | ||
134 | { | ||
135 | let mut res = LibrarySymbolsStats::default(); | ||
136 | for entry in iter { | ||
137 | let value = entry.value.unwrap(); | ||
138 | for symbols in value.values() { | ||
139 | res.total += symbols.len(); | ||
140 | res.size += symbols.memory_size(); | ||
141 | } | ||
142 | } | ||
143 | res | ||
144 | } | ||
145 | } | ||
diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs new file mode 100644 index 000000000..5d7c7e8d0 --- /dev/null +++ b/crates/ide/src/syntax_highlighting.rs | |||
@@ -0,0 +1,872 @@ | |||
1 | mod tags; | ||
2 | mod html; | ||
3 | mod injection; | ||
4 | #[cfg(test)] | ||
5 | mod tests; | ||
6 | |||
7 | use hir::{Name, Semantics, VariantDef}; | ||
8 | use ide_db::{ | ||
9 | defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, | ||
10 | RootDatabase, | ||
11 | }; | ||
12 | use rustc_hash::FxHashMap; | ||
13 | use syntax::{ | ||
14 | ast::{self, HasFormatSpecifier}, | ||
15 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, | ||
16 | SyntaxKind::*, | ||
17 | TextRange, WalkEvent, T, | ||
18 | }; | ||
19 | |||
20 | use crate::FileId; | ||
21 | |||
22 | use ast::FormatSpecifier; | ||
23 | pub(crate) use html::highlight_as_html; | ||
24 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; | ||
25 | |||
26 | #[derive(Debug, Clone)] | ||
27 | pub struct HighlightedRange { | ||
28 | pub range: TextRange, | ||
29 | pub highlight: Highlight, | ||
30 | pub binding_hash: Option<u64>, | ||
31 | } | ||
32 | |||
33 | // Feature: Semantic Syntax Highlighting | ||
34 | // | ||
35 | // rust-analyzer highlights the code semantically. | ||
36 | // For example, `bar` in `foo::Bar` might be colored differently depending on whether `Bar` is an enum or a trait. | ||
37 | // rust-analyzer does not specify colors directly, instead it assigns tag (like `struct`) and a set of modifiers (like `declaration`) to each token. | ||
38 | // It's up to the client to map those to specific colors. | ||
39 | // | ||
40 | // The general rule is that a reference to an entity gets colored the same way as the entity itself. | ||
41 | // We also give special modifier for `mut` and `&mut` local variables. | ||
42 | pub(crate) fn highlight( | ||
43 | db: &RootDatabase, | ||
44 | file_id: FileId, | ||
45 | range_to_highlight: Option<TextRange>, | ||
46 | syntactic_name_ref_highlighting: bool, | ||
47 | ) -> Vec<HighlightedRange> { | ||
48 | let _p = profile::span("highlight"); | ||
49 | let sema = Semantics::new(db); | ||
50 | |||
51 | // Determine the root based on the given range. | ||
52 | let (root, range_to_highlight) = { | ||
53 | let source_file = sema.parse(file_id); | ||
54 | match range_to_highlight { | ||
55 | Some(range) => { | ||
56 | let node = match source_file.syntax().covering_element(range) { | ||
57 | NodeOrToken::Node(it) => it, | ||
58 | NodeOrToken::Token(it) => it.parent(), | ||
59 | }; | ||
60 | (node, range) | ||
61 | } | ||
62 | None => (source_file.syntax().clone(), source_file.syntax().text_range()), | ||
63 | } | ||
64 | }; | ||
65 | |||
66 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); | ||
67 | // We use a stack for the DFS traversal below. | ||
68 | // When we leave a node, the we use it to flatten the highlighted ranges. | ||
69 | let mut stack = HighlightedRangeStack::new(); | ||
70 | |||
71 | let mut current_macro_call: Option<ast::MacroCall> = None; | ||
72 | let mut format_string: Option<SyntaxElement> = None; | ||
73 | |||
74 | // Walk all nodes, keeping track of whether we are inside a macro or not. | ||
75 | // If in macro, expand it first and highlight the expanded code. | ||
76 | for event in root.preorder_with_tokens() { | ||
77 | match &event { | ||
78 | WalkEvent::Enter(_) => stack.push(), | ||
79 | WalkEvent::Leave(_) => stack.pop(), | ||
80 | }; | ||
81 | |||
82 | let event_range = match &event { | ||
83 | WalkEvent::Enter(it) => it.text_range(), | ||
84 | WalkEvent::Leave(it) => it.text_range(), | ||
85 | }; | ||
86 | |||
87 | // Element outside of the viewport, no need to highlight | ||
88 | if range_to_highlight.intersect(event_range).is_none() { | ||
89 | continue; | ||
90 | } | ||
91 | |||
92 | // Track "inside macro" state | ||
93 | match event.clone().map(|it| it.into_node().and_then(ast::MacroCall::cast)) { | ||
94 | WalkEvent::Enter(Some(mc)) => { | ||
95 | current_macro_call = Some(mc.clone()); | ||
96 | if let Some(range) = macro_call_range(&mc) { | ||
97 | stack.add(HighlightedRange { | ||
98 | range, | ||
99 | highlight: HighlightTag::Macro.into(), | ||
100 | binding_hash: None, | ||
101 | }); | ||
102 | } | ||
103 | if let Some(name) = mc.is_macro_rules() { | ||
104 | if let Some((highlight, binding_hash)) = highlight_element( | ||
105 | &sema, | ||
106 | &mut bindings_shadow_count, | ||
107 | syntactic_name_ref_highlighting, | ||
108 | name.syntax().clone().into(), | ||
109 | ) { | ||
110 | stack.add(HighlightedRange { | ||
111 | range: name.syntax().text_range(), | ||
112 | highlight, | ||
113 | binding_hash, | ||
114 | }); | ||
115 | } | ||
116 | } | ||
117 | continue; | ||
118 | } | ||
119 | WalkEvent::Leave(Some(mc)) => { | ||
120 | assert!(current_macro_call == Some(mc)); | ||
121 | current_macro_call = None; | ||
122 | format_string = None; | ||
123 | } | ||
124 | _ => (), | ||
125 | } | ||
126 | |||
127 | // Check for Rust code in documentation | ||
128 | match &event { | ||
129 | WalkEvent::Leave(NodeOrToken::Node(node)) => { | ||
130 | if let Some((doctest, range_mapping, new_comments)) = | ||
131 | injection::extract_doc_comments(node) | ||
132 | { | ||
133 | injection::highlight_doc_comment( | ||
134 | doctest, | ||
135 | range_mapping, | ||
136 | new_comments, | ||
137 | &mut stack, | ||
138 | ); | ||
139 | } | ||
140 | } | ||
141 | _ => (), | ||
142 | } | ||
143 | |||
144 | let element = match event { | ||
145 | WalkEvent::Enter(it) => it, | ||
146 | WalkEvent::Leave(_) => continue, | ||
147 | }; | ||
148 | |||
149 | let range = element.text_range(); | ||
150 | |||
151 | let element_to_highlight = if current_macro_call.is_some() && element.kind() != COMMENT { | ||
152 | // Inside a macro -- expand it first | ||
153 | let token = match element.clone().into_token() { | ||
154 | Some(it) if it.parent().kind() == TOKEN_TREE => it, | ||
155 | _ => continue, | ||
156 | }; | ||
157 | let token = sema.descend_into_macros(token.clone()); | ||
158 | let parent = token.parent(); | ||
159 | |||
160 | // Check if macro takes a format string and remember it for highlighting later. | ||
161 | // The macros that accept a format string expand to a compiler builtin macros | ||
162 | // `format_args` and `format_args_nl`. | ||
163 | if let Some(name) = parent | ||
164 | .parent() | ||
165 | .and_then(ast::MacroCall::cast) | ||
166 | .and_then(|mc| mc.path()) | ||
167 | .and_then(|p| p.segment()) | ||
168 | .and_then(|s| s.name_ref()) | ||
169 | { | ||
170 | match name.text().as_str() { | ||
171 | "format_args" | "format_args_nl" => { | ||
172 | format_string = parent | ||
173 | .children_with_tokens() | ||
174 | .filter(|t| t.kind() != WHITESPACE) | ||
175 | .nth(1) | ||
176 | .filter(|e| { | ||
177 | ast::String::can_cast(e.kind()) | ||
178 | || ast::RawString::can_cast(e.kind()) | ||
179 | }) | ||
180 | } | ||
181 | _ => {} | ||
182 | } | ||
183 | } | ||
184 | |||
185 | // We only care Name and Name_ref | ||
186 | match (token.kind(), parent.kind()) { | ||
187 | (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(), | ||
188 | _ => token.into(), | ||
189 | } | ||
190 | } else { | ||
191 | element.clone() | ||
192 | }; | ||
193 | |||
194 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { | ||
195 | let expanded = element_to_highlight.as_token().unwrap().clone(); | ||
196 | if injection::highlight_injection(&mut stack, &sema, token, expanded).is_some() { | ||
197 | continue; | ||
198 | } | ||
199 | } | ||
200 | |||
201 | let is_format_string = format_string.as_ref() == Some(&element_to_highlight); | ||
202 | |||
203 | if let Some((highlight, binding_hash)) = highlight_element( | ||
204 | &sema, | ||
205 | &mut bindings_shadow_count, | ||
206 | syntactic_name_ref_highlighting, | ||
207 | element_to_highlight.clone(), | ||
208 | ) { | ||
209 | stack.add(HighlightedRange { range, highlight, binding_hash }); | ||
210 | if let Some(string) = | ||
211 | element_to_highlight.as_token().cloned().and_then(ast::String::cast) | ||
212 | { | ||
213 | if is_format_string { | ||
214 | stack.push(); | ||
215 | string.lex_format_specifier(|piece_range, kind| { | ||
216 | if let Some(highlight) = highlight_format_specifier(kind) { | ||
217 | stack.add(HighlightedRange { | ||
218 | range: piece_range + range.start(), | ||
219 | highlight: highlight.into(), | ||
220 | binding_hash: None, | ||
221 | }); | ||
222 | } | ||
223 | }); | ||
224 | stack.pop(); | ||
225 | } | ||
226 | // Highlight escape sequences | ||
227 | if let Some(char_ranges) = string.char_ranges() { | ||
228 | stack.push(); | ||
229 | for (piece_range, _) in char_ranges.iter().filter(|(_, char)| char.is_ok()) { | ||
230 | if string.text()[piece_range.start().into()..].starts_with('\\') { | ||
231 | stack.add(HighlightedRange { | ||
232 | range: piece_range + range.start(), | ||
233 | highlight: HighlightTag::EscapeSequence.into(), | ||
234 | binding_hash: None, | ||
235 | }); | ||
236 | } | ||
237 | } | ||
238 | stack.pop_and_inject(None); | ||
239 | } | ||
240 | } else if let Some(string) = | ||
241 | element_to_highlight.as_token().cloned().and_then(ast::RawString::cast) | ||
242 | { | ||
243 | if is_format_string { | ||
244 | stack.push(); | ||
245 | string.lex_format_specifier(|piece_range, kind| { | ||
246 | if let Some(highlight) = highlight_format_specifier(kind) { | ||
247 | stack.add(HighlightedRange { | ||
248 | range: piece_range + range.start(), | ||
249 | highlight: highlight.into(), | ||
250 | binding_hash: None, | ||
251 | }); | ||
252 | } | ||
253 | }); | ||
254 | stack.pop(); | ||
255 | } | ||
256 | } | ||
257 | } | ||
258 | } | ||
259 | |||
260 | stack.flattened() | ||
261 | } | ||
262 | |||
263 | #[derive(Debug)] | ||
264 | struct HighlightedRangeStack { | ||
265 | stack: Vec<Vec<HighlightedRange>>, | ||
266 | } | ||
267 | |||
268 | /// We use a stack to implement the flattening logic for the highlighted | ||
269 | /// syntax ranges. | ||
270 | impl HighlightedRangeStack { | ||
271 | fn new() -> Self { | ||
272 | Self { stack: vec![Vec::new()] } | ||
273 | } | ||
274 | |||
275 | fn push(&mut self) { | ||
276 | self.stack.push(Vec::new()); | ||
277 | } | ||
278 | |||
279 | /// Flattens the highlighted ranges. | ||
280 | /// | ||
281 | /// For example `#[cfg(feature = "foo")]` contains the nested ranges: | ||
282 | /// 1) parent-range: Attribute [0, 23) | ||
283 | /// 2) child-range: String [16, 21) | ||
284 | /// | ||
285 | /// The following code implements the flattening, for our example this results to: | ||
286 | /// `[Attribute [0, 16), String [16, 21), Attribute [21, 23)]` | ||
287 | fn pop(&mut self) { | ||
288 | let children = self.stack.pop().unwrap(); | ||
289 | let prev = self.stack.last_mut().unwrap(); | ||
290 | let needs_flattening = !children.is_empty() | ||
291 | && !prev.is_empty() | ||
292 | && prev.last().unwrap().range.contains_range(children.first().unwrap().range); | ||
293 | if !needs_flattening { | ||
294 | prev.extend(children); | ||
295 | } else { | ||
296 | let mut parent = prev.pop().unwrap(); | ||
297 | for ele in children { | ||
298 | assert!(parent.range.contains_range(ele.range)); | ||
299 | |||
300 | let cloned = Self::intersect(&mut parent, &ele); | ||
301 | if !parent.range.is_empty() { | ||
302 | prev.push(parent); | ||
303 | } | ||
304 | prev.push(ele); | ||
305 | parent = cloned; | ||
306 | } | ||
307 | if !parent.range.is_empty() { | ||
308 | prev.push(parent); | ||
309 | } | ||
310 | } | ||
311 | } | ||
312 | |||
313 | /// Intersects the `HighlightedRange` `parent` with `child`. | ||
314 | /// `parent` is mutated in place, becoming the range before `child`. | ||
315 | /// Returns the range (of the same type as `parent`) *after* `child`. | ||
316 | fn intersect(parent: &mut HighlightedRange, child: &HighlightedRange) -> HighlightedRange { | ||
317 | assert!(parent.range.contains_range(child.range)); | ||
318 | |||
319 | let mut cloned = parent.clone(); | ||
320 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
321 | cloned.range = TextRange::new(child.range.end(), cloned.range.end()); | ||
322 | |||
323 | cloned | ||
324 | } | ||
325 | |||
326 | /// Remove the `HighlightRange` of `parent` that's currently covered by `child`. | ||
327 | fn intersect_partial(parent: &mut HighlightedRange, child: &HighlightedRange) { | ||
328 | assert!( | ||
329 | parent.range.start() <= child.range.start() | ||
330 | && parent.range.end() >= child.range.start() | ||
331 | && child.range.end() > parent.range.end() | ||
332 | ); | ||
333 | |||
334 | parent.range = TextRange::new(parent.range.start(), child.range.start()); | ||
335 | } | ||
336 | |||
337 | /// Similar to `pop`, but can modify arbitrary prior ranges (where `pop`) | ||
338 | /// can only modify the last range currently on the stack. | ||
339 | /// Can be used to do injections that span multiple ranges, like the | ||
340 | /// doctest injection below. | ||
341 | /// If `overwrite_parent` is non-optional, the highlighting of the parent range | ||
342 | /// is overwritten with the argument. | ||
343 | /// | ||
344 | /// Note that `pop` can be simulated by `pop_and_inject(false)` but the | ||
345 | /// latter is computationally more expensive. | ||
346 | fn pop_and_inject(&mut self, overwrite_parent: Option<Highlight>) { | ||
347 | let mut children = self.stack.pop().unwrap(); | ||
348 | let prev = self.stack.last_mut().unwrap(); | ||
349 | children.sort_by_key(|range| range.range.start()); | ||
350 | prev.sort_by_key(|range| range.range.start()); | ||
351 | |||
352 | for child in children { | ||
353 | if let Some(idx) = | ||
354 | prev.iter().position(|parent| parent.range.contains_range(child.range)) | ||
355 | { | ||
356 | if let Some(tag) = overwrite_parent { | ||
357 | prev[idx].highlight = tag; | ||
358 | } | ||
359 | |||
360 | let cloned = Self::intersect(&mut prev[idx], &child); | ||
361 | let insert_idx = if prev[idx].range.is_empty() { | ||
362 | prev.remove(idx); | ||
363 | idx | ||
364 | } else { | ||
365 | idx + 1 | ||
366 | }; | ||
367 | prev.insert(insert_idx, child); | ||
368 | if !cloned.range.is_empty() { | ||
369 | prev.insert(insert_idx + 1, cloned); | ||
370 | } | ||
371 | } else { | ||
372 | let maybe_idx = | ||
373 | prev.iter().position(|parent| parent.range.contains(child.range.start())); | ||
374 | match (overwrite_parent, maybe_idx) { | ||
375 | (Some(_), Some(idx)) => { | ||
376 | Self::intersect_partial(&mut prev[idx], &child); | ||
377 | let insert_idx = if prev[idx].range.is_empty() { | ||
378 | prev.remove(idx); | ||
379 | idx | ||
380 | } else { | ||
381 | idx + 1 | ||
382 | }; | ||
383 | prev.insert(insert_idx, child); | ||
384 | } | ||
385 | (_, None) => { | ||
386 | let idx = prev | ||
387 | .binary_search_by_key(&child.range.start(), |range| range.range.start()) | ||
388 | .unwrap_or_else(|x| x); | ||
389 | prev.insert(idx, child); | ||
390 | } | ||
391 | _ => { | ||
392 | unreachable!("child range should be completely contained in parent range"); | ||
393 | } | ||
394 | } | ||
395 | } | ||
396 | } | ||
397 | } | ||
398 | |||
399 | fn add(&mut self, range: HighlightedRange) { | ||
400 | self.stack | ||
401 | .last_mut() | ||
402 | .expect("during DFS traversal, the stack must not be empty") | ||
403 | .push(range) | ||
404 | } | ||
405 | |||
406 | fn flattened(mut self) -> Vec<HighlightedRange> { | ||
407 | assert_eq!( | ||
408 | self.stack.len(), | ||
409 | 1, | ||
410 | "after DFS traversal, the stack should only contain a single element" | ||
411 | ); | ||
412 | let mut res = self.stack.pop().unwrap(); | ||
413 | res.sort_by_key(|range| range.range.start()); | ||
414 | // Check that ranges are sorted and disjoint | ||
415 | assert!(res | ||
416 | .iter() | ||
417 | .zip(res.iter().skip(1)) | ||
418 | .all(|(left, right)| left.range.end() <= right.range.start())); | ||
419 | res | ||
420 | } | ||
421 | } | ||
422 | |||
423 | fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HighlightTag> { | ||
424 | Some(match kind { | ||
425 | FormatSpecifier::Open | ||
426 | | FormatSpecifier::Close | ||
427 | | FormatSpecifier::Colon | ||
428 | | FormatSpecifier::Fill | ||
429 | | FormatSpecifier::Align | ||
430 | | FormatSpecifier::Sign | ||
431 | | FormatSpecifier::NumberSign | ||
432 | | FormatSpecifier::DollarSign | ||
433 | | FormatSpecifier::Dot | ||
434 | | FormatSpecifier::Asterisk | ||
435 | | FormatSpecifier::QuestionMark => HighlightTag::FormatSpecifier, | ||
436 | FormatSpecifier::Integer | FormatSpecifier::Zero => HighlightTag::NumericLiteral, | ||
437 | FormatSpecifier::Identifier => HighlightTag::Local, | ||
438 | }) | ||
439 | } | ||
440 | |||
441 | fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { | ||
442 | let path = macro_call.path()?; | ||
443 | let name_ref = path.segment()?.name_ref()?; | ||
444 | |||
445 | let range_start = name_ref.syntax().text_range().start(); | ||
446 | let mut range_end = name_ref.syntax().text_range().end(); | ||
447 | for sibling in path.syntax().siblings_with_tokens(Direction::Next) { | ||
448 | match sibling.kind() { | ||
449 | T![!] | IDENT => range_end = sibling.text_range().end(), | ||
450 | _ => (), | ||
451 | } | ||
452 | } | ||
453 | |||
454 | Some(TextRange::new(range_start, range_end)) | ||
455 | } | ||
456 | |||
457 | fn is_possibly_unsafe(name_ref: &ast::NameRef) -> bool { | ||
458 | name_ref | ||
459 | .syntax() | ||
460 | .parent() | ||
461 | .and_then(|parent| { | ||
462 | ast::FieldExpr::cast(parent.clone()) | ||
463 | .map(|_| true) | ||
464 | .or_else(|| ast::RecordPatField::cast(parent).map(|_| true)) | ||
465 | }) | ||
466 | .unwrap_or(false) | ||
467 | } | ||
468 | |||
469 | fn highlight_element( | ||
470 | sema: &Semantics<RootDatabase>, | ||
471 | bindings_shadow_count: &mut FxHashMap<Name, u32>, | ||
472 | syntactic_name_ref_highlighting: bool, | ||
473 | element: SyntaxElement, | ||
474 | ) -> Option<(Highlight, Option<u64>)> { | ||
475 | let db = sema.db; | ||
476 | let mut binding_hash = None; | ||
477 | let highlight: Highlight = match element.kind() { | ||
478 | FN => { | ||
479 | bindings_shadow_count.clear(); | ||
480 | return None; | ||
481 | } | ||
482 | |||
483 | // Highlight definitions depending on the "type" of the definition. | ||
484 | NAME => { | ||
485 | let name = element.into_node().and_then(ast::Name::cast).unwrap(); | ||
486 | let name_kind = classify_name(sema, &name); | ||
487 | |||
488 | if let Some(NameClass::Definition(Definition::Local(local))) = &name_kind { | ||
489 | if let Some(name) = local.name(db) { | ||
490 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | ||
491 | *shadow_count += 1; | ||
492 | binding_hash = Some(calc_binding_hash(&name, *shadow_count)) | ||
493 | } | ||
494 | }; | ||
495 | |||
496 | match name_kind { | ||
497 | Some(NameClass::ExternCrate(_)) => HighlightTag::Module.into(), | ||
498 | Some(NameClass::Definition(def)) => { | ||
499 | highlight_name(sema, db, def, None, false) | HighlightModifier::Definition | ||
500 | } | ||
501 | Some(NameClass::ConstReference(def)) => highlight_name(sema, db, def, None, false), | ||
502 | Some(NameClass::FieldShorthand { field, .. }) => { | ||
503 | let mut h = HighlightTag::Field.into(); | ||
504 | if let Definition::Field(field) = field { | ||
505 | if let VariantDef::Union(_) = field.parent_def(db) { | ||
506 | h |= HighlightModifier::Unsafe; | ||
507 | } | ||
508 | } | ||
509 | |||
510 | h | ||
511 | } | ||
512 | None => highlight_name_by_syntax(name) | HighlightModifier::Definition, | ||
513 | } | ||
514 | } | ||
515 | |||
516 | // Highlight references like the definitions they resolve to | ||
517 | NAME_REF if element.ancestors().any(|it| it.kind() == ATTR) => { | ||
518 | Highlight::from(HighlightTag::Function) | HighlightModifier::Attribute | ||
519 | } | ||
520 | NAME_REF => { | ||
521 | let name_ref = element.into_node().and_then(ast::NameRef::cast).unwrap(); | ||
522 | let possibly_unsafe = is_possibly_unsafe(&name_ref); | ||
523 | match classify_name_ref(sema, &name_ref) { | ||
524 | Some(name_kind) => match name_kind { | ||
525 | NameRefClass::ExternCrate(_) => HighlightTag::Module.into(), | ||
526 | NameRefClass::Definition(def) => { | ||
527 | if let Definition::Local(local) = &def { | ||
528 | if let Some(name) = local.name(db) { | ||
529 | let shadow_count = | ||
530 | bindings_shadow_count.entry(name.clone()).or_default(); | ||
531 | binding_hash = Some(calc_binding_hash(&name, *shadow_count)) | ||
532 | } | ||
533 | }; | ||
534 | highlight_name(sema, db, def, Some(name_ref), possibly_unsafe) | ||
535 | } | ||
536 | NameRefClass::FieldShorthand { .. } => HighlightTag::Field.into(), | ||
537 | }, | ||
538 | None if syntactic_name_ref_highlighting => { | ||
539 | highlight_name_ref_by_syntax(name_ref, sema) | ||
540 | } | ||
541 | None => HighlightTag::UnresolvedReference.into(), | ||
542 | } | ||
543 | } | ||
544 | |||
545 | // Simple token-based highlighting | ||
546 | COMMENT => { | ||
547 | let comment = element.into_token().and_then(ast::Comment::cast)?; | ||
548 | let h = HighlightTag::Comment; | ||
549 | match comment.kind().doc { | ||
550 | Some(_) => h | HighlightModifier::Documentation, | ||
551 | None => h.into(), | ||
552 | } | ||
553 | } | ||
554 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => HighlightTag::StringLiteral.into(), | ||
555 | ATTR => HighlightTag::Attribute.into(), | ||
556 | INT_NUMBER | FLOAT_NUMBER => HighlightTag::NumericLiteral.into(), | ||
557 | BYTE => HighlightTag::ByteLiteral.into(), | ||
558 | CHAR => HighlightTag::CharLiteral.into(), | ||
559 | QUESTION => Highlight::new(HighlightTag::Operator) | HighlightModifier::ControlFlow, | ||
560 | LIFETIME => { | ||
561 | let h = Highlight::new(HighlightTag::Lifetime); | ||
562 | match element.parent().map(|it| it.kind()) { | ||
563 | Some(LIFETIME_PARAM) | Some(LABEL) => h | HighlightModifier::Definition, | ||
564 | _ => h, | ||
565 | } | ||
566 | } | ||
567 | p if p.is_punct() => match p { | ||
568 | T![&] => { | ||
569 | let h = HighlightTag::Operator.into(); | ||
570 | let is_unsafe = element | ||
571 | .parent() | ||
572 | .and_then(ast::RefExpr::cast) | ||
573 | .map(|ref_expr| sema.is_unsafe_ref_expr(&ref_expr)) | ||
574 | .unwrap_or(false); | ||
575 | if is_unsafe { | ||
576 | h | HighlightModifier::Unsafe | ||
577 | } else { | ||
578 | h | ||
579 | } | ||
580 | } | ||
581 | T![::] | T![->] | T![=>] | T![..] | T![=] | T![@] => HighlightTag::Operator.into(), | ||
582 | T![!] if element.parent().and_then(ast::MacroCall::cast).is_some() => { | ||
583 | HighlightTag::Macro.into() | ||
584 | } | ||
585 | T![*] if element.parent().and_then(ast::PtrType::cast).is_some() => { | ||
586 | HighlightTag::Keyword.into() | ||
587 | } | ||
588 | T![*] if element.parent().and_then(ast::PrefixExpr::cast).is_some() => { | ||
589 | let prefix_expr = element.parent().and_then(ast::PrefixExpr::cast)?; | ||
590 | |||
591 | let expr = prefix_expr.expr()?; | ||
592 | let ty = sema.type_of_expr(&expr)?; | ||
593 | if ty.is_raw_ptr() { | ||
594 | HighlightTag::Operator | HighlightModifier::Unsafe | ||
595 | } else if let Some(ast::PrefixOp::Deref) = prefix_expr.op_kind() { | ||
596 | HighlightTag::Operator.into() | ||
597 | } else { | ||
598 | HighlightTag::Punctuation.into() | ||
599 | } | ||
600 | } | ||
601 | T![-] if element.parent().and_then(ast::PrefixExpr::cast).is_some() => { | ||
602 | HighlightTag::NumericLiteral.into() | ||
603 | } | ||
604 | _ if element.parent().and_then(ast::PrefixExpr::cast).is_some() => { | ||
605 | HighlightTag::Operator.into() | ||
606 | } | ||
607 | _ if element.parent().and_then(ast::BinExpr::cast).is_some() => { | ||
608 | HighlightTag::Operator.into() | ||
609 | } | ||
610 | _ if element.parent().and_then(ast::RangeExpr::cast).is_some() => { | ||
611 | HighlightTag::Operator.into() | ||
612 | } | ||
613 | _ if element.parent().and_then(ast::RangePat::cast).is_some() => { | ||
614 | HighlightTag::Operator.into() | ||
615 | } | ||
616 | _ if element.parent().and_then(ast::RestPat::cast).is_some() => { | ||
617 | HighlightTag::Operator.into() | ||
618 | } | ||
619 | _ if element.parent().and_then(ast::Attr::cast).is_some() => { | ||
620 | HighlightTag::Attribute.into() | ||
621 | } | ||
622 | _ => HighlightTag::Punctuation.into(), | ||
623 | }, | ||
624 | |||
625 | k if k.is_keyword() => { | ||
626 | let h = Highlight::new(HighlightTag::Keyword); | ||
627 | match k { | ||
628 | T![break] | ||
629 | | T![continue] | ||
630 | | T![else] | ||
631 | | T![if] | ||
632 | | T![loop] | ||
633 | | T![match] | ||
634 | | T![return] | ||
635 | | T![while] | ||
636 | | T![in] => h | HighlightModifier::ControlFlow, | ||
637 | T![for] if !is_child_of_impl(&element) => h | HighlightModifier::ControlFlow, | ||
638 | T![unsafe] => h | HighlightModifier::Unsafe, | ||
639 | T![true] | T![false] => HighlightTag::BoolLiteral.into(), | ||
640 | T![self] => { | ||
641 | let self_param_is_mut = element | ||
642 | .parent() | ||
643 | .and_then(ast::SelfParam::cast) | ||
644 | .and_then(|p| p.mut_token()) | ||
645 | .is_some(); | ||
646 | // closure to enforce lazyness | ||
647 | let self_path = || { | ||
648 | sema.resolve_path(&element.parent()?.parent().and_then(ast::Path::cast)?) | ||
649 | }; | ||
650 | if self_param_is_mut | ||
651 | || matches!(self_path(), | ||
652 | Some(hir::PathResolution::Local(local)) | ||
653 | if local.is_self(db) | ||
654 | && (local.is_mut(db) || local.ty(db).is_mutable_reference()) | ||
655 | ) | ||
656 | { | ||
657 | HighlightTag::SelfKeyword | HighlightModifier::Mutable | ||
658 | } else { | ||
659 | HighlightTag::SelfKeyword.into() | ||
660 | } | ||
661 | } | ||
662 | T![ref] => element | ||
663 | .parent() | ||
664 | .and_then(ast::IdentPat::cast) | ||
665 | .and_then(|ident_pat| { | ||
666 | if sema.is_unsafe_ident_pat(&ident_pat) { | ||
667 | Some(HighlightModifier::Unsafe) | ||
668 | } else { | ||
669 | None | ||
670 | } | ||
671 | }) | ||
672 | .map(|modifier| h | modifier) | ||
673 | .unwrap_or(h), | ||
674 | _ => h, | ||
675 | } | ||
676 | } | ||
677 | |||
678 | _ => return None, | ||
679 | }; | ||
680 | |||
681 | return Some((highlight, binding_hash)); | ||
682 | |||
683 | fn calc_binding_hash(name: &Name, shadow_count: u32) -> u64 { | ||
684 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { | ||
685 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | ||
686 | |||
687 | let mut hasher = DefaultHasher::new(); | ||
688 | x.hash(&mut hasher); | ||
689 | hasher.finish() | ||
690 | } | ||
691 | |||
692 | hash((name, shadow_count)) | ||
693 | } | ||
694 | } | ||
695 | |||
696 | fn is_child_of_impl(element: &SyntaxElement) -> bool { | ||
697 | match element.parent() { | ||
698 | Some(e) => e.kind() == IMPL, | ||
699 | _ => false, | ||
700 | } | ||
701 | } | ||
702 | |||
703 | fn highlight_name( | ||
704 | sema: &Semantics<RootDatabase>, | ||
705 | db: &RootDatabase, | ||
706 | def: Definition, | ||
707 | name_ref: Option<ast::NameRef>, | ||
708 | possibly_unsafe: bool, | ||
709 | ) -> Highlight { | ||
710 | match def { | ||
711 | Definition::Macro(_) => HighlightTag::Macro, | ||
712 | Definition::Field(field) => { | ||
713 | let mut h = HighlightTag::Field.into(); | ||
714 | if possibly_unsafe { | ||
715 | if let VariantDef::Union(_) = field.parent_def(db) { | ||
716 | h |= HighlightModifier::Unsafe; | ||
717 | } | ||
718 | } | ||
719 | |||
720 | return h; | ||
721 | } | ||
722 | Definition::ModuleDef(def) => match def { | ||
723 | hir::ModuleDef::Module(_) => HighlightTag::Module, | ||
724 | hir::ModuleDef::Function(func) => { | ||
725 | let mut h = HighlightTag::Function.into(); | ||
726 | if func.is_unsafe(db) { | ||
727 | h |= HighlightModifier::Unsafe; | ||
728 | } else { | ||
729 | let is_unsafe = name_ref | ||
730 | .and_then(|name_ref| name_ref.syntax().parent()) | ||
731 | .and_then(ast::MethodCallExpr::cast) | ||
732 | .map(|method_call_expr| sema.is_unsafe_method_call(method_call_expr)) | ||
733 | .unwrap_or(false); | ||
734 | if is_unsafe { | ||
735 | h |= HighlightModifier::Unsafe; | ||
736 | } | ||
737 | } | ||
738 | return h; | ||
739 | } | ||
740 | hir::ModuleDef::Adt(hir::Adt::Struct(_)) => HighlightTag::Struct, | ||
741 | hir::ModuleDef::Adt(hir::Adt::Enum(_)) => HighlightTag::Enum, | ||
742 | hir::ModuleDef::Adt(hir::Adt::Union(_)) => HighlightTag::Union, | ||
743 | hir::ModuleDef::EnumVariant(_) => HighlightTag::EnumVariant, | ||
744 | hir::ModuleDef::Const(_) => HighlightTag::Constant, | ||
745 | hir::ModuleDef::Trait(_) => HighlightTag::Trait, | ||
746 | hir::ModuleDef::TypeAlias(_) => HighlightTag::TypeAlias, | ||
747 | hir::ModuleDef::BuiltinType(_) => HighlightTag::BuiltinType, | ||
748 | hir::ModuleDef::Static(s) => { | ||
749 | let mut h = Highlight::new(HighlightTag::Static); | ||
750 | if s.is_mut(db) { | ||
751 | h |= HighlightModifier::Mutable; | ||
752 | h |= HighlightModifier::Unsafe; | ||
753 | } | ||
754 | return h; | ||
755 | } | ||
756 | }, | ||
757 | Definition::SelfType(_) => HighlightTag::SelfType, | ||
758 | Definition::TypeParam(_) => HighlightTag::TypeParam, | ||
759 | Definition::Local(local) => { | ||
760 | let tag = | ||
761 | if local.is_param(db) { HighlightTag::ValueParam } else { HighlightTag::Local }; | ||
762 | let mut h = Highlight::new(tag); | ||
763 | if local.is_mut(db) || local.ty(db).is_mutable_reference() { | ||
764 | h |= HighlightModifier::Mutable; | ||
765 | } | ||
766 | return h; | ||
767 | } | ||
768 | } | ||
769 | .into() | ||
770 | } | ||
771 | |||
772 | fn highlight_name_by_syntax(name: ast::Name) -> Highlight { | ||
773 | let default = HighlightTag::UnresolvedReference; | ||
774 | |||
775 | let parent = match name.syntax().parent() { | ||
776 | Some(it) => it, | ||
777 | _ => return default.into(), | ||
778 | }; | ||
779 | |||
780 | let tag = match parent.kind() { | ||
781 | STRUCT => HighlightTag::Struct, | ||
782 | ENUM => HighlightTag::Enum, | ||
783 | UNION => HighlightTag::Union, | ||
784 | TRAIT => HighlightTag::Trait, | ||
785 | TYPE_ALIAS => HighlightTag::TypeAlias, | ||
786 | TYPE_PARAM => HighlightTag::TypeParam, | ||
787 | RECORD_FIELD => HighlightTag::Field, | ||
788 | MODULE => HighlightTag::Module, | ||
789 | FN => HighlightTag::Function, | ||
790 | CONST => HighlightTag::Constant, | ||
791 | STATIC => HighlightTag::Static, | ||
792 | VARIANT => HighlightTag::EnumVariant, | ||
793 | IDENT_PAT => HighlightTag::Local, | ||
794 | _ => default, | ||
795 | }; | ||
796 | |||
797 | tag.into() | ||
798 | } | ||
799 | |||
800 | fn highlight_name_ref_by_syntax(name: ast::NameRef, sema: &Semantics<RootDatabase>) -> Highlight { | ||
801 | let default = HighlightTag::UnresolvedReference; | ||
802 | |||
803 | let parent = match name.syntax().parent() { | ||
804 | Some(it) => it, | ||
805 | _ => return default.into(), | ||
806 | }; | ||
807 | |||
808 | match parent.kind() { | ||
809 | METHOD_CALL_EXPR => { | ||
810 | let mut h = Highlight::new(HighlightTag::Function); | ||
811 | let is_unsafe = ast::MethodCallExpr::cast(parent) | ||
812 | .map(|method_call_expr| sema.is_unsafe_method_call(method_call_expr)) | ||
813 | .unwrap_or(false); | ||
814 | if is_unsafe { | ||
815 | h |= HighlightModifier::Unsafe; | ||
816 | } | ||
817 | |||
818 | h | ||
819 | } | ||
820 | FIELD_EXPR => { | ||
821 | let h = HighlightTag::Field; | ||
822 | let is_union = ast::FieldExpr::cast(parent) | ||
823 | .and_then(|field_expr| { | ||
824 | let field = sema.resolve_field(&field_expr)?; | ||
825 | Some(if let VariantDef::Union(_) = field.parent_def(sema.db) { | ||
826 | true | ||
827 | } else { | ||
828 | false | ||
829 | }) | ||
830 | }) | ||
831 | .unwrap_or(false); | ||
832 | if is_union { | ||
833 | h | HighlightModifier::Unsafe | ||
834 | } else { | ||
835 | h.into() | ||
836 | } | ||
837 | } | ||
838 | PATH_SEGMENT => { | ||
839 | let path = match parent.parent().and_then(ast::Path::cast) { | ||
840 | Some(it) => it, | ||
841 | _ => return default.into(), | ||
842 | }; | ||
843 | let expr = match path.syntax().parent().and_then(ast::PathExpr::cast) { | ||
844 | Some(it) => it, | ||
845 | _ => { | ||
846 | // within path, decide whether it is module or adt by checking for uppercase name | ||
847 | return if name.text().chars().next().unwrap_or_default().is_uppercase() { | ||
848 | HighlightTag::Struct | ||
849 | } else { | ||
850 | HighlightTag::Module | ||
851 | } | ||
852 | .into(); | ||
853 | } | ||
854 | }; | ||
855 | let parent = match expr.syntax().parent() { | ||
856 | Some(it) => it, | ||
857 | None => return default.into(), | ||
858 | }; | ||
859 | |||
860 | match parent.kind() { | ||
861 | CALL_EXPR => HighlightTag::Function.into(), | ||
862 | _ => if name.text().chars().next().unwrap_or_default().is_uppercase() { | ||
863 | HighlightTag::Struct.into() | ||
864 | } else { | ||
865 | HighlightTag::Constant | ||
866 | } | ||
867 | .into(), | ||
868 | } | ||
869 | } | ||
870 | _ => default.into(), | ||
871 | } | ||
872 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/html.rs b/crates/ide/src/syntax_highlighting/html.rs new file mode 100644 index 000000000..249368ff8 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/html.rs | |||
@@ -0,0 +1,97 @@ | |||
1 | //! Renders a bit of code as HTML. | ||
2 | |||
3 | use base_db::SourceDatabase; | ||
4 | use oorandom::Rand32; | ||
5 | use syntax::{AstNode, TextRange, TextSize}; | ||
6 | |||
7 | use crate::{syntax_highlighting::highlight, FileId, RootDatabase}; | ||
8 | |||
9 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { | ||
10 | let parse = db.parse(file_id); | ||
11 | |||
12 | fn rainbowify(seed: u64) -> String { | ||
13 | let mut rng = Rand32::new(seed); | ||
14 | format!( | ||
15 | "hsl({h},{s}%,{l}%)", | ||
16 | h = rng.rand_range(0..361), | ||
17 | s = rng.rand_range(42..99), | ||
18 | l = rng.rand_range(40..91), | ||
19 | ) | ||
20 | } | ||
21 | |||
22 | let ranges = highlight(db, file_id, None, false); | ||
23 | let text = parse.tree().syntax().to_string(); | ||
24 | let mut prev_pos = TextSize::from(0); | ||
25 | let mut buf = String::new(); | ||
26 | buf.push_str(&STYLE); | ||
27 | buf.push_str("<pre><code>"); | ||
28 | for range in &ranges { | ||
29 | if range.range.start() > prev_pos { | ||
30 | let curr = &text[TextRange::new(prev_pos, range.range.start())]; | ||
31 | let text = html_escape(curr); | ||
32 | buf.push_str(&text); | ||
33 | } | ||
34 | let curr = &text[TextRange::new(range.range.start(), range.range.end())]; | ||
35 | |||
36 | let class = range.highlight.to_string().replace('.', " "); | ||
37 | let color = match (rainbow, range.binding_hash) { | ||
38 | (true, Some(hash)) => { | ||
39 | format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash)) | ||
40 | } | ||
41 | _ => "".into(), | ||
42 | }; | ||
43 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", class, color, html_escape(curr))); | ||
44 | |||
45 | prev_pos = range.range.end(); | ||
46 | } | ||
47 | // Add the remaining (non-highlighted) text | ||
48 | let curr = &text[TextRange::new(prev_pos, TextSize::of(&text))]; | ||
49 | let text = html_escape(curr); | ||
50 | buf.push_str(&text); | ||
51 | buf.push_str("</code></pre>"); | ||
52 | buf | ||
53 | } | ||
54 | |||
55 | //FIXME: like, real html escaping | ||
56 | fn html_escape(text: &str) -> String { | ||
57 | text.replace("<", "<").replace(">", ">") | ||
58 | } | ||
59 | |||
60 | const STYLE: &str = " | ||
61 | <style> | ||
62 | body { margin: 0; } | ||
63 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
64 | |||
65 | .lifetime { color: #DFAF8F; font-style: italic; } | ||
66 | .comment { color: #7F9F7F; } | ||
67 | .documentation { color: #629755; } | ||
68 | .injected { opacity: 0.65 ; } | ||
69 | .struct, .enum { color: #7CB8BB; } | ||
70 | .enum_variant { color: #BDE0F3; } | ||
71 | .string_literal { color: #CC9393; } | ||
72 | .field { color: #94BFF3; } | ||
73 | .function { color: #93E0E3; } | ||
74 | .function.unsafe { color: #BC8383; } | ||
75 | .operator.unsafe { color: #BC8383; } | ||
76 | .parameter { color: #94BFF3; } | ||
77 | .text { color: #DCDCCC; } | ||
78 | .type { color: #7CB8BB; } | ||
79 | .builtin_type { color: #8CD0D3; } | ||
80 | .type_param { color: #DFAF8F; } | ||
81 | .attribute { color: #94BFF3; } | ||
82 | .numeric_literal { color: #BFEBBF; } | ||
83 | .bool_literal { color: #BFE6EB; } | ||
84 | .macro { color: #94BFF3; } | ||
85 | .module { color: #AFD8AF; } | ||
86 | .value_param { color: #DCDCCC; } | ||
87 | .variable { color: #DCDCCC; } | ||
88 | .format_specifier { color: #CC696B; } | ||
89 | .mutable { text-decoration: underline; } | ||
90 | .escape_sequence { color: #94BFF3; } | ||
91 | .keyword { color: #F0DFAF; font-weight: bold; } | ||
92 | .keyword.unsafe { color: #BC8383; font-weight: bold; } | ||
93 | .control { font-style: italic; } | ||
94 | |||
95 | .unresolved_reference { color: #FC5555; text-decoration: wavy underline; } | ||
96 | </style> | ||
97 | "; | ||
diff --git a/crates/ide/src/syntax_highlighting/injection.rs b/crates/ide/src/syntax_highlighting/injection.rs new file mode 100644 index 000000000..43f4e6fea --- /dev/null +++ b/crates/ide/src/syntax_highlighting/injection.rs | |||
@@ -0,0 +1,187 @@ | |||
1 | //! Syntax highlighting injections such as highlighting of documentation tests. | ||
2 | |||
3 | use std::{collections::BTreeMap, convert::TryFrom}; | ||
4 | |||
5 | use ast::{HasQuotes, HasStringValue}; | ||
6 | use hir::Semantics; | ||
7 | use itertools::Itertools; | ||
8 | use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; | ||
9 | |||
10 | use crate::{ | ||
11 | call_info::ActiveParameter, Analysis, Highlight, HighlightModifier, HighlightTag, | ||
12 | HighlightedRange, RootDatabase, | ||
13 | }; | ||
14 | |||
15 | use super::HighlightedRangeStack; | ||
16 | |||
17 | pub(super) fn highlight_injection( | ||
18 | acc: &mut HighlightedRangeStack, | ||
19 | sema: &Semantics<RootDatabase>, | ||
20 | literal: ast::RawString, | ||
21 | expanded: SyntaxToken, | ||
22 | ) -> Option<()> { | ||
23 | let active_parameter = ActiveParameter::at_token(&sema, expanded)?; | ||
24 | if !active_parameter.name.starts_with("ra_fixture") { | ||
25 | return None; | ||
26 | } | ||
27 | let value = literal.value()?; | ||
28 | let (analysis, tmp_file_id) = Analysis::from_single_file(value.into_owned()); | ||
29 | |||
30 | if let Some(range) = literal.open_quote_text_range() { | ||
31 | acc.add(HighlightedRange { | ||
32 | range, | ||
33 | highlight: HighlightTag::StringLiteral.into(), | ||
34 | binding_hash: None, | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
39 | if let Some(r) = literal.map_range_up(h.range) { | ||
40 | h.range = r; | ||
41 | acc.add(h) | ||
42 | } | ||
43 | } | ||
44 | |||
45 | if let Some(range) = literal.close_quote_text_range() { | ||
46 | acc.add(HighlightedRange { | ||
47 | range, | ||
48 | highlight: HighlightTag::StringLiteral.into(), | ||
49 | binding_hash: None, | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | Some(()) | ||
54 | } | ||
55 | |||
56 | /// Mapping from extracted documentation code to original code | ||
57 | type RangesMap = BTreeMap<TextSize, TextSize>; | ||
58 | |||
59 | const RUSTDOC_FENCE: &'static str = "```"; | ||
60 | const RUSTDOC_FENCE_TOKENS: &[&'static str] = | ||
61 | &["", "rust", "should_panic", "ignore", "no_run", "compile_fail", "edition2015", "edition2018"]; | ||
62 | |||
63 | /// Extracts Rust code from documentation comments as well as a mapping from | ||
64 | /// the extracted source code back to the original source ranges. | ||
65 | /// Lastly, a vector of new comment highlight ranges (spanning only the | ||
66 | /// comment prefix) is returned which is used in the syntax highlighting | ||
67 | /// injection to replace the previous (line-spanning) comment ranges. | ||
68 | pub(super) fn extract_doc_comments( | ||
69 | node: &SyntaxNode, | ||
70 | ) -> Option<(String, RangesMap, Vec<HighlightedRange>)> { | ||
71 | // wrap the doctest into function body to get correct syntax highlighting | ||
72 | let prefix = "fn doctest() {\n"; | ||
73 | let suffix = "}\n"; | ||
74 | // Mapping from extracted documentation code to original code | ||
75 | let mut range_mapping: RangesMap = BTreeMap::new(); | ||
76 | let mut line_start = TextSize::try_from(prefix.len()).unwrap(); | ||
77 | let mut is_codeblock = false; | ||
78 | let mut is_doctest = false; | ||
79 | // Replace the original, line-spanning comment ranges by new, only comment-prefix | ||
80 | // spanning comment ranges. | ||
81 | let mut new_comments = Vec::new(); | ||
82 | let doctest = node | ||
83 | .children_with_tokens() | ||
84 | .filter_map(|el| el.into_token().and_then(ast::Comment::cast)) | ||
85 | .filter(|comment| comment.kind().doc.is_some()) | ||
86 | .filter(|comment| { | ||
87 | if let Some(idx) = comment.text().find(RUSTDOC_FENCE) { | ||
88 | is_codeblock = !is_codeblock; | ||
89 | // Check whether code is rust by inspecting fence guards | ||
90 | let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..]; | ||
91 | let is_rust = | ||
92 | guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim())); | ||
93 | is_doctest = is_codeblock && is_rust; | ||
94 | false | ||
95 | } else { | ||
96 | is_doctest | ||
97 | } | ||
98 | }) | ||
99 | .map(|comment| { | ||
100 | let prefix_len = comment.prefix().len(); | ||
101 | let line: &str = comment.text().as_str(); | ||
102 | let range = comment.syntax().text_range(); | ||
103 | |||
104 | // whitespace after comment is ignored | ||
105 | let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) { | ||
106 | prefix_len + ws.len_utf8() | ||
107 | } else { | ||
108 | prefix_len | ||
109 | }; | ||
110 | |||
111 | // lines marked with `#` should be ignored in output, we skip the `#` char | ||
112 | let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') { | ||
113 | pos + ws.len_utf8() | ||
114 | } else { | ||
115 | pos | ||
116 | }; | ||
117 | |||
118 | range_mapping.insert(line_start, range.start() + TextSize::try_from(pos).unwrap()); | ||
119 | new_comments.push(HighlightedRange { | ||
120 | range: TextRange::new( | ||
121 | range.start(), | ||
122 | range.start() + TextSize::try_from(pos).unwrap(), | ||
123 | ), | ||
124 | highlight: HighlightTag::Comment | HighlightModifier::Documentation, | ||
125 | binding_hash: None, | ||
126 | }); | ||
127 | line_start += range.len() - TextSize::try_from(pos).unwrap(); | ||
128 | line_start += TextSize::try_from('\n'.len_utf8()).unwrap(); | ||
129 | |||
130 | line[pos..].to_owned() | ||
131 | }) | ||
132 | .join("\n"); | ||
133 | |||
134 | if doctest.is_empty() { | ||
135 | return None; | ||
136 | } | ||
137 | |||
138 | let doctest = format!("{}{}{}", prefix, doctest, suffix); | ||
139 | Some((doctest, range_mapping, new_comments)) | ||
140 | } | ||
141 | |||
142 | /// Injection of syntax highlighting of doctests. | ||
143 | pub(super) fn highlight_doc_comment( | ||
144 | text: String, | ||
145 | range_mapping: RangesMap, | ||
146 | new_comments: Vec<HighlightedRange>, | ||
147 | stack: &mut HighlightedRangeStack, | ||
148 | ) { | ||
149 | let (analysis, tmp_file_id) = Analysis::from_single_file(text); | ||
150 | |||
151 | stack.push(); | ||
152 | for mut h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() { | ||
153 | // Determine start offset and end offset in case of multi-line ranges | ||
154 | let mut start_offset = None; | ||
155 | let mut end_offset = None; | ||
156 | for (line_start, orig_line_start) in range_mapping.range(..h.range.end()).rev() { | ||
157 | // It's possible for orig_line_start - line_start to be negative. Add h.range.start() | ||
158 | // here and remove it from the end range after the loop below so that the values are | ||
159 | // always non-negative. | ||
160 | let offset = h.range.start() + orig_line_start - line_start; | ||
161 | if line_start <= &h.range.start() { | ||
162 | start_offset.get_or_insert(offset); | ||
163 | break; | ||
164 | } else { | ||
165 | end_offset.get_or_insert(offset); | ||
166 | } | ||
167 | } | ||
168 | if let Some(start_offset) = start_offset { | ||
169 | h.range = TextRange::new( | ||
170 | start_offset, | ||
171 | h.range.end() + end_offset.unwrap_or(start_offset) - h.range.start(), | ||
172 | ); | ||
173 | |||
174 | h.highlight |= HighlightModifier::Injected; | ||
175 | stack.add(h); | ||
176 | } | ||
177 | } | ||
178 | |||
179 | // Inject the comment prefix highlight ranges | ||
180 | stack.push(); | ||
181 | for comment in new_comments { | ||
182 | stack.add(comment); | ||
183 | } | ||
184 | stack.pop_and_inject(None); | ||
185 | stack | ||
186 | .pop_and_inject(Some(Highlight::from(HighlightTag::Generic) | HighlightModifier::Injected)); | ||
187 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs new file mode 100644 index 000000000..49ec94bdc --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tags.rs | |||
@@ -0,0 +1,203 @@ | |||
1 | //! Defines token tags we use for syntax highlighting. | ||
2 | //! A tag is not unlike a CSS class. | ||
3 | |||
4 | use std::{fmt, ops}; | ||
5 | |||
6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
7 | pub struct Highlight { | ||
8 | pub tag: HighlightTag, | ||
9 | pub modifiers: HighlightModifiers, | ||
10 | } | ||
11 | |||
12 | #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
13 | pub struct HighlightModifiers(u32); | ||
14 | |||
15 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
16 | pub enum HighlightTag { | ||
17 | Attribute, | ||
18 | BoolLiteral, | ||
19 | BuiltinType, | ||
20 | ByteLiteral, | ||
21 | CharLiteral, | ||
22 | Comment, | ||
23 | Constant, | ||
24 | Enum, | ||
25 | EnumVariant, | ||
26 | EscapeSequence, | ||
27 | Field, | ||
28 | Function, | ||
29 | Generic, | ||
30 | Keyword, | ||
31 | Lifetime, | ||
32 | Macro, | ||
33 | Module, | ||
34 | NumericLiteral, | ||
35 | Punctuation, | ||
36 | SelfKeyword, | ||
37 | SelfType, | ||
38 | Static, | ||
39 | StringLiteral, | ||
40 | Struct, | ||
41 | Trait, | ||
42 | TypeAlias, | ||
43 | TypeParam, | ||
44 | Union, | ||
45 | ValueParam, | ||
46 | Local, | ||
47 | UnresolvedReference, | ||
48 | FormatSpecifier, | ||
49 | Operator, | ||
50 | } | ||
51 | |||
52 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
53 | #[repr(u8)] | ||
54 | pub enum HighlightModifier { | ||
55 | /// Used to differentiate individual elements within attributes. | ||
56 | Attribute = 0, | ||
57 | /// Used with keywords like `if` and `break`. | ||
58 | ControlFlow, | ||
59 | /// `foo` in `fn foo(x: i32)` is a definition, `foo` in `foo(90 + 2)` is | ||
60 | /// not. | ||
61 | Definition, | ||
62 | Documentation, | ||
63 | Injected, | ||
64 | Mutable, | ||
65 | Unsafe, | ||
66 | } | ||
67 | |||
68 | impl HighlightTag { | ||
69 | fn as_str(self) -> &'static str { | ||
70 | match self { | ||
71 | HighlightTag::Attribute => "attribute", | ||
72 | HighlightTag::BoolLiteral => "bool_literal", | ||
73 | HighlightTag::BuiltinType => "builtin_type", | ||
74 | HighlightTag::ByteLiteral => "byte_literal", | ||
75 | HighlightTag::CharLiteral => "char_literal", | ||
76 | HighlightTag::Comment => "comment", | ||
77 | HighlightTag::Constant => "constant", | ||
78 | HighlightTag::Enum => "enum", | ||
79 | HighlightTag::EnumVariant => "enum_variant", | ||
80 | HighlightTag::EscapeSequence => "escape_sequence", | ||
81 | HighlightTag::Field => "field", | ||
82 | HighlightTag::FormatSpecifier => "format_specifier", | ||
83 | HighlightTag::Function => "function", | ||
84 | HighlightTag::Generic => "generic", | ||
85 | HighlightTag::Keyword => "keyword", | ||
86 | HighlightTag::Lifetime => "lifetime", | ||
87 | HighlightTag::Punctuation => "punctuation", | ||
88 | HighlightTag::Macro => "macro", | ||
89 | HighlightTag::Module => "module", | ||
90 | HighlightTag::NumericLiteral => "numeric_literal", | ||
91 | HighlightTag::Operator => "operator", | ||
92 | HighlightTag::SelfKeyword => "self_keyword", | ||
93 | HighlightTag::SelfType => "self_type", | ||
94 | HighlightTag::Static => "static", | ||
95 | HighlightTag::StringLiteral => "string_literal", | ||
96 | HighlightTag::Struct => "struct", | ||
97 | HighlightTag::Trait => "trait", | ||
98 | HighlightTag::TypeAlias => "type_alias", | ||
99 | HighlightTag::TypeParam => "type_param", | ||
100 | HighlightTag::Union => "union", | ||
101 | HighlightTag::ValueParam => "value_param", | ||
102 | HighlightTag::Local => "variable", | ||
103 | HighlightTag::UnresolvedReference => "unresolved_reference", | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | |||
108 | impl fmt::Display for HighlightTag { | ||
109 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
110 | fmt::Display::fmt(self.as_str(), f) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | impl HighlightModifier { | ||
115 | const ALL: &'static [HighlightModifier] = &[ | ||
116 | HighlightModifier::Attribute, | ||
117 | HighlightModifier::ControlFlow, | ||
118 | HighlightModifier::Definition, | ||
119 | HighlightModifier::Documentation, | ||
120 | HighlightModifier::Injected, | ||
121 | HighlightModifier::Mutable, | ||
122 | HighlightModifier::Unsafe, | ||
123 | ]; | ||
124 | |||
125 | fn as_str(self) -> &'static str { | ||
126 | match self { | ||
127 | HighlightModifier::Attribute => "attribute", | ||
128 | HighlightModifier::ControlFlow => "control", | ||
129 | HighlightModifier::Definition => "declaration", | ||
130 | HighlightModifier::Documentation => "documentation", | ||
131 | HighlightModifier::Injected => "injected", | ||
132 | HighlightModifier::Mutable => "mutable", | ||
133 | HighlightModifier::Unsafe => "unsafe", | ||
134 | } | ||
135 | } | ||
136 | |||
137 | fn mask(self) -> u32 { | ||
138 | 1 << (self as u32) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | impl fmt::Display for HighlightModifier { | ||
143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
144 | fmt::Display::fmt(self.as_str(), f) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | impl fmt::Display for Highlight { | ||
149 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
150 | write!(f, "{}", self.tag)?; | ||
151 | for modifier in self.modifiers.iter() { | ||
152 | write!(f, ".{}", modifier)? | ||
153 | } | ||
154 | Ok(()) | ||
155 | } | ||
156 | } | ||
157 | |||
158 | impl From<HighlightTag> for Highlight { | ||
159 | fn from(tag: HighlightTag) -> Highlight { | ||
160 | Highlight::new(tag) | ||
161 | } | ||
162 | } | ||
163 | |||
164 | impl Highlight { | ||
165 | pub(crate) fn new(tag: HighlightTag) -> Highlight { | ||
166 | Highlight { tag, modifiers: HighlightModifiers::default() } | ||
167 | } | ||
168 | } | ||
169 | |||
170 | impl ops::BitOr<HighlightModifier> for HighlightTag { | ||
171 | type Output = Highlight; | ||
172 | |||
173 | fn bitor(self, rhs: HighlightModifier) -> Highlight { | ||
174 | Highlight::new(self) | rhs | ||
175 | } | ||
176 | } | ||
177 | |||
178 | impl ops::BitOrAssign<HighlightModifier> for HighlightModifiers { | ||
179 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
180 | self.0 |= rhs.mask(); | ||
181 | } | ||
182 | } | ||
183 | |||
184 | impl ops::BitOrAssign<HighlightModifier> for Highlight { | ||
185 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
186 | self.modifiers |= rhs; | ||
187 | } | ||
188 | } | ||
189 | |||
190 | impl ops::BitOr<HighlightModifier> for Highlight { | ||
191 | type Output = Highlight; | ||
192 | |||
193 | fn bitor(mut self, rhs: HighlightModifier) -> Highlight { | ||
194 | self |= rhs; | ||
195 | self | ||
196 | } | ||
197 | } | ||
198 | |||
199 | impl HighlightModifiers { | ||
200 | pub fn iter(self) -> impl Iterator<Item = HighlightModifier> { | ||
201 | HighlightModifier::ALL.iter().copied().filter(move |it| self.0 & it.mask() == it.mask()) | ||
202 | } | ||
203 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs new file mode 100644 index 000000000..94f37d773 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -0,0 +1,445 @@ | |||
1 | use std::fs; | ||
2 | |||
3 | use expect::{expect_file, ExpectFile}; | ||
4 | use test_utils::project_dir; | ||
5 | |||
6 | use crate::{mock_analysis::single_file, FileRange, TextRange}; | ||
7 | |||
8 | #[test] | ||
9 | fn test_highlighting() { | ||
10 | check_highlighting( | ||
11 | r#" | ||
12 | use inner::{self as inner_mod}; | ||
13 | mod inner {} | ||
14 | |||
15 | #[derive(Clone, Debug)] | ||
16 | struct Foo { | ||
17 | pub x: i32, | ||
18 | pub y: i32, | ||
19 | } | ||
20 | |||
21 | trait Bar { | ||
22 | fn bar(&self) -> i32; | ||
23 | } | ||
24 | |||
25 | impl Bar for Foo { | ||
26 | fn bar(&self) -> i32 { | ||
27 | self.x | ||
28 | } | ||
29 | } | ||
30 | |||
31 | impl Foo { | ||
32 | fn baz(mut self) -> i32 { | ||
33 | self.x | ||
34 | } | ||
35 | |||
36 | fn qux(&mut self) { | ||
37 | self.x = 0; | ||
38 | } | ||
39 | } | ||
40 | |||
41 | static mut STATIC_MUT: i32 = 0; | ||
42 | |||
43 | fn foo<'a, T>() -> T { | ||
44 | foo::<'a, i32>() | ||
45 | } | ||
46 | |||
47 | macro_rules! def_fn { | ||
48 | ($($tt:tt)*) => {$($tt)*} | ||
49 | } | ||
50 | |||
51 | def_fn! { | ||
52 | fn bar() -> u32 { | ||
53 | 100 | ||
54 | } | ||
55 | } | ||
56 | |||
57 | macro_rules! noop { | ||
58 | ($expr:expr) => { | ||
59 | $expr | ||
60 | } | ||
61 | } | ||
62 | |||
63 | // comment | ||
64 | fn main() { | ||
65 | println!("Hello, {}!", 92); | ||
66 | |||
67 | let mut vec = Vec::new(); | ||
68 | if true { | ||
69 | let x = 92; | ||
70 | vec.push(Foo { x, y: 1 }); | ||
71 | } | ||
72 | unsafe { | ||
73 | vec.set_len(0); | ||
74 | STATIC_MUT = 1; | ||
75 | } | ||
76 | |||
77 | for e in vec { | ||
78 | // Do nothing | ||
79 | } | ||
80 | |||
81 | noop!(noop!(1)); | ||
82 | |||
83 | let mut x = 42; | ||
84 | let y = &mut x; | ||
85 | let z = &y; | ||
86 | |||
87 | let Foo { x: z, y } = Foo { x: z, y }; | ||
88 | |||
89 | y; | ||
90 | } | ||
91 | |||
92 | enum Option<T> { | ||
93 | Some(T), | ||
94 | None, | ||
95 | } | ||
96 | use Option::*; | ||
97 | |||
98 | impl<T> Option<T> { | ||
99 | fn and<U>(self, other: Option<U>) -> Option<(T, U)> { | ||
100 | match other { | ||
101 | None => unimplemented!(), | ||
102 | Nope => Nope, | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | "# | ||
107 | .trim(), | ||
108 | expect_file!["crates/ide/test_data/highlighting.html"], | ||
109 | false, | ||
110 | ); | ||
111 | } | ||
112 | |||
113 | #[test] | ||
114 | fn test_rainbow_highlighting() { | ||
115 | check_highlighting( | ||
116 | r#" | ||
117 | fn main() { | ||
118 | let hello = "hello"; | ||
119 | let x = hello.to_string(); | ||
120 | let y = hello.to_string(); | ||
121 | |||
122 | let x = "other color please!"; | ||
123 | let y = x.to_string(); | ||
124 | } | ||
125 | |||
126 | fn bar() { | ||
127 | let mut hello = "hello"; | ||
128 | } | ||
129 | "# | ||
130 | .trim(), | ||
131 | expect_file!["crates/ide/test_data/rainbow_highlighting.html"], | ||
132 | true, | ||
133 | ); | ||
134 | } | ||
135 | |||
136 | #[test] | ||
137 | fn accidentally_quadratic() { | ||
138 | let file = project_dir().join("crates/syntax/test_data/accidentally_quadratic"); | ||
139 | let src = fs::read_to_string(file).unwrap(); | ||
140 | |||
141 | let (analysis, file_id) = single_file(&src); | ||
142 | |||
143 | // let t = std::time::Instant::now(); | ||
144 | let _ = analysis.highlight(file_id).unwrap(); | ||
145 | // eprintln!("elapsed: {:?}", t.elapsed()); | ||
146 | } | ||
147 | |||
148 | #[test] | ||
149 | fn test_ranges() { | ||
150 | let (analysis, file_id) = single_file( | ||
151 | r#" | ||
152 | #[derive(Clone, Debug)] | ||
153 | struct Foo { | ||
154 | pub x: i32, | ||
155 | pub y: i32, | ||
156 | } | ||
157 | "#, | ||
158 | ); | ||
159 | |||
160 | // The "x" | ||
161 | let highlights = &analysis | ||
162 | .highlight_range(FileRange { file_id, range: TextRange::at(45.into(), 1.into()) }) | ||
163 | .unwrap(); | ||
164 | |||
165 | assert_eq!(&highlights[0].highlight.to_string(), "field.declaration"); | ||
166 | } | ||
167 | |||
168 | #[test] | ||
169 | fn test_flattening() { | ||
170 | check_highlighting( | ||
171 | r##" | ||
172 | fn fixture(ra_fixture: &str) {} | ||
173 | |||
174 | fn main() { | ||
175 | fixture(r#" | ||
176 | trait Foo { | ||
177 | fn foo() { | ||
178 | println!("2 + 2 = {}", 4); | ||
179 | } | ||
180 | }"# | ||
181 | ); | ||
182 | }"## | ||
183 | .trim(), | ||
184 | expect_file!["crates/ide/test_data/highlight_injection.html"], | ||
185 | false, | ||
186 | ); | ||
187 | } | ||
188 | |||
189 | #[test] | ||
190 | fn ranges_sorted() { | ||
191 | let (analysis, file_id) = single_file( | ||
192 | r#" | ||
193 | #[foo(bar = "bar")] | ||
194 | macro_rules! test {} | ||
195 | }"# | ||
196 | .trim(), | ||
197 | ); | ||
198 | let _ = analysis.highlight(file_id).unwrap(); | ||
199 | } | ||
200 | |||
201 | #[test] | ||
202 | fn test_string_highlighting() { | ||
203 | // The format string detection is based on macro-expansion, | ||
204 | // thus, we have to copy the macro definition from `std` | ||
205 | check_highlighting( | ||
206 | r#" | ||
207 | macro_rules! println { | ||
208 | ($($arg:tt)*) => ({ | ||
209 | $crate::io::_print($crate::format_args_nl!($($arg)*)); | ||
210 | }) | ||
211 | } | ||
212 | #[rustc_builtin_macro] | ||
213 | macro_rules! format_args_nl { | ||
214 | ($fmt:expr) => {{ /* compiler built-in */ }}; | ||
215 | ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; | ||
216 | } | ||
217 | |||
218 | fn main() { | ||
219 | // from https://doc.rust-lang.org/std/fmt/index.html | ||
220 | println!("Hello"); // => "Hello" | ||
221 | println!("Hello, {}!", "world"); // => "Hello, world!" | ||
222 | println!("The number is {}", 1); // => "The number is 1" | ||
223 | println!("{:?}", (3, 4)); // => "(3, 4)" | ||
224 | println!("{value}", value=4); // => "4" | ||
225 | println!("{} {}", 1, 2); // => "1 2" | ||
226 | println!("{:04}", 42); // => "0042" with leading zerosV | ||
227 | println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" | ||
228 | println!("{argument}", argument = "test"); // => "test" | ||
229 | println!("{name} {}", 1, name = 2); // => "2 1" | ||
230 | println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" | ||
231 | println!("{{{}}}", 2); // => "{2}" | ||
232 | println!("Hello {:5}!", "x"); | ||
233 | println!("Hello {:1$}!", "x", 5); | ||
234 | println!("Hello {1:0$}!", 5, "x"); | ||
235 | println!("Hello {:width$}!", "x", width = 5); | ||
236 | println!("Hello {:<5}!", "x"); | ||
237 | println!("Hello {:-<5}!", "x"); | ||
238 | println!("Hello {:^5}!", "x"); | ||
239 | println!("Hello {:>5}!", "x"); | ||
240 | println!("Hello {:+}!", 5); | ||
241 | println!("{:#x}!", 27); | ||
242 | println!("Hello {:05}!", 5); | ||
243 | println!("Hello {:05}!", -5); | ||
244 | println!("{:#010x}!", 27); | ||
245 | println!("Hello {0} is {1:.5}", "x", 0.01); | ||
246 | println!("Hello {1} is {2:.0$}", 5, "x", 0.01); | ||
247 | println!("Hello {0} is {2:.1$}", "x", 5, 0.01); | ||
248 | println!("Hello {} is {:.*}", "x", 5, 0.01); | ||
249 | println!("Hello {} is {2:.*}", "x", 5, 0.01); | ||
250 | println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); | ||
251 | println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); | ||
252 | println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); | ||
253 | println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); | ||
254 | println!("Hello {{}}"); | ||
255 | println!("{{ Hello"); | ||
256 | |||
257 | println!(r"Hello, {}!", "world"); | ||
258 | |||
259 | // escape sequences | ||
260 | println!("Hello\nWorld"); | ||
261 | println!("\u{48}\x65\x6C\x6C\x6F World"); | ||
262 | |||
263 | println!("{\x41}", A = 92); | ||
264 | println!("{ничоси}", ничоси = 92); | ||
265 | }"# | ||
266 | .trim(), | ||
267 | expect_file!["crates/ide/test_data/highlight_strings.html"], | ||
268 | false, | ||
269 | ); | ||
270 | } | ||
271 | |||
272 | #[test] | ||
273 | fn test_unsafe_highlighting() { | ||
274 | check_highlighting( | ||
275 | r#" | ||
276 | unsafe fn unsafe_fn() {} | ||
277 | |||
278 | union Union { | ||
279 | a: u32, | ||
280 | b: f32, | ||
281 | } | ||
282 | |||
283 | struct HasUnsafeFn; | ||
284 | |||
285 | impl HasUnsafeFn { | ||
286 | unsafe fn unsafe_method(&self) {} | ||
287 | } | ||
288 | |||
289 | struct TypeForStaticMut { | ||
290 | a: u8 | ||
291 | } | ||
292 | |||
293 | static mut global_mut: TypeForStaticMut = TypeForStaticMut { a: 0 }; | ||
294 | |||
295 | #[repr(packed)] | ||
296 | struct Packed { | ||
297 | a: u16, | ||
298 | } | ||
299 | |||
300 | trait DoTheAutoref { | ||
301 | fn calls_autoref(&self); | ||
302 | } | ||
303 | |||
304 | impl DoTheAutoref for u16 { | ||
305 | fn calls_autoref(&self) {} | ||
306 | } | ||
307 | |||
308 | fn main() { | ||
309 | let x = &5 as *const _ as *const usize; | ||
310 | let u = Union { b: 0 }; | ||
311 | unsafe { | ||
312 | // unsafe fn and method calls | ||
313 | unsafe_fn(); | ||
314 | let b = u.b; | ||
315 | match u { | ||
316 | Union { b: 0 } => (), | ||
317 | Union { a } => (), | ||
318 | } | ||
319 | HasUnsafeFn.unsafe_method(); | ||
320 | |||
321 | // unsafe deref | ||
322 | let y = *x; | ||
323 | |||
324 | // unsafe access to a static mut | ||
325 | let a = global_mut.a; | ||
326 | |||
327 | // unsafe ref of packed fields | ||
328 | let packed = Packed { a: 0 }; | ||
329 | let a = &packed.a; | ||
330 | let ref a = packed.a; | ||
331 | let Packed { ref a } = packed; | ||
332 | let Packed { a: ref _a } = packed; | ||
333 | |||
334 | // unsafe auto ref of packed field | ||
335 | packed.a.calls_autoref(); | ||
336 | } | ||
337 | } | ||
338 | "# | ||
339 | .trim(), | ||
340 | expect_file!["crates/ide/test_data/highlight_unsafe.html"], | ||
341 | false, | ||
342 | ); | ||
343 | } | ||
344 | |||
345 | #[test] | ||
346 | fn test_highlight_doctest() { | ||
347 | check_highlighting( | ||
348 | r#" | ||
349 | /// ``` | ||
350 | /// let _ = "early doctests should not go boom"; | ||
351 | /// ``` | ||
352 | struct Foo { | ||
353 | bar: bool, | ||
354 | } | ||
355 | |||
356 | impl Foo { | ||
357 | pub const bar: bool = true; | ||
358 | |||
359 | /// Constructs a new `Foo`. | ||
360 | /// | ||
361 | /// # Examples | ||
362 | /// | ||
363 | /// ``` | ||
364 | /// # #![allow(unused_mut)] | ||
365 | /// let mut foo: Foo = Foo::new(); | ||
366 | /// ``` | ||
367 | pub const fn new() -> Foo { | ||
368 | Foo { bar: true } | ||
369 | } | ||
370 | |||
371 | /// `bar` method on `Foo`. | ||
372 | /// | ||
373 | /// # Examples | ||
374 | /// | ||
375 | /// ``` | ||
376 | /// use x::y; | ||
377 | /// | ||
378 | /// let foo = Foo::new(); | ||
379 | /// | ||
380 | /// // calls bar on foo | ||
381 | /// assert!(foo.bar()); | ||
382 | /// | ||
383 | /// let bar = foo.bar || Foo::bar; | ||
384 | /// | ||
385 | /// /* multi-line | ||
386 | /// comment */ | ||
387 | /// | ||
388 | /// let multi_line_string = "Foo | ||
389 | /// bar | ||
390 | /// "; | ||
391 | /// | ||
392 | /// ``` | ||
393 | /// | ||
394 | /// ```rust,no_run | ||
395 | /// let foobar = Foo::new().bar(); | ||
396 | /// ``` | ||
397 | /// | ||
398 | /// ```sh | ||
399 | /// echo 1 | ||
400 | /// ``` | ||
401 | pub fn foo(&self) -> bool { | ||
402 | true | ||
403 | } | ||
404 | } | ||
405 | |||
406 | /// ``` | ||
407 | /// noop!(1); | ||
408 | /// ``` | ||
409 | macro_rules! noop { | ||
410 | ($expr:expr) => { | ||
411 | $expr | ||
412 | } | ||
413 | } | ||
414 | "# | ||
415 | .trim(), | ||
416 | expect_file!["crates/ide/test_data/highlight_doctest.html"], | ||
417 | false, | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn test_extern_crate() { | ||
423 | check_highlighting( | ||
424 | r#" | ||
425 | //- /main.rs | ||
426 | extern crate std; | ||
427 | extern crate alloc as abc; | ||
428 | //- /std/lib.rs | ||
429 | pub struct S; | ||
430 | //- /alloc/lib.rs | ||
431 | pub struct A | ||
432 | "#, | ||
433 | expect_file!["crates/ide/test_data/highlight_extern_crate.html"], | ||
434 | false, | ||
435 | ); | ||
436 | } | ||
437 | |||
438 | /// Highlights the code given by the `ra_fixture` argument, renders the | ||
439 | /// result as HTML, and compares it with the HTML file given as `snapshot`. | ||
440 | /// Note that the `snapshot` file is overwritten by the rendered HTML. | ||
441 | fn check_highlighting(ra_fixture: &str, expect: ExpectFile, rainbow: bool) { | ||
442 | let (analysis, file_id) = single_file(ra_fixture); | ||
443 | let actual_html = &analysis.highlight_as_html(file_id, rainbow).unwrap(); | ||
444 | expect.assert_eq(actual_html) | ||
445 | } | ||
diff --git a/crates/ide/src/syntax_tree.rs b/crates/ide/src/syntax_tree.rs new file mode 100644 index 000000000..f80044959 --- /dev/null +++ b/crates/ide/src/syntax_tree.rs | |||
@@ -0,0 +1,359 @@ | |||
1 | use base_db::{FileId, SourceDatabase}; | ||
2 | use ide_db::RootDatabase; | ||
3 | use syntax::{ | ||
4 | algo, AstNode, NodeOrToken, SourceFile, | ||
5 | SyntaxKind::{RAW_STRING, STRING}, | ||
6 | SyntaxToken, TextRange, TextSize, | ||
7 | }; | ||
8 | |||
9 | // Feature: Show Syntax Tree | ||
10 | // | ||
11 | // Shows the parse tree of the current file. It exists mostly for debugging | ||
12 | // rust-analyzer itself. | ||
13 | // | ||
14 | // |=== | ||
15 | // | Editor | Action Name | ||
16 | // | ||
17 | // | VS Code | **Rust Analyzer: Show Syntax Tree** | ||
18 | // |=== | ||
19 | pub(crate) fn syntax_tree( | ||
20 | db: &RootDatabase, | ||
21 | file_id: FileId, | ||
22 | text_range: Option<TextRange>, | ||
23 | ) -> String { | ||
24 | let parse = db.parse(file_id); | ||
25 | if let Some(text_range) = text_range { | ||
26 | let node = match algo::find_covering_element(parse.tree().syntax(), text_range) { | ||
27 | NodeOrToken::Node(node) => node, | ||
28 | NodeOrToken::Token(token) => { | ||
29 | if let Some(tree) = syntax_tree_for_string(&token, text_range) { | ||
30 | return tree; | ||
31 | } | ||
32 | token.parent() | ||
33 | } | ||
34 | }; | ||
35 | |||
36 | format!("{:#?}", node) | ||
37 | } else { | ||
38 | format!("{:#?}", parse.tree().syntax()) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | /// Attempts parsing the selected contents of a string literal | ||
43 | /// as rust syntax and returns its syntax tree | ||
44 | fn syntax_tree_for_string(token: &SyntaxToken, text_range: TextRange) -> Option<String> { | ||
45 | // When the range is inside a string | ||
46 | // we'll attempt parsing it as rust syntax | ||
47 | // to provide the syntax tree of the contents of the string | ||
48 | match token.kind() { | ||
49 | STRING | RAW_STRING => syntax_tree_for_token(token, text_range), | ||
50 | _ => None, | ||
51 | } | ||
52 | } | ||
53 | |||
54 | fn syntax_tree_for_token(node: &SyntaxToken, text_range: TextRange) -> Option<String> { | ||
55 | // Range of the full node | ||
56 | let node_range = node.text_range(); | ||
57 | let text = node.text().to_string(); | ||
58 | |||
59 | // We start at some point inside the node | ||
60 | // Either we have selected the whole string | ||
61 | // or our selection is inside it | ||
62 | let start = text_range.start() - node_range.start(); | ||
63 | |||
64 | // how many characters we have selected | ||
65 | let len = text_range.len(); | ||
66 | |||
67 | let node_len = node_range.len(); | ||
68 | |||
69 | let start = start; | ||
70 | |||
71 | // We want to cap our length | ||
72 | let len = len.min(node_len); | ||
73 | |||
74 | // Ensure our slice is inside the actual string | ||
75 | let end = | ||
76 | if start + len < TextSize::of(&text) { start + len } else { TextSize::of(&text) - start }; | ||
77 | |||
78 | let text = &text[TextRange::new(start, end)]; | ||
79 | |||
80 | // Remove possible extra string quotes from the start | ||
81 | // and the end of the string | ||
82 | let text = text | ||
83 | .trim_start_matches('r') | ||
84 | .trim_start_matches('#') | ||
85 | .trim_start_matches('"') | ||
86 | .trim_end_matches('#') | ||
87 | .trim_end_matches('"') | ||
88 | .trim() | ||
89 | // Remove custom markers | ||
90 | .replace("<|>", ""); | ||
91 | |||
92 | let parsed = SourceFile::parse(&text); | ||
93 | |||
94 | // If the "file" parsed without errors, | ||
95 | // return its syntax | ||
96 | if parsed.errors().is_empty() { | ||
97 | return Some(format!("{:#?}", parsed.tree().syntax())); | ||
98 | } | ||
99 | |||
100 | None | ||
101 | } | ||
102 | |||
103 | #[cfg(test)] | ||
104 | mod tests { | ||
105 | use test_utils::assert_eq_text; | ||
106 | |||
107 | use crate::mock_analysis::{analysis_and_range, single_file}; | ||
108 | |||
109 | #[test] | ||
110 | fn test_syntax_tree_without_range() { | ||
111 | // Basic syntax | ||
112 | let (analysis, file_id) = single_file(r#"fn foo() {}"#); | ||
113 | let syn = analysis.syntax_tree(file_id, None).unwrap(); | ||
114 | |||
115 | assert_eq_text!( | ||
116 | syn.trim(), | ||
117 | r#" | ||
118 | [email protected] | ||
119 | [email protected] | ||
120 | [email protected] "fn" | ||
121 | [email protected] " " | ||
122 | [email protected] | ||
123 | [email protected] "foo" | ||
124 | [email protected] | ||
125 | [email protected] "(" | ||
126 | [email protected] ")" | ||
127 | [email protected] " " | ||
128 | [email protected] | ||
129 | [email protected] "{" | ||
130 | [email protected] "}" | ||
131 | "# | ||
132 | .trim() | ||
133 | ); | ||
134 | |||
135 | let (analysis, file_id) = single_file( | ||
136 | r#" | ||
137 | fn test() { | ||
138 | assert!(" | ||
139 | fn foo() { | ||
140 | } | ||
141 | ", ""); | ||
142 | }"# | ||
143 | .trim(), | ||
144 | ); | ||
145 | let syn = analysis.syntax_tree(file_id, None).unwrap(); | ||
146 | |||
147 | assert_eq_text!( | ||
148 | syn.trim(), | ||
149 | r#" | ||
150 | [email protected] | ||
151 | [email protected] | ||
152 | [email protected] "fn" | ||
153 | [email protected] " " | ||
154 | [email protected] | ||
155 | [email protected] "test" | ||
156 | [email protected] | ||
157 | [email protected] "(" | ||
158 | [email protected] ")" | ||
159 | [email protected] " " | ||
160 | [email protected] | ||
161 | [email protected] "{" | ||
162 | [email protected] "\n " | ||
163 | [email protected] | ||
164 | [email protected] | ||
165 | [email protected] | ||
166 | [email protected] | ||
167 | [email protected] | ||
168 | [email protected] "assert" | ||
169 | [email protected] "!" | ||
170 | [email protected] | ||
171 | [email protected] "(" | ||
172 | [email protected] "\"\n fn foo() {\n ..." | ||
173 | [email protected] "," | ||
174 | [email protected] " " | ||
175 | [email protected] "\"\"" | ||
176 | [email protected] ")" | ||
177 | [email protected] ";" | ||
178 | [email protected] "\n" | ||
179 | [email protected] "}" | ||
180 | "# | ||
181 | .trim() | ||
182 | ); | ||
183 | } | ||
184 | |||
185 | #[test] | ||
186 | fn test_syntax_tree_with_range() { | ||
187 | let (analysis, range) = analysis_and_range(r#"<|>fn foo() {}<|>"#.trim()); | ||
188 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
189 | |||
190 | assert_eq_text!( | ||
191 | syn.trim(), | ||
192 | r#" | ||
193 | [email protected] | ||
194 | [email protected] "fn" | ||
195 | [email protected] " " | ||
196 | [email protected] | ||
197 | [email protected] "foo" | ||
198 | [email protected] | ||
199 | [email protected] "(" | ||
200 | [email protected] ")" | ||
201 | [email protected] " " | ||
202 | [email protected] | ||
203 | [email protected] "{" | ||
204 | [email protected] "}" | ||
205 | "# | ||
206 | .trim() | ||
207 | ); | ||
208 | |||
209 | let (analysis, range) = analysis_and_range( | ||
210 | r#"fn test() { | ||
211 | <|>assert!(" | ||
212 | fn foo() { | ||
213 | } | ||
214 | ", "");<|> | ||
215 | }"# | ||
216 | .trim(), | ||
217 | ); | ||
218 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
219 | |||
220 | assert_eq_text!( | ||
221 | syn.trim(), | ||
222 | r#" | ||
223 | [email protected] | ||
224 | [email protected] | ||
225 | [email protected] | ||
226 | [email protected] | ||
227 | [email protected] | ||
228 | [email protected] "assert" | ||
229 | [email protected] "!" | ||
230 | [email protected] | ||
231 | [email protected] "(" | ||
232 | [email protected] "\"\n fn foo() {\n ..." | ||
233 | [email protected] "," | ||
234 | [email protected] " " | ||
235 | [email protected] "\"\"" | ||
236 | [email protected] ")" | ||
237 | [email protected] ";" | ||
238 | "# | ||
239 | .trim() | ||
240 | ); | ||
241 | } | ||
242 | |||
243 | #[test] | ||
244 | fn test_syntax_tree_inside_string() { | ||
245 | let (analysis, range) = analysis_and_range( | ||
246 | r#"fn test() { | ||
247 | assert!(" | ||
248 | <|>fn foo() { | ||
249 | }<|> | ||
250 | fn bar() { | ||
251 | } | ||
252 | ", ""); | ||
253 | }"# | ||
254 | .trim(), | ||
255 | ); | ||
256 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
257 | assert_eq_text!( | ||
258 | syn.trim(), | ||
259 | r#" | ||
260 | [email protected] | ||
261 | [email protected] | ||
262 | [email protected] "fn" | ||
263 | [email protected] " " | ||
264 | [email protected] | ||
265 | [email protected] "foo" | ||
266 | [email protected] | ||
267 | [email protected] "(" | ||
268 | [email protected] ")" | ||
269 | [email protected] " " | ||
270 | [email protected] | ||
271 | [email protected] "{" | ||
272 | [email protected] "\n" | ||
273 | [email protected] "}" | ||
274 | "# | ||
275 | .trim() | ||
276 | ); | ||
277 | |||
278 | // With a raw string | ||
279 | let (analysis, range) = analysis_and_range( | ||
280 | r###"fn test() { | ||
281 | assert!(r#" | ||
282 | <|>fn foo() { | ||
283 | }<|> | ||
284 | fn bar() { | ||
285 | } | ||
286 | "#, ""); | ||
287 | }"### | ||
288 | .trim(), | ||
289 | ); | ||
290 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
291 | assert_eq_text!( | ||
292 | syn.trim(), | ||
293 | r#" | ||
294 | [email protected] | ||
295 | [email protected] | ||
296 | [email protected] "fn" | ||
297 | [email protected] " " | ||
298 | [email protected] | ||
299 | [email protected] "foo" | ||
300 | [email protected] | ||
301 | [email protected] "(" | ||
302 | [email protected] ")" | ||
303 | [email protected] " " | ||
304 | [email protected] | ||
305 | [email protected] "{" | ||
306 | [email protected] "\n" | ||
307 | [email protected] "}" | ||
308 | "# | ||
309 | .trim() | ||
310 | ); | ||
311 | |||
312 | // With a raw string | ||
313 | let (analysis, range) = analysis_and_range( | ||
314 | r###"fn test() { | ||
315 | assert!(r<|>#" | ||
316 | fn foo() { | ||
317 | } | ||
318 | fn bar() { | ||
319 | }"<|>#, ""); | ||
320 | }"### | ||
321 | .trim(), | ||
322 | ); | ||
323 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
324 | assert_eq_text!( | ||
325 | syn.trim(), | ||
326 | r#" | ||
327 | [email protected] | ||
328 | [email protected] | ||
329 | [email protected] "fn" | ||
330 | [email protected] " " | ||
331 | [email protected] | ||
332 | [email protected] "foo" | ||
333 | [email protected] | ||
334 | [email protected] "(" | ||
335 | [email protected] ")" | ||
336 | [email protected] " " | ||
337 | [email protected] | ||
338 | [email protected] "{" | ||
339 | [email protected] "\n" | ||
340 | [email protected] "}" | ||
341 | [email protected] "\n" | ||
342 | [email protected] | ||
343 | [email protected] "fn" | ||
344 | [email protected] " " | ||
345 | [email protected] | ||
346 | [email protected] "bar" | ||
347 | [email protected] | ||
348 | [email protected] "(" | ||
349 | [email protected] ")" | ||
350 | [email protected] " " | ||
351 | [email protected] | ||
352 | [email protected] "{" | ||
353 | [email protected] "\n" | ||
354 | [email protected] "}" | ||
355 | "# | ||
356 | .trim() | ||
357 | ); | ||
358 | } | ||
359 | } | ||
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs new file mode 100644 index 000000000..899ce5f26 --- /dev/null +++ b/crates/ide/src/typing.rs | |||
@@ -0,0 +1,364 @@ | |||
1 | //! This module handles auto-magic editing actions applied together with users | ||
2 | //! edits. For example, if the user typed | ||
3 | //! | ||
4 | //! ```text | ||
5 | //! foo | ||
6 | //! .bar() | ||
7 | //! .baz() | ||
8 | //! | // <- cursor is here | ||
9 | //! ``` | ||
10 | //! | ||
11 | //! and types `.` next, we want to indent the dot. | ||
12 | //! | ||
13 | //! Language server executes such typing assists synchronously. That is, they | ||
14 | //! block user's typing and should be pretty fast for this reason! | ||
15 | |||
16 | mod on_enter; | ||
17 | |||
18 | use base_db::{FilePosition, SourceDatabase}; | ||
19 | use ide_db::{source_change::SourceFileEdit, RootDatabase}; | ||
20 | use syntax::{ | ||
21 | algo::find_node_at_offset, | ||
22 | ast::{self, edit::IndentLevel, AstToken}, | ||
23 | AstNode, SourceFile, | ||
24 | SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, | ||
25 | TextRange, TextSize, | ||
26 | }; | ||
27 | |||
28 | use text_edit::TextEdit; | ||
29 | |||
30 | use crate::SourceChange; | ||
31 | |||
32 | pub(crate) use on_enter::on_enter; | ||
33 | |||
34 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | ||
35 | |||
36 | // Feature: On Typing Assists | ||
37 | // | ||
38 | // Some features trigger on typing certain characters: | ||
39 | // | ||
40 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression | ||
41 | // - typing `.` in a chain method call auto-indents | ||
42 | pub(crate) fn on_char_typed( | ||
43 | db: &RootDatabase, | ||
44 | position: FilePosition, | ||
45 | char_typed: char, | ||
46 | ) -> Option<SourceChange> { | ||
47 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
48 | let file = &db.parse(position.file_id).tree(); | ||
49 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
50 | let edit = on_char_typed_inner(file, position.offset, char_typed)?; | ||
51 | Some(SourceFileEdit { file_id: position.file_id, edit }.into()) | ||
52 | } | ||
53 | |||
54 | fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { | ||
55 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
56 | match char_typed { | ||
57 | '.' => on_dot_typed(file, offset), | ||
58 | '=' => on_eq_typed(file, offset), | ||
59 | '>' => on_arrow_typed(file, offset), | ||
60 | _ => unreachable!(), | ||
61 | } | ||
62 | } | ||
63 | |||
64 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
65 | /// this works when adding `let =`. | ||
66 | // FIXME: use a snippet completion instead of this hack here. | ||
67 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | ||
68 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
69 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
70 | if let_stmt.semicolon_token().is_some() { | ||
71 | return None; | ||
72 | } | ||
73 | if let Some(expr) = let_stmt.initializer() { | ||
74 | let expr_range = expr.syntax().text_range(); | ||
75 | if expr_range.contains(offset) && offset != expr_range.start() { | ||
76 | return None; | ||
77 | } | ||
78 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { | ||
79 | return None; | ||
80 | } | ||
81 | } else { | ||
82 | return None; | ||
83 | } | ||
84 | let offset = let_stmt.syntax().text_range().end(); | ||
85 | Some(TextEdit::insert(offset, ";".to_string())) | ||
86 | } | ||
87 | |||
88 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | ||
89 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | ||
90 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | ||
91 | let whitespace = | ||
92 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | ||
93 | |||
94 | let current_indent = { | ||
95 | let text = whitespace.text(); | ||
96 | let newline = text.rfind('\n')?; | ||
97 | &text[newline + 1..] | ||
98 | }; | ||
99 | let current_indent_len = TextSize::of(current_indent); | ||
100 | |||
101 | let parent = whitespace.syntax().parent(); | ||
102 | // Make sure dot is a part of call chain | ||
103 | if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) { | ||
104 | return None; | ||
105 | } | ||
106 | let prev_indent = IndentLevel::from_node(&parent); | ||
107 | let target_indent = format!(" {}", prev_indent); | ||
108 | let target_indent_len = TextSize::of(&target_indent); | ||
109 | if current_indent_len == target_indent_len { | ||
110 | return None; | ||
111 | } | ||
112 | |||
113 | Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent)) | ||
114 | } | ||
115 | |||
116 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | ||
117 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { | ||
118 | let file_text = file.syntax().text(); | ||
119 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
120 | let after_arrow = offset + TextSize::of('>'); | ||
121 | if file_text.char_at(after_arrow) != Some('{') { | ||
122 | return None; | ||
123 | } | ||
124 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
125 | return None; | ||
126 | } | ||
127 | |||
128 | Some(TextEdit::insert(after_arrow, " ".to_string())) | ||
129 | } | ||
130 | |||
131 | #[cfg(test)] | ||
132 | mod tests { | ||
133 | use test_utils::{assert_eq_text, extract_offset}; | ||
134 | |||
135 | use super::*; | ||
136 | |||
137 | fn do_type_char(char_typed: char, before: &str) -> Option<String> { | ||
138 | let (offset, before) = extract_offset(before); | ||
139 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
140 | let mut before = before.to_string(); | ||
141 | edit.apply(&mut before); | ||
142 | let parse = SourceFile::parse(&before); | ||
143 | on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { | ||
144 | it.apply(&mut before); | ||
145 | before.to_string() | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) { | ||
150 | let actual = do_type_char(char_typed, ra_fixture_before) | ||
151 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); | ||
152 | |||
153 | assert_eq_text!(ra_fixture_after, &actual); | ||
154 | } | ||
155 | |||
156 | fn type_char_noop(char_typed: char, before: &str) { | ||
157 | let file_change = do_type_char(char_typed, before); | ||
158 | assert!(file_change.is_none()) | ||
159 | } | ||
160 | |||
161 | #[test] | ||
162 | fn test_on_eq_typed() { | ||
163 | // do_check(r" | ||
164 | // fn foo() { | ||
165 | // let foo =<|> | ||
166 | // } | ||
167 | // ", r" | ||
168 | // fn foo() { | ||
169 | // let foo =; | ||
170 | // } | ||
171 | // "); | ||
172 | type_char( | ||
173 | '=', | ||
174 | r" | ||
175 | fn foo() { | ||
176 | let foo <|> 1 + 1 | ||
177 | } | ||
178 | ", | ||
179 | r" | ||
180 | fn foo() { | ||
181 | let foo = 1 + 1; | ||
182 | } | ||
183 | ", | ||
184 | ); | ||
185 | // do_check(r" | ||
186 | // fn foo() { | ||
187 | // let foo =<|> | ||
188 | // let bar = 1; | ||
189 | // } | ||
190 | // ", r" | ||
191 | // fn foo() { | ||
192 | // let foo =; | ||
193 | // let bar = 1; | ||
194 | // } | ||
195 | // "); | ||
196 | } | ||
197 | |||
198 | #[test] | ||
199 | fn indents_new_chain_call() { | ||
200 | type_char( | ||
201 | '.', | ||
202 | r" | ||
203 | fn main() { | ||
204 | xs.foo() | ||
205 | <|> | ||
206 | } | ||
207 | ", | ||
208 | r" | ||
209 | fn main() { | ||
210 | xs.foo() | ||
211 | . | ||
212 | } | ||
213 | ", | ||
214 | ); | ||
215 | type_char_noop( | ||
216 | '.', | ||
217 | r" | ||
218 | fn main() { | ||
219 | xs.foo() | ||
220 | <|> | ||
221 | } | ||
222 | ", | ||
223 | ) | ||
224 | } | ||
225 | |||
226 | #[test] | ||
227 | fn indents_new_chain_call_with_semi() { | ||
228 | type_char( | ||
229 | '.', | ||
230 | r" | ||
231 | fn main() { | ||
232 | xs.foo() | ||
233 | <|>; | ||
234 | } | ||
235 | ", | ||
236 | r" | ||
237 | fn main() { | ||
238 | xs.foo() | ||
239 | .; | ||
240 | } | ||
241 | ", | ||
242 | ); | ||
243 | type_char_noop( | ||
244 | '.', | ||
245 | r" | ||
246 | fn main() { | ||
247 | xs.foo() | ||
248 | <|>; | ||
249 | } | ||
250 | ", | ||
251 | ) | ||
252 | } | ||
253 | |||
254 | #[test] | ||
255 | fn indents_new_chain_call_with_let() { | ||
256 | type_char( | ||
257 | '.', | ||
258 | r#" | ||
259 | fn main() { | ||
260 | let _ = foo | ||
261 | <|> | ||
262 | bar() | ||
263 | } | ||
264 | "#, | ||
265 | r#" | ||
266 | fn main() { | ||
267 | let _ = foo | ||
268 | . | ||
269 | bar() | ||
270 | } | ||
271 | "#, | ||
272 | ); | ||
273 | } | ||
274 | |||
275 | #[test] | ||
276 | fn indents_continued_chain_call() { | ||
277 | type_char( | ||
278 | '.', | ||
279 | r" | ||
280 | fn main() { | ||
281 | xs.foo() | ||
282 | .first() | ||
283 | <|> | ||
284 | } | ||
285 | ", | ||
286 | r" | ||
287 | fn main() { | ||
288 | xs.foo() | ||
289 | .first() | ||
290 | . | ||
291 | } | ||
292 | ", | ||
293 | ); | ||
294 | type_char_noop( | ||
295 | '.', | ||
296 | r" | ||
297 | fn main() { | ||
298 | xs.foo() | ||
299 | .first() | ||
300 | <|> | ||
301 | } | ||
302 | ", | ||
303 | ); | ||
304 | } | ||
305 | |||
306 | #[test] | ||
307 | fn indents_middle_of_chain_call() { | ||
308 | type_char( | ||
309 | '.', | ||
310 | r" | ||
311 | fn source_impl() { | ||
312 | let var = enum_defvariant_list().unwrap() | ||
313 | <|> | ||
314 | .nth(92) | ||
315 | .unwrap(); | ||
316 | } | ||
317 | ", | ||
318 | r" | ||
319 | fn source_impl() { | ||
320 | let var = enum_defvariant_list().unwrap() | ||
321 | . | ||
322 | .nth(92) | ||
323 | .unwrap(); | ||
324 | } | ||
325 | ", | ||
326 | ); | ||
327 | type_char_noop( | ||
328 | '.', | ||
329 | r" | ||
330 | fn source_impl() { | ||
331 | let var = enum_defvariant_list().unwrap() | ||
332 | <|> | ||
333 | .nth(92) | ||
334 | .unwrap(); | ||
335 | } | ||
336 | ", | ||
337 | ); | ||
338 | } | ||
339 | |||
340 | #[test] | ||
341 | fn dont_indent_freestanding_dot() { | ||
342 | type_char_noop( | ||
343 | '.', | ||
344 | r" | ||
345 | fn main() { | ||
346 | <|> | ||
347 | } | ||
348 | ", | ||
349 | ); | ||
350 | type_char_noop( | ||
351 | '.', | ||
352 | r" | ||
353 | fn main() { | ||
354 | <|> | ||
355 | } | ||
356 | ", | ||
357 | ); | ||
358 | } | ||
359 | |||
360 | #[test] | ||
361 | fn adds_space_after_return_type() { | ||
362 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -> { 92 }") | ||
363 | } | ||
364 | } | ||
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs new file mode 100644 index 000000000..f7d46146c --- /dev/null +++ b/crates/ide/src/typing/on_enter.rs | |||
@@ -0,0 +1,256 @@ | |||
1 | //! Handles the `Enter` key press. At the momently, this only continues | ||
2 | //! comments, but should handle indent some time in the future as well. | ||
3 | |||
4 | use base_db::{FilePosition, SourceDatabase}; | ||
5 | use ide_db::RootDatabase; | ||
6 | use syntax::{ | ||
7 | ast::{self, AstToken}, | ||
8 | AstNode, SmolStr, SourceFile, | ||
9 | SyntaxKind::*, | ||
10 | SyntaxToken, TextRange, TextSize, TokenAtOffset, | ||
11 | }; | ||
12 | use test_utils::mark; | ||
13 | use text_edit::TextEdit; | ||
14 | |||
15 | // Feature: On Enter | ||
16 | // | ||
17 | // rust-analyzer can override kbd:[Enter] key to make it smarter: | ||
18 | // | ||
19 | // - kbd:[Enter] inside triple-slash comments automatically inserts `///` | ||
20 | // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` | ||
21 | // | ||
22 | // This action needs to be assigned to shortcut explicitly. | ||
23 | // | ||
24 | // VS Code:: | ||
25 | // | ||
26 | // Add the following to `keybindings.json`: | ||
27 | // [source,json] | ||
28 | // ---- | ||
29 | // { | ||
30 | // "key": "Enter", | ||
31 | // "command": "rust-analyzer.onEnter", | ||
32 | // "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust" | ||
33 | // } | ||
34 | // ---- | ||
35 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { | ||
36 | let parse = db.parse(position.file_id); | ||
37 | let file = parse.tree(); | ||
38 | let comment = file | ||
39 | .syntax() | ||
40 | .token_at_offset(position.offset) | ||
41 | .left_biased() | ||
42 | .and_then(ast::Comment::cast)?; | ||
43 | |||
44 | if comment.kind().shape.is_block() { | ||
45 | return None; | ||
46 | } | ||
47 | |||
48 | let prefix = comment.prefix(); | ||
49 | let comment_range = comment.syntax().text_range(); | ||
50 | if position.offset < comment_range.start() + TextSize::of(prefix) { | ||
51 | return None; | ||
52 | } | ||
53 | |||
54 | let mut remove_last_space = false; | ||
55 | // Continuing single-line non-doc comments (like this one :) ) is annoying | ||
56 | if prefix == "//" && comment_range.end() == position.offset { | ||
57 | if comment.text().ends_with(' ') { | ||
58 | mark::hit!(continues_end_of_line_comment_with_space); | ||
59 | remove_last_space = true; | ||
60 | } else if !followed_by_comment(&comment) { | ||
61 | return None; | ||
62 | } | ||
63 | } | ||
64 | |||
65 | let indent = node_indent(&file, comment.syntax())?; | ||
66 | let inserted = format!("\n{}{} $0", indent, prefix); | ||
67 | let delete = if remove_last_space { | ||
68 | TextRange::new(position.offset - TextSize::of(' '), position.offset) | ||
69 | } else { | ||
70 | TextRange::empty(position.offset) | ||
71 | }; | ||
72 | let edit = TextEdit::replace(delete, inserted); | ||
73 | Some(edit) | ||
74 | } | ||
75 | |||
76 | fn followed_by_comment(comment: &ast::Comment) -> bool { | ||
77 | let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { | ||
78 | Some(it) => it, | ||
79 | None => return false, | ||
80 | }; | ||
81 | if ws.spans_multiple_lines() { | ||
82 | return false; | ||
83 | } | ||
84 | ws.syntax().next_token().and_then(ast::Comment::cast).is_some() | ||
85 | } | ||
86 | |||
87 | fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | ||
88 | let ws = match file.syntax().token_at_offset(token.text_range().start()) { | ||
89 | TokenAtOffset::Between(l, r) => { | ||
90 | assert!(r == *token); | ||
91 | l | ||
92 | } | ||
93 | TokenAtOffset::Single(n) => { | ||
94 | assert!(n == *token); | ||
95 | return Some("".into()); | ||
96 | } | ||
97 | TokenAtOffset::None => unreachable!(), | ||
98 | }; | ||
99 | if ws.kind() != WHITESPACE { | ||
100 | return None; | ||
101 | } | ||
102 | let text = ws.text(); | ||
103 | let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0); | ||
104 | Some(text[pos..].into()) | ||
105 | } | ||
106 | |||
107 | #[cfg(test)] | ||
108 | mod tests { | ||
109 | use stdx::trim_indent; | ||
110 | use test_utils::{assert_eq_text, mark}; | ||
111 | |||
112 | use crate::mock_analysis::analysis_and_position; | ||
113 | |||
114 | fn apply_on_enter(before: &str) -> Option<String> { | ||
115 | let (analysis, position) = analysis_and_position(&before); | ||
116 | let result = analysis.on_enter(position).unwrap()?; | ||
117 | |||
118 | let mut actual = analysis.file_text(position.file_id).unwrap().to_string(); | ||
119 | result.apply(&mut actual); | ||
120 | Some(actual) | ||
121 | } | ||
122 | |||
123 | fn do_check(ra_fixture_before: &str, ra_fixture_after: &str) { | ||
124 | let ra_fixture_after = &trim_indent(ra_fixture_after); | ||
125 | let actual = apply_on_enter(ra_fixture_before).unwrap(); | ||
126 | assert_eq_text!(ra_fixture_after, &actual); | ||
127 | } | ||
128 | |||
129 | fn do_check_noop(ra_fixture_text: &str) { | ||
130 | assert!(apply_on_enter(ra_fixture_text).is_none()) | ||
131 | } | ||
132 | |||
133 | #[test] | ||
134 | fn continues_doc_comment() { | ||
135 | do_check( | ||
136 | r" | ||
137 | /// Some docs<|> | ||
138 | fn foo() { | ||
139 | } | ||
140 | ", | ||
141 | r" | ||
142 | /// Some docs | ||
143 | /// $0 | ||
144 | fn foo() { | ||
145 | } | ||
146 | ", | ||
147 | ); | ||
148 | |||
149 | do_check( | ||
150 | r" | ||
151 | impl S { | ||
152 | /// Some<|> docs. | ||
153 | fn foo() {} | ||
154 | } | ||
155 | ", | ||
156 | r" | ||
157 | impl S { | ||
158 | /// Some | ||
159 | /// $0 docs. | ||
160 | fn foo() {} | ||
161 | } | ||
162 | ", | ||
163 | ); | ||
164 | |||
165 | do_check( | ||
166 | r" | ||
167 | ///<|> Some docs | ||
168 | fn foo() { | ||
169 | } | ||
170 | ", | ||
171 | r" | ||
172 | /// | ||
173 | /// $0 Some docs | ||
174 | fn foo() { | ||
175 | } | ||
176 | ", | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | #[test] | ||
181 | fn does_not_continue_before_doc_comment() { | ||
182 | do_check_noop(r"<|>//! docz"); | ||
183 | } | ||
184 | |||
185 | #[test] | ||
186 | fn continues_code_comment_in_the_middle_of_line() { | ||
187 | do_check( | ||
188 | r" | ||
189 | fn main() { | ||
190 | // Fix<|> me | ||
191 | let x = 1 + 1; | ||
192 | } | ||
193 | ", | ||
194 | r" | ||
195 | fn main() { | ||
196 | // Fix | ||
197 | // $0 me | ||
198 | let x = 1 + 1; | ||
199 | } | ||
200 | ", | ||
201 | ); | ||
202 | } | ||
203 | |||
204 | #[test] | ||
205 | fn continues_code_comment_in_the_middle_several_lines() { | ||
206 | do_check( | ||
207 | r" | ||
208 | fn main() { | ||
209 | // Fix<|> | ||
210 | // me | ||
211 | let x = 1 + 1; | ||
212 | } | ||
213 | ", | ||
214 | r" | ||
215 | fn main() { | ||
216 | // Fix | ||
217 | // $0 | ||
218 | // me | ||
219 | let x = 1 + 1; | ||
220 | } | ||
221 | ", | ||
222 | ); | ||
223 | } | ||
224 | |||
225 | #[test] | ||
226 | fn does_not_continue_end_of_line_comment() { | ||
227 | do_check_noop( | ||
228 | r" | ||
229 | fn main() { | ||
230 | // Fix me<|> | ||
231 | let x = 1 + 1; | ||
232 | } | ||
233 | ", | ||
234 | ); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn continues_end_of_line_comment_with_space() { | ||
239 | mark::check!(continues_end_of_line_comment_with_space); | ||
240 | do_check( | ||
241 | r#" | ||
242 | fn main() { | ||
243 | // Fix me <|> | ||
244 | let x = 1 + 1; | ||
245 | } | ||
246 | "#, | ||
247 | r#" | ||
248 | fn main() { | ||
249 | // Fix me | ||
250 | // $0 | ||
251 | let x = 1 + 1; | ||
252 | } | ||
253 | "#, | ||
254 | ); | ||
255 | } | ||
256 | } | ||