diff options
author | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
---|---|---|
committer | Seivan Heidari <[email protected]> | 2019-11-28 07:19:14 +0000 |
commit | 18a0937585b836ec5ed054b9ae48e0156ab6d9ef (patch) | |
tree | 9de2c0267ddcc00df717f90034d0843d751a851b /crates/ra_ide/src | |
parent | a7394b44c870f585eacfeb3036a33471aff49ff8 (diff) | |
parent | 484acc8a61d599662ed63a4cbda091d38a982551 (diff) |
Merge branch 'master' of https://github.com/rust-analyzer/rust-analyzer into feature/themes
Diffstat (limited to 'crates/ra_ide/src')
59 files changed, 17146 insertions, 0 deletions
diff --git a/crates/ra_ide/src/assists.rs b/crates/ra_ide/src/assists.rs new file mode 100644 index 000000000..e00589733 --- /dev/null +++ b/crates/ra_ide/src/assists.rs | |||
@@ -0,0 +1,28 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_db::{FilePosition, FileRange}; | ||
4 | |||
5 | use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; | ||
6 | |||
7 | pub use ra_assists::AssistId; | ||
8 | |||
9 | #[derive(Debug)] | ||
10 | pub struct Assist { | ||
11 | pub id: AssistId, | ||
12 | pub change: SourceChange, | ||
13 | } | ||
14 | |||
15 | pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> { | ||
16 | ra_assists::assists(db, frange) | ||
17 | .into_iter() | ||
18 | .map(|(label, action)| { | ||
19 | let file_id = frange.file_id; | ||
20 | let file_edit = SourceFileEdit { file_id, edit: action.edit }; | ||
21 | let id = label.id; | ||
22 | let change = SourceChange::source_file_edit(label.label, file_edit).with_cursor_opt( | ||
23 | action.cursor_position.map(|offset| FilePosition { offset, file_id }), | ||
24 | ); | ||
25 | Assist { id, change } | ||
26 | }) | ||
27 | .collect() | ||
28 | } | ||
diff --git a/crates/ra_ide/src/call_info.rs b/crates/ra_ide/src/call_info.rs new file mode 100644 index 000000000..d559dc4d0 --- /dev/null +++ b/crates/ra_ide/src/call_info.rs | |||
@@ -0,0 +1,592 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_db::SourceDatabase; | ||
4 | use ra_syntax::{ | ||
5 | algo::ancestors_at_offset, | ||
6 | ast::{self, ArgListOwner}, | ||
7 | match_ast, AstNode, SyntaxNode, TextUnit, | ||
8 | }; | ||
9 | use test_utils::tested_by; | ||
10 | |||
11 | use crate::{db::RootDatabase, CallInfo, FilePosition, FunctionSignature}; | ||
12 | |||
13 | /// Computes parameter information for the given call expression. | ||
14 | pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option<CallInfo> { | ||
15 | let parse = db.parse(position.file_id); | ||
16 | let syntax = parse.tree().syntax().clone(); | ||
17 | |||
18 | // Find the calling expression and it's NameRef | ||
19 | let calling_node = FnCallNode::with_node(&syntax, position.offset)?; | ||
20 | let name_ref = calling_node.name_ref()?; | ||
21 | let name_ref = hir::Source::new(position.file_id.into(), name_ref.syntax()); | ||
22 | |||
23 | let analyzer = hir::SourceAnalyzer::new(db, name_ref, None); | ||
24 | let (mut call_info, has_self) = match &calling_node { | ||
25 | FnCallNode::CallExpr(expr) => { | ||
26 | //FIXME: Type::as_callable is broken | ||
27 | let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; | ||
28 | match callable_def { | ||
29 | hir::CallableDef::FunctionId(it) => { | ||
30 | let fn_def = it.into(); | ||
31 | (CallInfo::with_fn(db, fn_def), fn_def.has_self_param(db)) | ||
32 | } | ||
33 | hir::CallableDef::StructId(it) => (CallInfo::with_struct(db, it.into())?, false), | ||
34 | hir::CallableDef::EnumVariantId(it) => { | ||
35 | (CallInfo::with_enum_variant(db, it.into())?, false) | ||
36 | } | ||
37 | } | ||
38 | } | ||
39 | FnCallNode::MethodCallExpr(expr) => { | ||
40 | let function = analyzer.resolve_method_call(&expr)?; | ||
41 | (CallInfo::with_fn(db, function), function.has_self_param(db)) | ||
42 | } | ||
43 | FnCallNode::MacroCallExpr(expr) => { | ||
44 | let macro_def = analyzer.resolve_macro_call(db, name_ref.with_value(&expr))?; | ||
45 | (CallInfo::with_macro(db, macro_def)?, false) | ||
46 | } | ||
47 | }; | ||
48 | |||
49 | // If we have a calling expression let's find which argument we are on | ||
50 | let num_params = call_info.parameters().len(); | ||
51 | |||
52 | if num_params == 1 { | ||
53 | if !has_self { | ||
54 | call_info.active_parameter = Some(0); | ||
55 | } | ||
56 | } else if num_params > 1 { | ||
57 | // Count how many parameters into the call we are. | ||
58 | if let Some(arg_list) = calling_node.arg_list() { | ||
59 | // Number of arguments specified at the call site | ||
60 | let num_args_at_callsite = arg_list.args().count(); | ||
61 | |||
62 | let arg_list_range = arg_list.syntax().text_range(); | ||
63 | if !arg_list_range.contains_inclusive(position.offset) { | ||
64 | tested_by!(call_info_bad_offset); | ||
65 | return None; | ||
66 | } | ||
67 | |||
68 | let mut param = std::cmp::min( | ||
69 | num_args_at_callsite, | ||
70 | arg_list | ||
71 | .args() | ||
72 | .take_while(|arg| arg.syntax().text_range().end() < position.offset) | ||
73 | .count(), | ||
74 | ); | ||
75 | |||
76 | // If we are in a method account for `self` | ||
77 | if has_self { | ||
78 | param += 1; | ||
79 | } | ||
80 | |||
81 | call_info.active_parameter = Some(param); | ||
82 | } | ||
83 | } | ||
84 | |||
85 | Some(call_info) | ||
86 | } | ||
87 | |||
88 | #[derive(Debug)] | ||
89 | enum FnCallNode { | ||
90 | CallExpr(ast::CallExpr), | ||
91 | MethodCallExpr(ast::MethodCallExpr), | ||
92 | MacroCallExpr(ast::MacroCall), | ||
93 | } | ||
94 | |||
95 | impl FnCallNode { | ||
96 | fn with_node(syntax: &SyntaxNode, offset: TextUnit) -> Option<FnCallNode> { | ||
97 | ancestors_at_offset(syntax, offset).find_map(|node| { | ||
98 | match_ast! { | ||
99 | match node { | ||
100 | ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) }, | ||
101 | ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) }, | ||
102 | ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) }, | ||
103 | _ => { None }, | ||
104 | } | ||
105 | } | ||
106 | }) | ||
107 | } | ||
108 | |||
109 | fn name_ref(&self) -> Option<ast::NameRef> { | ||
110 | match self { | ||
111 | FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { | ||
112 | ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, | ||
113 | _ => return None, | ||
114 | }), | ||
115 | |||
116 | FnCallNode::MethodCallExpr(call_expr) => { | ||
117 | call_expr.syntax().children().filter_map(ast::NameRef::cast).nth(0) | ||
118 | } | ||
119 | |||
120 | FnCallNode::MacroCallExpr(call_expr) => call_expr.path()?.segment()?.name_ref(), | ||
121 | } | ||
122 | } | ||
123 | |||
124 | fn arg_list(&self) -> Option<ast::ArgList> { | ||
125 | match self { | ||
126 | FnCallNode::CallExpr(expr) => expr.arg_list(), | ||
127 | FnCallNode::MethodCallExpr(expr) => expr.arg_list(), | ||
128 | FnCallNode::MacroCallExpr(_) => None, | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
133 | impl CallInfo { | ||
134 | fn with_fn(db: &RootDatabase, function: hir::Function) -> Self { | ||
135 | let signature = FunctionSignature::from_hir(db, function); | ||
136 | |||
137 | CallInfo { signature, active_parameter: None } | ||
138 | } | ||
139 | |||
140 | fn with_struct(db: &RootDatabase, st: hir::Struct) -> Option<Self> { | ||
141 | let signature = FunctionSignature::from_struct(db, st)?; | ||
142 | |||
143 | Some(CallInfo { signature, active_parameter: None }) | ||
144 | } | ||
145 | |||
146 | fn with_enum_variant(db: &RootDatabase, variant: hir::EnumVariant) -> Option<Self> { | ||
147 | let signature = FunctionSignature::from_enum_variant(db, variant)?; | ||
148 | |||
149 | Some(CallInfo { signature, active_parameter: None }) | ||
150 | } | ||
151 | |||
152 | fn with_macro(db: &RootDatabase, macro_def: hir::MacroDef) -> Option<Self> { | ||
153 | let signature = FunctionSignature::from_macro(db, macro_def)?; | ||
154 | |||
155 | Some(CallInfo { signature, active_parameter: None }) | ||
156 | } | ||
157 | |||
158 | fn parameters(&self) -> &[String] { | ||
159 | &self.signature.parameters | ||
160 | } | ||
161 | } | ||
162 | |||
163 | #[cfg(test)] | ||
164 | mod tests { | ||
165 | use test_utils::covers; | ||
166 | |||
167 | use crate::mock_analysis::single_file_with_position; | ||
168 | |||
169 | use super::*; | ||
170 | |||
171 | // These are only used when testing | ||
172 | impl CallInfo { | ||
173 | fn doc(&self) -> Option<hir::Documentation> { | ||
174 | self.signature.doc.clone() | ||
175 | } | ||
176 | |||
177 | fn label(&self) -> String { | ||
178 | self.signature.to_string() | ||
179 | } | ||
180 | } | ||
181 | |||
182 | fn call_info(text: &str) -> CallInfo { | ||
183 | let (analysis, position) = single_file_with_position(text); | ||
184 | analysis.call_info(position).unwrap().unwrap() | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn test_fn_signature_two_args_firstx() { | ||
189 | let info = call_info( | ||
190 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
191 | fn bar() { foo(<|>3, ); }"#, | ||
192 | ); | ||
193 | |||
194 | assert_eq!(info.parameters(), ["x: u32", "y: u32"]); | ||
195 | assert_eq!(info.active_parameter, Some(0)); | ||
196 | } | ||
197 | |||
198 | #[test] | ||
199 | fn test_fn_signature_two_args_second() { | ||
200 | let info = call_info( | ||
201 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
202 | fn bar() { foo(3, <|>); }"#, | ||
203 | ); | ||
204 | |||
205 | assert_eq!(info.parameters(), ["x: u32", "y: u32"]); | ||
206 | assert_eq!(info.active_parameter, Some(1)); | ||
207 | } | ||
208 | |||
209 | #[test] | ||
210 | fn test_fn_signature_two_args_empty() { | ||
211 | let info = call_info( | ||
212 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
213 | fn bar() { foo(<|>); }"#, | ||
214 | ); | ||
215 | |||
216 | assert_eq!(info.parameters(), ["x: u32", "y: u32"]); | ||
217 | assert_eq!(info.active_parameter, Some(0)); | ||
218 | } | ||
219 | |||
220 | #[test] | ||
221 | fn test_fn_signature_two_args_first_generics() { | ||
222 | let info = call_info( | ||
223 | r#"fn foo<T, U: Copy + Display>(x: T, y: U) -> u32 where T: Copy + Display, U: Debug {x + y} | ||
224 | fn bar() { foo(<|>3, ); }"#, | ||
225 | ); | ||
226 | |||
227 | assert_eq!(info.parameters(), ["x: T", "y: U"]); | ||
228 | assert_eq!( | ||
229 | info.label(), | ||
230 | r#" | ||
231 | fn foo<T, U: Copy + Display>(x: T, y: U) -> u32 | ||
232 | where T: Copy + Display, | ||
233 | U: Debug | ||
234 | "# | ||
235 | .trim() | ||
236 | ); | ||
237 | assert_eq!(info.active_parameter, Some(0)); | ||
238 | } | ||
239 | |||
240 | #[test] | ||
241 | fn test_fn_signature_no_params() { | ||
242 | let info = call_info( | ||
243 | r#"fn foo<T>() -> T where T: Copy + Display {} | ||
244 | fn bar() { foo(<|>); }"#, | ||
245 | ); | ||
246 | |||
247 | assert!(info.parameters().is_empty()); | ||
248 | assert_eq!( | ||
249 | info.label(), | ||
250 | r#" | ||
251 | fn foo<T>() -> T | ||
252 | where T: Copy + Display | ||
253 | "# | ||
254 | .trim() | ||
255 | ); | ||
256 | assert!(info.active_parameter.is_none()); | ||
257 | } | ||
258 | |||
259 | #[test] | ||
260 | fn test_fn_signature_for_impl() { | ||
261 | let info = call_info( | ||
262 | r#"struct F; impl F { pub fn new() { F{}} } | ||
263 | fn bar() {let _ : F = F::new(<|>);}"#, | ||
264 | ); | ||
265 | |||
266 | assert!(info.parameters().is_empty()); | ||
267 | assert_eq!(info.active_parameter, None); | ||
268 | } | ||
269 | |||
270 | #[test] | ||
271 | fn test_fn_signature_for_method_self() { | ||
272 | let info = call_info( | ||
273 | r#"struct F; | ||
274 | impl F { | ||
275 | pub fn new() -> F{ | ||
276 | F{} | ||
277 | } | ||
278 | |||
279 | pub fn do_it(&self) {} | ||
280 | } | ||
281 | |||
282 | fn bar() { | ||
283 | let f : F = F::new(); | ||
284 | f.do_it(<|>); | ||
285 | }"#, | ||
286 | ); | ||
287 | |||
288 | assert_eq!(info.parameters(), ["&self"]); | ||
289 | assert_eq!(info.active_parameter, None); | ||
290 | } | ||
291 | |||
292 | #[test] | ||
293 | fn test_fn_signature_for_method_with_arg() { | ||
294 | let info = call_info( | ||
295 | r#"struct F; | ||
296 | impl F { | ||
297 | pub fn new() -> F{ | ||
298 | F{} | ||
299 | } | ||
300 | |||
301 | pub fn do_it(&self, x: i32) {} | ||
302 | } | ||
303 | |||
304 | fn bar() { | ||
305 | let f : F = F::new(); | ||
306 | f.do_it(<|>); | ||
307 | }"#, | ||
308 | ); | ||
309 | |||
310 | assert_eq!(info.parameters(), ["&self", "x: i32"]); | ||
311 | assert_eq!(info.active_parameter, Some(1)); | ||
312 | } | ||
313 | |||
314 | #[test] | ||
315 | fn test_fn_signature_with_docs_simple() { | ||
316 | let info = call_info( | ||
317 | r#" | ||
318 | /// test | ||
319 | // non-doc-comment | ||
320 | fn foo(j: u32) -> u32 { | ||
321 | j | ||
322 | } | ||
323 | |||
324 | fn bar() { | ||
325 | let _ = foo(<|>); | ||
326 | } | ||
327 | "#, | ||
328 | ); | ||
329 | |||
330 | assert_eq!(info.parameters(), ["j: u32"]); | ||
331 | assert_eq!(info.active_parameter, Some(0)); | ||
332 | assert_eq!(info.label(), "fn foo(j: u32) -> u32"); | ||
333 | assert_eq!(info.doc().map(|it| it.into()), Some("test".to_string())); | ||
334 | } | ||
335 | |||
336 | #[test] | ||
337 | fn test_fn_signature_with_docs() { | ||
338 | let info = call_info( | ||
339 | r#" | ||
340 | /// Adds one to the number given. | ||
341 | /// | ||
342 | /// # Examples | ||
343 | /// | ||
344 | /// ``` | ||
345 | /// let five = 5; | ||
346 | /// | ||
347 | /// assert_eq!(6, my_crate::add_one(5)); | ||
348 | /// ``` | ||
349 | pub fn add_one(x: i32) -> i32 { | ||
350 | x + 1 | ||
351 | } | ||
352 | |||
353 | pub fn do() { | ||
354 | add_one(<|> | ||
355 | }"#, | ||
356 | ); | ||
357 | |||
358 | assert_eq!(info.parameters(), ["x: i32"]); | ||
359 | assert_eq!(info.active_parameter, Some(0)); | ||
360 | assert_eq!(info.label(), "pub fn add_one(x: i32) -> i32"); | ||
361 | assert_eq!( | ||
362 | info.doc().map(|it| it.into()), | ||
363 | Some( | ||
364 | r#"Adds one to the number given. | ||
365 | |||
366 | # Examples | ||
367 | |||
368 | ``` | ||
369 | let five = 5; | ||
370 | |||
371 | assert_eq!(6, my_crate::add_one(5)); | ||
372 | ```"# | ||
373 | .to_string() | ||
374 | ) | ||
375 | ); | ||
376 | } | ||
377 | |||
378 | #[test] | ||
379 | fn test_fn_signature_with_docs_impl() { | ||
380 | let info = call_info( | ||
381 | r#" | ||
382 | struct addr; | ||
383 | impl addr { | ||
384 | /// Adds one to the number given. | ||
385 | /// | ||
386 | /// # Examples | ||
387 | /// | ||
388 | /// ``` | ||
389 | /// let five = 5; | ||
390 | /// | ||
391 | /// assert_eq!(6, my_crate::add_one(5)); | ||
392 | /// ``` | ||
393 | pub fn add_one(x: i32) -> i32 { | ||
394 | x + 1 | ||
395 | } | ||
396 | } | ||
397 | |||
398 | pub fn do_it() { | ||
399 | addr {}; | ||
400 | addr::add_one(<|>); | ||
401 | }"#, | ||
402 | ); | ||
403 | |||
404 | assert_eq!(info.parameters(), ["x: i32"]); | ||
405 | assert_eq!(info.active_parameter, Some(0)); | ||
406 | assert_eq!(info.label(), "pub fn add_one(x: i32) -> i32"); | ||
407 | assert_eq!( | ||
408 | info.doc().map(|it| it.into()), | ||
409 | Some( | ||
410 | r#"Adds one to the number given. | ||
411 | |||
412 | # Examples | ||
413 | |||
414 | ``` | ||
415 | let five = 5; | ||
416 | |||
417 | assert_eq!(6, my_crate::add_one(5)); | ||
418 | ```"# | ||
419 | .to_string() | ||
420 | ) | ||
421 | ); | ||
422 | } | ||
423 | |||
424 | #[test] | ||
425 | fn test_fn_signature_with_docs_from_actix() { | ||
426 | let info = call_info( | ||
427 | r#" | ||
428 | struct WriteHandler<E>; | ||
429 | |||
430 | impl<E> WriteHandler<E> { | ||
431 | /// Method is called when writer emits error. | ||
432 | /// | ||
433 | /// If this method returns `ErrorAction::Continue` writer processing | ||
434 | /// continues otherwise stream processing stops. | ||
435 | fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { | ||
436 | Running::Stop | ||
437 | } | ||
438 | |||
439 | /// Method is called when writer finishes. | ||
440 | /// | ||
441 | /// By default this method stops actor's `Context`. | ||
442 | fn finished(&mut self, ctx: &mut Self::Context) { | ||
443 | ctx.stop() | ||
444 | } | ||
445 | } | ||
446 | |||
447 | pub fn foo(mut r: WriteHandler<()>) { | ||
448 | r.finished(<|>); | ||
449 | } | ||
450 | |||
451 | "#, | ||
452 | ); | ||
453 | |||
454 | assert_eq!(info.label(), "fn finished(&mut self, ctx: &mut Self::Context)".to_string()); | ||
455 | assert_eq!(info.parameters(), ["&mut self", "ctx: &mut Self::Context"]); | ||
456 | assert_eq!(info.active_parameter, Some(1)); | ||
457 | assert_eq!( | ||
458 | info.doc().map(|it| it.into()), | ||
459 | Some( | ||
460 | r#"Method is called when writer finishes. | ||
461 | |||
462 | By default this method stops actor's `Context`."# | ||
463 | .to_string() | ||
464 | ) | ||
465 | ); | ||
466 | } | ||
467 | |||
468 | #[test] | ||
469 | fn call_info_bad_offset() { | ||
470 | covers!(call_info_bad_offset); | ||
471 | let (analysis, position) = single_file_with_position( | ||
472 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
473 | fn bar() { foo <|> (3, ); }"#, | ||
474 | ); | ||
475 | let call_info = analysis.call_info(position).unwrap(); | ||
476 | assert!(call_info.is_none()); | ||
477 | } | ||
478 | |||
479 | #[test] | ||
480 | fn test_nested_method_in_lamba() { | ||
481 | let info = call_info( | ||
482 | r#"struct Foo; | ||
483 | |||
484 | impl Foo { | ||
485 | fn bar(&self, _: u32) { } | ||
486 | } | ||
487 | |||
488 | fn bar(_: u32) { } | ||
489 | |||
490 | fn main() { | ||
491 | let foo = Foo; | ||
492 | std::thread::spawn(move || foo.bar(<|>)); | ||
493 | }"#, | ||
494 | ); | ||
495 | |||
496 | assert_eq!(info.parameters(), ["&self", "_: u32"]); | ||
497 | assert_eq!(info.active_parameter, Some(1)); | ||
498 | assert_eq!(info.label(), "fn bar(&self, _: u32)"); | ||
499 | } | ||
500 | |||
501 | #[test] | ||
502 | fn works_for_tuple_structs() { | ||
503 | let info = call_info( | ||
504 | r#" | ||
505 | /// A cool tuple struct | ||
506 | struct TS(u32, i32); | ||
507 | fn main() { | ||
508 | let s = TS(0, <|>); | ||
509 | }"#, | ||
510 | ); | ||
511 | |||
512 | assert_eq!(info.label(), "struct TS(u32, i32) -> TS"); | ||
513 | assert_eq!(info.doc().map(|it| it.into()), Some("A cool tuple struct".to_string())); | ||
514 | assert_eq!(info.active_parameter, Some(1)); | ||
515 | } | ||
516 | |||
517 | #[test] | ||
518 | #[should_panic] | ||
519 | fn cant_call_named_structs() { | ||
520 | let _ = call_info( | ||
521 | r#" | ||
522 | struct TS { x: u32, y: i32 } | ||
523 | fn main() { | ||
524 | let s = TS(<|>); | ||
525 | }"#, | ||
526 | ); | ||
527 | } | ||
528 | |||
529 | #[test] | ||
530 | fn works_for_enum_variants() { | ||
531 | let info = call_info( | ||
532 | r#" | ||
533 | enum E { | ||
534 | /// A Variant | ||
535 | A(i32), | ||
536 | /// Another | ||
537 | B, | ||
538 | /// And C | ||
539 | C { a: i32, b: i32 } | ||
540 | } | ||
541 | |||
542 | fn main() { | ||
543 | let a = E::A(<|>); | ||
544 | } | ||
545 | "#, | ||
546 | ); | ||
547 | |||
548 | assert_eq!(info.label(), "E::A(0: i32)"); | ||
549 | assert_eq!(info.doc().map(|it| it.into()), Some("A Variant".to_string())); | ||
550 | assert_eq!(info.active_parameter, Some(0)); | ||
551 | } | ||
552 | |||
553 | #[test] | ||
554 | #[should_panic] | ||
555 | fn cant_call_enum_records() { | ||
556 | let _ = call_info( | ||
557 | r#" | ||
558 | enum E { | ||
559 | /// A Variant | ||
560 | A(i32), | ||
561 | /// Another | ||
562 | B, | ||
563 | /// And C | ||
564 | C { a: i32, b: i32 } | ||
565 | } | ||
566 | |||
567 | fn main() { | ||
568 | let a = E::C(<|>); | ||
569 | } | ||
570 | "#, | ||
571 | ); | ||
572 | } | ||
573 | |||
574 | #[test] | ||
575 | fn fn_signature_for_macro() { | ||
576 | let info = call_info( | ||
577 | r#" | ||
578 | /// empty macro | ||
579 | macro_rules! foo { | ||
580 | () => {} | ||
581 | } | ||
582 | |||
583 | fn f() { | ||
584 | foo!(<|>); | ||
585 | } | ||
586 | "#, | ||
587 | ); | ||
588 | |||
589 | assert_eq!(info.label(), "foo!()"); | ||
590 | assert_eq!(info.doc().map(|it| it.into()), Some("empty macro".to_string())); | ||
591 | } | ||
592 | } | ||
diff --git a/crates/ra_ide/src/change.rs b/crates/ra_ide/src/change.rs new file mode 100644 index 000000000..4a76d1dd8 --- /dev/null +++ b/crates/ra_ide/src/change.rs | |||
@@ -0,0 +1,354 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::{fmt, sync::Arc, time}; | ||
4 | |||
5 | use ra_db::{ | ||
6 | salsa::{Database, Durability, SweepStrategy}, | ||
7 | CrateGraph, CrateId, FileId, RelativePathBuf, SourceDatabase, SourceDatabaseExt, SourceRoot, | ||
8 | SourceRootId, | ||
9 | }; | ||
10 | use ra_prof::{memory_usage, profile, Bytes}; | ||
11 | use ra_syntax::SourceFile; | ||
12 | #[cfg(not(feature = "wasm"))] | ||
13 | use rayon::prelude::*; | ||
14 | use rustc_hash::FxHashMap; | ||
15 | |||
16 | use crate::{ | ||
17 | db::{DebugData, RootDatabase}, | ||
18 | symbol_index::{SymbolIndex, SymbolsDatabase}, | ||
19 | }; | ||
20 | |||
21 | #[derive(Default)] | ||
22 | pub struct AnalysisChange { | ||
23 | new_roots: Vec<(SourceRootId, bool)>, | ||
24 | roots_changed: FxHashMap<SourceRootId, RootChange>, | ||
25 | files_changed: Vec<(FileId, Arc<String>)>, | ||
26 | libraries_added: Vec<LibraryData>, | ||
27 | crate_graph: Option<CrateGraph>, | ||
28 | debug_data: DebugData, | ||
29 | } | ||
30 | |||
31 | impl fmt::Debug for AnalysisChange { | ||
32 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
33 | let mut d = fmt.debug_struct("AnalysisChange"); | ||
34 | if !self.new_roots.is_empty() { | ||
35 | d.field("new_roots", &self.new_roots); | ||
36 | } | ||
37 | if !self.roots_changed.is_empty() { | ||
38 | d.field("roots_changed", &self.roots_changed); | ||
39 | } | ||
40 | if !self.files_changed.is_empty() { | ||
41 | d.field("files_changed", &self.files_changed.len()); | ||
42 | } | ||
43 | if !self.libraries_added.is_empty() { | ||
44 | d.field("libraries_added", &self.libraries_added.len()); | ||
45 | } | ||
46 | if !self.crate_graph.is_none() { | ||
47 | d.field("crate_graph", &self.crate_graph); | ||
48 | } | ||
49 | d.finish() | ||
50 | } | ||
51 | } | ||
52 | |||
53 | impl AnalysisChange { | ||
54 | pub fn new() -> AnalysisChange { | ||
55 | AnalysisChange::default() | ||
56 | } | ||
57 | |||
58 | pub fn add_root(&mut self, root_id: SourceRootId, is_local: bool) { | ||
59 | self.new_roots.push((root_id, is_local)); | ||
60 | } | ||
61 | |||
62 | pub fn add_file( | ||
63 | &mut self, | ||
64 | root_id: SourceRootId, | ||
65 | file_id: FileId, | ||
66 | path: RelativePathBuf, | ||
67 | text: Arc<String>, | ||
68 | ) { | ||
69 | let file = AddFile { file_id, path, text }; | ||
70 | self.roots_changed.entry(root_id).or_default().added.push(file); | ||
71 | } | ||
72 | |||
73 | pub fn change_file(&mut self, file_id: FileId, new_text: Arc<String>) { | ||
74 | self.files_changed.push((file_id, new_text)) | ||
75 | } | ||
76 | |||
77 | pub fn remove_file(&mut self, root_id: SourceRootId, file_id: FileId, path: RelativePathBuf) { | ||
78 | let file = RemoveFile { file_id, path }; | ||
79 | self.roots_changed.entry(root_id).or_default().removed.push(file); | ||
80 | } | ||
81 | |||
82 | pub fn add_library(&mut self, data: LibraryData) { | ||
83 | self.libraries_added.push(data) | ||
84 | } | ||
85 | |||
86 | pub fn set_crate_graph(&mut self, graph: CrateGraph) { | ||
87 | self.crate_graph = Some(graph); | ||
88 | } | ||
89 | |||
90 | pub fn set_debug_crate_name(&mut self, crate_id: CrateId, name: String) { | ||
91 | self.debug_data.crate_names.insert(crate_id, name); | ||
92 | } | ||
93 | |||
94 | pub fn set_debug_root_path(&mut self, source_root_id: SourceRootId, path: String) { | ||
95 | self.debug_data.root_paths.insert(source_root_id, path); | ||
96 | } | ||
97 | } | ||
98 | |||
99 | #[derive(Debug)] | ||
100 | struct AddFile { | ||
101 | file_id: FileId, | ||
102 | path: RelativePathBuf, | ||
103 | text: Arc<String>, | ||
104 | } | ||
105 | |||
106 | #[derive(Debug)] | ||
107 | struct RemoveFile { | ||
108 | file_id: FileId, | ||
109 | path: RelativePathBuf, | ||
110 | } | ||
111 | |||
112 | #[derive(Default)] | ||
113 | struct RootChange { | ||
114 | added: Vec<AddFile>, | ||
115 | removed: Vec<RemoveFile>, | ||
116 | } | ||
117 | |||
118 | impl fmt::Debug for RootChange { | ||
119 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
120 | fmt.debug_struct("AnalysisChange") | ||
121 | .field("added", &self.added.len()) | ||
122 | .field("removed", &self.removed.len()) | ||
123 | .finish() | ||
124 | } | ||
125 | } | ||
126 | |||
127 | pub struct LibraryData { | ||
128 | root_id: SourceRootId, | ||
129 | root_change: RootChange, | ||
130 | symbol_index: SymbolIndex, | ||
131 | } | ||
132 | |||
133 | impl fmt::Debug for LibraryData { | ||
134 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
135 | f.debug_struct("LibraryData") | ||
136 | .field("root_id", &self.root_id) | ||
137 | .field("root_change", &self.root_change) | ||
138 | .field("n_symbols", &self.symbol_index.len()) | ||
139 | .finish() | ||
140 | } | ||
141 | } | ||
142 | |||
143 | impl LibraryData { | ||
144 | pub fn prepare( | ||
145 | root_id: SourceRootId, | ||
146 | files: Vec<(FileId, RelativePathBuf, Arc<String>)>, | ||
147 | ) -> LibraryData { | ||
148 | #[cfg(not(feature = "wasm"))] | ||
149 | let iter = files.par_iter(); | ||
150 | #[cfg(feature = "wasm")] | ||
151 | let iter = files.iter(); | ||
152 | |||
153 | let symbol_index = SymbolIndex::for_files(iter.map(|(file_id, _, text)| { | ||
154 | let parse = SourceFile::parse(text); | ||
155 | (*file_id, parse) | ||
156 | })); | ||
157 | let mut root_change = RootChange::default(); | ||
158 | root_change.added = files | ||
159 | .into_iter() | ||
160 | .map(|(file_id, path, text)| AddFile { file_id, path, text }) | ||
161 | .collect(); | ||
162 | LibraryData { root_id, root_change, symbol_index } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | const GC_COOLDOWN: time::Duration = time::Duration::from_millis(100); | ||
167 | |||
168 | impl RootDatabase { | ||
169 | pub(crate) fn apply_change(&mut self, change: AnalysisChange) { | ||
170 | let _p = profile("RootDatabase::apply_change"); | ||
171 | log::info!("apply_change {:?}", change); | ||
172 | { | ||
173 | let _p = profile("RootDatabase::apply_change/cancellation"); | ||
174 | self.salsa_runtime_mut().synthetic_write(Durability::LOW); | ||
175 | } | ||
176 | if !change.new_roots.is_empty() { | ||
177 | let mut local_roots = Vec::clone(&self.local_roots()); | ||
178 | for (root_id, is_local) in change.new_roots { | ||
179 | let root = if is_local { SourceRoot::new() } else { SourceRoot::new_library() }; | ||
180 | let durability = durability(&root); | ||
181 | self.set_source_root_with_durability(root_id, Arc::new(root), durability); | ||
182 | if is_local { | ||
183 | local_roots.push(root_id); | ||
184 | } | ||
185 | } | ||
186 | self.set_local_roots_with_durability(Arc::new(local_roots), Durability::HIGH); | ||
187 | } | ||
188 | |||
189 | for (root_id, root_change) in change.roots_changed { | ||
190 | self.apply_root_change(root_id, root_change); | ||
191 | } | ||
192 | for (file_id, text) in change.files_changed { | ||
193 | let source_root_id = self.file_source_root(file_id); | ||
194 | let source_root = self.source_root(source_root_id); | ||
195 | let durability = durability(&source_root); | ||
196 | self.set_file_text_with_durability(file_id, text, durability) | ||
197 | } | ||
198 | if !change.libraries_added.is_empty() { | ||
199 | let mut libraries = Vec::clone(&self.library_roots()); | ||
200 | for library in change.libraries_added { | ||
201 | libraries.push(library.root_id); | ||
202 | self.set_source_root_with_durability( | ||
203 | library.root_id, | ||
204 | Default::default(), | ||
205 | Durability::HIGH, | ||
206 | ); | ||
207 | self.set_library_symbols_with_durability( | ||
208 | library.root_id, | ||
209 | Arc::new(library.symbol_index), | ||
210 | Durability::HIGH, | ||
211 | ); | ||
212 | self.apply_root_change(library.root_id, library.root_change); | ||
213 | } | ||
214 | self.set_library_roots_with_durability(Arc::new(libraries), Durability::HIGH); | ||
215 | } | ||
216 | if let Some(crate_graph) = change.crate_graph { | ||
217 | self.set_crate_graph_with_durability(Arc::new(crate_graph), Durability::HIGH) | ||
218 | } | ||
219 | |||
220 | Arc::make_mut(&mut self.debug_data).merge(change.debug_data) | ||
221 | } | ||
222 | |||
223 | fn apply_root_change(&mut self, root_id: SourceRootId, root_change: RootChange) { | ||
224 | let mut source_root = SourceRoot::clone(&self.source_root(root_id)); | ||
225 | let durability = durability(&source_root); | ||
226 | for add_file in root_change.added { | ||
227 | self.set_file_text_with_durability(add_file.file_id, add_file.text, durability); | ||
228 | self.set_file_relative_path_with_durability( | ||
229 | add_file.file_id, | ||
230 | add_file.path.clone(), | ||
231 | durability, | ||
232 | ); | ||
233 | self.set_file_source_root_with_durability(add_file.file_id, root_id, durability); | ||
234 | source_root.insert_file(add_file.path, add_file.file_id); | ||
235 | } | ||
236 | for remove_file in root_change.removed { | ||
237 | self.set_file_text_with_durability(remove_file.file_id, Default::default(), durability); | ||
238 | source_root.remove_file(&remove_file.path); | ||
239 | } | ||
240 | self.set_source_root_with_durability(root_id, Arc::new(source_root), durability); | ||
241 | } | ||
242 | |||
243 | pub(crate) fn maybe_collect_garbage(&mut self) { | ||
244 | if cfg!(feature = "wasm") { | ||
245 | return; | ||
246 | } | ||
247 | |||
248 | if self.last_gc_check.elapsed() > GC_COOLDOWN { | ||
249 | self.last_gc_check = crate::wasm_shims::Instant::now(); | ||
250 | } | ||
251 | } | ||
252 | |||
253 | pub(crate) fn collect_garbage(&mut self) { | ||
254 | if cfg!(feature = "wasm") { | ||
255 | return; | ||
256 | } | ||
257 | |||
258 | let _p = profile("RootDatabase::collect_garbage"); | ||
259 | self.last_gc = crate::wasm_shims::Instant::now(); | ||
260 | |||
261 | let sweep = SweepStrategy::default().discard_values().sweep_all_revisions(); | ||
262 | |||
263 | self.query(ra_db::ParseQuery).sweep(sweep); | ||
264 | self.query(hir::db::ParseMacroQuery).sweep(sweep); | ||
265 | |||
266 | // Macros do take significant space, but less then the syntax trees | ||
267 | // self.query(hir::db::MacroDefQuery).sweep(sweep); | ||
268 | // self.query(hir::db::MacroArgQuery).sweep(sweep); | ||
269 | // self.query(hir::db::MacroExpandQuery).sweep(sweep); | ||
270 | |||
271 | self.query(hir::db::AstIdMapQuery).sweep(sweep); | ||
272 | |||
273 | self.query(hir::db::RawItemsWithSourceMapQuery).sweep(sweep); | ||
274 | self.query(hir::db::BodyWithSourceMapQuery).sweep(sweep); | ||
275 | |||
276 | self.query(hir::db::ExprScopesQuery).sweep(sweep); | ||
277 | self.query(hir::db::InferQuery).sweep(sweep); | ||
278 | self.query(hir::db::BodyQuery).sweep(sweep); | ||
279 | } | ||
280 | |||
281 | pub(crate) fn per_query_memory_usage(&mut self) -> Vec<(String, Bytes)> { | ||
282 | let mut acc: Vec<(String, Bytes)> = vec![]; | ||
283 | let sweep = SweepStrategy::default().discard_values().sweep_all_revisions(); | ||
284 | macro_rules! sweep_each_query { | ||
285 | ($($q:path)*) => {$( | ||
286 | let before = memory_usage().allocated; | ||
287 | self.query($q).sweep(sweep); | ||
288 | let after = memory_usage().allocated; | ||
289 | let q: $q = Default::default(); | ||
290 | let name = format!("{:?}", q); | ||
291 | acc.push((name, before - after)); | ||
292 | |||
293 | let before = memory_usage().allocated; | ||
294 | self.query($q).sweep(sweep.discard_everything()); | ||
295 | let after = memory_usage().allocated; | ||
296 | let q: $q = Default::default(); | ||
297 | let name = format!("{:?} (deps)", q); | ||
298 | acc.push((name, before - after)); | ||
299 | )*} | ||
300 | } | ||
301 | sweep_each_query![ | ||
302 | ra_db::ParseQuery | ||
303 | ra_db::SourceRootCratesQuery | ||
304 | hir::db::AstIdMapQuery | ||
305 | hir::db::ParseMacroQuery | ||
306 | hir::db::MacroDefQuery | ||
307 | hir::db::MacroArgQuery | ||
308 | hir::db::MacroExpandQuery | ||
309 | hir::db::StructDataQuery | ||
310 | hir::db::EnumDataQuery | ||
311 | hir::db::TraitDataQuery | ||
312 | hir::db::RawItemsWithSourceMapQuery | ||
313 | hir::db::RawItemsQuery | ||
314 | hir::db::CrateDefMapQuery | ||
315 | hir::db::GenericParamsQuery | ||
316 | hir::db::FunctionDataQuery | ||
317 | hir::db::TypeAliasDataQuery | ||
318 | hir::db::ConstDataQuery | ||
319 | hir::db::StaticDataQuery | ||
320 | hir::db::ModuleLangItemsQuery | ||
321 | hir::db::CrateLangItemsQuery | ||
322 | hir::db::LangItemQuery | ||
323 | hir::db::DocumentationQuery | ||
324 | hir::db::ExprScopesQuery | ||
325 | hir::db::InferQuery | ||
326 | hir::db::TyQuery | ||
327 | hir::db::ValueTyQuery | ||
328 | hir::db::FieldTypesQuery | ||
329 | hir::db::CallableItemSignatureQuery | ||
330 | hir::db::GenericPredicatesQuery | ||
331 | hir::db::GenericDefaultsQuery | ||
332 | hir::db::BodyWithSourceMapQuery | ||
333 | hir::db::BodyQuery | ||
334 | hir::db::ImplsInCrateQuery | ||
335 | hir::db::ImplsForTraitQuery | ||
336 | hir::db::AssociatedTyDataQuery | ||
337 | hir::db::TraitDatumQuery | ||
338 | hir::db::StructDatumQuery | ||
339 | hir::db::ImplDatumQuery | ||
340 | hir::db::ImplDataQuery | ||
341 | hir::db::TraitSolveQuery | ||
342 | ]; | ||
343 | acc.sort_by_key(|it| std::cmp::Reverse(it.1)); | ||
344 | acc | ||
345 | } | ||
346 | } | ||
347 | |||
348 | fn durability(source_root: &SourceRoot) -> Durability { | ||
349 | if source_root.is_library { | ||
350 | Durability::HIGH | ||
351 | } else { | ||
352 | Durability::LOW | ||
353 | } | ||
354 | } | ||
diff --git a/crates/ra_ide/src/completion.rs b/crates/ra_ide/src/completion.rs new file mode 100644 index 000000000..abe1f36ce --- /dev/null +++ b/crates/ra_ide/src/completion.rs | |||
@@ -0,0 +1,77 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | mod completion_item; | ||
4 | mod completion_context; | ||
5 | mod presentation; | ||
6 | |||
7 | mod complete_dot; | ||
8 | mod complete_record_literal; | ||
9 | mod complete_record_pattern; | ||
10 | mod complete_pattern; | ||
11 | mod complete_fn_param; | ||
12 | mod complete_keyword; | ||
13 | mod complete_snippet; | ||
14 | mod complete_path; | ||
15 | mod complete_scope; | ||
16 | mod complete_postfix; | ||
17 | mod complete_macro_in_item_position; | ||
18 | |||
19 | use ra_db::SourceDatabase; | ||
20 | |||
21 | #[cfg(test)] | ||
22 | use crate::completion::completion_item::do_completion; | ||
23 | use crate::{ | ||
24 | completion::{ | ||
25 | completion_context::CompletionContext, | ||
26 | completion_item::{CompletionKind, Completions}, | ||
27 | }, | ||
28 | db, FilePosition, | ||
29 | }; | ||
30 | |||
31 | pub use crate::completion::completion_item::{ | ||
32 | CompletionItem, CompletionItemKind, InsertTextFormat, | ||
33 | }; | ||
34 | |||
35 | /// Main entry point for completion. We run completion as a two-phase process. | ||
36 | /// | ||
37 | /// First, we look at the position and collect a so-called `CompletionContext. | ||
38 | /// This is a somewhat messy process, because, during completion, syntax tree is | ||
39 | /// incomplete and can look really weird. | ||
40 | /// | ||
41 | /// Once the context is collected, we run a series of completion routines which | ||
42 | /// look at the context and produce completion items. One subtlety about this | ||
43 | /// phase is that completion engine should not filter by the substring which is | ||
44 | /// already present, it should give all possible variants for the identifier at | ||
45 | /// the caret. In other words, for | ||
46 | /// | ||
47 | /// ```no-run | ||
48 | /// fn f() { | ||
49 | /// let foo = 92; | ||
50 | /// let _ = bar<|> | ||
51 | /// } | ||
52 | /// ``` | ||
53 | /// | ||
54 | /// `foo` *should* be present among the completion variants. Filtering by | ||
55 | /// identifier prefix/fuzzy match should be done higher in the stack, together | ||
56 | /// with ordering of completions (currently this is done by the client). | ||
57 | pub(crate) fn completions(db: &db::RootDatabase, position: FilePosition) -> Option<Completions> { | ||
58 | let original_parse = db.parse(position.file_id); | ||
59 | let ctx = CompletionContext::new(db, &original_parse, position)?; | ||
60 | |||
61 | let mut acc = Completions::default(); | ||
62 | |||
63 | complete_fn_param::complete_fn_param(&mut acc, &ctx); | ||
64 | complete_keyword::complete_expr_keyword(&mut acc, &ctx); | ||
65 | complete_keyword::complete_use_tree_keyword(&mut acc, &ctx); | ||
66 | complete_snippet::complete_expr_snippet(&mut acc, &ctx); | ||
67 | complete_snippet::complete_item_snippet(&mut acc, &ctx); | ||
68 | complete_path::complete_path(&mut acc, &ctx); | ||
69 | complete_scope::complete_scope(&mut acc, &ctx); | ||
70 | complete_dot::complete_dot(&mut acc, &ctx); | ||
71 | complete_record_literal::complete_record_literal(&mut acc, &ctx); | ||
72 | complete_record_pattern::complete_record_pattern(&mut acc, &ctx); | ||
73 | complete_pattern::complete_pattern(&mut acc, &ctx); | ||
74 | complete_postfix::complete_postfix(&mut acc, &ctx); | ||
75 | complete_macro_in_item_position::complete_macro_in_item_position(&mut acc, &ctx); | ||
76 | Some(acc) | ||
77 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_dot.rs b/crates/ra_ide/src/completion/complete_dot.rs new file mode 100644 index 000000000..b6fe48627 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_dot.rs | |||
@@ -0,0 +1,456 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::Type; | ||
4 | |||
5 | use crate::completion::completion_item::CompletionKind; | ||
6 | use crate::{ | ||
7 | completion::{completion_context::CompletionContext, completion_item::Completions}, | ||
8 | CompletionItem, | ||
9 | }; | ||
10 | use rustc_hash::FxHashSet; | ||
11 | |||
12 | /// Complete dot accesses, i.e. fields or methods (and .await syntax). | ||
13 | pub(super) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) { | ||
14 | let dot_receiver = match &ctx.dot_receiver { | ||
15 | Some(expr) => expr, | ||
16 | _ => return, | ||
17 | }; | ||
18 | |||
19 | let receiver_ty = match ctx.analyzer.type_of(ctx.db, &dot_receiver) { | ||
20 | Some(ty) => ty, | ||
21 | _ => return, | ||
22 | }; | ||
23 | |||
24 | if !ctx.is_call { | ||
25 | complete_fields(acc, ctx, &receiver_ty); | ||
26 | } | ||
27 | complete_methods(acc, ctx, &receiver_ty); | ||
28 | |||
29 | // Suggest .await syntax for types that implement Future trait | ||
30 | if ctx.analyzer.impls_future(ctx.db, receiver_ty.into_ty()) { | ||
31 | CompletionItem::new(CompletionKind::Keyword, ctx.source_range(), "await") | ||
32 | .detail("expr.await") | ||
33 | .insert_text("await") | ||
34 | .add_to(acc); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) { | ||
39 | for receiver in receiver.autoderef(ctx.db) { | ||
40 | for (field, ty) in receiver.fields(ctx.db) { | ||
41 | acc.add_field(ctx, field, &ty); | ||
42 | } | ||
43 | for (i, ty) in receiver.tuple_fields(ctx.db).into_iter().enumerate() { | ||
44 | acc.add_tuple_field(ctx, i, &ty); | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | |||
49 | fn complete_methods(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) { | ||
50 | let mut seen_methods = FxHashSet::default(); | ||
51 | ctx.analyzer.iterate_method_candidates(ctx.db, receiver, None, |_ty, func| { | ||
52 | if func.has_self_param(ctx.db) && seen_methods.insert(func.name(ctx.db)) { | ||
53 | acc.add_function(ctx, func); | ||
54 | } | ||
55 | None::<()> | ||
56 | }); | ||
57 | } | ||
58 | |||
59 | #[cfg(test)] | ||
60 | mod tests { | ||
61 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
62 | use insta::assert_debug_snapshot; | ||
63 | |||
64 | fn do_ref_completion(code: &str) -> Vec<CompletionItem> { | ||
65 | do_completion(code, CompletionKind::Reference) | ||
66 | } | ||
67 | |||
68 | #[test] | ||
69 | fn test_struct_field_completion() { | ||
70 | assert_debug_snapshot!( | ||
71 | do_ref_completion( | ||
72 | r" | ||
73 | struct A { the_field: u32 } | ||
74 | fn foo(a: A) { | ||
75 | a.<|> | ||
76 | } | ||
77 | ", | ||
78 | ), | ||
79 | @r###" | ||
80 | [ | ||
81 | CompletionItem { | ||
82 | label: "the_field", | ||
83 | source_range: [94; 94), | ||
84 | delete: [94; 94), | ||
85 | insert: "the_field", | ||
86 | kind: Field, | ||
87 | detail: "u32", | ||
88 | }, | ||
89 | ] | ||
90 | "### | ||
91 | ); | ||
92 | } | ||
93 | |||
94 | #[test] | ||
95 | fn test_struct_field_completion_self() { | ||
96 | assert_debug_snapshot!( | ||
97 | do_ref_completion( | ||
98 | r" | ||
99 | struct A { | ||
100 | /// This is the_field | ||
101 | the_field: (u32,) | ||
102 | } | ||
103 | impl A { | ||
104 | fn foo(self) { | ||
105 | self.<|> | ||
106 | } | ||
107 | } | ||
108 | ", | ||
109 | ), | ||
110 | @r###" | ||
111 | [ | ||
112 | CompletionItem { | ||
113 | label: "foo()", | ||
114 | source_range: [187; 187), | ||
115 | delete: [187; 187), | ||
116 | insert: "foo()$0", | ||
117 | kind: Method, | ||
118 | lookup: "foo", | ||
119 | detail: "fn foo(self)", | ||
120 | }, | ||
121 | CompletionItem { | ||
122 | label: "the_field", | ||
123 | source_range: [187; 187), | ||
124 | delete: [187; 187), | ||
125 | insert: "the_field", | ||
126 | kind: Field, | ||
127 | detail: "(u32,)", | ||
128 | documentation: Documentation( | ||
129 | "This is the_field", | ||
130 | ), | ||
131 | }, | ||
132 | ] | ||
133 | "### | ||
134 | ); | ||
135 | } | ||
136 | |||
137 | #[test] | ||
138 | fn test_struct_field_completion_autoderef() { | ||
139 | assert_debug_snapshot!( | ||
140 | do_ref_completion( | ||
141 | r" | ||
142 | struct A { the_field: (u32, i32) } | ||
143 | impl A { | ||
144 | fn foo(&self) { | ||
145 | self.<|> | ||
146 | } | ||
147 | } | ||
148 | ", | ||
149 | ), | ||
150 | @r###" | ||
151 | [ | ||
152 | CompletionItem { | ||
153 | label: "foo()", | ||
154 | source_range: [126; 126), | ||
155 | delete: [126; 126), | ||
156 | insert: "foo()$0", | ||
157 | kind: Method, | ||
158 | lookup: "foo", | ||
159 | detail: "fn foo(&self)", | ||
160 | }, | ||
161 | CompletionItem { | ||
162 | label: "the_field", | ||
163 | source_range: [126; 126), | ||
164 | delete: [126; 126), | ||
165 | insert: "the_field", | ||
166 | kind: Field, | ||
167 | detail: "(u32, i32)", | ||
168 | }, | ||
169 | ] | ||
170 | "### | ||
171 | ); | ||
172 | } | ||
173 | |||
174 | #[test] | ||
175 | fn test_no_struct_field_completion_for_method_call() { | ||
176 | assert_debug_snapshot!( | ||
177 | do_ref_completion( | ||
178 | r" | ||
179 | struct A { the_field: u32 } | ||
180 | fn foo(a: A) { | ||
181 | a.<|>() | ||
182 | } | ||
183 | ", | ||
184 | ), | ||
185 | @"[]" | ||
186 | ); | ||
187 | } | ||
188 | |||
189 | #[test] | ||
190 | fn test_method_completion() { | ||
191 | assert_debug_snapshot!( | ||
192 | do_ref_completion( | ||
193 | r" | ||
194 | struct A {} | ||
195 | impl A { | ||
196 | fn the_method(&self) {} | ||
197 | } | ||
198 | fn foo(a: A) { | ||
199 | a.<|> | ||
200 | } | ||
201 | ", | ||
202 | ), | ||
203 | @r###" | ||
204 | [ | ||
205 | CompletionItem { | ||
206 | label: "the_method()", | ||
207 | source_range: [144; 144), | ||
208 | delete: [144; 144), | ||
209 | insert: "the_method()$0", | ||
210 | kind: Method, | ||
211 | lookup: "the_method", | ||
212 | detail: "fn the_method(&self)", | ||
213 | }, | ||
214 | ] | ||
215 | "### | ||
216 | ); | ||
217 | } | ||
218 | |||
219 | #[test] | ||
220 | fn test_trait_method_completion() { | ||
221 | assert_debug_snapshot!( | ||
222 | do_ref_completion( | ||
223 | r" | ||
224 | struct A {} | ||
225 | trait Trait { fn the_method(&self); } | ||
226 | impl Trait for A {} | ||
227 | fn foo(a: A) { | ||
228 | a.<|> | ||
229 | } | ||
230 | ", | ||
231 | ), | ||
232 | @r###" | ||
233 | [ | ||
234 | CompletionItem { | ||
235 | label: "the_method()", | ||
236 | source_range: [151; 151), | ||
237 | delete: [151; 151), | ||
238 | insert: "the_method()$0", | ||
239 | kind: Method, | ||
240 | lookup: "the_method", | ||
241 | detail: "fn the_method(&self)", | ||
242 | }, | ||
243 | ] | ||
244 | "### | ||
245 | ); | ||
246 | } | ||
247 | |||
248 | #[test] | ||
249 | fn test_trait_method_completion_deduplicated() { | ||
250 | assert_debug_snapshot!( | ||
251 | do_ref_completion( | ||
252 | r" | ||
253 | struct A {} | ||
254 | trait Trait { fn the_method(&self); } | ||
255 | impl<T> Trait for T {} | ||
256 | fn foo(a: &A) { | ||
257 | a.<|> | ||
258 | } | ||
259 | ", | ||
260 | ), | ||
261 | @r###" | ||
262 | [ | ||
263 | CompletionItem { | ||
264 | label: "the_method()", | ||
265 | source_range: [155; 155), | ||
266 | delete: [155; 155), | ||
267 | insert: "the_method()$0", | ||
268 | kind: Method, | ||
269 | lookup: "the_method", | ||
270 | detail: "fn the_method(&self)", | ||
271 | }, | ||
272 | ] | ||
273 | "### | ||
274 | ); | ||
275 | } | ||
276 | |||
277 | #[test] | ||
278 | fn test_no_non_self_method() { | ||
279 | assert_debug_snapshot!( | ||
280 | do_ref_completion( | ||
281 | r" | ||
282 | struct A {} | ||
283 | impl A { | ||
284 | fn the_method() {} | ||
285 | } | ||
286 | fn foo(a: A) { | ||
287 | a.<|> | ||
288 | } | ||
289 | ", | ||
290 | ), | ||
291 | @"[]" | ||
292 | ); | ||
293 | } | ||
294 | |||
295 | #[test] | ||
296 | fn test_method_attr_filtering() { | ||
297 | assert_debug_snapshot!( | ||
298 | do_ref_completion( | ||
299 | r" | ||
300 | struct A {} | ||
301 | impl A { | ||
302 | #[inline] | ||
303 | fn the_method(&self) { | ||
304 | let x = 1; | ||
305 | let y = 2; | ||
306 | } | ||
307 | } | ||
308 | fn foo(a: A) { | ||
309 | a.<|> | ||
310 | } | ||
311 | ", | ||
312 | ), | ||
313 | @r###" | ||
314 | [ | ||
315 | CompletionItem { | ||
316 | label: "the_method()", | ||
317 | source_range: [249; 249), | ||
318 | delete: [249; 249), | ||
319 | insert: "the_method()$0", | ||
320 | kind: Method, | ||
321 | lookup: "the_method", | ||
322 | detail: "fn the_method(&self)", | ||
323 | }, | ||
324 | ] | ||
325 | "### | ||
326 | ); | ||
327 | } | ||
328 | |||
329 | #[test] | ||
330 | fn test_tuple_field_completion() { | ||
331 | assert_debug_snapshot!( | ||
332 | do_ref_completion( | ||
333 | r" | ||
334 | fn foo() { | ||
335 | let b = (0, 3.14); | ||
336 | b.<|> | ||
337 | } | ||
338 | ", | ||
339 | ), | ||
340 | @r###" | ||
341 | [ | ||
342 | CompletionItem { | ||
343 | label: "0", | ||
344 | source_range: [75; 75), | ||
345 | delete: [75; 75), | ||
346 | insert: "0", | ||
347 | kind: Field, | ||
348 | detail: "i32", | ||
349 | }, | ||
350 | CompletionItem { | ||
351 | label: "1", | ||
352 | source_range: [75; 75), | ||
353 | delete: [75; 75), | ||
354 | insert: "1", | ||
355 | kind: Field, | ||
356 | detail: "f64", | ||
357 | }, | ||
358 | ] | ||
359 | "### | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn test_tuple_field_inference() { | ||
365 | assert_debug_snapshot!( | ||
366 | do_ref_completion( | ||
367 | r" | ||
368 | pub struct S; | ||
369 | impl S { | ||
370 | pub fn blah(&self) {} | ||
371 | } | ||
372 | |||
373 | struct T(S); | ||
374 | |||
375 | impl T { | ||
376 | fn foo(&self) { | ||
377 | // FIXME: This doesn't work without the trailing `a` as `0.` is a float | ||
378 | self.0.a<|> | ||
379 | } | ||
380 | } | ||
381 | ", | ||
382 | ), | ||
383 | @r###" | ||
384 | [ | ||
385 | CompletionItem { | ||
386 | label: "blah()", | ||
387 | source_range: [299; 300), | ||
388 | delete: [299; 300), | ||
389 | insert: "blah()$0", | ||
390 | kind: Method, | ||
391 | lookup: "blah", | ||
392 | detail: "pub fn blah(&self)", | ||
393 | }, | ||
394 | ] | ||
395 | "### | ||
396 | ); | ||
397 | } | ||
398 | |||
399 | #[test] | ||
400 | fn test_completion_works_in_consts() { | ||
401 | assert_debug_snapshot!( | ||
402 | do_ref_completion( | ||
403 | r" | ||
404 | struct A { the_field: u32 } | ||
405 | const X: u32 = { | ||
406 | A { the_field: 92 }.<|> | ||
407 | }; | ||
408 | ", | ||
409 | ), | ||
410 | @r###" | ||
411 | [ | ||
412 | CompletionItem { | ||
413 | label: "the_field", | ||
414 | source_range: [106; 106), | ||
415 | delete: [106; 106), | ||
416 | insert: "the_field", | ||
417 | kind: Field, | ||
418 | detail: "u32", | ||
419 | }, | ||
420 | ] | ||
421 | "### | ||
422 | ); | ||
423 | } | ||
424 | |||
425 | #[test] | ||
426 | fn test_completion_await_impls_future() { | ||
427 | assert_debug_snapshot!( | ||
428 | do_completion( | ||
429 | r###" | ||
430 | //- /main.rs | ||
431 | use std::future::*; | ||
432 | struct A {} | ||
433 | impl Future for A {} | ||
434 | fn foo(a: A) { | ||
435 | a.<|> | ||
436 | } | ||
437 | |||
438 | //- /std/lib.rs | ||
439 | pub mod future { | ||
440 | pub trait Future {} | ||
441 | } | ||
442 | "###, CompletionKind::Keyword), | ||
443 | @r###" | ||
444 | [ | ||
445 | CompletionItem { | ||
446 | label: "await", | ||
447 | source_range: [74; 74), | ||
448 | delete: [74; 74), | ||
449 | insert: "await", | ||
450 | detail: "expr.await", | ||
451 | }, | ||
452 | ] | ||
453 | "### | ||
454 | ) | ||
455 | } | ||
456 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_fn_param.rs b/crates/ra_ide/src/completion/complete_fn_param.rs new file mode 100644 index 000000000..502458706 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_fn_param.rs | |||
@@ -0,0 +1,136 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ast, match_ast, AstNode}; | ||
4 | use rustc_hash::FxHashMap; | ||
5 | |||
6 | use crate::completion::{CompletionContext, CompletionItem, CompletionKind, Completions}; | ||
7 | |||
8 | /// Complete repeated parameters, both name and type. For example, if all | ||
9 | /// functions in a file have a `spam: &mut Spam` parameter, a completion with | ||
10 | /// `spam: &mut Spam` insert text/label and `spam` lookup string will be | ||
11 | /// suggested. | ||
12 | pub(super) fn complete_fn_param(acc: &mut Completions, ctx: &CompletionContext) { | ||
13 | if !ctx.is_param { | ||
14 | return; | ||
15 | } | ||
16 | |||
17 | let mut params = FxHashMap::default(); | ||
18 | for node in ctx.token.parent().ancestors() { | ||
19 | match_ast! { | ||
20 | match node { | ||
21 | ast::SourceFile(it) => { process(it, &mut params) }, | ||
22 | ast::ItemList(it) => { process(it, &mut params) }, | ||
23 | _ => (), | ||
24 | } | ||
25 | } | ||
26 | } | ||
27 | params | ||
28 | .into_iter() | ||
29 | .filter_map(|(label, (count, param))| { | ||
30 | let lookup = param.pat()?.syntax().text().to_string(); | ||
31 | if count < 2 { | ||
32 | None | ||
33 | } else { | ||
34 | Some((label, lookup)) | ||
35 | } | ||
36 | }) | ||
37 | .for_each(|(label, lookup)| { | ||
38 | CompletionItem::new(CompletionKind::Magic, ctx.source_range(), label) | ||
39 | .lookup_by(lookup) | ||
40 | .add_to(acc) | ||
41 | }); | ||
42 | |||
43 | fn process<N: ast::FnDefOwner>(node: N, params: &mut FxHashMap<String, (u32, ast::Param)>) { | ||
44 | node.functions().filter_map(|it| it.param_list()).flat_map(|it| it.params()).for_each( | ||
45 | |param| { | ||
46 | let text = param.syntax().text().to_string(); | ||
47 | params.entry(text).or_insert((0, param)).0 += 1; | ||
48 | }, | ||
49 | ) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | #[cfg(test)] | ||
54 | mod tests { | ||
55 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
56 | use insta::assert_debug_snapshot; | ||
57 | |||
58 | fn do_magic_completion(code: &str) -> Vec<CompletionItem> { | ||
59 | do_completion(code, CompletionKind::Magic) | ||
60 | } | ||
61 | |||
62 | #[test] | ||
63 | fn test_param_completion_last_param() { | ||
64 | assert_debug_snapshot!( | ||
65 | do_magic_completion( | ||
66 | r" | ||
67 | fn foo(file_id: FileId) {} | ||
68 | fn bar(file_id: FileId) {} | ||
69 | fn baz(file<|>) {} | ||
70 | ", | ||
71 | ), | ||
72 | @r###" | ||
73 | [ | ||
74 | CompletionItem { | ||
75 | label: "file_id: FileId", | ||
76 | source_range: [110; 114), | ||
77 | delete: [110; 114), | ||
78 | insert: "file_id: FileId", | ||
79 | lookup: "file_id", | ||
80 | }, | ||
81 | ] | ||
82 | "### | ||
83 | ); | ||
84 | } | ||
85 | |||
86 | #[test] | ||
87 | fn test_param_completion_nth_param() { | ||
88 | assert_debug_snapshot!( | ||
89 | do_magic_completion( | ||
90 | r" | ||
91 | fn foo(file_id: FileId) {} | ||
92 | fn bar(file_id: FileId) {} | ||
93 | fn baz(file<|>, x: i32) {} | ||
94 | ", | ||
95 | ), | ||
96 | @r###" | ||
97 | [ | ||
98 | CompletionItem { | ||
99 | label: "file_id: FileId", | ||
100 | source_range: [110; 114), | ||
101 | delete: [110; 114), | ||
102 | insert: "file_id: FileId", | ||
103 | lookup: "file_id", | ||
104 | }, | ||
105 | ] | ||
106 | "### | ||
107 | ); | ||
108 | } | ||
109 | |||
110 | #[test] | ||
111 | fn test_param_completion_trait_param() { | ||
112 | assert_debug_snapshot!( | ||
113 | do_magic_completion( | ||
114 | r" | ||
115 | pub(crate) trait SourceRoot { | ||
116 | pub fn contains(&self, file_id: FileId) -> bool; | ||
117 | pub fn module_map(&self) -> &ModuleMap; | ||
118 | pub fn lines(&self, file_id: FileId) -> &LineIndex; | ||
119 | pub fn syntax(&self, file<|>) | ||
120 | } | ||
121 | ", | ||
122 | ), | ||
123 | @r###" | ||
124 | [ | ||
125 | CompletionItem { | ||
126 | label: "file_id: FileId", | ||
127 | source_range: [289; 293), | ||
128 | delete: [289; 293), | ||
129 | insert: "file_id: FileId", | ||
130 | lookup: "file_id", | ||
131 | }, | ||
132 | ] | ||
133 | "### | ||
134 | ); | ||
135 | } | ||
136 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_keyword.rs b/crates/ra_ide/src/completion/complete_keyword.rs new file mode 100644 index 000000000..eb7cd9ac2 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_keyword.rs | |||
@@ -0,0 +1,781 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | ast::{self, LoopBodyOwner}, | ||
5 | match_ast, AstNode, | ||
6 | SyntaxKind::*, | ||
7 | SyntaxToken, | ||
8 | }; | ||
9 | |||
10 | use crate::completion::{ | ||
11 | CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, | ||
12 | }; | ||
13 | |||
14 | pub(super) fn complete_use_tree_keyword(acc: &mut Completions, ctx: &CompletionContext) { | ||
15 | // complete keyword "crate" in use stmt | ||
16 | let source_range = ctx.source_range(); | ||
17 | match (ctx.use_item_syntax.as_ref(), ctx.path_prefix.as_ref()) { | ||
18 | (Some(_), None) => { | ||
19 | CompletionItem::new(CompletionKind::Keyword, source_range, "crate") | ||
20 | .kind(CompletionItemKind::Keyword) | ||
21 | .insert_text("crate::") | ||
22 | .add_to(acc); | ||
23 | CompletionItem::new(CompletionKind::Keyword, source_range, "self") | ||
24 | .kind(CompletionItemKind::Keyword) | ||
25 | .add_to(acc); | ||
26 | CompletionItem::new(CompletionKind::Keyword, source_range, "super") | ||
27 | .kind(CompletionItemKind::Keyword) | ||
28 | .insert_text("super::") | ||
29 | .add_to(acc); | ||
30 | } | ||
31 | (Some(_), Some(_)) => { | ||
32 | CompletionItem::new(CompletionKind::Keyword, source_range, "self") | ||
33 | .kind(CompletionItemKind::Keyword) | ||
34 | .add_to(acc); | ||
35 | CompletionItem::new(CompletionKind::Keyword, source_range, "super") | ||
36 | .kind(CompletionItemKind::Keyword) | ||
37 | .insert_text("super::") | ||
38 | .add_to(acc); | ||
39 | } | ||
40 | _ => {} | ||
41 | } | ||
42 | } | ||
43 | |||
44 | fn keyword(ctx: &CompletionContext, kw: &str, snippet: &str) -> CompletionItem { | ||
45 | CompletionItem::new(CompletionKind::Keyword, ctx.source_range(), kw) | ||
46 | .kind(CompletionItemKind::Keyword) | ||
47 | .insert_snippet(snippet) | ||
48 | .build() | ||
49 | } | ||
50 | |||
51 | pub(super) fn complete_expr_keyword(acc: &mut Completions, ctx: &CompletionContext) { | ||
52 | if !ctx.is_trivial_path { | ||
53 | return; | ||
54 | } | ||
55 | |||
56 | let fn_def = match &ctx.function_syntax { | ||
57 | Some(it) => it, | ||
58 | None => return, | ||
59 | }; | ||
60 | acc.add(keyword(ctx, "if", "if $0 {}")); | ||
61 | acc.add(keyword(ctx, "match", "match $0 {}")); | ||
62 | acc.add(keyword(ctx, "while", "while $0 {}")); | ||
63 | acc.add(keyword(ctx, "loop", "loop {$0}")); | ||
64 | |||
65 | if ctx.after_if { | ||
66 | acc.add(keyword(ctx, "else", "else {$0}")); | ||
67 | acc.add(keyword(ctx, "else if", "else if $0 {}")); | ||
68 | } | ||
69 | if is_in_loop_body(&ctx.token) { | ||
70 | if ctx.can_be_stmt { | ||
71 | acc.add(keyword(ctx, "continue", "continue;")); | ||
72 | acc.add(keyword(ctx, "break", "break;")); | ||
73 | } else { | ||
74 | acc.add(keyword(ctx, "continue", "continue")); | ||
75 | acc.add(keyword(ctx, "break", "break")); | ||
76 | } | ||
77 | } | ||
78 | acc.add_all(complete_return(ctx, &fn_def, ctx.can_be_stmt)); | ||
79 | } | ||
80 | |||
81 | fn is_in_loop_body(leaf: &SyntaxToken) -> bool { | ||
82 | for node in leaf.parent().ancestors() { | ||
83 | if node.kind() == FN_DEF || node.kind() == LAMBDA_EXPR { | ||
84 | break; | ||
85 | } | ||
86 | let loop_body = match_ast! { | ||
87 | match node { | ||
88 | ast::ForExpr(it) => { it.loop_body() }, | ||
89 | ast::WhileExpr(it) => { it.loop_body() }, | ||
90 | ast::LoopExpr(it) => { it.loop_body() }, | ||
91 | _ => None, | ||
92 | } | ||
93 | }; | ||
94 | if let Some(body) = loop_body { | ||
95 | if leaf.text_range().is_subrange(&body.syntax().text_range()) { | ||
96 | return true; | ||
97 | } | ||
98 | } | ||
99 | } | ||
100 | false | ||
101 | } | ||
102 | |||
103 | fn complete_return( | ||
104 | ctx: &CompletionContext, | ||
105 | fn_def: &ast::FnDef, | ||
106 | can_be_stmt: bool, | ||
107 | ) -> Option<CompletionItem> { | ||
108 | let snip = match (can_be_stmt, fn_def.ret_type().is_some()) { | ||
109 | (true, true) => "return $0;", | ||
110 | (true, false) => "return;", | ||
111 | (false, true) => "return $0", | ||
112 | (false, false) => "return", | ||
113 | }; | ||
114 | Some(keyword(ctx, "return", snip)) | ||
115 | } | ||
116 | |||
117 | #[cfg(test)] | ||
118 | mod tests { | ||
119 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
120 | use insta::assert_debug_snapshot; | ||
121 | |||
122 | fn do_keyword_completion(code: &str) -> Vec<CompletionItem> { | ||
123 | do_completion(code, CompletionKind::Keyword) | ||
124 | } | ||
125 | |||
126 | #[test] | ||
127 | fn completes_keywords_in_use_stmt() { | ||
128 | assert_debug_snapshot!( | ||
129 | do_keyword_completion( | ||
130 | r" | ||
131 | use <|> | ||
132 | ", | ||
133 | ), | ||
134 | @r###" | ||
135 | [ | ||
136 | CompletionItem { | ||
137 | label: "crate", | ||
138 | source_range: [21; 21), | ||
139 | delete: [21; 21), | ||
140 | insert: "crate::", | ||
141 | kind: Keyword, | ||
142 | }, | ||
143 | CompletionItem { | ||
144 | label: "self", | ||
145 | source_range: [21; 21), | ||
146 | delete: [21; 21), | ||
147 | insert: "self", | ||
148 | kind: Keyword, | ||
149 | }, | ||
150 | CompletionItem { | ||
151 | label: "super", | ||
152 | source_range: [21; 21), | ||
153 | delete: [21; 21), | ||
154 | insert: "super::", | ||
155 | kind: Keyword, | ||
156 | }, | ||
157 | ] | ||
158 | "### | ||
159 | ); | ||
160 | |||
161 | assert_debug_snapshot!( | ||
162 | do_keyword_completion( | ||
163 | r" | ||
164 | use a::<|> | ||
165 | ", | ||
166 | ), | ||
167 | @r###" | ||
168 | [ | ||
169 | CompletionItem { | ||
170 | label: "self", | ||
171 | source_range: [24; 24), | ||
172 | delete: [24; 24), | ||
173 | insert: "self", | ||
174 | kind: Keyword, | ||
175 | }, | ||
176 | CompletionItem { | ||
177 | label: "super", | ||
178 | source_range: [24; 24), | ||
179 | delete: [24; 24), | ||
180 | insert: "super::", | ||
181 | kind: Keyword, | ||
182 | }, | ||
183 | ] | ||
184 | "### | ||
185 | ); | ||
186 | |||
187 | assert_debug_snapshot!( | ||
188 | do_keyword_completion( | ||
189 | r" | ||
190 | use a::{b, <|>} | ||
191 | ", | ||
192 | ), | ||
193 | @r###" | ||
194 | [ | ||
195 | CompletionItem { | ||
196 | label: "self", | ||
197 | source_range: [28; 28), | ||
198 | delete: [28; 28), | ||
199 | insert: "self", | ||
200 | kind: Keyword, | ||
201 | }, | ||
202 | CompletionItem { | ||
203 | label: "super", | ||
204 | source_range: [28; 28), | ||
205 | delete: [28; 28), | ||
206 | insert: "super::", | ||
207 | kind: Keyword, | ||
208 | }, | ||
209 | ] | ||
210 | "### | ||
211 | ); | ||
212 | } | ||
213 | |||
214 | #[test] | ||
215 | fn completes_various_keywords_in_function() { | ||
216 | assert_debug_snapshot!( | ||
217 | do_keyword_completion( | ||
218 | r" | ||
219 | fn quux() { | ||
220 | <|> | ||
221 | } | ||
222 | ", | ||
223 | ), | ||
224 | @r###" | ||
225 | [ | ||
226 | CompletionItem { | ||
227 | label: "if", | ||
228 | source_range: [49; 49), | ||
229 | delete: [49; 49), | ||
230 | insert: "if $0 {}", | ||
231 | kind: Keyword, | ||
232 | }, | ||
233 | CompletionItem { | ||
234 | label: "loop", | ||
235 | source_range: [49; 49), | ||
236 | delete: [49; 49), | ||
237 | insert: "loop {$0}", | ||
238 | kind: Keyword, | ||
239 | }, | ||
240 | CompletionItem { | ||
241 | label: "match", | ||
242 | source_range: [49; 49), | ||
243 | delete: [49; 49), | ||
244 | insert: "match $0 {}", | ||
245 | kind: Keyword, | ||
246 | }, | ||
247 | CompletionItem { | ||
248 | label: "return", | ||
249 | source_range: [49; 49), | ||
250 | delete: [49; 49), | ||
251 | insert: "return;", | ||
252 | kind: Keyword, | ||
253 | }, | ||
254 | CompletionItem { | ||
255 | label: "while", | ||
256 | source_range: [49; 49), | ||
257 | delete: [49; 49), | ||
258 | insert: "while $0 {}", | ||
259 | kind: Keyword, | ||
260 | }, | ||
261 | ] | ||
262 | "### | ||
263 | ); | ||
264 | } | ||
265 | |||
266 | #[test] | ||
267 | fn completes_else_after_if() { | ||
268 | assert_debug_snapshot!( | ||
269 | do_keyword_completion( | ||
270 | r" | ||
271 | fn quux() { | ||
272 | if true { | ||
273 | () | ||
274 | } <|> | ||
275 | } | ||
276 | ", | ||
277 | ), | ||
278 | @r###" | ||
279 | [ | ||
280 | CompletionItem { | ||
281 | label: "else", | ||
282 | source_range: [108; 108), | ||
283 | delete: [108; 108), | ||
284 | insert: "else {$0}", | ||
285 | kind: Keyword, | ||
286 | }, | ||
287 | CompletionItem { | ||
288 | label: "else if", | ||
289 | source_range: [108; 108), | ||
290 | delete: [108; 108), | ||
291 | insert: "else if $0 {}", | ||
292 | kind: Keyword, | ||
293 | }, | ||
294 | CompletionItem { | ||
295 | label: "if", | ||
296 | source_range: [108; 108), | ||
297 | delete: [108; 108), | ||
298 | insert: "if $0 {}", | ||
299 | kind: Keyword, | ||
300 | }, | ||
301 | CompletionItem { | ||
302 | label: "loop", | ||
303 | source_range: [108; 108), | ||
304 | delete: [108; 108), | ||
305 | insert: "loop {$0}", | ||
306 | kind: Keyword, | ||
307 | }, | ||
308 | CompletionItem { | ||
309 | label: "match", | ||
310 | source_range: [108; 108), | ||
311 | delete: [108; 108), | ||
312 | insert: "match $0 {}", | ||
313 | kind: Keyword, | ||
314 | }, | ||
315 | CompletionItem { | ||
316 | label: "return", | ||
317 | source_range: [108; 108), | ||
318 | delete: [108; 108), | ||
319 | insert: "return;", | ||
320 | kind: Keyword, | ||
321 | }, | ||
322 | CompletionItem { | ||
323 | label: "while", | ||
324 | source_range: [108; 108), | ||
325 | delete: [108; 108), | ||
326 | insert: "while $0 {}", | ||
327 | kind: Keyword, | ||
328 | }, | ||
329 | ] | ||
330 | "### | ||
331 | ); | ||
332 | } | ||
333 | |||
334 | #[test] | ||
335 | fn test_completion_return_value() { | ||
336 | assert_debug_snapshot!( | ||
337 | do_keyword_completion( | ||
338 | r" | ||
339 | fn quux() -> i32 { | ||
340 | <|> | ||
341 | 92 | ||
342 | } | ||
343 | ", | ||
344 | ), | ||
345 | @r###" | ||
346 | [ | ||
347 | CompletionItem { | ||
348 | label: "if", | ||
349 | source_range: [56; 56), | ||
350 | delete: [56; 56), | ||
351 | insert: "if $0 {}", | ||
352 | kind: Keyword, | ||
353 | }, | ||
354 | CompletionItem { | ||
355 | label: "loop", | ||
356 | source_range: [56; 56), | ||
357 | delete: [56; 56), | ||
358 | insert: "loop {$0}", | ||
359 | kind: Keyword, | ||
360 | }, | ||
361 | CompletionItem { | ||
362 | label: "match", | ||
363 | source_range: [56; 56), | ||
364 | delete: [56; 56), | ||
365 | insert: "match $0 {}", | ||
366 | kind: Keyword, | ||
367 | }, | ||
368 | CompletionItem { | ||
369 | label: "return", | ||
370 | source_range: [56; 56), | ||
371 | delete: [56; 56), | ||
372 | insert: "return $0;", | ||
373 | kind: Keyword, | ||
374 | }, | ||
375 | CompletionItem { | ||
376 | label: "while", | ||
377 | source_range: [56; 56), | ||
378 | delete: [56; 56), | ||
379 | insert: "while $0 {}", | ||
380 | kind: Keyword, | ||
381 | }, | ||
382 | ] | ||
383 | "### | ||
384 | ); | ||
385 | assert_debug_snapshot!( | ||
386 | do_keyword_completion( | ||
387 | r" | ||
388 | fn quux() { | ||
389 | <|> | ||
390 | 92 | ||
391 | } | ||
392 | ", | ||
393 | ), | ||
394 | @r###" | ||
395 | [ | ||
396 | CompletionItem { | ||
397 | label: "if", | ||
398 | source_range: [49; 49), | ||
399 | delete: [49; 49), | ||
400 | insert: "if $0 {}", | ||
401 | kind: Keyword, | ||
402 | }, | ||
403 | CompletionItem { | ||
404 | label: "loop", | ||
405 | source_range: [49; 49), | ||
406 | delete: [49; 49), | ||
407 | insert: "loop {$0}", | ||
408 | kind: Keyword, | ||
409 | }, | ||
410 | CompletionItem { | ||
411 | label: "match", | ||
412 | source_range: [49; 49), | ||
413 | delete: [49; 49), | ||
414 | insert: "match $0 {}", | ||
415 | kind: Keyword, | ||
416 | }, | ||
417 | CompletionItem { | ||
418 | label: "return", | ||
419 | source_range: [49; 49), | ||
420 | delete: [49; 49), | ||
421 | insert: "return;", | ||
422 | kind: Keyword, | ||
423 | }, | ||
424 | CompletionItem { | ||
425 | label: "while", | ||
426 | source_range: [49; 49), | ||
427 | delete: [49; 49), | ||
428 | insert: "while $0 {}", | ||
429 | kind: Keyword, | ||
430 | }, | ||
431 | ] | ||
432 | "### | ||
433 | ); | ||
434 | } | ||
435 | |||
436 | #[test] | ||
437 | fn dont_add_semi_after_return_if_not_a_statement() { | ||
438 | assert_debug_snapshot!( | ||
439 | do_keyword_completion( | ||
440 | r" | ||
441 | fn quux() -> i32 { | ||
442 | match () { | ||
443 | () => <|> | ||
444 | } | ||
445 | } | ||
446 | ", | ||
447 | ), | ||
448 | @r###" | ||
449 | [ | ||
450 | CompletionItem { | ||
451 | label: "if", | ||
452 | source_range: [97; 97), | ||
453 | delete: [97; 97), | ||
454 | insert: "if $0 {}", | ||
455 | kind: Keyword, | ||
456 | }, | ||
457 | CompletionItem { | ||
458 | label: "loop", | ||
459 | source_range: [97; 97), | ||
460 | delete: [97; 97), | ||
461 | insert: "loop {$0}", | ||
462 | kind: Keyword, | ||
463 | }, | ||
464 | CompletionItem { | ||
465 | label: "match", | ||
466 | source_range: [97; 97), | ||
467 | delete: [97; 97), | ||
468 | insert: "match $0 {}", | ||
469 | kind: Keyword, | ||
470 | }, | ||
471 | CompletionItem { | ||
472 | label: "return", | ||
473 | source_range: [97; 97), | ||
474 | delete: [97; 97), | ||
475 | insert: "return $0", | ||
476 | kind: Keyword, | ||
477 | }, | ||
478 | CompletionItem { | ||
479 | label: "while", | ||
480 | source_range: [97; 97), | ||
481 | delete: [97; 97), | ||
482 | insert: "while $0 {}", | ||
483 | kind: Keyword, | ||
484 | }, | ||
485 | ] | ||
486 | "### | ||
487 | ); | ||
488 | } | ||
489 | |||
490 | #[test] | ||
491 | fn last_return_in_block_has_semi() { | ||
492 | assert_debug_snapshot!( | ||
493 | do_keyword_completion( | ||
494 | r" | ||
495 | fn quux() -> i32 { | ||
496 | if condition { | ||
497 | <|> | ||
498 | } | ||
499 | } | ||
500 | ", | ||
501 | ), | ||
502 | @r###" | ||
503 | [ | ||
504 | CompletionItem { | ||
505 | label: "if", | ||
506 | source_range: [95; 95), | ||
507 | delete: [95; 95), | ||
508 | insert: "if $0 {}", | ||
509 | kind: Keyword, | ||
510 | }, | ||
511 | CompletionItem { | ||
512 | label: "loop", | ||
513 | source_range: [95; 95), | ||
514 | delete: [95; 95), | ||
515 | insert: "loop {$0}", | ||
516 | kind: Keyword, | ||
517 | }, | ||
518 | CompletionItem { | ||
519 | label: "match", | ||
520 | source_range: [95; 95), | ||
521 | delete: [95; 95), | ||
522 | insert: "match $0 {}", | ||
523 | kind: Keyword, | ||
524 | }, | ||
525 | CompletionItem { | ||
526 | label: "return", | ||
527 | source_range: [95; 95), | ||
528 | delete: [95; 95), | ||
529 | insert: "return $0;", | ||
530 | kind: Keyword, | ||
531 | }, | ||
532 | CompletionItem { | ||
533 | label: "while", | ||
534 | source_range: [95; 95), | ||
535 | delete: [95; 95), | ||
536 | insert: "while $0 {}", | ||
537 | kind: Keyword, | ||
538 | }, | ||
539 | ] | ||
540 | "### | ||
541 | ); | ||
542 | assert_debug_snapshot!( | ||
543 | do_keyword_completion( | ||
544 | r" | ||
545 | fn quux() -> i32 { | ||
546 | if condition { | ||
547 | <|> | ||
548 | } | ||
549 | let x = 92; | ||
550 | x | ||
551 | } | ||
552 | ", | ||
553 | ), | ||
554 | @r###" | ||
555 | [ | ||
556 | CompletionItem { | ||
557 | label: "if", | ||
558 | source_range: [95; 95), | ||
559 | delete: [95; 95), | ||
560 | insert: "if $0 {}", | ||
561 | kind: Keyword, | ||
562 | }, | ||
563 | CompletionItem { | ||
564 | label: "loop", | ||
565 | source_range: [95; 95), | ||
566 | delete: [95; 95), | ||
567 | insert: "loop {$0}", | ||
568 | kind: Keyword, | ||
569 | }, | ||
570 | CompletionItem { | ||
571 | label: "match", | ||
572 | source_range: [95; 95), | ||
573 | delete: [95; 95), | ||
574 | insert: "match $0 {}", | ||
575 | kind: Keyword, | ||
576 | }, | ||
577 | CompletionItem { | ||
578 | label: "return", | ||
579 | source_range: [95; 95), | ||
580 | delete: [95; 95), | ||
581 | insert: "return $0;", | ||
582 | kind: Keyword, | ||
583 | }, | ||
584 | CompletionItem { | ||
585 | label: "while", | ||
586 | source_range: [95; 95), | ||
587 | delete: [95; 95), | ||
588 | insert: "while $0 {}", | ||
589 | kind: Keyword, | ||
590 | }, | ||
591 | ] | ||
592 | "### | ||
593 | ); | ||
594 | } | ||
595 | |||
596 | #[test] | ||
597 | fn completes_break_and_continue_in_loops() { | ||
598 | assert_debug_snapshot!( | ||
599 | do_keyword_completion( | ||
600 | r" | ||
601 | fn quux() -> i32 { | ||
602 | loop { <|> } | ||
603 | } | ||
604 | ", | ||
605 | ), | ||
606 | @r###" | ||
607 | [ | ||
608 | CompletionItem { | ||
609 | label: "break", | ||
610 | source_range: [63; 63), | ||
611 | delete: [63; 63), | ||
612 | insert: "break;", | ||
613 | kind: Keyword, | ||
614 | }, | ||
615 | CompletionItem { | ||
616 | label: "continue", | ||
617 | source_range: [63; 63), | ||
618 | delete: [63; 63), | ||
619 | insert: "continue;", | ||
620 | kind: Keyword, | ||
621 | }, | ||
622 | CompletionItem { | ||
623 | label: "if", | ||
624 | source_range: [63; 63), | ||
625 | delete: [63; 63), | ||
626 | insert: "if $0 {}", | ||
627 | kind: Keyword, | ||
628 | }, | ||
629 | CompletionItem { | ||
630 | label: "loop", | ||
631 | source_range: [63; 63), | ||
632 | delete: [63; 63), | ||
633 | insert: "loop {$0}", | ||
634 | kind: Keyword, | ||
635 | }, | ||
636 | CompletionItem { | ||
637 | label: "match", | ||
638 | source_range: [63; 63), | ||
639 | delete: [63; 63), | ||
640 | insert: "match $0 {}", | ||
641 | kind: Keyword, | ||
642 | }, | ||
643 | CompletionItem { | ||
644 | label: "return", | ||
645 | source_range: [63; 63), | ||
646 | delete: [63; 63), | ||
647 | insert: "return $0;", | ||
648 | kind: Keyword, | ||
649 | }, | ||
650 | CompletionItem { | ||
651 | label: "while", | ||
652 | source_range: [63; 63), | ||
653 | delete: [63; 63), | ||
654 | insert: "while $0 {}", | ||
655 | kind: Keyword, | ||
656 | }, | ||
657 | ] | ||
658 | "### | ||
659 | ); | ||
660 | |||
661 | // No completion: lambda isolates control flow | ||
662 | assert_debug_snapshot!( | ||
663 | do_keyword_completion( | ||
664 | r" | ||
665 | fn quux() -> i32 { | ||
666 | loop { || { <|> } } | ||
667 | } | ||
668 | ", | ||
669 | ), | ||
670 | @r###" | ||
671 | [ | ||
672 | CompletionItem { | ||
673 | label: "if", | ||
674 | source_range: [68; 68), | ||
675 | delete: [68; 68), | ||
676 | insert: "if $0 {}", | ||
677 | kind: Keyword, | ||
678 | }, | ||
679 | CompletionItem { | ||
680 | label: "loop", | ||
681 | source_range: [68; 68), | ||
682 | delete: [68; 68), | ||
683 | insert: "loop {$0}", | ||
684 | kind: Keyword, | ||
685 | }, | ||
686 | CompletionItem { | ||
687 | label: "match", | ||
688 | source_range: [68; 68), | ||
689 | delete: [68; 68), | ||
690 | insert: "match $0 {}", | ||
691 | kind: Keyword, | ||
692 | }, | ||
693 | CompletionItem { | ||
694 | label: "return", | ||
695 | source_range: [68; 68), | ||
696 | delete: [68; 68), | ||
697 | insert: "return $0;", | ||
698 | kind: Keyword, | ||
699 | }, | ||
700 | CompletionItem { | ||
701 | label: "while", | ||
702 | source_range: [68; 68), | ||
703 | delete: [68; 68), | ||
704 | insert: "while $0 {}", | ||
705 | kind: Keyword, | ||
706 | }, | ||
707 | ] | ||
708 | "### | ||
709 | ); | ||
710 | } | ||
711 | |||
712 | #[test] | ||
713 | fn no_semi_after_break_continue_in_expr() { | ||
714 | assert_debug_snapshot!( | ||
715 | do_keyword_completion( | ||
716 | r" | ||
717 | fn f() { | ||
718 | loop { | ||
719 | match () { | ||
720 | () => br<|> | ||
721 | } | ||
722 | } | ||
723 | } | ||
724 | ", | ||
725 | ), | ||
726 | @r###" | ||
727 | [ | ||
728 | CompletionItem { | ||
729 | label: "break", | ||
730 | source_range: [122; 124), | ||
731 | delete: [122; 124), | ||
732 | insert: "break", | ||
733 | kind: Keyword, | ||
734 | }, | ||
735 | CompletionItem { | ||
736 | label: "continue", | ||
737 | source_range: [122; 124), | ||
738 | delete: [122; 124), | ||
739 | insert: "continue", | ||
740 | kind: Keyword, | ||
741 | }, | ||
742 | CompletionItem { | ||
743 | label: "if", | ||
744 | source_range: [122; 124), | ||
745 | delete: [122; 124), | ||
746 | insert: "if $0 {}", | ||
747 | kind: Keyword, | ||
748 | }, | ||
749 | CompletionItem { | ||
750 | label: "loop", | ||
751 | source_range: [122; 124), | ||
752 | delete: [122; 124), | ||
753 | insert: "loop {$0}", | ||
754 | kind: Keyword, | ||
755 | }, | ||
756 | CompletionItem { | ||
757 | label: "match", | ||
758 | source_range: [122; 124), | ||
759 | delete: [122; 124), | ||
760 | insert: "match $0 {}", | ||
761 | kind: Keyword, | ||
762 | }, | ||
763 | CompletionItem { | ||
764 | label: "return", | ||
765 | source_range: [122; 124), | ||
766 | delete: [122; 124), | ||
767 | insert: "return", | ||
768 | kind: Keyword, | ||
769 | }, | ||
770 | CompletionItem { | ||
771 | label: "while", | ||
772 | source_range: [122; 124), | ||
773 | delete: [122; 124), | ||
774 | insert: "while $0 {}", | ||
775 | kind: Keyword, | ||
776 | }, | ||
777 | ] | ||
778 | "### | ||
779 | ) | ||
780 | } | ||
781 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_macro_in_item_position.rs b/crates/ra_ide/src/completion/complete_macro_in_item_position.rs new file mode 100644 index 000000000..faadd1e3f --- /dev/null +++ b/crates/ra_ide/src/completion/complete_macro_in_item_position.rs | |||
@@ -0,0 +1,143 @@ | |||
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.analyzer.process_all_names(ctx.db, &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 crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
19 | use insta::assert_debug_snapshot; | ||
20 | |||
21 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { | ||
22 | do_completion(code, CompletionKind::Reference) | ||
23 | } | ||
24 | |||
25 | #[test] | ||
26 | fn completes_macros_as_item() { | ||
27 | assert_debug_snapshot!( | ||
28 | do_reference_completion( | ||
29 | " | ||
30 | //- /main.rs | ||
31 | macro_rules! foo { | ||
32 | () => {} | ||
33 | } | ||
34 | |||
35 | fn foo() {} | ||
36 | |||
37 | <|> | ||
38 | " | ||
39 | ), | ||
40 | @r###" | ||
41 | [ | ||
42 | CompletionItem { | ||
43 | label: "foo!", | ||
44 | source_range: [46; 46), | ||
45 | delete: [46; 46), | ||
46 | insert: "foo!($0)", | ||
47 | kind: Macro, | ||
48 | detail: "macro_rules! foo", | ||
49 | }, | ||
50 | ] | ||
51 | "### | ||
52 | ); | ||
53 | } | ||
54 | |||
55 | #[test] | ||
56 | fn completes_vec_macros_with_square_brackets() { | ||
57 | assert_debug_snapshot!( | ||
58 | do_reference_completion( | ||
59 | " | ||
60 | //- /main.rs | ||
61 | /// Creates a [`Vec`] containing the arguments. | ||
62 | /// | ||
63 | /// - Create a [`Vec`] containing a given list of elements: | ||
64 | /// | ||
65 | /// ``` | ||
66 | /// let v = vec![1, 2, 3]; | ||
67 | /// assert_eq!(v[0], 1); | ||
68 | /// assert_eq!(v[1], 2); | ||
69 | /// assert_eq!(v[2], 3); | ||
70 | /// ``` | ||
71 | macro_rules! vec { | ||
72 | () => {} | ||
73 | } | ||
74 | |||
75 | fn foo() {} | ||
76 | |||
77 | <|> | ||
78 | " | ||
79 | ), | ||
80 | @r###" | ||
81 | [ | ||
82 | CompletionItem { | ||
83 | label: "vec!", | ||
84 | source_range: [280; 280), | ||
85 | delete: [280; 280), | ||
86 | insert: "vec![$0]", | ||
87 | kind: Macro, | ||
88 | detail: "macro_rules! vec", | ||
89 | documentation: Documentation( | ||
90 | "Creates a [`Vec`] containing the arguments.\n\n- Create a [`Vec`] containing a given list of elements:\n\n```\nlet v = vec![1, 2, 3];\nassert_eq!(v[0], 1);\nassert_eq!(v[1], 2);\nassert_eq!(v[2], 3);\n```", | ||
91 | ), | ||
92 | }, | ||
93 | ] | ||
94 | "### | ||
95 | ); | ||
96 | } | ||
97 | |||
98 | #[test] | ||
99 | fn completes_macros_braces_guessing() { | ||
100 | assert_debug_snapshot!( | ||
101 | do_reference_completion( | ||
102 | " | ||
103 | //- /main.rs | ||
104 | /// Foo | ||
105 | /// | ||
106 | /// Not call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`. | ||
107 | /// Call as `let _=foo! { hello world };` | ||
108 | macro_rules! foo { | ||
109 | () => {} | ||
110 | } | ||
111 | |||
112 | fn main() { | ||
113 | <|> | ||
114 | } | ||
115 | " | ||
116 | ), | ||
117 | @r###" | ||
118 | [ | ||
119 | CompletionItem { | ||
120 | label: "foo!", | ||
121 | source_range: [163; 163), | ||
122 | delete: [163; 163), | ||
123 | insert: "foo! {$0}", | ||
124 | kind: Macro, | ||
125 | detail: "macro_rules! foo", | ||
126 | documentation: Documentation( | ||
127 | "Foo\n\nNot call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`.\nCall as `let _=foo! { hello world };`", | ||
128 | ), | ||
129 | }, | ||
130 | CompletionItem { | ||
131 | label: "main()", | ||
132 | source_range: [163; 163), | ||
133 | delete: [163; 163), | ||
134 | insert: "main()$0", | ||
135 | kind: Function, | ||
136 | lookup: "main", | ||
137 | detail: "fn main()", | ||
138 | }, | ||
139 | ] | ||
140 | "### | ||
141 | ); | ||
142 | } | ||
143 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_path.rs b/crates/ra_ide/src/completion/complete_path.rs new file mode 100644 index 000000000..89e0009a1 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_path.rs | |||
@@ -0,0 +1,785 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::{Adt, Either, HasSource, PathResolution}; | ||
4 | use ra_syntax::AstNode; | ||
5 | use test_utils::tested_by; | ||
6 | |||
7 | use crate::completion::{CompletionContext, Completions}; | ||
8 | |||
9 | pub(super) fn complete_path(acc: &mut Completions, ctx: &CompletionContext) { | ||
10 | let path = match &ctx.path_prefix { | ||
11 | Some(path) => path.clone(), | ||
12 | _ => return, | ||
13 | }; | ||
14 | let def = match ctx.analyzer.resolve_hir_path(ctx.db, &path) { | ||
15 | Some(PathResolution::Def(def)) => def, | ||
16 | _ => return, | ||
17 | }; | ||
18 | match def { | ||
19 | hir::ModuleDef::Module(module) => { | ||
20 | let module_scope = module.scope(ctx.db); | ||
21 | for (name, def, import) in module_scope { | ||
22 | if let hir::ScopeDef::ModuleDef(hir::ModuleDef::BuiltinType(..)) = def { | ||
23 | if ctx.use_item_syntax.is_some() { | ||
24 | tested_by!(dont_complete_primitive_in_use); | ||
25 | continue; | ||
26 | } | ||
27 | } | ||
28 | if Some(module) == ctx.module { | ||
29 | if let Some(import) = import { | ||
30 | if let Either::A(use_tree) = import.source(ctx.db).value { | ||
31 | if use_tree.syntax().text_range().contains_inclusive(ctx.offset) { | ||
32 | // for `use self::foo<|>`, don't suggest `foo` as a completion | ||
33 | tested_by!(dont_complete_current_use); | ||
34 | continue; | ||
35 | } | ||
36 | } | ||
37 | } | ||
38 | } | ||
39 | acc.add_resolution(ctx, name.to_string(), &def); | ||
40 | } | ||
41 | } | ||
42 | hir::ModuleDef::Adt(_) | hir::ModuleDef::TypeAlias(_) => { | ||
43 | if let hir::ModuleDef::Adt(Adt::Enum(e)) = def { | ||
44 | for variant in e.variants(ctx.db) { | ||
45 | acc.add_enum_variant(ctx, variant); | ||
46 | } | ||
47 | } | ||
48 | let ty = match def { | ||
49 | hir::ModuleDef::Adt(adt) => adt.ty(ctx.db), | ||
50 | hir::ModuleDef::TypeAlias(a) => a.ty(ctx.db), | ||
51 | _ => unreachable!(), | ||
52 | }; | ||
53 | ctx.analyzer.iterate_path_candidates(ctx.db, &ty, None, |_ty, item| { | ||
54 | match item { | ||
55 | hir::AssocItem::Function(func) => { | ||
56 | if !func.has_self_param(ctx.db) { | ||
57 | acc.add_function(ctx, func); | ||
58 | } | ||
59 | } | ||
60 | hir::AssocItem::Const(ct) => acc.add_const(ctx, ct), | ||
61 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
62 | } | ||
63 | None::<()> | ||
64 | }); | ||
65 | // Iterate assoc types separately | ||
66 | // FIXME: complete T::AssocType | ||
67 | let krate = ctx.module.map(|m| m.krate()); | ||
68 | if let Some(krate) = krate { | ||
69 | ty.iterate_impl_items(ctx.db, krate, |item| { | ||
70 | match item { | ||
71 | hir::AssocItem::Function(_) | hir::AssocItem::Const(_) => {} | ||
72 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
73 | } | ||
74 | None::<()> | ||
75 | }); | ||
76 | } | ||
77 | } | ||
78 | hir::ModuleDef::Trait(t) => { | ||
79 | for item in t.items(ctx.db) { | ||
80 | match item { | ||
81 | hir::AssocItem::Function(func) => { | ||
82 | if !func.has_self_param(ctx.db) { | ||
83 | acc.add_function(ctx, func); | ||
84 | } | ||
85 | } | ||
86 | hir::AssocItem::Const(ct) => acc.add_const(ctx, ct), | ||
87 | hir::AssocItem::TypeAlias(ty) => acc.add_type_alias(ctx, ty), | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | _ => {} | ||
92 | }; | ||
93 | } | ||
94 | |||
95 | #[cfg(test)] | ||
96 | mod tests { | ||
97 | use test_utils::covers; | ||
98 | |||
99 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
100 | use insta::assert_debug_snapshot; | ||
101 | |||
102 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { | ||
103 | do_completion(code, CompletionKind::Reference) | ||
104 | } | ||
105 | |||
106 | #[test] | ||
107 | fn dont_complete_current_use() { | ||
108 | covers!(dont_complete_current_use); | ||
109 | let completions = do_completion(r"use self::foo<|>;", CompletionKind::Reference); | ||
110 | assert!(completions.is_empty()); | ||
111 | } | ||
112 | |||
113 | #[test] | ||
114 | fn dont_complete_current_use_in_braces_with_glob() { | ||
115 | let completions = do_completion( | ||
116 | r" | ||
117 | mod foo { pub struct S; } | ||
118 | use self::{foo::*, bar<|>}; | ||
119 | ", | ||
120 | CompletionKind::Reference, | ||
121 | ); | ||
122 | assert_eq!(completions.len(), 2); | ||
123 | } | ||
124 | |||
125 | #[test] | ||
126 | fn dont_complete_primitive_in_use() { | ||
127 | covers!(dont_complete_primitive_in_use); | ||
128 | let completions = do_completion(r"use self::<|>;", CompletionKind::BuiltinType); | ||
129 | assert!(completions.is_empty()); | ||
130 | } | ||
131 | |||
132 | #[test] | ||
133 | fn completes_primitives() { | ||
134 | let completions = | ||
135 | do_completion(r"fn main() { let _: <|> = 92; }", CompletionKind::BuiltinType); | ||
136 | assert_eq!(completions.len(), 17); | ||
137 | } | ||
138 | |||
139 | #[test] | ||
140 | fn completes_mod_with_docs() { | ||
141 | assert_debug_snapshot!( | ||
142 | do_reference_completion( | ||
143 | r" | ||
144 | use self::my<|>; | ||
145 | |||
146 | /// Some simple | ||
147 | /// docs describing `mod my`. | ||
148 | mod my { | ||
149 | struct Bar; | ||
150 | } | ||
151 | " | ||
152 | ), | ||
153 | @r###" | ||
154 | [ | ||
155 | CompletionItem { | ||
156 | label: "my", | ||
157 | source_range: [27; 29), | ||
158 | delete: [27; 29), | ||
159 | insert: "my", | ||
160 | kind: Module, | ||
161 | documentation: Documentation( | ||
162 | "Some simple\ndocs describing `mod my`.", | ||
163 | ), | ||
164 | }, | ||
165 | ] | ||
166 | "### | ||
167 | ); | ||
168 | } | ||
169 | |||
170 | #[test] | ||
171 | fn completes_use_item_starting_with_self() { | ||
172 | assert_debug_snapshot!( | ||
173 | do_reference_completion( | ||
174 | r" | ||
175 | use self::m::<|>; | ||
176 | |||
177 | mod m { | ||
178 | struct Bar; | ||
179 | } | ||
180 | " | ||
181 | ), | ||
182 | @r###" | ||
183 | [ | ||
184 | CompletionItem { | ||
185 | label: "Bar", | ||
186 | source_range: [30; 30), | ||
187 | delete: [30; 30), | ||
188 | insert: "Bar", | ||
189 | kind: Struct, | ||
190 | }, | ||
191 | ] | ||
192 | "### | ||
193 | ); | ||
194 | } | ||
195 | |||
196 | #[test] | ||
197 | fn completes_use_item_starting_with_crate() { | ||
198 | assert_debug_snapshot!( | ||
199 | do_reference_completion( | ||
200 | " | ||
201 | //- /lib.rs | ||
202 | mod foo; | ||
203 | struct Spam; | ||
204 | //- /foo.rs | ||
205 | use crate::Sp<|> | ||
206 | " | ||
207 | ), | ||
208 | @r###" | ||
209 | [ | ||
210 | CompletionItem { | ||
211 | label: "Spam", | ||
212 | source_range: [11; 13), | ||
213 | delete: [11; 13), | ||
214 | insert: "Spam", | ||
215 | kind: Struct, | ||
216 | }, | ||
217 | CompletionItem { | ||
218 | label: "foo", | ||
219 | source_range: [11; 13), | ||
220 | delete: [11; 13), | ||
221 | insert: "foo", | ||
222 | kind: Module, | ||
223 | }, | ||
224 | ] | ||
225 | "### | ||
226 | ); | ||
227 | } | ||
228 | |||
229 | #[test] | ||
230 | fn completes_nested_use_tree() { | ||
231 | assert_debug_snapshot!( | ||
232 | do_reference_completion( | ||
233 | " | ||
234 | //- /lib.rs | ||
235 | mod foo; | ||
236 | struct Spam; | ||
237 | //- /foo.rs | ||
238 | use crate::{Sp<|>}; | ||
239 | " | ||
240 | ), | ||
241 | @r###" | ||
242 | [ | ||
243 | CompletionItem { | ||
244 | label: "Spam", | ||
245 | source_range: [12; 14), | ||
246 | delete: [12; 14), | ||
247 | insert: "Spam", | ||
248 | kind: Struct, | ||
249 | }, | ||
250 | CompletionItem { | ||
251 | label: "foo", | ||
252 | source_range: [12; 14), | ||
253 | delete: [12; 14), | ||
254 | insert: "foo", | ||
255 | kind: Module, | ||
256 | }, | ||
257 | ] | ||
258 | "### | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | #[test] | ||
263 | fn completes_deeply_nested_use_tree() { | ||
264 | assert_debug_snapshot!( | ||
265 | do_reference_completion( | ||
266 | " | ||
267 | //- /lib.rs | ||
268 | mod foo; | ||
269 | pub mod bar { | ||
270 | pub mod baz { | ||
271 | pub struct Spam; | ||
272 | } | ||
273 | } | ||
274 | //- /foo.rs | ||
275 | use crate::{bar::{baz::Sp<|>}}; | ||
276 | " | ||
277 | ), | ||
278 | @r###" | ||
279 | [ | ||
280 | CompletionItem { | ||
281 | label: "Spam", | ||
282 | source_range: [23; 25), | ||
283 | delete: [23; 25), | ||
284 | insert: "Spam", | ||
285 | kind: Struct, | ||
286 | }, | ||
287 | ] | ||
288 | "### | ||
289 | ); | ||
290 | } | ||
291 | |||
292 | #[test] | ||
293 | fn completes_enum_variant() { | ||
294 | assert_debug_snapshot!( | ||
295 | do_reference_completion( | ||
296 | " | ||
297 | //- /lib.rs | ||
298 | /// An enum | ||
299 | enum E { | ||
300 | /// Foo Variant | ||
301 | Foo, | ||
302 | /// Bar Variant with i32 | ||
303 | Bar(i32) | ||
304 | } | ||
305 | fn foo() { let _ = E::<|> } | ||
306 | " | ||
307 | ), | ||
308 | @r###" | ||
309 | [ | ||
310 | CompletionItem { | ||
311 | label: "Bar", | ||
312 | source_range: [116; 116), | ||
313 | delete: [116; 116), | ||
314 | insert: "Bar", | ||
315 | kind: EnumVariant, | ||
316 | detail: "(i32)", | ||
317 | documentation: Documentation( | ||
318 | "Bar Variant with i32", | ||
319 | ), | ||
320 | }, | ||
321 | CompletionItem { | ||
322 | label: "Foo", | ||
323 | source_range: [116; 116), | ||
324 | delete: [116; 116), | ||
325 | insert: "Foo", | ||
326 | kind: EnumVariant, | ||
327 | detail: "()", | ||
328 | documentation: Documentation( | ||
329 | "Foo Variant", | ||
330 | ), | ||
331 | }, | ||
332 | ] | ||
333 | "### | ||
334 | ); | ||
335 | } | ||
336 | |||
337 | #[test] | ||
338 | fn completes_enum_variant_with_details() { | ||
339 | assert_debug_snapshot!( | ||
340 | do_reference_completion( | ||
341 | " | ||
342 | //- /lib.rs | ||
343 | struct S { field: u32 } | ||
344 | /// An enum | ||
345 | enum E { | ||
346 | /// Foo Variant (empty) | ||
347 | Foo, | ||
348 | /// Bar Variant with i32 and u32 | ||
349 | Bar(i32, u32), | ||
350 | /// | ||
351 | S(S), | ||
352 | } | ||
353 | fn foo() { let _ = E::<|> } | ||
354 | " | ||
355 | ), | ||
356 | @r###" | ||
357 | [ | ||
358 | CompletionItem { | ||
359 | label: "Bar", | ||
360 | source_range: [180; 180), | ||
361 | delete: [180; 180), | ||
362 | insert: "Bar", | ||
363 | kind: EnumVariant, | ||
364 | detail: "(i32, u32)", | ||
365 | documentation: Documentation( | ||
366 | "Bar Variant with i32 and u32", | ||
367 | ), | ||
368 | }, | ||
369 | CompletionItem { | ||
370 | label: "Foo", | ||
371 | source_range: [180; 180), | ||
372 | delete: [180; 180), | ||
373 | insert: "Foo", | ||
374 | kind: EnumVariant, | ||
375 | detail: "()", | ||
376 | documentation: Documentation( | ||
377 | "Foo Variant (empty)", | ||
378 | ), | ||
379 | }, | ||
380 | CompletionItem { | ||
381 | label: "S", | ||
382 | source_range: [180; 180), | ||
383 | delete: [180; 180), | ||
384 | insert: "S", | ||
385 | kind: EnumVariant, | ||
386 | detail: "(S)", | ||
387 | documentation: Documentation( | ||
388 | "", | ||
389 | ), | ||
390 | }, | ||
391 | ] | ||
392 | "### | ||
393 | ); | ||
394 | } | ||
395 | |||
396 | #[test] | ||
397 | fn completes_struct_associated_method() { | ||
398 | assert_debug_snapshot!( | ||
399 | do_reference_completion( | ||
400 | " | ||
401 | //- /lib.rs | ||
402 | /// A Struct | ||
403 | struct S; | ||
404 | |||
405 | impl S { | ||
406 | /// An associated method | ||
407 | fn m() { } | ||
408 | } | ||
409 | |||
410 | fn foo() { let _ = S::<|> } | ||
411 | " | ||
412 | ), | ||
413 | @r###" | ||
414 | [ | ||
415 | CompletionItem { | ||
416 | label: "m()", | ||
417 | source_range: [100; 100), | ||
418 | delete: [100; 100), | ||
419 | insert: "m()$0", | ||
420 | kind: Function, | ||
421 | lookup: "m", | ||
422 | detail: "fn m()", | ||
423 | documentation: Documentation( | ||
424 | "An associated method", | ||
425 | ), | ||
426 | }, | ||
427 | ] | ||
428 | "### | ||
429 | ); | ||
430 | } | ||
431 | |||
432 | #[test] | ||
433 | fn completes_struct_associated_const() { | ||
434 | assert_debug_snapshot!( | ||
435 | do_reference_completion( | ||
436 | " | ||
437 | //- /lib.rs | ||
438 | /// A Struct | ||
439 | struct S; | ||
440 | |||
441 | impl S { | ||
442 | /// An associated const | ||
443 | const C: i32 = 42; | ||
444 | } | ||
445 | |||
446 | fn foo() { let _ = S::<|> } | ||
447 | " | ||
448 | ), | ||
449 | @r###" | ||
450 | [ | ||
451 | CompletionItem { | ||
452 | label: "C", | ||
453 | source_range: [107; 107), | ||
454 | delete: [107; 107), | ||
455 | insert: "C", | ||
456 | kind: Const, | ||
457 | detail: "const C: i32 = 42;", | ||
458 | documentation: Documentation( | ||
459 | "An associated const", | ||
460 | ), | ||
461 | }, | ||
462 | ] | ||
463 | "### | ||
464 | ); | ||
465 | } | ||
466 | |||
467 | #[test] | ||
468 | fn completes_struct_associated_type() { | ||
469 | assert_debug_snapshot!( | ||
470 | do_reference_completion( | ||
471 | " | ||
472 | //- /lib.rs | ||
473 | /// A Struct | ||
474 | struct S; | ||
475 | |||
476 | impl S { | ||
477 | /// An associated type | ||
478 | type T = i32; | ||
479 | } | ||
480 | |||
481 | fn foo() { let _ = S::<|> } | ||
482 | " | ||
483 | ), | ||
484 | @r###" | ||
485 | [ | ||
486 | CompletionItem { | ||
487 | label: "T", | ||
488 | source_range: [101; 101), | ||
489 | delete: [101; 101), | ||
490 | insert: "T", | ||
491 | kind: TypeAlias, | ||
492 | detail: "type T = i32;", | ||
493 | documentation: Documentation( | ||
494 | "An associated type", | ||
495 | ), | ||
496 | }, | ||
497 | ] | ||
498 | "### | ||
499 | ); | ||
500 | } | ||
501 | |||
502 | #[test] | ||
503 | fn completes_enum_associated_method() { | ||
504 | assert_debug_snapshot!( | ||
505 | do_reference_completion( | ||
506 | " | ||
507 | //- /lib.rs | ||
508 | /// An enum | ||
509 | enum S {}; | ||
510 | |||
511 | impl S { | ||
512 | /// An associated method | ||
513 | fn m() { } | ||
514 | } | ||
515 | |||
516 | fn foo() { let _ = S::<|> } | ||
517 | " | ||
518 | ), | ||
519 | @r###" | ||
520 | [ | ||
521 | CompletionItem { | ||
522 | label: "m()", | ||
523 | source_range: [100; 100), | ||
524 | delete: [100; 100), | ||
525 | insert: "m()$0", | ||
526 | kind: Function, | ||
527 | lookup: "m", | ||
528 | detail: "fn m()", | ||
529 | documentation: Documentation( | ||
530 | "An associated method", | ||
531 | ), | ||
532 | }, | ||
533 | ] | ||
534 | "### | ||
535 | ); | ||
536 | } | ||
537 | |||
538 | #[test] | ||
539 | fn completes_union_associated_method() { | ||
540 | assert_debug_snapshot!( | ||
541 | do_reference_completion( | ||
542 | " | ||
543 | //- /lib.rs | ||
544 | /// A union | ||
545 | union U {}; | ||
546 | |||
547 | impl U { | ||
548 | /// An associated method | ||
549 | fn m() { } | ||
550 | } | ||
551 | |||
552 | fn foo() { let _ = U::<|> } | ||
553 | " | ||
554 | ), | ||
555 | @r###" | ||
556 | [ | ||
557 | CompletionItem { | ||
558 | label: "m()", | ||
559 | source_range: [101; 101), | ||
560 | delete: [101; 101), | ||
561 | insert: "m()$0", | ||
562 | kind: Function, | ||
563 | lookup: "m", | ||
564 | detail: "fn m()", | ||
565 | documentation: Documentation( | ||
566 | "An associated method", | ||
567 | ), | ||
568 | }, | ||
569 | ] | ||
570 | "### | ||
571 | ); | ||
572 | } | ||
573 | |||
574 | #[test] | ||
575 | fn completes_use_paths_across_crates() { | ||
576 | assert_debug_snapshot!( | ||
577 | do_reference_completion( | ||
578 | " | ||
579 | //- /main.rs | ||
580 | use foo::<|>; | ||
581 | |||
582 | //- /foo/lib.rs | ||
583 | pub mod bar { | ||
584 | pub struct S; | ||
585 | } | ||
586 | " | ||
587 | ), | ||
588 | @r###" | ||
589 | [ | ||
590 | CompletionItem { | ||
591 | label: "bar", | ||
592 | source_range: [9; 9), | ||
593 | delete: [9; 9), | ||
594 | insert: "bar", | ||
595 | kind: Module, | ||
596 | }, | ||
597 | ] | ||
598 | "### | ||
599 | ); | ||
600 | } | ||
601 | |||
602 | #[test] | ||
603 | fn completes_trait_associated_method_1() { | ||
604 | assert_debug_snapshot!( | ||
605 | do_reference_completion( | ||
606 | " | ||
607 | //- /lib.rs | ||
608 | trait Trait { | ||
609 | /// A trait method | ||
610 | fn m(); | ||
611 | } | ||
612 | |||
613 | fn foo() { let _ = Trait::<|> } | ||
614 | " | ||
615 | ), | ||
616 | @r###" | ||
617 | [ | ||
618 | CompletionItem { | ||
619 | label: "m()", | ||
620 | source_range: [73; 73), | ||
621 | delete: [73; 73), | ||
622 | insert: "m()$0", | ||
623 | kind: Function, | ||
624 | lookup: "m", | ||
625 | detail: "fn m()", | ||
626 | documentation: Documentation( | ||
627 | "A trait method", | ||
628 | ), | ||
629 | }, | ||
630 | ] | ||
631 | "### | ||
632 | ); | ||
633 | } | ||
634 | |||
635 | #[test] | ||
636 | fn completes_trait_associated_method_2() { | ||
637 | assert_debug_snapshot!( | ||
638 | do_reference_completion( | ||
639 | " | ||
640 | //- /lib.rs | ||
641 | trait Trait { | ||
642 | /// A trait method | ||
643 | fn m(); | ||
644 | } | ||
645 | |||
646 | struct S; | ||
647 | impl Trait for S {} | ||
648 | |||
649 | fn foo() { let _ = S::<|> } | ||
650 | " | ||
651 | ), | ||
652 | @r###" | ||
653 | [ | ||
654 | CompletionItem { | ||
655 | label: "m()", | ||
656 | source_range: [99; 99), | ||
657 | delete: [99; 99), | ||
658 | insert: "m()$0", | ||
659 | kind: Function, | ||
660 | lookup: "m", | ||
661 | detail: "fn m()", | ||
662 | documentation: Documentation( | ||
663 | "A trait method", | ||
664 | ), | ||
665 | }, | ||
666 | ] | ||
667 | "### | ||
668 | ); | ||
669 | } | ||
670 | |||
671 | #[test] | ||
672 | fn completes_trait_associated_method_3() { | ||
673 | assert_debug_snapshot!( | ||
674 | do_reference_completion( | ||
675 | " | ||
676 | //- /lib.rs | ||
677 | trait Trait { | ||
678 | /// A trait method | ||
679 | fn m(); | ||
680 | } | ||
681 | |||
682 | struct S; | ||
683 | impl Trait for S {} | ||
684 | |||
685 | fn foo() { let _ = <S as Trait>::<|> } | ||
686 | " | ||
687 | ), | ||
688 | @r###" | ||
689 | [ | ||
690 | CompletionItem { | ||
691 | label: "m()", | ||
692 | source_range: [110; 110), | ||
693 | delete: [110; 110), | ||
694 | insert: "m()$0", | ||
695 | kind: Function, | ||
696 | lookup: "m", | ||
697 | detail: "fn m()", | ||
698 | documentation: Documentation( | ||
699 | "A trait method", | ||
700 | ), | ||
701 | }, | ||
702 | ] | ||
703 | "### | ||
704 | ); | ||
705 | } | ||
706 | |||
707 | #[test] | ||
708 | fn completes_type_alias() { | ||
709 | assert_debug_snapshot!( | ||
710 | do_reference_completion( | ||
711 | " | ||
712 | struct S; | ||
713 | impl S { fn foo() {} } | ||
714 | type T = S; | ||
715 | impl T { fn bar() {} } | ||
716 | |||
717 | fn main() { | ||
718 | T::<|>; | ||
719 | } | ||
720 | " | ||
721 | ), | ||
722 | @r###" | ||
723 | [ | ||
724 | CompletionItem { | ||
725 | label: "bar()", | ||
726 | source_range: [185; 185), | ||
727 | delete: [185; 185), | ||
728 | insert: "bar()$0", | ||
729 | kind: Function, | ||
730 | lookup: "bar", | ||
731 | detail: "fn bar()", | ||
732 | }, | ||
733 | CompletionItem { | ||
734 | label: "foo()", | ||
735 | source_range: [185; 185), | ||
736 | delete: [185; 185), | ||
737 | insert: "foo()$0", | ||
738 | kind: Function, | ||
739 | lookup: "foo", | ||
740 | detail: "fn foo()", | ||
741 | }, | ||
742 | ] | ||
743 | "### | ||
744 | ); | ||
745 | } | ||
746 | |||
747 | #[test] | ||
748 | fn completes_qualified_macros() { | ||
749 | assert_debug_snapshot!( | ||
750 | do_reference_completion( | ||
751 | " | ||
752 | #[macro_export] | ||
753 | macro_rules! foo { | ||
754 | () => {} | ||
755 | } | ||
756 | |||
757 | fn main() { | ||
758 | let _ = crate::<|> | ||
759 | } | ||
760 | " | ||
761 | ), | ||
762 | @r###" | ||
763 | [ | ||
764 | CompletionItem { | ||
765 | label: "foo!", | ||
766 | source_range: [179; 179), | ||
767 | delete: [179; 179), | ||
768 | insert: "foo!($0)", | ||
769 | kind: Macro, | ||
770 | detail: "#[macro_export]\nmacro_rules! foo", | ||
771 | }, | ||
772 | CompletionItem { | ||
773 | label: "main()", | ||
774 | source_range: [179; 179), | ||
775 | delete: [179; 179), | ||
776 | insert: "main()$0", | ||
777 | kind: Function, | ||
778 | lookup: "main", | ||
779 | detail: "fn main()", | ||
780 | }, | ||
781 | ] | ||
782 | "### | ||
783 | ); | ||
784 | } | ||
785 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_pattern.rs b/crates/ra_ide/src/completion/complete_pattern.rs new file mode 100644 index 000000000..fd03b1c40 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_pattern.rs | |||
@@ -0,0 +1,89 @@ | |||
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 { | ||
8 | return; | ||
9 | } | ||
10 | // FIXME: ideally, we should look at the type we are matching against and | ||
11 | // suggest variants + auto-imports | ||
12 | ctx.analyzer.process_all_names(ctx.db, &mut |name, res| { | ||
13 | let def = match &res { | ||
14 | hir::ScopeDef::ModuleDef(def) => def, | ||
15 | _ => return, | ||
16 | }; | ||
17 | match def { | ||
18 | hir::ModuleDef::Adt(hir::Adt::Enum(..)) | ||
19 | | hir::ModuleDef::EnumVariant(..) | ||
20 | | hir::ModuleDef::Const(..) | ||
21 | | hir::ModuleDef::Module(..) => (), | ||
22 | _ => return, | ||
23 | } | ||
24 | acc.add_resolution(ctx, name.to_string(), &res) | ||
25 | }); | ||
26 | } | ||
27 | |||
28 | #[cfg(test)] | ||
29 | mod tests { | ||
30 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
31 | use insta::assert_debug_snapshot; | ||
32 | |||
33 | fn complete(code: &str) -> Vec<CompletionItem> { | ||
34 | do_completion(code, CompletionKind::Reference) | ||
35 | } | ||
36 | |||
37 | #[test] | ||
38 | fn completes_enum_variants_and_modules() { | ||
39 | let completions = complete( | ||
40 | r" | ||
41 | enum E { X } | ||
42 | use self::E::X; | ||
43 | const Z: E = E::X; | ||
44 | mod m {} | ||
45 | |||
46 | static FOO: E = E::X; | ||
47 | struct Bar { f: u32 } | ||
48 | |||
49 | fn foo() { | ||
50 | match E::X { | ||
51 | <|> | ||
52 | } | ||
53 | } | ||
54 | ", | ||
55 | ); | ||
56 | assert_debug_snapshot!(completions, @r###" | ||
57 | [ | ||
58 | CompletionItem { | ||
59 | label: "E", | ||
60 | source_range: [246; 246), | ||
61 | delete: [246; 246), | ||
62 | insert: "E", | ||
63 | kind: Enum, | ||
64 | }, | ||
65 | CompletionItem { | ||
66 | label: "X", | ||
67 | source_range: [246; 246), | ||
68 | delete: [246; 246), | ||
69 | insert: "X", | ||
70 | kind: EnumVariant, | ||
71 | }, | ||
72 | CompletionItem { | ||
73 | label: "Z", | ||
74 | source_range: [246; 246), | ||
75 | delete: [246; 246), | ||
76 | insert: "Z", | ||
77 | kind: Const, | ||
78 | }, | ||
79 | CompletionItem { | ||
80 | label: "m", | ||
81 | source_range: [246; 246), | ||
82 | delete: [246; 246), | ||
83 | insert: "m", | ||
84 | kind: Module, | ||
85 | }, | ||
86 | ] | ||
87 | "###); | ||
88 | } | ||
89 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_postfix.rs b/crates/ra_ide/src/completion/complete_postfix.rs new file mode 100644 index 000000000..646a30c76 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_postfix.rs | |||
@@ -0,0 +1,282 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ast::AstNode, TextRange, TextUnit}; | ||
4 | use ra_text_edit::TextEdit; | ||
5 | |||
6 | use crate::{ | ||
7 | completion::{ | ||
8 | completion_context::CompletionContext, | ||
9 | completion_item::{Builder, CompletionKind, Completions}, | ||
10 | }, | ||
11 | CompletionItem, | ||
12 | }; | ||
13 | |||
14 | pub(super) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { | ||
15 | if ctx.db.feature_flags.get("completion.enable-postfix") == false { | ||
16 | return; | ||
17 | } | ||
18 | |||
19 | let dot_receiver = match &ctx.dot_receiver { | ||
20 | Some(it) => it, | ||
21 | None => return, | ||
22 | }; | ||
23 | |||
24 | let receiver_text = if ctx.dot_receiver_is_ambiguous_float_literal { | ||
25 | let text = dot_receiver.syntax().text(); | ||
26 | let without_dot = ..text.len() - TextUnit::of_char('.'); | ||
27 | text.slice(without_dot).to_string() | ||
28 | } else { | ||
29 | dot_receiver.syntax().text().to_string() | ||
30 | }; | ||
31 | |||
32 | let receiver_ty = match ctx.analyzer.type_of(ctx.db, &dot_receiver) { | ||
33 | Some(it) => it, | ||
34 | None => return, | ||
35 | }; | ||
36 | |||
37 | if receiver_ty.is_bool() || receiver_ty.is_unknown() { | ||
38 | postfix_snippet(ctx, "if", "if expr {}", &format!("if {} {{$0}}", receiver_text)) | ||
39 | .add_to(acc); | ||
40 | postfix_snippet( | ||
41 | ctx, | ||
42 | "while", | ||
43 | "while expr {}", | ||
44 | &format!("while {} {{\n$0\n}}", receiver_text), | ||
45 | ) | ||
46 | .add_to(acc); | ||
47 | } | ||
48 | |||
49 | postfix_snippet(ctx, "not", "!expr", &format!("!{}", receiver_text)).add_to(acc); | ||
50 | |||
51 | postfix_snippet(ctx, "ref", "&expr", &format!("&{}", receiver_text)).add_to(acc); | ||
52 | postfix_snippet(ctx, "refm", "&mut expr", &format!("&mut {}", receiver_text)).add_to(acc); | ||
53 | |||
54 | postfix_snippet( | ||
55 | ctx, | ||
56 | "match", | ||
57 | "match expr {}", | ||
58 | &format!("match {} {{\n ${{1:_}} => {{$0\\}},\n}}", receiver_text), | ||
59 | ) | ||
60 | .add_to(acc); | ||
61 | |||
62 | postfix_snippet(ctx, "dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc); | ||
63 | |||
64 | postfix_snippet(ctx, "box", "Box::new(expr)", &format!("Box::new({})", receiver_text)) | ||
65 | .add_to(acc); | ||
66 | } | ||
67 | |||
68 | fn postfix_snippet(ctx: &CompletionContext, label: &str, detail: &str, snippet: &str) -> Builder { | ||
69 | let edit = { | ||
70 | let receiver_range = | ||
71 | ctx.dot_receiver.as_ref().expect("no receiver available").syntax().text_range(); | ||
72 | let delete_range = TextRange::from_to(receiver_range.start(), ctx.source_range().end()); | ||
73 | TextEdit::replace(delete_range, snippet.to_string()) | ||
74 | }; | ||
75 | CompletionItem::new(CompletionKind::Postfix, ctx.source_range(), label) | ||
76 | .detail(detail) | ||
77 | .snippet_edit(edit) | ||
78 | } | ||
79 | |||
80 | #[cfg(test)] | ||
81 | mod tests { | ||
82 | use insta::assert_debug_snapshot; | ||
83 | |||
84 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
85 | |||
86 | fn do_postfix_completion(code: &str) -> Vec<CompletionItem> { | ||
87 | do_completion(code, CompletionKind::Postfix) | ||
88 | } | ||
89 | |||
90 | #[test] | ||
91 | fn postfix_completion_works_for_trivial_path_expression() { | ||
92 | assert_debug_snapshot!( | ||
93 | do_postfix_completion( | ||
94 | r#" | ||
95 | fn main() { | ||
96 | let bar = true; | ||
97 | bar.<|> | ||
98 | } | ||
99 | "#, | ||
100 | ), | ||
101 | @r###" | ||
102 | [ | ||
103 | CompletionItem { | ||
104 | label: "box", | ||
105 | source_range: [89; 89), | ||
106 | delete: [85; 89), | ||
107 | insert: "Box::new(bar)", | ||
108 | detail: "Box::new(expr)", | ||
109 | }, | ||
110 | CompletionItem { | ||
111 | label: "dbg", | ||
112 | source_range: [89; 89), | ||
113 | delete: [85; 89), | ||
114 | insert: "dbg!(bar)", | ||
115 | detail: "dbg!(expr)", | ||
116 | }, | ||
117 | CompletionItem { | ||
118 | label: "if", | ||
119 | source_range: [89; 89), | ||
120 | delete: [85; 89), | ||
121 | insert: "if bar {$0}", | ||
122 | detail: "if expr {}", | ||
123 | }, | ||
124 | CompletionItem { | ||
125 | label: "match", | ||
126 | source_range: [89; 89), | ||
127 | delete: [85; 89), | ||
128 | insert: "match bar {\n ${1:_} => {$0\\},\n}", | ||
129 | detail: "match expr {}", | ||
130 | }, | ||
131 | CompletionItem { | ||
132 | label: "not", | ||
133 | source_range: [89; 89), | ||
134 | delete: [85; 89), | ||
135 | insert: "!bar", | ||
136 | detail: "!expr", | ||
137 | }, | ||
138 | CompletionItem { | ||
139 | label: "ref", | ||
140 | source_range: [89; 89), | ||
141 | delete: [85; 89), | ||
142 | insert: "&bar", | ||
143 | detail: "&expr", | ||
144 | }, | ||
145 | CompletionItem { | ||
146 | label: "refm", | ||
147 | source_range: [89; 89), | ||
148 | delete: [85; 89), | ||
149 | insert: "&mut bar", | ||
150 | detail: "&mut expr", | ||
151 | }, | ||
152 | CompletionItem { | ||
153 | label: "while", | ||
154 | source_range: [89; 89), | ||
155 | delete: [85; 89), | ||
156 | insert: "while bar {\n$0\n}", | ||
157 | detail: "while expr {}", | ||
158 | }, | ||
159 | ] | ||
160 | "### | ||
161 | ); | ||
162 | } | ||
163 | |||
164 | #[test] | ||
165 | fn some_postfix_completions_ignored() { | ||
166 | assert_debug_snapshot!( | ||
167 | do_postfix_completion( | ||
168 | r#" | ||
169 | fn main() { | ||
170 | let bar: u8 = 12; | ||
171 | bar.<|> | ||
172 | } | ||
173 | "#, | ||
174 | ), | ||
175 | @r###" | ||
176 | [ | ||
177 | CompletionItem { | ||
178 | label: "box", | ||
179 | source_range: [91; 91), | ||
180 | delete: [87; 91), | ||
181 | insert: "Box::new(bar)", | ||
182 | detail: "Box::new(expr)", | ||
183 | }, | ||
184 | CompletionItem { | ||
185 | label: "dbg", | ||
186 | source_range: [91; 91), | ||
187 | delete: [87; 91), | ||
188 | insert: "dbg!(bar)", | ||
189 | detail: "dbg!(expr)", | ||
190 | }, | ||
191 | CompletionItem { | ||
192 | label: "match", | ||
193 | source_range: [91; 91), | ||
194 | delete: [87; 91), | ||
195 | insert: "match bar {\n ${1:_} => {$0\\},\n}", | ||
196 | detail: "match expr {}", | ||
197 | }, | ||
198 | CompletionItem { | ||
199 | label: "not", | ||
200 | source_range: [91; 91), | ||
201 | delete: [87; 91), | ||
202 | insert: "!bar", | ||
203 | detail: "!expr", | ||
204 | }, | ||
205 | CompletionItem { | ||
206 | label: "ref", | ||
207 | source_range: [91; 91), | ||
208 | delete: [87; 91), | ||
209 | insert: "&bar", | ||
210 | detail: "&expr", | ||
211 | }, | ||
212 | CompletionItem { | ||
213 | label: "refm", | ||
214 | source_range: [91; 91), | ||
215 | delete: [87; 91), | ||
216 | insert: "&mut bar", | ||
217 | detail: "&mut expr", | ||
218 | }, | ||
219 | ] | ||
220 | "### | ||
221 | ); | ||
222 | } | ||
223 | |||
224 | #[test] | ||
225 | fn postfix_completion_works_for_ambiguous_float_literal() { | ||
226 | assert_debug_snapshot!( | ||
227 | do_postfix_completion( | ||
228 | r#" | ||
229 | fn main() { | ||
230 | 42.<|> | ||
231 | } | ||
232 | "#, | ||
233 | ), | ||
234 | @r###" | ||
235 | [ | ||
236 | CompletionItem { | ||
237 | label: "box", | ||
238 | source_range: [52; 52), | ||
239 | delete: [49; 52), | ||
240 | insert: "Box::new(42)", | ||
241 | detail: "Box::new(expr)", | ||
242 | }, | ||
243 | CompletionItem { | ||
244 | label: "dbg", | ||
245 | source_range: [52; 52), | ||
246 | delete: [49; 52), | ||
247 | insert: "dbg!(42)", | ||
248 | detail: "dbg!(expr)", | ||
249 | }, | ||
250 | CompletionItem { | ||
251 | label: "match", | ||
252 | source_range: [52; 52), | ||
253 | delete: [49; 52), | ||
254 | insert: "match 42 {\n ${1:_} => {$0\\},\n}", | ||
255 | detail: "match expr {}", | ||
256 | }, | ||
257 | CompletionItem { | ||
258 | label: "not", | ||
259 | source_range: [52; 52), | ||
260 | delete: [49; 52), | ||
261 | insert: "!42", | ||
262 | detail: "!expr", | ||
263 | }, | ||
264 | CompletionItem { | ||
265 | label: "ref", | ||
266 | source_range: [52; 52), | ||
267 | delete: [49; 52), | ||
268 | insert: "&42", | ||
269 | detail: "&expr", | ||
270 | }, | ||
271 | CompletionItem { | ||
272 | label: "refm", | ||
273 | source_range: [52; 52), | ||
274 | delete: [49; 52), | ||
275 | insert: "&mut 42", | ||
276 | detail: "&mut expr", | ||
277 | }, | ||
278 | ] | ||
279 | "### | ||
280 | ); | ||
281 | } | ||
282 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_record_literal.rs b/crates/ra_ide/src/completion/complete_record_literal.rs new file mode 100644 index 000000000..577c394d2 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_record_literal.rs | |||
@@ -0,0 +1,159 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{CompletionContext, Completions}; | ||
4 | |||
5 | /// Complete fields in fields literals. | ||
6 | pub(super) fn complete_record_literal(acc: &mut Completions, ctx: &CompletionContext) { | ||
7 | let (ty, variant) = match ctx.record_lit_syntax.as_ref().and_then(|it| { | ||
8 | Some(( | ||
9 | ctx.analyzer.type_of(ctx.db, &it.clone().into())?, | ||
10 | ctx.analyzer.resolve_record_literal(it)?, | ||
11 | )) | ||
12 | }) { | ||
13 | Some(it) => it, | ||
14 | _ => return, | ||
15 | }; | ||
16 | |||
17 | for (field, field_ty) in ty.variant_fields(ctx.db, variant) { | ||
18 | acc.add_field(ctx, field, &field_ty); | ||
19 | } | ||
20 | } | ||
21 | |||
22 | #[cfg(test)] | ||
23 | mod tests { | ||
24 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
25 | use insta::assert_debug_snapshot; | ||
26 | |||
27 | fn complete(code: &str) -> Vec<CompletionItem> { | ||
28 | do_completion(code, CompletionKind::Reference) | ||
29 | } | ||
30 | |||
31 | #[test] | ||
32 | fn test_record_literal_deprecated_field() { | ||
33 | let completions = complete( | ||
34 | r" | ||
35 | struct A { | ||
36 | #[deprecated] | ||
37 | the_field: u32, | ||
38 | } | ||
39 | fn foo() { | ||
40 | A { the<|> } | ||
41 | } | ||
42 | ", | ||
43 | ); | ||
44 | assert_debug_snapshot!(completions, @r###" | ||
45 | [ | ||
46 | CompletionItem { | ||
47 | label: "the_field", | ||
48 | source_range: [142; 145), | ||
49 | delete: [142; 145), | ||
50 | insert: "the_field", | ||
51 | kind: Field, | ||
52 | detail: "u32", | ||
53 | deprecated: true, | ||
54 | }, | ||
55 | ] | ||
56 | "###); | ||
57 | } | ||
58 | |||
59 | #[test] | ||
60 | fn test_record_literal_field() { | ||
61 | let completions = complete( | ||
62 | r" | ||
63 | struct A { the_field: u32 } | ||
64 | fn foo() { | ||
65 | A { the<|> } | ||
66 | } | ||
67 | ", | ||
68 | ); | ||
69 | assert_debug_snapshot!(completions, @r###" | ||
70 | [ | ||
71 | CompletionItem { | ||
72 | label: "the_field", | ||
73 | source_range: [83; 86), | ||
74 | delete: [83; 86), | ||
75 | insert: "the_field", | ||
76 | kind: Field, | ||
77 | detail: "u32", | ||
78 | }, | ||
79 | ] | ||
80 | "###); | ||
81 | } | ||
82 | |||
83 | #[test] | ||
84 | fn test_record_literal_enum_variant() { | ||
85 | let completions = complete( | ||
86 | r" | ||
87 | enum E { | ||
88 | A { a: u32 } | ||
89 | } | ||
90 | fn foo() { | ||
91 | let _ = E::A { <|> } | ||
92 | } | ||
93 | ", | ||
94 | ); | ||
95 | assert_debug_snapshot!(completions, @r###" | ||
96 | [ | ||
97 | CompletionItem { | ||
98 | label: "a", | ||
99 | source_range: [119; 119), | ||
100 | delete: [119; 119), | ||
101 | insert: "a", | ||
102 | kind: Field, | ||
103 | detail: "u32", | ||
104 | }, | ||
105 | ] | ||
106 | "###); | ||
107 | } | ||
108 | |||
109 | #[test] | ||
110 | fn test_record_literal_two_structs() { | ||
111 | let completions = complete( | ||
112 | r" | ||
113 | struct A { a: u32 } | ||
114 | struct B { b: u32 } | ||
115 | |||
116 | fn foo() { | ||
117 | let _: A = B { <|> } | ||
118 | } | ||
119 | ", | ||
120 | ); | ||
121 | assert_debug_snapshot!(completions, @r###" | ||
122 | [ | ||
123 | CompletionItem { | ||
124 | label: "b", | ||
125 | source_range: [119; 119), | ||
126 | delete: [119; 119), | ||
127 | insert: "b", | ||
128 | kind: Field, | ||
129 | detail: "u32", | ||
130 | }, | ||
131 | ] | ||
132 | "###); | ||
133 | } | ||
134 | |||
135 | #[test] | ||
136 | fn test_record_literal_generic_struct() { | ||
137 | let completions = complete( | ||
138 | r" | ||
139 | struct A<T> { a: T } | ||
140 | |||
141 | fn foo() { | ||
142 | let _: A<u32> = A { <|> } | ||
143 | } | ||
144 | ", | ||
145 | ); | ||
146 | assert_debug_snapshot!(completions, @r###" | ||
147 | [ | ||
148 | CompletionItem { | ||
149 | label: "a", | ||
150 | source_range: [93; 93), | ||
151 | delete: [93; 93), | ||
152 | insert: "a", | ||
153 | kind: Field, | ||
154 | detail: "u32", | ||
155 | }, | ||
156 | ] | ||
157 | "###); | ||
158 | } | ||
159 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_record_pattern.rs b/crates/ra_ide/src/completion/complete_record_pattern.rs new file mode 100644 index 000000000..a56c7e3a1 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_record_pattern.rs | |||
@@ -0,0 +1,93 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{CompletionContext, Completions}; | ||
4 | |||
5 | pub(super) fn complete_record_pattern(acc: &mut Completions, ctx: &CompletionContext) { | ||
6 | let (ty, variant) = match ctx.record_lit_pat.as_ref().and_then(|it| { | ||
7 | Some(( | ||
8 | ctx.analyzer.type_of_pat(ctx.db, &it.clone().into())?, | ||
9 | ctx.analyzer.resolve_record_pattern(it)?, | ||
10 | )) | ||
11 | }) { | ||
12 | Some(it) => it, | ||
13 | _ => return, | ||
14 | }; | ||
15 | |||
16 | for (field, field_ty) in ty.variant_fields(ctx.db, variant) { | ||
17 | acc.add_field(ctx, field, &field_ty); | ||
18 | } | ||
19 | } | ||
20 | |||
21 | #[cfg(test)] | ||
22 | mod tests { | ||
23 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
24 | use insta::assert_debug_snapshot; | ||
25 | |||
26 | fn complete(code: &str) -> Vec<CompletionItem> { | ||
27 | do_completion(code, CompletionKind::Reference) | ||
28 | } | ||
29 | |||
30 | #[test] | ||
31 | fn test_record_pattern_field() { | ||
32 | let completions = complete( | ||
33 | r" | ||
34 | struct S { foo: u32 } | ||
35 | |||
36 | fn process(f: S) { | ||
37 | match f { | ||
38 | S { f<|>: 92 } => (), | ||
39 | } | ||
40 | } | ||
41 | ", | ||
42 | ); | ||
43 | assert_debug_snapshot!(completions, @r###" | ||
44 | [ | ||
45 | CompletionItem { | ||
46 | label: "foo", | ||
47 | source_range: [117; 118), | ||
48 | delete: [117; 118), | ||
49 | insert: "foo", | ||
50 | kind: Field, | ||
51 | detail: "u32", | ||
52 | }, | ||
53 | ] | ||
54 | "###); | ||
55 | } | ||
56 | |||
57 | #[test] | ||
58 | fn test_record_pattern_enum_variant() { | ||
59 | let completions = complete( | ||
60 | r" | ||
61 | enum E { | ||
62 | S { foo: u32, bar: () } | ||
63 | } | ||
64 | |||
65 | fn process(e: E) { | ||
66 | match e { | ||
67 | E::S { <|> } => (), | ||
68 | } | ||
69 | } | ||
70 | ", | ||
71 | ); | ||
72 | assert_debug_snapshot!(completions, @r###" | ||
73 | [ | ||
74 | CompletionItem { | ||
75 | label: "bar", | ||
76 | source_range: [161; 161), | ||
77 | delete: [161; 161), | ||
78 | insert: "bar", | ||
79 | kind: Field, | ||
80 | detail: "()", | ||
81 | }, | ||
82 | CompletionItem { | ||
83 | label: "foo", | ||
84 | source_range: [161; 161), | ||
85 | delete: [161; 161), | ||
86 | insert: "foo", | ||
87 | kind: Field, | ||
88 | detail: "u32", | ||
89 | }, | ||
90 | ] | ||
91 | "###); | ||
92 | } | ||
93 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_scope.rs b/crates/ra_ide/src/completion/complete_scope.rs new file mode 100644 index 000000000..d5739b58a --- /dev/null +++ b/crates/ra_ide/src/completion/complete_scope.rs | |||
@@ -0,0 +1,876 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_assists::auto_import_text_edit; | ||
4 | use ra_syntax::{ast, AstNode, SmolStr}; | ||
5 | use ra_text_edit::TextEditBuilder; | ||
6 | use rustc_hash::FxHashMap; | ||
7 | |||
8 | use crate::completion::{CompletionContext, CompletionItem, CompletionKind, Completions}; | ||
9 | |||
10 | pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) { | ||
11 | if !ctx.is_trivial_path { | ||
12 | return; | ||
13 | } | ||
14 | |||
15 | ctx.analyzer.process_all_names(ctx.db, &mut |name, res| { | ||
16 | acc.add_resolution(ctx, name.to_string(), &res) | ||
17 | }); | ||
18 | |||
19 | // auto-import | ||
20 | // We fetch ident from the original file, because we need to pre-filter auto-imports | ||
21 | if ast::NameRef::cast(ctx.token.parent()).is_some() { | ||
22 | let import_resolver = ImportResolver::new(); | ||
23 | let import_names = import_resolver.all_names(ctx.token.text()); | ||
24 | import_names.into_iter().for_each(|(name, path)| { | ||
25 | let edit = { | ||
26 | let mut builder = TextEditBuilder::default(); | ||
27 | builder.replace(ctx.source_range(), name.to_string()); | ||
28 | auto_import_text_edit( | ||
29 | &ctx.token.parent(), | ||
30 | &ctx.token.parent(), | ||
31 | &path, | ||
32 | &mut builder, | ||
33 | ); | ||
34 | builder.finish() | ||
35 | }; | ||
36 | |||
37 | // Hack: copied this check form conv.rs beacause auto import can produce edits | ||
38 | // that invalidate assert in conv_with. | ||
39 | if edit | ||
40 | .as_atoms() | ||
41 | .iter() | ||
42 | .filter(|atom| !ctx.source_range().is_subrange(&atom.delete)) | ||
43 | .all(|atom| ctx.source_range().intersection(&atom.delete).is_none()) | ||
44 | { | ||
45 | CompletionItem::new( | ||
46 | CompletionKind::Reference, | ||
47 | ctx.source_range(), | ||
48 | build_import_label(&name, &path), | ||
49 | ) | ||
50 | .text_edit(edit) | ||
51 | .add_to(acc); | ||
52 | } | ||
53 | }); | ||
54 | } | ||
55 | } | ||
56 | |||
57 | fn build_import_label(name: &str, path: &[SmolStr]) -> String { | ||
58 | let mut buf = String::with_capacity(64); | ||
59 | buf.push_str(name); | ||
60 | buf.push_str(" ("); | ||
61 | fmt_import_path(path, &mut buf); | ||
62 | buf.push_str(")"); | ||
63 | buf | ||
64 | } | ||
65 | |||
66 | fn fmt_import_path(path: &[SmolStr], buf: &mut String) { | ||
67 | let mut segments = path.iter(); | ||
68 | if let Some(s) = segments.next() { | ||
69 | buf.push_str(&s); | ||
70 | } | ||
71 | for s in segments { | ||
72 | buf.push_str("::"); | ||
73 | buf.push_str(&s); | ||
74 | } | ||
75 | } | ||
76 | |||
77 | #[derive(Debug, Clone, Default)] | ||
78 | pub(crate) struct ImportResolver { | ||
79 | // todo: use fst crate or something like that | ||
80 | dummy_names: Vec<(SmolStr, Vec<SmolStr>)>, | ||
81 | } | ||
82 | |||
83 | impl ImportResolver { | ||
84 | pub(crate) fn new() -> Self { | ||
85 | let dummy_names = vec![ | ||
86 | (SmolStr::new("fmt"), vec![SmolStr::new("std"), SmolStr::new("fmt")]), | ||
87 | (SmolStr::new("io"), vec![SmolStr::new("std"), SmolStr::new("io")]), | ||
88 | (SmolStr::new("iter"), vec![SmolStr::new("std"), SmolStr::new("iter")]), | ||
89 | (SmolStr::new("hash"), vec![SmolStr::new("std"), SmolStr::new("hash")]), | ||
90 | ( | ||
91 | SmolStr::new("Debug"), | ||
92 | vec![SmolStr::new("std"), SmolStr::new("fmt"), SmolStr::new("Debug")], | ||
93 | ), | ||
94 | ( | ||
95 | SmolStr::new("Display"), | ||
96 | vec![SmolStr::new("std"), SmolStr::new("fmt"), SmolStr::new("Display")], | ||
97 | ), | ||
98 | ( | ||
99 | SmolStr::new("Hash"), | ||
100 | vec![SmolStr::new("std"), SmolStr::new("hash"), SmolStr::new("Hash")], | ||
101 | ), | ||
102 | ( | ||
103 | SmolStr::new("Hasher"), | ||
104 | vec![SmolStr::new("std"), SmolStr::new("hash"), SmolStr::new("Hasher")], | ||
105 | ), | ||
106 | ( | ||
107 | SmolStr::new("Iterator"), | ||
108 | vec![SmolStr::new("std"), SmolStr::new("iter"), SmolStr::new("Iterator")], | ||
109 | ), | ||
110 | ]; | ||
111 | |||
112 | ImportResolver { dummy_names } | ||
113 | } | ||
114 | |||
115 | // Returns a map of importable items filtered by name. | ||
116 | // The map associates item name with its full path. | ||
117 | // todo: should return Resolutions | ||
118 | pub(crate) fn all_names(&self, name: &str) -> FxHashMap<SmolStr, Vec<SmolStr>> { | ||
119 | if name.len() > 1 { | ||
120 | self.dummy_names.iter().filter(|(n, _)| n.contains(name)).cloned().collect() | ||
121 | } else { | ||
122 | FxHashMap::default() | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | #[cfg(test)] | ||
128 | mod tests { | ||
129 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
130 | use insta::assert_debug_snapshot; | ||
131 | |||
132 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { | ||
133 | do_completion(code, CompletionKind::Reference) | ||
134 | } | ||
135 | |||
136 | #[test] | ||
137 | fn completes_bindings_from_let() { | ||
138 | assert_debug_snapshot!( | ||
139 | do_reference_completion( | ||
140 | r" | ||
141 | fn quux(x: i32) { | ||
142 | let y = 92; | ||
143 | 1 + <|>; | ||
144 | let z = (); | ||
145 | } | ||
146 | " | ||
147 | ), | ||
148 | @r###" | ||
149 | [ | ||
150 | CompletionItem { | ||
151 | label: "quux(…)", | ||
152 | source_range: [91; 91), | ||
153 | delete: [91; 91), | ||
154 | insert: "quux($0)", | ||
155 | kind: Function, | ||
156 | lookup: "quux", | ||
157 | detail: "fn quux(x: i32)", | ||
158 | }, | ||
159 | CompletionItem { | ||
160 | label: "x", | ||
161 | source_range: [91; 91), | ||
162 | delete: [91; 91), | ||
163 | insert: "x", | ||
164 | kind: Binding, | ||
165 | detail: "i32", | ||
166 | }, | ||
167 | CompletionItem { | ||
168 | label: "y", | ||
169 | source_range: [91; 91), | ||
170 | delete: [91; 91), | ||
171 | insert: "y", | ||
172 | kind: Binding, | ||
173 | detail: "i32", | ||
174 | }, | ||
175 | ] | ||
176 | "### | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | #[test] | ||
181 | fn completes_bindings_from_if_let() { | ||
182 | assert_debug_snapshot!( | ||
183 | do_reference_completion( | ||
184 | r" | ||
185 | fn quux() { | ||
186 | if let Some(x) = foo() { | ||
187 | let y = 92; | ||
188 | }; | ||
189 | if let Some(a) = bar() { | ||
190 | let b = 62; | ||
191 | 1 + <|> | ||
192 | } | ||
193 | } | ||
194 | " | ||
195 | ), | ||
196 | @r###" | ||
197 | [ | ||
198 | CompletionItem { | ||
199 | label: "a", | ||
200 | source_range: [242; 242), | ||
201 | delete: [242; 242), | ||
202 | insert: "a", | ||
203 | kind: Binding, | ||
204 | }, | ||
205 | CompletionItem { | ||
206 | label: "b", | ||
207 | source_range: [242; 242), | ||
208 | delete: [242; 242), | ||
209 | insert: "b", | ||
210 | kind: Binding, | ||
211 | detail: "i32", | ||
212 | }, | ||
213 | CompletionItem { | ||
214 | label: "quux()", | ||
215 | source_range: [242; 242), | ||
216 | delete: [242; 242), | ||
217 | insert: "quux()$0", | ||
218 | kind: Function, | ||
219 | lookup: "quux", | ||
220 | detail: "fn quux()", | ||
221 | }, | ||
222 | ] | ||
223 | "### | ||
224 | ); | ||
225 | } | ||
226 | |||
227 | #[test] | ||
228 | fn completes_bindings_from_for() { | ||
229 | assert_debug_snapshot!( | ||
230 | do_reference_completion( | ||
231 | r" | ||
232 | fn quux() { | ||
233 | for x in &[1, 2, 3] { | ||
234 | <|> | ||
235 | } | ||
236 | } | ||
237 | " | ||
238 | ), | ||
239 | @r###" | ||
240 | [ | ||
241 | CompletionItem { | ||
242 | label: "quux()", | ||
243 | source_range: [95; 95), | ||
244 | delete: [95; 95), | ||
245 | insert: "quux()$0", | ||
246 | kind: Function, | ||
247 | lookup: "quux", | ||
248 | detail: "fn quux()", | ||
249 | }, | ||
250 | CompletionItem { | ||
251 | label: "x", | ||
252 | source_range: [95; 95), | ||
253 | delete: [95; 95), | ||
254 | insert: "x", | ||
255 | kind: Binding, | ||
256 | }, | ||
257 | ] | ||
258 | "### | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | #[test] | ||
263 | fn completes_generic_params() { | ||
264 | assert_debug_snapshot!( | ||
265 | do_reference_completion( | ||
266 | r" | ||
267 | fn quux<T>() { | ||
268 | <|> | ||
269 | } | ||
270 | " | ||
271 | ), | ||
272 | @r###" | ||
273 | [ | ||
274 | CompletionItem { | ||
275 | label: "T", | ||
276 | source_range: [52; 52), | ||
277 | delete: [52; 52), | ||
278 | insert: "T", | ||
279 | kind: TypeParam, | ||
280 | }, | ||
281 | CompletionItem { | ||
282 | label: "quux()", | ||
283 | source_range: [52; 52), | ||
284 | delete: [52; 52), | ||
285 | insert: "quux()$0", | ||
286 | kind: Function, | ||
287 | lookup: "quux", | ||
288 | detail: "fn quux<T>()", | ||
289 | }, | ||
290 | ] | ||
291 | "### | ||
292 | ); | ||
293 | } | ||
294 | |||
295 | #[test] | ||
296 | fn completes_generic_params_in_struct() { | ||
297 | assert_debug_snapshot!( | ||
298 | do_reference_completion( | ||
299 | r" | ||
300 | struct X<T> { | ||
301 | x: <|> | ||
302 | } | ||
303 | " | ||
304 | ), | ||
305 | @r###" | ||
306 | [ | ||
307 | CompletionItem { | ||
308 | label: "Self", | ||
309 | source_range: [54; 54), | ||
310 | delete: [54; 54), | ||
311 | insert: "Self", | ||
312 | kind: TypeParam, | ||
313 | }, | ||
314 | CompletionItem { | ||
315 | label: "T", | ||
316 | source_range: [54; 54), | ||
317 | delete: [54; 54), | ||
318 | insert: "T", | ||
319 | kind: TypeParam, | ||
320 | }, | ||
321 | CompletionItem { | ||
322 | label: "X<…>", | ||
323 | source_range: [54; 54), | ||
324 | delete: [54; 54), | ||
325 | insert: "X<$0>", | ||
326 | kind: Struct, | ||
327 | lookup: "X", | ||
328 | }, | ||
329 | ] | ||
330 | "### | ||
331 | ); | ||
332 | } | ||
333 | |||
334 | #[test] | ||
335 | fn completes_self_in_enum() { | ||
336 | assert_debug_snapshot!( | ||
337 | do_reference_completion( | ||
338 | r" | ||
339 | enum X { | ||
340 | Y(<|>) | ||
341 | } | ||
342 | " | ||
343 | ), | ||
344 | @r###" | ||
345 | [ | ||
346 | CompletionItem { | ||
347 | label: "Self", | ||
348 | source_range: [48; 48), | ||
349 | delete: [48; 48), | ||
350 | insert: "Self", | ||
351 | kind: TypeParam, | ||
352 | }, | ||
353 | CompletionItem { | ||
354 | label: "X", | ||
355 | source_range: [48; 48), | ||
356 | delete: [48; 48), | ||
357 | insert: "X", | ||
358 | kind: Enum, | ||
359 | }, | ||
360 | ] | ||
361 | "### | ||
362 | ); | ||
363 | } | ||
364 | |||
365 | #[test] | ||
366 | fn completes_module_items() { | ||
367 | assert_debug_snapshot!( | ||
368 | do_reference_completion( | ||
369 | r" | ||
370 | struct Foo; | ||
371 | enum Baz {} | ||
372 | fn quux() { | ||
373 | <|> | ||
374 | } | ||
375 | " | ||
376 | ), | ||
377 | @r###" | ||
378 | [ | ||
379 | CompletionItem { | ||
380 | label: "Baz", | ||
381 | source_range: [105; 105), | ||
382 | delete: [105; 105), | ||
383 | insert: "Baz", | ||
384 | kind: Enum, | ||
385 | }, | ||
386 | CompletionItem { | ||
387 | label: "Foo", | ||
388 | source_range: [105; 105), | ||
389 | delete: [105; 105), | ||
390 | insert: "Foo", | ||
391 | kind: Struct, | ||
392 | }, | ||
393 | CompletionItem { | ||
394 | label: "quux()", | ||
395 | source_range: [105; 105), | ||
396 | delete: [105; 105), | ||
397 | insert: "quux()$0", | ||
398 | kind: Function, | ||
399 | lookup: "quux", | ||
400 | detail: "fn quux()", | ||
401 | }, | ||
402 | ] | ||
403 | "### | ||
404 | ); | ||
405 | } | ||
406 | |||
407 | #[test] | ||
408 | fn completes_extern_prelude() { | ||
409 | assert_debug_snapshot!( | ||
410 | do_reference_completion( | ||
411 | r" | ||
412 | //- /lib.rs | ||
413 | use <|>; | ||
414 | |||
415 | //- /other_crate/lib.rs | ||
416 | // nothing here | ||
417 | " | ||
418 | ), | ||
419 | @r###" | ||
420 | [ | ||
421 | CompletionItem { | ||
422 | label: "other_crate", | ||
423 | source_range: [4; 4), | ||
424 | delete: [4; 4), | ||
425 | insert: "other_crate", | ||
426 | kind: Module, | ||
427 | }, | ||
428 | ] | ||
429 | "### | ||
430 | ); | ||
431 | } | ||
432 | |||
433 | #[test] | ||
434 | fn completes_module_items_in_nested_modules() { | ||
435 | assert_debug_snapshot!( | ||
436 | do_reference_completion( | ||
437 | r" | ||
438 | struct Foo; | ||
439 | mod m { | ||
440 | struct Bar; | ||
441 | fn quux() { <|> } | ||
442 | } | ||
443 | " | ||
444 | ), | ||
445 | @r###" | ||
446 | [ | ||
447 | CompletionItem { | ||
448 | label: "Bar", | ||
449 | source_range: [117; 117), | ||
450 | delete: [117; 117), | ||
451 | insert: "Bar", | ||
452 | kind: Struct, | ||
453 | }, | ||
454 | CompletionItem { | ||
455 | label: "quux()", | ||
456 | source_range: [117; 117), | ||
457 | delete: [117; 117), | ||
458 | insert: "quux()$0", | ||
459 | kind: Function, | ||
460 | lookup: "quux", | ||
461 | detail: "fn quux()", | ||
462 | }, | ||
463 | ] | ||
464 | "### | ||
465 | ); | ||
466 | } | ||
467 | |||
468 | #[test] | ||
469 | fn completes_return_type() { | ||
470 | assert_debug_snapshot!( | ||
471 | do_reference_completion( | ||
472 | r" | ||
473 | struct Foo; | ||
474 | fn x() -> <|> | ||
475 | " | ||
476 | ), | ||
477 | @r###" | ||
478 | [ | ||
479 | CompletionItem { | ||
480 | label: "Foo", | ||
481 | source_range: [55; 55), | ||
482 | delete: [55; 55), | ||
483 | insert: "Foo", | ||
484 | kind: Struct, | ||
485 | }, | ||
486 | CompletionItem { | ||
487 | label: "x()", | ||
488 | source_range: [55; 55), | ||
489 | delete: [55; 55), | ||
490 | insert: "x()$0", | ||
491 | kind: Function, | ||
492 | lookup: "x", | ||
493 | detail: "fn x()", | ||
494 | }, | ||
495 | ] | ||
496 | "### | ||
497 | ); | ||
498 | } | ||
499 | |||
500 | #[test] | ||
501 | fn dont_show_both_completions_for_shadowing() { | ||
502 | assert_debug_snapshot!( | ||
503 | do_reference_completion( | ||
504 | r" | ||
505 | fn foo() { | ||
506 | let bar = 92; | ||
507 | { | ||
508 | let bar = 62; | ||
509 | <|> | ||
510 | } | ||
511 | } | ||
512 | " | ||
513 | ), | ||
514 | @r###" | ||
515 | [ | ||
516 | CompletionItem { | ||
517 | label: "bar", | ||
518 | source_range: [146; 146), | ||
519 | delete: [146; 146), | ||
520 | insert: "bar", | ||
521 | kind: Binding, | ||
522 | detail: "i32", | ||
523 | }, | ||
524 | CompletionItem { | ||
525 | label: "foo()", | ||
526 | source_range: [146; 146), | ||
527 | delete: [146; 146), | ||
528 | insert: "foo()$0", | ||
529 | kind: Function, | ||
530 | lookup: "foo", | ||
531 | detail: "fn foo()", | ||
532 | }, | ||
533 | ] | ||
534 | "### | ||
535 | ); | ||
536 | } | ||
537 | |||
538 | #[test] | ||
539 | fn completes_self_in_methods() { | ||
540 | assert_debug_snapshot!( | ||
541 | do_reference_completion(r"impl S { fn foo(&self) { <|> } }"), | ||
542 | @r###" | ||
543 | [ | ||
544 | CompletionItem { | ||
545 | label: "Self", | ||
546 | source_range: [25; 25), | ||
547 | delete: [25; 25), | ||
548 | insert: "Self", | ||
549 | kind: TypeParam, | ||
550 | }, | ||
551 | CompletionItem { | ||
552 | label: "self", | ||
553 | source_range: [25; 25), | ||
554 | delete: [25; 25), | ||
555 | insert: "self", | ||
556 | kind: Binding, | ||
557 | detail: "&{unknown}", | ||
558 | }, | ||
559 | ] | ||
560 | "### | ||
561 | ); | ||
562 | } | ||
563 | |||
564 | #[test] | ||
565 | fn completes_prelude() { | ||
566 | assert_debug_snapshot!( | ||
567 | do_reference_completion( | ||
568 | " | ||
569 | //- /main.rs | ||
570 | fn foo() { let x: <|> } | ||
571 | |||
572 | //- /std/lib.rs | ||
573 | #[prelude_import] | ||
574 | use prelude::*; | ||
575 | |||
576 | mod prelude { | ||
577 | struct Option; | ||
578 | } | ||
579 | " | ||
580 | ), | ||
581 | @r###" | ||
582 | [ | ||
583 | CompletionItem { | ||
584 | label: "Option", | ||
585 | source_range: [18; 18), | ||
586 | delete: [18; 18), | ||
587 | insert: "Option", | ||
588 | kind: Struct, | ||
589 | }, | ||
590 | CompletionItem { | ||
591 | label: "foo()", | ||
592 | source_range: [18; 18), | ||
593 | delete: [18; 18), | ||
594 | insert: "foo()$0", | ||
595 | kind: Function, | ||
596 | lookup: "foo", | ||
597 | detail: "fn foo()", | ||
598 | }, | ||
599 | CompletionItem { | ||
600 | label: "std", | ||
601 | source_range: [18; 18), | ||
602 | delete: [18; 18), | ||
603 | insert: "std", | ||
604 | kind: Module, | ||
605 | }, | ||
606 | ] | ||
607 | "### | ||
608 | ); | ||
609 | } | ||
610 | |||
611 | #[test] | ||
612 | fn completes_std_prelude_if_core_is_defined() { | ||
613 | assert_debug_snapshot!( | ||
614 | do_reference_completion( | ||
615 | " | ||
616 | //- /main.rs | ||
617 | fn foo() { let x: <|> } | ||
618 | |||
619 | //- /core/lib.rs | ||
620 | #[prelude_import] | ||
621 | use prelude::*; | ||
622 | |||
623 | mod prelude { | ||
624 | struct Option; | ||
625 | } | ||
626 | |||
627 | //- /std/lib.rs | ||
628 | #[prelude_import] | ||
629 | use prelude::*; | ||
630 | |||
631 | mod prelude { | ||
632 | struct String; | ||
633 | } | ||
634 | " | ||
635 | ), | ||
636 | @r###" | ||
637 | [ | ||
638 | CompletionItem { | ||
639 | label: "String", | ||
640 | source_range: [18; 18), | ||
641 | delete: [18; 18), | ||
642 | insert: "String", | ||
643 | kind: Struct, | ||
644 | }, | ||
645 | CompletionItem { | ||
646 | label: "core", | ||
647 | source_range: [18; 18), | ||
648 | delete: [18; 18), | ||
649 | insert: "core", | ||
650 | kind: Module, | ||
651 | }, | ||
652 | CompletionItem { | ||
653 | label: "foo()", | ||
654 | source_range: [18; 18), | ||
655 | delete: [18; 18), | ||
656 | insert: "foo()$0", | ||
657 | kind: Function, | ||
658 | lookup: "foo", | ||
659 | detail: "fn foo()", | ||
660 | }, | ||
661 | CompletionItem { | ||
662 | label: "std", | ||
663 | source_range: [18; 18), | ||
664 | delete: [18; 18), | ||
665 | insert: "std", | ||
666 | kind: Module, | ||
667 | }, | ||
668 | ] | ||
669 | "### | ||
670 | ); | ||
671 | } | ||
672 | |||
673 | #[test] | ||
674 | fn completes_macros_as_value() { | ||
675 | assert_debug_snapshot!( | ||
676 | do_reference_completion( | ||
677 | " | ||
678 | //- /main.rs | ||
679 | macro_rules! foo { | ||
680 | () => {} | ||
681 | } | ||
682 | |||
683 | #[macro_use] | ||
684 | mod m1 { | ||
685 | macro_rules! bar { | ||
686 | () => {} | ||
687 | } | ||
688 | } | ||
689 | |||
690 | mod m2 { | ||
691 | macro_rules! nope { | ||
692 | () => {} | ||
693 | } | ||
694 | |||
695 | #[macro_export] | ||
696 | macro_rules! baz { | ||
697 | () => {} | ||
698 | } | ||
699 | } | ||
700 | |||
701 | fn main() { | ||
702 | let v = <|> | ||
703 | } | ||
704 | " | ||
705 | ), | ||
706 | @r###" | ||
707 | [ | ||
708 | CompletionItem { | ||
709 | label: "bar!", | ||
710 | source_range: [252; 252), | ||
711 | delete: [252; 252), | ||
712 | insert: "bar!($0)", | ||
713 | kind: Macro, | ||
714 | detail: "macro_rules! bar", | ||
715 | }, | ||
716 | CompletionItem { | ||
717 | label: "baz!", | ||
718 | source_range: [252; 252), | ||
719 | delete: [252; 252), | ||
720 | insert: "baz!($0)", | ||
721 | kind: Macro, | ||
722 | detail: "#[macro_export]\nmacro_rules! baz", | ||
723 | }, | ||
724 | CompletionItem { | ||
725 | label: "foo!", | ||
726 | source_range: [252; 252), | ||
727 | delete: [252; 252), | ||
728 | insert: "foo!($0)", | ||
729 | kind: Macro, | ||
730 | detail: "macro_rules! foo", | ||
731 | }, | ||
732 | CompletionItem { | ||
733 | label: "m1", | ||
734 | source_range: [252; 252), | ||
735 | delete: [252; 252), | ||
736 | insert: "m1", | ||
737 | kind: Module, | ||
738 | }, | ||
739 | CompletionItem { | ||
740 | label: "m2", | ||
741 | source_range: [252; 252), | ||
742 | delete: [252; 252), | ||
743 | insert: "m2", | ||
744 | kind: Module, | ||
745 | }, | ||
746 | CompletionItem { | ||
747 | label: "main()", | ||
748 | source_range: [252; 252), | ||
749 | delete: [252; 252), | ||
750 | insert: "main()$0", | ||
751 | kind: Function, | ||
752 | lookup: "main", | ||
753 | detail: "fn main()", | ||
754 | }, | ||
755 | ] | ||
756 | "### | ||
757 | ); | ||
758 | } | ||
759 | |||
760 | #[test] | ||
761 | fn completes_both_macro_and_value() { | ||
762 | assert_debug_snapshot!( | ||
763 | do_reference_completion( | ||
764 | " | ||
765 | //- /main.rs | ||
766 | macro_rules! foo { | ||
767 | () => {} | ||
768 | } | ||
769 | |||
770 | fn foo() { | ||
771 | <|> | ||
772 | } | ||
773 | " | ||
774 | ), | ||
775 | @r###" | ||
776 | [ | ||
777 | CompletionItem { | ||
778 | label: "foo!", | ||
779 | source_range: [49; 49), | ||
780 | delete: [49; 49), | ||
781 | insert: "foo!($0)", | ||
782 | kind: Macro, | ||
783 | detail: "macro_rules! foo", | ||
784 | }, | ||
785 | CompletionItem { | ||
786 | label: "foo()", | ||
787 | source_range: [49; 49), | ||
788 | delete: [49; 49), | ||
789 | insert: "foo()$0", | ||
790 | kind: Function, | ||
791 | lookup: "foo", | ||
792 | detail: "fn foo()", | ||
793 | }, | ||
794 | ] | ||
795 | "### | ||
796 | ); | ||
797 | } | ||
798 | |||
799 | #[test] | ||
800 | fn completes_macros_as_type() { | ||
801 | assert_debug_snapshot!( | ||
802 | do_reference_completion( | ||
803 | " | ||
804 | //- /main.rs | ||
805 | macro_rules! foo { | ||
806 | () => {} | ||
807 | } | ||
808 | |||
809 | fn main() { | ||
810 | let x: <|> | ||
811 | } | ||
812 | " | ||
813 | ), | ||
814 | @r###" | ||
815 | [ | ||
816 | CompletionItem { | ||
817 | label: "foo!", | ||
818 | source_range: [57; 57), | ||
819 | delete: [57; 57), | ||
820 | insert: "foo!($0)", | ||
821 | kind: Macro, | ||
822 | detail: "macro_rules! foo", | ||
823 | }, | ||
824 | CompletionItem { | ||
825 | label: "main()", | ||
826 | source_range: [57; 57), | ||
827 | delete: [57; 57), | ||
828 | insert: "main()$0", | ||
829 | kind: Function, | ||
830 | lookup: "main", | ||
831 | detail: "fn main()", | ||
832 | }, | ||
833 | ] | ||
834 | "### | ||
835 | ); | ||
836 | } | ||
837 | |||
838 | #[test] | ||
839 | fn completes_macros_as_stmt() { | ||
840 | assert_debug_snapshot!( | ||
841 | do_reference_completion( | ||
842 | " | ||
843 | //- /main.rs | ||
844 | macro_rules! foo { | ||
845 | () => {} | ||
846 | } | ||
847 | |||
848 | fn main() { | ||
849 | <|> | ||
850 | } | ||
851 | " | ||
852 | ), | ||
853 | @r###" | ||
854 | [ | ||
855 | CompletionItem { | ||
856 | label: "foo!", | ||
857 | source_range: [50; 50), | ||
858 | delete: [50; 50), | ||
859 | insert: "foo!($0)", | ||
860 | kind: Macro, | ||
861 | detail: "macro_rules! foo", | ||
862 | }, | ||
863 | CompletionItem { | ||
864 | label: "main()", | ||
865 | source_range: [50; 50), | ||
866 | delete: [50; 50), | ||
867 | insert: "main()$0", | ||
868 | kind: Function, | ||
869 | lookup: "main", | ||
870 | detail: "fn main()", | ||
871 | }, | ||
872 | ] | ||
873 | "### | ||
874 | ); | ||
875 | } | ||
876 | } | ||
diff --git a/crates/ra_ide/src/completion/complete_snippet.rs b/crates/ra_ide/src/completion/complete_snippet.rs new file mode 100644 index 000000000..1f2988b36 --- /dev/null +++ b/crates/ra_ide/src/completion/complete_snippet.rs | |||
@@ -0,0 +1,120 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::completion::{ | ||
4 | completion_item::Builder, CompletionContext, CompletionItem, CompletionItemKind, | ||
5 | CompletionKind, Completions, | ||
6 | }; | ||
7 | |||
8 | fn snippet(ctx: &CompletionContext, label: &str, snippet: &str) -> Builder { | ||
9 | CompletionItem::new(CompletionKind::Snippet, ctx.source_range(), label) | ||
10 | .insert_snippet(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 | |||
19 | snippet(ctx, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); | ||
20 | snippet(ctx, "ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc); | ||
21 | } | ||
22 | |||
23 | pub(super) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionContext) { | ||
24 | if !ctx.is_new_item { | ||
25 | return; | ||
26 | } | ||
27 | snippet( | ||
28 | ctx, | ||
29 | "Test function", | ||
30 | "\ | ||
31 | #[test] | ||
32 | fn ${1:feature}() { | ||
33 | $0 | ||
34 | }", | ||
35 | ) | ||
36 | .lookup_by("tfn") | ||
37 | .add_to(acc); | ||
38 | |||
39 | snippet(ctx, "pub(crate)", "pub(crate) $0").add_to(acc); | ||
40 | } | ||
41 | |||
42 | #[cfg(test)] | ||
43 | mod tests { | ||
44 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
45 | use insta::assert_debug_snapshot; | ||
46 | |||
47 | fn do_snippet_completion(code: &str) -> Vec<CompletionItem> { | ||
48 | do_completion(code, CompletionKind::Snippet) | ||
49 | } | ||
50 | |||
51 | #[test] | ||
52 | fn completes_snippets_in_expressions() { | ||
53 | assert_debug_snapshot!( | ||
54 | do_snippet_completion(r"fn foo(x: i32) { <|> }"), | ||
55 | @r###" | ||
56 | [ | ||
57 | CompletionItem { | ||
58 | label: "pd", | ||
59 | source_range: [17; 17), | ||
60 | delete: [17; 17), | ||
61 | insert: "eprintln!(\"$0 = {:?}\", $0);", | ||
62 | kind: Snippet, | ||
63 | }, | ||
64 | CompletionItem { | ||
65 | label: "ppd", | ||
66 | source_range: [17; 17), | ||
67 | delete: [17; 17), | ||
68 | insert: "eprintln!(\"$0 = {:#?}\", $0);", | ||
69 | kind: Snippet, | ||
70 | }, | ||
71 | ] | ||
72 | "### | ||
73 | ); | ||
74 | } | ||
75 | |||
76 | #[test] | ||
77 | fn should_not_complete_snippets_in_path() { | ||
78 | assert_debug_snapshot!( | ||
79 | do_snippet_completion(r"fn foo(x: i32) { ::foo<|> }"), | ||
80 | @"[]" | ||
81 | ); | ||
82 | assert_debug_snapshot!( | ||
83 | do_snippet_completion(r"fn foo(x: i32) { ::<|> }"), | ||
84 | @"[]" | ||
85 | ); | ||
86 | } | ||
87 | |||
88 | #[test] | ||
89 | fn completes_snippets_in_items() { | ||
90 | assert_debug_snapshot!( | ||
91 | do_snippet_completion( | ||
92 | r" | ||
93 | #[cfg(test)] | ||
94 | mod tests { | ||
95 | <|> | ||
96 | } | ||
97 | " | ||
98 | ), | ||
99 | @r###" | ||
100 | [ | ||
101 | CompletionItem { | ||
102 | label: "Test function", | ||
103 | source_range: [78; 78), | ||
104 | delete: [78; 78), | ||
105 | insert: "#[test]\nfn ${1:feature}() {\n $0\n}", | ||
106 | kind: Snippet, | ||
107 | lookup: "tfn", | ||
108 | }, | ||
109 | CompletionItem { | ||
110 | label: "pub(crate)", | ||
111 | source_range: [78; 78), | ||
112 | delete: [78; 78), | ||
113 | insert: "pub(crate) $0", | ||
114 | kind: Snippet, | ||
115 | }, | ||
116 | ] | ||
117 | "### | ||
118 | ); | ||
119 | } | ||
120 | } | ||
diff --git a/crates/ra_ide/src/completion/completion_context.rs b/crates/ra_ide/src/completion/completion_context.rs new file mode 100644 index 000000000..b8345c91d --- /dev/null +++ b/crates/ra_ide/src/completion/completion_context.rs | |||
@@ -0,0 +1,274 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ | ||
4 | algo::{find_covering_element, find_node_at_offset}, | ||
5 | ast, AstNode, Parse, SourceFile, | ||
6 | SyntaxKind::*, | ||
7 | SyntaxNode, SyntaxToken, TextRange, TextUnit, | ||
8 | }; | ||
9 | use ra_text_edit::AtomTextEdit; | ||
10 | |||
11 | use crate::{db, FilePosition}; | ||
12 | |||
13 | /// `CompletionContext` is created early during completion to figure out, where | ||
14 | /// exactly is the cursor, syntax-wise. | ||
15 | #[derive(Debug)] | ||
16 | pub(crate) struct CompletionContext<'a> { | ||
17 | pub(super) db: &'a db::RootDatabase, | ||
18 | pub(super) analyzer: hir::SourceAnalyzer, | ||
19 | pub(super) offset: TextUnit, | ||
20 | pub(super) token: SyntaxToken, | ||
21 | pub(super) module: Option<hir::Module>, | ||
22 | pub(super) function_syntax: Option<ast::FnDef>, | ||
23 | pub(super) use_item_syntax: Option<ast::UseItem>, | ||
24 | pub(super) record_lit_syntax: Option<ast::RecordLit>, | ||
25 | pub(super) record_lit_pat: Option<ast::RecordPat>, | ||
26 | pub(super) is_param: bool, | ||
27 | /// If a name-binding or reference to a const in a pattern. | ||
28 | /// Irrefutable patterns (like let) are excluded. | ||
29 | pub(super) is_pat_binding: bool, | ||
30 | /// A single-indent path, like `foo`. `::foo` should not be considered a trivial path. | ||
31 | pub(super) is_trivial_path: bool, | ||
32 | /// If not a trivial path, the prefix (qualifier). | ||
33 | pub(super) path_prefix: Option<hir::Path>, | ||
34 | pub(super) after_if: bool, | ||
35 | /// `true` if we are a statement or a last expr in the block. | ||
36 | pub(super) can_be_stmt: bool, | ||
37 | /// Something is typed at the "top" level, in module or impl/trait. | ||
38 | pub(super) is_new_item: bool, | ||
39 | /// The receiver if this is a field or method access, i.e. writing something.<|> | ||
40 | pub(super) dot_receiver: Option<ast::Expr>, | ||
41 | pub(super) dot_receiver_is_ambiguous_float_literal: bool, | ||
42 | /// If this is a call (method or function) in particular, i.e. the () are already there. | ||
43 | pub(super) is_call: bool, | ||
44 | pub(super) is_path_type: bool, | ||
45 | pub(super) has_type_args: bool, | ||
46 | } | ||
47 | |||
48 | impl<'a> CompletionContext<'a> { | ||
49 | pub(super) fn new( | ||
50 | db: &'a db::RootDatabase, | ||
51 | original_parse: &'a Parse<ast::SourceFile>, | ||
52 | position: FilePosition, | ||
53 | ) -> Option<CompletionContext<'a>> { | ||
54 | let src = hir::ModuleSource::from_position(db, position); | ||
55 | let module = hir::Module::from_definition( | ||
56 | db, | ||
57 | hir::Source { file_id: position.file_id.into(), value: src }, | ||
58 | ); | ||
59 | let token = | ||
60 | original_parse.tree().syntax().token_at_offset(position.offset).left_biased()?; | ||
61 | let analyzer = hir::SourceAnalyzer::new( | ||
62 | db, | ||
63 | hir::Source::new(position.file_id.into(), &token.parent()), | ||
64 | Some(position.offset), | ||
65 | ); | ||
66 | let mut ctx = CompletionContext { | ||
67 | db, | ||
68 | analyzer, | ||
69 | token, | ||
70 | offset: position.offset, | ||
71 | module, | ||
72 | function_syntax: None, | ||
73 | use_item_syntax: None, | ||
74 | record_lit_syntax: None, | ||
75 | record_lit_pat: None, | ||
76 | is_param: false, | ||
77 | is_pat_binding: false, | ||
78 | is_trivial_path: false, | ||
79 | path_prefix: None, | ||
80 | after_if: false, | ||
81 | can_be_stmt: false, | ||
82 | is_new_item: false, | ||
83 | dot_receiver: None, | ||
84 | is_call: false, | ||
85 | is_path_type: false, | ||
86 | has_type_args: false, | ||
87 | dot_receiver_is_ambiguous_float_literal: false, | ||
88 | }; | ||
89 | ctx.fill(&original_parse, position.offset); | ||
90 | Some(ctx) | ||
91 | } | ||
92 | |||
93 | // The range of the identifier that is being completed. | ||
94 | pub(crate) fn source_range(&self) -> TextRange { | ||
95 | match self.token.kind() { | ||
96 | // workaroud when completion is triggered by trigger characters. | ||
97 | IDENT => self.token.text_range(), | ||
98 | _ => TextRange::offset_len(self.offset, 0.into()), | ||
99 | } | ||
100 | } | ||
101 | |||
102 | fn fill(&mut self, original_parse: &'a Parse<ast::SourceFile>, offset: TextUnit) { | ||
103 | // Insert a fake ident to get a valid parse tree. We will use this file | ||
104 | // to determine context, though the original_file will be used for | ||
105 | // actual completion. | ||
106 | let file = { | ||
107 | let edit = AtomTextEdit::insert(offset, "intellijRulezz".to_string()); | ||
108 | original_parse.reparse(&edit).tree() | ||
109 | }; | ||
110 | |||
111 | // First, let's try to complete a reference to some declaration. | ||
112 | if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), offset) { | ||
113 | // Special case, `trait T { fn foo(i_am_a_name_ref) {} }`. | ||
114 | // See RFC#1685. | ||
115 | if is_node::<ast::Param>(name_ref.syntax()) { | ||
116 | self.is_param = true; | ||
117 | return; | ||
118 | } | ||
119 | self.classify_name_ref(original_parse.tree(), name_ref); | ||
120 | } | ||
121 | |||
122 | // Otherwise, see if this is a declaration. We can use heuristics to | ||
123 | // suggest declaration names, see `CompletionKind::Magic`. | ||
124 | if let Some(name) = find_node_at_offset::<ast::Name>(file.syntax(), offset) { | ||
125 | if let Some(bind_pat) = name.syntax().ancestors().find_map(ast::BindPat::cast) { | ||
126 | let parent = bind_pat.syntax().parent(); | ||
127 | if parent.clone().and_then(ast::MatchArm::cast).is_some() | ||
128 | || parent.and_then(ast::Condition::cast).is_some() | ||
129 | { | ||
130 | self.is_pat_binding = true; | ||
131 | } | ||
132 | } | ||
133 | if is_node::<ast::Param>(name.syntax()) { | ||
134 | self.is_param = true; | ||
135 | return; | ||
136 | } | ||
137 | if name.syntax().ancestors().find_map(ast::RecordFieldPatList::cast).is_some() { | ||
138 | self.record_lit_pat = | ||
139 | find_node_at_offset(original_parse.tree().syntax(), self.offset); | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | |||
144 | fn classify_name_ref(&mut self, original_file: SourceFile, name_ref: ast::NameRef) { | ||
145 | let name_range = name_ref.syntax().text_range(); | ||
146 | if name_ref.syntax().parent().and_then(ast::RecordField::cast).is_some() { | ||
147 | self.record_lit_syntax = find_node_at_offset(original_file.syntax(), self.offset); | ||
148 | } | ||
149 | |||
150 | let top_node = name_ref | ||
151 | .syntax() | ||
152 | .ancestors() | ||
153 | .take_while(|it| it.text_range() == name_range) | ||
154 | .last() | ||
155 | .unwrap(); | ||
156 | |||
157 | match top_node.parent().map(|it| it.kind()) { | ||
158 | Some(SOURCE_FILE) | Some(ITEM_LIST) => { | ||
159 | self.is_new_item = true; | ||
160 | return; | ||
161 | } | ||
162 | _ => (), | ||
163 | } | ||
164 | |||
165 | self.use_item_syntax = self.token.parent().ancestors().find_map(ast::UseItem::cast); | ||
166 | |||
167 | self.function_syntax = self | ||
168 | .token | ||
169 | .parent() | ||
170 | .ancestors() | ||
171 | .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) | ||
172 | .find_map(ast::FnDef::cast); | ||
173 | |||
174 | let parent = match name_ref.syntax().parent() { | ||
175 | Some(it) => it, | ||
176 | None => return, | ||
177 | }; | ||
178 | |||
179 | if let Some(segment) = ast::PathSegment::cast(parent.clone()) { | ||
180 | let path = segment.parent_path(); | ||
181 | self.is_call = path | ||
182 | .syntax() | ||
183 | .parent() | ||
184 | .and_then(ast::PathExpr::cast) | ||
185 | .and_then(|it| it.syntax().parent().and_then(ast::CallExpr::cast)) | ||
186 | .is_some(); | ||
187 | |||
188 | self.is_path_type = path.syntax().parent().and_then(ast::PathType::cast).is_some(); | ||
189 | self.has_type_args = segment.type_arg_list().is_some(); | ||
190 | |||
191 | if let Some(mut path) = hir::Path::from_ast(path.clone()) { | ||
192 | if !path.is_ident() { | ||
193 | path.segments.pop().unwrap(); | ||
194 | self.path_prefix = Some(path); | ||
195 | return; | ||
196 | } | ||
197 | } | ||
198 | |||
199 | if path.qualifier().is_none() { | ||
200 | self.is_trivial_path = true; | ||
201 | |||
202 | // Find either enclosing expr statement (thing with `;`) or a | ||
203 | // block. If block, check that we are the last expr. | ||
204 | self.can_be_stmt = name_ref | ||
205 | .syntax() | ||
206 | .ancestors() | ||
207 | .find_map(|node| { | ||
208 | if let Some(stmt) = ast::ExprStmt::cast(node.clone()) { | ||
209 | return Some( | ||
210 | stmt.syntax().text_range() == name_ref.syntax().text_range(), | ||
211 | ); | ||
212 | } | ||
213 | if let Some(block) = ast::Block::cast(node) { | ||
214 | return Some( | ||
215 | block.expr().map(|e| e.syntax().text_range()) | ||
216 | == Some(name_ref.syntax().text_range()), | ||
217 | ); | ||
218 | } | ||
219 | None | ||
220 | }) | ||
221 | .unwrap_or(false); | ||
222 | |||
223 | if let Some(off) = name_ref.syntax().text_range().start().checked_sub(2.into()) { | ||
224 | if let Some(if_expr) = | ||
225 | find_node_at_offset::<ast::IfExpr>(original_file.syntax(), off) | ||
226 | { | ||
227 | if if_expr.syntax().text_range().end() | ||
228 | < name_ref.syntax().text_range().start() | ||
229 | { | ||
230 | self.after_if = true; | ||
231 | } | ||
232 | } | ||
233 | } | ||
234 | } | ||
235 | } | ||
236 | if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) { | ||
237 | // The receiver comes before the point of insertion of the fake | ||
238 | // ident, so it should have the same range in the non-modified file | ||
239 | self.dot_receiver = field_expr | ||
240 | .expr() | ||
241 | .map(|e| e.syntax().text_range()) | ||
242 | .and_then(|r| find_node_with_range(original_file.syntax(), r)); | ||
243 | self.dot_receiver_is_ambiguous_float_literal = if let Some(ast::Expr::Literal(l)) = | ||
244 | &self.dot_receiver | ||
245 | { | ||
246 | match l.kind() { | ||
247 | ast::LiteralKind::FloatNumber { suffix: _ } => l.token().text().ends_with('.'), | ||
248 | _ => false, | ||
249 | } | ||
250 | } else { | ||
251 | false | ||
252 | } | ||
253 | } | ||
254 | if let Some(method_call_expr) = ast::MethodCallExpr::cast(parent) { | ||
255 | // As above | ||
256 | self.dot_receiver = method_call_expr | ||
257 | .expr() | ||
258 | .map(|e| e.syntax().text_range()) | ||
259 | .and_then(|r| find_node_with_range(original_file.syntax(), r)); | ||
260 | self.is_call = true; | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | |||
265 | fn find_node_with_range<N: AstNode>(syntax: &SyntaxNode, range: TextRange) -> Option<N> { | ||
266 | find_covering_element(syntax, range).ancestors().find_map(N::cast) | ||
267 | } | ||
268 | |||
269 | fn is_node<N: AstNode>(node: &SyntaxNode) -> bool { | ||
270 | match node.ancestors().find_map(N::cast) { | ||
271 | None => false, | ||
272 | Some(n) => n.syntax().text_range() == node.text_range(), | ||
273 | } | ||
274 | } | ||
diff --git a/crates/ra_ide/src/completion/completion_item.rs b/crates/ra_ide/src/completion/completion_item.rs new file mode 100644 index 000000000..93f336370 --- /dev/null +++ b/crates/ra_ide/src/completion/completion_item.rs | |||
@@ -0,0 +1,322 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::fmt; | ||
4 | |||
5 | use hir::Documentation; | ||
6 | use ra_syntax::TextRange; | ||
7 | use ra_text_edit::TextEdit; | ||
8 | |||
9 | /// `CompletionItem` describes a single completion variant in the editor pop-up. | ||
10 | /// It is basically a POD with various properties. To construct a | ||
11 | /// `CompletionItem`, use `new` method and the `Builder` struct. | ||
12 | pub struct CompletionItem { | ||
13 | /// Used only internally in tests, to check only specific kind of | ||
14 | /// completion (postfix, keyword, reference, etc). | ||
15 | #[allow(unused)] | ||
16 | completion_kind: CompletionKind, | ||
17 | /// Label in the completion pop up which identifies completion. | ||
18 | label: String, | ||
19 | /// Range of identifier that is being completed. | ||
20 | /// | ||
21 | /// It should be used primarily for UI, but we also use this to convert | ||
22 | /// genetic TextEdit into LSP's completion edit (see conv.rs). | ||
23 | /// | ||
24 | /// `source_range` must contain the completion offset. `insert_text` should | ||
25 | /// start with what `source_range` points to, or VSCode will filter out the | ||
26 | /// completion silently. | ||
27 | source_range: TextRange, | ||
28 | /// What happens when user selects this item. | ||
29 | /// | ||
30 | /// Typically, replaces `source_range` with new identifier. | ||
31 | text_edit: TextEdit, | ||
32 | insert_text_format: InsertTextFormat, | ||
33 | |||
34 | /// What item (struct, function, etc) are we completing. | ||
35 | kind: Option<CompletionItemKind>, | ||
36 | |||
37 | /// Lookup is used to check if completion item indeed can complete current | ||
38 | /// ident. | ||
39 | /// | ||
40 | /// That is, in `foo.bar<|>` lookup of `abracadabra` will be accepted (it | ||
41 | /// contains `bar` sub sequence), and `quux` will rejected. | ||
42 | lookup: Option<String>, | ||
43 | |||
44 | /// Additional info to show in the UI pop up. | ||
45 | detail: Option<String>, | ||
46 | documentation: Option<Documentation>, | ||
47 | |||
48 | /// Whether this item is marked as deprecated | ||
49 | deprecated: bool, | ||
50 | } | ||
51 | |||
52 | // We use custom debug for CompletionItem to make `insta`'s diffs more readable. | ||
53 | impl fmt::Debug for CompletionItem { | ||
54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
55 | let mut s = f.debug_struct("CompletionItem"); | ||
56 | s.field("label", &self.label()).field("source_range", &self.source_range()); | ||
57 | if self.text_edit().as_atoms().len() == 1 { | ||
58 | let atom = &self.text_edit().as_atoms()[0]; | ||
59 | s.field("delete", &atom.delete); | ||
60 | s.field("insert", &atom.insert); | ||
61 | } else { | ||
62 | s.field("text_edit", &self.text_edit); | ||
63 | } | ||
64 | if let Some(kind) = self.kind().as_ref() { | ||
65 | s.field("kind", kind); | ||
66 | } | ||
67 | if self.lookup() != self.label() { | ||
68 | s.field("lookup", &self.lookup()); | ||
69 | } | ||
70 | if let Some(detail) = self.detail() { | ||
71 | s.field("detail", &detail); | ||
72 | } | ||
73 | if let Some(documentation) = self.documentation() { | ||
74 | s.field("documentation", &documentation); | ||
75 | } | ||
76 | if self.deprecated { | ||
77 | s.field("deprecated", &true); | ||
78 | } | ||
79 | s.finish() | ||
80 | } | ||
81 | } | ||
82 | |||
83 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
84 | pub enum CompletionItemKind { | ||
85 | Snippet, | ||
86 | Keyword, | ||
87 | Module, | ||
88 | Function, | ||
89 | BuiltinType, | ||
90 | Struct, | ||
91 | Enum, | ||
92 | EnumVariant, | ||
93 | Binding, | ||
94 | Field, | ||
95 | Static, | ||
96 | Const, | ||
97 | Trait, | ||
98 | TypeAlias, | ||
99 | Method, | ||
100 | TypeParam, | ||
101 | Macro, | ||
102 | } | ||
103 | |||
104 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
105 | pub(crate) enum CompletionKind { | ||
106 | /// Parser-based keyword completion. | ||
107 | Keyword, | ||
108 | /// Your usual "complete all valid identifiers". | ||
109 | Reference, | ||
110 | /// "Secret sauce" completions. | ||
111 | Magic, | ||
112 | Snippet, | ||
113 | Postfix, | ||
114 | BuiltinType, | ||
115 | } | ||
116 | |||
117 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] | ||
118 | pub enum InsertTextFormat { | ||
119 | PlainText, | ||
120 | Snippet, | ||
121 | } | ||
122 | |||
123 | impl CompletionItem { | ||
124 | pub(crate) fn new( | ||
125 | completion_kind: CompletionKind, | ||
126 | source_range: TextRange, | ||
127 | label: impl Into<String>, | ||
128 | ) -> Builder { | ||
129 | let label = label.into(); | ||
130 | Builder { | ||
131 | source_range, | ||
132 | completion_kind, | ||
133 | label, | ||
134 | insert_text: None, | ||
135 | insert_text_format: InsertTextFormat::PlainText, | ||
136 | detail: None, | ||
137 | documentation: None, | ||
138 | lookup: None, | ||
139 | kind: None, | ||
140 | text_edit: None, | ||
141 | deprecated: None, | ||
142 | } | ||
143 | } | ||
144 | /// What user sees in pop-up in the UI. | ||
145 | pub fn label(&self) -> &str { | ||
146 | &self.label | ||
147 | } | ||
148 | pub fn source_range(&self) -> TextRange { | ||
149 | self.source_range | ||
150 | } | ||
151 | |||
152 | pub fn insert_text_format(&self) -> InsertTextFormat { | ||
153 | self.insert_text_format | ||
154 | } | ||
155 | |||
156 | pub fn text_edit(&self) -> &TextEdit { | ||
157 | &self.text_edit | ||
158 | } | ||
159 | |||
160 | /// Short one-line additional information, like a type | ||
161 | pub fn detail(&self) -> Option<&str> { | ||
162 | self.detail.as_ref().map(|it| it.as_str()) | ||
163 | } | ||
164 | /// A doc-comment | ||
165 | pub fn documentation(&self) -> Option<Documentation> { | ||
166 | self.documentation.clone() | ||
167 | } | ||
168 | /// What string is used for filtering. | ||
169 | pub fn lookup(&self) -> &str { | ||
170 | self.lookup.as_ref().map(|it| it.as_str()).unwrap_or_else(|| self.label()) | ||
171 | } | ||
172 | |||
173 | pub fn kind(&self) -> Option<CompletionItemKind> { | ||
174 | self.kind | ||
175 | } | ||
176 | |||
177 | pub fn deprecated(&self) -> bool { | ||
178 | self.deprecated | ||
179 | } | ||
180 | } | ||
181 | |||
182 | /// A helper to make `CompletionItem`s. | ||
183 | #[must_use] | ||
184 | pub(crate) struct Builder { | ||
185 | source_range: TextRange, | ||
186 | completion_kind: CompletionKind, | ||
187 | label: String, | ||
188 | insert_text: Option<String>, | ||
189 | insert_text_format: InsertTextFormat, | ||
190 | detail: Option<String>, | ||
191 | documentation: Option<Documentation>, | ||
192 | lookup: Option<String>, | ||
193 | kind: Option<CompletionItemKind>, | ||
194 | text_edit: Option<TextEdit>, | ||
195 | deprecated: Option<bool>, | ||
196 | } | ||
197 | |||
198 | impl Builder { | ||
199 | pub(crate) fn add_to(self, acc: &mut Completions) { | ||
200 | acc.add(self.build()) | ||
201 | } | ||
202 | |||
203 | pub(crate) fn build(self) -> CompletionItem { | ||
204 | let label = self.label; | ||
205 | let text_edit = match self.text_edit { | ||
206 | Some(it) => it, | ||
207 | None => TextEdit::replace( | ||
208 | self.source_range, | ||
209 | self.insert_text.unwrap_or_else(|| label.clone()), | ||
210 | ), | ||
211 | }; | ||
212 | |||
213 | CompletionItem { | ||
214 | source_range: self.source_range, | ||
215 | label, | ||
216 | insert_text_format: self.insert_text_format, | ||
217 | text_edit, | ||
218 | detail: self.detail, | ||
219 | documentation: self.documentation, | ||
220 | lookup: self.lookup, | ||
221 | kind: self.kind, | ||
222 | completion_kind: self.completion_kind, | ||
223 | deprecated: self.deprecated.unwrap_or(false), | ||
224 | } | ||
225 | } | ||
226 | pub(crate) fn lookup_by(mut self, lookup: impl Into<String>) -> Builder { | ||
227 | self.lookup = Some(lookup.into()); | ||
228 | self | ||
229 | } | ||
230 | pub(crate) fn label(mut self, label: impl Into<String>) -> Builder { | ||
231 | self.label = label.into(); | ||
232 | self | ||
233 | } | ||
234 | pub(crate) fn insert_text(mut self, insert_text: impl Into<String>) -> Builder { | ||
235 | self.insert_text = Some(insert_text.into()); | ||
236 | self | ||
237 | } | ||
238 | pub(crate) fn insert_snippet(mut self, snippet: impl Into<String>) -> Builder { | ||
239 | self.insert_text_format = InsertTextFormat::Snippet; | ||
240 | self.insert_text(snippet) | ||
241 | } | ||
242 | pub(crate) fn kind(mut self, kind: CompletionItemKind) -> Builder { | ||
243 | self.kind = Some(kind); | ||
244 | self | ||
245 | } | ||
246 | pub(crate) fn text_edit(mut self, edit: TextEdit) -> Builder { | ||
247 | self.text_edit = Some(edit); | ||
248 | self | ||
249 | } | ||
250 | pub(crate) fn snippet_edit(mut self, edit: TextEdit) -> Builder { | ||
251 | self.insert_text_format = InsertTextFormat::Snippet; | ||
252 | self.text_edit(edit) | ||
253 | } | ||
254 | #[allow(unused)] | ||
255 | pub(crate) fn detail(self, detail: impl Into<String>) -> Builder { | ||
256 | self.set_detail(Some(detail)) | ||
257 | } | ||
258 | pub(crate) fn set_detail(mut self, detail: Option<impl Into<String>>) -> Builder { | ||
259 | self.detail = detail.map(Into::into); | ||
260 | self | ||
261 | } | ||
262 | #[allow(unused)] | ||
263 | pub(crate) fn documentation(self, docs: Documentation) -> Builder { | ||
264 | self.set_documentation(Some(docs)) | ||
265 | } | ||
266 | pub(crate) fn set_documentation(mut self, docs: Option<Documentation>) -> Builder { | ||
267 | self.documentation = docs.map(Into::into); | ||
268 | self | ||
269 | } | ||
270 | pub(crate) fn set_deprecated(mut self, deprecated: bool) -> Builder { | ||
271 | self.deprecated = Some(deprecated); | ||
272 | self | ||
273 | } | ||
274 | } | ||
275 | |||
276 | impl<'a> Into<CompletionItem> for Builder { | ||
277 | fn into(self) -> CompletionItem { | ||
278 | self.build() | ||
279 | } | ||
280 | } | ||
281 | |||
282 | /// Represents an in-progress set of completions being built. | ||
283 | #[derive(Debug, Default)] | ||
284 | pub(crate) struct Completions { | ||
285 | buf: Vec<CompletionItem>, | ||
286 | } | ||
287 | |||
288 | impl Completions { | ||
289 | pub(crate) fn add(&mut self, item: impl Into<CompletionItem>) { | ||
290 | self.buf.push(item.into()) | ||
291 | } | ||
292 | pub(crate) fn add_all<I>(&mut self, items: I) | ||
293 | where | ||
294 | I: IntoIterator, | ||
295 | I::Item: Into<CompletionItem>, | ||
296 | { | ||
297 | items.into_iter().for_each(|item| self.add(item.into())) | ||
298 | } | ||
299 | } | ||
300 | |||
301 | impl Into<Vec<CompletionItem>> for Completions { | ||
302 | fn into(self) -> Vec<CompletionItem> { | ||
303 | self.buf | ||
304 | } | ||
305 | } | ||
306 | |||
307 | #[cfg(test)] | ||
308 | pub(crate) fn do_completion(code: &str, kind: CompletionKind) -> Vec<CompletionItem> { | ||
309 | use crate::completion::completions; | ||
310 | use crate::mock_analysis::{analysis_and_position, single_file_with_position}; | ||
311 | let (analysis, position) = if code.contains("//-") { | ||
312 | analysis_and_position(code) | ||
313 | } else { | ||
314 | single_file_with_position(code) | ||
315 | }; | ||
316 | let completions = completions(&analysis.db, position).unwrap(); | ||
317 | let completion_items: Vec<CompletionItem> = completions.into(); | ||
318 | let mut kind_completions: Vec<CompletionItem> = | ||
319 | completion_items.into_iter().filter(|c| c.completion_kind == kind).collect(); | ||
320 | kind_completions.sort_by_key(|c| c.label.clone()); | ||
321 | kind_completions | ||
322 | } | ||
diff --git a/crates/ra_ide/src/completion/presentation.rs b/crates/ra_ide/src/completion/presentation.rs new file mode 100644 index 000000000..97475fc0b --- /dev/null +++ b/crates/ra_ide/src/completion/presentation.rs | |||
@@ -0,0 +1,673 @@ | |||
1 | //! This modules takes care of rendering various definitions as completion items. | ||
2 | |||
3 | use hir::{db::HirDatabase, Docs, HasAttrs, HasSource, HirDisplay, ScopeDef, Type}; | ||
4 | use join_to_string::join; | ||
5 | use ra_syntax::ast::NameOwner; | ||
6 | use test_utils::tested_by; | ||
7 | |||
8 | use crate::completion::{ | ||
9 | CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, | ||
10 | }; | ||
11 | |||
12 | use crate::display::{const_label, function_label, macro_label, type_label}; | ||
13 | |||
14 | impl Completions { | ||
15 | pub(crate) fn add_field( | ||
16 | &mut self, | ||
17 | ctx: &CompletionContext, | ||
18 | field: hir::StructField, | ||
19 | ty: &Type, | ||
20 | ) { | ||
21 | let is_deprecated = is_deprecated(field, ctx.db); | ||
22 | CompletionItem::new( | ||
23 | CompletionKind::Reference, | ||
24 | ctx.source_range(), | ||
25 | field.name(ctx.db).to_string(), | ||
26 | ) | ||
27 | .kind(CompletionItemKind::Field) | ||
28 | .detail(ty.display(ctx.db).to_string()) | ||
29 | .set_documentation(field.docs(ctx.db)) | ||
30 | .set_deprecated(is_deprecated) | ||
31 | .add_to(self); | ||
32 | } | ||
33 | |||
34 | pub(crate) fn add_tuple_field(&mut self, ctx: &CompletionContext, field: usize, ty: &Type) { | ||
35 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), field.to_string()) | ||
36 | .kind(CompletionItemKind::Field) | ||
37 | .detail(ty.display(ctx.db).to_string()) | ||
38 | .add_to(self); | ||
39 | } | ||
40 | |||
41 | pub(crate) fn add_resolution( | ||
42 | &mut self, | ||
43 | ctx: &CompletionContext, | ||
44 | local_name: String, | ||
45 | resolution: &ScopeDef, | ||
46 | ) { | ||
47 | use hir::ModuleDef::*; | ||
48 | |||
49 | let completion_kind = match resolution { | ||
50 | ScopeDef::ModuleDef(BuiltinType(..)) => CompletionKind::BuiltinType, | ||
51 | _ => CompletionKind::Reference, | ||
52 | }; | ||
53 | |||
54 | let kind = match resolution { | ||
55 | ScopeDef::ModuleDef(Module(..)) => CompletionItemKind::Module, | ||
56 | ScopeDef::ModuleDef(Function(func)) => { | ||
57 | return self.add_function_with_name(ctx, Some(local_name), *func); | ||
58 | } | ||
59 | ScopeDef::ModuleDef(Adt(hir::Adt::Struct(_))) => CompletionItemKind::Struct, | ||
60 | // FIXME: add CompletionItemKind::Union | ||
61 | ScopeDef::ModuleDef(Adt(hir::Adt::Union(_))) => CompletionItemKind::Struct, | ||
62 | ScopeDef::ModuleDef(Adt(hir::Adt::Enum(_))) => CompletionItemKind::Enum, | ||
63 | |||
64 | ScopeDef::ModuleDef(EnumVariant(..)) => CompletionItemKind::EnumVariant, | ||
65 | ScopeDef::ModuleDef(Const(..)) => CompletionItemKind::Const, | ||
66 | ScopeDef::ModuleDef(Static(..)) => CompletionItemKind::Static, | ||
67 | ScopeDef::ModuleDef(Trait(..)) => CompletionItemKind::Trait, | ||
68 | ScopeDef::ModuleDef(TypeAlias(..)) => CompletionItemKind::TypeAlias, | ||
69 | ScopeDef::ModuleDef(BuiltinType(..)) => CompletionItemKind::BuiltinType, | ||
70 | ScopeDef::GenericParam(..) => CompletionItemKind::TypeParam, | ||
71 | ScopeDef::Local(..) => CompletionItemKind::Binding, | ||
72 | // (does this need its own kind?) | ||
73 | ScopeDef::AdtSelfType(..) | ScopeDef::ImplSelfType(..) => CompletionItemKind::TypeParam, | ||
74 | ScopeDef::MacroDef(mac) => { | ||
75 | return self.add_macro(ctx, Some(local_name), *mac); | ||
76 | } | ||
77 | ScopeDef::Unknown => { | ||
78 | return self.add(CompletionItem::new( | ||
79 | CompletionKind::Reference, | ||
80 | ctx.source_range(), | ||
81 | local_name, | ||
82 | )); | ||
83 | } | ||
84 | }; | ||
85 | |||
86 | let docs = match resolution { | ||
87 | ScopeDef::ModuleDef(Module(it)) => it.docs(ctx.db), | ||
88 | ScopeDef::ModuleDef(Adt(it)) => it.docs(ctx.db), | ||
89 | ScopeDef::ModuleDef(EnumVariant(it)) => it.docs(ctx.db), | ||
90 | ScopeDef::ModuleDef(Const(it)) => it.docs(ctx.db), | ||
91 | ScopeDef::ModuleDef(Static(it)) => it.docs(ctx.db), | ||
92 | ScopeDef::ModuleDef(Trait(it)) => it.docs(ctx.db), | ||
93 | ScopeDef::ModuleDef(TypeAlias(it)) => it.docs(ctx.db), | ||
94 | _ => None, | ||
95 | }; | ||
96 | |||
97 | let mut completion_item = | ||
98 | CompletionItem::new(completion_kind, ctx.source_range(), local_name.clone()); | ||
99 | if let ScopeDef::Local(local) = resolution { | ||
100 | let ty = local.ty(ctx.db); | ||
101 | if !ty.is_unknown() { | ||
102 | completion_item = completion_item.detail(ty.display(ctx.db).to_string()); | ||
103 | } | ||
104 | }; | ||
105 | |||
106 | // If not an import, add parenthesis automatically. | ||
107 | if ctx.is_path_type | ||
108 | && !ctx.has_type_args | ||
109 | && ctx.db.feature_flags.get("completion.insertion.add-call-parenthesis") | ||
110 | { | ||
111 | let has_non_default_type_params = match resolution { | ||
112 | ScopeDef::ModuleDef(Adt(it)) => it.has_non_default_type_params(ctx.db), | ||
113 | ScopeDef::ModuleDef(TypeAlias(it)) => it.has_non_default_type_params(ctx.db), | ||
114 | _ => false, | ||
115 | }; | ||
116 | if has_non_default_type_params { | ||
117 | tested_by!(inserts_angle_brackets_for_generics); | ||
118 | completion_item = completion_item | ||
119 | .lookup_by(local_name.clone()) | ||
120 | .label(format!("{}<…>", local_name)) | ||
121 | .insert_snippet(format!("{}<$0>", local_name)); | ||
122 | } | ||
123 | } | ||
124 | |||
125 | completion_item.kind(kind).set_documentation(docs).add_to(self) | ||
126 | } | ||
127 | |||
128 | pub(crate) fn add_function(&mut self, ctx: &CompletionContext, func: hir::Function) { | ||
129 | self.add_function_with_name(ctx, None, func) | ||
130 | } | ||
131 | |||
132 | fn guess_macro_braces(&self, macro_name: &str, docs: &str) -> &'static str { | ||
133 | let mut votes = [0, 0, 0]; | ||
134 | for (idx, s) in docs.match_indices(¯o_name) { | ||
135 | let (before, after) = (&docs[..idx], &docs[idx + s.len()..]); | ||
136 | // Ensure to match the full word | ||
137 | if after.starts_with('!') | ||
138 | && before | ||
139 | .chars() | ||
140 | .rev() | ||
141 | .next() | ||
142 | .map_or(true, |c| c != '_' && !c.is_ascii_alphanumeric()) | ||
143 | { | ||
144 | // It may have spaces before the braces like `foo! {}` | ||
145 | match after[1..].chars().find(|&c| !c.is_whitespace()) { | ||
146 | Some('{') => votes[0] += 1, | ||
147 | Some('[') => votes[1] += 1, | ||
148 | Some('(') => votes[2] += 1, | ||
149 | _ => {} | ||
150 | } | ||
151 | } | ||
152 | } | ||
153 | |||
154 | // Insert a space before `{}`. | ||
155 | // We prefer the last one when some votes equal. | ||
156 | *votes.iter().zip(&[" {$0}", "[$0]", "($0)"]).max_by_key(|&(&vote, _)| vote).unwrap().1 | ||
157 | } | ||
158 | |||
159 | pub(crate) fn add_macro( | ||
160 | &mut self, | ||
161 | ctx: &CompletionContext, | ||
162 | name: Option<String>, | ||
163 | macro_: hir::MacroDef, | ||
164 | ) { | ||
165 | let name = match name { | ||
166 | Some(it) => it, | ||
167 | None => return, | ||
168 | }; | ||
169 | |||
170 | let ast_node = macro_.source(ctx.db).value; | ||
171 | let detail = macro_label(&ast_node); | ||
172 | |||
173 | let docs = macro_.docs(ctx.db); | ||
174 | let macro_declaration = format!("{}!", name); | ||
175 | |||
176 | let mut builder = | ||
177 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), ¯o_declaration) | ||
178 | .kind(CompletionItemKind::Macro) | ||
179 | .set_documentation(docs.clone()) | ||
180 | .set_deprecated(is_deprecated(macro_, ctx.db)) | ||
181 | .detail(detail); | ||
182 | |||
183 | builder = if ctx.use_item_syntax.is_some() { | ||
184 | builder.insert_text(name) | ||
185 | } else { | ||
186 | let macro_braces_to_insert = | ||
187 | self.guess_macro_braces(&name, docs.as_ref().map_or("", |s| s.as_str())); | ||
188 | builder.insert_snippet(macro_declaration + macro_braces_to_insert) | ||
189 | }; | ||
190 | |||
191 | self.add(builder); | ||
192 | } | ||
193 | |||
194 | fn add_function_with_name( | ||
195 | &mut self, | ||
196 | ctx: &CompletionContext, | ||
197 | name: Option<String>, | ||
198 | func: hir::Function, | ||
199 | ) { | ||
200 | let func_name = func.name(ctx.db); | ||
201 | let has_self_param = func.has_self_param(ctx.db); | ||
202 | let params = func.params(ctx.db); | ||
203 | |||
204 | let name = name.unwrap_or_else(|| func_name.to_string()); | ||
205 | let ast_node = func.source(ctx.db).value; | ||
206 | let detail = function_label(&ast_node); | ||
207 | |||
208 | let mut builder = | ||
209 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.clone()) | ||
210 | .kind(if has_self_param { | ||
211 | CompletionItemKind::Method | ||
212 | } else { | ||
213 | CompletionItemKind::Function | ||
214 | }) | ||
215 | .set_documentation(func.docs(ctx.db)) | ||
216 | .set_deprecated(is_deprecated(func, ctx.db)) | ||
217 | .detail(detail); | ||
218 | |||
219 | // Add `<>` for generic types | ||
220 | if ctx.use_item_syntax.is_none() | ||
221 | && !ctx.is_call | ||
222 | && ctx.db.feature_flags.get("completion.insertion.add-call-parenthesis") | ||
223 | { | ||
224 | tested_by!(inserts_parens_for_function_calls); | ||
225 | let (snippet, label) = if params.is_empty() || has_self_param && params.len() == 1 { | ||
226 | (format!("{}()$0", func_name), format!("{}()", name)) | ||
227 | } else { | ||
228 | (format!("{}($0)", func_name), format!("{}(…)", name)) | ||
229 | }; | ||
230 | builder = builder.lookup_by(name).label(label).insert_snippet(snippet); | ||
231 | } | ||
232 | |||
233 | self.add(builder) | ||
234 | } | ||
235 | |||
236 | pub(crate) fn add_const(&mut self, ctx: &CompletionContext, constant: hir::Const) { | ||
237 | let ast_node = constant.source(ctx.db).value; | ||
238 | let name = match ast_node.name() { | ||
239 | Some(name) => name, | ||
240 | _ => return, | ||
241 | }; | ||
242 | let detail = const_label(&ast_node); | ||
243 | |||
244 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.text().to_string()) | ||
245 | .kind(CompletionItemKind::Const) | ||
246 | .set_documentation(constant.docs(ctx.db)) | ||
247 | .set_deprecated(is_deprecated(constant, ctx.db)) | ||
248 | .detail(detail) | ||
249 | .add_to(self); | ||
250 | } | ||
251 | |||
252 | pub(crate) fn add_type_alias(&mut self, ctx: &CompletionContext, type_alias: hir::TypeAlias) { | ||
253 | let type_def = type_alias.source(ctx.db).value; | ||
254 | let name = match type_def.name() { | ||
255 | Some(name) => name, | ||
256 | _ => return, | ||
257 | }; | ||
258 | let detail = type_label(&type_def); | ||
259 | |||
260 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.text().to_string()) | ||
261 | .kind(CompletionItemKind::TypeAlias) | ||
262 | .set_documentation(type_alias.docs(ctx.db)) | ||
263 | .set_deprecated(is_deprecated(type_alias, ctx.db)) | ||
264 | .detail(detail) | ||
265 | .add_to(self); | ||
266 | } | ||
267 | |||
268 | pub(crate) fn add_enum_variant(&mut self, ctx: &CompletionContext, variant: hir::EnumVariant) { | ||
269 | let is_deprecated = is_deprecated(variant, ctx.db); | ||
270 | let name = variant.name(ctx.db); | ||
271 | let detail_types = variant.fields(ctx.db).into_iter().map(|field| field.ty(ctx.db)); | ||
272 | let detail = join(detail_types.map(|t| t.display(ctx.db).to_string())) | ||
273 | .separator(", ") | ||
274 | .surround_with("(", ")") | ||
275 | .to_string(); | ||
276 | CompletionItem::new(CompletionKind::Reference, ctx.source_range(), name.to_string()) | ||
277 | .kind(CompletionItemKind::EnumVariant) | ||
278 | .set_documentation(variant.docs(ctx.db)) | ||
279 | .set_deprecated(is_deprecated) | ||
280 | .detail(detail) | ||
281 | .add_to(self); | ||
282 | } | ||
283 | } | ||
284 | |||
285 | fn is_deprecated(node: impl HasAttrs, db: &impl HirDatabase) -> bool { | ||
286 | node.attrs(db).by_key("deprecated").exists() | ||
287 | } | ||
288 | |||
289 | #[cfg(test)] | ||
290 | mod tests { | ||
291 | use insta::assert_debug_snapshot; | ||
292 | use test_utils::covers; | ||
293 | |||
294 | use crate::completion::{do_completion, CompletionItem, CompletionKind}; | ||
295 | |||
296 | fn do_reference_completion(code: &str) -> Vec<CompletionItem> { | ||
297 | do_completion(code, CompletionKind::Reference) | ||
298 | } | ||
299 | |||
300 | #[test] | ||
301 | fn sets_deprecated_flag_in_completion_items() { | ||
302 | assert_debug_snapshot!( | ||
303 | do_reference_completion( | ||
304 | r#" | ||
305 | #[deprecated] | ||
306 | fn something_deprecated() {} | ||
307 | |||
308 | #[deprecated(since = "1.0.0")] | ||
309 | fn something_else_deprecated() {} | ||
310 | |||
311 | fn main() { som<|> } | ||
312 | "#, | ||
313 | ), | ||
314 | @r###" | ||
315 | [ | ||
316 | CompletionItem { | ||
317 | label: "main()", | ||
318 | source_range: [203; 206), | ||
319 | delete: [203; 206), | ||
320 | insert: "main()$0", | ||
321 | kind: Function, | ||
322 | lookup: "main", | ||
323 | detail: "fn main()", | ||
324 | }, | ||
325 | CompletionItem { | ||
326 | label: "something_deprecated()", | ||
327 | source_range: [203; 206), | ||
328 | delete: [203; 206), | ||
329 | insert: "something_deprecated()$0", | ||
330 | kind: Function, | ||
331 | lookup: "something_deprecated", | ||
332 | detail: "fn something_deprecated()", | ||
333 | deprecated: true, | ||
334 | }, | ||
335 | CompletionItem { | ||
336 | label: "something_else_deprecated()", | ||
337 | source_range: [203; 206), | ||
338 | delete: [203; 206), | ||
339 | insert: "something_else_deprecated()$0", | ||
340 | kind: Function, | ||
341 | lookup: "something_else_deprecated", | ||
342 | detail: "fn something_else_deprecated()", | ||
343 | deprecated: true, | ||
344 | }, | ||
345 | ] | ||
346 | "### | ||
347 | ); | ||
348 | } | ||
349 | |||
350 | #[test] | ||
351 | fn inserts_parens_for_function_calls() { | ||
352 | covers!(inserts_parens_for_function_calls); | ||
353 | assert_debug_snapshot!( | ||
354 | do_reference_completion( | ||
355 | r" | ||
356 | fn no_args() {} | ||
357 | fn main() { no_<|> } | ||
358 | " | ||
359 | ), | ||
360 | @r###" | ||
361 | [ | ||
362 | CompletionItem { | ||
363 | label: "main()", | ||
364 | source_range: [61; 64), | ||
365 | delete: [61; 64), | ||
366 | insert: "main()$0", | ||
367 | kind: Function, | ||
368 | lookup: "main", | ||
369 | detail: "fn main()", | ||
370 | }, | ||
371 | CompletionItem { | ||
372 | label: "no_args()", | ||
373 | source_range: [61; 64), | ||
374 | delete: [61; 64), | ||
375 | insert: "no_args()$0", | ||
376 | kind: Function, | ||
377 | lookup: "no_args", | ||
378 | detail: "fn no_args()", | ||
379 | }, | ||
380 | ] | ||
381 | "### | ||
382 | ); | ||
383 | assert_debug_snapshot!( | ||
384 | do_reference_completion( | ||
385 | r" | ||
386 | fn with_args(x: i32, y: String) {} | ||
387 | fn main() { with_<|> } | ||
388 | " | ||
389 | ), | ||
390 | @r###" | ||
391 | [ | ||
392 | CompletionItem { | ||
393 | label: "main()", | ||
394 | source_range: [80; 85), | ||
395 | delete: [80; 85), | ||
396 | insert: "main()$0", | ||
397 | kind: Function, | ||
398 | lookup: "main", | ||
399 | detail: "fn main()", | ||
400 | }, | ||
401 | CompletionItem { | ||
402 | label: "with_args(…)", | ||
403 | source_range: [80; 85), | ||
404 | delete: [80; 85), | ||
405 | insert: "with_args($0)", | ||
406 | kind: Function, | ||
407 | lookup: "with_args", | ||
408 | detail: "fn with_args(x: i32, y: String)", | ||
409 | }, | ||
410 | ] | ||
411 | "### | ||
412 | ); | ||
413 | assert_debug_snapshot!( | ||
414 | do_reference_completion( | ||
415 | r" | ||
416 | struct S {} | ||
417 | impl S { | ||
418 | fn foo(&self) {} | ||
419 | } | ||
420 | fn bar(s: &S) { | ||
421 | s.f<|> | ||
422 | } | ||
423 | " | ||
424 | ), | ||
425 | @r###" | ||
426 | [ | ||
427 | CompletionItem { | ||
428 | label: "foo()", | ||
429 | source_range: [163; 164), | ||
430 | delete: [163; 164), | ||
431 | insert: "foo()$0", | ||
432 | kind: Method, | ||
433 | lookup: "foo", | ||
434 | detail: "fn foo(&self)", | ||
435 | }, | ||
436 | ] | ||
437 | "### | ||
438 | ); | ||
439 | } | ||
440 | |||
441 | #[test] | ||
442 | fn dont_render_function_parens_in_use_item() { | ||
443 | assert_debug_snapshot!( | ||
444 | do_reference_completion( | ||
445 | " | ||
446 | //- /lib.rs | ||
447 | mod m { pub fn foo() {} } | ||
448 | use crate::m::f<|>; | ||
449 | " | ||
450 | ), | ||
451 | @r###" | ||
452 | [ | ||
453 | CompletionItem { | ||
454 | label: "foo", | ||
455 | source_range: [40; 41), | ||
456 | delete: [40; 41), | ||
457 | insert: "foo", | ||
458 | kind: Function, | ||
459 | detail: "pub fn foo()", | ||
460 | }, | ||
461 | ] | ||
462 | "### | ||
463 | ); | ||
464 | } | ||
465 | |||
466 | #[test] | ||
467 | fn dont_render_function_parens_if_already_call() { | ||
468 | assert_debug_snapshot!( | ||
469 | do_reference_completion( | ||
470 | " | ||
471 | //- /lib.rs | ||
472 | fn frobnicate() {} | ||
473 | fn main() { | ||
474 | frob<|>(); | ||
475 | } | ||
476 | " | ||
477 | ), | ||
478 | @r###" | ||
479 | [ | ||
480 | CompletionItem { | ||
481 | label: "frobnicate", | ||
482 | source_range: [35; 39), | ||
483 | delete: [35; 39), | ||
484 | insert: "frobnicate", | ||
485 | kind: Function, | ||
486 | detail: "fn frobnicate()", | ||
487 | }, | ||
488 | CompletionItem { | ||
489 | label: "main", | ||
490 | source_range: [35; 39), | ||
491 | delete: [35; 39), | ||
492 | insert: "main", | ||
493 | kind: Function, | ||
494 | detail: "fn main()", | ||
495 | }, | ||
496 | ] | ||
497 | "### | ||
498 | ); | ||
499 | assert_debug_snapshot!( | ||
500 | do_reference_completion( | ||
501 | " | ||
502 | //- /lib.rs | ||
503 | struct Foo {} | ||
504 | impl Foo { fn new() -> Foo {} } | ||
505 | fn main() { | ||
506 | Foo::ne<|>(); | ||
507 | } | ||
508 | " | ||
509 | ), | ||
510 | @r###" | ||
511 | [ | ||
512 | CompletionItem { | ||
513 | label: "new", | ||
514 | source_range: [67; 69), | ||
515 | delete: [67; 69), | ||
516 | insert: "new", | ||
517 | kind: Function, | ||
518 | detail: "fn new() -> Foo", | ||
519 | }, | ||
520 | ] | ||
521 | "### | ||
522 | ); | ||
523 | } | ||
524 | |||
525 | #[test] | ||
526 | fn inserts_angle_brackets_for_generics() { | ||
527 | covers!(inserts_angle_brackets_for_generics); | ||
528 | assert_debug_snapshot!( | ||
529 | do_reference_completion( | ||
530 | r" | ||
531 | struct Vec<T> {} | ||
532 | fn foo(xs: Ve<|>) | ||
533 | " | ||
534 | ), | ||
535 | @r###" | ||
536 | [ | ||
537 | CompletionItem { | ||
538 | label: "Vec<…>", | ||
539 | source_range: [61; 63), | ||
540 | delete: [61; 63), | ||
541 | insert: "Vec<$0>", | ||
542 | kind: Struct, | ||
543 | lookup: "Vec", | ||
544 | }, | ||
545 | CompletionItem { | ||
546 | label: "foo(…)", | ||
547 | source_range: [61; 63), | ||
548 | delete: [61; 63), | ||
549 | insert: "foo($0)", | ||
550 | kind: Function, | ||
551 | lookup: "foo", | ||
552 | detail: "fn foo(xs: Ve)", | ||
553 | }, | ||
554 | ] | ||
555 | "### | ||
556 | ); | ||
557 | assert_debug_snapshot!( | ||
558 | do_reference_completion( | ||
559 | r" | ||
560 | type Vec<T> = (T,); | ||
561 | fn foo(xs: Ve<|>) | ||
562 | " | ||
563 | ), | ||
564 | @r###" | ||
565 | [ | ||
566 | CompletionItem { | ||
567 | label: "Vec<…>", | ||
568 | source_range: [64; 66), | ||
569 | delete: [64; 66), | ||
570 | insert: "Vec<$0>", | ||
571 | kind: TypeAlias, | ||
572 | lookup: "Vec", | ||
573 | }, | ||
574 | CompletionItem { | ||
575 | label: "foo(…)", | ||
576 | source_range: [64; 66), | ||
577 | delete: [64; 66), | ||
578 | insert: "foo($0)", | ||
579 | kind: Function, | ||
580 | lookup: "foo", | ||
581 | detail: "fn foo(xs: Ve)", | ||
582 | }, | ||
583 | ] | ||
584 | "### | ||
585 | ); | ||
586 | assert_debug_snapshot!( | ||
587 | do_reference_completion( | ||
588 | r" | ||
589 | struct Vec<T = i128> {} | ||
590 | fn foo(xs: Ve<|>) | ||
591 | " | ||
592 | ), | ||
593 | @r###" | ||
594 | [ | ||
595 | CompletionItem { | ||
596 | label: "Vec", | ||
597 | source_range: [68; 70), | ||
598 | delete: [68; 70), | ||
599 | insert: "Vec", | ||
600 | kind: Struct, | ||
601 | }, | ||
602 | CompletionItem { | ||
603 | label: "foo(…)", | ||
604 | source_range: [68; 70), | ||
605 | delete: [68; 70), | ||
606 | insert: "foo($0)", | ||
607 | kind: Function, | ||
608 | lookup: "foo", | ||
609 | detail: "fn foo(xs: Ve)", | ||
610 | }, | ||
611 | ] | ||
612 | "### | ||
613 | ); | ||
614 | assert_debug_snapshot!( | ||
615 | do_reference_completion( | ||
616 | r" | ||
617 | struct Vec<T> {} | ||
618 | fn foo(xs: Ve<|><i128>) | ||
619 | " | ||
620 | ), | ||
621 | @r###" | ||
622 | [ | ||
623 | CompletionItem { | ||
624 | label: "Vec", | ||
625 | source_range: [61; 63), | ||
626 | delete: [61; 63), | ||
627 | insert: "Vec", | ||
628 | kind: Struct, | ||
629 | }, | ||
630 | CompletionItem { | ||
631 | label: "foo(…)", | ||
632 | source_range: [61; 63), | ||
633 | delete: [61; 63), | ||
634 | insert: "foo($0)", | ||
635 | kind: Function, | ||
636 | lookup: "foo", | ||
637 | detail: "fn foo(xs: Ve<i128>)", | ||
638 | }, | ||
639 | ] | ||
640 | "### | ||
641 | ); | ||
642 | } | ||
643 | |||
644 | #[test] | ||
645 | fn dont_insert_macro_call_braces_in_use() { | ||
646 | assert_debug_snapshot!( | ||
647 | do_reference_completion( | ||
648 | r" | ||
649 | //- /main.rs | ||
650 | use foo::<|>; | ||
651 | |||
652 | //- /foo/lib.rs | ||
653 | #[macro_export] | ||
654 | macro_rules frobnicate { | ||
655 | () => () | ||
656 | } | ||
657 | " | ||
658 | ), | ||
659 | @r###" | ||
660 | [ | ||
661 | CompletionItem { | ||
662 | label: "frobnicate!", | ||
663 | source_range: [9; 9), | ||
664 | delete: [9; 9), | ||
665 | insert: "frobnicate", | ||
666 | kind: Macro, | ||
667 | detail: "#[macro_export]\nmacro_rules! frobnicate", | ||
668 | }, | ||
669 | ] | ||
670 | "### | ||
671 | ) | ||
672 | } | ||
673 | } | ||
diff --git a/crates/ra_ide/src/db.rs b/crates/ra_ide/src/db.rs new file mode 100644 index 000000000..f739ebecd --- /dev/null +++ b/crates/ra_ide/src/db.rs | |||
@@ -0,0 +1,144 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::sync::Arc; | ||
4 | |||
5 | use ra_db::{ | ||
6 | salsa::{self, Database, Durability}, | ||
7 | Canceled, CheckCanceled, CrateId, FileId, FileLoader, FileLoaderDelegate, RelativePath, | ||
8 | SourceDatabase, SourceDatabaseExt, SourceRootId, | ||
9 | }; | ||
10 | use rustc_hash::FxHashMap; | ||
11 | |||
12 | use crate::{ | ||
13 | symbol_index::{self, SymbolsDatabase}, | ||
14 | FeatureFlags, LineIndex, | ||
15 | }; | ||
16 | |||
17 | #[salsa::database( | ||
18 | ra_db::SourceDatabaseStorage, | ||
19 | ra_db::SourceDatabaseExtStorage, | ||
20 | LineIndexDatabaseStorage, | ||
21 | symbol_index::SymbolsDatabaseStorage, | ||
22 | hir::db::InternDatabaseStorage, | ||
23 | hir::db::AstDatabaseStorage, | ||
24 | hir::db::DefDatabaseStorage, | ||
25 | hir::db::HirDatabaseStorage | ||
26 | )] | ||
27 | #[derive(Debug)] | ||
28 | pub(crate) struct RootDatabase { | ||
29 | runtime: salsa::Runtime<RootDatabase>, | ||
30 | pub(crate) feature_flags: Arc<FeatureFlags>, | ||
31 | pub(crate) debug_data: Arc<DebugData>, | ||
32 | pub(crate) last_gc: crate::wasm_shims::Instant, | ||
33 | pub(crate) last_gc_check: crate::wasm_shims::Instant, | ||
34 | } | ||
35 | |||
36 | impl FileLoader for RootDatabase { | ||
37 | fn file_text(&self, file_id: FileId) -> Arc<String> { | ||
38 | FileLoaderDelegate(self).file_text(file_id) | ||
39 | } | ||
40 | fn resolve_relative_path( | ||
41 | &self, | ||
42 | anchor: FileId, | ||
43 | relative_path: &RelativePath, | ||
44 | ) -> Option<FileId> { | ||
45 | FileLoaderDelegate(self).resolve_relative_path(anchor, relative_path) | ||
46 | } | ||
47 | fn relevant_crates(&self, file_id: FileId) -> Arc<Vec<CrateId>> { | ||
48 | FileLoaderDelegate(self).relevant_crates(file_id) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl hir::debug::HirDebugHelper for RootDatabase { | ||
53 | fn crate_name(&self, krate: CrateId) -> Option<String> { | ||
54 | self.debug_data.crate_names.get(&krate).cloned() | ||
55 | } | ||
56 | fn file_path(&self, file_id: FileId) -> Option<String> { | ||
57 | let source_root_id = self.file_source_root(file_id); | ||
58 | let source_root_path = self.debug_data.root_paths.get(&source_root_id)?; | ||
59 | let file_path = self.file_relative_path(file_id); | ||
60 | Some(format!("{}/{}", source_root_path, file_path)) | ||
61 | } | ||
62 | } | ||
63 | |||
64 | impl salsa::Database for RootDatabase { | ||
65 | fn salsa_runtime(&self) -> &salsa::Runtime<RootDatabase> { | ||
66 | &self.runtime | ||
67 | } | ||
68 | fn salsa_runtime_mut(&mut self) -> &mut salsa::Runtime<Self> { | ||
69 | &mut self.runtime | ||
70 | } | ||
71 | fn on_propagated_panic(&self) -> ! { | ||
72 | Canceled::throw() | ||
73 | } | ||
74 | fn salsa_event(&self, event: impl Fn() -> salsa::Event<RootDatabase>) { | ||
75 | match event().kind { | ||
76 | salsa::EventKind::DidValidateMemoizedValue { .. } | ||
77 | | salsa::EventKind::WillExecute { .. } => { | ||
78 | self.check_canceled(); | ||
79 | } | ||
80 | _ => (), | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | |||
85 | impl Default for RootDatabase { | ||
86 | fn default() -> RootDatabase { | ||
87 | RootDatabase::new(None, FeatureFlags::default()) | ||
88 | } | ||
89 | } | ||
90 | |||
91 | impl RootDatabase { | ||
92 | pub fn new(lru_capacity: Option<usize>, feature_flags: FeatureFlags) -> RootDatabase { | ||
93 | let mut db = RootDatabase { | ||
94 | runtime: salsa::Runtime::default(), | ||
95 | last_gc: crate::wasm_shims::Instant::now(), | ||
96 | last_gc_check: crate::wasm_shims::Instant::now(), | ||
97 | feature_flags: Arc::new(feature_flags), | ||
98 | debug_data: Default::default(), | ||
99 | }; | ||
100 | db.set_crate_graph_with_durability(Default::default(), Durability::HIGH); | ||
101 | db.set_local_roots_with_durability(Default::default(), Durability::HIGH); | ||
102 | db.set_library_roots_with_durability(Default::default(), Durability::HIGH); | ||
103 | let lru_capacity = lru_capacity.unwrap_or(ra_db::DEFAULT_LRU_CAP); | ||
104 | db.query_mut(ra_db::ParseQuery).set_lru_capacity(lru_capacity); | ||
105 | db.query_mut(hir::db::ParseMacroQuery).set_lru_capacity(lru_capacity); | ||
106 | db.query_mut(hir::db::MacroExpandQuery).set_lru_capacity(lru_capacity); | ||
107 | db | ||
108 | } | ||
109 | } | ||
110 | |||
111 | impl salsa::ParallelDatabase for RootDatabase { | ||
112 | fn snapshot(&self) -> salsa::Snapshot<RootDatabase> { | ||
113 | salsa::Snapshot::new(RootDatabase { | ||
114 | runtime: self.runtime.snapshot(self), | ||
115 | last_gc: self.last_gc, | ||
116 | last_gc_check: self.last_gc_check, | ||
117 | feature_flags: Arc::clone(&self.feature_flags), | ||
118 | debug_data: Arc::clone(&self.debug_data), | ||
119 | }) | ||
120 | } | ||
121 | } | ||
122 | |||
123 | #[salsa::query_group(LineIndexDatabaseStorage)] | ||
124 | pub(crate) trait LineIndexDatabase: ra_db::SourceDatabase + CheckCanceled { | ||
125 | fn line_index(&self, file_id: FileId) -> Arc<LineIndex>; | ||
126 | } | ||
127 | |||
128 | fn line_index(db: &impl LineIndexDatabase, file_id: FileId) -> Arc<LineIndex> { | ||
129 | let text = db.file_text(file_id); | ||
130 | Arc::new(LineIndex::new(&*text)) | ||
131 | } | ||
132 | |||
133 | #[derive(Debug, Default, Clone)] | ||
134 | pub(crate) struct DebugData { | ||
135 | pub(crate) root_paths: FxHashMap<SourceRootId, String>, | ||
136 | pub(crate) crate_names: FxHashMap<CrateId, String>, | ||
137 | } | ||
138 | |||
139 | impl DebugData { | ||
140 | pub(crate) fn merge(&mut self, other: DebugData) { | ||
141 | self.root_paths.extend(other.root_paths.into_iter()); | ||
142 | self.crate_names.extend(other.crate_names.into_iter()); | ||
143 | } | ||
144 | } | ||
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs new file mode 100644 index 000000000..cc1ccab4b --- /dev/null +++ b/crates/ra_ide/src/diagnostics.rs | |||
@@ -0,0 +1,652 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::cell::RefCell; | ||
4 | |||
5 | use hir::diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink}; | ||
6 | use itertools::Itertools; | ||
7 | use ra_db::{RelativePath, SourceDatabase, SourceDatabaseExt}; | ||
8 | use ra_prof::profile; | ||
9 | use ra_syntax::{ | ||
10 | algo, | ||
11 | ast::{self, make, AstNode}, | ||
12 | Location, SyntaxNode, TextRange, T, | ||
13 | }; | ||
14 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
15 | |||
16 | use crate::{db::RootDatabase, Diagnostic, FileId, FileSystemEdit, SourceChange, SourceFileEdit}; | ||
17 | |||
18 | #[derive(Debug, Copy, Clone)] | ||
19 | pub enum Severity { | ||
20 | Error, | ||
21 | WeakWarning, | ||
22 | } | ||
23 | |||
24 | pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> { | ||
25 | let _p = profile("diagnostics"); | ||
26 | let parse = db.parse(file_id); | ||
27 | let mut res = Vec::new(); | ||
28 | |||
29 | res.extend(parse.errors().iter().map(|err| Diagnostic { | ||
30 | range: location_to_range(err.location()), | ||
31 | message: format!("Syntax Error: {}", err), | ||
32 | severity: Severity::Error, | ||
33 | fix: None, | ||
34 | })); | ||
35 | |||
36 | for node in parse.tree().syntax().descendants() { | ||
37 | check_unnecessary_braces_in_use_statement(&mut res, file_id, &node); | ||
38 | check_struct_shorthand_initialization(&mut res, file_id, &node); | ||
39 | } | ||
40 | let res = RefCell::new(res); | ||
41 | let mut sink = DiagnosticSink::new(|d| { | ||
42 | res.borrow_mut().push(Diagnostic { | ||
43 | message: d.message(), | ||
44 | range: d.highlight_range(), | ||
45 | severity: Severity::Error, | ||
46 | fix: None, | ||
47 | }) | ||
48 | }) | ||
49 | .on::<hir::diagnostics::UnresolvedModule, _>(|d| { | ||
50 | let original_file = d.source().file_id.original_file(db); | ||
51 | let source_root = db.file_source_root(original_file); | ||
52 | let path = db | ||
53 | .file_relative_path(original_file) | ||
54 | .parent() | ||
55 | .unwrap_or_else(|| RelativePath::new("")) | ||
56 | .join(&d.candidate); | ||
57 | let create_file = FileSystemEdit::CreateFile { source_root, path }; | ||
58 | let fix = SourceChange::file_system_edit("create module", create_file); | ||
59 | res.borrow_mut().push(Diagnostic { | ||
60 | range: d.highlight_range(), | ||
61 | message: d.message(), | ||
62 | severity: Severity::Error, | ||
63 | fix: Some(fix), | ||
64 | }) | ||
65 | }) | ||
66 | .on::<hir::diagnostics::MissingFields, _>(|d| { | ||
67 | let mut field_list = d.ast(db); | ||
68 | for f in d.missed_fields.iter() { | ||
69 | let field = make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit())); | ||
70 | field_list = field_list.append_field(&field); | ||
71 | } | ||
72 | |||
73 | let mut builder = TextEditBuilder::default(); | ||
74 | algo::diff(&d.ast(db).syntax(), &field_list.syntax()).into_text_edit(&mut builder); | ||
75 | |||
76 | let fix = | ||
77 | SourceChange::source_file_edit_from("fill struct fields", file_id, builder.finish()); | ||
78 | res.borrow_mut().push(Diagnostic { | ||
79 | range: d.highlight_range(), | ||
80 | message: d.message(), | ||
81 | severity: Severity::Error, | ||
82 | fix: Some(fix), | ||
83 | }) | ||
84 | }) | ||
85 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | ||
86 | let node = d.ast(db); | ||
87 | let replacement = format!("Ok({})", node.syntax()); | ||
88 | let edit = TextEdit::replace(node.syntax().text_range(), replacement); | ||
89 | let fix = SourceChange::source_file_edit_from("wrap with ok", file_id, edit); | ||
90 | res.borrow_mut().push(Diagnostic { | ||
91 | range: d.highlight_range(), | ||
92 | message: d.message(), | ||
93 | severity: Severity::Error, | ||
94 | fix: Some(fix), | ||
95 | }) | ||
96 | }); | ||
97 | let source_file = db.parse(file_id).tree(); | ||
98 | let src = | ||
99 | hir::Source { file_id: file_id.into(), value: hir::ModuleSource::SourceFile(source_file) }; | ||
100 | if let Some(m) = hir::Module::from_definition(db, src) { | ||
101 | m.diagnostics(db, &mut sink); | ||
102 | }; | ||
103 | drop(sink); | ||
104 | res.into_inner() | ||
105 | } | ||
106 | fn location_to_range(location: Location) -> TextRange { | ||
107 | match location { | ||
108 | Location::Offset(offset) => TextRange::offset_len(offset, 1.into()), | ||
109 | Location::Range(range) => range, | ||
110 | } | ||
111 | } | ||
112 | |||
113 | fn check_unnecessary_braces_in_use_statement( | ||
114 | acc: &mut Vec<Diagnostic>, | ||
115 | file_id: FileId, | ||
116 | node: &SyntaxNode, | ||
117 | ) -> Option<()> { | ||
118 | let use_tree_list = ast::UseTreeList::cast(node.clone())?; | ||
119 | if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { | ||
120 | let range = use_tree_list.syntax().text_range(); | ||
121 | let edit = | ||
122 | text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) | ||
123 | .unwrap_or_else(|| { | ||
124 | let to_replace = single_use_tree.syntax().text().to_string(); | ||
125 | let mut edit_builder = TextEditBuilder::default(); | ||
126 | edit_builder.delete(range); | ||
127 | edit_builder.insert(range.start(), to_replace); | ||
128 | edit_builder.finish() | ||
129 | }); | ||
130 | |||
131 | acc.push(Diagnostic { | ||
132 | range, | ||
133 | message: "Unnecessary braces in use statement".to_string(), | ||
134 | severity: Severity::WeakWarning, | ||
135 | fix: Some(SourceChange::source_file_edit( | ||
136 | "Remove unnecessary braces", | ||
137 | SourceFileEdit { file_id, edit }, | ||
138 | )), | ||
139 | }); | ||
140 | } | ||
141 | |||
142 | Some(()) | ||
143 | } | ||
144 | |||
145 | fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( | ||
146 | single_use_tree: &ast::UseTree, | ||
147 | ) -> Option<TextEdit> { | ||
148 | let use_tree_list_node = single_use_tree.syntax().parent()?; | ||
149 | if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { | ||
150 | let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); | ||
151 | let end = use_tree_list_node.text_range().end(); | ||
152 | let range = TextRange::from_to(start, end); | ||
153 | return Some(TextEdit::delete(range)); | ||
154 | } | ||
155 | None | ||
156 | } | ||
157 | |||
158 | fn check_struct_shorthand_initialization( | ||
159 | acc: &mut Vec<Diagnostic>, | ||
160 | file_id: FileId, | ||
161 | node: &SyntaxNode, | ||
162 | ) -> Option<()> { | ||
163 | let record_lit = ast::RecordLit::cast(node.clone())?; | ||
164 | let record_field_list = record_lit.record_field_list()?; | ||
165 | for record_field in record_field_list.fields() { | ||
166 | if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) { | ||
167 | let field_name = name_ref.syntax().text().to_string(); | ||
168 | let field_expr = expr.syntax().text().to_string(); | ||
169 | if field_name == field_expr { | ||
170 | let mut edit_builder = TextEditBuilder::default(); | ||
171 | edit_builder.delete(record_field.syntax().text_range()); | ||
172 | edit_builder.insert(record_field.syntax().text_range().start(), field_name); | ||
173 | let edit = edit_builder.finish(); | ||
174 | |||
175 | acc.push(Diagnostic { | ||
176 | range: record_field.syntax().text_range(), | ||
177 | message: "Shorthand struct initialization".to_string(), | ||
178 | severity: Severity::WeakWarning, | ||
179 | fix: Some(SourceChange::source_file_edit( | ||
180 | "use struct shorthand initialization", | ||
181 | SourceFileEdit { file_id, edit }, | ||
182 | )), | ||
183 | }); | ||
184 | } | ||
185 | } | ||
186 | } | ||
187 | Some(()) | ||
188 | } | ||
189 | |||
190 | #[cfg(test)] | ||
191 | mod tests { | ||
192 | use insta::assert_debug_snapshot; | ||
193 | use join_to_string::join; | ||
194 | use ra_syntax::SourceFile; | ||
195 | use test_utils::assert_eq_text; | ||
196 | |||
197 | use crate::mock_analysis::{analysis_and_position, single_file}; | ||
198 | |||
199 | use super::*; | ||
200 | |||
201 | type DiagnosticChecker = fn(&mut Vec<Diagnostic>, FileId, &SyntaxNode) -> Option<()>; | ||
202 | |||
203 | fn check_not_applicable(code: &str, func: DiagnosticChecker) { | ||
204 | let parse = SourceFile::parse(code); | ||
205 | let mut diagnostics = Vec::new(); | ||
206 | for node in parse.tree().syntax().descendants() { | ||
207 | func(&mut diagnostics, FileId(0), &node); | ||
208 | } | ||
209 | assert!(diagnostics.is_empty()); | ||
210 | } | ||
211 | |||
212 | fn check_apply(before: &str, after: &str, func: DiagnosticChecker) { | ||
213 | let parse = SourceFile::parse(before); | ||
214 | let mut diagnostics = Vec::new(); | ||
215 | for node in parse.tree().syntax().descendants() { | ||
216 | func(&mut diagnostics, FileId(0), &node); | ||
217 | } | ||
218 | let diagnostic = | ||
219 | diagnostics.pop().unwrap_or_else(|| panic!("no diagnostics for:\n{}\n", before)); | ||
220 | let mut fix = diagnostic.fix.unwrap(); | ||
221 | let edit = fix.source_file_edits.pop().unwrap().edit; | ||
222 | let actual = edit.apply(&before); | ||
223 | assert_eq_text!(after, &actual); | ||
224 | } | ||
225 | |||
226 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
227 | /// and checks that: | ||
228 | /// * a diagnostic is produced | ||
229 | /// * this diagnostic touches the input cursor position | ||
230 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | ||
231 | fn check_apply_diagnostic_fix_from_position(fixture: &str, after: &str) { | ||
232 | let (analysis, file_position) = analysis_and_position(fixture); | ||
233 | let diagnostic = analysis.diagnostics(file_position.file_id).unwrap().pop().unwrap(); | ||
234 | let mut fix = diagnostic.fix.unwrap(); | ||
235 | let edit = fix.source_file_edits.pop().unwrap().edit; | ||
236 | let target_file_contents = analysis.file_text(file_position.file_id).unwrap(); | ||
237 | let actual = edit.apply(&target_file_contents); | ||
238 | |||
239 | // Strip indent and empty lines from `after`, to match the behaviour of | ||
240 | // `parse_fixture` called from `analysis_and_position`. | ||
241 | let margin = fixture | ||
242 | .lines() | ||
243 | .filter(|it| it.trim_start().starts_with("//-")) | ||
244 | .map(|it| it.len() - it.trim_start().len()) | ||
245 | .next() | ||
246 | .expect("empty fixture"); | ||
247 | let after = join(after.lines().filter_map(|line| { | ||
248 | if line.len() > margin { | ||
249 | Some(&line[margin..]) | ||
250 | } else { | ||
251 | None | ||
252 | } | ||
253 | })) | ||
254 | .separator("\n") | ||
255 | .suffix("\n") | ||
256 | .to_string(); | ||
257 | |||
258 | assert_eq_text!(&after, &actual); | ||
259 | assert!( | ||
260 | diagnostic.range.start() <= file_position.offset | ||
261 | && diagnostic.range.end() >= file_position.offset, | ||
262 | "diagnostic range {} does not touch cursor position {}", | ||
263 | diagnostic.range, | ||
264 | file_position.offset | ||
265 | ); | ||
266 | } | ||
267 | |||
268 | fn check_apply_diagnostic_fix(before: &str, after: &str) { | ||
269 | let (analysis, file_id) = single_file(before); | ||
270 | let diagnostic = analysis.diagnostics(file_id).unwrap().pop().unwrap(); | ||
271 | let mut fix = diagnostic.fix.unwrap(); | ||
272 | let edit = fix.source_file_edits.pop().unwrap().edit; | ||
273 | let actual = edit.apply(&before); | ||
274 | assert_eq_text!(after, &actual); | ||
275 | } | ||
276 | |||
277 | /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics | ||
278 | /// apply to the file containing the cursor. | ||
279 | fn check_no_diagnostic_for_target_file(fixture: &str) { | ||
280 | let (analysis, file_position) = analysis_and_position(fixture); | ||
281 | let diagnostics = analysis.diagnostics(file_position.file_id).unwrap(); | ||
282 | assert_eq!(diagnostics.len(), 0); | ||
283 | } | ||
284 | |||
285 | fn check_no_diagnostic(content: &str) { | ||
286 | let (analysis, file_id) = single_file(content); | ||
287 | let diagnostics = analysis.diagnostics(file_id).unwrap(); | ||
288 | assert_eq!(diagnostics.len(), 0); | ||
289 | } | ||
290 | |||
291 | #[test] | ||
292 | fn test_wrap_return_type() { | ||
293 | let before = r#" | ||
294 | //- /main.rs | ||
295 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
296 | |||
297 | fn div(x: i32, y: i32) -> Result<i32, String> { | ||
298 | if y == 0 { | ||
299 | return Err("div by zero".into()); | ||
300 | } | ||
301 | x / y<|> | ||
302 | } | ||
303 | |||
304 | //- /std/lib.rs | ||
305 | pub mod string { | ||
306 | pub struct String { } | ||
307 | } | ||
308 | pub mod result { | ||
309 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
310 | } | ||
311 | "#; | ||
312 | let after = r#" | ||
313 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
314 | |||
315 | fn div(x: i32, y: i32) -> Result<i32, String> { | ||
316 | if y == 0 { | ||
317 | return Err("div by zero".into()); | ||
318 | } | ||
319 | Ok(x / y) | ||
320 | } | ||
321 | "#; | ||
322 | check_apply_diagnostic_fix_from_position(before, after); | ||
323 | } | ||
324 | |||
325 | #[test] | ||
326 | fn test_wrap_return_type_handles_generic_functions() { | ||
327 | let before = r#" | ||
328 | //- /main.rs | ||
329 | use std::result::Result::{self, Ok, Err}; | ||
330 | |||
331 | fn div<T>(x: T) -> Result<T, i32> { | ||
332 | if x == 0 { | ||
333 | return Err(7); | ||
334 | } | ||
335 | <|>x | ||
336 | } | ||
337 | |||
338 | //- /std/lib.rs | ||
339 | pub mod result { | ||
340 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
341 | } | ||
342 | "#; | ||
343 | let after = r#" | ||
344 | use std::result::Result::{self, Ok, Err}; | ||
345 | |||
346 | fn div<T>(x: T) -> Result<T, i32> { | ||
347 | if x == 0 { | ||
348 | return Err(7); | ||
349 | } | ||
350 | Ok(x) | ||
351 | } | ||
352 | "#; | ||
353 | check_apply_diagnostic_fix_from_position(before, after); | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn test_wrap_return_type_handles_type_aliases() { | ||
358 | let before = r#" | ||
359 | //- /main.rs | ||
360 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
361 | |||
362 | type MyResult<T> = Result<T, String>; | ||
363 | |||
364 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
365 | if y == 0 { | ||
366 | return Err("div by zero".into()); | ||
367 | } | ||
368 | x <|>/ y | ||
369 | } | ||
370 | |||
371 | //- /std/lib.rs | ||
372 | pub mod string { | ||
373 | pub struct String { } | ||
374 | } | ||
375 | pub mod result { | ||
376 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
377 | } | ||
378 | "#; | ||
379 | let after = r#" | ||
380 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
381 | |||
382 | type MyResult<T> = Result<T, String>; | ||
383 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
384 | if y == 0 { | ||
385 | return Err("div by zero".into()); | ||
386 | } | ||
387 | Ok(x / y) | ||
388 | } | ||
389 | "#; | ||
390 | check_apply_diagnostic_fix_from_position(before, after); | ||
391 | } | ||
392 | |||
393 | #[test] | ||
394 | fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { | ||
395 | let content = r#" | ||
396 | //- /main.rs | ||
397 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
398 | |||
399 | fn foo() -> Result<String, i32> { | ||
400 | 0<|> | ||
401 | } | ||
402 | |||
403 | //- /std/lib.rs | ||
404 | pub mod string { | ||
405 | pub struct String { } | ||
406 | } | ||
407 | pub mod result { | ||
408 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
409 | } | ||
410 | "#; | ||
411 | check_no_diagnostic_for_target_file(content); | ||
412 | } | ||
413 | |||
414 | #[test] | ||
415 | fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() { | ||
416 | let content = r#" | ||
417 | //- /main.rs | ||
418 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
419 | |||
420 | enum SomeOtherEnum { | ||
421 | Ok(i32), | ||
422 | Err(String), | ||
423 | } | ||
424 | |||
425 | fn foo() -> SomeOtherEnum { | ||
426 | 0<|> | ||
427 | } | ||
428 | |||
429 | //- /std/lib.rs | ||
430 | pub mod string { | ||
431 | pub struct String { } | ||
432 | } | ||
433 | pub mod result { | ||
434 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
435 | } | ||
436 | "#; | ||
437 | check_no_diagnostic_for_target_file(content); | ||
438 | } | ||
439 | |||
440 | #[test] | ||
441 | fn test_fill_struct_fields_empty() { | ||
442 | let before = r" | ||
443 | struct TestStruct { | ||
444 | one: i32, | ||
445 | two: i64, | ||
446 | } | ||
447 | |||
448 | fn test_fn() { | ||
449 | let s = TestStruct{}; | ||
450 | } | ||
451 | "; | ||
452 | let after = r" | ||
453 | struct TestStruct { | ||
454 | one: i32, | ||
455 | two: i64, | ||
456 | } | ||
457 | |||
458 | fn test_fn() { | ||
459 | let s = TestStruct{ one: (), two: ()}; | ||
460 | } | ||
461 | "; | ||
462 | check_apply_diagnostic_fix(before, after); | ||
463 | } | ||
464 | |||
465 | #[test] | ||
466 | fn test_fill_struct_fields_partial() { | ||
467 | let before = r" | ||
468 | struct TestStruct { | ||
469 | one: i32, | ||
470 | two: i64, | ||
471 | } | ||
472 | |||
473 | fn test_fn() { | ||
474 | let s = TestStruct{ two: 2 }; | ||
475 | } | ||
476 | "; | ||
477 | let after = r" | ||
478 | struct TestStruct { | ||
479 | one: i32, | ||
480 | two: i64, | ||
481 | } | ||
482 | |||
483 | fn test_fn() { | ||
484 | let s = TestStruct{ two: 2, one: () }; | ||
485 | } | ||
486 | "; | ||
487 | check_apply_diagnostic_fix(before, after); | ||
488 | } | ||
489 | |||
490 | #[test] | ||
491 | fn test_fill_struct_fields_no_diagnostic() { | ||
492 | let content = r" | ||
493 | struct TestStruct { | ||
494 | one: i32, | ||
495 | two: i64, | ||
496 | } | ||
497 | |||
498 | fn test_fn() { | ||
499 | let one = 1; | ||
500 | let s = TestStruct{ one, two: 2 }; | ||
501 | } | ||
502 | "; | ||
503 | |||
504 | check_no_diagnostic(content); | ||
505 | } | ||
506 | |||
507 | #[test] | ||
508 | fn test_fill_struct_fields_no_diagnostic_on_spread() { | ||
509 | let content = r" | ||
510 | struct TestStruct { | ||
511 | one: i32, | ||
512 | two: i64, | ||
513 | } | ||
514 | |||
515 | fn test_fn() { | ||
516 | let one = 1; | ||
517 | let s = TestStruct{ ..a }; | ||
518 | } | ||
519 | "; | ||
520 | |||
521 | check_no_diagnostic(content); | ||
522 | } | ||
523 | |||
524 | #[test] | ||
525 | fn test_unresolved_module_diagnostic() { | ||
526 | let (analysis, file_id) = single_file("mod foo;"); | ||
527 | let diagnostics = analysis.diagnostics(file_id).unwrap(); | ||
528 | assert_debug_snapshot!(diagnostics, @r###" | ||
529 | [ | ||
530 | Diagnostic { | ||
531 | message: "unresolved module", | ||
532 | range: [0; 8), | ||
533 | fix: Some( | ||
534 | SourceChange { | ||
535 | label: "create module", | ||
536 | source_file_edits: [], | ||
537 | file_system_edits: [ | ||
538 | CreateFile { | ||
539 | source_root: SourceRootId( | ||
540 | 0, | ||
541 | ), | ||
542 | path: "foo.rs", | ||
543 | }, | ||
544 | ], | ||
545 | cursor_position: None, | ||
546 | }, | ||
547 | ), | ||
548 | severity: Error, | ||
549 | }, | ||
550 | ] | ||
551 | "###); | ||
552 | } | ||
553 | |||
554 | #[test] | ||
555 | fn test_check_unnecessary_braces_in_use_statement() { | ||
556 | check_not_applicable( | ||
557 | " | ||
558 | use a; | ||
559 | use a::{c, d::e}; | ||
560 | ", | ||
561 | check_unnecessary_braces_in_use_statement, | ||
562 | ); | ||
563 | check_apply("use {b};", "use b;", check_unnecessary_braces_in_use_statement); | ||
564 | check_apply("use a::{c};", "use a::c;", check_unnecessary_braces_in_use_statement); | ||
565 | check_apply("use a::{self};", "use a;", check_unnecessary_braces_in_use_statement); | ||
566 | check_apply( | ||
567 | "use a::{c, d::{e}};", | ||
568 | "use a::{c, d::e};", | ||
569 | check_unnecessary_braces_in_use_statement, | ||
570 | ); | ||
571 | } | ||
572 | |||
573 | #[test] | ||
574 | fn test_check_struct_shorthand_initialization() { | ||
575 | check_not_applicable( | ||
576 | r#" | ||
577 | struct A { | ||
578 | a: &'static str | ||
579 | } | ||
580 | |||
581 | fn main() { | ||
582 | A { | ||
583 | a: "hello" | ||
584 | } | ||
585 | } | ||
586 | "#, | ||
587 | check_struct_shorthand_initialization, | ||
588 | ); | ||
589 | |||
590 | check_apply( | ||
591 | r#" | ||
592 | struct A { | ||
593 | a: &'static str | ||
594 | } | ||
595 | |||
596 | fn main() { | ||
597 | let a = "haha"; | ||
598 | A { | ||
599 | a: a | ||
600 | } | ||
601 | } | ||
602 | "#, | ||
603 | r#" | ||
604 | struct A { | ||
605 | a: &'static str | ||
606 | } | ||
607 | |||
608 | fn main() { | ||
609 | let a = "haha"; | ||
610 | A { | ||
611 | a | ||
612 | } | ||
613 | } | ||
614 | "#, | ||
615 | check_struct_shorthand_initialization, | ||
616 | ); | ||
617 | |||
618 | check_apply( | ||
619 | r#" | ||
620 | struct A { | ||
621 | a: &'static str, | ||
622 | b: &'static str | ||
623 | } | ||
624 | |||
625 | fn main() { | ||
626 | let a = "haha"; | ||
627 | let b = "bb"; | ||
628 | A { | ||
629 | a: a, | ||
630 | b | ||
631 | } | ||
632 | } | ||
633 | "#, | ||
634 | r#" | ||
635 | struct A { | ||
636 | a: &'static str, | ||
637 | b: &'static str | ||
638 | } | ||
639 | |||
640 | fn main() { | ||
641 | let a = "haha"; | ||
642 | let b = "bb"; | ||
643 | A { | ||
644 | a, | ||
645 | b | ||
646 | } | ||
647 | } | ||
648 | "#, | ||
649 | check_struct_shorthand_initialization, | ||
650 | ); | ||
651 | } | ||
652 | } | ||
diff --git a/crates/ra_ide/src/display.rs b/crates/ra_ide/src/display.rs new file mode 100644 index 000000000..30617412a --- /dev/null +++ b/crates/ra_ide/src/display.rs | |||
@@ -0,0 +1,84 @@ | |||
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 function_signature; | ||
5 | mod navigation_target; | ||
6 | mod structure; | ||
7 | mod short_label; | ||
8 | |||
9 | use ra_syntax::{ | ||
10 | ast::{self, AstNode, AttrsOwner, NameOwner, TypeParamsOwner}, | ||
11 | SyntaxKind::{ATTR, COMMENT}, | ||
12 | }; | ||
13 | |||
14 | pub use function_signature::FunctionSignature; | ||
15 | pub use navigation_target::NavigationTarget; | ||
16 | pub use structure::{file_structure, StructureNode}; | ||
17 | |||
18 | pub(crate) use navigation_target::{description_from_symbol, docs_from_symbol, ToNav}; | ||
19 | pub(crate) use short_label::ShortLabel; | ||
20 | |||
21 | pub(crate) fn function_label(node: &ast::FnDef) -> String { | ||
22 | FunctionSignature::from(node).to_string() | ||
23 | } | ||
24 | |||
25 | pub(crate) fn const_label(node: &ast::ConstDef) -> String { | ||
26 | let label: String = node | ||
27 | .syntax() | ||
28 | .children_with_tokens() | ||
29 | .filter(|child| !(child.kind() == COMMENT || child.kind() == ATTR)) | ||
30 | .map(|node| node.to_string()) | ||
31 | .collect(); | ||
32 | |||
33 | label.trim().to_owned() | ||
34 | } | ||
35 | |||
36 | pub(crate) fn type_label(node: &ast::TypeAliasDef) -> String { | ||
37 | let label: String = node | ||
38 | .syntax() | ||
39 | .children_with_tokens() | ||
40 | .filter(|child| !(child.kind() == COMMENT || child.kind() == ATTR)) | ||
41 | .map(|node| node.to_string()) | ||
42 | .collect(); | ||
43 | |||
44 | label.trim().to_owned() | ||
45 | } | ||
46 | |||
47 | pub(crate) fn generic_parameters<N: TypeParamsOwner>(node: &N) -> Vec<String> { | ||
48 | let mut res = vec![]; | ||
49 | if let Some(type_params) = node.type_param_list() { | ||
50 | res.extend(type_params.lifetime_params().map(|p| p.syntax().text().to_string())); | ||
51 | res.extend(type_params.type_params().map(|p| p.syntax().text().to_string())); | ||
52 | } | ||
53 | res | ||
54 | } | ||
55 | |||
56 | pub(crate) fn where_predicates<N: TypeParamsOwner>(node: &N) -> Vec<String> { | ||
57 | let mut res = vec![]; | ||
58 | if let Some(clause) = node.where_clause() { | ||
59 | res.extend(clause.predicates().map(|p| p.syntax().text().to_string())); | ||
60 | } | ||
61 | res | ||
62 | } | ||
63 | |||
64 | pub(crate) fn macro_label(node: &ast::MacroCall) -> String { | ||
65 | let name = node.name().map(|name| name.syntax().text().to_string()).unwrap_or_default(); | ||
66 | let vis = if node.has_atom_attr("macro_export") { "#[macro_export]\n" } else { "" }; | ||
67 | format!("{}macro_rules! {}", vis, name) | ||
68 | } | ||
69 | |||
70 | pub(crate) fn rust_code_markup<CODE: AsRef<str>>(val: CODE) -> String { | ||
71 | rust_code_markup_with_doc::<_, &str>(val, None) | ||
72 | } | ||
73 | |||
74 | pub(crate) fn rust_code_markup_with_doc<CODE, DOC>(val: CODE, doc: Option<DOC>) -> String | ||
75 | where | ||
76 | CODE: AsRef<str>, | ||
77 | DOC: AsRef<str>, | ||
78 | { | ||
79 | if let Some(doc) = doc { | ||
80 | format!("```rust\n{}\n```\n\n{}", val.as_ref(), doc.as_ref()) | ||
81 | } else { | ||
82 | format!("```rust\n{}\n```", val.as_ref()) | ||
83 | } | ||
84 | } | ||
diff --git a/crates/ra_ide/src/display/function_signature.rs b/crates/ra_ide/src/display/function_signature.rs new file mode 100644 index 000000000..324ad9552 --- /dev/null +++ b/crates/ra_ide/src/display/function_signature.rs | |||
@@ -0,0 +1,212 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::fmt::{self, Display}; | ||
4 | |||
5 | use hir::{Docs, Documentation, HasSource, HirDisplay}; | ||
6 | use join_to_string::join; | ||
7 | use ra_syntax::ast::{self, AstNode, NameOwner, VisibilityOwner}; | ||
8 | use std::convert::From; | ||
9 | |||
10 | use crate::{ | ||
11 | db, | ||
12 | display::{generic_parameters, where_predicates}, | ||
13 | }; | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | pub enum CallableKind { | ||
17 | Function, | ||
18 | StructConstructor, | ||
19 | VariantConstructor, | ||
20 | Macro, | ||
21 | } | ||
22 | |||
23 | /// Contains information about a function signature | ||
24 | #[derive(Debug)] | ||
25 | pub struct FunctionSignature { | ||
26 | pub kind: CallableKind, | ||
27 | /// Optional visibility | ||
28 | pub visibility: Option<String>, | ||
29 | /// Name of the function | ||
30 | pub name: Option<String>, | ||
31 | /// Documentation for the function | ||
32 | pub doc: Option<Documentation>, | ||
33 | /// Generic parameters | ||
34 | pub generic_parameters: Vec<String>, | ||
35 | /// Parameters of the function | ||
36 | pub parameters: Vec<String>, | ||
37 | /// Optional return type | ||
38 | pub ret_type: Option<String>, | ||
39 | /// Where predicates | ||
40 | pub where_predicates: Vec<String>, | ||
41 | } | ||
42 | |||
43 | impl FunctionSignature { | ||
44 | pub(crate) fn with_doc_opt(mut self, doc: Option<Documentation>) -> Self { | ||
45 | self.doc = doc; | ||
46 | self | ||
47 | } | ||
48 | |||
49 | pub(crate) fn from_hir(db: &db::RootDatabase, function: hir::Function) -> Self { | ||
50 | let doc = function.docs(db); | ||
51 | let ast_node = function.source(db).value; | ||
52 | FunctionSignature::from(&ast_node).with_doc_opt(doc) | ||
53 | } | ||
54 | |||
55 | pub(crate) fn from_struct(db: &db::RootDatabase, st: hir::Struct) -> Option<Self> { | ||
56 | let node: ast::StructDef = st.source(db).value; | ||
57 | match node.kind() { | ||
58 | ast::StructKind::Record(_) => return None, | ||
59 | _ => (), | ||
60 | }; | ||
61 | |||
62 | let params = st | ||
63 | .fields(db) | ||
64 | .into_iter() | ||
65 | .map(|field: hir::StructField| { | ||
66 | let ty = field.ty(db); | ||
67 | format!("{}", ty.display(db)) | ||
68 | }) | ||
69 | .collect(); | ||
70 | |||
71 | Some( | ||
72 | FunctionSignature { | ||
73 | kind: CallableKind::StructConstructor, | ||
74 | visibility: node.visibility().map(|n| n.syntax().text().to_string()), | ||
75 | name: node.name().map(|n| n.text().to_string()), | ||
76 | ret_type: node.name().map(|n| n.text().to_string()), | ||
77 | parameters: params, | ||
78 | generic_parameters: generic_parameters(&node), | ||
79 | where_predicates: where_predicates(&node), | ||
80 | doc: None, | ||
81 | } | ||
82 | .with_doc_opt(st.docs(db)), | ||
83 | ) | ||
84 | } | ||
85 | |||
86 | pub(crate) fn from_enum_variant( | ||
87 | db: &db::RootDatabase, | ||
88 | variant: hir::EnumVariant, | ||
89 | ) -> Option<Self> { | ||
90 | let node: ast::EnumVariant = variant.source(db).value; | ||
91 | match node.kind() { | ||
92 | ast::StructKind::Record(_) | ast::StructKind::Unit => return None, | ||
93 | _ => (), | ||
94 | }; | ||
95 | |||
96 | let parent_name = variant.parent_enum(db).name(db).to_string(); | ||
97 | |||
98 | let name = format!("{}::{}", parent_name, variant.name(db)); | ||
99 | |||
100 | let params = variant | ||
101 | .fields(db) | ||
102 | .into_iter() | ||
103 | .map(|field: hir::StructField| { | ||
104 | let name = field.name(db); | ||
105 | let ty = field.ty(db); | ||
106 | format!("{}: {}", name, ty.display(db)) | ||
107 | }) | ||
108 | .collect(); | ||
109 | |||
110 | Some( | ||
111 | FunctionSignature { | ||
112 | kind: CallableKind::VariantConstructor, | ||
113 | visibility: None, | ||
114 | name: Some(name), | ||
115 | ret_type: None, | ||
116 | parameters: params, | ||
117 | generic_parameters: vec![], | ||
118 | where_predicates: vec![], | ||
119 | doc: None, | ||
120 | } | ||
121 | .with_doc_opt(variant.docs(db)), | ||
122 | ) | ||
123 | } | ||
124 | |||
125 | pub(crate) fn from_macro(db: &db::RootDatabase, macro_def: hir::MacroDef) -> Option<Self> { | ||
126 | let node: ast::MacroCall = macro_def.source(db).value; | ||
127 | |||
128 | let params = vec![]; | ||
129 | |||
130 | Some( | ||
131 | FunctionSignature { | ||
132 | kind: CallableKind::Macro, | ||
133 | visibility: None, | ||
134 | name: node.name().map(|n| n.text().to_string()), | ||
135 | ret_type: None, | ||
136 | parameters: params, | ||
137 | generic_parameters: vec![], | ||
138 | where_predicates: vec![], | ||
139 | doc: None, | ||
140 | } | ||
141 | .with_doc_opt(macro_def.docs(db)), | ||
142 | ) | ||
143 | } | ||
144 | } | ||
145 | |||
146 | impl From<&'_ ast::FnDef> for FunctionSignature { | ||
147 | fn from(node: &ast::FnDef) -> FunctionSignature { | ||
148 | fn param_list(node: &ast::FnDef) -> Vec<String> { | ||
149 | let mut res = vec![]; | ||
150 | if let Some(param_list) = node.param_list() { | ||
151 | if let Some(self_param) = param_list.self_param() { | ||
152 | res.push(self_param.syntax().text().to_string()) | ||
153 | } | ||
154 | |||
155 | res.extend(param_list.params().map(|param| param.syntax().text().to_string())); | ||
156 | } | ||
157 | res | ||
158 | } | ||
159 | |||
160 | FunctionSignature { | ||
161 | kind: CallableKind::Function, | ||
162 | visibility: node.visibility().map(|n| n.syntax().text().to_string()), | ||
163 | name: node.name().map(|n| n.text().to_string()), | ||
164 | ret_type: node | ||
165 | .ret_type() | ||
166 | .and_then(|r| r.type_ref()) | ||
167 | .map(|n| n.syntax().text().to_string()), | ||
168 | parameters: param_list(node), | ||
169 | generic_parameters: generic_parameters(node), | ||
170 | where_predicates: where_predicates(node), | ||
171 | // docs are processed separately | ||
172 | doc: None, | ||
173 | } | ||
174 | } | ||
175 | } | ||
176 | |||
177 | impl Display for FunctionSignature { | ||
178 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
179 | if let Some(t) = &self.visibility { | ||
180 | write!(f, "{} ", t)?; | ||
181 | } | ||
182 | |||
183 | if let Some(name) = &self.name { | ||
184 | match self.kind { | ||
185 | CallableKind::Function => write!(f, "fn {}", name)?, | ||
186 | CallableKind::StructConstructor => write!(f, "struct {}", name)?, | ||
187 | CallableKind::VariantConstructor => write!(f, "{}", name)?, | ||
188 | CallableKind::Macro => write!(f, "{}!", name)?, | ||
189 | } | ||
190 | } | ||
191 | |||
192 | if !self.generic_parameters.is_empty() { | ||
193 | join(self.generic_parameters.iter()) | ||
194 | .separator(", ") | ||
195 | .surround_with("<", ">") | ||
196 | .to_fmt(f)?; | ||
197 | } | ||
198 | |||
199 | join(self.parameters.iter()).separator(", ").surround_with("(", ")").to_fmt(f)?; | ||
200 | |||
201 | if let Some(t) = &self.ret_type { | ||
202 | write!(f, " -> {}", t)?; | ||
203 | } | ||
204 | |||
205 | if !self.where_predicates.is_empty() { | ||
206 | write!(f, "\nwhere ")?; | ||
207 | join(self.where_predicates.iter()).separator(",\n ").to_fmt(f)?; | ||
208 | } | ||
209 | |||
210 | Ok(()) | ||
211 | } | ||
212 | } | ||
diff --git a/crates/ra_ide/src/display/navigation_target.rs b/crates/ra_ide/src/display/navigation_target.rs new file mode 100644 index 000000000..6ac60722b --- /dev/null +++ b/crates/ra_ide/src/display/navigation_target.rs | |||
@@ -0,0 +1,411 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::{AssocItem, Either, FieldSource, HasSource, ModuleSource, Source}; | ||
4 | use ra_db::{FileId, SourceDatabase}; | ||
5 | use ra_syntax::{ | ||
6 | ast::{self, DocCommentsOwner, NameOwner}, | ||
7 | match_ast, AstNode, SmolStr, | ||
8 | SyntaxKind::{self, BIND_PAT}, | ||
9 | TextRange, | ||
10 | }; | ||
11 | |||
12 | use crate::{db::RootDatabase, expand::original_range, FileSymbol}; | ||
13 | |||
14 | use super::short_label::ShortLabel; | ||
15 | |||
16 | /// `NavigationTarget` represents and element in the editor's UI which you can | ||
17 | /// click on to navigate to a particular piece of code. | ||
18 | /// | ||
19 | /// Typically, a `NavigationTarget` corresponds to some element in the source | ||
20 | /// code, like a function or a struct, but this is not strictly required. | ||
21 | #[derive(Debug, Clone)] | ||
22 | pub struct NavigationTarget { | ||
23 | file_id: FileId, | ||
24 | name: SmolStr, | ||
25 | kind: SyntaxKind, | ||
26 | full_range: TextRange, | ||
27 | focus_range: Option<TextRange>, | ||
28 | container_name: Option<SmolStr>, | ||
29 | description: Option<String>, | ||
30 | docs: Option<String>, | ||
31 | } | ||
32 | |||
33 | pub(crate) trait ToNav { | ||
34 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget; | ||
35 | } | ||
36 | |||
37 | impl NavigationTarget { | ||
38 | /// When `focus_range` is specified, returns it. otherwise | ||
39 | /// returns `full_range` | ||
40 | pub fn range(&self) -> TextRange { | ||
41 | self.focus_range.unwrap_or(self.full_range) | ||
42 | } | ||
43 | |||
44 | pub fn name(&self) -> &SmolStr { | ||
45 | &self.name | ||
46 | } | ||
47 | |||
48 | pub fn container_name(&self) -> Option<&SmolStr> { | ||
49 | self.container_name.as_ref() | ||
50 | } | ||
51 | |||
52 | pub fn kind(&self) -> SyntaxKind { | ||
53 | self.kind | ||
54 | } | ||
55 | |||
56 | pub fn file_id(&self) -> FileId { | ||
57 | self.file_id | ||
58 | } | ||
59 | |||
60 | pub fn full_range(&self) -> TextRange { | ||
61 | self.full_range | ||
62 | } | ||
63 | |||
64 | pub fn docs(&self) -> Option<&str> { | ||
65 | self.docs.as_ref().map(String::as_str) | ||
66 | } | ||
67 | |||
68 | pub fn description(&self) -> Option<&str> { | ||
69 | self.description.as_ref().map(String::as_str) | ||
70 | } | ||
71 | |||
72 | /// A "most interesting" range withing the `full_range`. | ||
73 | /// | ||
74 | /// Typically, `full_range` is the whole syntax node, | ||
75 | /// including doc comments, and `focus_range` is the range of the identifier. | ||
76 | pub fn focus_range(&self) -> Option<TextRange> { | ||
77 | self.focus_range | ||
78 | } | ||
79 | |||
80 | pub(crate) fn from_module_to_decl(db: &RootDatabase, module: hir::Module) -> NavigationTarget { | ||
81 | let name = module.name(db).map(|it| it.to_string().into()).unwrap_or_default(); | ||
82 | if let Some(src) = module.declaration_source(db) { | ||
83 | let frange = original_range(db, src.as_ref().map(|it| it.syntax())); | ||
84 | return NavigationTarget::from_syntax( | ||
85 | frange.file_id, | ||
86 | name, | ||
87 | None, | ||
88 | frange.range, | ||
89 | src.value.syntax().kind(), | ||
90 | src.value.doc_comment_text(), | ||
91 | src.value.short_label(), | ||
92 | ); | ||
93 | } | ||
94 | module.to_nav(db) | ||
95 | } | ||
96 | |||
97 | pub(crate) fn from_def( | ||
98 | db: &RootDatabase, | ||
99 | module_def: hir::ModuleDef, | ||
100 | ) -> Option<NavigationTarget> { | ||
101 | let nav = match module_def { | ||
102 | hir::ModuleDef::Module(module) => module.to_nav(db), | ||
103 | hir::ModuleDef::Function(it) => it.to_nav(db), | ||
104 | hir::ModuleDef::Adt(it) => it.to_nav(db), | ||
105 | hir::ModuleDef::Const(it) => it.to_nav(db), | ||
106 | hir::ModuleDef::Static(it) => it.to_nav(db), | ||
107 | hir::ModuleDef::EnumVariant(it) => it.to_nav(db), | ||
108 | hir::ModuleDef::Trait(it) => it.to_nav(db), | ||
109 | hir::ModuleDef::TypeAlias(it) => it.to_nav(db), | ||
110 | hir::ModuleDef::BuiltinType(..) => { | ||
111 | return None; | ||
112 | } | ||
113 | }; | ||
114 | Some(nav) | ||
115 | } | ||
116 | |||
117 | #[cfg(test)] | ||
118 | pub(crate) fn assert_match(&self, expected: &str) { | ||
119 | let actual = self.debug_render(); | ||
120 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
121 | } | ||
122 | |||
123 | #[cfg(test)] | ||
124 | pub(crate) fn debug_render(&self) -> String { | ||
125 | let mut buf = format!( | ||
126 | "{} {:?} {:?} {:?}", | ||
127 | self.name(), | ||
128 | self.kind(), | ||
129 | self.file_id(), | ||
130 | self.full_range() | ||
131 | ); | ||
132 | if let Some(focus_range) = self.focus_range() { | ||
133 | buf.push_str(&format!(" {:?}", focus_range)) | ||
134 | } | ||
135 | if let Some(container_name) = self.container_name() { | ||
136 | buf.push_str(&format!(" {}", container_name)) | ||
137 | } | ||
138 | buf | ||
139 | } | ||
140 | |||
141 | /// Allows `NavigationTarget` to be created from a `NameOwner` | ||
142 | pub(crate) fn from_named( | ||
143 | db: &RootDatabase, | ||
144 | node: Source<&dyn ast::NameOwner>, | ||
145 | docs: Option<String>, | ||
146 | description: Option<String>, | ||
147 | ) -> NavigationTarget { | ||
148 | //FIXME: use `_` instead of empty string | ||
149 | let name = node.value.name().map(|it| it.text().clone()).unwrap_or_default(); | ||
150 | let focus_range = | ||
151 | node.value.name().map(|it| original_range(db, node.with_value(it.syntax())).range); | ||
152 | let frange = original_range(db, node.map(|it| it.syntax())); | ||
153 | |||
154 | NavigationTarget::from_syntax( | ||
155 | frange.file_id, | ||
156 | name, | ||
157 | focus_range, | ||
158 | frange.range, | ||
159 | node.value.syntax().kind(), | ||
160 | docs, | ||
161 | description, | ||
162 | ) | ||
163 | } | ||
164 | |||
165 | fn from_syntax( | ||
166 | file_id: FileId, | ||
167 | name: SmolStr, | ||
168 | focus_range: Option<TextRange>, | ||
169 | full_range: TextRange, | ||
170 | kind: SyntaxKind, | ||
171 | docs: Option<String>, | ||
172 | description: Option<String>, | ||
173 | ) -> NavigationTarget { | ||
174 | NavigationTarget { | ||
175 | file_id, | ||
176 | name, | ||
177 | kind, | ||
178 | full_range, | ||
179 | focus_range, | ||
180 | container_name: None, | ||
181 | description, | ||
182 | docs, | ||
183 | } | ||
184 | } | ||
185 | } | ||
186 | |||
187 | impl ToNav for FileSymbol { | ||
188 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
189 | NavigationTarget { | ||
190 | file_id: self.file_id, | ||
191 | name: self.name.clone(), | ||
192 | kind: self.ptr.kind(), | ||
193 | full_range: self.ptr.range(), | ||
194 | focus_range: self.name_range, | ||
195 | container_name: self.container_name.clone(), | ||
196 | description: description_from_symbol(db, self), | ||
197 | docs: docs_from_symbol(db, self), | ||
198 | } | ||
199 | } | ||
200 | } | ||
201 | |||
202 | pub(crate) trait ToNavFromAst {} | ||
203 | impl ToNavFromAst for hir::Function {} | ||
204 | impl ToNavFromAst for hir::Const {} | ||
205 | impl ToNavFromAst for hir::Static {} | ||
206 | impl ToNavFromAst for hir::Struct {} | ||
207 | impl ToNavFromAst for hir::Enum {} | ||
208 | impl ToNavFromAst for hir::EnumVariant {} | ||
209 | impl ToNavFromAst for hir::Union {} | ||
210 | impl ToNavFromAst for hir::TypeAlias {} | ||
211 | impl ToNavFromAst for hir::Trait {} | ||
212 | |||
213 | impl<D> ToNav for D | ||
214 | where | ||
215 | D: HasSource + ToNavFromAst + Copy, | ||
216 | D::Ast: ast::DocCommentsOwner + ast::NameOwner + ShortLabel, | ||
217 | { | ||
218 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
219 | let src = self.source(db); | ||
220 | NavigationTarget::from_named( | ||
221 | db, | ||
222 | src.as_ref().map(|it| it as &dyn ast::NameOwner), | ||
223 | src.value.doc_comment_text(), | ||
224 | src.value.short_label(), | ||
225 | ) | ||
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 | match &src.value { | ||
234 | ModuleSource::SourceFile(node) => { | ||
235 | let frange = original_range(db, src.with_value(node.syntax())); | ||
236 | |||
237 | NavigationTarget::from_syntax( | ||
238 | frange.file_id, | ||
239 | name, | ||
240 | None, | ||
241 | frange.range, | ||
242 | node.syntax().kind(), | ||
243 | None, | ||
244 | None, | ||
245 | ) | ||
246 | } | ||
247 | ModuleSource::Module(node) => { | ||
248 | let frange = original_range(db, src.with_value(node.syntax())); | ||
249 | |||
250 | NavigationTarget::from_syntax( | ||
251 | frange.file_id, | ||
252 | name, | ||
253 | None, | ||
254 | frange.range, | ||
255 | node.syntax().kind(), | ||
256 | node.doc_comment_text(), | ||
257 | node.short_label(), | ||
258 | ) | ||
259 | } | ||
260 | } | ||
261 | } | ||
262 | } | ||
263 | |||
264 | impl ToNav for hir::ImplBlock { | ||
265 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
266 | let src = self.source(db); | ||
267 | let frange = original_range(db, src.as_ref().map(|it| it.syntax())); | ||
268 | |||
269 | NavigationTarget::from_syntax( | ||
270 | frange.file_id, | ||
271 | "impl".into(), | ||
272 | None, | ||
273 | frange.range, | ||
274 | src.value.syntax().kind(), | ||
275 | None, | ||
276 | None, | ||
277 | ) | ||
278 | } | ||
279 | } | ||
280 | |||
281 | impl ToNav for hir::StructField { | ||
282 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
283 | let src = self.source(db); | ||
284 | |||
285 | match &src.value { | ||
286 | FieldSource::Named(it) => NavigationTarget::from_named( | ||
287 | db, | ||
288 | src.with_value(it), | ||
289 | it.doc_comment_text(), | ||
290 | it.short_label(), | ||
291 | ), | ||
292 | FieldSource::Pos(it) => { | ||
293 | let frange = original_range(db, src.with_value(it.syntax())); | ||
294 | NavigationTarget::from_syntax( | ||
295 | frange.file_id, | ||
296 | "".into(), | ||
297 | None, | ||
298 | frange.range, | ||
299 | it.syntax().kind(), | ||
300 | None, | ||
301 | None, | ||
302 | ) | ||
303 | } | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | |||
308 | impl ToNav for hir::MacroDef { | ||
309 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
310 | let src = self.source(db); | ||
311 | log::debug!("nav target {:#?}", src.value.syntax()); | ||
312 | NavigationTarget::from_named( | ||
313 | db, | ||
314 | src.as_ref().map(|it| it as &dyn ast::NameOwner), | ||
315 | src.value.doc_comment_text(), | ||
316 | None, | ||
317 | ) | ||
318 | } | ||
319 | } | ||
320 | |||
321 | impl ToNav for hir::Adt { | ||
322 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
323 | match self { | ||
324 | hir::Adt::Struct(it) => it.to_nav(db), | ||
325 | hir::Adt::Union(it) => it.to_nav(db), | ||
326 | hir::Adt::Enum(it) => it.to_nav(db), | ||
327 | } | ||
328 | } | ||
329 | } | ||
330 | |||
331 | impl ToNav for hir::AssocItem { | ||
332 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
333 | match self { | ||
334 | AssocItem::Function(it) => it.to_nav(db), | ||
335 | AssocItem::Const(it) => it.to_nav(db), | ||
336 | AssocItem::TypeAlias(it) => it.to_nav(db), | ||
337 | } | ||
338 | } | ||
339 | } | ||
340 | |||
341 | impl ToNav for hir::Local { | ||
342 | fn to_nav(&self, db: &RootDatabase) -> NavigationTarget { | ||
343 | let src = self.source(db); | ||
344 | let (full_range, focus_range) = match src.value { | ||
345 | Either::A(it) => { | ||
346 | (it.syntax().text_range(), it.name().map(|it| it.syntax().text_range())) | ||
347 | } | ||
348 | Either::B(it) => (it.syntax().text_range(), Some(it.self_kw_token().text_range())), | ||
349 | }; | ||
350 | let name = match self.name(db) { | ||
351 | Some(it) => it.to_string().into(), | ||
352 | None => "".into(), | ||
353 | }; | ||
354 | NavigationTarget { | ||
355 | file_id: src.file_id.original_file(db), | ||
356 | name, | ||
357 | kind: BIND_PAT, | ||
358 | full_range, | ||
359 | focus_range, | ||
360 | container_name: None, | ||
361 | description: None, | ||
362 | docs: None, | ||
363 | } | ||
364 | } | ||
365 | } | ||
366 | |||
367 | pub(crate) fn docs_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> { | ||
368 | let parse = db.parse(symbol.file_id); | ||
369 | let node = symbol.ptr.to_node(parse.tree().syntax()); | ||
370 | |||
371 | match_ast! { | ||
372 | match node { | ||
373 | ast::FnDef(it) => { it.doc_comment_text() }, | ||
374 | ast::StructDef(it) => { it.doc_comment_text() }, | ||
375 | ast::EnumDef(it) => { it.doc_comment_text() }, | ||
376 | ast::TraitDef(it) => { it.doc_comment_text() }, | ||
377 | ast::Module(it) => { it.doc_comment_text() }, | ||
378 | ast::TypeAliasDef(it) => { it.doc_comment_text() }, | ||
379 | ast::ConstDef(it) => { it.doc_comment_text() }, | ||
380 | ast::StaticDef(it) => { it.doc_comment_text() }, | ||
381 | ast::RecordFieldDef(it) => { it.doc_comment_text() }, | ||
382 | ast::EnumVariant(it) => { it.doc_comment_text() }, | ||
383 | ast::MacroCall(it) => { it.doc_comment_text() }, | ||
384 | _ => None, | ||
385 | } | ||
386 | } | ||
387 | } | ||
388 | |||
389 | /// Get a description of a symbol. | ||
390 | /// | ||
391 | /// e.g. `struct Name`, `enum Name`, `fn Name` | ||
392 | pub(crate) fn description_from_symbol(db: &RootDatabase, symbol: &FileSymbol) -> Option<String> { | ||
393 | let parse = db.parse(symbol.file_id); | ||
394 | let node = symbol.ptr.to_node(parse.tree().syntax()); | ||
395 | |||
396 | match_ast! { | ||
397 | match node { | ||
398 | ast::FnDef(it) => { it.short_label() }, | ||
399 | ast::StructDef(it) => { it.short_label() }, | ||
400 | ast::EnumDef(it) => { it.short_label() }, | ||
401 | ast::TraitDef(it) => { it.short_label() }, | ||
402 | ast::Module(it) => { it.short_label() }, | ||
403 | ast::TypeAliasDef(it) => { it.short_label() }, | ||
404 | ast::ConstDef(it) => { it.short_label() }, | ||
405 | ast::StaticDef(it) => { it.short_label() }, | ||
406 | ast::RecordFieldDef(it) => { it.short_label() }, | ||
407 | ast::EnumVariant(it) => { it.short_label() }, | ||
408 | _ => None, | ||
409 | } | ||
410 | } | ||
411 | } | ||
diff --git a/crates/ra_ide/src/display/short_label.rs b/crates/ra_ide/src/display/short_label.rs new file mode 100644 index 000000000..9ffc9b980 --- /dev/null +++ b/crates/ra_ide/src/display/short_label.rs | |||
@@ -0,0 +1,97 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use format_buf::format; | ||
4 | use ra_syntax::ast::{self, AstNode, NameOwner, TypeAscriptionOwner, VisibilityOwner}; | ||
5 | |||
6 | pub(crate) trait ShortLabel { | ||
7 | fn short_label(&self) -> Option<String>; | ||
8 | } | ||
9 | |||
10 | impl ShortLabel for ast::FnDef { | ||
11 | fn short_label(&self) -> Option<String> { | ||
12 | Some(crate::display::function_label(self)) | ||
13 | } | ||
14 | } | ||
15 | |||
16 | impl ShortLabel for ast::StructDef { | ||
17 | fn short_label(&self) -> Option<String> { | ||
18 | short_label_from_node(self, "struct ") | ||
19 | } | ||
20 | } | ||
21 | |||
22 | impl ShortLabel for ast::UnionDef { | ||
23 | fn short_label(&self) -> Option<String> { | ||
24 | short_label_from_node(self, "union ") | ||
25 | } | ||
26 | } | ||
27 | |||
28 | impl ShortLabel for ast::EnumDef { | ||
29 | fn short_label(&self) -> Option<String> { | ||
30 | short_label_from_node(self, "enum ") | ||
31 | } | ||
32 | } | ||
33 | |||
34 | impl ShortLabel for ast::TraitDef { | ||
35 | fn short_label(&self) -> Option<String> { | ||
36 | short_label_from_node(self, "trait ") | ||
37 | } | ||
38 | } | ||
39 | |||
40 | impl ShortLabel for ast::Module { | ||
41 | fn short_label(&self) -> Option<String> { | ||
42 | short_label_from_node(self, "mod ") | ||
43 | } | ||
44 | } | ||
45 | |||
46 | impl ShortLabel for ast::TypeAliasDef { | ||
47 | fn short_label(&self) -> Option<String> { | ||
48 | short_label_from_node(self, "type ") | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl ShortLabel for ast::ConstDef { | ||
53 | fn short_label(&self) -> Option<String> { | ||
54 | short_label_from_ascribed_node(self, "const ") | ||
55 | } | ||
56 | } | ||
57 | |||
58 | impl ShortLabel for ast::StaticDef { | ||
59 | fn short_label(&self) -> Option<String> { | ||
60 | short_label_from_ascribed_node(self, "static ") | ||
61 | } | ||
62 | } | ||
63 | |||
64 | impl ShortLabel for ast::RecordFieldDef { | ||
65 | fn short_label(&self) -> Option<String> { | ||
66 | short_label_from_ascribed_node(self, "") | ||
67 | } | ||
68 | } | ||
69 | |||
70 | impl ShortLabel for ast::EnumVariant { | ||
71 | fn short_label(&self) -> Option<String> { | ||
72 | Some(self.name()?.text().to_string()) | ||
73 | } | ||
74 | } | ||
75 | |||
76 | fn short_label_from_ascribed_node<T>(node: &T, prefix: &str) -> Option<String> | ||
77 | where | ||
78 | T: NameOwner + VisibilityOwner + TypeAscriptionOwner, | ||
79 | { | ||
80 | let mut buf = short_label_from_node(node, prefix)?; | ||
81 | |||
82 | if let Some(type_ref) = node.ascribed_type() { | ||
83 | format!(buf, ": {}", type_ref.syntax()); | ||
84 | } | ||
85 | |||
86 | Some(buf) | ||
87 | } | ||
88 | |||
89 | fn short_label_from_node<T>(node: &T, label: &str) -> Option<String> | ||
90 | where | ||
91 | T: NameOwner + VisibilityOwner, | ||
92 | { | ||
93 | let mut buf = node.visibility().map(|v| format!("{} ", v.syntax())).unwrap_or_default(); | ||
94 | buf.push_str(label); | ||
95 | buf.push_str(node.name()?.text().as_str()); | ||
96 | Some(buf) | ||
97 | } | ||
diff --git a/crates/ra_ide/src/display/structure.rs b/crates/ra_ide/src/display/structure.rs new file mode 100644 index 000000000..a80d65ac7 --- /dev/null +++ b/crates/ra_ide/src/display/structure.rs | |||
@@ -0,0 +1,401 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::TextRange; | ||
4 | |||
5 | use ra_syntax::{ | ||
6 | ast::{self, AttrsOwner, NameOwner, TypeAscriptionOwner, TypeParamsOwner}, | ||
7 | match_ast, AstNode, SourceFile, SyntaxKind, SyntaxNode, WalkEvent, | ||
8 | }; | ||
9 | |||
10 | #[derive(Debug, Clone)] | ||
11 | pub struct StructureNode { | ||
12 | pub parent: Option<usize>, | ||
13 | pub label: String, | ||
14 | pub navigation_range: TextRange, | ||
15 | pub node_range: TextRange, | ||
16 | pub kind: SyntaxKind, | ||
17 | pub detail: Option<String>, | ||
18 | pub deprecated: bool, | ||
19 | } | ||
20 | |||
21 | pub fn file_structure(file: &SourceFile) -> Vec<StructureNode> { | ||
22 | let mut res = Vec::new(); | ||
23 | let mut stack = Vec::new(); | ||
24 | |||
25 | for event in file.syntax().preorder() { | ||
26 | match event { | ||
27 | WalkEvent::Enter(node) => { | ||
28 | if let Some(mut symbol) = structure_node(&node) { | ||
29 | symbol.parent = stack.last().copied(); | ||
30 | stack.push(res.len()); | ||
31 | res.push(symbol); | ||
32 | } | ||
33 | } | ||
34 | WalkEvent::Leave(node) => { | ||
35 | if structure_node(&node).is_some() { | ||
36 | stack.pop().unwrap(); | ||
37 | } | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | res | ||
42 | } | ||
43 | |||
44 | fn structure_node(node: &SyntaxNode) -> Option<StructureNode> { | ||
45 | fn decl<N: NameOwner + AttrsOwner>(node: N) -> Option<StructureNode> { | ||
46 | decl_with_detail(node, None) | ||
47 | } | ||
48 | |||
49 | fn decl_with_ascription<N: NameOwner + AttrsOwner + TypeAscriptionOwner>( | ||
50 | node: N, | ||
51 | ) -> Option<StructureNode> { | ||
52 | let ty = node.ascribed_type(); | ||
53 | decl_with_type_ref(node, ty) | ||
54 | } | ||
55 | |||
56 | fn decl_with_type_ref<N: NameOwner + AttrsOwner>( | ||
57 | node: N, | ||
58 | type_ref: Option<ast::TypeRef>, | ||
59 | ) -> Option<StructureNode> { | ||
60 | let detail = type_ref.map(|type_ref| { | ||
61 | let mut detail = String::new(); | ||
62 | collapse_ws(type_ref.syntax(), &mut detail); | ||
63 | detail | ||
64 | }); | ||
65 | decl_with_detail(node, detail) | ||
66 | } | ||
67 | |||
68 | fn decl_with_detail<N: NameOwner + AttrsOwner>( | ||
69 | node: N, | ||
70 | detail: Option<String>, | ||
71 | ) -> Option<StructureNode> { | ||
72 | let name = node.name()?; | ||
73 | |||
74 | Some(StructureNode { | ||
75 | parent: None, | ||
76 | label: name.text().to_string(), | ||
77 | navigation_range: name.syntax().text_range(), | ||
78 | node_range: node.syntax().text_range(), | ||
79 | kind: node.syntax().kind(), | ||
80 | detail, | ||
81 | deprecated: node.attrs().filter_map(|x| x.simple_name()).any(|x| x == "deprecated"), | ||
82 | }) | ||
83 | } | ||
84 | |||
85 | fn collapse_ws(node: &SyntaxNode, output: &mut String) { | ||
86 | let mut can_insert_ws = false; | ||
87 | node.text().for_each_chunk(|chunk| { | ||
88 | for line in chunk.lines() { | ||
89 | let line = line.trim(); | ||
90 | if line.is_empty() { | ||
91 | if can_insert_ws { | ||
92 | output.push(' '); | ||
93 | can_insert_ws = false; | ||
94 | } | ||
95 | } else { | ||
96 | output.push_str(line); | ||
97 | can_insert_ws = true; | ||
98 | } | ||
99 | } | ||
100 | }) | ||
101 | } | ||
102 | |||
103 | match_ast! { | ||
104 | match node { | ||
105 | ast::FnDef(it) => { | ||
106 | let mut detail = String::from("fn"); | ||
107 | if let Some(type_param_list) = it.type_param_list() { | ||
108 | collapse_ws(type_param_list.syntax(), &mut detail); | ||
109 | } | ||
110 | if let Some(param_list) = it.param_list() { | ||
111 | collapse_ws(param_list.syntax(), &mut detail); | ||
112 | } | ||
113 | if let Some(ret_type) = it.ret_type() { | ||
114 | detail.push_str(" "); | ||
115 | collapse_ws(ret_type.syntax(), &mut detail); | ||
116 | } | ||
117 | |||
118 | decl_with_detail(it, Some(detail)) | ||
119 | }, | ||
120 | ast::StructDef(it) => { decl(it) }, | ||
121 | ast::EnumDef(it) => { decl(it) }, | ||
122 | ast::EnumVariant(it) => { decl(it) }, | ||
123 | ast::TraitDef(it) => { decl(it) }, | ||
124 | ast::Module(it) => { decl(it) }, | ||
125 | ast::TypeAliasDef(it) => { | ||
126 | let ty = it.type_ref(); | ||
127 | decl_with_type_ref(it, ty) | ||
128 | }, | ||
129 | ast::RecordFieldDef(it) => { decl_with_ascription(it) }, | ||
130 | ast::ConstDef(it) => { decl_with_ascription(it) }, | ||
131 | ast::StaticDef(it) => { decl_with_ascription(it) }, | ||
132 | ast::ImplBlock(it) => { | ||
133 | let target_type = it.target_type()?; | ||
134 | let target_trait = it.target_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 | let first_token = it.syntax().first_token().unwrap(); | ||
155 | if first_token.text().as_str() != "macro_rules" { | ||
156 | return None; | ||
157 | } | ||
158 | decl(it) | ||
159 | }, | ||
160 | _ => None, | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | |||
165 | #[cfg(test)] | ||
166 | mod tests { | ||
167 | use super::*; | ||
168 | use insta::assert_debug_snapshot; | ||
169 | |||
170 | #[test] | ||
171 | fn test_file_structure() { | ||
172 | let file = SourceFile::parse( | ||
173 | r#" | ||
174 | struct Foo { | ||
175 | x: i32 | ||
176 | } | ||
177 | |||
178 | mod m { | ||
179 | fn bar1() {} | ||
180 | fn bar2<T>(t: T) -> T {} | ||
181 | fn bar3<A, | ||
182 | B>(a: A, | ||
183 | b: B) -> Vec< | ||
184 | u32 | ||
185 | > {} | ||
186 | } | ||
187 | |||
188 | enum E { X, Y(i32) } | ||
189 | type T = (); | ||
190 | static S: i32 = 92; | ||
191 | const C: i32 = 92; | ||
192 | |||
193 | impl E {} | ||
194 | |||
195 | impl fmt::Debug for E {} | ||
196 | |||
197 | macro_rules! mc { | ||
198 | () => {} | ||
199 | } | ||
200 | |||
201 | #[deprecated] | ||
202 | fn obsolete() {} | ||
203 | |||
204 | #[deprecated(note = "for awhile")] | ||
205 | fn very_obsolete() {} | ||
206 | "#, | ||
207 | ) | ||
208 | .ok() | ||
209 | .unwrap(); | ||
210 | let structure = file_structure(&file); | ||
211 | assert_debug_snapshot!(structure, | ||
212 | @r###" | ||
213 | [ | ||
214 | StructureNode { | ||
215 | parent: None, | ||
216 | label: "Foo", | ||
217 | navigation_range: [8; 11), | ||
218 | node_range: [1; 26), | ||
219 | kind: STRUCT_DEF, | ||
220 | detail: None, | ||
221 | deprecated: false, | ||
222 | }, | ||
223 | StructureNode { | ||
224 | parent: Some( | ||
225 | 0, | ||
226 | ), | ||
227 | label: "x", | ||
228 | navigation_range: [18; 19), | ||
229 | node_range: [18; 24), | ||
230 | kind: RECORD_FIELD_DEF, | ||
231 | detail: Some( | ||
232 | "i32", | ||
233 | ), | ||
234 | deprecated: false, | ||
235 | }, | ||
236 | StructureNode { | ||
237 | parent: None, | ||
238 | label: "m", | ||
239 | navigation_range: [32; 33), | ||
240 | node_range: [28; 158), | ||
241 | kind: MODULE, | ||
242 | detail: None, | ||
243 | deprecated: false, | ||
244 | }, | ||
245 | StructureNode { | ||
246 | parent: Some( | ||
247 | 2, | ||
248 | ), | ||
249 | label: "bar1", | ||
250 | navigation_range: [43; 47), | ||
251 | node_range: [40; 52), | ||
252 | kind: FN_DEF, | ||
253 | detail: Some( | ||
254 | "fn()", | ||
255 | ), | ||
256 | deprecated: false, | ||
257 | }, | ||
258 | StructureNode { | ||
259 | parent: Some( | ||
260 | 2, | ||
261 | ), | ||
262 | label: "bar2", | ||
263 | navigation_range: [60; 64), | ||
264 | node_range: [57; 81), | ||
265 | kind: FN_DEF, | ||
266 | detail: Some( | ||
267 | "fn<T>(t: T) -> T", | ||
268 | ), | ||
269 | deprecated: false, | ||
270 | }, | ||
271 | StructureNode { | ||
272 | parent: Some( | ||
273 | 2, | ||
274 | ), | ||
275 | label: "bar3", | ||
276 | navigation_range: [89; 93), | ||
277 | node_range: [86; 156), | ||
278 | kind: FN_DEF, | ||
279 | detail: Some( | ||
280 | "fn<A, B>(a: A, b: B) -> Vec< u32 >", | ||
281 | ), | ||
282 | deprecated: false, | ||
283 | }, | ||
284 | StructureNode { | ||
285 | parent: None, | ||
286 | label: "E", | ||
287 | navigation_range: [165; 166), | ||
288 | node_range: [160; 180), | ||
289 | kind: ENUM_DEF, | ||
290 | detail: None, | ||
291 | deprecated: false, | ||
292 | }, | ||
293 | StructureNode { | ||
294 | parent: Some( | ||
295 | 6, | ||
296 | ), | ||
297 | label: "X", | ||
298 | navigation_range: [169; 170), | ||
299 | node_range: [169; 170), | ||
300 | kind: ENUM_VARIANT, | ||
301 | detail: None, | ||
302 | deprecated: false, | ||
303 | }, | ||
304 | StructureNode { | ||
305 | parent: Some( | ||
306 | 6, | ||
307 | ), | ||
308 | label: "Y", | ||
309 | navigation_range: [172; 173), | ||
310 | node_range: [172; 178), | ||
311 | kind: ENUM_VARIANT, | ||
312 | detail: None, | ||
313 | deprecated: false, | ||
314 | }, | ||
315 | StructureNode { | ||
316 | parent: None, | ||
317 | label: "T", | ||
318 | navigation_range: [186; 187), | ||
319 | node_range: [181; 193), | ||
320 | kind: TYPE_ALIAS_DEF, | ||
321 | detail: Some( | ||
322 | "()", | ||
323 | ), | ||
324 | deprecated: false, | ||
325 | }, | ||
326 | StructureNode { | ||
327 | parent: None, | ||
328 | label: "S", | ||
329 | navigation_range: [201; 202), | ||
330 | node_range: [194; 213), | ||
331 | kind: STATIC_DEF, | ||
332 | detail: Some( | ||
333 | "i32", | ||
334 | ), | ||
335 | deprecated: false, | ||
336 | }, | ||
337 | StructureNode { | ||
338 | parent: None, | ||
339 | label: "C", | ||
340 | navigation_range: [220; 221), | ||
341 | node_range: [214; 232), | ||
342 | kind: CONST_DEF, | ||
343 | detail: Some( | ||
344 | "i32", | ||
345 | ), | ||
346 | deprecated: false, | ||
347 | }, | ||
348 | StructureNode { | ||
349 | parent: None, | ||
350 | label: "impl E", | ||
351 | navigation_range: [239; 240), | ||
352 | node_range: [234; 243), | ||
353 | kind: IMPL_BLOCK, | ||
354 | detail: None, | ||
355 | deprecated: false, | ||
356 | }, | ||
357 | StructureNode { | ||
358 | parent: None, | ||
359 | label: "impl fmt::Debug for E", | ||
360 | navigation_range: [265; 266), | ||
361 | node_range: [245; 269), | ||
362 | kind: IMPL_BLOCK, | ||
363 | detail: None, | ||
364 | deprecated: false, | ||
365 | }, | ||
366 | StructureNode { | ||
367 | parent: None, | ||
368 | label: "mc", | ||
369 | navigation_range: [284; 286), | ||
370 | node_range: [271; 303), | ||
371 | kind: MACRO_CALL, | ||
372 | detail: None, | ||
373 | deprecated: false, | ||
374 | }, | ||
375 | StructureNode { | ||
376 | parent: None, | ||
377 | label: "obsolete", | ||
378 | navigation_range: [322; 330), | ||
379 | node_range: [305; 335), | ||
380 | kind: FN_DEF, | ||
381 | detail: Some( | ||
382 | "fn()", | ||
383 | ), | ||
384 | deprecated: true, | ||
385 | }, | ||
386 | StructureNode { | ||
387 | parent: None, | ||
388 | label: "very_obsolete", | ||
389 | navigation_range: [375; 388), | ||
390 | node_range: [337; 393), | ||
391 | kind: FN_DEF, | ||
392 | detail: Some( | ||
393 | "fn()", | ||
394 | ), | ||
395 | deprecated: true, | ||
396 | }, | ||
397 | ] | ||
398 | "### | ||
399 | ); | ||
400 | } | ||
401 | } | ||
diff --git a/crates/ra_ide/src/expand.rs b/crates/ra_ide/src/expand.rs new file mode 100644 index 000000000..2f1abf509 --- /dev/null +++ b/crates/ra_ide/src/expand.rs | |||
@@ -0,0 +1,63 @@ | |||
1 | //! Utilities to work with files, produced by macros. | ||
2 | use std::iter::successors; | ||
3 | |||
4 | use hir::Source; | ||
5 | use ra_db::FileId; | ||
6 | use ra_syntax::{ast, AstNode, SyntaxNode, SyntaxToken}; | ||
7 | |||
8 | use crate::{db::RootDatabase, FileRange}; | ||
9 | |||
10 | pub(crate) fn original_range(db: &RootDatabase, node: Source<&SyntaxNode>) -> FileRange { | ||
11 | let expansion = match node.file_id.expansion_info(db) { | ||
12 | None => { | ||
13 | return FileRange { | ||
14 | file_id: node.file_id.original_file(db), | ||
15 | range: node.value.text_range(), | ||
16 | } | ||
17 | } | ||
18 | Some(it) => it, | ||
19 | }; | ||
20 | // FIXME: the following completely wrong. | ||
21 | // | ||
22 | // *First*, we should try to map first and last tokens of node, and, if that | ||
23 | // fails, return the range of the overall macro expansions. | ||
24 | // | ||
25 | // *Second*, we should handle recurside macro expansions | ||
26 | |||
27 | let token = node | ||
28 | .value | ||
29 | .descendants_with_tokens() | ||
30 | .filter_map(|it| it.into_token()) | ||
31 | .find_map(|it| expansion.map_token_up(node.with_value(&it))); | ||
32 | |||
33 | match token { | ||
34 | Some(it) => { | ||
35 | FileRange { file_id: it.file_id.original_file(db), range: it.value.text_range() } | ||
36 | } | ||
37 | None => { | ||
38 | FileRange { file_id: node.file_id.original_file(db), range: node.value.text_range() } | ||
39 | } | ||
40 | } | ||
41 | } | ||
42 | |||
43 | pub(crate) fn descend_into_macros( | ||
44 | db: &RootDatabase, | ||
45 | file_id: FileId, | ||
46 | token: SyntaxToken, | ||
47 | ) -> Source<SyntaxToken> { | ||
48 | let src = Source::new(file_id.into(), token); | ||
49 | |||
50 | successors(Some(src), |token| { | ||
51 | let macro_call = token.value.ancestors().find_map(ast::MacroCall::cast)?; | ||
52 | let tt = macro_call.token_tree()?; | ||
53 | if !token.value.text_range().is_subrange(&tt.syntax().text_range()) { | ||
54 | return None; | ||
55 | } | ||
56 | let source_analyzer = | ||
57 | hir::SourceAnalyzer::new(db, token.with_value(token.value.parent()).as_ref(), None); | ||
58 | let exp = source_analyzer.expand(db, token.with_value(¯o_call))?; | ||
59 | exp.map_token_down(db, token.as_ref()) | ||
60 | }) | ||
61 | .last() | ||
62 | .unwrap() | ||
63 | } | ||
diff --git a/crates/ra_ide/src/expand_macro.rs b/crates/ra_ide/src/expand_macro.rs new file mode 100644 index 000000000..abc602244 --- /dev/null +++ b/crates/ra_ide/src/expand_macro.rs | |||
@@ -0,0 +1,295 @@ | |||
1 | //! This modules implements "expand macro" functionality in the IDE | ||
2 | |||
3 | use crate::{db::RootDatabase, FilePosition}; | ||
4 | use hir::db::AstDatabase; | ||
5 | use ra_db::SourceDatabase; | ||
6 | use rustc_hash::FxHashMap; | ||
7 | |||
8 | use ra_syntax::{ | ||
9 | algo::{find_node_at_offset, replace_descendants}, | ||
10 | ast::{self}, | ||
11 | AstNode, NodeOrToken, SyntaxKind, SyntaxNode, WalkEvent, T, | ||
12 | }; | ||
13 | |||
14 | pub struct ExpandedMacro { | ||
15 | pub name: String, | ||
16 | pub expansion: String, | ||
17 | } | ||
18 | |||
19 | pub(crate) fn expand_macro(db: &RootDatabase, position: FilePosition) -> Option<ExpandedMacro> { | ||
20 | let parse = db.parse(position.file_id); | ||
21 | let file = parse.tree(); | ||
22 | let name_ref = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset)?; | ||
23 | let mac = name_ref.syntax().ancestors().find_map(ast::MacroCall::cast)?; | ||
24 | |||
25 | let source = hir::Source::new(position.file_id.into(), mac.syntax()); | ||
26 | let expanded = expand_macro_recur(db, source, source.with_value(&mac))?; | ||
27 | |||
28 | // FIXME: | ||
29 | // macro expansion may lose all white space information | ||
30 | // But we hope someday we can use ra_fmt for that | ||
31 | let expansion = insert_whitespaces(expanded); | ||
32 | Some(ExpandedMacro { name: name_ref.text().to_string(), expansion }) | ||
33 | } | ||
34 | |||
35 | fn expand_macro_recur( | ||
36 | db: &RootDatabase, | ||
37 | source: hir::Source<&SyntaxNode>, | ||
38 | macro_call: hir::Source<&ast::MacroCall>, | ||
39 | ) -> Option<SyntaxNode> { | ||
40 | let analyzer = hir::SourceAnalyzer::new(db, source, None); | ||
41 | let expansion = analyzer.expand(db, macro_call)?; | ||
42 | let macro_file_id = expansion.file_id(); | ||
43 | let mut expanded: SyntaxNode = db.parse_or_expand(macro_file_id)?; | ||
44 | |||
45 | let children = expanded.descendants().filter_map(ast::MacroCall::cast); | ||
46 | let mut replaces = FxHashMap::default(); | ||
47 | |||
48 | for child in children.into_iter() { | ||
49 | let node = hir::Source::new(macro_file_id, &child); | ||
50 | if let Some(new_node) = expand_macro_recur(db, source, node) { | ||
51 | // Replace the whole node if it is root | ||
52 | // `replace_descendants` will not replace the parent node | ||
53 | // but `SyntaxNode::descendants include itself | ||
54 | if expanded == *child.syntax() { | ||
55 | expanded = new_node; | ||
56 | } else { | ||
57 | replaces.insert(child.syntax().clone().into(), new_node.into()); | ||
58 | } | ||
59 | } | ||
60 | } | ||
61 | |||
62 | Some(replace_descendants(&expanded, &replaces)) | ||
63 | } | ||
64 | |||
65 | // FIXME: It would also be cool to share logic here and in the mbe tests, | ||
66 | // which are pretty unreadable at the moment. | ||
67 | fn insert_whitespaces(syn: SyntaxNode) -> String { | ||
68 | use SyntaxKind::*; | ||
69 | |||
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 = |f: fn(SyntaxKind) -> bool, default| -> bool { | ||
90 | last.map(|it| f(it)).unwrap_or(default) | ||
91 | }; | ||
92 | |||
93 | res += &match token.kind() { | ||
94 | k @ _ if is_text(k) && is_next(|it| !it.is_punct(), true) => { | ||
95 | token.text().to_string() + " " | ||
96 | } | ||
97 | L_CURLY if is_next(|it| it != R_CURLY, true) => { | ||
98 | indent += 1; | ||
99 | let leading_space = if is_last(|it| is_text(it), false) { " " } else { "" }; | ||
100 | format!("{}{{\n{}", leading_space, " ".repeat(indent)) | ||
101 | } | ||
102 | R_CURLY if is_last(|it| it != L_CURLY, true) => { | ||
103 | indent = indent.checked_sub(1).unwrap_or(0); | ||
104 | format!("\n{}}}", " ".repeat(indent)) | ||
105 | } | ||
106 | R_CURLY => format!("}}\n{}", " ".repeat(indent)), | ||
107 | T![;] => format!(";\n{}", " ".repeat(indent)), | ||
108 | T![->] => " -> ".to_string(), | ||
109 | T![=] => " = ".to_string(), | ||
110 | T![=>] => " => ".to_string(), | ||
111 | _ => token.text().to_string(), | ||
112 | }; | ||
113 | |||
114 | last = Some(token.kind()); | ||
115 | } | ||
116 | |||
117 | return res; | ||
118 | |||
119 | fn is_text(k: SyntaxKind) -> bool { | ||
120 | k.is_keyword() || k.is_literal() || k == IDENT | ||
121 | } | ||
122 | } | ||
123 | |||
124 | #[cfg(test)] | ||
125 | mod tests { | ||
126 | use super::*; | ||
127 | use crate::mock_analysis::analysis_and_position; | ||
128 | use insta::assert_snapshot; | ||
129 | |||
130 | fn check_expand_macro(fixture: &str) -> ExpandedMacro { | ||
131 | let (analysis, pos) = analysis_and_position(fixture); | ||
132 | analysis.expand_macro(pos).unwrap().unwrap() | ||
133 | } | ||
134 | |||
135 | #[test] | ||
136 | fn macro_expand_recursive_expansion() { | ||
137 | let res = check_expand_macro( | ||
138 | r#" | ||
139 | //- /lib.rs | ||
140 | macro_rules! bar { | ||
141 | () => { fn b() {} } | ||
142 | } | ||
143 | macro_rules! foo { | ||
144 | () => { bar!(); } | ||
145 | } | ||
146 | macro_rules! baz { | ||
147 | () => { foo!(); } | ||
148 | } | ||
149 | f<|>oo!(); | ||
150 | "#, | ||
151 | ); | ||
152 | |||
153 | assert_eq!(res.name, "foo"); | ||
154 | assert_snapshot!(res.expansion, @r###" | ||
155 | fn b(){} | ||
156 | "###); | ||
157 | } | ||
158 | |||
159 | #[test] | ||
160 | fn macro_expand_multiple_lines() { | ||
161 | let res = check_expand_macro( | ||
162 | r#" | ||
163 | //- /lib.rs | ||
164 | macro_rules! foo { | ||
165 | () => { | ||
166 | fn some_thing() -> u32 { | ||
167 | let a = 0; | ||
168 | a + 10 | ||
169 | } | ||
170 | } | ||
171 | } | ||
172 | f<|>oo!(); | ||
173 | "#, | ||
174 | ); | ||
175 | |||
176 | assert_eq!(res.name, "foo"); | ||
177 | assert_snapshot!(res.expansion, @r###" | ||
178 | fn some_thing() -> u32 { | ||
179 | let a = 0; | ||
180 | a+10 | ||
181 | } | ||
182 | "###); | ||
183 | } | ||
184 | |||
185 | #[test] | ||
186 | fn macro_expand_match_ast() { | ||
187 | let res = check_expand_macro( | ||
188 | r#" | ||
189 | //- /lib.rs | ||
190 | macro_rules! match_ast { | ||
191 | (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; | ||
192 | |||
193 | (match ($node:expr) { | ||
194 | $( ast::$ast:ident($it:ident) => $res:block, )* | ||
195 | _ => $catch_all:expr $(,)? | ||
196 | }) => {{ | ||
197 | $( if let Some($it) = ast::$ast::cast($node.clone()) $res else )* | ||
198 | { $catch_all } | ||
199 | }}; | ||
200 | } | ||
201 | |||
202 | fn main() { | ||
203 | mat<|>ch_ast! { | ||
204 | match container { | ||
205 | ast::TraitDef(it) => {}, | ||
206 | ast::ImplBlock(it) => {}, | ||
207 | _ => { continue }, | ||
208 | } | ||
209 | } | ||
210 | } | ||
211 | "#, | ||
212 | ); | ||
213 | |||
214 | assert_eq!(res.name, "match_ast"); | ||
215 | assert_snapshot!(res.expansion, @r###" | ||
216 | { | ||
217 | if let Some(it) = ast::TraitDef::cast(container.clone()){} | ||
218 | else if let Some(it) = ast::ImplBlock::cast(container.clone()){} | ||
219 | else { | ||
220 | { | ||
221 | continue | ||
222 | } | ||
223 | } | ||
224 | } | ||
225 | "###); | ||
226 | } | ||
227 | |||
228 | #[test] | ||
229 | fn macro_expand_match_ast_inside_let_statement() { | ||
230 | let res = check_expand_macro( | ||
231 | r#" | ||
232 | //- /lib.rs | ||
233 | macro_rules! match_ast { | ||
234 | (match $node:ident { $($tt:tt)* }) => { match_ast!(match ($node) { $($tt)* }) }; | ||
235 | (match ($node:expr) {}) => {{}}; | ||
236 | } | ||
237 | |||
238 | fn main() { | ||
239 | let p = f(|it| { | ||
240 | let res = mat<|>ch_ast! { match c {}}; | ||
241 | Some(res) | ||
242 | })?; | ||
243 | } | ||
244 | "#, | ||
245 | ); | ||
246 | |||
247 | assert_eq!(res.name, "match_ast"); | ||
248 | assert_snapshot!(res.expansion, @r###"{}"###); | ||
249 | } | ||
250 | |||
251 | #[test] | ||
252 | fn macro_expand_inner_macro_fail_to_expand() { | ||
253 | let res = check_expand_macro( | ||
254 | r#" | ||
255 | //- /lib.rs | ||
256 | macro_rules! bar { | ||
257 | (BAD) => {}; | ||
258 | } | ||
259 | macro_rules! foo { | ||
260 | () => {bar!()}; | ||
261 | } | ||
262 | |||
263 | fn main() { | ||
264 | let res = fo<|>o!(); | ||
265 | } | ||
266 | "#, | ||
267 | ); | ||
268 | |||
269 | assert_eq!(res.name, "foo"); | ||
270 | assert_snapshot!(res.expansion, @r###"bar!()"###); | ||
271 | } | ||
272 | |||
273 | #[test] | ||
274 | fn macro_expand_with_dollar_crate() { | ||
275 | let res = check_expand_macro( | ||
276 | r#" | ||
277 | //- /lib.rs | ||
278 | #[macro_export] | ||
279 | macro_rules! bar { | ||
280 | () => {0}; | ||
281 | } | ||
282 | macro_rules! foo { | ||
283 | () => {$crate::bar!()}; | ||
284 | } | ||
285 | |||
286 | fn main() { | ||
287 | let res = fo<|>o!(); | ||
288 | } | ||
289 | "#, | ||
290 | ); | ||
291 | |||
292 | assert_eq!(res.name, "foo"); | ||
293 | assert_snapshot!(res.expansion, @r###"0"###); | ||
294 | } | ||
295 | } | ||
diff --git a/crates/ra_ide/src/extend_selection.rs b/crates/ra_ide/src/extend_selection.rs new file mode 100644 index 000000000..4b7bfc0b1 --- /dev/null +++ b/crates/ra_ide/src/extend_selection.rs | |||
@@ -0,0 +1,452 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_db::SourceDatabase; | ||
4 | use ra_syntax::{ | ||
5 | algo::find_covering_element, | ||
6 | ast::{self, AstNode, AstToken}, | ||
7 | Direction, NodeOrToken, | ||
8 | SyntaxKind::{self, *}, | ||
9 | SyntaxNode, SyntaxToken, TextRange, TextUnit, TokenAtOffset, T, | ||
10 | }; | ||
11 | |||
12 | use crate::{db::RootDatabase, FileRange}; | ||
13 | |||
14 | // FIXME: restore macro support | ||
15 | pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRange { | ||
16 | let parse = db.parse(frange.file_id); | ||
17 | try_extend_selection(parse.tree().syntax(), frange.range).unwrap_or(frange.range) | ||
18 | } | ||
19 | |||
20 | fn try_extend_selection(root: &SyntaxNode, range: TextRange) -> Option<TextRange> { | ||
21 | let string_kinds = [COMMENT, STRING, RAW_STRING, BYTE_STRING, RAW_BYTE_STRING]; | ||
22 | let list_kinds = [ | ||
23 | RECORD_FIELD_PAT_LIST, | ||
24 | MATCH_ARM_LIST, | ||
25 | RECORD_FIELD_DEF_LIST, | ||
26 | TUPLE_FIELD_DEF_LIST, | ||
27 | RECORD_FIELD_LIST, | ||
28 | ENUM_VARIANT_LIST, | ||
29 | USE_TREE_LIST, | ||
30 | TYPE_PARAM_LIST, | ||
31 | TYPE_ARG_LIST, | ||
32 | TYPE_BOUND_LIST, | ||
33 | PARAM_LIST, | ||
34 | ARG_LIST, | ||
35 | ARRAY_EXPR, | ||
36 | TUPLE_EXPR, | ||
37 | WHERE_CLAUSE, | ||
38 | ]; | ||
39 | |||
40 | if range.is_empty() { | ||
41 | let offset = range.start(); | ||
42 | let mut leaves = root.token_at_offset(offset); | ||
43 | if leaves.clone().all(|it| it.kind() == WHITESPACE) { | ||
44 | return Some(extend_ws(root, leaves.next()?, offset)); | ||
45 | } | ||
46 | let leaf_range = match leaves { | ||
47 | TokenAtOffset::None => return None, | ||
48 | TokenAtOffset::Single(l) => { | ||
49 | if string_kinds.contains(&l.kind()) { | ||
50 | extend_single_word_in_comment_or_string(&l, offset) | ||
51 | .unwrap_or_else(|| l.text_range()) | ||
52 | } else { | ||
53 | l.text_range() | ||
54 | } | ||
55 | } | ||
56 | TokenAtOffset::Between(l, r) => pick_best(l, r).text_range(), | ||
57 | }; | ||
58 | return Some(leaf_range); | ||
59 | }; | ||
60 | let node = match find_covering_element(root, range) { | ||
61 | NodeOrToken::Token(token) => { | ||
62 | if token.text_range() != range { | ||
63 | return Some(token.text_range()); | ||
64 | } | ||
65 | if let Some(comment) = ast::Comment::cast(token.clone()) { | ||
66 | if let Some(range) = extend_comments(comment) { | ||
67 | return Some(range); | ||
68 | } | ||
69 | } | ||
70 | token.parent() | ||
71 | } | ||
72 | NodeOrToken::Node(node) => node, | ||
73 | }; | ||
74 | if node.text_range() != range { | ||
75 | return Some(node.text_range()); | ||
76 | } | ||
77 | |||
78 | // Using shallowest node with same range allows us to traverse siblings. | ||
79 | let node = node.ancestors().take_while(|n| n.text_range() == node.text_range()).last().unwrap(); | ||
80 | |||
81 | if node.parent().map(|n| list_kinds.contains(&n.kind())) == Some(true) { | ||
82 | if let Some(range) = extend_list_item(&node) { | ||
83 | return Some(range); | ||
84 | } | ||
85 | } | ||
86 | |||
87 | node.parent().map(|it| it.text_range()) | ||
88 | } | ||
89 | |||
90 | fn extend_single_word_in_comment_or_string( | ||
91 | leaf: &SyntaxToken, | ||
92 | offset: TextUnit, | ||
93 | ) -> Option<TextRange> { | ||
94 | let text: &str = leaf.text(); | ||
95 | let cursor_position: u32 = (offset - leaf.text_range().start()).into(); | ||
96 | |||
97 | let (before, after) = text.split_at(cursor_position as usize); | ||
98 | |||
99 | fn non_word_char(c: char) -> bool { | ||
100 | !(c.is_alphanumeric() || c == '_') | ||
101 | } | ||
102 | |||
103 | let start_idx = before.rfind(non_word_char)? as u32; | ||
104 | let end_idx = after.find(non_word_char).unwrap_or_else(|| after.len()) as u32; | ||
105 | |||
106 | let from: TextUnit = (start_idx + 1).into(); | ||
107 | let to: TextUnit = (cursor_position + end_idx).into(); | ||
108 | |||
109 | let range = TextRange::from_to(from, to); | ||
110 | if range.is_empty() { | ||
111 | None | ||
112 | } else { | ||
113 | Some(range + leaf.text_range().start()) | ||
114 | } | ||
115 | } | ||
116 | |||
117 | fn extend_ws(root: &SyntaxNode, ws: SyntaxToken, offset: TextUnit) -> TextRange { | ||
118 | let ws_text = ws.text(); | ||
119 | let suffix = TextRange::from_to(offset, ws.text_range().end()) - ws.text_range().start(); | ||
120 | let prefix = TextRange::from_to(ws.text_range().start(), offset) - ws.text_range().start(); | ||
121 | let ws_suffix = &ws_text.as_str()[suffix]; | ||
122 | let ws_prefix = &ws_text.as_str()[prefix]; | ||
123 | if ws_text.contains('\n') && !ws_suffix.contains('\n') { | ||
124 | if let Some(node) = ws.next_sibling_or_token() { | ||
125 | let start = match ws_prefix.rfind('\n') { | ||
126 | Some(idx) => ws.text_range().start() + TextUnit::from((idx + 1) as u32), | ||
127 | None => node.text_range().start(), | ||
128 | }; | ||
129 | let end = if root.text().char_at(node.text_range().end()) == Some('\n') { | ||
130 | node.text_range().end() + TextUnit::of_char('\n') | ||
131 | } else { | ||
132 | node.text_range().end() | ||
133 | }; | ||
134 | return TextRange::from_to(start, end); | ||
135 | } | ||
136 | } | ||
137 | ws.text_range() | ||
138 | } | ||
139 | |||
140 | fn pick_best<'a>(l: SyntaxToken, r: SyntaxToken) -> SyntaxToken { | ||
141 | return if priority(&r) > priority(&l) { r } else { l }; | ||
142 | fn priority(n: &SyntaxToken) -> usize { | ||
143 | match n.kind() { | ||
144 | WHITESPACE => 0, | ||
145 | IDENT | T![self] | T![super] | T![crate] | LIFETIME => 2, | ||
146 | _ => 1, | ||
147 | } | ||
148 | } | ||
149 | } | ||
150 | |||
151 | /// Extend list item selection to include nearby delimiter and whitespace. | ||
152 | fn extend_list_item(node: &SyntaxNode) -> Option<TextRange> { | ||
153 | fn is_single_line_ws(node: &SyntaxToken) -> bool { | ||
154 | node.kind() == WHITESPACE && !node.text().contains('\n') | ||
155 | } | ||
156 | |||
157 | fn nearby_delimiter( | ||
158 | delimiter_kind: SyntaxKind, | ||
159 | node: &SyntaxNode, | ||
160 | dir: Direction, | ||
161 | ) -> Option<SyntaxToken> { | ||
162 | node.siblings_with_tokens(dir) | ||
163 | .skip(1) | ||
164 | .skip_while(|node| match node { | ||
165 | NodeOrToken::Node(_) => false, | ||
166 | NodeOrToken::Token(it) => is_single_line_ws(it), | ||
167 | }) | ||
168 | .next() | ||
169 | .and_then(|it| it.into_token()) | ||
170 | .filter(|node| node.kind() == delimiter_kind) | ||
171 | } | ||
172 | |||
173 | let delimiter = match node.kind() { | ||
174 | TYPE_BOUND => T![+], | ||
175 | _ => T![,], | ||
176 | }; | ||
177 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Prev) { | ||
178 | return Some(TextRange::from_to( | ||
179 | delimiter_node.text_range().start(), | ||
180 | node.text_range().end(), | ||
181 | )); | ||
182 | } | ||
183 | if let Some(delimiter_node) = nearby_delimiter(delimiter, node, Direction::Next) { | ||
184 | // Include any following whitespace when delimiter is after list item. | ||
185 | let final_node = delimiter_node | ||
186 | .next_sibling_or_token() | ||
187 | .and_then(|it| it.into_token()) | ||
188 | .filter(|node| is_single_line_ws(node)) | ||
189 | .unwrap_or(delimiter_node); | ||
190 | |||
191 | return Some(TextRange::from_to(node.text_range().start(), final_node.text_range().end())); | ||
192 | } | ||
193 | |||
194 | None | ||
195 | } | ||
196 | |||
197 | fn extend_comments(comment: ast::Comment) -> Option<TextRange> { | ||
198 | let prev = adj_comments(&comment, Direction::Prev); | ||
199 | let next = adj_comments(&comment, Direction::Next); | ||
200 | if prev != next { | ||
201 | Some(TextRange::from_to( | ||
202 | prev.syntax().text_range().start(), | ||
203 | next.syntax().text_range().end(), | ||
204 | )) | ||
205 | } else { | ||
206 | None | ||
207 | } | ||
208 | } | ||
209 | |||
210 | fn adj_comments(comment: &ast::Comment, dir: Direction) -> ast::Comment { | ||
211 | let mut res = comment.clone(); | ||
212 | for element in comment.syntax().siblings_with_tokens(dir) { | ||
213 | let token = match element.as_token() { | ||
214 | None => break, | ||
215 | Some(token) => token, | ||
216 | }; | ||
217 | if let Some(c) = ast::Comment::cast(token.clone()) { | ||
218 | res = c | ||
219 | } else if token.kind() != WHITESPACE || token.text().contains("\n\n") { | ||
220 | break; | ||
221 | } | ||
222 | } | ||
223 | res | ||
224 | } | ||
225 | |||
226 | #[cfg(test)] | ||
227 | mod tests { | ||
228 | use ra_syntax::{AstNode, SourceFile}; | ||
229 | use test_utils::extract_offset; | ||
230 | |||
231 | use super::*; | ||
232 | |||
233 | fn do_check(before: &str, afters: &[&str]) { | ||
234 | let (cursor, before) = extract_offset(before); | ||
235 | let parse = SourceFile::parse(&before); | ||
236 | let mut range = TextRange::offset_len(cursor, 0.into()); | ||
237 | for &after in afters { | ||
238 | range = try_extend_selection(parse.tree().syntax(), range).unwrap(); | ||
239 | let actual = &before[range]; | ||
240 | assert_eq!(after, actual); | ||
241 | } | ||
242 | } | ||
243 | |||
244 | #[test] | ||
245 | fn test_extend_selection_arith() { | ||
246 | do_check(r#"fn foo() { <|>1 + 1 }"#, &["1", "1 + 1", "{ 1 + 1 }"]); | ||
247 | } | ||
248 | |||
249 | #[test] | ||
250 | fn test_extend_selection_list() { | ||
251 | do_check(r#"fn foo(<|>x: i32) {}"#, &["x", "x: i32"]); | ||
252 | do_check(r#"fn foo(<|>x: i32, y: i32) {}"#, &["x", "x: i32", "x: i32, "]); | ||
253 | do_check(r#"fn foo(<|>x: i32,y: i32) {}"#, &["x", "x: i32", "x: i32,"]); | ||
254 | do_check(r#"fn foo(x: i32, <|>y: i32) {}"#, &["y", "y: i32", ", y: i32"]); | ||
255 | do_check(r#"fn foo(x: i32, <|>y: i32, ) {}"#, &["y", "y: i32", ", y: i32"]); | ||
256 | do_check(r#"fn foo(x: i32,<|>y: i32) {}"#, &["y", "y: i32", ",y: i32"]); | ||
257 | |||
258 | do_check(r#"const FOO: [usize; 2] = [ 22<|> , 33];"#, &["22", "22 , "]); | ||
259 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|>];"#, &["33", ", 33"]); | ||
260 | do_check(r#"const FOO: [usize; 2] = [ 22 , 33<|> ,];"#, &["33", ", 33"]); | ||
261 | |||
262 | do_check(r#"fn main() { (1, 2<|>) }"#, &["2", ", 2", "(1, 2)"]); | ||
263 | |||
264 | do_check( | ||
265 | r#" | ||
266 | const FOO: [usize; 2] = [ | ||
267 | 22, | ||
268 | <|>33, | ||
269 | ]"#, | ||
270 | &["33", "33,"], | ||
271 | ); | ||
272 | |||
273 | do_check( | ||
274 | r#" | ||
275 | const FOO: [usize; 2] = [ | ||
276 | 22 | ||
277 | , 33<|>, | ||
278 | ]"#, | ||
279 | &["33", ", 33"], | ||
280 | ); | ||
281 | } | ||
282 | |||
283 | #[test] | ||
284 | fn test_extend_selection_start_of_the_line() { | ||
285 | do_check( | ||
286 | r#" | ||
287 | impl S { | ||
288 | <|> fn foo() { | ||
289 | |||
290 | } | ||
291 | }"#, | ||
292 | &[" fn foo() {\n\n }\n"], | ||
293 | ); | ||
294 | } | ||
295 | |||
296 | #[test] | ||
297 | fn test_extend_selection_doc_comments() { | ||
298 | do_check( | ||
299 | r#" | ||
300 | struct A; | ||
301 | |||
302 | /// bla | ||
303 | /// bla | ||
304 | struct B { | ||
305 | <|> | ||
306 | } | ||
307 | "#, | ||
308 | &["\n \n", "{\n \n}", "/// bla\n/// bla\nstruct B {\n \n}"], | ||
309 | ) | ||
310 | } | ||
311 | |||
312 | #[test] | ||
313 | fn test_extend_selection_comments() { | ||
314 | do_check( | ||
315 | r#" | ||
316 | fn bar(){} | ||
317 | |||
318 | // fn foo() { | ||
319 | // 1 + <|>1 | ||
320 | // } | ||
321 | |||
322 | // fn foo(){} | ||
323 | "#, | ||
324 | &["1", "// 1 + 1", "// fn foo() {\n// 1 + 1\n// }"], | ||
325 | ); | ||
326 | |||
327 | do_check( | ||
328 | r#" | ||
329 | // #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
330 | // pub enum Direction { | ||
331 | // <|> Next, | ||
332 | // Prev | ||
333 | // } | ||
334 | "#, | ||
335 | &[ | ||
336 | "// Next,", | ||
337 | "// #[derive(Debug, Clone, Copy, PartialEq, Eq)]\n// pub enum Direction {\n// Next,\n// Prev\n// }", | ||
338 | ], | ||
339 | ); | ||
340 | |||
341 | do_check( | ||
342 | r#" | ||
343 | /* | ||
344 | foo | ||
345 | _bar1<|>*/ | ||
346 | "#, | ||
347 | &["_bar1", "/*\nfoo\n_bar1*/"], | ||
348 | ); | ||
349 | |||
350 | do_check(r#"//!<|>foo_2 bar"#, &["foo_2", "//!foo_2 bar"]); | ||
351 | |||
352 | do_check(r#"/<|>/foo bar"#, &["//foo bar"]); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn test_extend_selection_prefer_idents() { | ||
357 | do_check( | ||
358 | r#" | ||
359 | fn main() { foo<|>+bar;} | ||
360 | "#, | ||
361 | &["foo", "foo+bar"], | ||
362 | ); | ||
363 | do_check( | ||
364 | r#" | ||
365 | fn main() { foo+<|>bar;} | ||
366 | "#, | ||
367 | &["bar", "foo+bar"], | ||
368 | ); | ||
369 | } | ||
370 | |||
371 | #[test] | ||
372 | fn test_extend_selection_prefer_lifetimes() { | ||
373 | do_check(r#"fn foo<<|>'a>() {}"#, &["'a", "<'a>"]); | ||
374 | do_check(r#"fn foo<'a<|>>() {}"#, &["'a", "<'a>"]); | ||
375 | } | ||
376 | |||
377 | #[test] | ||
378 | fn test_extend_selection_select_first_word() { | ||
379 | do_check(r#"// foo bar b<|>az quxx"#, &["baz", "// foo bar baz quxx"]); | ||
380 | do_check( | ||
381 | r#" | ||
382 | impl S { | ||
383 | fn foo() { | ||
384 | // hel<|>lo world | ||
385 | } | ||
386 | } | ||
387 | "#, | ||
388 | &["hello", "// hello world"], | ||
389 | ); | ||
390 | } | ||
391 | |||
392 | #[test] | ||
393 | fn test_extend_selection_string() { | ||
394 | do_check( | ||
395 | r#" | ||
396 | fn bar(){} | ||
397 | |||
398 | " fn f<|>oo() {" | ||
399 | "#, | ||
400 | &["foo", "\" fn foo() {\""], | ||
401 | ); | ||
402 | } | ||
403 | |||
404 | #[test] | ||
405 | fn test_extend_trait_bounds_list_in_where_clause() { | ||
406 | do_check( | ||
407 | r#" | ||
408 | fn foo<R>() | ||
409 | where | ||
410 | R: req::Request + 'static, | ||
411 | R::Params: DeserializeOwned<|> + panic::UnwindSafe + 'static, | ||
412 | R::Result: Serialize + 'static, | ||
413 | "#, | ||
414 | &[ | ||
415 | "DeserializeOwned", | ||
416 | "DeserializeOwned + ", | ||
417 | "DeserializeOwned + panic::UnwindSafe + 'static", | ||
418 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static", | ||
419 | "R::Params: DeserializeOwned + panic::UnwindSafe + 'static,", | ||
420 | ], | ||
421 | ); | ||
422 | do_check(r#"fn foo<T>() where T: <|>Copy"#, &["Copy"]); | ||
423 | do_check(r#"fn foo<T>() where T: <|>Copy + Display"#, &["Copy", "Copy + "]); | ||
424 | do_check(r#"fn foo<T>() where T: <|>Copy +Display"#, &["Copy", "Copy +"]); | ||
425 | do_check(r#"fn foo<T>() where T: <|>Copy+Display"#, &["Copy", "Copy+"]); | ||
426 | do_check(r#"fn foo<T>() where T: Copy + <|>Display"#, &["Display", "+ Display"]); | ||
427 | do_check(r#"fn foo<T>() where T: Copy + <|>Display + Sync"#, &["Display", "+ Display"]); | ||
428 | do_check(r#"fn foo<T>() where T: Copy +<|>Display"#, &["Display", "+Display"]); | ||
429 | } | ||
430 | |||
431 | #[test] | ||
432 | fn test_extend_trait_bounds_list_inline() { | ||
433 | do_check(r#"fn foo<T: <|>Copy>() {}"#, &["Copy"]); | ||
434 | do_check(r#"fn foo<T: <|>Copy + Display>() {}"#, &["Copy", "Copy + "]); | ||
435 | do_check(r#"fn foo<T: <|>Copy +Display>() {}"#, &["Copy", "Copy +"]); | ||
436 | do_check(r#"fn foo<T: <|>Copy+Display>() {}"#, &["Copy", "Copy+"]); | ||
437 | do_check(r#"fn foo<T: Copy + <|>Display>() {}"#, &["Display", "+ Display"]); | ||
438 | do_check(r#"fn foo<T: Copy + <|>Display + Sync>() {}"#, &["Display", "+ Display"]); | ||
439 | do_check(r#"fn foo<T: Copy +<|>Display>() {}"#, &["Display", "+Display"]); | ||
440 | do_check( | ||
441 | r#"fn foo<T: Copy<|> + Display, U: Copy>() {}"#, | ||
442 | &[ | ||
443 | "Copy", | ||
444 | "Copy + ", | ||
445 | "Copy + Display", | ||
446 | "T: Copy + Display", | ||
447 | "T: Copy + Display, ", | ||
448 | "<T: Copy + Display, U: Copy>", | ||
449 | ], | ||
450 | ); | ||
451 | } | ||
452 | } | ||
diff --git a/crates/ra_ide/src/feature_flags.rs b/crates/ra_ide/src/feature_flags.rs new file mode 100644 index 000000000..de4ae513d --- /dev/null +++ b/crates/ra_ide/src/feature_flags.rs | |||
@@ -0,0 +1,70 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use rustc_hash::FxHashMap; | ||
4 | |||
5 | /// Feature flags hold fine-grained toggles for all *user-visible* features of | ||
6 | /// rust-analyzer. | ||
7 | /// | ||
8 | /// The exists such that users are able to disable any annoying feature (and, | ||
9 | /// with many users and many features, some features are bound to be annoying | ||
10 | /// for some users) | ||
11 | /// | ||
12 | /// Note that we purposefully use run-time checked strings, and not something | ||
13 | /// checked at compile time, to keep things simple and flexible. | ||
14 | /// | ||
15 | /// Also note that, at the moment, `FeatureFlags` also store features for | ||
16 | /// `ra_lsp_server`. This should be benign layering violation. | ||
17 | #[derive(Debug)] | ||
18 | pub struct FeatureFlags { | ||
19 | flags: FxHashMap<String, bool>, | ||
20 | } | ||
21 | |||
22 | impl FeatureFlags { | ||
23 | fn new(flags: &[(&str, bool)]) -> FeatureFlags { | ||
24 | let flags = flags | ||
25 | .iter() | ||
26 | .map(|&(name, value)| { | ||
27 | check_flag_name(name); | ||
28 | (name.to_string(), value) | ||
29 | }) | ||
30 | .collect(); | ||
31 | FeatureFlags { flags } | ||
32 | } | ||
33 | |||
34 | pub fn set(&mut self, flag: &str, value: bool) -> Result<(), ()> { | ||
35 | match self.flags.get_mut(flag) { | ||
36 | None => Err(()), | ||
37 | Some(slot) => { | ||
38 | *slot = value; | ||
39 | Ok(()) | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | |||
44 | pub fn get(&self, flag: &str) -> bool { | ||
45 | match self.flags.get(flag) { | ||
46 | None => panic!("unknown flag: {:?}", flag), | ||
47 | Some(value) => *value, | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl Default for FeatureFlags { | ||
53 | fn default() -> FeatureFlags { | ||
54 | FeatureFlags::new(&[ | ||
55 | ("lsp.diagnostics", true), | ||
56 | ("completion.insertion.add-call-parenthesis", true), | ||
57 | ("completion.enable-postfix", true), | ||
58 | ("notifications.workspace-loaded", true), | ||
59 | ]) | ||
60 | } | ||
61 | } | ||
62 | |||
63 | fn check_flag_name(flag: &str) { | ||
64 | for c in flag.bytes() { | ||
65 | match c { | ||
66 | b'a'..=b'z' | b'-' | b'.' => (), | ||
67 | _ => panic!("flag name does not match conventions: {:?}", flag), | ||
68 | } | ||
69 | } | ||
70 | } | ||
diff --git a/crates/ra_ide/src/folding_ranges.rs b/crates/ra_ide/src/folding_ranges.rs new file mode 100644 index 000000000..4eeb76d14 --- /dev/null +++ b/crates/ra_ide/src/folding_ranges.rs | |||
@@ -0,0 +1,378 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use rustc_hash::FxHashSet; | ||
4 | |||
5 | use ra_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 | } | ||
19 | |||
20 | #[derive(Debug)] | ||
21 | pub struct Fold { | ||
22 | pub range: TextRange, | ||
23 | pub kind: FoldKind, | ||
24 | } | ||
25 | |||
26 | pub(crate) fn folding_ranges(file: &SourceFile) -> Vec<Fold> { | ||
27 | let mut res = vec![]; | ||
28 | let mut visited_comments = FxHashSet::default(); | ||
29 | let mut visited_imports = FxHashSet::default(); | ||
30 | let mut visited_mods = FxHashSet::default(); | ||
31 | |||
32 | for element in file.syntax().descendants_with_tokens() { | ||
33 | // Fold items that span multiple lines | ||
34 | if let Some(kind) = fold_kind(element.kind()) { | ||
35 | let is_multiline = match &element { | ||
36 | NodeOrToken::Node(node) => node.text().contains_char('\n'), | ||
37 | NodeOrToken::Token(token) => token.text().contains('\n'), | ||
38 | }; | ||
39 | if is_multiline { | ||
40 | res.push(Fold { range: element.text_range(), kind }); | ||
41 | continue; | ||
42 | } | ||
43 | } | ||
44 | |||
45 | match element { | ||
46 | NodeOrToken::Token(token) => { | ||
47 | // Fold groups of comments | ||
48 | if let Some(comment) = ast::Comment::cast(token) { | ||
49 | if !visited_comments.contains(&comment) { | ||
50 | if let Some(range) = | ||
51 | contiguous_range_for_comment(comment, &mut visited_comments) | ||
52 | { | ||
53 | res.push(Fold { range, kind: FoldKind::Comment }) | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | NodeOrToken::Node(node) => { | ||
59 | // Fold groups of imports | ||
60 | if node.kind() == USE_ITEM && !visited_imports.contains(&node) { | ||
61 | if let Some(range) = contiguous_range_for_group(&node, &mut visited_imports) { | ||
62 | res.push(Fold { range, kind: FoldKind::Imports }) | ||
63 | } | ||
64 | } | ||
65 | |||
66 | // Fold groups of mods | ||
67 | if node.kind() == MODULE && !has_visibility(&node) && !visited_mods.contains(&node) | ||
68 | { | ||
69 | if let Some(range) = | ||
70 | contiguous_range_for_group_unless(&node, has_visibility, &mut visited_mods) | ||
71 | { | ||
72 | res.push(Fold { range, kind: FoldKind::Mods }) | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | } | ||
78 | |||
79 | res | ||
80 | } | ||
81 | |||
82 | fn fold_kind(kind: SyntaxKind) -> Option<FoldKind> { | ||
83 | match kind { | ||
84 | COMMENT => Some(FoldKind::Comment), | ||
85 | USE_ITEM => Some(FoldKind::Imports), | ||
86 | RECORD_FIELD_DEF_LIST | ||
87 | | RECORD_FIELD_PAT_LIST | ||
88 | | ITEM_LIST | ||
89 | | EXTERN_ITEM_LIST | ||
90 | | USE_TREE_LIST | ||
91 | | BLOCK | ||
92 | | MATCH_ARM_LIST | ||
93 | | ENUM_VARIANT_LIST | ||
94 | | TOKEN_TREE => Some(FoldKind::Block), | ||
95 | _ => None, | ||
96 | } | ||
97 | } | ||
98 | |||
99 | fn has_visibility(node: &SyntaxNode) -> bool { | ||
100 | ast::Module::cast(node.clone()).and_then(|m| m.visibility()).is_some() | ||
101 | } | ||
102 | |||
103 | fn contiguous_range_for_group( | ||
104 | first: &SyntaxNode, | ||
105 | visited: &mut FxHashSet<SyntaxNode>, | ||
106 | ) -> Option<TextRange> { | ||
107 | contiguous_range_for_group_unless(first, |_| false, visited) | ||
108 | } | ||
109 | |||
110 | fn contiguous_range_for_group_unless( | ||
111 | first: &SyntaxNode, | ||
112 | unless: impl Fn(&SyntaxNode) -> bool, | ||
113 | visited: &mut FxHashSet<SyntaxNode>, | ||
114 | ) -> Option<TextRange> { | ||
115 | visited.insert(first.clone()); | ||
116 | |||
117 | let mut last = first.clone(); | ||
118 | for element in first.siblings_with_tokens(Direction::Next) { | ||
119 | let node = match element { | ||
120 | NodeOrToken::Token(token) => { | ||
121 | if let Some(ws) = ast::Whitespace::cast(token) { | ||
122 | if !ws.spans_multiple_lines() { | ||
123 | // Ignore whitespace without blank lines | ||
124 | continue; | ||
125 | } | ||
126 | } | ||
127 | // There is a blank line or another token, which means that the | ||
128 | // group ends here | ||
129 | break; | ||
130 | } | ||
131 | NodeOrToken::Node(node) => node, | ||
132 | }; | ||
133 | |||
134 | // Stop if we find a node that doesn't belong to the group | ||
135 | if node.kind() != first.kind() || unless(&node) { | ||
136 | break; | ||
137 | } | ||
138 | |||
139 | visited.insert(node.clone()); | ||
140 | last = node; | ||
141 | } | ||
142 | |||
143 | if first != &last { | ||
144 | Some(TextRange::from_to(first.text_range().start(), last.text_range().end())) | ||
145 | } else { | ||
146 | // The group consists of only one element, therefore it cannot be folded | ||
147 | None | ||
148 | } | ||
149 | } | ||
150 | |||
151 | fn contiguous_range_for_comment( | ||
152 | first: ast::Comment, | ||
153 | visited: &mut FxHashSet<ast::Comment>, | ||
154 | ) -> Option<TextRange> { | ||
155 | visited.insert(first.clone()); | ||
156 | |||
157 | // Only fold comments of the same flavor | ||
158 | let group_kind = first.kind(); | ||
159 | if !group_kind.shape.is_line() { | ||
160 | return None; | ||
161 | } | ||
162 | |||
163 | let mut last = first.clone(); | ||
164 | for element in first.syntax().siblings_with_tokens(Direction::Next) { | ||
165 | match element { | ||
166 | NodeOrToken::Token(token) => { | ||
167 | if let Some(ws) = ast::Whitespace::cast(token.clone()) { | ||
168 | if !ws.spans_multiple_lines() { | ||
169 | // Ignore whitespace without blank lines | ||
170 | continue; | ||
171 | } | ||
172 | } | ||
173 | if let Some(c) = ast::Comment::cast(token) { | ||
174 | if c.kind() == group_kind { | ||
175 | visited.insert(c.clone()); | ||
176 | last = c; | ||
177 | continue; | ||
178 | } | ||
179 | } | ||
180 | // The comment group ends because either: | ||
181 | // * An element of a different kind was reached | ||
182 | // * A comment of a different flavor was reached | ||
183 | break; | ||
184 | } | ||
185 | NodeOrToken::Node(_) => break, | ||
186 | }; | ||
187 | } | ||
188 | |||
189 | if first != last { | ||
190 | Some(TextRange::from_to( | ||
191 | first.syntax().text_range().start(), | ||
192 | last.syntax().text_range().end(), | ||
193 | )) | ||
194 | } else { | ||
195 | // The group consists of only one element, therefore it cannot be folded | ||
196 | None | ||
197 | } | ||
198 | } | ||
199 | |||
200 | #[cfg(test)] | ||
201 | mod tests { | ||
202 | use super::*; | ||
203 | use test_utils::extract_ranges; | ||
204 | |||
205 | fn do_check(text: &str, fold_kinds: &[FoldKind]) { | ||
206 | let (ranges, text) = extract_ranges(text, "fold"); | ||
207 | let parse = SourceFile::parse(&text); | ||
208 | let folds = folding_ranges(&parse.tree()); | ||
209 | |||
210 | assert_eq!( | ||
211 | folds.len(), | ||
212 | ranges.len(), | ||
213 | "The amount of folds is different than the expected amount" | ||
214 | ); | ||
215 | assert_eq!( | ||
216 | folds.len(), | ||
217 | fold_kinds.len(), | ||
218 | "The amount of fold kinds is different than the expected amount" | ||
219 | ); | ||
220 | for ((fold, range), fold_kind) in | ||
221 | folds.iter().zip(ranges.into_iter()).zip(fold_kinds.iter()) | ||
222 | { | ||
223 | assert_eq!(fold.range.start(), range.start()); | ||
224 | assert_eq!(fold.range.end(), range.end()); | ||
225 | assert_eq!(&fold.kind, fold_kind); | ||
226 | } | ||
227 | } | ||
228 | |||
229 | #[test] | ||
230 | fn test_fold_comments() { | ||
231 | let text = r#" | ||
232 | <fold>// Hello | ||
233 | // this is a multiline | ||
234 | // comment | ||
235 | //</fold> | ||
236 | |||
237 | // But this is not | ||
238 | |||
239 | fn main() <fold>{ | ||
240 | <fold>// We should | ||
241 | // also | ||
242 | // fold | ||
243 | // this one.</fold> | ||
244 | <fold>//! But this one is different | ||
245 | //! because it has another flavor</fold> | ||
246 | <fold>/* As does this | ||
247 | multiline comment */</fold> | ||
248 | }</fold>"#; | ||
249 | |||
250 | let fold_kinds = &[ | ||
251 | FoldKind::Comment, | ||
252 | FoldKind::Block, | ||
253 | FoldKind::Comment, | ||
254 | FoldKind::Comment, | ||
255 | FoldKind::Comment, | ||
256 | ]; | ||
257 | do_check(text, fold_kinds); | ||
258 | } | ||
259 | |||
260 | #[test] | ||
261 | fn test_fold_imports() { | ||
262 | let text = r#" | ||
263 | <fold>use std::<fold>{ | ||
264 | str, | ||
265 | vec, | ||
266 | io as iop | ||
267 | }</fold>;</fold> | ||
268 | |||
269 | fn main() <fold>{ | ||
270 | }</fold>"#; | ||
271 | |||
272 | let folds = &[FoldKind::Imports, FoldKind::Block, FoldKind::Block]; | ||
273 | do_check(text, folds); | ||
274 | } | ||
275 | |||
276 | #[test] | ||
277 | fn test_fold_mods() { | ||
278 | let text = r#" | ||
279 | |||
280 | pub mod foo; | ||
281 | <fold>mod after_pub; | ||
282 | mod after_pub_next;</fold> | ||
283 | |||
284 | <fold>mod before_pub; | ||
285 | mod before_pub_next;</fold> | ||
286 | pub mod bar; | ||
287 | |||
288 | mod not_folding_single; | ||
289 | pub mod foobar; | ||
290 | pub not_folding_single_next; | ||
291 | |||
292 | <fold>#[cfg(test)] | ||
293 | mod with_attribute; | ||
294 | mod with_attribute_next;</fold> | ||
295 | |||
296 | fn main() <fold>{ | ||
297 | }</fold>"#; | ||
298 | |||
299 | let folds = &[FoldKind::Mods, FoldKind::Mods, FoldKind::Mods, FoldKind::Block]; | ||
300 | do_check(text, folds); | ||
301 | } | ||
302 | |||
303 | #[test] | ||
304 | fn test_fold_import_groups() { | ||
305 | let text = r#" | ||
306 | <fold>use std::str; | ||
307 | use std::vec; | ||
308 | use std::io as iop;</fold> | ||
309 | |||
310 | <fold>use std::mem; | ||
311 | use std::f64;</fold> | ||
312 | |||
313 | use std::collections::HashMap; | ||
314 | // Some random comment | ||
315 | use std::collections::VecDeque; | ||
316 | |||
317 | fn main() <fold>{ | ||
318 | }</fold>"#; | ||
319 | |||
320 | let folds = &[FoldKind::Imports, FoldKind::Imports, FoldKind::Block]; | ||
321 | do_check(text, folds); | ||
322 | } | ||
323 | |||
324 | #[test] | ||
325 | fn test_fold_import_and_groups() { | ||
326 | let text = r#" | ||
327 | <fold>use std::str; | ||
328 | use std::vec; | ||
329 | use std::io as iop;</fold> | ||
330 | |||
331 | <fold>use std::mem; | ||
332 | use std::f64;</fold> | ||
333 | |||
334 | <fold>use std::collections::<fold>{ | ||
335 | HashMap, | ||
336 | VecDeque, | ||
337 | }</fold>;</fold> | ||
338 | // Some random comment | ||
339 | |||
340 | fn main() <fold>{ | ||
341 | }</fold>"#; | ||
342 | |||
343 | let folds = &[ | ||
344 | FoldKind::Imports, | ||
345 | FoldKind::Imports, | ||
346 | FoldKind::Imports, | ||
347 | FoldKind::Block, | ||
348 | FoldKind::Block, | ||
349 | ]; | ||
350 | do_check(text, folds); | ||
351 | } | ||
352 | |||
353 | #[test] | ||
354 | fn test_folds_macros() { | ||
355 | let text = r#" | ||
356 | macro_rules! foo <fold>{ | ||
357 | ($($tt:tt)*) => { $($tt)* } | ||
358 | }</fold> | ||
359 | "#; | ||
360 | |||
361 | let folds = &[FoldKind::Block]; | ||
362 | do_check(text, folds); | ||
363 | } | ||
364 | |||
365 | #[test] | ||
366 | fn test_fold_match_arms() { | ||
367 | let text = r#" | ||
368 | fn main() <fold>{ | ||
369 | match 0 <fold>{ | ||
370 | 0 => 0, | ||
371 | _ => 1, | ||
372 | }</fold> | ||
373 | }</fold>"#; | ||
374 | |||
375 | let folds = &[FoldKind::Block, FoldKind::Block]; | ||
376 | do_check(text, folds); | ||
377 | } | ||
378 | } | ||
diff --git a/crates/ra_ide/src/goto_definition.rs b/crates/ra_ide/src/goto_definition.rs new file mode 100644 index 000000000..c10a6c844 --- /dev/null +++ b/crates/ra_ide/src/goto_definition.rs | |||
@@ -0,0 +1,696 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::{db::AstDatabase, Source}; | ||
4 | use ra_syntax::{ | ||
5 | ast::{self, DocCommentsOwner}, | ||
6 | match_ast, AstNode, SyntaxNode, | ||
7 | }; | ||
8 | |||
9 | use crate::{ | ||
10 | db::RootDatabase, | ||
11 | display::{ShortLabel, ToNav}, | ||
12 | expand::descend_into_macros, | ||
13 | references::{classify_name_ref, NameKind::*}, | ||
14 | FilePosition, NavigationTarget, RangeInfo, | ||
15 | }; | ||
16 | |||
17 | pub(crate) fn goto_definition( | ||
18 | db: &RootDatabase, | ||
19 | position: FilePosition, | ||
20 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
21 | let file = db.parse_or_expand(position.file_id.into())?; | ||
22 | let token = file.token_at_offset(position.offset).filter(|it| !it.kind().is_trivia()).next()?; | ||
23 | let token = descend_into_macros(db, position.file_id, token); | ||
24 | |||
25 | let res = match_ast! { | ||
26 | match (token.value.parent()) { | ||
27 | ast::NameRef(name_ref) => { | ||
28 | let navs = reference_definition(db, token.with_value(&name_ref)).to_vec(); | ||
29 | RangeInfo::new(name_ref.syntax().text_range(), navs.to_vec()) | ||
30 | }, | ||
31 | ast::Name(name) => { | ||
32 | let navs = name_definition(db, token.with_value(&name))?; | ||
33 | RangeInfo::new(name.syntax().text_range(), navs) | ||
34 | |||
35 | }, | ||
36 | _ => return None, | ||
37 | } | ||
38 | }; | ||
39 | |||
40 | Some(res) | ||
41 | } | ||
42 | |||
43 | #[derive(Debug)] | ||
44 | pub(crate) enum ReferenceResult { | ||
45 | Exact(NavigationTarget), | ||
46 | Approximate(Vec<NavigationTarget>), | ||
47 | } | ||
48 | |||
49 | impl ReferenceResult { | ||
50 | fn to_vec(self) -> Vec<NavigationTarget> { | ||
51 | use self::ReferenceResult::*; | ||
52 | match self { | ||
53 | Exact(target) => vec![target], | ||
54 | Approximate(vec) => vec, | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | pub(crate) fn reference_definition( | ||
60 | db: &RootDatabase, | ||
61 | name_ref: Source<&ast::NameRef>, | ||
62 | ) -> ReferenceResult { | ||
63 | use self::ReferenceResult::*; | ||
64 | |||
65 | let name_kind = classify_name_ref(db, name_ref).map(|d| d.kind); | ||
66 | match name_kind { | ||
67 | Some(Macro(mac)) => return Exact(mac.to_nav(db)), | ||
68 | Some(Field(field)) => return Exact(field.to_nav(db)), | ||
69 | Some(AssocItem(assoc)) => return Exact(assoc.to_nav(db)), | ||
70 | Some(Def(def)) => match NavigationTarget::from_def(db, def) { | ||
71 | Some(nav) => return Exact(nav), | ||
72 | None => return Approximate(vec![]), | ||
73 | }, | ||
74 | Some(SelfType(imp)) => { | ||
75 | // FIXME: ideally, this should point to the type in the impl, and | ||
76 | // not at the whole impl. And goto **type** definition should bring | ||
77 | // us to the actual type | ||
78 | return Exact(imp.to_nav(db)); | ||
79 | } | ||
80 | Some(Local(local)) => return Exact(local.to_nav(db)), | ||
81 | Some(GenericParam(_)) => { | ||
82 | // FIXME: go to the generic param def | ||
83 | } | ||
84 | None => {} | ||
85 | }; | ||
86 | |||
87 | // Fallback index based approach: | ||
88 | let navs = crate::symbol_index::index_resolve(db, name_ref.value) | ||
89 | .into_iter() | ||
90 | .map(|s| s.to_nav(db)) | ||
91 | .collect(); | ||
92 | Approximate(navs) | ||
93 | } | ||
94 | |||
95 | pub(crate) fn name_definition( | ||
96 | db: &RootDatabase, | ||
97 | name: Source<&ast::Name>, | ||
98 | ) -> Option<Vec<NavigationTarget>> { | ||
99 | let parent = name.value.syntax().parent()?; | ||
100 | |||
101 | if let Some(module) = ast::Module::cast(parent.clone()) { | ||
102 | if module.has_semi() { | ||
103 | let src = name.with_value(module); | ||
104 | if let Some(child_module) = hir::Module::from_declaration(db, src) { | ||
105 | let nav = child_module.to_nav(db); | ||
106 | return Some(vec![nav]); | ||
107 | } | ||
108 | } | ||
109 | } | ||
110 | |||
111 | if let Some(nav) = named_target(db, name.with_value(&parent)) { | ||
112 | return Some(vec![nav]); | ||
113 | } | ||
114 | |||
115 | None | ||
116 | } | ||
117 | |||
118 | fn named_target(db: &RootDatabase, node: Source<&SyntaxNode>) -> Option<NavigationTarget> { | ||
119 | match_ast! { | ||
120 | match (node.value) { | ||
121 | ast::StructDef(it) => { | ||
122 | Some(NavigationTarget::from_named( | ||
123 | db, | ||
124 | node.with_value(&it), | ||
125 | it.doc_comment_text(), | ||
126 | it.short_label(), | ||
127 | )) | ||
128 | }, | ||
129 | ast::EnumDef(it) => { | ||
130 | Some(NavigationTarget::from_named( | ||
131 | db, | ||
132 | node.with_value(&it), | ||
133 | it.doc_comment_text(), | ||
134 | it.short_label(), | ||
135 | )) | ||
136 | }, | ||
137 | ast::EnumVariant(it) => { | ||
138 | Some(NavigationTarget::from_named( | ||
139 | db, | ||
140 | node.with_value(&it), | ||
141 | it.doc_comment_text(), | ||
142 | it.short_label(), | ||
143 | )) | ||
144 | }, | ||
145 | ast::FnDef(it) => { | ||
146 | Some(NavigationTarget::from_named( | ||
147 | db, | ||
148 | node.with_value(&it), | ||
149 | it.doc_comment_text(), | ||
150 | it.short_label(), | ||
151 | )) | ||
152 | }, | ||
153 | ast::TypeAliasDef(it) => { | ||
154 | Some(NavigationTarget::from_named( | ||
155 | db, | ||
156 | node.with_value(&it), | ||
157 | it.doc_comment_text(), | ||
158 | it.short_label(), | ||
159 | )) | ||
160 | }, | ||
161 | ast::ConstDef(it) => { | ||
162 | Some(NavigationTarget::from_named( | ||
163 | db, | ||
164 | node.with_value(&it), | ||
165 | it.doc_comment_text(), | ||
166 | it.short_label(), | ||
167 | )) | ||
168 | }, | ||
169 | ast::StaticDef(it) => { | ||
170 | Some(NavigationTarget::from_named( | ||
171 | db, | ||
172 | node.with_value(&it), | ||
173 | it.doc_comment_text(), | ||
174 | it.short_label(), | ||
175 | )) | ||
176 | }, | ||
177 | ast::TraitDef(it) => { | ||
178 | Some(NavigationTarget::from_named( | ||
179 | db, | ||
180 | node.with_value(&it), | ||
181 | it.doc_comment_text(), | ||
182 | it.short_label(), | ||
183 | )) | ||
184 | }, | ||
185 | ast::RecordFieldDef(it) => { | ||
186 | Some(NavigationTarget::from_named( | ||
187 | db, | ||
188 | node.with_value(&it), | ||
189 | it.doc_comment_text(), | ||
190 | it.short_label(), | ||
191 | )) | ||
192 | }, | ||
193 | ast::Module(it) => { | ||
194 | Some(NavigationTarget::from_named( | ||
195 | db, | ||
196 | node.with_value(&it), | ||
197 | it.doc_comment_text(), | ||
198 | it.short_label(), | ||
199 | )) | ||
200 | }, | ||
201 | ast::MacroCall(it) => { | ||
202 | Some(NavigationTarget::from_named( | ||
203 | db, | ||
204 | node.with_value(&it), | ||
205 | it.doc_comment_text(), | ||
206 | None, | ||
207 | )) | ||
208 | }, | ||
209 | _ => None, | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | |||
214 | #[cfg(test)] | ||
215 | mod tests { | ||
216 | use test_utils::covers; | ||
217 | |||
218 | use crate::mock_analysis::analysis_and_position; | ||
219 | |||
220 | fn check_goto(fixture: &str, expected: &str) { | ||
221 | let (analysis, pos) = analysis_and_position(fixture); | ||
222 | |||
223 | let mut navs = analysis.goto_definition(pos).unwrap().unwrap().info; | ||
224 | assert_eq!(navs.len(), 1); | ||
225 | let nav = navs.pop().unwrap(); | ||
226 | nav.assert_match(expected); | ||
227 | } | ||
228 | |||
229 | #[test] | ||
230 | fn goto_definition_works_in_items() { | ||
231 | check_goto( | ||
232 | " | ||
233 | //- /lib.rs | ||
234 | struct Foo; | ||
235 | enum E { X(Foo<|>) } | ||
236 | ", | ||
237 | "Foo STRUCT_DEF FileId(1) [0; 11) [7; 10)", | ||
238 | ); | ||
239 | } | ||
240 | |||
241 | #[test] | ||
242 | fn goto_definition_resolves_correct_name() { | ||
243 | check_goto( | ||
244 | " | ||
245 | //- /lib.rs | ||
246 | use a::Foo; | ||
247 | mod a; | ||
248 | mod b; | ||
249 | enum E { X(Foo<|>) } | ||
250 | //- /a.rs | ||
251 | struct Foo; | ||
252 | //- /b.rs | ||
253 | struct Foo; | ||
254 | ", | ||
255 | "Foo STRUCT_DEF FileId(2) [0; 11) [7; 10)", | ||
256 | ); | ||
257 | } | ||
258 | |||
259 | #[test] | ||
260 | fn goto_definition_works_for_module_declaration() { | ||
261 | check_goto( | ||
262 | " | ||
263 | //- /lib.rs | ||
264 | mod <|>foo; | ||
265 | //- /foo.rs | ||
266 | // empty | ||
267 | ", | ||
268 | "foo SOURCE_FILE FileId(2) [0; 10)", | ||
269 | ); | ||
270 | |||
271 | check_goto( | ||
272 | " | ||
273 | //- /lib.rs | ||
274 | mod <|>foo; | ||
275 | //- /foo/mod.rs | ||
276 | // empty | ||
277 | ", | ||
278 | "foo SOURCE_FILE FileId(2) [0; 10)", | ||
279 | ); | ||
280 | } | ||
281 | |||
282 | #[test] | ||
283 | fn goto_definition_works_for_macros() { | ||
284 | covers!(goto_definition_works_for_macros); | ||
285 | check_goto( | ||
286 | " | ||
287 | //- /lib.rs | ||
288 | macro_rules! foo { | ||
289 | () => { | ||
290 | {} | ||
291 | }; | ||
292 | } | ||
293 | |||
294 | fn bar() { | ||
295 | <|>foo!(); | ||
296 | } | ||
297 | ", | ||
298 | "foo MACRO_CALL FileId(1) [0; 50) [13; 16)", | ||
299 | ); | ||
300 | } | ||
301 | |||
302 | #[test] | ||
303 | fn goto_definition_works_for_macros_from_other_crates() { | ||
304 | covers!(goto_definition_works_for_macros); | ||
305 | check_goto( | ||
306 | " | ||
307 | //- /lib.rs | ||
308 | use foo::foo; | ||
309 | fn bar() { | ||
310 | <|>foo!(); | ||
311 | } | ||
312 | |||
313 | //- /foo/lib.rs | ||
314 | #[macro_export] | ||
315 | macro_rules! foo { | ||
316 | () => { | ||
317 | {} | ||
318 | }; | ||
319 | } | ||
320 | ", | ||
321 | "foo MACRO_CALL FileId(2) [0; 66) [29; 32)", | ||
322 | ); | ||
323 | } | ||
324 | |||
325 | #[test] | ||
326 | fn goto_definition_works_for_macros_in_use_tree() { | ||
327 | check_goto( | ||
328 | " | ||
329 | //- /lib.rs | ||
330 | use foo::foo<|>; | ||
331 | |||
332 | //- /foo/lib.rs | ||
333 | #[macro_export] | ||
334 | macro_rules! foo { | ||
335 | () => { | ||
336 | {} | ||
337 | }; | ||
338 | } | ||
339 | ", | ||
340 | "foo MACRO_CALL FileId(2) [0; 66) [29; 32)", | ||
341 | ); | ||
342 | } | ||
343 | |||
344 | #[test] | ||
345 | fn goto_definition_works_for_macro_defined_fn_with_arg() { | ||
346 | check_goto( | ||
347 | " | ||
348 | //- /lib.rs | ||
349 | macro_rules! define_fn { | ||
350 | ($name:ident) => (fn $name() {}) | ||
351 | } | ||
352 | |||
353 | define_fn!( | ||
354 | foo | ||
355 | ) | ||
356 | |||
357 | fn bar() { | ||
358 | <|>foo(); | ||
359 | } | ||
360 | ", | ||
361 | "foo FN_DEF FileId(1) [80; 83) [80; 83)", | ||
362 | ); | ||
363 | } | ||
364 | |||
365 | #[test] | ||
366 | fn goto_definition_works_for_macro_defined_fn_no_arg() { | ||
367 | check_goto( | ||
368 | " | ||
369 | //- /lib.rs | ||
370 | macro_rules! define_fn { | ||
371 | () => (fn foo() {}) | ||
372 | } | ||
373 | |||
374 | define_fn!(); | ||
375 | |||
376 | fn bar() { | ||
377 | <|>foo(); | ||
378 | } | ||
379 | ", | ||
380 | "foo FN_DEF FileId(1) [39; 42) [39; 42)", | ||
381 | ); | ||
382 | } | ||
383 | |||
384 | #[test] | ||
385 | fn goto_definition_works_for_methods() { | ||
386 | covers!(goto_definition_works_for_methods); | ||
387 | check_goto( | ||
388 | " | ||
389 | //- /lib.rs | ||
390 | struct Foo; | ||
391 | impl Foo { | ||
392 | fn frobnicate(&self) { } | ||
393 | } | ||
394 | |||
395 | fn bar(foo: &Foo) { | ||
396 | foo.frobnicate<|>(); | ||
397 | } | ||
398 | ", | ||
399 | "frobnicate FN_DEF FileId(1) [27; 52) [30; 40)", | ||
400 | ); | ||
401 | } | ||
402 | |||
403 | #[test] | ||
404 | fn goto_definition_works_for_fields() { | ||
405 | covers!(goto_definition_works_for_fields); | ||
406 | check_goto( | ||
407 | " | ||
408 | //- /lib.rs | ||
409 | struct Foo { | ||
410 | spam: u32, | ||
411 | } | ||
412 | |||
413 | fn bar(foo: &Foo) { | ||
414 | foo.spam<|>; | ||
415 | } | ||
416 | ", | ||
417 | "spam RECORD_FIELD_DEF FileId(1) [17; 26) [17; 21)", | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn goto_definition_works_for_record_fields() { | ||
423 | covers!(goto_definition_works_for_record_fields); | ||
424 | check_goto( | ||
425 | " | ||
426 | //- /lib.rs | ||
427 | struct Foo { | ||
428 | spam: u32, | ||
429 | } | ||
430 | |||
431 | fn bar() -> Foo { | ||
432 | Foo { | ||
433 | spam<|>: 0, | ||
434 | } | ||
435 | } | ||
436 | ", | ||
437 | "spam RECORD_FIELD_DEF FileId(1) [17; 26) [17; 21)", | ||
438 | ); | ||
439 | } | ||
440 | |||
441 | #[test] | ||
442 | fn goto_definition_works_for_ufcs_inherent_methods() { | ||
443 | check_goto( | ||
444 | " | ||
445 | //- /lib.rs | ||
446 | struct Foo; | ||
447 | impl Foo { | ||
448 | fn frobnicate() { } | ||
449 | } | ||
450 | |||
451 | fn bar(foo: &Foo) { | ||
452 | Foo::frobnicate<|>(); | ||
453 | } | ||
454 | ", | ||
455 | "frobnicate FN_DEF FileId(1) [27; 47) [30; 40)", | ||
456 | ); | ||
457 | } | ||
458 | |||
459 | #[test] | ||
460 | fn goto_definition_works_for_ufcs_trait_methods_through_traits() { | ||
461 | check_goto( | ||
462 | " | ||
463 | //- /lib.rs | ||
464 | trait Foo { | ||
465 | fn frobnicate(); | ||
466 | } | ||
467 | |||
468 | fn bar() { | ||
469 | Foo::frobnicate<|>(); | ||
470 | } | ||
471 | ", | ||
472 | "frobnicate FN_DEF FileId(1) [16; 32) [19; 29)", | ||
473 | ); | ||
474 | } | ||
475 | |||
476 | #[test] | ||
477 | fn goto_definition_works_for_ufcs_trait_methods_through_self() { | ||
478 | check_goto( | ||
479 | " | ||
480 | //- /lib.rs | ||
481 | struct Foo; | ||
482 | trait Trait { | ||
483 | fn frobnicate(); | ||
484 | } | ||
485 | impl Trait for Foo {} | ||
486 | |||
487 | fn bar() { | ||
488 | Foo::frobnicate<|>(); | ||
489 | } | ||
490 | ", | ||
491 | "frobnicate FN_DEF FileId(1) [30; 46) [33; 43)", | ||
492 | ); | ||
493 | } | ||
494 | |||
495 | #[test] | ||
496 | fn goto_definition_on_self() { | ||
497 | check_goto( | ||
498 | " | ||
499 | //- /lib.rs | ||
500 | struct Foo; | ||
501 | impl Foo { | ||
502 | pub fn new() -> Self { | ||
503 | Self<|> {} | ||
504 | } | ||
505 | } | ||
506 | ", | ||
507 | "impl IMPL_BLOCK FileId(1) [12; 73)", | ||
508 | ); | ||
509 | |||
510 | check_goto( | ||
511 | " | ||
512 | //- /lib.rs | ||
513 | struct Foo; | ||
514 | impl Foo { | ||
515 | pub fn new() -> Self<|> { | ||
516 | Self {} | ||
517 | } | ||
518 | } | ||
519 | ", | ||
520 | "impl IMPL_BLOCK FileId(1) [12; 73)", | ||
521 | ); | ||
522 | |||
523 | check_goto( | ||
524 | " | ||
525 | //- /lib.rs | ||
526 | enum Foo { A } | ||
527 | impl Foo { | ||
528 | pub fn new() -> Self<|> { | ||
529 | Foo::A | ||
530 | } | ||
531 | } | ||
532 | ", | ||
533 | "impl IMPL_BLOCK FileId(1) [15; 75)", | ||
534 | ); | ||
535 | |||
536 | check_goto( | ||
537 | " | ||
538 | //- /lib.rs | ||
539 | enum Foo { A } | ||
540 | impl Foo { | ||
541 | pub fn thing(a: &Self<|>) { | ||
542 | } | ||
543 | } | ||
544 | ", | ||
545 | "impl IMPL_BLOCK FileId(1) [15; 62)", | ||
546 | ); | ||
547 | } | ||
548 | |||
549 | #[test] | ||
550 | fn goto_definition_on_self_in_trait_impl() { | ||
551 | check_goto( | ||
552 | " | ||
553 | //- /lib.rs | ||
554 | struct Foo; | ||
555 | trait Make { | ||
556 | fn new() -> Self; | ||
557 | } | ||
558 | impl Make for Foo { | ||
559 | fn new() -> Self { | ||
560 | Self<|> {} | ||
561 | } | ||
562 | } | ||
563 | ", | ||
564 | "impl IMPL_BLOCK FileId(1) [49; 115)", | ||
565 | ); | ||
566 | |||
567 | check_goto( | ||
568 | " | ||
569 | //- /lib.rs | ||
570 | struct Foo; | ||
571 | trait Make { | ||
572 | fn new() -> Self; | ||
573 | } | ||
574 | impl Make for Foo { | ||
575 | fn new() -> Self<|> { | ||
576 | Self {} | ||
577 | } | ||
578 | } | ||
579 | ", | ||
580 | "impl IMPL_BLOCK FileId(1) [49; 115)", | ||
581 | ); | ||
582 | } | ||
583 | |||
584 | #[test] | ||
585 | fn goto_definition_works_when_used_on_definition_name_itself() { | ||
586 | check_goto( | ||
587 | " | ||
588 | //- /lib.rs | ||
589 | struct Foo<|> { value: u32 } | ||
590 | ", | ||
591 | "Foo STRUCT_DEF FileId(1) [0; 25) [7; 10)", | ||
592 | ); | ||
593 | |||
594 | check_goto( | ||
595 | r#" | ||
596 | //- /lib.rs | ||
597 | struct Foo { | ||
598 | field<|>: string, | ||
599 | } | ||
600 | "#, | ||
601 | "field RECORD_FIELD_DEF FileId(1) [17; 30) [17; 22)", | ||
602 | ); | ||
603 | |||
604 | check_goto( | ||
605 | " | ||
606 | //- /lib.rs | ||
607 | fn foo_test<|>() { | ||
608 | } | ||
609 | ", | ||
610 | "foo_test FN_DEF FileId(1) [0; 17) [3; 11)", | ||
611 | ); | ||
612 | |||
613 | check_goto( | ||
614 | " | ||
615 | //- /lib.rs | ||
616 | enum Foo<|> { | ||
617 | Variant, | ||
618 | } | ||
619 | ", | ||
620 | "Foo ENUM_DEF FileId(1) [0; 25) [5; 8)", | ||
621 | ); | ||
622 | |||
623 | check_goto( | ||
624 | " | ||
625 | //- /lib.rs | ||
626 | enum Foo { | ||
627 | Variant1, | ||
628 | Variant2<|>, | ||
629 | Variant3, | ||
630 | } | ||
631 | ", | ||
632 | "Variant2 ENUM_VARIANT FileId(1) [29; 37) [29; 37)", | ||
633 | ); | ||
634 | |||
635 | check_goto( | ||
636 | r#" | ||
637 | //- /lib.rs | ||
638 | static inner<|>: &str = ""; | ||
639 | "#, | ||
640 | "inner STATIC_DEF FileId(1) [0; 24) [7; 12)", | ||
641 | ); | ||
642 | |||
643 | check_goto( | ||
644 | r#" | ||
645 | //- /lib.rs | ||
646 | const inner<|>: &str = ""; | ||
647 | "#, | ||
648 | "inner CONST_DEF FileId(1) [0; 23) [6; 11)", | ||
649 | ); | ||
650 | |||
651 | check_goto( | ||
652 | r#" | ||
653 | //- /lib.rs | ||
654 | type Thing<|> = Option<()>; | ||
655 | "#, | ||
656 | "Thing TYPE_ALIAS_DEF FileId(1) [0; 24) [5; 10)", | ||
657 | ); | ||
658 | |||
659 | check_goto( | ||
660 | r#" | ||
661 | //- /lib.rs | ||
662 | trait Foo<|> { | ||
663 | } | ||
664 | "#, | ||
665 | "Foo TRAIT_DEF FileId(1) [0; 13) [6; 9)", | ||
666 | ); | ||
667 | |||
668 | check_goto( | ||
669 | r#" | ||
670 | //- /lib.rs | ||
671 | mod bar<|> { | ||
672 | } | ||
673 | "#, | ||
674 | "bar MODULE FileId(1) [0; 11) [4; 7)", | ||
675 | ); | ||
676 | } | ||
677 | |||
678 | #[test] | ||
679 | fn goto_from_macro() { | ||
680 | check_goto( | ||
681 | " | ||
682 | //- /lib.rs | ||
683 | macro_rules! id { | ||
684 | ($($tt:tt)*) => { $($tt)* } | ||
685 | } | ||
686 | fn foo() {} | ||
687 | id! { | ||
688 | fn bar() { | ||
689 | fo<|>o(); | ||
690 | } | ||
691 | } | ||
692 | ", | ||
693 | "foo FN_DEF FileId(1) [52; 63) [55; 58)", | ||
694 | ); | ||
695 | } | ||
696 | } | ||
diff --git a/crates/ra_ide/src/goto_type_definition.rs b/crates/ra_ide/src/goto_type_definition.rs new file mode 100644 index 000000000..992a08809 --- /dev/null +++ b/crates/ra_ide/src/goto_type_definition.rs | |||
@@ -0,0 +1,105 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::db::AstDatabase; | ||
4 | use ra_syntax::{ast, AstNode}; | ||
5 | |||
6 | use crate::{ | ||
7 | db::RootDatabase, display::ToNav, expand::descend_into_macros, FilePosition, NavigationTarget, | ||
8 | RangeInfo, | ||
9 | }; | ||
10 | |||
11 | pub(crate) fn goto_type_definition( | ||
12 | db: &RootDatabase, | ||
13 | position: FilePosition, | ||
14 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
15 | let file = db.parse_or_expand(position.file_id.into())?; | ||
16 | let token = file.token_at_offset(position.offset).filter(|it| !it.kind().is_trivia()).next()?; | ||
17 | let token = descend_into_macros(db, position.file_id, token); | ||
18 | |||
19 | let node = token.value.ancestors().find_map(|token| { | ||
20 | token | ||
21 | .ancestors() | ||
22 | .find(|n| ast::Expr::cast(n.clone()).is_some() || ast::Pat::cast(n.clone()).is_some()) | ||
23 | })?; | ||
24 | |||
25 | let analyzer = hir::SourceAnalyzer::new(db, token.with_value(&node), None); | ||
26 | |||
27 | let ty: hir::Type = if let Some(ty) = | ||
28 | ast::Expr::cast(node.clone()).and_then(|e| analyzer.type_of(db, &e)) | ||
29 | { | ||
30 | ty | ||
31 | } else if let Some(ty) = ast::Pat::cast(node.clone()).and_then(|p| analyzer.type_of_pat(db, &p)) | ||
32 | { | ||
33 | ty | ||
34 | } else { | ||
35 | return None; | ||
36 | }; | ||
37 | |||
38 | let adt_def = ty.autoderef(db).find_map(|ty| ty.as_adt())?; | ||
39 | |||
40 | let nav = adt_def.to_nav(db); | ||
41 | Some(RangeInfo::new(node.text_range(), vec![nav])) | ||
42 | } | ||
43 | |||
44 | #[cfg(test)] | ||
45 | mod tests { | ||
46 | use crate::mock_analysis::analysis_and_position; | ||
47 | |||
48 | fn check_goto(fixture: &str, expected: &str) { | ||
49 | let (analysis, pos) = analysis_and_position(fixture); | ||
50 | |||
51 | let mut navs = analysis.goto_type_definition(pos).unwrap().unwrap().info; | ||
52 | assert_eq!(navs.len(), 1); | ||
53 | let nav = navs.pop().unwrap(); | ||
54 | nav.assert_match(expected); | ||
55 | } | ||
56 | |||
57 | #[test] | ||
58 | fn goto_type_definition_works_simple() { | ||
59 | check_goto( | ||
60 | " | ||
61 | //- /lib.rs | ||
62 | struct Foo; | ||
63 | fn foo() { | ||
64 | let f: Foo; | ||
65 | f<|> | ||
66 | } | ||
67 | ", | ||
68 | "Foo STRUCT_DEF FileId(1) [0; 11) [7; 10)", | ||
69 | ); | ||
70 | } | ||
71 | |||
72 | #[test] | ||
73 | fn goto_type_definition_works_simple_ref() { | ||
74 | check_goto( | ||
75 | " | ||
76 | //- /lib.rs | ||
77 | struct Foo; | ||
78 | fn foo() { | ||
79 | let f: &Foo; | ||
80 | f<|> | ||
81 | } | ||
82 | ", | ||
83 | "Foo STRUCT_DEF FileId(1) [0; 11) [7; 10)", | ||
84 | ); | ||
85 | } | ||
86 | |||
87 | #[test] | ||
88 | fn goto_type_definition_works_through_macro() { | ||
89 | check_goto( | ||
90 | " | ||
91 | //- /lib.rs | ||
92 | macro_rules! id { | ||
93 | ($($tt:tt)*) => { $($tt)* } | ||
94 | } | ||
95 | struct Foo {} | ||
96 | id! { | ||
97 | fn bar() { | ||
98 | let f<|> = Foo {}; | ||
99 | } | ||
100 | } | ||
101 | ", | ||
102 | "Foo STRUCT_DEF FileId(1) [52; 65) [59; 62)", | ||
103 | ); | ||
104 | } | ||
105 | } | ||
diff --git a/crates/ra_ide/src/hover.rs b/crates/ra_ide/src/hover.rs new file mode 100644 index 000000000..260a7b869 --- /dev/null +++ b/crates/ra_ide/src/hover.rs | |||
@@ -0,0 +1,730 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::{db::AstDatabase, Adt, HasSource, HirDisplay}; | ||
4 | use ra_db::SourceDatabase; | ||
5 | use ra_syntax::{ | ||
6 | algo::find_covering_element, | ||
7 | ast::{self, DocCommentsOwner}, | ||
8 | match_ast, AstNode, | ||
9 | }; | ||
10 | |||
11 | use crate::{ | ||
12 | db::RootDatabase, | ||
13 | display::{ | ||
14 | description_from_symbol, docs_from_symbol, macro_label, rust_code_markup, | ||
15 | rust_code_markup_with_doc, ShortLabel, | ||
16 | }, | ||
17 | expand::descend_into_macros, | ||
18 | references::{classify_name, classify_name_ref, NameKind, NameKind::*}, | ||
19 | FilePosition, FileRange, RangeInfo, | ||
20 | }; | ||
21 | |||
22 | /// Contains the results when hovering over an item | ||
23 | #[derive(Debug, Clone)] | ||
24 | pub struct HoverResult { | ||
25 | results: Vec<String>, | ||
26 | exact: bool, | ||
27 | } | ||
28 | |||
29 | impl Default for HoverResult { | ||
30 | fn default() -> Self { | ||
31 | HoverResult::new() | ||
32 | } | ||
33 | } | ||
34 | |||
35 | impl HoverResult { | ||
36 | pub fn new() -> HoverResult { | ||
37 | HoverResult { | ||
38 | results: Vec::new(), | ||
39 | // We assume exact by default | ||
40 | exact: true, | ||
41 | } | ||
42 | } | ||
43 | |||
44 | pub fn extend(&mut self, item: Option<String>) { | ||
45 | self.results.extend(item); | ||
46 | } | ||
47 | |||
48 | pub fn is_exact(&self) -> bool { | ||
49 | self.exact | ||
50 | } | ||
51 | |||
52 | pub fn is_empty(&self) -> bool { | ||
53 | self.results.is_empty() | ||
54 | } | ||
55 | |||
56 | pub fn len(&self) -> usize { | ||
57 | self.results.len() | ||
58 | } | ||
59 | |||
60 | pub fn first(&self) -> Option<&str> { | ||
61 | self.results.first().map(String::as_str) | ||
62 | } | ||
63 | |||
64 | pub fn results(&self) -> &[String] { | ||
65 | &self.results | ||
66 | } | ||
67 | |||
68 | /// Returns the results converted into markup | ||
69 | /// for displaying in a UI | ||
70 | pub fn to_markup(&self) -> String { | ||
71 | let mut markup = if !self.exact { | ||
72 | let mut msg = String::from("Failed to exactly resolve the symbol. This is probably because rust_analyzer does not yet support traits."); | ||
73 | if !self.results.is_empty() { | ||
74 | msg.push_str(" \nThese items were found instead:"); | ||
75 | } | ||
76 | msg.push_str("\n\n---\n"); | ||
77 | msg | ||
78 | } else { | ||
79 | String::new() | ||
80 | }; | ||
81 | |||
82 | markup.push_str(&self.results.join("\n\n---\n")); | ||
83 | |||
84 | markup | ||
85 | } | ||
86 | } | ||
87 | |||
88 | fn hover_text(docs: Option<String>, desc: Option<String>) -> Option<String> { | ||
89 | match (desc, docs) { | ||
90 | (Some(desc), docs) => Some(rust_code_markup_with_doc(desc, docs)), | ||
91 | (None, Some(docs)) => Some(docs), | ||
92 | _ => None, | ||
93 | } | ||
94 | } | ||
95 | |||
96 | fn hover_text_from_name_kind( | ||
97 | db: &RootDatabase, | ||
98 | name_kind: NameKind, | ||
99 | no_fallback: &mut bool, | ||
100 | ) -> Option<String> { | ||
101 | return match name_kind { | ||
102 | Macro(it) => { | ||
103 | let src = it.source(db); | ||
104 | hover_text(src.value.doc_comment_text(), Some(macro_label(&src.value))) | ||
105 | } | ||
106 | Field(it) => { | ||
107 | let src = it.source(db); | ||
108 | match src.value { | ||
109 | hir::FieldSource::Named(it) => hover_text(it.doc_comment_text(), it.short_label()), | ||
110 | _ => None, | ||
111 | } | ||
112 | } | ||
113 | AssocItem(it) => match it { | ||
114 | hir::AssocItem::Function(it) => from_def_source(db, it), | ||
115 | hir::AssocItem::Const(it) => from_def_source(db, it), | ||
116 | hir::AssocItem::TypeAlias(it) => from_def_source(db, it), | ||
117 | }, | ||
118 | Def(it) => match it { | ||
119 | hir::ModuleDef::Module(it) => match it.definition_source(db).value { | ||
120 | hir::ModuleSource::Module(it) => { | ||
121 | hover_text(it.doc_comment_text(), it.short_label()) | ||
122 | } | ||
123 | _ => None, | ||
124 | }, | ||
125 | hir::ModuleDef::Function(it) => from_def_source(db, it), | ||
126 | hir::ModuleDef::Adt(Adt::Struct(it)) => from_def_source(db, it), | ||
127 | hir::ModuleDef::Adt(Adt::Union(it)) => from_def_source(db, it), | ||
128 | hir::ModuleDef::Adt(Adt::Enum(it)) => from_def_source(db, it), | ||
129 | hir::ModuleDef::EnumVariant(it) => from_def_source(db, it), | ||
130 | hir::ModuleDef::Const(it) => from_def_source(db, it), | ||
131 | hir::ModuleDef::Static(it) => from_def_source(db, it), | ||
132 | hir::ModuleDef::Trait(it) => from_def_source(db, it), | ||
133 | hir::ModuleDef::TypeAlias(it) => from_def_source(db, it), | ||
134 | hir::ModuleDef::BuiltinType(it) => Some(it.to_string()), | ||
135 | }, | ||
136 | Local(_) => { | ||
137 | // Hover for these shows type names | ||
138 | *no_fallback = true; | ||
139 | None | ||
140 | } | ||
141 | GenericParam(_) | SelfType(_) => { | ||
142 | // FIXME: Hover for generic param | ||
143 | None | ||
144 | } | ||
145 | }; | ||
146 | |||
147 | fn from_def_source<A, D>(db: &RootDatabase, def: D) -> Option<String> | ||
148 | where | ||
149 | D: HasSource<Ast = A>, | ||
150 | A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel, | ||
151 | { | ||
152 | let src = def.source(db); | ||
153 | hover_text(src.value.doc_comment_text(), src.value.short_label()) | ||
154 | } | ||
155 | } | ||
156 | |||
157 | pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeInfo<HoverResult>> { | ||
158 | let file = db.parse_or_expand(position.file_id.into())?; | ||
159 | let token = file.token_at_offset(position.offset).filter(|it| !it.kind().is_trivia()).next()?; | ||
160 | let token = descend_into_macros(db, position.file_id, token); | ||
161 | |||
162 | let mut res = HoverResult::new(); | ||
163 | |||
164 | let mut range = match_ast! { | ||
165 | match (token.value.parent()) { | ||
166 | ast::NameRef(name_ref) => { | ||
167 | let mut no_fallback = false; | ||
168 | if let Some(name_kind) = | ||
169 | classify_name_ref(db, token.with_value(&name_ref)).map(|d| d.kind) | ||
170 | { | ||
171 | res.extend(hover_text_from_name_kind(db, name_kind, &mut no_fallback)) | ||
172 | } | ||
173 | |||
174 | if res.is_empty() && !no_fallback { | ||
175 | // Fallback index based approach: | ||
176 | let symbols = crate::symbol_index::index_resolve(db, &name_ref); | ||
177 | for sym in symbols { | ||
178 | let docs = docs_from_symbol(db, &sym); | ||
179 | let desc = description_from_symbol(db, &sym); | ||
180 | res.extend(hover_text(docs, desc)); | ||
181 | } | ||
182 | } | ||
183 | |||
184 | if !res.is_empty() { | ||
185 | Some(name_ref.syntax().text_range()) | ||
186 | } else { | ||
187 | None | ||
188 | } | ||
189 | }, | ||
190 | ast::Name(name) => { | ||
191 | if let Some(name_kind) = classify_name(db, token.with_value(&name)).map(|d| d.kind) { | ||
192 | res.extend(hover_text_from_name_kind(db, name_kind, &mut true)); | ||
193 | } | ||
194 | |||
195 | if !res.is_empty() { | ||
196 | Some(name.syntax().text_range()) | ||
197 | } else { | ||
198 | None | ||
199 | } | ||
200 | }, | ||
201 | _ => None, | ||
202 | } | ||
203 | }; | ||
204 | |||
205 | if range.is_none() { | ||
206 | let node = token.value.ancestors().find(|n| { | ||
207 | ast::Expr::cast(n.clone()).is_some() || ast::Pat::cast(n.clone()).is_some() | ||
208 | })?; | ||
209 | let frange = FileRange { file_id: position.file_id, range: node.text_range() }; | ||
210 | res.extend(type_of(db, frange).map(rust_code_markup)); | ||
211 | range = Some(node.text_range()); | ||
212 | }; | ||
213 | |||
214 | let range = range?; | ||
215 | if res.is_empty() { | ||
216 | return None; | ||
217 | } | ||
218 | Some(RangeInfo::new(range, res)) | ||
219 | } | ||
220 | |||
221 | pub(crate) fn type_of(db: &RootDatabase, frange: FileRange) -> Option<String> { | ||
222 | let parse = db.parse(frange.file_id); | ||
223 | let leaf_node = find_covering_element(parse.tree().syntax(), frange.range); | ||
224 | // if we picked identifier, expand to pattern/expression | ||
225 | let node = leaf_node | ||
226 | .ancestors() | ||
227 | .take_while(|it| it.text_range() == leaf_node.text_range()) | ||
228 | .find(|it| ast::Expr::cast(it.clone()).is_some() || ast::Pat::cast(it.clone()).is_some())?; | ||
229 | let analyzer = | ||
230 | hir::SourceAnalyzer::new(db, hir::Source::new(frange.file_id.into(), &node), None); | ||
231 | let ty = if let Some(ty) = ast::Expr::cast(node.clone()).and_then(|e| analyzer.type_of(db, &e)) | ||
232 | { | ||
233 | ty | ||
234 | } else if let Some(ty) = ast::Pat::cast(node).and_then(|p| analyzer.type_of_pat(db, &p)) { | ||
235 | ty | ||
236 | } else { | ||
237 | return None; | ||
238 | }; | ||
239 | Some(ty.display(db).to_string()) | ||
240 | } | ||
241 | |||
242 | #[cfg(test)] | ||
243 | mod tests { | ||
244 | use crate::mock_analysis::{ | ||
245 | analysis_and_position, single_file_with_position, single_file_with_range, | ||
246 | }; | ||
247 | use ra_syntax::TextRange; | ||
248 | |||
249 | fn trim_markup(s: &str) -> &str { | ||
250 | s.trim_start_matches("```rust\n").trim_end_matches("\n```") | ||
251 | } | ||
252 | |||
253 | fn trim_markup_opt(s: Option<&str>) -> Option<&str> { | ||
254 | s.map(trim_markup) | ||
255 | } | ||
256 | |||
257 | fn check_hover_result(fixture: &str, expected: &[&str]) { | ||
258 | let (analysis, position) = analysis_and_position(fixture); | ||
259 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
260 | let mut results = Vec::from(hover.info.results()); | ||
261 | results.sort(); | ||
262 | |||
263 | for (markup, expected) in | ||
264 | results.iter().zip(expected.iter().chain(std::iter::repeat(&"<missing>"))) | ||
265 | { | ||
266 | assert_eq!(trim_markup(&markup), *expected); | ||
267 | } | ||
268 | |||
269 | assert_eq!(hover.info.len(), expected.len()); | ||
270 | } | ||
271 | |||
272 | #[test] | ||
273 | fn hover_shows_type_of_an_expression() { | ||
274 | let (analysis, position) = single_file_with_position( | ||
275 | " | ||
276 | pub fn foo() -> u32 { 1 } | ||
277 | |||
278 | fn main() { | ||
279 | let foo_test = foo()<|>; | ||
280 | } | ||
281 | ", | ||
282 | ); | ||
283 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
284 | assert_eq!(hover.range, TextRange::from_to(95.into(), 100.into())); | ||
285 | assert_eq!(trim_markup_opt(hover.info.first()), Some("u32")); | ||
286 | } | ||
287 | |||
288 | #[test] | ||
289 | fn hover_shows_fn_signature() { | ||
290 | // Single file with result | ||
291 | check_hover_result( | ||
292 | r#" | ||
293 | //- /main.rs | ||
294 | pub fn foo() -> u32 { 1 } | ||
295 | |||
296 | fn main() { | ||
297 | let foo_test = fo<|>o(); | ||
298 | } | ||
299 | "#, | ||
300 | &["pub fn foo() -> u32"], | ||
301 | ); | ||
302 | |||
303 | // Multiple results | ||
304 | check_hover_result( | ||
305 | r#" | ||
306 | //- /a.rs | ||
307 | pub fn foo() -> u32 { 1 } | ||
308 | |||
309 | //- /b.rs | ||
310 | pub fn foo() -> &str { "" } | ||
311 | |||
312 | //- /c.rs | ||
313 | pub fn foo(a: u32, b: u32) {} | ||
314 | |||
315 | //- /main.rs | ||
316 | mod a; | ||
317 | mod b; | ||
318 | mod c; | ||
319 | |||
320 | fn main() { | ||
321 | let foo_test = fo<|>o(); | ||
322 | } | ||
323 | "#, | ||
324 | &["pub fn foo() -> &str", "pub fn foo() -> u32", "pub fn foo(a: u32, b: u32)"], | ||
325 | ); | ||
326 | } | ||
327 | |||
328 | #[test] | ||
329 | fn hover_shows_fn_signature_with_type_params() { | ||
330 | check_hover_result( | ||
331 | r#" | ||
332 | //- /main.rs | ||
333 | pub fn foo<'a, T: AsRef<str>>(b: &'a T) -> &'a str { } | ||
334 | |||
335 | fn main() { | ||
336 | let foo_test = fo<|>o(); | ||
337 | } | ||
338 | "#, | ||
339 | &["pub fn foo<'a, T: AsRef<str>>(b: &'a T) -> &'a str"], | ||
340 | ); | ||
341 | } | ||
342 | |||
343 | #[test] | ||
344 | fn hover_shows_fn_signature_on_fn_name() { | ||
345 | check_hover_result( | ||
346 | r#" | ||
347 | //- /main.rs | ||
348 | pub fn foo<|>(a: u32, b: u32) -> u32 {} | ||
349 | |||
350 | fn main() { | ||
351 | } | ||
352 | "#, | ||
353 | &["pub fn foo(a: u32, b: u32) -> u32"], | ||
354 | ); | ||
355 | } | ||
356 | |||
357 | #[test] | ||
358 | fn hover_shows_struct_field_info() { | ||
359 | // Hovering over the field when instantiating | ||
360 | check_hover_result( | ||
361 | r#" | ||
362 | //- /main.rs | ||
363 | struct Foo { | ||
364 | field_a: u32, | ||
365 | } | ||
366 | |||
367 | fn main() { | ||
368 | let foo = Foo { | ||
369 | field_a<|>: 0, | ||
370 | }; | ||
371 | } | ||
372 | "#, | ||
373 | &["field_a: u32"], | ||
374 | ); | ||
375 | |||
376 | // Hovering over the field in the definition | ||
377 | check_hover_result( | ||
378 | r#" | ||
379 | //- /main.rs | ||
380 | struct Foo { | ||
381 | field_a<|>: u32, | ||
382 | } | ||
383 | |||
384 | fn main() { | ||
385 | let foo = Foo { | ||
386 | field_a: 0, | ||
387 | }; | ||
388 | } | ||
389 | "#, | ||
390 | &["field_a: u32"], | ||
391 | ); | ||
392 | } | ||
393 | |||
394 | #[test] | ||
395 | fn hover_const_static() { | ||
396 | check_hover_result( | ||
397 | r#" | ||
398 | //- /main.rs | ||
399 | const foo<|>: u32 = 0; | ||
400 | "#, | ||
401 | &["const foo: u32"], | ||
402 | ); | ||
403 | |||
404 | check_hover_result( | ||
405 | r#" | ||
406 | //- /main.rs | ||
407 | static foo<|>: u32 = 0; | ||
408 | "#, | ||
409 | &["static foo: u32"], | ||
410 | ); | ||
411 | } | ||
412 | |||
413 | #[test] | ||
414 | fn hover_some() { | ||
415 | let (analysis, position) = single_file_with_position( | ||
416 | " | ||
417 | enum Option<T> { Some(T) } | ||
418 | use Option::Some; | ||
419 | |||
420 | fn main() { | ||
421 | So<|>me(12); | ||
422 | } | ||
423 | ", | ||
424 | ); | ||
425 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
426 | assert_eq!(trim_markup_opt(hover.info.first()), Some("Some")); | ||
427 | |||
428 | let (analysis, position) = single_file_with_position( | ||
429 | " | ||
430 | enum Option<T> { Some(T) } | ||
431 | use Option::Some; | ||
432 | |||
433 | fn main() { | ||
434 | let b<|>ar = Some(12); | ||
435 | } | ||
436 | ", | ||
437 | ); | ||
438 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
439 | assert_eq!(trim_markup_opt(hover.info.first()), Some("Option<i32>")); | ||
440 | } | ||
441 | |||
442 | #[test] | ||
443 | fn hover_enum_variant() { | ||
444 | check_hover_result( | ||
445 | r#" | ||
446 | //- /main.rs | ||
447 | enum Option<T> { | ||
448 | /// The None variant | ||
449 | Non<|>e | ||
450 | } | ||
451 | "#, | ||
452 | &[" | ||
453 | None | ||
454 | ``` | ||
455 | |||
456 | The None variant | ||
457 | " | ||
458 | .trim()], | ||
459 | ); | ||
460 | |||
461 | check_hover_result( | ||
462 | r#" | ||
463 | //- /main.rs | ||
464 | enum Option<T> { | ||
465 | /// The Some variant | ||
466 | Some(T) | ||
467 | } | ||
468 | fn main() { | ||
469 | let s = Option::Som<|>e(12); | ||
470 | } | ||
471 | "#, | ||
472 | &[" | ||
473 | Some | ||
474 | ``` | ||
475 | |||
476 | The Some variant | ||
477 | " | ||
478 | .trim()], | ||
479 | ); | ||
480 | } | ||
481 | |||
482 | #[test] | ||
483 | fn hover_for_local_variable() { | ||
484 | let (analysis, position) = single_file_with_position("fn func(foo: i32) { fo<|>o; }"); | ||
485 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
486 | assert_eq!(trim_markup_opt(hover.info.first()), Some("i32")); | ||
487 | } | ||
488 | |||
489 | #[test] | ||
490 | fn hover_for_local_variable_pat() { | ||
491 | let (analysis, position) = single_file_with_position("fn func(fo<|>o: i32) {}"); | ||
492 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
493 | assert_eq!(trim_markup_opt(hover.info.first()), Some("i32")); | ||
494 | } | ||
495 | |||
496 | #[test] | ||
497 | fn hover_local_var_edge() { | ||
498 | let (analysis, position) = single_file_with_position( | ||
499 | " | ||
500 | fn func(foo: i32) { if true { <|>foo; }; } | ||
501 | ", | ||
502 | ); | ||
503 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
504 | assert_eq!(trim_markup_opt(hover.info.first()), Some("i32")); | ||
505 | } | ||
506 | |||
507 | #[test] | ||
508 | fn test_type_of_for_function() { | ||
509 | let (analysis, range) = single_file_with_range( | ||
510 | " | ||
511 | pub fn foo() -> u32 { 1 }; | ||
512 | |||
513 | fn main() { | ||
514 | let foo_test = <|>foo()<|>; | ||
515 | } | ||
516 | ", | ||
517 | ); | ||
518 | |||
519 | let type_name = analysis.type_of(range).unwrap().unwrap(); | ||
520 | assert_eq!("u32", &type_name); | ||
521 | } | ||
522 | |||
523 | #[test] | ||
524 | fn test_type_of_for_expr() { | ||
525 | let (analysis, range) = single_file_with_range( | ||
526 | " | ||
527 | fn main() { | ||
528 | let foo: usize = 1; | ||
529 | let bar = <|>1 + foo<|>; | ||
530 | } | ||
531 | ", | ||
532 | ); | ||
533 | |||
534 | let type_name = analysis.type_of(range).unwrap().unwrap(); | ||
535 | assert_eq!("usize", &type_name); | ||
536 | } | ||
537 | |||
538 | #[test] | ||
539 | fn test_hover_infer_associated_method_result() { | ||
540 | let (analysis, position) = single_file_with_position( | ||
541 | " | ||
542 | struct Thing { x: u32 } | ||
543 | |||
544 | impl Thing { | ||
545 | fn new() -> Thing { | ||
546 | Thing { x: 0 } | ||
547 | } | ||
548 | } | ||
549 | |||
550 | fn main() { | ||
551 | let foo_<|>test = Thing::new(); | ||
552 | } | ||
553 | ", | ||
554 | ); | ||
555 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
556 | assert_eq!(trim_markup_opt(hover.info.first()), Some("Thing")); | ||
557 | } | ||
558 | |||
559 | #[test] | ||
560 | fn test_hover_infer_associated_method_exact() { | ||
561 | let (analysis, position) = single_file_with_position( | ||
562 | " | ||
563 | struct Thing { x: u32 } | ||
564 | |||
565 | impl Thing { | ||
566 | fn new() -> Thing { | ||
567 | Thing { x: 0 } | ||
568 | } | ||
569 | } | ||
570 | |||
571 | fn main() { | ||
572 | let foo_test = Thing::new<|>(); | ||
573 | } | ||
574 | ", | ||
575 | ); | ||
576 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
577 | assert_eq!(trim_markup_opt(hover.info.first()), Some("fn new() -> Thing")); | ||
578 | assert_eq!(hover.info.is_exact(), true); | ||
579 | } | ||
580 | |||
581 | #[test] | ||
582 | fn test_hover_infer_associated_const_in_pattern() { | ||
583 | let (analysis, position) = single_file_with_position( | ||
584 | " | ||
585 | struct X; | ||
586 | impl X { | ||
587 | const C: u32 = 1; | ||
588 | } | ||
589 | |||
590 | fn main() { | ||
591 | match 1 { | ||
592 | X::C<|> => {}, | ||
593 | 2 => {}, | ||
594 | _ => {} | ||
595 | }; | ||
596 | } | ||
597 | ", | ||
598 | ); | ||
599 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
600 | assert_eq!(trim_markup_opt(hover.info.first()), Some("const C: u32")); | ||
601 | assert_eq!(hover.info.is_exact(), true); | ||
602 | } | ||
603 | |||
604 | #[test] | ||
605 | fn test_hover_self() { | ||
606 | let (analysis, position) = single_file_with_position( | ||
607 | " | ||
608 | struct Thing { x: u32 } | ||
609 | impl Thing { | ||
610 | fn new() -> Self { | ||
611 | Self<|> { x: 0 } | ||
612 | } | ||
613 | } | ||
614 | ", | ||
615 | ); | ||
616 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
617 | assert_eq!(trim_markup_opt(hover.info.first()), Some("Thing")); | ||
618 | assert_eq!(hover.info.is_exact(), true); | ||
619 | |||
620 | /* FIXME: revive these tests | ||
621 | let (analysis, position) = single_file_with_position( | ||
622 | " | ||
623 | struct Thing { x: u32 } | ||
624 | impl Thing { | ||
625 | fn new() -> Self<|> { | ||
626 | Self { x: 0 } | ||
627 | } | ||
628 | } | ||
629 | ", | ||
630 | ); | ||
631 | |||
632 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
633 | assert_eq!(trim_markup_opt(hover.info.first()), Some("Thing")); | ||
634 | assert_eq!(hover.info.is_exact(), true); | ||
635 | |||
636 | let (analysis, position) = single_file_with_position( | ||
637 | " | ||
638 | enum Thing { A } | ||
639 | impl Thing { | ||
640 | pub fn new() -> Self<|> { | ||
641 | Thing::A | ||
642 | } | ||
643 | } | ||
644 | ", | ||
645 | ); | ||
646 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
647 | assert_eq!(trim_markup_opt(hover.info.first()), Some("enum Thing")); | ||
648 | assert_eq!(hover.info.is_exact(), true); | ||
649 | |||
650 | let (analysis, position) = single_file_with_position( | ||
651 | " | ||
652 | enum Thing { A } | ||
653 | impl Thing { | ||
654 | pub fn thing(a: Self<|>) { | ||
655 | } | ||
656 | } | ||
657 | ", | ||
658 | ); | ||
659 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
660 | assert_eq!(trim_markup_opt(hover.info.first()), Some("enum Thing")); | ||
661 | assert_eq!(hover.info.is_exact(), true); | ||
662 | */ | ||
663 | } | ||
664 | |||
665 | #[test] | ||
666 | fn test_hover_shadowing_pat() { | ||
667 | let (analysis, position) = single_file_with_position( | ||
668 | " | ||
669 | fn x() {} | ||
670 | |||
671 | fn y() { | ||
672 | let x = 0i32; | ||
673 | x<|>; | ||
674 | } | ||
675 | ", | ||
676 | ); | ||
677 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
678 | assert_eq!(trim_markup_opt(hover.info.first()), Some("i32")); | ||
679 | assert_eq!(hover.info.is_exact(), true); | ||
680 | } | ||
681 | |||
682 | #[test] | ||
683 | fn test_hover_macro_invocation() { | ||
684 | let (analysis, position) = single_file_with_position( | ||
685 | " | ||
686 | macro_rules! foo { | ||
687 | () => {} | ||
688 | } | ||
689 | |||
690 | fn f() { | ||
691 | fo<|>o!(); | ||
692 | } | ||
693 | ", | ||
694 | ); | ||
695 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
696 | assert_eq!(trim_markup_opt(hover.info.first()), Some("macro_rules! foo")); | ||
697 | assert_eq!(hover.info.is_exact(), true); | ||
698 | } | ||
699 | |||
700 | #[test] | ||
701 | fn test_hover_tuple_field() { | ||
702 | let (analysis, position) = single_file_with_position( | ||
703 | " | ||
704 | struct TS(String, i32<|>); | ||
705 | ", | ||
706 | ); | ||
707 | let hover = analysis.hover(position).unwrap().unwrap(); | ||
708 | assert_eq!(trim_markup_opt(hover.info.first()), Some("i32")); | ||
709 | assert_eq!(hover.info.is_exact(), true); | ||
710 | } | ||
711 | |||
712 | #[test] | ||
713 | fn test_hover_through_macro() { | ||
714 | check_hover_result( | ||
715 | " | ||
716 | //- /lib.rs | ||
717 | macro_rules! id { | ||
718 | ($($tt:tt)*) => { $($tt)* } | ||
719 | } | ||
720 | fn foo() {} | ||
721 | id! { | ||
722 | fn bar() { | ||
723 | fo<|>o(); | ||
724 | } | ||
725 | } | ||
726 | ", | ||
727 | &["fn foo()"], | ||
728 | ); | ||
729 | } | ||
730 | } | ||
diff --git a/crates/ra_ide/src/impls.rs b/crates/ra_ide/src/impls.rs new file mode 100644 index 000000000..aa480e399 --- /dev/null +++ b/crates/ra_ide/src/impls.rs | |||
@@ -0,0 +1,206 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::{FromSource, ImplBlock}; | ||
4 | use ra_db::SourceDatabase; | ||
5 | use ra_syntax::{algo::find_node_at_offset, ast, AstNode}; | ||
6 | |||
7 | use crate::{db::RootDatabase, display::ToNav, FilePosition, NavigationTarget, RangeInfo}; | ||
8 | |||
9 | pub(crate) fn goto_implementation( | ||
10 | db: &RootDatabase, | ||
11 | position: FilePosition, | ||
12 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
13 | let parse = db.parse(position.file_id); | ||
14 | let syntax = parse.tree().syntax().clone(); | ||
15 | |||
16 | let src = hir::ModuleSource::from_position(db, position); | ||
17 | let module = hir::Module::from_definition( | ||
18 | db, | ||
19 | hir::Source { file_id: position.file_id.into(), value: src }, | ||
20 | )?; | ||
21 | |||
22 | if let Some(nominal_def) = find_node_at_offset::<ast::NominalDef>(&syntax, position.offset) { | ||
23 | return Some(RangeInfo::new( | ||
24 | nominal_def.syntax().text_range(), | ||
25 | impls_for_def(db, position, &nominal_def, module)?, | ||
26 | )); | ||
27 | } else if let Some(trait_def) = find_node_at_offset::<ast::TraitDef>(&syntax, position.offset) { | ||
28 | return Some(RangeInfo::new( | ||
29 | trait_def.syntax().text_range(), | ||
30 | impls_for_trait(db, position, &trait_def, module)?, | ||
31 | )); | ||
32 | } | ||
33 | |||
34 | None | ||
35 | } | ||
36 | |||
37 | fn impls_for_def( | ||
38 | db: &RootDatabase, | ||
39 | position: FilePosition, | ||
40 | node: &ast::NominalDef, | ||
41 | module: hir::Module, | ||
42 | ) -> Option<Vec<NavigationTarget>> { | ||
43 | let ty = match node { | ||
44 | ast::NominalDef::StructDef(def) => { | ||
45 | let src = hir::Source { file_id: position.file_id.into(), value: def.clone() }; | ||
46 | hir::Struct::from_source(db, src)?.ty(db) | ||
47 | } | ||
48 | ast::NominalDef::EnumDef(def) => { | ||
49 | let src = hir::Source { file_id: position.file_id.into(), value: def.clone() }; | ||
50 | hir::Enum::from_source(db, src)?.ty(db) | ||
51 | } | ||
52 | ast::NominalDef::UnionDef(def) => { | ||
53 | let src = hir::Source { file_id: position.file_id.into(), value: def.clone() }; | ||
54 | hir::Union::from_source(db, src)?.ty(db) | ||
55 | } | ||
56 | }; | ||
57 | |||
58 | let krate = module.krate(); | ||
59 | let impls = ImplBlock::all_in_crate(db, krate); | ||
60 | |||
61 | Some( | ||
62 | impls | ||
63 | .into_iter() | ||
64 | .filter(|impl_block| ty.is_equal_for_find_impls(&impl_block.target_ty(db))) | ||
65 | .map(|imp| imp.to_nav(db)) | ||
66 | .collect(), | ||
67 | ) | ||
68 | } | ||
69 | |||
70 | fn impls_for_trait( | ||
71 | db: &RootDatabase, | ||
72 | position: FilePosition, | ||
73 | node: &ast::TraitDef, | ||
74 | module: hir::Module, | ||
75 | ) -> Option<Vec<NavigationTarget>> { | ||
76 | let src = hir::Source { file_id: position.file_id.into(), value: node.clone() }; | ||
77 | let tr = hir::Trait::from_source(db, src)?; | ||
78 | |||
79 | let krate = module.krate(); | ||
80 | let impls = ImplBlock::for_trait(db, krate, tr); | ||
81 | |||
82 | Some(impls.into_iter().map(|imp| imp.to_nav(db)).collect()) | ||
83 | } | ||
84 | |||
85 | #[cfg(test)] | ||
86 | mod tests { | ||
87 | use crate::mock_analysis::analysis_and_position; | ||
88 | |||
89 | fn check_goto(fixture: &str, expected: &[&str]) { | ||
90 | let (analysis, pos) = analysis_and_position(fixture); | ||
91 | |||
92 | let mut navs = analysis.goto_implementation(pos).unwrap().unwrap().info; | ||
93 | assert_eq!(navs.len(), expected.len()); | ||
94 | navs.sort_by_key(|nav| (nav.file_id(), nav.full_range().start())); | ||
95 | navs.into_iter().enumerate().for_each(|(i, nav)| nav.assert_match(expected[i])); | ||
96 | } | ||
97 | |||
98 | #[test] | ||
99 | fn goto_implementation_works() { | ||
100 | check_goto( | ||
101 | " | ||
102 | //- /lib.rs | ||
103 | struct Foo<|>; | ||
104 | impl Foo {} | ||
105 | ", | ||
106 | &["impl IMPL_BLOCK FileId(1) [12; 23)"], | ||
107 | ); | ||
108 | } | ||
109 | |||
110 | #[test] | ||
111 | fn goto_implementation_works_multiple_blocks() { | ||
112 | check_goto( | ||
113 | " | ||
114 | //- /lib.rs | ||
115 | struct Foo<|>; | ||
116 | impl Foo {} | ||
117 | impl Foo {} | ||
118 | ", | ||
119 | &["impl IMPL_BLOCK FileId(1) [12; 23)", "impl IMPL_BLOCK FileId(1) [24; 35)"], | ||
120 | ); | ||
121 | } | ||
122 | |||
123 | #[test] | ||
124 | fn goto_implementation_works_multiple_mods() { | ||
125 | check_goto( | ||
126 | " | ||
127 | //- /lib.rs | ||
128 | struct Foo<|>; | ||
129 | mod a { | ||
130 | impl super::Foo {} | ||
131 | } | ||
132 | mod b { | ||
133 | impl super::Foo {} | ||
134 | } | ||
135 | ", | ||
136 | &["impl IMPL_BLOCK FileId(1) [24; 42)", "impl IMPL_BLOCK FileId(1) [57; 75)"], | ||
137 | ); | ||
138 | } | ||
139 | |||
140 | #[test] | ||
141 | fn goto_implementation_works_multiple_files() { | ||
142 | check_goto( | ||
143 | " | ||
144 | //- /lib.rs | ||
145 | struct Foo<|>; | ||
146 | mod a; | ||
147 | mod b; | ||
148 | //- /a.rs | ||
149 | impl crate::Foo {} | ||
150 | //- /b.rs | ||
151 | impl crate::Foo {} | ||
152 | ", | ||
153 | &["impl IMPL_BLOCK FileId(2) [0; 18)", "impl IMPL_BLOCK FileId(3) [0; 18)"], | ||
154 | ); | ||
155 | } | ||
156 | |||
157 | #[test] | ||
158 | fn goto_implementation_for_trait() { | ||
159 | check_goto( | ||
160 | " | ||
161 | //- /lib.rs | ||
162 | trait T<|> {} | ||
163 | struct Foo; | ||
164 | impl T for Foo {} | ||
165 | ", | ||
166 | &["impl IMPL_BLOCK FileId(1) [23; 40)"], | ||
167 | ); | ||
168 | } | ||
169 | |||
170 | #[test] | ||
171 | fn goto_implementation_for_trait_multiple_files() { | ||
172 | check_goto( | ||
173 | " | ||
174 | //- /lib.rs | ||
175 | trait T<|> {}; | ||
176 | struct Foo; | ||
177 | mod a; | ||
178 | mod b; | ||
179 | //- /a.rs | ||
180 | impl crate::T for crate::Foo {} | ||
181 | //- /b.rs | ||
182 | impl crate::T for crate::Foo {} | ||
183 | ", | ||
184 | &["impl IMPL_BLOCK FileId(2) [0; 31)", "impl IMPL_BLOCK FileId(3) [0; 31)"], | ||
185 | ); | ||
186 | } | ||
187 | |||
188 | #[test] | ||
189 | fn goto_implementation_all_impls() { | ||
190 | check_goto( | ||
191 | " | ||
192 | //- /lib.rs | ||
193 | trait T {} | ||
194 | struct Foo<|>; | ||
195 | impl Foo {} | ||
196 | impl T for Foo {} | ||
197 | impl T for &Foo {} | ||
198 | ", | ||
199 | &[ | ||
200 | "impl IMPL_BLOCK FileId(1) [23; 34)", | ||
201 | "impl IMPL_BLOCK FileId(1) [35; 52)", | ||
202 | "impl IMPL_BLOCK FileId(1) [53; 71)", | ||
203 | ], | ||
204 | ); | ||
205 | } | ||
206 | } | ||
diff --git a/crates/ra_ide/src/inlay_hints.rs b/crates/ra_ide/src/inlay_hints.rs new file mode 100644 index 000000000..45149bf0c --- /dev/null +++ b/crates/ra_ide/src/inlay_hints.rs | |||
@@ -0,0 +1,543 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::{db::RootDatabase, FileId}; | ||
4 | use hir::{HirDisplay, SourceAnalyzer}; | ||
5 | use ra_syntax::{ | ||
6 | ast::{self, AstNode, TypeAscriptionOwner}, | ||
7 | match_ast, SmolStr, SourceFile, SyntaxKind, SyntaxNode, TextRange, | ||
8 | }; | ||
9 | |||
10 | #[derive(Debug, PartialEq, Eq)] | ||
11 | pub enum InlayKind { | ||
12 | TypeHint, | ||
13 | } | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | pub struct InlayHint { | ||
17 | pub range: TextRange, | ||
18 | pub kind: InlayKind, | ||
19 | pub label: SmolStr, | ||
20 | } | ||
21 | |||
22 | pub(crate) fn inlay_hints( | ||
23 | db: &RootDatabase, | ||
24 | file_id: FileId, | ||
25 | file: &SourceFile, | ||
26 | max_inlay_hint_length: Option<usize>, | ||
27 | ) -> Vec<InlayHint> { | ||
28 | file.syntax() | ||
29 | .descendants() | ||
30 | .map(|node| get_inlay_hints(db, file_id, &node, max_inlay_hint_length).unwrap_or_default()) | ||
31 | .flatten() | ||
32 | .collect() | ||
33 | } | ||
34 | |||
35 | fn get_inlay_hints( | ||
36 | db: &RootDatabase, | ||
37 | file_id: FileId, | ||
38 | node: &SyntaxNode, | ||
39 | max_inlay_hint_length: Option<usize>, | ||
40 | ) -> Option<Vec<InlayHint>> { | ||
41 | let analyzer = SourceAnalyzer::new(db, hir::Source::new(file_id.into(), node), None); | ||
42 | match_ast! { | ||
43 | match node { | ||
44 | ast::LetStmt(it) => { | ||
45 | if it.ascribed_type().is_some() { | ||
46 | return None; | ||
47 | } | ||
48 | let pat = it.pat()?; | ||
49 | Some(get_pat_type_hints(db, &analyzer, pat, false, max_inlay_hint_length)) | ||
50 | }, | ||
51 | ast::LambdaExpr(it) => { | ||
52 | it.param_list().map(|param_list| { | ||
53 | param_list | ||
54 | .params() | ||
55 | .filter(|closure_param| closure_param.ascribed_type().is_none()) | ||
56 | .filter_map(|closure_param| closure_param.pat()) | ||
57 | .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, false, max_inlay_hint_length)) | ||
58 | .flatten() | ||
59 | .collect() | ||
60 | }) | ||
61 | }, | ||
62 | ast::ForExpr(it) => { | ||
63 | let pat = it.pat()?; | ||
64 | Some(get_pat_type_hints(db, &analyzer, pat, false, max_inlay_hint_length)) | ||
65 | }, | ||
66 | ast::IfExpr(it) => { | ||
67 | let pat = it.condition()?.pat()?; | ||
68 | Some(get_pat_type_hints(db, &analyzer, pat, true, max_inlay_hint_length)) | ||
69 | }, | ||
70 | ast::WhileExpr(it) => { | ||
71 | let pat = it.condition()?.pat()?; | ||
72 | Some(get_pat_type_hints(db, &analyzer, pat, true, max_inlay_hint_length)) | ||
73 | }, | ||
74 | ast::MatchArmList(it) => { | ||
75 | Some( | ||
76 | it | ||
77 | .arms() | ||
78 | .map(|match_arm| match_arm.pats()) | ||
79 | .flatten() | ||
80 | .map(|root_pat| get_pat_type_hints(db, &analyzer, root_pat, true, max_inlay_hint_length)) | ||
81 | .flatten() | ||
82 | .collect(), | ||
83 | ) | ||
84 | }, | ||
85 | _ => None, | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | |||
90 | fn get_pat_type_hints( | ||
91 | db: &RootDatabase, | ||
92 | analyzer: &SourceAnalyzer, | ||
93 | root_pat: ast::Pat, | ||
94 | skip_root_pat_hint: bool, | ||
95 | max_inlay_hint_length: Option<usize>, | ||
96 | ) -> Vec<InlayHint> { | ||
97 | let original_pat = &root_pat.clone(); | ||
98 | |||
99 | get_leaf_pats(root_pat) | ||
100 | .into_iter() | ||
101 | .filter(|pat| !skip_root_pat_hint || pat != original_pat) | ||
102 | .filter_map(|pat| { | ||
103 | let ty = analyzer.type_of_pat(db, &pat)?; | ||
104 | if ty.is_unknown() { | ||
105 | return None; | ||
106 | } | ||
107 | Some((pat.syntax().text_range(), ty)) | ||
108 | }) | ||
109 | .map(|(range, pat_type)| InlayHint { | ||
110 | range, | ||
111 | kind: InlayKind::TypeHint, | ||
112 | label: pat_type.display_truncated(db, max_inlay_hint_length).to_string().into(), | ||
113 | }) | ||
114 | .collect() | ||
115 | } | ||
116 | |||
117 | fn get_leaf_pats(root_pat: ast::Pat) -> Vec<ast::Pat> { | ||
118 | let mut pats_to_process = std::collections::VecDeque::<ast::Pat>::new(); | ||
119 | pats_to_process.push_back(root_pat); | ||
120 | |||
121 | let mut leaf_pats = Vec::new(); | ||
122 | |||
123 | while let Some(maybe_leaf_pat) = pats_to_process.pop_front() { | ||
124 | match &maybe_leaf_pat { | ||
125 | ast::Pat::BindPat(bind_pat) => { | ||
126 | if let Some(pat) = bind_pat.pat() { | ||
127 | pats_to_process.push_back(pat); | ||
128 | } else { | ||
129 | leaf_pats.push(maybe_leaf_pat); | ||
130 | } | ||
131 | } | ||
132 | ast::Pat::TuplePat(tuple_pat) => { | ||
133 | for arg_pat in tuple_pat.args() { | ||
134 | pats_to_process.push_back(arg_pat); | ||
135 | } | ||
136 | } | ||
137 | ast::Pat::RecordPat(record_pat) => { | ||
138 | if let Some(pat_list) = record_pat.record_field_pat_list() { | ||
139 | pats_to_process.extend( | ||
140 | pat_list | ||
141 | .record_field_pats() | ||
142 | .filter_map(|record_field_pat| { | ||
143 | record_field_pat | ||
144 | .pat() | ||
145 | .filter(|pat| pat.syntax().kind() != SyntaxKind::BIND_PAT) | ||
146 | }) | ||
147 | .chain(pat_list.bind_pats().map(|bind_pat| { | ||
148 | bind_pat.pat().unwrap_or_else(|| ast::Pat::from(bind_pat)) | ||
149 | })), | ||
150 | ); | ||
151 | } | ||
152 | } | ||
153 | ast::Pat::TupleStructPat(tuple_struct_pat) => { | ||
154 | for arg_pat in tuple_struct_pat.args() { | ||
155 | pats_to_process.push_back(arg_pat); | ||
156 | } | ||
157 | } | ||
158 | _ => (), | ||
159 | } | ||
160 | } | ||
161 | leaf_pats | ||
162 | } | ||
163 | |||
164 | #[cfg(test)] | ||
165 | mod tests { | ||
166 | use crate::mock_analysis::single_file; | ||
167 | use insta::assert_debug_snapshot; | ||
168 | |||
169 | #[test] | ||
170 | fn let_statement() { | ||
171 | let (analysis, file_id) = single_file( | ||
172 | r#" | ||
173 | #[derive(PartialEq)] | ||
174 | enum CustomOption<T> { | ||
175 | None, | ||
176 | Some(T), | ||
177 | } | ||
178 | |||
179 | #[derive(PartialEq)] | ||
180 | struct Test { | ||
181 | a: CustomOption<u32>, | ||
182 | b: u8, | ||
183 | } | ||
184 | |||
185 | fn main() { | ||
186 | struct InnerStruct {} | ||
187 | |||
188 | let test = 54; | ||
189 | let test: i32 = 33; | ||
190 | let mut test = 33; | ||
191 | let _ = 22; | ||
192 | let test = "test"; | ||
193 | let test = InnerStruct {}; | ||
194 | |||
195 | let test = vec![222]; | ||
196 | let test: Vec<_> = (0..3).collect(); | ||
197 | let test = (0..3).collect::<Vec<i128>>(); | ||
198 | let test = (0..3).collect::<Vec<_>>(); | ||
199 | |||
200 | let mut test = Vec::new(); | ||
201 | test.push(333); | ||
202 | |||
203 | let test = (42, 'a'); | ||
204 | let (a, (b, c, (d, e), f)) = (2, (3, 4, (6.6, 7.7), 5)); | ||
205 | }"#, | ||
206 | ); | ||
207 | |||
208 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
209 | [ | ||
210 | InlayHint { | ||
211 | range: [193; 197), | ||
212 | kind: TypeHint, | ||
213 | label: "i32", | ||
214 | }, | ||
215 | InlayHint { | ||
216 | range: [236; 244), | ||
217 | kind: TypeHint, | ||
218 | label: "i32", | ||
219 | }, | ||
220 | InlayHint { | ||
221 | range: [275; 279), | ||
222 | kind: TypeHint, | ||
223 | label: "&str", | ||
224 | }, | ||
225 | InlayHint { | ||
226 | range: [539; 543), | ||
227 | kind: TypeHint, | ||
228 | label: "(i32, char)", | ||
229 | }, | ||
230 | InlayHint { | ||
231 | range: [566; 567), | ||
232 | kind: TypeHint, | ||
233 | label: "i32", | ||
234 | }, | ||
235 | InlayHint { | ||
236 | range: [570; 571), | ||
237 | kind: TypeHint, | ||
238 | label: "i32", | ||
239 | }, | ||
240 | InlayHint { | ||
241 | range: [573; 574), | ||
242 | kind: TypeHint, | ||
243 | label: "i32", | ||
244 | }, | ||
245 | InlayHint { | ||
246 | range: [584; 585), | ||
247 | kind: TypeHint, | ||
248 | label: "i32", | ||
249 | }, | ||
250 | InlayHint { | ||
251 | range: [577; 578), | ||
252 | kind: TypeHint, | ||
253 | label: "f64", | ||
254 | }, | ||
255 | InlayHint { | ||
256 | range: [580; 581), | ||
257 | kind: TypeHint, | ||
258 | label: "f64", | ||
259 | }, | ||
260 | ] | ||
261 | "### | ||
262 | ); | ||
263 | } | ||
264 | |||
265 | #[test] | ||
266 | fn closure_parameter() { | ||
267 | let (analysis, file_id) = single_file( | ||
268 | r#" | ||
269 | fn main() { | ||
270 | let mut start = 0; | ||
271 | (0..2).for_each(|increment| { | ||
272 | start += increment; | ||
273 | }) | ||
274 | }"#, | ||
275 | ); | ||
276 | |||
277 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
278 | [ | ||
279 | InlayHint { | ||
280 | range: [21; 30), | ||
281 | kind: TypeHint, | ||
282 | label: "i32", | ||
283 | }, | ||
284 | InlayHint { | ||
285 | range: [57; 66), | ||
286 | kind: TypeHint, | ||
287 | label: "i32", | ||
288 | }, | ||
289 | ] | ||
290 | "### | ||
291 | ); | ||
292 | } | ||
293 | |||
294 | #[test] | ||
295 | fn for_expression() { | ||
296 | let (analysis, file_id) = single_file( | ||
297 | r#" | ||
298 | fn main() { | ||
299 | let mut start = 0; | ||
300 | for increment in 0..2 { | ||
301 | start += increment; | ||
302 | } | ||
303 | }"#, | ||
304 | ); | ||
305 | |||
306 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
307 | [ | ||
308 | InlayHint { | ||
309 | range: [21; 30), | ||
310 | kind: TypeHint, | ||
311 | label: "i32", | ||
312 | }, | ||
313 | InlayHint { | ||
314 | range: [44; 53), | ||
315 | kind: TypeHint, | ||
316 | label: "i32", | ||
317 | }, | ||
318 | ] | ||
319 | "### | ||
320 | ); | ||
321 | } | ||
322 | |||
323 | #[test] | ||
324 | fn if_expr() { | ||
325 | let (analysis, file_id) = single_file( | ||
326 | r#" | ||
327 | #[derive(PartialEq)] | ||
328 | enum CustomOption<T> { | ||
329 | None, | ||
330 | Some(T), | ||
331 | } | ||
332 | |||
333 | #[derive(PartialEq)] | ||
334 | struct Test { | ||
335 | a: CustomOption<u32>, | ||
336 | b: u8, | ||
337 | } | ||
338 | |||
339 | fn main() { | ||
340 | let test = CustomOption::Some(Test { a: CustomOption::Some(3), b: 1 }); | ||
341 | if let CustomOption::None = &test {}; | ||
342 | if let test = &test {}; | ||
343 | if let CustomOption::Some(test) = &test {}; | ||
344 | if let CustomOption::Some(Test { a, b }) = &test {}; | ||
345 | if let CustomOption::Some(Test { a: x, b: y }) = &test {}; | ||
346 | if let CustomOption::Some(Test { a: CustomOption::Some(x), b: y }) = &test {}; | ||
347 | if let CustomOption::Some(Test { a: CustomOption::None, b: y }) = &test {}; | ||
348 | if let CustomOption::Some(Test { b: y, .. }) = &test {}; | ||
349 | |||
350 | if test == CustomOption::None {} | ||
351 | }"#, | ||
352 | ); | ||
353 | |||
354 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
355 | [ | ||
356 | InlayHint { | ||
357 | range: [166; 170), | ||
358 | kind: TypeHint, | ||
359 | label: "CustomOption<Test>", | ||
360 | }, | ||
361 | InlayHint { | ||
362 | range: [334; 338), | ||
363 | kind: TypeHint, | ||
364 | label: "&Test", | ||
365 | }, | ||
366 | InlayHint { | ||
367 | range: [389; 390), | ||
368 | kind: TypeHint, | ||
369 | label: "&CustomOption<u32>", | ||
370 | }, | ||
371 | InlayHint { | ||
372 | range: [392; 393), | ||
373 | kind: TypeHint, | ||
374 | label: "&u8", | ||
375 | }, | ||
376 | InlayHint { | ||
377 | range: [531; 532), | ||
378 | kind: TypeHint, | ||
379 | label: "&u32", | ||
380 | }, | ||
381 | ] | ||
382 | "### | ||
383 | ); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn while_expr() { | ||
388 | let (analysis, file_id) = single_file( | ||
389 | r#" | ||
390 | #[derive(PartialEq)] | ||
391 | enum CustomOption<T> { | ||
392 | None, | ||
393 | Some(T), | ||
394 | } | ||
395 | |||
396 | #[derive(PartialEq)] | ||
397 | struct Test { | ||
398 | a: CustomOption<u32>, | ||
399 | b: u8, | ||
400 | } | ||
401 | |||
402 | fn main() { | ||
403 | let test = CustomOption::Some(Test { a: CustomOption::Some(3), b: 1 }); | ||
404 | while let CustomOption::None = &test {}; | ||
405 | while let test = &test {}; | ||
406 | while let CustomOption::Some(test) = &test {}; | ||
407 | while let CustomOption::Some(Test { a, b }) = &test {}; | ||
408 | while let CustomOption::Some(Test { a: x, b: y }) = &test {}; | ||
409 | while let CustomOption::Some(Test { a: CustomOption::Some(x), b: y }) = &test {}; | ||
410 | while let CustomOption::Some(Test { a: CustomOption::None, b: y }) = &test {}; | ||
411 | while let CustomOption::Some(Test { b: y, .. }) = &test {}; | ||
412 | |||
413 | while test == CustomOption::None {} | ||
414 | }"#, | ||
415 | ); | ||
416 | |||
417 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
418 | [ | ||
419 | InlayHint { | ||
420 | range: [166; 170), | ||
421 | kind: TypeHint, | ||
422 | label: "CustomOption<Test>", | ||
423 | }, | ||
424 | InlayHint { | ||
425 | range: [343; 347), | ||
426 | kind: TypeHint, | ||
427 | label: "&Test", | ||
428 | }, | ||
429 | InlayHint { | ||
430 | range: [401; 402), | ||
431 | kind: TypeHint, | ||
432 | label: "&CustomOption<u32>", | ||
433 | }, | ||
434 | InlayHint { | ||
435 | range: [404; 405), | ||
436 | kind: TypeHint, | ||
437 | label: "&u8", | ||
438 | }, | ||
439 | InlayHint { | ||
440 | range: [549; 550), | ||
441 | kind: TypeHint, | ||
442 | label: "&u32", | ||
443 | }, | ||
444 | ] | ||
445 | "### | ||
446 | ); | ||
447 | } | ||
448 | |||
449 | #[test] | ||
450 | fn match_arm_list() { | ||
451 | let (analysis, file_id) = single_file( | ||
452 | r#" | ||
453 | #[derive(PartialEq)] | ||
454 | enum CustomOption<T> { | ||
455 | None, | ||
456 | Some(T), | ||
457 | } | ||
458 | |||
459 | #[derive(PartialEq)] | ||
460 | struct Test { | ||
461 | a: CustomOption<u32>, | ||
462 | b: u8, | ||
463 | } | ||
464 | |||
465 | fn main() { | ||
466 | match CustomOption::Some(Test { a: CustomOption::Some(3), b: 1 }) { | ||
467 | CustomOption::None => (), | ||
468 | test => (), | ||
469 | CustomOption::Some(test) => (), | ||
470 | CustomOption::Some(Test { a, b }) => (), | ||
471 | CustomOption::Some(Test { a: x, b: y }) => (), | ||
472 | CustomOption::Some(Test { a: CustomOption::Some(x), b: y }) => (), | ||
473 | CustomOption::Some(Test { a: CustomOption::None, b: y }) => (), | ||
474 | CustomOption::Some(Test { b: y, .. }) => (), | ||
475 | _ => {} | ||
476 | } | ||
477 | }"#, | ||
478 | ); | ||
479 | |||
480 | assert_debug_snapshot!(analysis.inlay_hints(file_id, None).unwrap(), @r###" | ||
481 | [ | ||
482 | InlayHint { | ||
483 | range: [311; 315), | ||
484 | kind: TypeHint, | ||
485 | label: "Test", | ||
486 | }, | ||
487 | InlayHint { | ||
488 | range: [358; 359), | ||
489 | kind: TypeHint, | ||
490 | label: "CustomOption<u32>", | ||
491 | }, | ||
492 | InlayHint { | ||
493 | range: [361; 362), | ||
494 | kind: TypeHint, | ||
495 | label: "u8", | ||
496 | }, | ||
497 | InlayHint { | ||
498 | range: [484; 485), | ||
499 | kind: TypeHint, | ||
500 | label: "u32", | ||
501 | }, | ||
502 | ] | ||
503 | "### | ||
504 | ); | ||
505 | } | ||
506 | |||
507 | #[test] | ||
508 | fn hint_truncation() { | ||
509 | let (analysis, file_id) = single_file( | ||
510 | r#" | ||
511 | struct Smol<T>(T); | ||
512 | |||
513 | struct VeryLongOuterName<T>(T); | ||
514 | |||
515 | fn main() { | ||
516 | let a = Smol(0u32); | ||
517 | let b = VeryLongOuterName(0usize); | ||
518 | let c = Smol(Smol(0u32)) | ||
519 | }"#, | ||
520 | ); | ||
521 | |||
522 | assert_debug_snapshot!(analysis.inlay_hints(file_id, Some(8)).unwrap(), @r###" | ||
523 | [ | ||
524 | InlayHint { | ||
525 | range: [74; 75), | ||
526 | kind: TypeHint, | ||
527 | label: "Smol<u32>", | ||
528 | }, | ||
529 | InlayHint { | ||
530 | range: [98; 99), | ||
531 | kind: TypeHint, | ||
532 | label: "VeryLongOuterName<…>", | ||
533 | }, | ||
534 | InlayHint { | ||
535 | range: [137; 138), | ||
536 | kind: TypeHint, | ||
537 | label: "Smol<Smol<…>>", | ||
538 | }, | ||
539 | ] | ||
540 | "### | ||
541 | ); | ||
542 | } | ||
543 | } | ||
diff --git a/crates/ra_ide/src/join_lines.rs b/crates/ra_ide/src/join_lines.rs new file mode 100644 index 000000000..7deeb3494 --- /dev/null +++ b/crates/ra_ide/src/join_lines.rs | |||
@@ -0,0 +1,611 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use itertools::Itertools; | ||
4 | use ra_fmt::{compute_ws, extract_trivial_expression}; | ||
5 | use ra_syntax::{ | ||
6 | algo::{find_covering_element, non_trivia_sibling}, | ||
7 | ast::{self, AstNode, AstToken}, | ||
8 | Direction, NodeOrToken, SourceFile, | ||
9 | SyntaxKind::{self, WHITESPACE}, | ||
10 | SyntaxNode, SyntaxToken, TextRange, TextUnit, T, | ||
11 | }; | ||
12 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
13 | |||
14 | pub fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit { | ||
15 | let range = if range.is_empty() { | ||
16 | let syntax = file.syntax(); | ||
17 | let text = syntax.text().slice(range.start()..); | ||
18 | let pos = match text.find_char('\n') { | ||
19 | None => return TextEditBuilder::default().finish(), | ||
20 | Some(pos) => pos, | ||
21 | }; | ||
22 | TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n')) | ||
23 | } else { | ||
24 | range | ||
25 | }; | ||
26 | |||
27 | let node = match find_covering_element(file.syntax(), range) { | ||
28 | NodeOrToken::Node(node) => node, | ||
29 | NodeOrToken::Token(token) => token.parent(), | ||
30 | }; | ||
31 | let mut edit = TextEditBuilder::default(); | ||
32 | for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) { | ||
33 | let range = match range.intersection(&token.text_range()) { | ||
34 | Some(range) => range, | ||
35 | None => continue, | ||
36 | } - token.text_range().start(); | ||
37 | let text = token.text(); | ||
38 | for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') { | ||
39 | let pos: TextUnit = (pos as u32).into(); | ||
40 | let off = token.text_range().start() + range.start() + pos; | ||
41 | if !edit.invalidates_offset(off) { | ||
42 | remove_newline(&mut edit, &token, off); | ||
43 | } | ||
44 | } | ||
45 | } | ||
46 | |||
47 | edit.finish() | ||
48 | } | ||
49 | |||
50 | fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextUnit) { | ||
51 | if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 { | ||
52 | // The node is either the first or the last in the file | ||
53 | let suff = &token.text()[TextRange::from_to( | ||
54 | offset - token.text_range().start() + TextUnit::of_char('\n'), | ||
55 | TextUnit::of_str(token.text()), | ||
56 | )]; | ||
57 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); | ||
58 | |||
59 | edit.replace(TextRange::offset_len(offset, ((spaces + 1) as u32).into()), " ".to_string()); | ||
60 | return; | ||
61 | } | ||
62 | |||
63 | // Special case that turns something like: | ||
64 | // | ||
65 | // ``` | ||
66 | // my_function({<|> | ||
67 | // <some-expr> | ||
68 | // }) | ||
69 | // ``` | ||
70 | // | ||
71 | // into `my_function(<some-expr>)` | ||
72 | if join_single_expr_block(edit, token).is_some() { | ||
73 | return; | ||
74 | } | ||
75 | // ditto for | ||
76 | // | ||
77 | // ``` | ||
78 | // use foo::{<|> | ||
79 | // bar | ||
80 | // }; | ||
81 | // ``` | ||
82 | if join_single_use_tree(edit, token).is_some() { | ||
83 | return; | ||
84 | } | ||
85 | |||
86 | // The node is between two other nodes | ||
87 | let prev = token.prev_sibling_or_token().unwrap(); | ||
88 | let next = token.next_sibling_or_token().unwrap(); | ||
89 | if is_trailing_comma(prev.kind(), next.kind()) { | ||
90 | // Removes: trailing comma, newline (incl. surrounding whitespace) | ||
91 | edit.delete(TextRange::from_to(prev.text_range().start(), token.text_range().end())); | ||
92 | } else if prev.kind() == T![,] && next.kind() == T!['}'] { | ||
93 | // Removes: comma, newline (incl. surrounding whitespace) | ||
94 | let space = if let Some(left) = prev.prev_sibling_or_token() { | ||
95 | compute_ws(left.kind(), next.kind()) | ||
96 | } else { | ||
97 | " " | ||
98 | }; | ||
99 | edit.replace( | ||
100 | TextRange::from_to(prev.text_range().start(), token.text_range().end()), | ||
101 | space.to_string(), | ||
102 | ); | ||
103 | } else if let (Some(_), Some(next)) = ( | ||
104 | prev.as_token().cloned().and_then(ast::Comment::cast), | ||
105 | next.as_token().cloned().and_then(ast::Comment::cast), | ||
106 | ) { | ||
107 | // Removes: newline (incl. surrounding whitespace), start of the next comment | ||
108 | edit.delete(TextRange::from_to( | ||
109 | token.text_range().start(), | ||
110 | next.syntax().text_range().start() + TextUnit::of_str(next.prefix()), | ||
111 | )); | ||
112 | } else { | ||
113 | // Remove newline but add a computed amount of whitespace characters | ||
114 | edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string()); | ||
115 | } | ||
116 | } | ||
117 | |||
118 | fn has_comma_after(node: &SyntaxNode) -> bool { | ||
119 | match non_trivia_sibling(node.clone().into(), Direction::Next) { | ||
120 | Some(n) => n.kind() == T![,], | ||
121 | _ => false, | ||
122 | } | ||
123 | } | ||
124 | |||
125 | fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { | ||
126 | let block = ast::Block::cast(token.parent())?; | ||
127 | let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?; | ||
128 | let expr = extract_trivial_expression(&block_expr)?; | ||
129 | |||
130 | let block_range = block_expr.syntax().text_range(); | ||
131 | let mut buf = expr.syntax().text().to_string(); | ||
132 | |||
133 | // Match block needs to have a comma after the block | ||
134 | if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) { | ||
135 | if !has_comma_after(match_arm.syntax()) { | ||
136 | buf.push(','); | ||
137 | } | ||
138 | } | ||
139 | |||
140 | edit.replace(block_range, buf); | ||
141 | |||
142 | Some(()) | ||
143 | } | ||
144 | |||
145 | fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> { | ||
146 | let use_tree_list = ast::UseTreeList::cast(token.parent())?; | ||
147 | let (tree,) = use_tree_list.use_trees().collect_tuple()?; | ||
148 | edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string()); | ||
149 | Some(()) | ||
150 | } | ||
151 | |||
152 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
153 | match (left, right) { | ||
154 | (T![,], T![')']) | (T![,], T![']']) => true, | ||
155 | _ => false, | ||
156 | } | ||
157 | } | ||
158 | |||
159 | #[cfg(test)] | ||
160 | mod tests { | ||
161 | use crate::test_utils::{assert_eq_text, check_action, extract_range}; | ||
162 | |||
163 | use super::*; | ||
164 | |||
165 | fn check_join_lines(before: &str, after: &str) { | ||
166 | check_action(before, after, |file, offset| { | ||
167 | let range = TextRange::offset_len(offset, 0.into()); | ||
168 | let res = join_lines(file, range); | ||
169 | Some(res) | ||
170 | }) | ||
171 | } | ||
172 | |||
173 | #[test] | ||
174 | fn test_join_lines_comma() { | ||
175 | check_join_lines( | ||
176 | r" | ||
177 | fn foo() { | ||
178 | <|>foo(1, | ||
179 | ) | ||
180 | } | ||
181 | ", | ||
182 | r" | ||
183 | fn foo() { | ||
184 | <|>foo(1) | ||
185 | } | ||
186 | ", | ||
187 | ); | ||
188 | } | ||
189 | |||
190 | #[test] | ||
191 | fn test_join_lines_lambda_block() { | ||
192 | check_join_lines( | ||
193 | r" | ||
194 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
195 | <|>self.incremental_reparse(edit).unwrap_or_else(|| { | ||
196 | self.full_reparse(edit) | ||
197 | }) | ||
198 | } | ||
199 | ", | ||
200 | r" | ||
201 | pub fn reparse(&self, edit: &AtomTextEdit) -> File { | ||
202 | <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit)) | ||
203 | } | ||
204 | ", | ||
205 | ); | ||
206 | } | ||
207 | |||
208 | #[test] | ||
209 | fn test_join_lines_block() { | ||
210 | check_join_lines( | ||
211 | r" | ||
212 | fn foo() { | ||
213 | foo(<|>{ | ||
214 | 92 | ||
215 | }) | ||
216 | }", | ||
217 | r" | ||
218 | fn foo() { | ||
219 | foo(<|>92) | ||
220 | }", | ||
221 | ); | ||
222 | } | ||
223 | |||
224 | #[test] | ||
225 | fn join_lines_adds_comma_for_block_in_match_arm() { | ||
226 | check_join_lines( | ||
227 | r" | ||
228 | fn foo(e: Result<U, V>) { | ||
229 | match e { | ||
230 | Ok(u) => <|>{ | ||
231 | u.foo() | ||
232 | } | ||
233 | Err(v) => v, | ||
234 | } | ||
235 | }", | ||
236 | r" | ||
237 | fn foo(e: Result<U, V>) { | ||
238 | match e { | ||
239 | Ok(u) => <|>u.foo(), | ||
240 | Err(v) => v, | ||
241 | } | ||
242 | }", | ||
243 | ); | ||
244 | } | ||
245 | |||
246 | #[test] | ||
247 | fn join_lines_multiline_in_block() { | ||
248 | check_join_lines( | ||
249 | r" | ||
250 | fn foo() { | ||
251 | match ty { | ||
252 | <|> Some(ty) => { | ||
253 | match ty { | ||
254 | _ => false, | ||
255 | } | ||
256 | } | ||
257 | _ => true, | ||
258 | } | ||
259 | } | ||
260 | ", | ||
261 | r" | ||
262 | fn foo() { | ||
263 | match ty { | ||
264 | <|> Some(ty) => match ty { | ||
265 | _ => false, | ||
266 | }, | ||
267 | _ => true, | ||
268 | } | ||
269 | } | ||
270 | ", | ||
271 | ); | ||
272 | } | ||
273 | |||
274 | #[test] | ||
275 | fn join_lines_keeps_comma_for_block_in_match_arm() { | ||
276 | // We already have a comma | ||
277 | check_join_lines( | ||
278 | r" | ||
279 | fn foo(e: Result<U, V>) { | ||
280 | match e { | ||
281 | Ok(u) => <|>{ | ||
282 | u.foo() | ||
283 | }, | ||
284 | Err(v) => v, | ||
285 | } | ||
286 | }", | ||
287 | r" | ||
288 | fn foo(e: Result<U, V>) { | ||
289 | match e { | ||
290 | Ok(u) => <|>u.foo(), | ||
291 | Err(v) => v, | ||
292 | } | ||
293 | }", | ||
294 | ); | ||
295 | |||
296 | // comma with whitespace between brace and , | ||
297 | check_join_lines( | ||
298 | r" | ||
299 | fn foo(e: Result<U, V>) { | ||
300 | match e { | ||
301 | Ok(u) => <|>{ | ||
302 | u.foo() | ||
303 | } , | ||
304 | Err(v) => v, | ||
305 | } | ||
306 | }", | ||
307 | r" | ||
308 | fn foo(e: Result<U, V>) { | ||
309 | match e { | ||
310 | Ok(u) => <|>u.foo() , | ||
311 | Err(v) => v, | ||
312 | } | ||
313 | }", | ||
314 | ); | ||
315 | |||
316 | // comma with newline between brace and , | ||
317 | check_join_lines( | ||
318 | r" | ||
319 | fn foo(e: Result<U, V>) { | ||
320 | match e { | ||
321 | Ok(u) => <|>{ | ||
322 | u.foo() | ||
323 | } | ||
324 | , | ||
325 | Err(v) => v, | ||
326 | } | ||
327 | }", | ||
328 | r" | ||
329 | fn foo(e: Result<U, V>) { | ||
330 | match e { | ||
331 | Ok(u) => <|>u.foo() | ||
332 | , | ||
333 | Err(v) => v, | ||
334 | } | ||
335 | }", | ||
336 | ); | ||
337 | } | ||
338 | |||
339 | #[test] | ||
340 | fn join_lines_keeps_comma_with_single_arg_tuple() { | ||
341 | // A single arg tuple | ||
342 | check_join_lines( | ||
343 | r" | ||
344 | fn foo() { | ||
345 | let x = (<|>{ | ||
346 | 4 | ||
347 | },); | ||
348 | }", | ||
349 | r" | ||
350 | fn foo() { | ||
351 | let x = (<|>4,); | ||
352 | }", | ||
353 | ); | ||
354 | |||
355 | // single arg tuple with whitespace between brace and comma | ||
356 | check_join_lines( | ||
357 | r" | ||
358 | fn foo() { | ||
359 | let x = (<|>{ | ||
360 | 4 | ||
361 | } ,); | ||
362 | }", | ||
363 | r" | ||
364 | fn foo() { | ||
365 | let x = (<|>4 ,); | ||
366 | }", | ||
367 | ); | ||
368 | |||
369 | // single arg tuple with newline between brace and comma | ||
370 | check_join_lines( | ||
371 | r" | ||
372 | fn foo() { | ||
373 | let x = (<|>{ | ||
374 | 4 | ||
375 | } | ||
376 | ,); | ||
377 | }", | ||
378 | r" | ||
379 | fn foo() { | ||
380 | let x = (<|>4 | ||
381 | ,); | ||
382 | }", | ||
383 | ); | ||
384 | } | ||
385 | |||
386 | #[test] | ||
387 | fn test_join_lines_use_items_left() { | ||
388 | // No space after the '{' | ||
389 | check_join_lines( | ||
390 | r" | ||
391 | <|>use ra_syntax::{ | ||
392 | TextUnit, TextRange, | ||
393 | };", | ||
394 | r" | ||
395 | <|>use ra_syntax::{TextUnit, TextRange, | ||
396 | };", | ||
397 | ); | ||
398 | } | ||
399 | |||
400 | #[test] | ||
401 | fn test_join_lines_use_items_right() { | ||
402 | // No space after the '}' | ||
403 | check_join_lines( | ||
404 | r" | ||
405 | use ra_syntax::{ | ||
406 | <|> TextUnit, TextRange | ||
407 | };", | ||
408 | r" | ||
409 | use ra_syntax::{ | ||
410 | <|> TextUnit, TextRange};", | ||
411 | ); | ||
412 | } | ||
413 | |||
414 | #[test] | ||
415 | fn test_join_lines_use_items_right_comma() { | ||
416 | // No space after the '}' | ||
417 | check_join_lines( | ||
418 | r" | ||
419 | use ra_syntax::{ | ||
420 | <|> TextUnit, TextRange, | ||
421 | };", | ||
422 | r" | ||
423 | use ra_syntax::{ | ||
424 | <|> TextUnit, TextRange};", | ||
425 | ); | ||
426 | } | ||
427 | |||
428 | #[test] | ||
429 | fn test_join_lines_use_tree() { | ||
430 | check_join_lines( | ||
431 | r" | ||
432 | use ra_syntax::{ | ||
433 | algo::<|>{ | ||
434 | find_token_at_offset, | ||
435 | }, | ||
436 | ast, | ||
437 | };", | ||
438 | r" | ||
439 | use ra_syntax::{ | ||
440 | algo::<|>find_token_at_offset, | ||
441 | ast, | ||
442 | };", | ||
443 | ); | ||
444 | } | ||
445 | |||
446 | #[test] | ||
447 | fn test_join_lines_normal_comments() { | ||
448 | check_join_lines( | ||
449 | r" | ||
450 | fn foo() { | ||
451 | // Hello<|> | ||
452 | // world! | ||
453 | } | ||
454 | ", | ||
455 | r" | ||
456 | fn foo() { | ||
457 | // Hello<|> world! | ||
458 | } | ||
459 | ", | ||
460 | ); | ||
461 | } | ||
462 | |||
463 | #[test] | ||
464 | fn test_join_lines_doc_comments() { | ||
465 | check_join_lines( | ||
466 | r" | ||
467 | fn foo() { | ||
468 | /// Hello<|> | ||
469 | /// world! | ||
470 | } | ||
471 | ", | ||
472 | r" | ||
473 | fn foo() { | ||
474 | /// Hello<|> world! | ||
475 | } | ||
476 | ", | ||
477 | ); | ||
478 | } | ||
479 | |||
480 | #[test] | ||
481 | fn test_join_lines_mod_comments() { | ||
482 | check_join_lines( | ||
483 | r" | ||
484 | fn foo() { | ||
485 | //! Hello<|> | ||
486 | //! world! | ||
487 | } | ||
488 | ", | ||
489 | r" | ||
490 | fn foo() { | ||
491 | //! Hello<|> world! | ||
492 | } | ||
493 | ", | ||
494 | ); | ||
495 | } | ||
496 | |||
497 | #[test] | ||
498 | fn test_join_lines_multiline_comments_1() { | ||
499 | check_join_lines( | ||
500 | r" | ||
501 | fn foo() { | ||
502 | // Hello<|> | ||
503 | /* world! */ | ||
504 | } | ||
505 | ", | ||
506 | r" | ||
507 | fn foo() { | ||
508 | // Hello<|> world! */ | ||
509 | } | ||
510 | ", | ||
511 | ); | ||
512 | } | ||
513 | |||
514 | #[test] | ||
515 | fn test_join_lines_multiline_comments_2() { | ||
516 | check_join_lines( | ||
517 | r" | ||
518 | fn foo() { | ||
519 | // The<|> | ||
520 | /* quick | ||
521 | brown | ||
522 | fox! */ | ||
523 | } | ||
524 | ", | ||
525 | r" | ||
526 | fn foo() { | ||
527 | // The<|> quick | ||
528 | brown | ||
529 | fox! */ | ||
530 | } | ||
531 | ", | ||
532 | ); | ||
533 | } | ||
534 | |||
535 | fn check_join_lines_sel(before: &str, after: &str) { | ||
536 | let (sel, before) = extract_range(before); | ||
537 | let parse = SourceFile::parse(&before); | ||
538 | let result = join_lines(&parse.tree(), sel); | ||
539 | let actual = result.apply(&before); | ||
540 | assert_eq_text!(after, &actual); | ||
541 | } | ||
542 | |||
543 | #[test] | ||
544 | fn test_join_lines_selection_fn_args() { | ||
545 | check_join_lines_sel( | ||
546 | r" | ||
547 | fn foo() { | ||
548 | <|>foo(1, | ||
549 | 2, | ||
550 | 3, | ||
551 | <|>) | ||
552 | } | ||
553 | ", | ||
554 | r" | ||
555 | fn foo() { | ||
556 | foo(1, 2, 3) | ||
557 | } | ||
558 | ", | ||
559 | ); | ||
560 | } | ||
561 | |||
562 | #[test] | ||
563 | fn test_join_lines_selection_struct() { | ||
564 | check_join_lines_sel( | ||
565 | r" | ||
566 | struct Foo <|>{ | ||
567 | f: u32, | ||
568 | }<|> | ||
569 | ", | ||
570 | r" | ||
571 | struct Foo { f: u32 } | ||
572 | ", | ||
573 | ); | ||
574 | } | ||
575 | |||
576 | #[test] | ||
577 | fn test_join_lines_selection_dot_chain() { | ||
578 | check_join_lines_sel( | ||
579 | r" | ||
580 | fn foo() { | ||
581 | join(<|>type_params.type_params() | ||
582 | .filter_map(|it| it.name()) | ||
583 | .map(|it| it.text())<|>) | ||
584 | }", | ||
585 | r" | ||
586 | fn foo() { | ||
587 | join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text())) | ||
588 | }", | ||
589 | ); | ||
590 | } | ||
591 | |||
592 | #[test] | ||
593 | fn test_join_lines_selection_lambda_block_body() { | ||
594 | check_join_lines_sel( | ||
595 | r" | ||
596 | pub fn handle_find_matching_brace() { | ||
597 | params.offsets | ||
598 | .map(|offset| <|>{ | ||
599 | world.analysis().matching_brace(&file, offset).unwrap_or(offset) | ||
600 | }<|>) | ||
601 | .collect(); | ||
602 | }", | ||
603 | r" | ||
604 | pub fn handle_find_matching_brace() { | ||
605 | params.offsets | ||
606 | .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset)) | ||
607 | .collect(); | ||
608 | }", | ||
609 | ); | ||
610 | } | ||
611 | } | ||
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs new file mode 100644 index 000000000..d1bff4a76 --- /dev/null +++ b/crates/ra_ide/src/lib.rs | |||
@@ -0,0 +1,489 @@ | |||
1 | //! ra_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 `ra_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 | mod db; | ||
14 | pub mod mock_analysis; | ||
15 | mod symbol_index; | ||
16 | mod change; | ||
17 | mod source_change; | ||
18 | mod feature_flags; | ||
19 | |||
20 | mod status; | ||
21 | mod completion; | ||
22 | mod runnables; | ||
23 | mod goto_definition; | ||
24 | mod goto_type_definition; | ||
25 | mod extend_selection; | ||
26 | mod hover; | ||
27 | mod call_info; | ||
28 | mod syntax_highlighting; | ||
29 | mod parent_module; | ||
30 | mod references; | ||
31 | mod impls; | ||
32 | mod assists; | ||
33 | mod diagnostics; | ||
34 | mod syntax_tree; | ||
35 | mod folding_ranges; | ||
36 | mod line_index; | ||
37 | mod line_index_utils; | ||
38 | mod join_lines; | ||
39 | mod typing; | ||
40 | mod matching_brace; | ||
41 | mod display; | ||
42 | mod inlay_hints; | ||
43 | mod wasm_shims; | ||
44 | mod expand; | ||
45 | mod expand_macro; | ||
46 | |||
47 | #[cfg(test)] | ||
48 | mod marks; | ||
49 | #[cfg(test)] | ||
50 | mod test_utils; | ||
51 | |||
52 | use std::sync::Arc; | ||
53 | |||
54 | use ra_cfg::CfgOptions; | ||
55 | use ra_db::{ | ||
56 | salsa::{self, ParallelDatabase}, | ||
57 | CheckCanceled, Env, FileLoader, SourceDatabase, | ||
58 | }; | ||
59 | use ra_syntax::{SourceFile, TextRange, TextUnit}; | ||
60 | |||
61 | use crate::{db::LineIndexDatabase, display::ToNav, symbol_index::FileSymbol}; | ||
62 | |||
63 | pub use crate::{ | ||
64 | assists::{Assist, AssistId}, | ||
65 | change::{AnalysisChange, LibraryData}, | ||
66 | completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, | ||
67 | diagnostics::Severity, | ||
68 | display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, | ||
69 | expand_macro::ExpandedMacro, | ||
70 | feature_flags::FeatureFlags, | ||
71 | folding_ranges::{Fold, FoldKind}, | ||
72 | hover::HoverResult, | ||
73 | inlay_hints::{InlayHint, InlayKind}, | ||
74 | line_index::{LineCol, LineIndex}, | ||
75 | line_index_utils::translate_offset_with_edit, | ||
76 | references::{ReferenceSearchResult, SearchScope}, | ||
77 | runnables::{Runnable, RunnableKind}, | ||
78 | source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, | ||
79 | syntax_highlighting::HighlightedRange, | ||
80 | }; | ||
81 | |||
82 | pub use hir::Documentation; | ||
83 | pub use ra_db::{ | ||
84 | Canceled, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRootId, | ||
85 | }; | ||
86 | |||
87 | pub type Cancelable<T> = Result<T, Canceled>; | ||
88 | |||
89 | #[derive(Debug)] | ||
90 | pub struct Diagnostic { | ||
91 | pub message: String, | ||
92 | pub range: TextRange, | ||
93 | pub fix: Option<SourceChange>, | ||
94 | pub severity: Severity, | ||
95 | } | ||
96 | |||
97 | #[derive(Debug)] | ||
98 | pub struct Query { | ||
99 | query: String, | ||
100 | lowercased: String, | ||
101 | only_types: bool, | ||
102 | libs: bool, | ||
103 | exact: bool, | ||
104 | limit: usize, | ||
105 | } | ||
106 | |||
107 | impl Query { | ||
108 | pub fn new(query: String) -> Query { | ||
109 | let lowercased = query.to_lowercase(); | ||
110 | Query { | ||
111 | query, | ||
112 | lowercased, | ||
113 | only_types: false, | ||
114 | libs: false, | ||
115 | exact: false, | ||
116 | limit: usize::max_value(), | ||
117 | } | ||
118 | } | ||
119 | |||
120 | pub fn only_types(&mut self) { | ||
121 | self.only_types = true; | ||
122 | } | ||
123 | |||
124 | pub fn libs(&mut self) { | ||
125 | self.libs = true; | ||
126 | } | ||
127 | |||
128 | pub fn exact(&mut self) { | ||
129 | self.exact = true; | ||
130 | } | ||
131 | |||
132 | pub fn limit(&mut self, limit: usize) { | ||
133 | self.limit = limit | ||
134 | } | ||
135 | } | ||
136 | |||
137 | /// Info associated with a text range. | ||
138 | #[derive(Debug)] | ||
139 | pub struct RangeInfo<T> { | ||
140 | pub range: TextRange, | ||
141 | pub info: T, | ||
142 | } | ||
143 | |||
144 | impl<T> RangeInfo<T> { | ||
145 | pub fn new(range: TextRange, info: T) -> RangeInfo<T> { | ||
146 | RangeInfo { range, info } | ||
147 | } | ||
148 | } | ||
149 | |||
150 | /// Contains information about a call site. Specifically the | ||
151 | /// `FunctionSignature`and current parameter. | ||
152 | #[derive(Debug)] | ||
153 | pub struct CallInfo { | ||
154 | pub signature: FunctionSignature, | ||
155 | pub active_parameter: Option<usize>, | ||
156 | } | ||
157 | |||
158 | /// `AnalysisHost` stores the current state of the world. | ||
159 | #[derive(Debug)] | ||
160 | pub struct AnalysisHost { | ||
161 | db: db::RootDatabase, | ||
162 | } | ||
163 | |||
164 | impl Default for AnalysisHost { | ||
165 | fn default() -> AnalysisHost { | ||
166 | AnalysisHost::new(None, FeatureFlags::default()) | ||
167 | } | ||
168 | } | ||
169 | |||
170 | impl AnalysisHost { | ||
171 | pub fn new(lru_capcity: Option<usize>, feature_flags: FeatureFlags) -> AnalysisHost { | ||
172 | AnalysisHost { db: db::RootDatabase::new(lru_capcity, feature_flags) } | ||
173 | } | ||
174 | /// Returns a snapshot of the current state, which you can query for | ||
175 | /// semantic information. | ||
176 | pub fn analysis(&self) -> Analysis { | ||
177 | Analysis { db: self.db.snapshot() } | ||
178 | } | ||
179 | |||
180 | pub fn feature_flags(&self) -> &FeatureFlags { | ||
181 | &self.db.feature_flags | ||
182 | } | ||
183 | |||
184 | /// Applies changes to the current state of the world. If there are | ||
185 | /// outstanding snapshots, they will be canceled. | ||
186 | pub fn apply_change(&mut self, change: AnalysisChange) { | ||
187 | self.db.apply_change(change) | ||
188 | } | ||
189 | |||
190 | pub fn maybe_collect_garbage(&mut self) { | ||
191 | self.db.maybe_collect_garbage(); | ||
192 | } | ||
193 | |||
194 | pub fn collect_garbage(&mut self) { | ||
195 | self.db.collect_garbage(); | ||
196 | } | ||
197 | /// NB: this clears the database | ||
198 | pub fn per_query_memory_usage(&mut self) -> Vec<(String, ra_prof::Bytes)> { | ||
199 | self.db.per_query_memory_usage() | ||
200 | } | ||
201 | pub fn raw_database( | ||
202 | &self, | ||
203 | ) -> &(impl hir::db::HirDatabase + salsa::Database + ra_db::SourceDatabaseExt) { | ||
204 | &self.db | ||
205 | } | ||
206 | pub fn raw_database_mut( | ||
207 | &mut self, | ||
208 | ) -> &mut (impl hir::db::HirDatabase + salsa::Database + ra_db::SourceDatabaseExt) { | ||
209 | &mut self.db | ||
210 | } | ||
211 | } | ||
212 | |||
213 | /// Analysis is a snapshot of a world state at a moment in time. It is the main | ||
214 | /// entry point for asking semantic information about the world. When the world | ||
215 | /// state is advanced using `AnalysisHost::apply_change` method, all existing | ||
216 | /// `Analysis` are canceled (most method return `Err(Canceled)`). | ||
217 | #[derive(Debug)] | ||
218 | pub struct Analysis { | ||
219 | db: salsa::Snapshot<db::RootDatabase>, | ||
220 | } | ||
221 | |||
222 | // As a general design guideline, `Analysis` API are intended to be independent | ||
223 | // from the language server protocol. That is, when exposing some functionality | ||
224 | // we should think in terms of "what API makes most sense" and not in terms of | ||
225 | // "what types LSP uses". Although currently LSP is the only consumer of the | ||
226 | // API, the API should in theory be usable as a library, or via a different | ||
227 | // protocol. | ||
228 | impl Analysis { | ||
229 | // Creates an analysis instance for a single file, without any extenal | ||
230 | // dependencies, stdlib support or ability to apply changes. See | ||
231 | // `AnalysisHost` for creating a fully-featured analysis. | ||
232 | pub fn from_single_file(text: String) -> (Analysis, FileId) { | ||
233 | let mut host = AnalysisHost::default(); | ||
234 | let source_root = SourceRootId(0); | ||
235 | let mut change = AnalysisChange::new(); | ||
236 | change.add_root(source_root, true); | ||
237 | let mut crate_graph = CrateGraph::default(); | ||
238 | let file_id = FileId(0); | ||
239 | // FIXME: cfg options | ||
240 | // Default to enable test for single file. | ||
241 | let mut cfg_options = CfgOptions::default(); | ||
242 | cfg_options.insert_atom("test".into()); | ||
243 | crate_graph.add_crate_root(file_id, Edition::Edition2018, cfg_options, Env::default()); | ||
244 | change.add_file(source_root, file_id, "main.rs".into(), Arc::new(text)); | ||
245 | change.set_crate_graph(crate_graph); | ||
246 | host.apply_change(change); | ||
247 | (host.analysis(), file_id) | ||
248 | } | ||
249 | |||
250 | /// Features for Analysis. | ||
251 | pub fn feature_flags(&self) -> &FeatureFlags { | ||
252 | &self.db.feature_flags | ||
253 | } | ||
254 | |||
255 | /// Debug info about the current state of the analysis. | ||
256 | pub fn status(&self) -> Cancelable<String> { | ||
257 | self.with_db(|db| status::status(&*db)) | ||
258 | } | ||
259 | |||
260 | /// Gets the text of the source file. | ||
261 | pub fn file_text(&self, file_id: FileId) -> Cancelable<Arc<String>> { | ||
262 | self.with_db(|db| db.file_text(file_id)) | ||
263 | } | ||
264 | |||
265 | /// Gets the syntax tree of the file. | ||
266 | pub fn parse(&self, file_id: FileId) -> Cancelable<SourceFile> { | ||
267 | self.with_db(|db| db.parse(file_id).tree()) | ||
268 | } | ||
269 | |||
270 | /// Gets the file's `LineIndex`: data structure to convert between absolute | ||
271 | /// offsets and line/column representation. | ||
272 | pub fn file_line_index(&self, file_id: FileId) -> Cancelable<Arc<LineIndex>> { | ||
273 | self.with_db(|db| db.line_index(file_id)) | ||
274 | } | ||
275 | |||
276 | /// Selects the next syntactic nodes encompassing the range. | ||
277 | pub fn extend_selection(&self, frange: FileRange) -> Cancelable<TextRange> { | ||
278 | self.with_db(|db| extend_selection::extend_selection(db, frange)) | ||
279 | } | ||
280 | |||
281 | /// Returns position of the matching brace (all types of braces are | ||
282 | /// supported). | ||
283 | pub fn matching_brace(&self, position: FilePosition) -> Cancelable<Option<TextUnit>> { | ||
284 | self.with_db(|db| { | ||
285 | let parse = db.parse(position.file_id); | ||
286 | let file = parse.tree(); | ||
287 | matching_brace::matching_brace(&file, position.offset) | ||
288 | }) | ||
289 | } | ||
290 | |||
291 | /// Returns a syntax tree represented as `String`, for debug purposes. | ||
292 | // FIXME: use a better name here. | ||
293 | pub fn syntax_tree( | ||
294 | &self, | ||
295 | file_id: FileId, | ||
296 | text_range: Option<TextRange>, | ||
297 | ) -> Cancelable<String> { | ||
298 | self.with_db(|db| syntax_tree::syntax_tree(&db, file_id, text_range)) | ||
299 | } | ||
300 | |||
301 | pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<ExpandedMacro>> { | ||
302 | self.with_db(|db| expand_macro::expand_macro(db, position)) | ||
303 | } | ||
304 | |||
305 | /// Returns an edit to remove all newlines in the range, cleaning up minor | ||
306 | /// stuff like trailing commas. | ||
307 | pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> { | ||
308 | self.with_db(|db| { | ||
309 | let parse = db.parse(frange.file_id); | ||
310 | let file_edit = SourceFileEdit { | ||
311 | file_id: frange.file_id, | ||
312 | edit: join_lines::join_lines(&parse.tree(), frange.range), | ||
313 | }; | ||
314 | SourceChange::source_file_edit("join lines", file_edit) | ||
315 | }) | ||
316 | } | ||
317 | |||
318 | /// Returns an edit which should be applied when opening a new line, fixing | ||
319 | /// up minor stuff like continuing the comment. | ||
320 | pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { | ||
321 | self.with_db(|db| typing::on_enter(&db, position)) | ||
322 | } | ||
323 | |||
324 | /// Returns an edit which should be applied after a character was typed. | ||
325 | /// | ||
326 | /// This is useful for some on-the-fly fixups, like adding `;` to `let =` | ||
327 | /// automatically. | ||
328 | pub fn on_char_typed( | ||
329 | &self, | ||
330 | position: FilePosition, | ||
331 | char_typed: char, | ||
332 | ) -> Cancelable<Option<SourceChange>> { | ||
333 | // Fast path to not even parse the file. | ||
334 | if !typing::TRIGGER_CHARS.contains(char_typed) { | ||
335 | return Ok(None); | ||
336 | } | ||
337 | self.with_db(|db| typing::on_char_typed(&db, position, char_typed)) | ||
338 | } | ||
339 | |||
340 | /// Returns a tree representation of symbols in the file. Useful to draw a | ||
341 | /// file outline. | ||
342 | pub fn file_structure(&self, file_id: FileId) -> Cancelable<Vec<StructureNode>> { | ||
343 | self.with_db(|db| file_structure(&db.parse(file_id).tree())) | ||
344 | } | ||
345 | |||
346 | /// Returns a list of the places in the file where type hints can be displayed. | ||
347 | pub fn inlay_hints( | ||
348 | &self, | ||
349 | file_id: FileId, | ||
350 | max_inlay_hint_length: Option<usize>, | ||
351 | ) -> Cancelable<Vec<InlayHint>> { | ||
352 | self.with_db(|db| { | ||
353 | inlay_hints::inlay_hints(db, file_id, &db.parse(file_id).tree(), max_inlay_hint_length) | ||
354 | }) | ||
355 | } | ||
356 | |||
357 | /// Returns the set of folding ranges. | ||
358 | pub fn folding_ranges(&self, file_id: FileId) -> Cancelable<Vec<Fold>> { | ||
359 | self.with_db(|db| folding_ranges::folding_ranges(&db.parse(file_id).tree())) | ||
360 | } | ||
361 | |||
362 | /// Fuzzy searches for a symbol. | ||
363 | pub fn symbol_search(&self, query: Query) -> Cancelable<Vec<NavigationTarget>> { | ||
364 | self.with_db(|db| { | ||
365 | symbol_index::world_symbols(db, query) | ||
366 | .into_iter() | ||
367 | .map(|s| s.to_nav(db)) | ||
368 | .collect::<Vec<_>>() | ||
369 | }) | ||
370 | } | ||
371 | |||
372 | /// Returns the definitions from the symbol at `position`. | ||
373 | pub fn goto_definition( | ||
374 | &self, | ||
375 | position: FilePosition, | ||
376 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
377 | self.with_db(|db| goto_definition::goto_definition(db, position)) | ||
378 | } | ||
379 | |||
380 | /// Returns the impls from the symbol at `position`. | ||
381 | pub fn goto_implementation( | ||
382 | &self, | ||
383 | position: FilePosition, | ||
384 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
385 | self.with_db(|db| impls::goto_implementation(db, position)) | ||
386 | } | ||
387 | |||
388 | /// Returns the type definitions for the symbol at `position`. | ||
389 | pub fn goto_type_definition( | ||
390 | &self, | ||
391 | position: FilePosition, | ||
392 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
393 | self.with_db(|db| goto_type_definition::goto_type_definition(db, position)) | ||
394 | } | ||
395 | |||
396 | /// Finds all usages of the reference at point. | ||
397 | pub fn find_all_refs( | ||
398 | &self, | ||
399 | position: FilePosition, | ||
400 | search_scope: Option<SearchScope>, | ||
401 | ) -> Cancelable<Option<ReferenceSearchResult>> { | ||
402 | self.with_db(|db| references::find_all_refs(db, position, search_scope).map(|it| it.info)) | ||
403 | } | ||
404 | |||
405 | /// Returns a short text describing element at position. | ||
406 | pub fn hover(&self, position: FilePosition) -> Cancelable<Option<RangeInfo<HoverResult>>> { | ||
407 | self.with_db(|db| hover::hover(db, position)) | ||
408 | } | ||
409 | |||
410 | /// Computes parameter information for the given call expression. | ||
411 | pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> { | ||
412 | self.with_db(|db| call_info::call_info(db, position)) | ||
413 | } | ||
414 | |||
415 | /// Returns a `mod name;` declaration which created the current module. | ||
416 | pub fn parent_module(&self, position: FilePosition) -> Cancelable<Vec<NavigationTarget>> { | ||
417 | self.with_db(|db| parent_module::parent_module(db, position)) | ||
418 | } | ||
419 | |||
420 | /// Returns crates this file belongs too. | ||
421 | pub fn crate_for(&self, file_id: FileId) -> Cancelable<Vec<CrateId>> { | ||
422 | self.with_db(|db| parent_module::crate_for(db, file_id)) | ||
423 | } | ||
424 | |||
425 | /// Returns the root file of the given crate. | ||
426 | pub fn crate_root(&self, crate_id: CrateId) -> Cancelable<FileId> { | ||
427 | self.with_db(|db| db.crate_graph().crate_root(crate_id)) | ||
428 | } | ||
429 | |||
430 | /// Returns the set of possible targets to run for the current file. | ||
431 | pub fn runnables(&self, file_id: FileId) -> Cancelable<Vec<Runnable>> { | ||
432 | self.with_db(|db| runnables::runnables(db, file_id)) | ||
433 | } | ||
434 | |||
435 | /// Computes syntax highlighting for the given file. | ||
436 | pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HighlightedRange>> { | ||
437 | self.with_db(|db| syntax_highlighting::highlight(db, file_id)) | ||
438 | } | ||
439 | |||
440 | /// Computes syntax highlighting for the given file. | ||
441 | pub fn highlight_as_html(&self, file_id: FileId, rainbow: bool) -> Cancelable<String> { | ||
442 | self.with_db(|db| syntax_highlighting::highlight_as_html(db, file_id, rainbow)) | ||
443 | } | ||
444 | |||
445 | /// Computes completions at the given position. | ||
446 | pub fn completions(&self, position: FilePosition) -> Cancelable<Option<Vec<CompletionItem>>> { | ||
447 | self.with_db(|db| completion::completions(db, position).map(Into::into)) | ||
448 | } | ||
449 | |||
450 | /// Computes assists (aka code actions aka intentions) for the given | ||
451 | /// position. | ||
452 | pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<Assist>> { | ||
453 | self.with_db(|db| assists::assists(db, frange)) | ||
454 | } | ||
455 | |||
456 | /// Computes the set of diagnostics for the given file. | ||
457 | pub fn diagnostics(&self, file_id: FileId) -> Cancelable<Vec<Diagnostic>> { | ||
458 | self.with_db(|db| diagnostics::diagnostics(db, file_id)) | ||
459 | } | ||
460 | |||
461 | /// Computes the type of the expression at the given position. | ||
462 | pub fn type_of(&self, frange: FileRange) -> Cancelable<Option<String>> { | ||
463 | self.with_db(|db| hover::type_of(db, frange)) | ||
464 | } | ||
465 | |||
466 | /// Returns the edit required to rename reference at the position to the new | ||
467 | /// name. | ||
468 | pub fn rename( | ||
469 | &self, | ||
470 | position: FilePosition, | ||
471 | new_name: &str, | ||
472 | ) -> Cancelable<Option<RangeInfo<SourceChange>>> { | ||
473 | self.with_db(|db| references::rename(db, position, new_name)) | ||
474 | } | ||
475 | |||
476 | /// Performs an operation on that may be Canceled. | ||
477 | fn with_db<F: FnOnce(&db::RootDatabase) -> T + std::panic::UnwindSafe, T>( | ||
478 | &self, | ||
479 | f: F, | ||
480 | ) -> Cancelable<T> { | ||
481 | self.db.catch_canceled(f) | ||
482 | } | ||
483 | } | ||
484 | |||
485 | #[test] | ||
486 | fn analysis_is_send() { | ||
487 | fn is_send<T: Send>() {} | ||
488 | is_send::<Analysis>(); | ||
489 | } | ||
diff --git a/crates/ra_ide/src/line_index.rs b/crates/ra_ide/src/line_index.rs new file mode 100644 index 000000000..710890d27 --- /dev/null +++ b/crates/ra_ide/src/line_index.rs | |||
@@ -0,0 +1,283 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::TextUnit; | ||
4 | use rustc_hash::FxHashMap; | ||
5 | use superslice::Ext; | ||
6 | |||
7 | #[derive(Clone, Debug, PartialEq, Eq)] | ||
8 | pub struct LineIndex { | ||
9 | pub(crate) newlines: Vec<TextUnit>, | ||
10 | pub(crate) utf16_lines: FxHashMap<u32, Vec<Utf16Char>>, | ||
11 | } | ||
12 | |||
13 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
14 | pub struct LineCol { | ||
15 | /// Zero-based | ||
16 | pub line: u32, | ||
17 | /// Zero-based | ||
18 | pub col_utf16: u32, | ||
19 | } | ||
20 | |||
21 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] | ||
22 | pub(crate) struct Utf16Char { | ||
23 | pub(crate) start: TextUnit, | ||
24 | pub(crate) end: TextUnit, | ||
25 | } | ||
26 | |||
27 | impl Utf16Char { | ||
28 | fn len(&self) -> TextUnit { | ||
29 | self.end - self.start | ||
30 | } | ||
31 | } | ||
32 | |||
33 | impl LineIndex { | ||
34 | pub fn new(text: &str) -> LineIndex { | ||
35 | let mut utf16_lines = FxHashMap::default(); | ||
36 | let mut utf16_chars = Vec::new(); | ||
37 | |||
38 | let mut newlines = vec![0.into()]; | ||
39 | let mut curr_row = 0.into(); | ||
40 | let mut curr_col = 0.into(); | ||
41 | let mut line = 0; | ||
42 | for c in text.chars() { | ||
43 | curr_row += TextUnit::of_char(c); | ||
44 | if c == '\n' { | ||
45 | newlines.push(curr_row); | ||
46 | |||
47 | // Save any utf-16 characters seen in the previous line | ||
48 | if !utf16_chars.is_empty() { | ||
49 | utf16_lines.insert(line, utf16_chars); | ||
50 | utf16_chars = Vec::new(); | ||
51 | } | ||
52 | |||
53 | // Prepare for processing the next line | ||
54 | curr_col = 0.into(); | ||
55 | line += 1; | ||
56 | continue; | ||
57 | } | ||
58 | |||
59 | let char_len = TextUnit::of_char(c); | ||
60 | if char_len.to_usize() > 1 { | ||
61 | utf16_chars.push(Utf16Char { start: curr_col, end: curr_col + char_len }); | ||
62 | } | ||
63 | |||
64 | curr_col += char_len; | ||
65 | } | ||
66 | |||
67 | // Save any utf-16 characters seen in the last line | ||
68 | if !utf16_chars.is_empty() { | ||
69 | utf16_lines.insert(line, utf16_chars); | ||
70 | } | ||
71 | |||
72 | LineIndex { newlines, utf16_lines } | ||
73 | } | ||
74 | |||
75 | pub fn line_col(&self, offset: TextUnit) -> LineCol { | ||
76 | let line = self.newlines.upper_bound(&offset) - 1; | ||
77 | let line_start_offset = self.newlines[line]; | ||
78 | let col = offset - line_start_offset; | ||
79 | |||
80 | LineCol { line: line as u32, col_utf16: self.utf8_to_utf16_col(line as u32, col) as u32 } | ||
81 | } | ||
82 | |||
83 | pub fn offset(&self, line_col: LineCol) -> TextUnit { | ||
84 | //FIXME: return Result | ||
85 | let col = self.utf16_to_utf8_col(line_col.line, line_col.col_utf16); | ||
86 | self.newlines[line_col.line as usize] + col | ||
87 | } | ||
88 | |||
89 | fn utf8_to_utf16_col(&self, line: u32, mut col: TextUnit) -> usize { | ||
90 | if let Some(utf16_chars) = self.utf16_lines.get(&line) { | ||
91 | let mut correction = TextUnit::from_usize(0); | ||
92 | for c in utf16_chars { | ||
93 | if col >= c.end { | ||
94 | correction += c.len() - TextUnit::from_usize(1); | ||
95 | } else { | ||
96 | // From here on, all utf16 characters come *after* the character we are mapping, | ||
97 | // so we don't need to take them into account | ||
98 | break; | ||
99 | } | ||
100 | } | ||
101 | |||
102 | col -= correction; | ||
103 | } | ||
104 | |||
105 | col.to_usize() | ||
106 | } | ||
107 | |||
108 | fn utf16_to_utf8_col(&self, line: u32, col: u32) -> TextUnit { | ||
109 | let mut col: TextUnit = col.into(); | ||
110 | if let Some(utf16_chars) = self.utf16_lines.get(&line) { | ||
111 | for c in utf16_chars { | ||
112 | if col >= c.start { | ||
113 | col += c.len() - TextUnit::from_usize(1); | ||
114 | } else { | ||
115 | // From here on, all utf16 characters come *after* the character we are mapping, | ||
116 | // so we don't need to take them into account | ||
117 | break; | ||
118 | } | ||
119 | } | ||
120 | } | ||
121 | |||
122 | col | ||
123 | } | ||
124 | } | ||
125 | |||
126 | #[cfg(test)] | ||
127 | /// Simple reference implementation to use in proptests | ||
128 | pub fn to_line_col(text: &str, offset: TextUnit) -> LineCol { | ||
129 | let mut res = LineCol { line: 0, col_utf16: 0 }; | ||
130 | for (i, c) in text.char_indices() { | ||
131 | if i + c.len_utf8() > offset.to_usize() { | ||
132 | // if it's an invalid offset, inside a multibyte char | ||
133 | // return as if it was at the start of the char | ||
134 | break; | ||
135 | } | ||
136 | if c == '\n' { | ||
137 | res.line += 1; | ||
138 | res.col_utf16 = 0; | ||
139 | } else { | ||
140 | res.col_utf16 += 1; | ||
141 | } | ||
142 | } | ||
143 | res | ||
144 | } | ||
145 | |||
146 | #[cfg(test)] | ||
147 | mod test_line_index { | ||
148 | use super::*; | ||
149 | use proptest::{prelude::*, proptest}; | ||
150 | use ra_text_edit::test_utils::{arb_offset, arb_text}; | ||
151 | |||
152 | #[test] | ||
153 | fn test_line_index() { | ||
154 | let text = "hello\nworld"; | ||
155 | let index = LineIndex::new(text); | ||
156 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col_utf16: 0 }); | ||
157 | assert_eq!(index.line_col(1.into()), LineCol { line: 0, col_utf16: 1 }); | ||
158 | assert_eq!(index.line_col(5.into()), LineCol { line: 0, col_utf16: 5 }); | ||
159 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col_utf16: 0 }); | ||
160 | assert_eq!(index.line_col(7.into()), LineCol { line: 1, col_utf16: 1 }); | ||
161 | assert_eq!(index.line_col(8.into()), LineCol { line: 1, col_utf16: 2 }); | ||
162 | assert_eq!(index.line_col(10.into()), LineCol { line: 1, col_utf16: 4 }); | ||
163 | assert_eq!(index.line_col(11.into()), LineCol { line: 1, col_utf16: 5 }); | ||
164 | assert_eq!(index.line_col(12.into()), LineCol { line: 1, col_utf16: 6 }); | ||
165 | |||
166 | let text = "\nhello\nworld"; | ||
167 | let index = LineIndex::new(text); | ||
168 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col_utf16: 0 }); | ||
169 | assert_eq!(index.line_col(1.into()), LineCol { line: 1, col_utf16: 0 }); | ||
170 | assert_eq!(index.line_col(2.into()), LineCol { line: 1, col_utf16: 1 }); | ||
171 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col_utf16: 5 }); | ||
172 | assert_eq!(index.line_col(7.into()), LineCol { line: 2, col_utf16: 0 }); | ||
173 | } | ||
174 | |||
175 | fn arb_text_with_offset() -> BoxedStrategy<(TextUnit, String)> { | ||
176 | arb_text().prop_flat_map(|text| (arb_offset(&text), Just(text))).boxed() | ||
177 | } | ||
178 | |||
179 | fn to_line_col(text: &str, offset: TextUnit) -> LineCol { | ||
180 | let mut res = LineCol { line: 0, col_utf16: 0 }; | ||
181 | for (i, c) in text.char_indices() { | ||
182 | if i + c.len_utf8() > offset.to_usize() { | ||
183 | // if it's an invalid offset, inside a multibyte char | ||
184 | // return as if it was at the start of the char | ||
185 | break; | ||
186 | } | ||
187 | if c == '\n' { | ||
188 | res.line += 1; | ||
189 | res.col_utf16 = 0; | ||
190 | } else { | ||
191 | res.col_utf16 += 1; | ||
192 | } | ||
193 | } | ||
194 | res | ||
195 | } | ||
196 | |||
197 | proptest! { | ||
198 | #[test] | ||
199 | fn test_line_index_proptest((offset, text) in arb_text_with_offset()) { | ||
200 | let expected = to_line_col(&text, offset); | ||
201 | let line_index = LineIndex::new(&text); | ||
202 | let actual = line_index.line_col(offset); | ||
203 | |||
204 | assert_eq!(actual, expected); | ||
205 | } | ||
206 | } | ||
207 | } | ||
208 | |||
209 | #[cfg(test)] | ||
210 | mod test_utf8_utf16_conv { | ||
211 | use super::*; | ||
212 | |||
213 | #[test] | ||
214 | fn test_char_len() { | ||
215 | assert_eq!('メ'.len_utf8(), 3); | ||
216 | assert_eq!('メ'.len_utf16(), 1); | ||
217 | } | ||
218 | |||
219 | #[test] | ||
220 | fn test_empty_index() { | ||
221 | let col_index = LineIndex::new( | ||
222 | " | ||
223 | const C: char = 'x'; | ||
224 | ", | ||
225 | ); | ||
226 | assert_eq!(col_index.utf16_lines.len(), 0); | ||
227 | } | ||
228 | |||
229 | #[test] | ||
230 | fn test_single_char() { | ||
231 | let col_index = LineIndex::new( | ||
232 | " | ||
233 | const C: char = 'メ'; | ||
234 | ", | ||
235 | ); | ||
236 | |||
237 | assert_eq!(col_index.utf16_lines.len(), 1); | ||
238 | assert_eq!(col_index.utf16_lines[&1].len(), 1); | ||
239 | assert_eq!(col_index.utf16_lines[&1][0], Utf16Char { start: 17.into(), end: 20.into() }); | ||
240 | |||
241 | // UTF-8 to UTF-16, no changes | ||
242 | assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); | ||
243 | |||
244 | // UTF-8 to UTF-16 | ||
245 | assert_eq!(col_index.utf8_to_utf16_col(1, 22.into()), 20); | ||
246 | |||
247 | // UTF-16 to UTF-8, no changes | ||
248 | assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from(15)); | ||
249 | |||
250 | // UTF-16 to UTF-8 | ||
251 | assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from(21)); | ||
252 | } | ||
253 | |||
254 | #[test] | ||
255 | fn test_string() { | ||
256 | let col_index = LineIndex::new( | ||
257 | " | ||
258 | const C: char = \"メ メ\"; | ||
259 | ", | ||
260 | ); | ||
261 | |||
262 | assert_eq!(col_index.utf16_lines.len(), 1); | ||
263 | assert_eq!(col_index.utf16_lines[&1].len(), 2); | ||
264 | assert_eq!(col_index.utf16_lines[&1][0], Utf16Char { start: 17.into(), end: 20.into() }); | ||
265 | assert_eq!(col_index.utf16_lines[&1][1], Utf16Char { start: 21.into(), end: 24.into() }); | ||
266 | |||
267 | // UTF-8 to UTF-16 | ||
268 | assert_eq!(col_index.utf8_to_utf16_col(1, 15.into()), 15); | ||
269 | |||
270 | assert_eq!(col_index.utf8_to_utf16_col(1, 21.into()), 19); | ||
271 | assert_eq!(col_index.utf8_to_utf16_col(1, 25.into()), 21); | ||
272 | |||
273 | assert!(col_index.utf8_to_utf16_col(2, 15.into()) == 15); | ||
274 | |||
275 | // UTF-16 to UTF-8 | ||
276 | assert_eq!(col_index.utf16_to_utf8_col(1, 15), TextUnit::from_usize(15)); | ||
277 | |||
278 | assert_eq!(col_index.utf16_to_utf8_col(1, 18), TextUnit::from_usize(20)); | ||
279 | assert_eq!(col_index.utf16_to_utf8_col(1, 19), TextUnit::from_usize(23)); | ||
280 | |||
281 | assert_eq!(col_index.utf16_to_utf8_col(2, 15), TextUnit::from_usize(15)); | ||
282 | } | ||
283 | } | ||
diff --git a/crates/ra_ide/src/line_index_utils.rs b/crates/ra_ide/src/line_index_utils.rs new file mode 100644 index 000000000..bd1e08feb --- /dev/null +++ b/crates/ra_ide/src/line_index_utils.rs | |||
@@ -0,0 +1,331 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::{line_index::Utf16Char, LineCol, LineIndex}; | ||
4 | use ra_syntax::{TextRange, TextUnit}; | ||
5 | use ra_text_edit::{AtomTextEdit, TextEdit}; | ||
6 | |||
7 | #[derive(Debug, Clone)] | ||
8 | enum Step { | ||
9 | Newline(TextUnit), | ||
10 | Utf16Char(TextRange), | ||
11 | } | ||
12 | |||
13 | #[derive(Debug)] | ||
14 | struct LineIndexStepIter<'a> { | ||
15 | line_index: &'a LineIndex, | ||
16 | next_newline_idx: usize, | ||
17 | utf16_chars: Option<(TextUnit, std::slice::Iter<'a, Utf16Char>)>, | ||
18 | } | ||
19 | |||
20 | impl<'a> LineIndexStepIter<'a> { | ||
21 | fn from(line_index: &LineIndex) -> LineIndexStepIter { | ||
22 | let mut x = LineIndexStepIter { line_index, next_newline_idx: 0, utf16_chars: None }; | ||
23 | // skip first newline since it's not real | ||
24 | x.next(); | ||
25 | x | ||
26 | } | ||
27 | } | ||
28 | |||
29 | impl<'a> Iterator for LineIndexStepIter<'a> { | ||
30 | type Item = Step; | ||
31 | fn next(&mut self) -> Option<Step> { | ||
32 | self.utf16_chars | ||
33 | .as_mut() | ||
34 | .and_then(|(newline, x)| { | ||
35 | let x = x.next()?; | ||
36 | Some(Step::Utf16Char(TextRange::from_to(*newline + x.start, *newline + x.end))) | ||
37 | }) | ||
38 | .or_else(|| { | ||
39 | let next_newline = *self.line_index.newlines.get(self.next_newline_idx)?; | ||
40 | self.utf16_chars = self | ||
41 | .line_index | ||
42 | .utf16_lines | ||
43 | .get(&(self.next_newline_idx as u32)) | ||
44 | .map(|x| (next_newline, x.iter())); | ||
45 | self.next_newline_idx += 1; | ||
46 | Some(Step::Newline(next_newline)) | ||
47 | }) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | #[derive(Debug)] | ||
52 | struct OffsetStepIter<'a> { | ||
53 | text: &'a str, | ||
54 | offset: TextUnit, | ||
55 | } | ||
56 | |||
57 | impl<'a> Iterator for OffsetStepIter<'a> { | ||
58 | type Item = Step; | ||
59 | fn next(&mut self) -> Option<Step> { | ||
60 | let (next, next_offset) = self | ||
61 | .text | ||
62 | .char_indices() | ||
63 | .filter_map(|(i, c)| { | ||
64 | if c == '\n' { | ||
65 | let next_offset = self.offset + TextUnit::from_usize(i + 1); | ||
66 | let next = Step::Newline(next_offset); | ||
67 | Some((next, next_offset)) | ||
68 | } else { | ||
69 | let char_len = TextUnit::of_char(c); | ||
70 | if char_len.to_usize() > 1 { | ||
71 | let start = self.offset + TextUnit::from_usize(i); | ||
72 | let end = start + char_len; | ||
73 | let next = Step::Utf16Char(TextRange::from_to(start, end)); | ||
74 | let next_offset = end; | ||
75 | Some((next, next_offset)) | ||
76 | } else { | ||
77 | None | ||
78 | } | ||
79 | } | ||
80 | }) | ||
81 | .next()?; | ||
82 | let next_idx = (next_offset - self.offset).to_usize(); | ||
83 | self.text = &self.text[next_idx..]; | ||
84 | self.offset = next_offset; | ||
85 | Some(next) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | #[derive(Debug)] | ||
90 | enum NextSteps<'a> { | ||
91 | Use, | ||
92 | ReplaceMany(OffsetStepIter<'a>), | ||
93 | AddMany(OffsetStepIter<'a>), | ||
94 | } | ||
95 | |||
96 | #[derive(Debug)] | ||
97 | struct TranslatedEdit<'a> { | ||
98 | delete: TextRange, | ||
99 | insert: &'a str, | ||
100 | diff: i64, | ||
101 | } | ||
102 | |||
103 | struct Edits<'a> { | ||
104 | edits: &'a [AtomTextEdit], | ||
105 | current: Option<TranslatedEdit<'a>>, | ||
106 | acc_diff: i64, | ||
107 | } | ||
108 | |||
109 | impl<'a> Edits<'a> { | ||
110 | fn from_text_edit(text_edit: &'a TextEdit) -> Edits<'a> { | ||
111 | let mut x = Edits { edits: text_edit.as_atoms(), current: None, acc_diff: 0 }; | ||
112 | x.advance_edit(); | ||
113 | x | ||
114 | } | ||
115 | fn advance_edit(&mut self) { | ||
116 | self.acc_diff += self.current.as_ref().map_or(0, |x| x.diff); | ||
117 | match self.edits.split_first() { | ||
118 | Some((next, rest)) => { | ||
119 | let delete = self.translate_range(next.delete); | ||
120 | let diff = next.insert.len() as i64 - next.delete.len().to_usize() as i64; | ||
121 | self.current = Some(TranslatedEdit { delete, insert: &next.insert, diff }); | ||
122 | self.edits = rest; | ||
123 | } | ||
124 | None => { | ||
125 | self.current = None; | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | fn next_inserted_steps(&mut self) -> Option<OffsetStepIter<'a>> { | ||
131 | let cur = self.current.as_ref()?; | ||
132 | let res = Some(OffsetStepIter { offset: cur.delete.start(), text: &cur.insert }); | ||
133 | self.advance_edit(); | ||
134 | res | ||
135 | } | ||
136 | |||
137 | fn next_steps(&mut self, step: &Step) -> NextSteps { | ||
138 | let step_pos = match *step { | ||
139 | Step::Newline(n) => n, | ||
140 | Step::Utf16Char(r) => r.end(), | ||
141 | }; | ||
142 | match &mut self.current { | ||
143 | Some(edit) => { | ||
144 | if step_pos <= edit.delete.start() { | ||
145 | NextSteps::Use | ||
146 | } else if step_pos <= edit.delete.end() { | ||
147 | let iter = OffsetStepIter { offset: edit.delete.start(), text: &edit.insert }; | ||
148 | // empty slice to avoid returning steps again | ||
149 | edit.insert = &edit.insert[edit.insert.len()..]; | ||
150 | NextSteps::ReplaceMany(iter) | ||
151 | } else { | ||
152 | let iter = OffsetStepIter { offset: edit.delete.start(), text: &edit.insert }; | ||
153 | // empty slice to avoid returning steps again | ||
154 | edit.insert = &edit.insert[edit.insert.len()..]; | ||
155 | self.advance_edit(); | ||
156 | NextSteps::AddMany(iter) | ||
157 | } | ||
158 | } | ||
159 | None => NextSteps::Use, | ||
160 | } | ||
161 | } | ||
162 | |||
163 | fn translate_range(&self, range: TextRange) -> TextRange { | ||
164 | if self.acc_diff == 0 { | ||
165 | range | ||
166 | } else { | ||
167 | let start = self.translate(range.start()); | ||
168 | let end = self.translate(range.end()); | ||
169 | TextRange::from_to(start, end) | ||
170 | } | ||
171 | } | ||
172 | |||
173 | fn translate(&self, x: TextUnit) -> TextUnit { | ||
174 | if self.acc_diff == 0 { | ||
175 | x | ||
176 | } else { | ||
177 | TextUnit::from((x.to_usize() as i64 + self.acc_diff) as u32) | ||
178 | } | ||
179 | } | ||
180 | |||
181 | fn translate_step(&self, x: &Step) -> Step { | ||
182 | if self.acc_diff == 0 { | ||
183 | x.clone() | ||
184 | } else { | ||
185 | match *x { | ||
186 | Step::Newline(n) => Step::Newline(self.translate(n)), | ||
187 | Step::Utf16Char(r) => Step::Utf16Char(self.translate_range(r)), | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | } | ||
192 | |||
193 | #[derive(Debug)] | ||
194 | struct RunningLineCol { | ||
195 | line: u32, | ||
196 | last_newline: TextUnit, | ||
197 | col_adjust: TextUnit, | ||
198 | } | ||
199 | |||
200 | impl RunningLineCol { | ||
201 | fn new() -> RunningLineCol { | ||
202 | RunningLineCol { line: 0, last_newline: TextUnit::from(0), col_adjust: TextUnit::from(0) } | ||
203 | } | ||
204 | |||
205 | fn to_line_col(&self, offset: TextUnit) -> LineCol { | ||
206 | LineCol { | ||
207 | line: self.line, | ||
208 | col_utf16: ((offset - self.last_newline) - self.col_adjust).into(), | ||
209 | } | ||
210 | } | ||
211 | |||
212 | fn add_line(&mut self, newline: TextUnit) { | ||
213 | self.line += 1; | ||
214 | self.last_newline = newline; | ||
215 | self.col_adjust = TextUnit::from(0); | ||
216 | } | ||
217 | |||
218 | fn adjust_col(&mut self, range: TextRange) { | ||
219 | self.col_adjust += range.len() - TextUnit::from(1); | ||
220 | } | ||
221 | } | ||
222 | |||
223 | pub fn translate_offset_with_edit( | ||
224 | line_index: &LineIndex, | ||
225 | offset: TextUnit, | ||
226 | text_edit: &TextEdit, | ||
227 | ) -> LineCol { | ||
228 | let mut state = Edits::from_text_edit(&text_edit); | ||
229 | |||
230 | let mut res = RunningLineCol::new(); | ||
231 | |||
232 | macro_rules! test_step { | ||
233 | ($x:ident) => { | ||
234 | match &$x { | ||
235 | Step::Newline(n) => { | ||
236 | if offset < *n { | ||
237 | return res.to_line_col(offset); | ||
238 | } else { | ||
239 | res.add_line(*n); | ||
240 | } | ||
241 | } | ||
242 | Step::Utf16Char(x) => { | ||
243 | if offset < x.end() { | ||
244 | // if the offset is inside a multibyte char it's invalid | ||
245 | // clamp it to the start of the char | ||
246 | let clamp = offset.min(x.start()); | ||
247 | return res.to_line_col(clamp); | ||
248 | } else { | ||
249 | res.adjust_col(*x); | ||
250 | } | ||
251 | } | ||
252 | } | ||
253 | }; | ||
254 | } | ||
255 | |||
256 | for orig_step in LineIndexStepIter::from(line_index) { | ||
257 | loop { | ||
258 | let translated_step = state.translate_step(&orig_step); | ||
259 | match state.next_steps(&translated_step) { | ||
260 | NextSteps::Use => { | ||
261 | test_step!(translated_step); | ||
262 | break; | ||
263 | } | ||
264 | NextSteps::ReplaceMany(ns) => { | ||
265 | for n in ns { | ||
266 | test_step!(n); | ||
267 | } | ||
268 | break; | ||
269 | } | ||
270 | NextSteps::AddMany(ns) => { | ||
271 | for n in ns { | ||
272 | test_step!(n); | ||
273 | } | ||
274 | } | ||
275 | } | ||
276 | } | ||
277 | } | ||
278 | |||
279 | loop { | ||
280 | match state.next_inserted_steps() { | ||
281 | None => break, | ||
282 | Some(ns) => { | ||
283 | for n in ns { | ||
284 | test_step!(n); | ||
285 | } | ||
286 | } | ||
287 | } | ||
288 | } | ||
289 | |||
290 | res.to_line_col(offset) | ||
291 | } | ||
292 | |||
293 | #[cfg(test)] | ||
294 | mod test { | ||
295 | use super::*; | ||
296 | use crate::line_index; | ||
297 | use proptest::{prelude::*, proptest}; | ||
298 | use ra_text_edit::test_utils::{arb_offset, arb_text_with_edit}; | ||
299 | use ra_text_edit::TextEdit; | ||
300 | |||
301 | #[derive(Debug)] | ||
302 | struct ArbTextWithEditAndOffset { | ||
303 | text: String, | ||
304 | edit: TextEdit, | ||
305 | edited_text: String, | ||
306 | offset: TextUnit, | ||
307 | } | ||
308 | |||
309 | fn arb_text_with_edit_and_offset() -> BoxedStrategy<ArbTextWithEditAndOffset> { | ||
310 | arb_text_with_edit() | ||
311 | .prop_flat_map(|x| { | ||
312 | let edited_text = x.edit.apply(&x.text); | ||
313 | let arb_offset = arb_offset(&edited_text); | ||
314 | (Just(x), Just(edited_text), arb_offset).prop_map(|(x, edited_text, offset)| { | ||
315 | ArbTextWithEditAndOffset { text: x.text, edit: x.edit, edited_text, offset } | ||
316 | }) | ||
317 | }) | ||
318 | .boxed() | ||
319 | } | ||
320 | |||
321 | proptest! { | ||
322 | #[test] | ||
323 | fn test_translate_offset_with_edit(x in arb_text_with_edit_and_offset()) { | ||
324 | let expected = line_index::to_line_col(&x.edited_text, x.offset); | ||
325 | let line_index = LineIndex::new(&x.text); | ||
326 | let actual = translate_offset_with_edit(&line_index, x.offset, &x.edit); | ||
327 | |||
328 | assert_eq!(actual, expected); | ||
329 | } | ||
330 | } | ||
331 | } | ||
diff --git a/crates/ra_ide/src/marks.rs b/crates/ra_ide/src/marks.rs new file mode 100644 index 000000000..848ae4dc7 --- /dev/null +++ b/crates/ra_ide/src/marks.rs | |||
@@ -0,0 +1,13 @@ | |||
1 | //! See test_utils/src/marks.rs | ||
2 | |||
3 | test_utils::marks!( | ||
4 | inserts_angle_brackets_for_generics | ||
5 | inserts_parens_for_function_calls | ||
6 | goto_definition_works_for_macros | ||
7 | goto_definition_works_for_methods | ||
8 | goto_definition_works_for_fields | ||
9 | goto_definition_works_for_record_fields | ||
10 | call_info_bad_offset | ||
11 | dont_complete_current_use | ||
12 | dont_complete_primitive_in_use | ||
13 | ); | ||
diff --git a/crates/ra_ide/src/matching_brace.rs b/crates/ra_ide/src/matching_brace.rs new file mode 100644 index 000000000..d1204fac0 --- /dev/null +++ b/crates/ra_ide/src/matching_brace.rs | |||
@@ -0,0 +1,43 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{ast::AstNode, SourceFile, SyntaxKind, TextUnit, T}; | ||
4 | |||
5 | pub fn matching_brace(file: &SourceFile, offset: TextUnit) -> Option<TextUnit> { | ||
6 | const BRACES: &[SyntaxKind] = | ||
7 | &[T!['{'], T!['}'], T!['['], T![']'], T!['('], T![')'], T![<], T![>]]; | ||
8 | let (brace_node, brace_idx) = file | ||
9 | .syntax() | ||
10 | .token_at_offset(offset) | ||
11 | .filter_map(|node| { | ||
12 | let idx = BRACES.iter().position(|&brace| brace == node.kind())?; | ||
13 | Some((node, idx)) | ||
14 | }) | ||
15 | .next()?; | ||
16 | let parent = brace_node.parent(); | ||
17 | let matching_kind = BRACES[brace_idx ^ 1]; | ||
18 | let matching_node = parent.children_with_tokens().find(|node| node.kind() == matching_kind)?; | ||
19 | Some(matching_node.text_range().start()) | ||
20 | } | ||
21 | |||
22 | #[cfg(test)] | ||
23 | mod tests { | ||
24 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | ||
25 | |||
26 | use super::*; | ||
27 | |||
28 | #[test] | ||
29 | fn test_matching_brace() { | ||
30 | fn do_check(before: &str, after: &str) { | ||
31 | let (pos, before) = extract_offset(before); | ||
32 | let parse = SourceFile::parse(&before); | ||
33 | let new_pos = match matching_brace(&parse.tree(), pos) { | ||
34 | None => pos, | ||
35 | Some(pos) => pos, | ||
36 | }; | ||
37 | let actual = add_cursor(&before, new_pos); | ||
38 | assert_eq_text!(after, &actual); | ||
39 | } | ||
40 | |||
41 | do_check("struct Foo { a: i32, }<|>", "struct Foo <|>{ a: i32, }"); | ||
42 | } | ||
43 | } | ||
diff --git a/crates/ra_ide/src/mock_analysis.rs b/crates/ra_ide/src/mock_analysis.rs new file mode 100644 index 000000000..bf8a54932 --- /dev/null +++ b/crates/ra_ide/src/mock_analysis.rs | |||
@@ -0,0 +1,149 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::sync::Arc; | ||
4 | |||
5 | use ra_cfg::CfgOptions; | ||
6 | use ra_db::{Env, RelativePathBuf}; | ||
7 | use test_utils::{extract_offset, extract_range, parse_fixture, CURSOR_MARKER}; | ||
8 | |||
9 | use crate::{ | ||
10 | Analysis, AnalysisChange, AnalysisHost, CrateGraph, Edition::Edition2018, FileId, FilePosition, | ||
11 | FileRange, SourceRootId, | ||
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<(String, String)>, | ||
19 | } | ||
20 | |||
21 | impl MockAnalysis { | ||
22 | pub fn new() -> MockAnalysis { | ||
23 | MockAnalysis::default() | ||
24 | } | ||
25 | /// Creates `MockAnalysis` using a fixture data in the following format: | ||
26 | /// | ||
27 | /// ```not_rust | ||
28 | /// //- /main.rs | ||
29 | /// mod foo; | ||
30 | /// fn main() {} | ||
31 | /// | ||
32 | /// //- /foo.rs | ||
33 | /// struct Baz; | ||
34 | /// ``` | ||
35 | pub fn with_files(fixture: &str) -> MockAnalysis { | ||
36 | let mut res = MockAnalysis::new(); | ||
37 | for entry in parse_fixture(fixture) { | ||
38 | res.add_file(&entry.meta, &entry.text); | ||
39 | } | ||
40 | res | ||
41 | } | ||
42 | |||
43 | /// Same as `with_files`, but requires that a single file contains a `<|>` marker, | ||
44 | /// whose position is also returned. | ||
45 | pub fn with_files_and_position(fixture: &str) -> (MockAnalysis, FilePosition) { | ||
46 | let mut position = None; | ||
47 | let mut res = MockAnalysis::new(); | ||
48 | for entry in parse_fixture(fixture) { | ||
49 | if entry.text.contains(CURSOR_MARKER) { | ||
50 | assert!(position.is_none(), "only one marker (<|>) per fixture is allowed"); | ||
51 | position = Some(res.add_file_with_position(&entry.meta, &entry.text)); | ||
52 | } else { | ||
53 | res.add_file(&entry.meta, &entry.text); | ||
54 | } | ||
55 | } | ||
56 | let position = position.expect("expected a marker (<|>)"); | ||
57 | (res, position) | ||
58 | } | ||
59 | |||
60 | pub fn add_file(&mut self, path: &str, text: &str) -> FileId { | ||
61 | let file_id = FileId((self.files.len() + 1) as u32); | ||
62 | self.files.push((path.to_string(), text.to_string())); | ||
63 | file_id | ||
64 | } | ||
65 | pub fn add_file_with_position(&mut self, path: &str, text: &str) -> FilePosition { | ||
66 | let (offset, text) = extract_offset(text); | ||
67 | let file_id = FileId((self.files.len() + 1) as u32); | ||
68 | self.files.push((path.to_string(), text)); | ||
69 | FilePosition { file_id, offset } | ||
70 | } | ||
71 | pub fn add_file_with_range(&mut self, path: &str, text: &str) -> FileRange { | ||
72 | let (range, text) = extract_range(text); | ||
73 | let file_id = FileId((self.files.len() + 1) as u32); | ||
74 | self.files.push((path.to_string(), text)); | ||
75 | FileRange { file_id, range } | ||
76 | } | ||
77 | pub fn id_of(&self, path: &str) -> FileId { | ||
78 | let (idx, _) = self | ||
79 | .files | ||
80 | .iter() | ||
81 | .enumerate() | ||
82 | .find(|(_, (p, _text))| path == p) | ||
83 | .expect("no file in this mock"); | ||
84 | FileId(idx as u32 + 1) | ||
85 | } | ||
86 | pub fn analysis_host(self) -> AnalysisHost { | ||
87 | let mut host = AnalysisHost::default(); | ||
88 | let source_root = SourceRootId(0); | ||
89 | let mut change = AnalysisChange::new(); | ||
90 | change.add_root(source_root, true); | ||
91 | let mut crate_graph = CrateGraph::default(); | ||
92 | let mut root_crate = None; | ||
93 | for (i, (path, contents)) in self.files.into_iter().enumerate() { | ||
94 | assert!(path.starts_with('/')); | ||
95 | let path = RelativePathBuf::from_path(&path[1..]).unwrap(); | ||
96 | let file_id = FileId(i as u32 + 1); | ||
97 | let cfg_options = CfgOptions::default(); | ||
98 | if path == "/lib.rs" || path == "/main.rs" { | ||
99 | root_crate = Some(crate_graph.add_crate_root( | ||
100 | file_id, | ||
101 | Edition2018, | ||
102 | cfg_options, | ||
103 | Env::default(), | ||
104 | )); | ||
105 | } else if path.ends_with("/lib.rs") { | ||
106 | let other_crate = | ||
107 | crate_graph.add_crate_root(file_id, Edition2018, cfg_options, Env::default()); | ||
108 | let crate_name = path.parent().unwrap().file_name().unwrap(); | ||
109 | if let Some(root_crate) = root_crate { | ||
110 | crate_graph.add_dep(root_crate, crate_name.into(), other_crate).unwrap(); | ||
111 | } | ||
112 | } | ||
113 | change.add_file(source_root, file_id, path, Arc::new(contents)); | ||
114 | } | ||
115 | change.set_crate_graph(crate_graph); | ||
116 | host.apply_change(change); | ||
117 | host | ||
118 | } | ||
119 | pub fn analysis(self) -> Analysis { | ||
120 | self.analysis_host().analysis() | ||
121 | } | ||
122 | } | ||
123 | |||
124 | /// Creates analysis from a multi-file fixture, returns positions marked with <|>. | ||
125 | pub fn analysis_and_position(fixture: &str) -> (Analysis, FilePosition) { | ||
126 | let (mock, position) = MockAnalysis::with_files_and_position(fixture); | ||
127 | (mock.analysis(), position) | ||
128 | } | ||
129 | |||
130 | /// Creates analysis for a single file. | ||
131 | pub fn single_file(code: &str) -> (Analysis, FileId) { | ||
132 | let mut mock = MockAnalysis::new(); | ||
133 | let file_id = mock.add_file("/main.rs", code); | ||
134 | (mock.analysis(), file_id) | ||
135 | } | ||
136 | |||
137 | /// Creates analysis for a single file, returns position marked with <|>. | ||
138 | pub fn single_file_with_position(code: &str) -> (Analysis, FilePosition) { | ||
139 | let mut mock = MockAnalysis::new(); | ||
140 | let pos = mock.add_file_with_position("/main.rs", code); | ||
141 | (mock.analysis(), pos) | ||
142 | } | ||
143 | |||
144 | /// Creates analysis for a single file, returns range marked with a pair of <|>. | ||
145 | pub fn single_file_with_range(code: &str) -> (Analysis, FileRange) { | ||
146 | let mut mock = MockAnalysis::new(); | ||
147 | let pos = mock.add_file_with_range("/main.rs", code); | ||
148 | (mock.analysis(), pos) | ||
149 | } | ||
diff --git a/crates/ra_ide/src/parent_module.rs b/crates/ra_ide/src/parent_module.rs new file mode 100644 index 000000000..6027e7d54 --- /dev/null +++ b/crates/ra_ide/src/parent_module.rs | |||
@@ -0,0 +1,104 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_db::{CrateId, FileId, FilePosition}; | ||
4 | |||
5 | use crate::{db::RootDatabase, NavigationTarget}; | ||
6 | |||
7 | /// This returns `Vec` because a module may be included from several places. We | ||
8 | /// don't handle this case yet though, so the Vec has length at most one. | ||
9 | pub(crate) fn parent_module(db: &RootDatabase, position: FilePosition) -> Vec<NavigationTarget> { | ||
10 | let src = hir::ModuleSource::from_position(db, position); | ||
11 | let module = match hir::Module::from_definition( | ||
12 | db, | ||
13 | hir::Source { file_id: position.file_id.into(), value: src }, | ||
14 | ) { | ||
15 | None => return Vec::new(), | ||
16 | Some(it) => it, | ||
17 | }; | ||
18 | let nav = NavigationTarget::from_module_to_decl(db, module); | ||
19 | vec![nav] | ||
20 | } | ||
21 | |||
22 | /// Returns `Vec` for the same reason as `parent_module` | ||
23 | pub(crate) fn crate_for(db: &RootDatabase, file_id: FileId) -> Vec<CrateId> { | ||
24 | let src = hir::ModuleSource::from_file_id(db, file_id); | ||
25 | let module = | ||
26 | match hir::Module::from_definition(db, hir::Source { file_id: file_id.into(), value: src }) | ||
27 | { | ||
28 | Some(it) => it, | ||
29 | None => return Vec::new(), | ||
30 | }; | ||
31 | let krate = module.krate(); | ||
32 | vec![krate.crate_id()] | ||
33 | } | ||
34 | |||
35 | #[cfg(test)] | ||
36 | mod tests { | ||
37 | use ra_cfg::CfgOptions; | ||
38 | use ra_db::Env; | ||
39 | |||
40 | use crate::{ | ||
41 | mock_analysis::{analysis_and_position, MockAnalysis}, | ||
42 | AnalysisChange, CrateGraph, | ||
43 | Edition::Edition2018, | ||
44 | }; | ||
45 | |||
46 | #[test] | ||
47 | fn test_resolve_parent_module() { | ||
48 | let (analysis, pos) = analysis_and_position( | ||
49 | " | ||
50 | //- /lib.rs | ||
51 | mod foo; | ||
52 | //- /foo.rs | ||
53 | <|>// empty | ||
54 | ", | ||
55 | ); | ||
56 | let nav = analysis.parent_module(pos).unwrap().pop().unwrap(); | ||
57 | nav.assert_match("foo MODULE FileId(1) [0; 8)"); | ||
58 | } | ||
59 | |||
60 | #[test] | ||
61 | fn test_resolve_parent_module_for_inline() { | ||
62 | let (analysis, pos) = analysis_and_position( | ||
63 | " | ||
64 | //- /lib.rs | ||
65 | mod foo { | ||
66 | mod bar { | ||
67 | mod baz { <|> } | ||
68 | } | ||
69 | } | ||
70 | ", | ||
71 | ); | ||
72 | let nav = analysis.parent_module(pos).unwrap().pop().unwrap(); | ||
73 | nav.assert_match("baz MODULE FileId(1) [32; 44)"); | ||
74 | } | ||
75 | |||
76 | #[test] | ||
77 | fn test_resolve_crate_root() { | ||
78 | let mock = MockAnalysis::with_files( | ||
79 | " | ||
80 | //- /bar.rs | ||
81 | mod foo; | ||
82 | //- /foo.rs | ||
83 | // empty <|> | ||
84 | ", | ||
85 | ); | ||
86 | let root_file = mock.id_of("/bar.rs"); | ||
87 | let mod_file = mock.id_of("/foo.rs"); | ||
88 | let mut host = mock.analysis_host(); | ||
89 | assert!(host.analysis().crate_for(mod_file).unwrap().is_empty()); | ||
90 | |||
91 | let mut crate_graph = CrateGraph::default(); | ||
92 | let crate_id = crate_graph.add_crate_root( | ||
93 | root_file, | ||
94 | Edition2018, | ||
95 | CfgOptions::default(), | ||
96 | Env::default(), | ||
97 | ); | ||
98 | let mut change = AnalysisChange::new(); | ||
99 | change.set_crate_graph(crate_graph); | ||
100 | host.apply_change(change); | ||
101 | |||
102 | assert_eq!(host.analysis().crate_for(mod_file).unwrap(), vec![crate_id]); | ||
103 | } | ||
104 | } | ||
diff --git a/crates/ra_ide/src/references.rs b/crates/ra_ide/src/references.rs new file mode 100644 index 000000000..21a1ea69e --- /dev/null +++ b/crates/ra_ide/src/references.rs | |||
@@ -0,0 +1,389 @@ | |||
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 classify; | ||
13 | mod name_definition; | ||
14 | mod rename; | ||
15 | mod search_scope; | ||
16 | |||
17 | use hir::Source; | ||
18 | use once_cell::unsync::Lazy; | ||
19 | use ra_db::{SourceDatabase, SourceDatabaseExt}; | ||
20 | use ra_prof::profile; | ||
21 | use ra_syntax::{algo::find_node_at_offset, ast, AstNode, SourceFile, SyntaxNode, TextUnit}; | ||
22 | |||
23 | use crate::{ | ||
24 | db::RootDatabase, display::ToNav, FilePosition, FileRange, NavigationTarget, RangeInfo, | ||
25 | }; | ||
26 | |||
27 | pub(crate) use self::{ | ||
28 | classify::{classify_name, classify_name_ref}, | ||
29 | name_definition::{NameDefinition, NameKind}, | ||
30 | rename::rename, | ||
31 | }; | ||
32 | |||
33 | pub use self::search_scope::SearchScope; | ||
34 | |||
35 | #[derive(Debug, Clone)] | ||
36 | pub struct ReferenceSearchResult { | ||
37 | declaration: NavigationTarget, | ||
38 | references: Vec<FileRange>, | ||
39 | } | ||
40 | |||
41 | impl ReferenceSearchResult { | ||
42 | pub fn declaration(&self) -> &NavigationTarget { | ||
43 | &self.declaration | ||
44 | } | ||
45 | |||
46 | pub fn references(&self) -> &[FileRange] { | ||
47 | &self.references | ||
48 | } | ||
49 | |||
50 | /// Total number of references | ||
51 | /// At least 1 since all valid references should | ||
52 | /// Have a declaration | ||
53 | pub fn len(&self) -> usize { | ||
54 | self.references.len() + 1 | ||
55 | } | ||
56 | } | ||
57 | |||
58 | // allow turning ReferenceSearchResult into an iterator | ||
59 | // over FileRanges | ||
60 | impl IntoIterator for ReferenceSearchResult { | ||
61 | type Item = FileRange; | ||
62 | type IntoIter = std::vec::IntoIter<FileRange>; | ||
63 | |||
64 | fn into_iter(mut self) -> Self::IntoIter { | ||
65 | let mut v = Vec::with_capacity(self.len()); | ||
66 | v.push(FileRange { file_id: self.declaration.file_id(), range: self.declaration.range() }); | ||
67 | v.append(&mut self.references); | ||
68 | v.into_iter() | ||
69 | } | ||
70 | } | ||
71 | |||
72 | pub(crate) fn find_all_refs( | ||
73 | db: &RootDatabase, | ||
74 | position: FilePosition, | ||
75 | search_scope: Option<SearchScope>, | ||
76 | ) -> Option<RangeInfo<ReferenceSearchResult>> { | ||
77 | let parse = db.parse(position.file_id); | ||
78 | let syntax = parse.tree().syntax().clone(); | ||
79 | let RangeInfo { range, info: (name, def) } = find_name(db, &syntax, position)?; | ||
80 | |||
81 | let declaration = match def.kind { | ||
82 | NameKind::Macro(mac) => mac.to_nav(db), | ||
83 | NameKind::Field(field) => field.to_nav(db), | ||
84 | NameKind::AssocItem(assoc) => assoc.to_nav(db), | ||
85 | NameKind::Def(def) => NavigationTarget::from_def(db, def)?, | ||
86 | NameKind::SelfType(imp) => imp.to_nav(db), | ||
87 | NameKind::Local(local) => local.to_nav(db), | ||
88 | NameKind::GenericParam(_) => return None, | ||
89 | }; | ||
90 | |||
91 | let search_scope = { | ||
92 | let base = def.search_scope(db); | ||
93 | match search_scope { | ||
94 | None => base, | ||
95 | Some(scope) => base.intersection(&scope), | ||
96 | } | ||
97 | }; | ||
98 | |||
99 | let references = process_definition(db, def, name, search_scope); | ||
100 | |||
101 | Some(RangeInfo::new(range, ReferenceSearchResult { declaration, references })) | ||
102 | } | ||
103 | |||
104 | fn find_name<'a>( | ||
105 | db: &RootDatabase, | ||
106 | syntax: &SyntaxNode, | ||
107 | position: FilePosition, | ||
108 | ) -> Option<RangeInfo<(String, NameDefinition)>> { | ||
109 | if let Some(name) = find_node_at_offset::<ast::Name>(&syntax, position.offset) { | ||
110 | let def = classify_name(db, Source::new(position.file_id.into(), &name))?; | ||
111 | let range = name.syntax().text_range(); | ||
112 | return Some(RangeInfo::new(range, (name.text().to_string(), def))); | ||
113 | } | ||
114 | let name_ref = find_node_at_offset::<ast::NameRef>(&syntax, position.offset)?; | ||
115 | let def = classify_name_ref(db, Source::new(position.file_id.into(), &name_ref))?; | ||
116 | let range = name_ref.syntax().text_range(); | ||
117 | Some(RangeInfo::new(range, (name_ref.text().to_string(), def))) | ||
118 | } | ||
119 | |||
120 | fn process_definition( | ||
121 | db: &RootDatabase, | ||
122 | def: NameDefinition, | ||
123 | name: String, | ||
124 | scope: SearchScope, | ||
125 | ) -> Vec<FileRange> { | ||
126 | let _p = profile("process_definition"); | ||
127 | |||
128 | let pat = name.as_str(); | ||
129 | let mut refs = vec![]; | ||
130 | |||
131 | for (file_id, search_range) in scope { | ||
132 | let text = db.file_text(file_id); | ||
133 | let parse = Lazy::new(|| SourceFile::parse(&text)); | ||
134 | |||
135 | for (idx, _) in text.match_indices(pat) { | ||
136 | let offset = TextUnit::from_usize(idx); | ||
137 | |||
138 | if let Some(name_ref) = | ||
139 | find_node_at_offset::<ast::NameRef>(parse.tree().syntax(), offset) | ||
140 | { | ||
141 | let range = name_ref.syntax().text_range(); | ||
142 | if let Some(search_range) = search_range { | ||
143 | if !range.is_subrange(&search_range) { | ||
144 | continue; | ||
145 | } | ||
146 | } | ||
147 | if let Some(d) = classify_name_ref(db, Source::new(file_id.into(), &name_ref)) { | ||
148 | if d == def { | ||
149 | refs.push(FileRange { file_id, range }); | ||
150 | } | ||
151 | } | ||
152 | } | ||
153 | } | ||
154 | } | ||
155 | refs | ||
156 | } | ||
157 | |||
158 | #[cfg(test)] | ||
159 | mod tests { | ||
160 | use crate::{ | ||
161 | mock_analysis::{analysis_and_position, single_file_with_position, MockAnalysis}, | ||
162 | ReferenceSearchResult, SearchScope, | ||
163 | }; | ||
164 | |||
165 | #[test] | ||
166 | fn test_find_all_refs_for_local() { | ||
167 | let code = r#" | ||
168 | fn main() { | ||
169 | let mut i = 1; | ||
170 | let j = 1; | ||
171 | i = i<|> + j; | ||
172 | |||
173 | { | ||
174 | i = 0; | ||
175 | } | ||
176 | |||
177 | i = 5; | ||
178 | }"#; | ||
179 | |||
180 | let refs = get_all_refs(code); | ||
181 | assert_eq!(refs.len(), 5); | ||
182 | } | ||
183 | |||
184 | #[test] | ||
185 | fn test_find_all_refs_for_param_inside() { | ||
186 | let code = r#" | ||
187 | fn foo(i : u32) -> u32 { | ||
188 | i<|> | ||
189 | }"#; | ||
190 | |||
191 | let refs = get_all_refs(code); | ||
192 | assert_eq!(refs.len(), 2); | ||
193 | } | ||
194 | |||
195 | #[test] | ||
196 | fn test_find_all_refs_for_fn_param() { | ||
197 | let code = r#" | ||
198 | fn foo(i<|> : u32) -> u32 { | ||
199 | i | ||
200 | }"#; | ||
201 | |||
202 | let refs = get_all_refs(code); | ||
203 | assert_eq!(refs.len(), 2); | ||
204 | } | ||
205 | |||
206 | #[test] | ||
207 | fn test_find_all_refs_field_name() { | ||
208 | let code = r#" | ||
209 | //- /lib.rs | ||
210 | struct Foo { | ||
211 | pub spam<|>: u32, | ||
212 | } | ||
213 | |||
214 | fn main(s: Foo) { | ||
215 | let f = s.spam; | ||
216 | } | ||
217 | "#; | ||
218 | |||
219 | let refs = get_all_refs(code); | ||
220 | assert_eq!(refs.len(), 2); | ||
221 | } | ||
222 | |||
223 | #[test] | ||
224 | fn test_find_all_refs_impl_item_name() { | ||
225 | let code = r#" | ||
226 | //- /lib.rs | ||
227 | struct Foo; | ||
228 | impl Foo { | ||
229 | fn f<|>(&self) { } | ||
230 | } | ||
231 | "#; | ||
232 | |||
233 | let refs = get_all_refs(code); | ||
234 | assert_eq!(refs.len(), 1); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn test_find_all_refs_enum_var_name() { | ||
239 | let code = r#" | ||
240 | //- /lib.rs | ||
241 | enum Foo { | ||
242 | A, | ||
243 | B<|>, | ||
244 | C, | ||
245 | } | ||
246 | "#; | ||
247 | |||
248 | let refs = get_all_refs(code); | ||
249 | assert_eq!(refs.len(), 1); | ||
250 | } | ||
251 | |||
252 | #[test] | ||
253 | fn test_find_all_refs_two_modules() { | ||
254 | let code = r#" | ||
255 | //- /lib.rs | ||
256 | pub mod foo; | ||
257 | pub mod bar; | ||
258 | |||
259 | fn f() { | ||
260 | let i = foo::Foo { n: 5 }; | ||
261 | } | ||
262 | |||
263 | //- /foo.rs | ||
264 | use crate::bar; | ||
265 | |||
266 | pub struct Foo { | ||
267 | pub n: u32, | ||
268 | } | ||
269 | |||
270 | fn f() { | ||
271 | let i = bar::Bar { n: 5 }; | ||
272 | } | ||
273 | |||
274 | //- /bar.rs | ||
275 | use crate::foo; | ||
276 | |||
277 | pub struct Bar { | ||
278 | pub n: u32, | ||
279 | } | ||
280 | |||
281 | fn f() { | ||
282 | let i = foo::Foo<|> { n: 5 }; | ||
283 | } | ||
284 | "#; | ||
285 | |||
286 | let (analysis, pos) = analysis_and_position(code); | ||
287 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
288 | assert_eq!(refs.len(), 3); | ||
289 | } | ||
290 | |||
291 | // `mod foo;` is not in the results because `foo` is an `ast::Name`. | ||
292 | // So, there are two references: the first one is a definition of the `foo` module, | ||
293 | // which is the whole `foo.rs`, and the second one is in `use foo::Foo`. | ||
294 | #[test] | ||
295 | fn test_find_all_refs_decl_module() { | ||
296 | let code = r#" | ||
297 | //- /lib.rs | ||
298 | mod foo<|>; | ||
299 | |||
300 | use foo::Foo; | ||
301 | |||
302 | fn f() { | ||
303 | let i = Foo { n: 5 }; | ||
304 | } | ||
305 | |||
306 | //- /foo.rs | ||
307 | pub struct Foo { | ||
308 | pub n: u32, | ||
309 | } | ||
310 | "#; | ||
311 | |||
312 | let (analysis, pos) = analysis_and_position(code); | ||
313 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
314 | assert_eq!(refs.len(), 2); | ||
315 | } | ||
316 | |||
317 | #[test] | ||
318 | fn test_find_all_refs_super_mod_vis() { | ||
319 | let code = r#" | ||
320 | //- /lib.rs | ||
321 | mod foo; | ||
322 | |||
323 | //- /foo.rs | ||
324 | mod some; | ||
325 | use some::Foo; | ||
326 | |||
327 | fn f() { | ||
328 | let i = Foo { n: 5 }; | ||
329 | } | ||
330 | |||
331 | //- /foo/some.rs | ||
332 | pub(super) struct Foo<|> { | ||
333 | pub n: u32, | ||
334 | } | ||
335 | "#; | ||
336 | |||
337 | let (analysis, pos) = analysis_and_position(code); | ||
338 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
339 | assert_eq!(refs.len(), 3); | ||
340 | } | ||
341 | |||
342 | #[test] | ||
343 | fn test_find_all_refs_with_scope() { | ||
344 | let code = r#" | ||
345 | //- /lib.rs | ||
346 | mod foo; | ||
347 | mod bar; | ||
348 | |||
349 | pub fn quux<|>() {} | ||
350 | |||
351 | //- /foo.rs | ||
352 | fn f() { super::quux(); } | ||
353 | |||
354 | //- /bar.rs | ||
355 | fn f() { super::quux(); } | ||
356 | "#; | ||
357 | |||
358 | let (mock, pos) = MockAnalysis::with_files_and_position(code); | ||
359 | let bar = mock.id_of("/bar.rs"); | ||
360 | let analysis = mock.analysis(); | ||
361 | |||
362 | let refs = analysis.find_all_refs(pos, None).unwrap().unwrap(); | ||
363 | assert_eq!(refs.len(), 3); | ||
364 | |||
365 | let refs = | ||
366 | analysis.find_all_refs(pos, Some(SearchScope::single_file(bar))).unwrap().unwrap(); | ||
367 | assert_eq!(refs.len(), 2); | ||
368 | } | ||
369 | |||
370 | #[test] | ||
371 | fn test_find_all_refs_macro_def() { | ||
372 | let code = r#" | ||
373 | #[macro_export] | ||
374 | macro_rules! m1<|> { () => (()) } | ||
375 | |||
376 | fn foo() { | ||
377 | m1(); | ||
378 | m1(); | ||
379 | }"#; | ||
380 | |||
381 | let refs = get_all_refs(code); | ||
382 | assert_eq!(refs.len(), 3); | ||
383 | } | ||
384 | |||
385 | fn get_all_refs(text: &str) -> ReferenceSearchResult { | ||
386 | let (analysis, position) = single_file_with_position(text); | ||
387 | analysis.find_all_refs(position, None).unwrap().unwrap() | ||
388 | } | ||
389 | } | ||
diff --git a/crates/ra_ide/src/references/classify.rs b/crates/ra_ide/src/references/classify.rs new file mode 100644 index 000000000..5cea805ec --- /dev/null +++ b/crates/ra_ide/src/references/classify.rs | |||
@@ -0,0 +1,186 @@ | |||
1 | //! Functions that are used to classify an element from its definition or reference. | ||
2 | |||
3 | use hir::{FromSource, Module, ModuleSource, PathResolution, Source, SourceAnalyzer}; | ||
4 | use ra_prof::profile; | ||
5 | use ra_syntax::{ast, match_ast, AstNode}; | ||
6 | use test_utils::tested_by; | ||
7 | |||
8 | use super::{ | ||
9 | name_definition::{from_assoc_item, from_module_def, from_struct_field}, | ||
10 | NameDefinition, NameKind, | ||
11 | }; | ||
12 | use crate::db::RootDatabase; | ||
13 | |||
14 | pub(crate) fn classify_name(db: &RootDatabase, name: Source<&ast::Name>) -> Option<NameDefinition> { | ||
15 | let _p = profile("classify_name"); | ||
16 | let parent = name.value.syntax().parent()?; | ||
17 | |||
18 | match_ast! { | ||
19 | match parent { | ||
20 | ast::BindPat(it) => { | ||
21 | let src = name.with_value(it); | ||
22 | let local = hir::Local::from_source(db, src)?; | ||
23 | Some(NameDefinition { | ||
24 | visibility: None, | ||
25 | container: local.module(db), | ||
26 | kind: NameKind::Local(local), | ||
27 | }) | ||
28 | }, | ||
29 | ast::RecordFieldDef(it) => { | ||
30 | let ast = hir::FieldSource::Named(it); | ||
31 | let src = name.with_value(ast); | ||
32 | let field = hir::StructField::from_source(db, src)?; | ||
33 | Some(from_struct_field(db, field)) | ||
34 | }, | ||
35 | ast::Module(it) => { | ||
36 | let def = { | ||
37 | if !it.has_semi() { | ||
38 | let ast = hir::ModuleSource::Module(it); | ||
39 | let src = name.with_value(ast); | ||
40 | hir::Module::from_definition(db, src) | ||
41 | } else { | ||
42 | let src = name.with_value(it); | ||
43 | hir::Module::from_declaration(db, src) | ||
44 | } | ||
45 | }?; | ||
46 | Some(from_module_def(db, def.into(), None)) | ||
47 | }, | ||
48 | ast::StructDef(it) => { | ||
49 | let src = name.with_value(it); | ||
50 | let def = hir::Struct::from_source(db, src)?; | ||
51 | Some(from_module_def(db, def.into(), None)) | ||
52 | }, | ||
53 | ast::EnumDef(it) => { | ||
54 | let src = name.with_value(it); | ||
55 | let def = hir::Enum::from_source(db, src)?; | ||
56 | Some(from_module_def(db, def.into(), None)) | ||
57 | }, | ||
58 | ast::TraitDef(it) => { | ||
59 | let src = name.with_value(it); | ||
60 | let def = hir::Trait::from_source(db, src)?; | ||
61 | Some(from_module_def(db, def.into(), None)) | ||
62 | }, | ||
63 | ast::StaticDef(it) => { | ||
64 | let src = name.with_value(it); | ||
65 | let def = hir::Static::from_source(db, src)?; | ||
66 | Some(from_module_def(db, def.into(), None)) | ||
67 | }, | ||
68 | ast::EnumVariant(it) => { | ||
69 | let src = name.with_value(it); | ||
70 | let def = hir::EnumVariant::from_source(db, src)?; | ||
71 | Some(from_module_def(db, def.into(), None)) | ||
72 | }, | ||
73 | ast::FnDef(it) => { | ||
74 | let src = name.with_value(it); | ||
75 | let def = hir::Function::from_source(db, src)?; | ||
76 | if parent.parent().and_then(ast::ItemList::cast).is_some() { | ||
77 | Some(from_assoc_item(db, def.into())) | ||
78 | } else { | ||
79 | Some(from_module_def(db, def.into(), None)) | ||
80 | } | ||
81 | }, | ||
82 | ast::ConstDef(it) => { | ||
83 | let src = name.with_value(it); | ||
84 | let def = hir::Const::from_source(db, src)?; | ||
85 | if parent.parent().and_then(ast::ItemList::cast).is_some() { | ||
86 | Some(from_assoc_item(db, def.into())) | ||
87 | } else { | ||
88 | Some(from_module_def(db, def.into(), None)) | ||
89 | } | ||
90 | }, | ||
91 | ast::TypeAliasDef(it) => { | ||
92 | let src = name.with_value(it); | ||
93 | let def = hir::TypeAlias::from_source(db, src)?; | ||
94 | if parent.parent().and_then(ast::ItemList::cast).is_some() { | ||
95 | Some(from_assoc_item(db, def.into())) | ||
96 | } else { | ||
97 | Some(from_module_def(db, def.into(), None)) | ||
98 | } | ||
99 | }, | ||
100 | ast::MacroCall(it) => { | ||
101 | let src = name.with_value(it); | ||
102 | let def = hir::MacroDef::from_source(db, src.clone())?; | ||
103 | |||
104 | let module_src = ModuleSource::from_child_node(db, src.as_ref().map(|it| it.syntax())); | ||
105 | let module = Module::from_definition(db, src.with_value(module_src))?; | ||
106 | |||
107 | Some(NameDefinition { | ||
108 | visibility: None, | ||
109 | container: module, | ||
110 | kind: NameKind::Macro(def), | ||
111 | }) | ||
112 | }, | ||
113 | _ => None, | ||
114 | } | ||
115 | } | ||
116 | } | ||
117 | |||
118 | pub(crate) fn classify_name_ref( | ||
119 | db: &RootDatabase, | ||
120 | name_ref: Source<&ast::NameRef>, | ||
121 | ) -> Option<NameDefinition> { | ||
122 | let _p = profile("classify_name_ref"); | ||
123 | |||
124 | let parent = name_ref.value.syntax().parent()?; | ||
125 | let analyzer = SourceAnalyzer::new(db, name_ref.map(|it| it.syntax()), None); | ||
126 | |||
127 | if let Some(method_call) = ast::MethodCallExpr::cast(parent.clone()) { | ||
128 | tested_by!(goto_definition_works_for_methods); | ||
129 | if let Some(func) = analyzer.resolve_method_call(&method_call) { | ||
130 | return Some(from_assoc_item(db, func.into())); | ||
131 | } | ||
132 | } | ||
133 | |||
134 | if let Some(field_expr) = ast::FieldExpr::cast(parent.clone()) { | ||
135 | tested_by!(goto_definition_works_for_fields); | ||
136 | if let Some(field) = analyzer.resolve_field(&field_expr) { | ||
137 | return Some(from_struct_field(db, field)); | ||
138 | } | ||
139 | } | ||
140 | |||
141 | if let Some(record_field) = ast::RecordField::cast(parent.clone()) { | ||
142 | tested_by!(goto_definition_works_for_record_fields); | ||
143 | if let Some(field_def) = analyzer.resolve_record_field(&record_field) { | ||
144 | return Some(from_struct_field(db, field_def)); | ||
145 | } | ||
146 | } | ||
147 | |||
148 | let ast = ModuleSource::from_child_node(db, name_ref.with_value(&parent)); | ||
149 | // FIXME: find correct container and visibility for each case | ||
150 | let container = Module::from_definition(db, name_ref.with_value(ast))?; | ||
151 | let visibility = None; | ||
152 | |||
153 | if let Some(macro_call) = parent.ancestors().find_map(ast::MacroCall::cast) { | ||
154 | tested_by!(goto_definition_works_for_macros); | ||
155 | if let Some(macro_def) = analyzer.resolve_macro_call(db, name_ref.with_value(¯o_call)) { | ||
156 | let kind = NameKind::Macro(macro_def); | ||
157 | return Some(NameDefinition { kind, container, visibility }); | ||
158 | } | ||
159 | } | ||
160 | |||
161 | let path = name_ref.value.syntax().ancestors().find_map(ast::Path::cast)?; | ||
162 | let resolved = analyzer.resolve_path(db, &path)?; | ||
163 | match resolved { | ||
164 | PathResolution::Def(def) => Some(from_module_def(db, def, Some(container))), | ||
165 | PathResolution::AssocItem(item) => Some(from_assoc_item(db, item)), | ||
166 | PathResolution::Local(local) => { | ||
167 | let container = local.module(db); | ||
168 | let kind = NameKind::Local(local); | ||
169 | Some(NameDefinition { kind, container, visibility: None }) | ||
170 | } | ||
171 | PathResolution::GenericParam(par) => { | ||
172 | // FIXME: get generic param def | ||
173 | let kind = NameKind::GenericParam(par); | ||
174 | Some(NameDefinition { kind, container, visibility }) | ||
175 | } | ||
176 | PathResolution::Macro(def) => { | ||
177 | let kind = NameKind::Macro(def); | ||
178 | Some(NameDefinition { kind, container, visibility }) | ||
179 | } | ||
180 | PathResolution::SelfType(impl_block) => { | ||
181 | let kind = NameKind::SelfType(impl_block); | ||
182 | let container = impl_block.module(db); | ||
183 | Some(NameDefinition { kind, container, visibility }) | ||
184 | } | ||
185 | } | ||
186 | } | ||
diff --git a/crates/ra_ide/src/references/name_definition.rs b/crates/ra_ide/src/references/name_definition.rs new file mode 100644 index 000000000..10d3a2364 --- /dev/null +++ b/crates/ra_ide/src/references/name_definition.rs | |||
@@ -0,0 +1,83 @@ | |||
1 | //! `NameDefinition` keeps information about the element we want to search references for. | ||
2 | //! The element is represented by `NameKind`. It's located inside some `container` and | ||
3 | //! has a `visibility`, which defines a search scope. | ||
4 | //! Note that the reference search is possible for not all of the classified items. | ||
5 | |||
6 | use hir::{ | ||
7 | Adt, AssocItem, GenericParam, HasSource, ImplBlock, Local, MacroDef, Module, ModuleDef, | ||
8 | StructField, VariantDef, | ||
9 | }; | ||
10 | use ra_syntax::{ast, ast::VisibilityOwner}; | ||
11 | |||
12 | use crate::db::RootDatabase; | ||
13 | |||
14 | #[derive(Debug, PartialEq, Eq)] | ||
15 | pub enum NameKind { | ||
16 | Macro(MacroDef), | ||
17 | Field(StructField), | ||
18 | AssocItem(AssocItem), | ||
19 | Def(ModuleDef), | ||
20 | SelfType(ImplBlock), | ||
21 | Local(Local), | ||
22 | GenericParam(GenericParam), | ||
23 | } | ||
24 | |||
25 | #[derive(PartialEq, Eq)] | ||
26 | pub(crate) struct NameDefinition { | ||
27 | pub visibility: Option<ast::Visibility>, | ||
28 | pub container: Module, | ||
29 | pub kind: NameKind, | ||
30 | } | ||
31 | |||
32 | pub(super) fn from_assoc_item(db: &RootDatabase, item: AssocItem) -> NameDefinition { | ||
33 | let container = item.module(db); | ||
34 | let visibility = match item { | ||
35 | AssocItem::Function(f) => f.source(db).value.visibility(), | ||
36 | AssocItem::Const(c) => c.source(db).value.visibility(), | ||
37 | AssocItem::TypeAlias(a) => a.source(db).value.visibility(), | ||
38 | }; | ||
39 | let kind = NameKind::AssocItem(item); | ||
40 | NameDefinition { kind, container, visibility } | ||
41 | } | ||
42 | |||
43 | pub(super) fn from_struct_field(db: &RootDatabase, field: StructField) -> NameDefinition { | ||
44 | let kind = NameKind::Field(field); | ||
45 | let parent = field.parent_def(db); | ||
46 | let container = parent.module(db); | ||
47 | let visibility = match parent { | ||
48 | VariantDef::Struct(s) => s.source(db).value.visibility(), | ||
49 | VariantDef::Union(e) => e.source(db).value.visibility(), | ||
50 | VariantDef::EnumVariant(e) => e.source(db).value.parent_enum().visibility(), | ||
51 | }; | ||
52 | NameDefinition { kind, container, visibility } | ||
53 | } | ||
54 | |||
55 | pub(super) fn from_module_def( | ||
56 | db: &RootDatabase, | ||
57 | def: ModuleDef, | ||
58 | module: Option<Module>, | ||
59 | ) -> NameDefinition { | ||
60 | let kind = NameKind::Def(def); | ||
61 | let (container, visibility) = match def { | ||
62 | ModuleDef::Module(it) => { | ||
63 | let container = it.parent(db).or_else(|| Some(it)).unwrap(); | ||
64 | let visibility = it.declaration_source(db).and_then(|s| s.value.visibility()); | ||
65 | (container, visibility) | ||
66 | } | ||
67 | ModuleDef::EnumVariant(it) => { | ||
68 | let container = it.module(db); | ||
69 | let visibility = it.source(db).value.parent_enum().visibility(); | ||
70 | (container, visibility) | ||
71 | } | ||
72 | ModuleDef::Function(it) => (it.module(db), it.source(db).value.visibility()), | ||
73 | ModuleDef::Const(it) => (it.module(db), it.source(db).value.visibility()), | ||
74 | ModuleDef::Static(it) => (it.module(db), it.source(db).value.visibility()), | ||
75 | ModuleDef::Trait(it) => (it.module(db), it.source(db).value.visibility()), | ||
76 | ModuleDef::TypeAlias(it) => (it.module(db), it.source(db).value.visibility()), | ||
77 | ModuleDef::Adt(Adt::Struct(it)) => (it.module(db), it.source(db).value.visibility()), | ||
78 | ModuleDef::Adt(Adt::Union(it)) => (it.module(db), it.source(db).value.visibility()), | ||
79 | ModuleDef::Adt(Adt::Enum(it)) => (it.module(db), it.source(db).value.visibility()), | ||
80 | ModuleDef::BuiltinType(..) => (module.unwrap(), None), | ||
81 | }; | ||
82 | NameDefinition { kind, container, visibility } | ||
83 | } | ||
diff --git a/crates/ra_ide/src/references/rename.rs b/crates/ra_ide/src/references/rename.rs new file mode 100644 index 000000000..d58496049 --- /dev/null +++ b/crates/ra_ide/src/references/rename.rs | |||
@@ -0,0 +1,328 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::ModuleSource; | ||
4 | use ra_db::{RelativePath, RelativePathBuf, SourceDatabase, SourceDatabaseExt}; | ||
5 | use ra_syntax::{algo::find_node_at_offset, ast, AstNode, SyntaxNode}; | ||
6 | use ra_text_edit::TextEdit; | ||
7 | |||
8 | use crate::{ | ||
9 | db::RootDatabase, FileId, FilePosition, FileSystemEdit, RangeInfo, SourceChange, | ||
10 | SourceFileEdit, TextRange, | ||
11 | }; | ||
12 | |||
13 | use super::find_all_refs; | ||
14 | |||
15 | pub(crate) fn rename( | ||
16 | db: &RootDatabase, | ||
17 | position: FilePosition, | ||
18 | new_name: &str, | ||
19 | ) -> Option<RangeInfo<SourceChange>> { | ||
20 | let parse = db.parse(position.file_id); | ||
21 | if let Some((ast_name, ast_module)) = | ||
22 | find_name_and_module_at_offset(parse.tree().syntax(), position) | ||
23 | { | ||
24 | let range = ast_name.syntax().text_range(); | ||
25 | rename_mod(db, &ast_name, &ast_module, position, new_name) | ||
26 | .map(|info| RangeInfo::new(range, info)) | ||
27 | } else { | ||
28 | rename_reference(db, position, new_name) | ||
29 | } | ||
30 | } | ||
31 | |||
32 | fn find_name_and_module_at_offset( | ||
33 | syntax: &SyntaxNode, | ||
34 | position: FilePosition, | ||
35 | ) -> Option<(ast::Name, ast::Module)> { | ||
36 | let ast_name = find_node_at_offset::<ast::Name>(syntax, position.offset)?; | ||
37 | let ast_module = ast::Module::cast(ast_name.syntax().parent()?)?; | ||
38 | Some((ast_name, ast_module)) | ||
39 | } | ||
40 | |||
41 | fn source_edit_from_file_id_range( | ||
42 | file_id: FileId, | ||
43 | range: TextRange, | ||
44 | new_name: &str, | ||
45 | ) -> SourceFileEdit { | ||
46 | SourceFileEdit { file_id, edit: TextEdit::replace(range, new_name.into()) } | ||
47 | } | ||
48 | |||
49 | fn rename_mod( | ||
50 | db: &RootDatabase, | ||
51 | ast_name: &ast::Name, | ||
52 | ast_module: &ast::Module, | ||
53 | position: FilePosition, | ||
54 | new_name: &str, | ||
55 | ) -> Option<SourceChange> { | ||
56 | let mut source_file_edits = Vec::new(); | ||
57 | let mut file_system_edits = Vec::new(); | ||
58 | let module_src = hir::Source { file_id: position.file_id.into(), value: ast_module.clone() }; | ||
59 | if let Some(module) = hir::Module::from_declaration(db, module_src) { | ||
60 | let src = module.definition_source(db); | ||
61 | let file_id = src.file_id.original_file(db); | ||
62 | match src.value { | ||
63 | ModuleSource::SourceFile(..) => { | ||
64 | let mod_path: RelativePathBuf = db.file_relative_path(file_id); | ||
65 | // mod is defined in path/to/dir/mod.rs | ||
66 | let dst_path = if mod_path.file_stem() == Some("mod") { | ||
67 | mod_path | ||
68 | .parent() | ||
69 | .and_then(|p| p.parent()) | ||
70 | .or_else(|| Some(RelativePath::new(""))) | ||
71 | .map(|p| p.join(new_name).join("mod.rs")) | ||
72 | } else { | ||
73 | Some(mod_path.with_file_name(new_name).with_extension("rs")) | ||
74 | }; | ||
75 | if let Some(path) = dst_path { | ||
76 | let move_file = FileSystemEdit::MoveFile { | ||
77 | src: file_id, | ||
78 | dst_source_root: db.file_source_root(position.file_id), | ||
79 | dst_path: path, | ||
80 | }; | ||
81 | file_system_edits.push(move_file); | ||
82 | } | ||
83 | } | ||
84 | ModuleSource::Module(..) => {} | ||
85 | } | ||
86 | } | ||
87 | |||
88 | let edit = SourceFileEdit { | ||
89 | file_id: position.file_id, | ||
90 | edit: TextEdit::replace(ast_name.syntax().text_range(), new_name.into()), | ||
91 | }; | ||
92 | source_file_edits.push(edit); | ||
93 | |||
94 | Some(SourceChange::from_edits("rename", source_file_edits, file_system_edits)) | ||
95 | } | ||
96 | |||
97 | fn rename_reference( | ||
98 | db: &RootDatabase, | ||
99 | position: FilePosition, | ||
100 | new_name: &str, | ||
101 | ) -> Option<RangeInfo<SourceChange>> { | ||
102 | let RangeInfo { range, info: refs } = find_all_refs(db, position, None)?; | ||
103 | |||
104 | let edit = refs | ||
105 | .into_iter() | ||
106 | .map(|range| source_edit_from_file_id_range(range.file_id, range.range, new_name)) | ||
107 | .collect::<Vec<_>>(); | ||
108 | |||
109 | if edit.is_empty() { | ||
110 | return None; | ||
111 | } | ||
112 | |||
113 | Some(RangeInfo::new(range, SourceChange::source_file_edits("rename", edit))) | ||
114 | } | ||
115 | |||
116 | #[cfg(test)] | ||
117 | mod tests { | ||
118 | use insta::assert_debug_snapshot; | ||
119 | use ra_text_edit::TextEditBuilder; | ||
120 | use test_utils::assert_eq_text; | ||
121 | |||
122 | use crate::{ | ||
123 | mock_analysis::analysis_and_position, mock_analysis::single_file_with_position, FileId, | ||
124 | }; | ||
125 | |||
126 | #[test] | ||
127 | fn test_rename_for_local() { | ||
128 | test_rename( | ||
129 | r#" | ||
130 | fn main() { | ||
131 | let mut i = 1; | ||
132 | let j = 1; | ||
133 | i = i<|> + j; | ||
134 | |||
135 | { | ||
136 | i = 0; | ||
137 | } | ||
138 | |||
139 | i = 5; | ||
140 | }"#, | ||
141 | "k", | ||
142 | r#" | ||
143 | fn main() { | ||
144 | let mut k = 1; | ||
145 | let j = 1; | ||
146 | k = k + j; | ||
147 | |||
148 | { | ||
149 | k = 0; | ||
150 | } | ||
151 | |||
152 | k = 5; | ||
153 | }"#, | ||
154 | ); | ||
155 | } | ||
156 | |||
157 | #[test] | ||
158 | fn test_rename_for_param_inside() { | ||
159 | test_rename( | ||
160 | r#" | ||
161 | fn foo(i : u32) -> u32 { | ||
162 | i<|> | ||
163 | }"#, | ||
164 | "j", | ||
165 | r#" | ||
166 | fn foo(j : u32) -> u32 { | ||
167 | j | ||
168 | }"#, | ||
169 | ); | ||
170 | } | ||
171 | |||
172 | #[test] | ||
173 | fn test_rename_refs_for_fn_param() { | ||
174 | test_rename( | ||
175 | r#" | ||
176 | fn foo(i<|> : u32) -> u32 { | ||
177 | i | ||
178 | }"#, | ||
179 | "new_name", | ||
180 | r#" | ||
181 | fn foo(new_name : u32) -> u32 { | ||
182 | new_name | ||
183 | }"#, | ||
184 | ); | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn test_rename_for_mut_param() { | ||
189 | test_rename( | ||
190 | r#" | ||
191 | fn foo(mut i<|> : u32) -> u32 { | ||
192 | i | ||
193 | }"#, | ||
194 | "new_name", | ||
195 | r#" | ||
196 | fn foo(mut new_name : u32) -> u32 { | ||
197 | new_name | ||
198 | }"#, | ||
199 | ); | ||
200 | } | ||
201 | |||
202 | #[test] | ||
203 | fn test_rename_mod() { | ||
204 | let (analysis, position) = analysis_and_position( | ||
205 | " | ||
206 | //- /lib.rs | ||
207 | mod bar; | ||
208 | |||
209 | //- /bar.rs | ||
210 | mod foo<|>; | ||
211 | |||
212 | //- /bar/foo.rs | ||
213 | // emtpy | ||
214 | ", | ||
215 | ); | ||
216 | let new_name = "foo2"; | ||
217 | let source_change = analysis.rename(position, new_name).unwrap(); | ||
218 | assert_debug_snapshot!(&source_change, | ||
219 | @r###" | ||
220 | Some( | ||
221 | RangeInfo { | ||
222 | range: [4; 7), | ||
223 | info: SourceChange { | ||
224 | label: "rename", | ||
225 | source_file_edits: [ | ||
226 | SourceFileEdit { | ||
227 | file_id: FileId( | ||
228 | 2, | ||
229 | ), | ||
230 | edit: TextEdit { | ||
231 | atoms: [ | ||
232 | AtomTextEdit { | ||
233 | delete: [4; 7), | ||
234 | insert: "foo2", | ||
235 | }, | ||
236 | ], | ||
237 | }, | ||
238 | }, | ||
239 | ], | ||
240 | file_system_edits: [ | ||
241 | MoveFile { | ||
242 | src: FileId( | ||
243 | 3, | ||
244 | ), | ||
245 | dst_source_root: SourceRootId( | ||
246 | 0, | ||
247 | ), | ||
248 | dst_path: "bar/foo2.rs", | ||
249 | }, | ||
250 | ], | ||
251 | cursor_position: None, | ||
252 | }, | ||
253 | }, | ||
254 | ) | ||
255 | "###); | ||
256 | } | ||
257 | |||
258 | #[test] | ||
259 | fn test_rename_mod_in_dir() { | ||
260 | let (analysis, position) = analysis_and_position( | ||
261 | " | ||
262 | //- /lib.rs | ||
263 | mod fo<|>o; | ||
264 | //- /foo/mod.rs | ||
265 | // emtpy | ||
266 | ", | ||
267 | ); | ||
268 | let new_name = "foo2"; | ||
269 | let source_change = analysis.rename(position, new_name).unwrap(); | ||
270 | assert_debug_snapshot!(&source_change, | ||
271 | @r###" | ||
272 | Some( | ||
273 | RangeInfo { | ||
274 | range: [4; 7), | ||
275 | info: SourceChange { | ||
276 | label: "rename", | ||
277 | source_file_edits: [ | ||
278 | SourceFileEdit { | ||
279 | file_id: FileId( | ||
280 | 1, | ||
281 | ), | ||
282 | edit: TextEdit { | ||
283 | atoms: [ | ||
284 | AtomTextEdit { | ||
285 | delete: [4; 7), | ||
286 | insert: "foo2", | ||
287 | }, | ||
288 | ], | ||
289 | }, | ||
290 | }, | ||
291 | ], | ||
292 | file_system_edits: [ | ||
293 | MoveFile { | ||
294 | src: FileId( | ||
295 | 2, | ||
296 | ), | ||
297 | dst_source_root: SourceRootId( | ||
298 | 0, | ||
299 | ), | ||
300 | dst_path: "foo2/mod.rs", | ||
301 | }, | ||
302 | ], | ||
303 | cursor_position: None, | ||
304 | }, | ||
305 | }, | ||
306 | ) | ||
307 | "### | ||
308 | ); | ||
309 | } | ||
310 | |||
311 | fn test_rename(text: &str, new_name: &str, expected: &str) { | ||
312 | let (analysis, position) = single_file_with_position(text); | ||
313 | let source_change = analysis.rename(position, new_name).unwrap(); | ||
314 | let mut text_edit_builder = TextEditBuilder::default(); | ||
315 | let mut file_id: Option<FileId> = None; | ||
316 | if let Some(change) = source_change { | ||
317 | for edit in change.info.source_file_edits { | ||
318 | file_id = Some(edit.file_id); | ||
319 | for atom in edit.edit.as_atoms() { | ||
320 | text_edit_builder.replace(atom.delete, atom.insert.clone()); | ||
321 | } | ||
322 | } | ||
323 | } | ||
324 | let result = | ||
325 | text_edit_builder.finish().apply(&*analysis.file_text(file_id.unwrap()).unwrap()); | ||
326 | assert_eq_text!(expected, &*result); | ||
327 | } | ||
328 | } | ||
diff --git a/crates/ra_ide/src/references/search_scope.rs b/crates/ra_ide/src/references/search_scope.rs new file mode 100644 index 000000000..f5c9589f4 --- /dev/null +++ b/crates/ra_ide/src/references/search_scope.rs | |||
@@ -0,0 +1,145 @@ | |||
1 | //! Generally, `search_scope` returns files that might contain references for the element. | ||
2 | //! For `pub(crate)` things it's a crate, for `pub` things it's a crate and dependant crates. | ||
3 | //! In some cases, the location of the references is known to within a `TextRange`, | ||
4 | //! e.g. for things like local variables. | ||
5 | use std::mem; | ||
6 | |||
7 | use hir::{DefWithBody, HasSource, ModuleSource}; | ||
8 | use ra_db::{FileId, SourceDatabase, SourceDatabaseExt}; | ||
9 | use ra_prof::profile; | ||
10 | use ra_syntax::{AstNode, TextRange}; | ||
11 | use rustc_hash::FxHashMap; | ||
12 | |||
13 | use crate::db::RootDatabase; | ||
14 | |||
15 | use super::{NameDefinition, NameKind}; | ||
16 | |||
17 | pub struct SearchScope { | ||
18 | entries: FxHashMap<FileId, Option<TextRange>>, | ||
19 | } | ||
20 | |||
21 | impl SearchScope { | ||
22 | fn new(entries: FxHashMap<FileId, Option<TextRange>>) -> SearchScope { | ||
23 | SearchScope { entries } | ||
24 | } | ||
25 | pub fn single_file(file: FileId) -> SearchScope { | ||
26 | SearchScope::new(std::iter::once((file, None)).collect()) | ||
27 | } | ||
28 | pub(crate) fn intersection(&self, other: &SearchScope) -> SearchScope { | ||
29 | let (mut small, mut large) = (&self.entries, &other.entries); | ||
30 | if small.len() > large.len() { | ||
31 | mem::swap(&mut small, &mut large) | ||
32 | } | ||
33 | |||
34 | let res = small | ||
35 | .iter() | ||
36 | .filter_map(|(file_id, r1)| { | ||
37 | let r2 = large.get(file_id)?; | ||
38 | let r = intersect_ranges(*r1, *r2)?; | ||
39 | Some((*file_id, r)) | ||
40 | }) | ||
41 | .collect(); | ||
42 | return SearchScope::new(res); | ||
43 | |||
44 | fn intersect_ranges( | ||
45 | r1: Option<TextRange>, | ||
46 | r2: Option<TextRange>, | ||
47 | ) -> Option<Option<TextRange>> { | ||
48 | match (r1, r2) { | ||
49 | (None, r) | (r, None) => Some(r), | ||
50 | (Some(r1), Some(r2)) => { | ||
51 | let r = r1.intersection(&r2)?; | ||
52 | Some(Some(r)) | ||
53 | } | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | impl IntoIterator for SearchScope { | ||
60 | type Item = (FileId, Option<TextRange>); | ||
61 | type IntoIter = std::collections::hash_map::IntoIter<FileId, Option<TextRange>>; | ||
62 | fn into_iter(self) -> Self::IntoIter { | ||
63 | self.entries.into_iter() | ||
64 | } | ||
65 | } | ||
66 | |||
67 | impl NameDefinition { | ||
68 | pub(crate) fn search_scope(&self, db: &RootDatabase) -> SearchScope { | ||
69 | let _p = profile("search_scope"); | ||
70 | |||
71 | let module_src = self.container.definition_source(db); | ||
72 | let file_id = module_src.file_id.original_file(db); | ||
73 | |||
74 | if let NameKind::Local(var) = self.kind { | ||
75 | let range = match var.parent(db) { | ||
76 | DefWithBody::Function(f) => f.source(db).value.syntax().text_range(), | ||
77 | DefWithBody::Const(c) => c.source(db).value.syntax().text_range(), | ||
78 | DefWithBody::Static(s) => s.source(db).value.syntax().text_range(), | ||
79 | }; | ||
80 | let mut res = FxHashMap::default(); | ||
81 | res.insert(file_id, Some(range)); | ||
82 | return SearchScope::new(res); | ||
83 | } | ||
84 | |||
85 | let vis = | ||
86 | self.visibility.as_ref().map(|v| v.syntax().to_string()).unwrap_or("".to_string()); | ||
87 | |||
88 | if vis.as_str() == "pub(super)" { | ||
89 | if let Some(parent_module) = self.container.parent(db) { | ||
90 | let mut res = FxHashMap::default(); | ||
91 | let parent_src = parent_module.definition_source(db); | ||
92 | let file_id = parent_src.file_id.original_file(db); | ||
93 | |||
94 | match parent_src.value { | ||
95 | ModuleSource::Module(m) => { | ||
96 | let range = Some(m.syntax().text_range()); | ||
97 | res.insert(file_id, range); | ||
98 | } | ||
99 | ModuleSource::SourceFile(_) => { | ||
100 | res.insert(file_id, None); | ||
101 | res.extend(parent_module.children(db).map(|m| { | ||
102 | let src = m.definition_source(db); | ||
103 | (src.file_id.original_file(db), None) | ||
104 | })); | ||
105 | } | ||
106 | } | ||
107 | return SearchScope::new(res); | ||
108 | } | ||
109 | } | ||
110 | |||
111 | if vis.as_str() != "" { | ||
112 | let source_root_id = db.file_source_root(file_id); | ||
113 | let source_root = db.source_root(source_root_id); | ||
114 | let mut res = source_root.walk().map(|id| (id, None)).collect::<FxHashMap<_, _>>(); | ||
115 | |||
116 | // FIXME: add "pub(in path)" | ||
117 | |||
118 | if vis.as_str() == "pub(crate)" { | ||
119 | return SearchScope::new(res); | ||
120 | } | ||
121 | if vis.as_str() == "pub" { | ||
122 | let krate = self.container.krate(); | ||
123 | let crate_graph = db.crate_graph(); | ||
124 | for crate_id in crate_graph.iter() { | ||
125 | let mut crate_deps = crate_graph.dependencies(crate_id); | ||
126 | if crate_deps.any(|dep| dep.crate_id() == krate.crate_id()) { | ||
127 | let root_file = crate_graph.crate_root(crate_id); | ||
128 | let source_root_id = db.file_source_root(root_file); | ||
129 | let source_root = db.source_root(source_root_id); | ||
130 | res.extend(source_root.walk().map(|id| (id, None))); | ||
131 | } | ||
132 | } | ||
133 | return SearchScope::new(res); | ||
134 | } | ||
135 | } | ||
136 | |||
137 | let mut res = FxHashMap::default(); | ||
138 | let range = match module_src.value { | ||
139 | ModuleSource::Module(m) => Some(m.syntax().text_range()), | ||
140 | ModuleSource::SourceFile(_) => None, | ||
141 | }; | ||
142 | res.insert(file_id, range); | ||
143 | SearchScope::new(res) | ||
144 | } | ||
145 | } | ||
diff --git a/crates/ra_ide/src/runnables.rs b/crates/ra_ide/src/runnables.rs new file mode 100644 index 000000000..8039a5164 --- /dev/null +++ b/crates/ra_ide/src/runnables.rs | |||
@@ -0,0 +1,242 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use hir::Source; | ||
4 | use itertools::Itertools; | ||
5 | use ra_db::SourceDatabase; | ||
6 | use ra_syntax::{ | ||
7 | ast::{self, AstNode, AttrsOwner, ModuleItemOwner, NameOwner}, | ||
8 | match_ast, SyntaxNode, TextRange, | ||
9 | }; | ||
10 | |||
11 | use crate::{db::RootDatabase, FileId}; | ||
12 | |||
13 | #[derive(Debug)] | ||
14 | pub struct Runnable { | ||
15 | pub range: TextRange, | ||
16 | pub kind: RunnableKind, | ||
17 | } | ||
18 | |||
19 | #[derive(Debug)] | ||
20 | pub enum RunnableKind { | ||
21 | Test { name: String }, | ||
22 | TestMod { path: String }, | ||
23 | Bench { name: String }, | ||
24 | Bin, | ||
25 | } | ||
26 | |||
27 | pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | ||
28 | let parse = db.parse(file_id); | ||
29 | parse.tree().syntax().descendants().filter_map(|i| runnable(db, file_id, i)).collect() | ||
30 | } | ||
31 | |||
32 | fn runnable(db: &RootDatabase, file_id: FileId, item: SyntaxNode) -> Option<Runnable> { | ||
33 | match_ast! { | ||
34 | match item { | ||
35 | ast::FnDef(it) => { runnable_fn(it) }, | ||
36 | ast::Module(it) => { runnable_mod(db, file_id, it) }, | ||
37 | _ => { None }, | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | |||
42 | fn runnable_fn(fn_def: ast::FnDef) -> Option<Runnable> { | ||
43 | let name = fn_def.name()?.text().clone(); | ||
44 | let kind = if name == "main" { | ||
45 | RunnableKind::Bin | ||
46 | } else if fn_def.has_atom_attr("test") { | ||
47 | RunnableKind::Test { name: name.to_string() } | ||
48 | } else if fn_def.has_atom_attr("bench") { | ||
49 | RunnableKind::Bench { name: name.to_string() } | ||
50 | } else { | ||
51 | return None; | ||
52 | }; | ||
53 | Some(Runnable { range: fn_def.syntax().text_range(), kind }) | ||
54 | } | ||
55 | |||
56 | fn runnable_mod(db: &RootDatabase, file_id: FileId, module: ast::Module) -> Option<Runnable> { | ||
57 | let has_test_function = module | ||
58 | .item_list()? | ||
59 | .items() | ||
60 | .filter_map(|it| match it { | ||
61 | ast::ModuleItem::FnDef(it) => Some(it), | ||
62 | _ => None, | ||
63 | }) | ||
64 | .any(|f| f.has_atom_attr("test")); | ||
65 | if !has_test_function { | ||
66 | return None; | ||
67 | } | ||
68 | let range = module.syntax().text_range(); | ||
69 | let src = hir::ModuleSource::from_child_node(db, Source::new(file_id.into(), &module.syntax())); | ||
70 | let module = hir::Module::from_definition(db, Source::new(file_id.into(), src))?; | ||
71 | |||
72 | let path = module.path_to_root(db).into_iter().rev().filter_map(|it| it.name(db)).join("::"); | ||
73 | Some(Runnable { range, kind: RunnableKind::TestMod { path } }) | ||
74 | } | ||
75 | |||
76 | #[cfg(test)] | ||
77 | mod tests { | ||
78 | use insta::assert_debug_snapshot; | ||
79 | |||
80 | use crate::mock_analysis::analysis_and_position; | ||
81 | |||
82 | #[test] | ||
83 | fn test_runnables() { | ||
84 | let (analysis, pos) = analysis_and_position( | ||
85 | r#" | ||
86 | //- /lib.rs | ||
87 | <|> //empty | ||
88 | fn main() {} | ||
89 | |||
90 | #[test] | ||
91 | fn test_foo() {} | ||
92 | |||
93 | #[test] | ||
94 | #[ignore] | ||
95 | fn test_foo() {} | ||
96 | "#, | ||
97 | ); | ||
98 | let runnables = analysis.runnables(pos.file_id).unwrap(); | ||
99 | assert_debug_snapshot!(&runnables, | ||
100 | @r###" | ||
101 | [ | ||
102 | Runnable { | ||
103 | range: [1; 21), | ||
104 | kind: Bin, | ||
105 | }, | ||
106 | Runnable { | ||
107 | range: [22; 46), | ||
108 | kind: Test { | ||
109 | name: "test_foo", | ||
110 | }, | ||
111 | }, | ||
112 | Runnable { | ||
113 | range: [47; 81), | ||
114 | kind: Test { | ||
115 | name: "test_foo", | ||
116 | }, | ||
117 | }, | ||
118 | ] | ||
119 | "### | ||
120 | ); | ||
121 | } | ||
122 | |||
123 | #[test] | ||
124 | fn test_runnables_module() { | ||
125 | let (analysis, pos) = analysis_and_position( | ||
126 | r#" | ||
127 | //- /lib.rs | ||
128 | <|> //empty | ||
129 | mod test_mod { | ||
130 | #[test] | ||
131 | fn test_foo1() {} | ||
132 | } | ||
133 | "#, | ||
134 | ); | ||
135 | let runnables = analysis.runnables(pos.file_id).unwrap(); | ||
136 | assert_debug_snapshot!(&runnables, | ||
137 | @r###" | ||
138 | [ | ||
139 | Runnable { | ||
140 | range: [1; 59), | ||
141 | kind: TestMod { | ||
142 | path: "test_mod", | ||
143 | }, | ||
144 | }, | ||
145 | Runnable { | ||
146 | range: [28; 57), | ||
147 | kind: Test { | ||
148 | name: "test_foo1", | ||
149 | }, | ||
150 | }, | ||
151 | ] | ||
152 | "### | ||
153 | ); | ||
154 | } | ||
155 | |||
156 | #[test] | ||
157 | fn test_runnables_one_depth_layer_module() { | ||
158 | let (analysis, pos) = analysis_and_position( | ||
159 | r#" | ||
160 | //- /lib.rs | ||
161 | <|> //empty | ||
162 | mod foo { | ||
163 | mod test_mod { | ||
164 | #[test] | ||
165 | fn test_foo1() {} | ||
166 | } | ||
167 | } | ||
168 | "#, | ||
169 | ); | ||
170 | let runnables = analysis.runnables(pos.file_id).unwrap(); | ||
171 | assert_debug_snapshot!(&runnables, | ||
172 | @r###" | ||
173 | [ | ||
174 | Runnable { | ||
175 | range: [23; 85), | ||
176 | kind: TestMod { | ||
177 | path: "foo::test_mod", | ||
178 | }, | ||
179 | }, | ||
180 | Runnable { | ||
181 | range: [46; 79), | ||
182 | kind: Test { | ||
183 | name: "test_foo1", | ||
184 | }, | ||
185 | }, | ||
186 | ] | ||
187 | "### | ||
188 | ); | ||
189 | } | ||
190 | |||
191 | #[test] | ||
192 | fn test_runnables_multiple_depth_module() { | ||
193 | let (analysis, pos) = analysis_and_position( | ||
194 | r#" | ||
195 | //- /lib.rs | ||
196 | <|> //empty | ||
197 | mod foo { | ||
198 | mod bar { | ||
199 | mod test_mod { | ||
200 | #[test] | ||
201 | fn test_foo1() {} | ||
202 | } | ||
203 | } | ||
204 | } | ||
205 | "#, | ||
206 | ); | ||
207 | let runnables = analysis.runnables(pos.file_id).unwrap(); | ||
208 | assert_debug_snapshot!(&runnables, | ||
209 | @r###" | ||
210 | [ | ||
211 | Runnable { | ||
212 | range: [41; 115), | ||
213 | kind: TestMod { | ||
214 | path: "foo::bar::test_mod", | ||
215 | }, | ||
216 | }, | ||
217 | Runnable { | ||
218 | range: [68; 105), | ||
219 | kind: Test { | ||
220 | name: "test_foo1", | ||
221 | }, | ||
222 | }, | ||
223 | ] | ||
224 | "### | ||
225 | ); | ||
226 | } | ||
227 | |||
228 | #[test] | ||
229 | fn test_runnables_no_test_function_in_module() { | ||
230 | let (analysis, pos) = analysis_and_position( | ||
231 | r#" | ||
232 | //- /lib.rs | ||
233 | <|> //empty | ||
234 | mod test_mod { | ||
235 | fn foo1() {} | ||
236 | } | ||
237 | "#, | ||
238 | ); | ||
239 | let runnables = analysis.runnables(pos.file_id).unwrap(); | ||
240 | assert!(runnables.is_empty()) | ||
241 | } | ||
242 | } | ||
diff --git a/crates/ra_ide/src/snapshots/highlighting.html b/crates/ra_ide/src/snapshots/highlighting.html new file mode 100644 index 000000000..b39c4d371 --- /dev/null +++ b/crates/ra_ide/src/snapshots/highlighting.html | |||
@@ -0,0 +1,48 @@ | |||
1 | |||
2 | <style> | ||
3 | body { margin: 0; } | ||
4 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
5 | |||
6 | .comment { color: #7F9F7F; } | ||
7 | .string { color: #CC9393; } | ||
8 | .function { color: #93E0E3; } | ||
9 | .parameter { color: #94BFF3; } | ||
10 | .builtin { color: #DD6718; } | ||
11 | .text { color: #DCDCCC; } | ||
12 | .attribute { color: #94BFF3; } | ||
13 | .literal { color: #BFEBBF; } | ||
14 | .macro { color: #94BFF3; } | ||
15 | .variable { color: #DCDCCC; } | ||
16 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } | ||
17 | |||
18 | .keyword { color: #F0DFAF; } | ||
19 | .keyword\.unsafe { color: #DFAF8F; } | ||
20 | .keyword\.control { color: #F0DFAF; font-weight: bold; } | ||
21 | </style> | ||
22 | <pre><code><span class="attribute">#</span><span class="attribute">[</span><span class="attribute">derive</span><span class="attribute">(</span><span class="attribute">Clone</span><span class="attribute">,</span><span class="attribute"> </span><span class="attribute">Debug</span><span class="attribute">)</span><span class="attribute">]</span> | ||
23 | <span class="keyword">struct</span> <span class="type">Foo</span> { | ||
24 | <span class="keyword">pub</span> <span class="field">x</span>: <span class="type">i32</span>, | ||
25 | <span class="keyword">pub</span> <span class="field">y</span>: <span class="type">i32</span>, | ||
26 | } | ||
27 | |||
28 | <span class="keyword">fn</span> <span class="function">foo</span><<span class="type">T</span>>() -> <span class="type">T</span> { | ||
29 | <span class="macro">unimplemented</span><span class="macro">!</span>(); | ||
30 | <span class="function">foo</span>::<<span class="type">i32</span>>(); | ||
31 | } | ||
32 | |||
33 | <span class="comment">// comment</span> | ||
34 | <span class="keyword">fn</span> <span class="function">main</span>() { | ||
35 | <span class="macro">println</span><span class="macro">!</span>(<span class="string">"Hello, {}!"</span>, <span class="literal">92</span>); | ||
36 | |||
37 | <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable.mut">vec</span> = <span class="text">Vec</span>::<span class="text">new</span>(); | ||
38 | <span class="keyword.control">if</span> <span class="keyword">true</span> { | ||
39 | <span class="variable.mut">vec</span>.<span class="text">push</span>(<span class="type">Foo</span> { <span class="field">x</span>: <span class="literal">0</span>, <span class="field">y</span>: <span class="literal">1</span> }); | ||
40 | } | ||
41 | <span class="keyword.unsafe">unsafe</span> { <span class="variable.mut">vec</span>.<span class="text">set_len</span>(<span class="literal">0</span>); } | ||
42 | |||
43 | <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable.mut">x</span> = <span class="literal">42</span>; | ||
44 | <span class="keyword">let</span> <span class="variable.mut">y</span> = &<span class="keyword">mut</span> <span class="variable.mut">x</span>; | ||
45 | <span class="keyword">let</span> <span class="variable">z</span> = &<span class="variable.mut">y</span>; | ||
46 | |||
47 | <span class="variable.mut">y</span>; | ||
48 | }</code></pre> \ No newline at end of file | ||
diff --git a/crates/ra_ide/src/snapshots/rainbow_highlighting.html b/crates/ra_ide/src/snapshots/rainbow_highlighting.html new file mode 100644 index 000000000..79f11ea80 --- /dev/null +++ b/crates/ra_ide/src/snapshots/rainbow_highlighting.html | |||
@@ -0,0 +1,33 @@ | |||
1 | |||
2 | <style> | ||
3 | body { margin: 0; } | ||
4 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
5 | |||
6 | .comment { color: #7F9F7F; } | ||
7 | .string { color: #CC9393; } | ||
8 | .function { color: #93E0E3; } | ||
9 | .parameter { color: #94BFF3; } | ||
10 | .builtin { color: #DD6718; } | ||
11 | .text { color: #DCDCCC; } | ||
12 | .attribute { color: #94BFF3; } | ||
13 | .literal { color: #BFEBBF; } | ||
14 | .macro { color: #94BFF3; } | ||
15 | .variable { color: #DCDCCC; } | ||
16 | .variable\.mut { color: #DCDCCC; text-decoration: underline; } | ||
17 | |||
18 | .keyword { color: #F0DFAF; } | ||
19 | .keyword\.unsafe { color: #DFAF8F; } | ||
20 | .keyword\.control { color: #F0DFAF; font-weight: bold; } | ||
21 | </style> | ||
22 | <pre><code><span class="keyword">fn</span> <span class="function">main</span>() { | ||
23 | <span class="keyword">let</span> <span class="variable" data-binding-hash="8723171760279909834" style="color: hsl(307,91%,75%);">hello</span> = <span class="string">"hello"</span>; | ||
24 | <span class="keyword">let</span> <span class="variable" data-binding-hash="14702933417323009544" style="color: hsl(108,90%,49%);">x</span> = <span class="variable" data-binding-hash="8723171760279909834" style="color: hsl(307,91%,75%);">hello</span>.<span class="text">to_string</span>(); | ||
25 | <span class="keyword">let</span> <span class="variable" data-binding-hash="5443150872754369068" style="color: hsl(215,43%,43%);">y</span> = <span class="variable" data-binding-hash="8723171760279909834" style="color: hsl(307,91%,75%);">hello</span>.<span class="text">to_string</span>(); | ||
26 | |||
27 | <span class="keyword">let</span> <span class="variable" data-binding-hash="17358108296605513516" style="color: hsl(331,46%,60%);">x</span> = <span class="string">"other color please!"</span>; | ||
28 | <span class="keyword">let</span> <span class="variable" data-binding-hash="2073121142529774969" style="color: hsl(320,43%,74%);">y</span> = <span class="variable" data-binding-hash="17358108296605513516" style="color: hsl(331,46%,60%);">x</span>.<span class="text">to_string</span>(); | ||
29 | } | ||
30 | |||
31 | <span class="keyword">fn</span> <span class="function">bar</span>() { | ||
32 | <span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable.mut" data-binding-hash="8723171760279909834" style="color: hsl(307,91%,75%);">hello</span> = <span class="string">"hello"</span>; | ||
33 | }</code></pre> \ No newline at end of file | ||
diff --git a/crates/ra_ide/src/source_change.rs b/crates/ra_ide/src/source_change.rs new file mode 100644 index 000000000..f5f7f8807 --- /dev/null +++ b/crates/ra_ide/src/source_change.rs | |||
@@ -0,0 +1,119 @@ | |||
1 | //! This modules defines type to represent changes to the source code, that flow | ||
2 | //! from the server to the client. | ||
3 | //! | ||
4 | //! It can be viewed as a dual for `AnalysisChange`. | ||
5 | |||
6 | use ra_db::RelativePathBuf; | ||
7 | use ra_text_edit::TextEdit; | ||
8 | |||
9 | use crate::{FileId, FilePosition, SourceRootId, TextUnit}; | ||
10 | |||
11 | #[derive(Debug)] | ||
12 | pub struct SourceChange { | ||
13 | pub label: String, | ||
14 | pub source_file_edits: Vec<SourceFileEdit>, | ||
15 | pub file_system_edits: Vec<FileSystemEdit>, | ||
16 | pub cursor_position: Option<FilePosition>, | ||
17 | } | ||
18 | |||
19 | impl SourceChange { | ||
20 | /// Creates a new SourceChange with the given label | ||
21 | /// from the edits. | ||
22 | pub(crate) fn from_edits<L: Into<String>>( | ||
23 | label: L, | ||
24 | source_file_edits: Vec<SourceFileEdit>, | ||
25 | file_system_edits: Vec<FileSystemEdit>, | ||
26 | ) -> Self { | ||
27 | SourceChange { | ||
28 | label: label.into(), | ||
29 | source_file_edits, | ||
30 | file_system_edits, | ||
31 | cursor_position: None, | ||
32 | } | ||
33 | } | ||
34 | |||
35 | /// Creates a new SourceChange with the given label, | ||
36 | /// containing only the given `SourceFileEdits`. | ||
37 | pub(crate) fn source_file_edits<L: Into<String>>(label: L, edits: Vec<SourceFileEdit>) -> Self { | ||
38 | SourceChange { | ||
39 | label: label.into(), | ||
40 | source_file_edits: edits, | ||
41 | file_system_edits: vec![], | ||
42 | cursor_position: None, | ||
43 | } | ||
44 | } | ||
45 | |||
46 | /// Creates a new SourceChange with the given label, | ||
47 | /// containing only the given `FileSystemEdits`. | ||
48 | pub(crate) fn file_system_edits<L: Into<String>>(label: L, edits: Vec<FileSystemEdit>) -> Self { | ||
49 | SourceChange { | ||
50 | label: label.into(), | ||
51 | source_file_edits: vec![], | ||
52 | file_system_edits: edits, | ||
53 | cursor_position: None, | ||
54 | } | ||
55 | } | ||
56 | |||
57 | /// Creates a new SourceChange with the given label, | ||
58 | /// containing only a single `SourceFileEdit`. | ||
59 | pub(crate) fn source_file_edit<L: Into<String>>(label: L, edit: SourceFileEdit) -> Self { | ||
60 | SourceChange::source_file_edits(label, vec![edit]) | ||
61 | } | ||
62 | |||
63 | /// Creates a new SourceChange with the given label | ||
64 | /// from the given `FileId` and `TextEdit` | ||
65 | pub(crate) fn source_file_edit_from<L: Into<String>>( | ||
66 | label: L, | ||
67 | file_id: FileId, | ||
68 | edit: TextEdit, | ||
69 | ) -> Self { | ||
70 | SourceChange::source_file_edit(label, SourceFileEdit { file_id, edit }) | ||
71 | } | ||
72 | |||
73 | /// Creates a new SourceChange with the given label | ||
74 | /// from the given `FileId` and `TextEdit` | ||
75 | pub(crate) fn file_system_edit<L: Into<String>>(label: L, edit: FileSystemEdit) -> Self { | ||
76 | SourceChange::file_system_edits(label, vec![edit]) | ||
77 | } | ||
78 | |||
79 | /// Sets the cursor position to the given `FilePosition` | ||
80 | pub(crate) fn with_cursor(mut self, cursor_position: FilePosition) -> Self { | ||
81 | self.cursor_position = Some(cursor_position); | ||
82 | self | ||
83 | } | ||
84 | |||
85 | /// Sets the cursor position to the given `FilePosition` | ||
86 | pub(crate) fn with_cursor_opt(mut self, cursor_position: Option<FilePosition>) -> Self { | ||
87 | self.cursor_position = cursor_position; | ||
88 | self | ||
89 | } | ||
90 | } | ||
91 | |||
92 | #[derive(Debug)] | ||
93 | pub struct SourceFileEdit { | ||
94 | pub file_id: FileId, | ||
95 | pub edit: TextEdit, | ||
96 | } | ||
97 | |||
98 | #[derive(Debug)] | ||
99 | pub enum FileSystemEdit { | ||
100 | CreateFile { source_root: SourceRootId, path: RelativePathBuf }, | ||
101 | MoveFile { src: FileId, dst_source_root: SourceRootId, dst_path: RelativePathBuf }, | ||
102 | } | ||
103 | |||
104 | pub(crate) struct SingleFileChange { | ||
105 | pub label: String, | ||
106 | pub edit: TextEdit, | ||
107 | pub cursor_position: Option<TextUnit>, | ||
108 | } | ||
109 | |||
110 | impl SingleFileChange { | ||
111 | pub(crate) fn into_source_change(self, file_id: FileId) -> SourceChange { | ||
112 | SourceChange { | ||
113 | label: self.label, | ||
114 | source_file_edits: vec![SourceFileEdit { file_id, edit: self.edit }], | ||
115 | file_system_edits: Vec::new(), | ||
116 | cursor_position: self.cursor_position.map(|offset| FilePosition { file_id, offset }), | ||
117 | } | ||
118 | } | ||
119 | } | ||
diff --git a/crates/ra_ide/src/status.rs b/crates/ra_ide/src/status.rs new file mode 100644 index 000000000..1bb27eb85 --- /dev/null +++ b/crates/ra_ide/src/status.rs | |||
@@ -0,0 +1,136 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::{fmt, iter::FromIterator, sync::Arc}; | ||
4 | |||
5 | use hir::MacroFile; | ||
6 | use ra_db::{ | ||
7 | salsa::{ | ||
8 | debug::{DebugQueryTable, TableEntry}, | ||
9 | Database, | ||
10 | }, | ||
11 | FileTextQuery, SourceRootId, | ||
12 | }; | ||
13 | use ra_prof::{memory_usage, Bytes}; | ||
14 | use ra_syntax::{ast, Parse, SyntaxNode}; | ||
15 | |||
16 | use crate::{ | ||
17 | db::RootDatabase, | ||
18 | symbol_index::{LibrarySymbolsQuery, SymbolIndex}, | ||
19 | FileId, | ||
20 | }; | ||
21 | |||
22 | fn syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { | ||
23 | db.query(ra_db::ParseQuery).entries::<SyntaxTreeStats>() | ||
24 | } | ||
25 | fn macro_syntax_tree_stats(db: &RootDatabase) -> SyntaxTreeStats { | ||
26 | db.query(hir::db::ParseMacroQuery).entries::<SyntaxTreeStats>() | ||
27 | } | ||
28 | |||
29 | pub(crate) fn status(db: &RootDatabase) -> String { | ||
30 | let files_stats = db.query(FileTextQuery).entries::<FilesStats>(); | ||
31 | let syntax_tree_stats = syntax_tree_stats(db); | ||
32 | let macro_syntax_tree_stats = macro_syntax_tree_stats(db); | ||
33 | let symbols_stats = db.query(LibrarySymbolsQuery).entries::<LibrarySymbolsStats>(); | ||
34 | format!( | ||
35 | "{}\n{}\n{}\n{} (macros)\n\n\nmemory:\n{}\ngc {:?} seconds ago", | ||
36 | files_stats, | ||
37 | symbols_stats, | ||
38 | syntax_tree_stats, | ||
39 | macro_syntax_tree_stats, | ||
40 | memory_usage(), | ||
41 | db.last_gc.elapsed().as_secs(), | ||
42 | ) | ||
43 | } | ||
44 | |||
45 | #[derive(Default)] | ||
46 | struct FilesStats { | ||
47 | total: usize, | ||
48 | size: Bytes, | ||
49 | } | ||
50 | |||
51 | impl fmt::Display for FilesStats { | ||
52 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
53 | write!(fmt, "{} ({}) files", self.total, self.size) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | impl FromIterator<TableEntry<FileId, Arc<String>>> for FilesStats { | ||
58 | fn from_iter<T>(iter: T) -> FilesStats | ||
59 | where | ||
60 | T: IntoIterator<Item = TableEntry<FileId, Arc<String>>>, | ||
61 | { | ||
62 | let mut res = FilesStats::default(); | ||
63 | for entry in iter { | ||
64 | res.total += 1; | ||
65 | res.size += entry.value.unwrap().len(); | ||
66 | } | ||
67 | res | ||
68 | } | ||
69 | } | ||
70 | |||
71 | #[derive(Default)] | ||
72 | pub(crate) struct SyntaxTreeStats { | ||
73 | total: usize, | ||
74 | pub(crate) retained: usize, | ||
75 | } | ||
76 | |||
77 | impl fmt::Display for SyntaxTreeStats { | ||
78 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
79 | write!(fmt, "{} trees, {} retained", self.total, self.retained) | ||
80 | } | ||
81 | } | ||
82 | |||
83 | impl FromIterator<TableEntry<FileId, Parse<ast::SourceFile>>> for SyntaxTreeStats { | ||
84 | fn from_iter<T>(iter: T) -> SyntaxTreeStats | ||
85 | where | ||
86 | T: IntoIterator<Item = TableEntry<FileId, Parse<ast::SourceFile>>>, | ||
87 | { | ||
88 | let mut res = SyntaxTreeStats::default(); | ||
89 | for entry in iter { | ||
90 | res.total += 1; | ||
91 | res.retained += entry.value.is_some() as usize; | ||
92 | } | ||
93 | res | ||
94 | } | ||
95 | } | ||
96 | |||
97 | impl<M> FromIterator<TableEntry<MacroFile, Option<(Parse<SyntaxNode>, M)>>> for SyntaxTreeStats { | ||
98 | fn from_iter<T>(iter: T) -> SyntaxTreeStats | ||
99 | where | ||
100 | T: IntoIterator<Item = TableEntry<MacroFile, Option<(Parse<SyntaxNode>, M)>>>, | ||
101 | { | ||
102 | let mut res = SyntaxTreeStats::default(); | ||
103 | for entry in iter { | ||
104 | res.total += 1; | ||
105 | res.retained += entry.value.is_some() as usize; | ||
106 | } | ||
107 | res | ||
108 | } | ||
109 | } | ||
110 | |||
111 | #[derive(Default)] | ||
112 | struct LibrarySymbolsStats { | ||
113 | total: usize, | ||
114 | size: Bytes, | ||
115 | } | ||
116 | |||
117 | impl fmt::Display for LibrarySymbolsStats { | ||
118 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { | ||
119 | write!(fmt, "{} ({}) symbols", self.total, self.size,) | ||
120 | } | ||
121 | } | ||
122 | |||
123 | impl FromIterator<TableEntry<SourceRootId, Arc<SymbolIndex>>> for LibrarySymbolsStats { | ||
124 | fn from_iter<T>(iter: T) -> LibrarySymbolsStats | ||
125 | where | ||
126 | T: IntoIterator<Item = TableEntry<SourceRootId, Arc<SymbolIndex>>>, | ||
127 | { | ||
128 | let mut res = LibrarySymbolsStats::default(); | ||
129 | for entry in iter { | ||
130 | let value = entry.value.unwrap(); | ||
131 | res.total += value.len(); | ||
132 | res.size += value.memory_size(); | ||
133 | } | ||
134 | res | ||
135 | } | ||
136 | } | ||
diff --git a/crates/ra_ide/src/symbol_index.rs b/crates/ra_ide/src/symbol_index.rs new file mode 100644 index 000000000..5729eb5b3 --- /dev/null +++ b/crates/ra_ide/src/symbol_index.rs | |||
@@ -0,0 +1,405 @@ | |||
1 | //! This module handles fuzzy-searching of functions, structs and other symbols | ||
2 | //! by name across the whole workspace and dependencies. | ||
3 | //! | ||
4 | //! It works by building an incrementally-updated text-search index of all | ||
5 | //! symbols. The backbone of the index is the **awesome** `fst` crate by | ||
6 | //! @BurntSushi. | ||
7 | //! | ||
8 | //! In a nutshell, you give a set of strings to `fst`, and it builds a | ||
9 | //! finite state machine describing this set of strings. The strings which | ||
10 | //! could fuzzy-match a pattern can also be described by a finite state machine. | ||
11 | //! What is freaking cool is that you can now traverse both state machines in | ||
12 | //! lock-step to enumerate the strings which are both in the input set and | ||
13 | //! fuzz-match the query. Or, more formally, given two languages described by | ||
14 | //! FSTs, one can build a product FST which describes the intersection of the | ||
15 | //! languages. | ||
16 | //! | ||
17 | //! `fst` does not support cheap updating of the index, but it supports unioning | ||
18 | //! of state machines. So, to account for changing source code, we build an FST | ||
19 | //! for each library (which is assumed to never change) and an FST for each Rust | ||
20 | //! file in the current workspace, and run a query against the union of all | ||
21 | //! those FSTs. | ||
22 | use std::{ | ||
23 | fmt, | ||
24 | hash::{Hash, Hasher}, | ||
25 | mem, | ||
26 | sync::Arc, | ||
27 | }; | ||
28 | |||
29 | use fst::{self, Streamer}; | ||
30 | use ra_db::{ | ||
31 | salsa::{self, ParallelDatabase}, | ||
32 | SourceDatabaseExt, SourceRootId, | ||
33 | }; | ||
34 | use ra_syntax::{ | ||
35 | ast::{self, NameOwner}, | ||
36 | match_ast, AstNode, Parse, SmolStr, SourceFile, | ||
37 | SyntaxKind::{self, *}, | ||
38 | SyntaxNode, SyntaxNodePtr, TextRange, WalkEvent, | ||
39 | }; | ||
40 | #[cfg(not(feature = "wasm"))] | ||
41 | use rayon::prelude::*; | ||
42 | |||
43 | use crate::{db::RootDatabase, FileId, Query}; | ||
44 | |||
45 | #[salsa::query_group(SymbolsDatabaseStorage)] | ||
46 | pub(crate) trait SymbolsDatabase: hir::db::HirDatabase { | ||
47 | fn file_symbols(&self, file_id: FileId) -> Arc<SymbolIndex>; | ||
48 | #[salsa::input] | ||
49 | fn library_symbols(&self, id: SourceRootId) -> Arc<SymbolIndex>; | ||
50 | /// The set of "local" (that is, from the current workspace) roots. | ||
51 | /// Files in local roots are assumed to change frequently. | ||
52 | #[salsa::input] | ||
53 | fn local_roots(&self) -> Arc<Vec<SourceRootId>>; | ||
54 | /// The set of roots for crates.io libraries. | ||
55 | /// Files in libraries are assumed to never change. | ||
56 | #[salsa::input] | ||
57 | fn library_roots(&self) -> Arc<Vec<SourceRootId>>; | ||
58 | } | ||
59 | |||
60 | fn file_symbols(db: &impl SymbolsDatabase, file_id: FileId) -> Arc<SymbolIndex> { | ||
61 | db.check_canceled(); | ||
62 | let parse = db.parse(file_id); | ||
63 | |||
64 | let symbols = source_file_to_file_symbols(&parse.tree(), file_id); | ||
65 | |||
66 | // FIXME: add macros here | ||
67 | |||
68 | Arc::new(SymbolIndex::new(symbols)) | ||
69 | } | ||
70 | |||
71 | pub(crate) fn world_symbols(db: &RootDatabase, query: Query) -> Vec<FileSymbol> { | ||
72 | /// Need to wrap Snapshot to provide `Clone` impl for `map_with` | ||
73 | struct Snap(salsa::Snapshot<RootDatabase>); | ||
74 | impl Clone for Snap { | ||
75 | fn clone(&self) -> Snap { | ||
76 | Snap(self.0.snapshot()) | ||
77 | } | ||
78 | } | ||
79 | |||
80 | let buf: Vec<Arc<SymbolIndex>> = if query.libs { | ||
81 | let snap = Snap(db.snapshot()); | ||
82 | #[cfg(not(feature = "wasm"))] | ||
83 | let buf = db | ||
84 | .library_roots() | ||
85 | .par_iter() | ||
86 | .map_with(snap, |db, &lib_id| db.0.library_symbols(lib_id)) | ||
87 | .collect(); | ||
88 | |||
89 | #[cfg(feature = "wasm")] | ||
90 | let buf = db.library_roots().iter().map(|&lib_id| snap.0.library_symbols(lib_id)).collect(); | ||
91 | |||
92 | buf | ||
93 | } else { | ||
94 | let mut files = Vec::new(); | ||
95 | for &root in db.local_roots().iter() { | ||
96 | let sr = db.source_root(root); | ||
97 | files.extend(sr.walk()) | ||
98 | } | ||
99 | |||
100 | let snap = Snap(db.snapshot()); | ||
101 | #[cfg(not(feature = "wasm"))] | ||
102 | let buf = | ||
103 | files.par_iter().map_with(snap, |db, &file_id| db.0.file_symbols(file_id)).collect(); | ||
104 | |||
105 | #[cfg(feature = "wasm")] | ||
106 | let buf = files.iter().map(|&file_id| snap.0.file_symbols(file_id)).collect(); | ||
107 | |||
108 | buf | ||
109 | }; | ||
110 | query.search(&buf) | ||
111 | } | ||
112 | |||
113 | pub(crate) fn index_resolve(db: &RootDatabase, name_ref: &ast::NameRef) -> Vec<FileSymbol> { | ||
114 | let name = name_ref.text(); | ||
115 | let mut query = Query::new(name.to_string()); | ||
116 | query.exact(); | ||
117 | query.limit(4); | ||
118 | crate::symbol_index::world_symbols(db, query) | ||
119 | } | ||
120 | |||
121 | #[derive(Default)] | ||
122 | pub(crate) struct SymbolIndex { | ||
123 | symbols: Vec<FileSymbol>, | ||
124 | map: fst::Map, | ||
125 | } | ||
126 | |||
127 | impl fmt::Debug for SymbolIndex { | ||
128 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
129 | f.debug_struct("SymbolIndex").field("n_symbols", &self.symbols.len()).finish() | ||
130 | } | ||
131 | } | ||
132 | |||
133 | impl PartialEq for SymbolIndex { | ||
134 | fn eq(&self, other: &SymbolIndex) -> bool { | ||
135 | self.symbols == other.symbols | ||
136 | } | ||
137 | } | ||
138 | |||
139 | impl Eq for SymbolIndex {} | ||
140 | |||
141 | impl Hash for SymbolIndex { | ||
142 | fn hash<H: Hasher>(&self, hasher: &mut H) { | ||
143 | self.symbols.hash(hasher) | ||
144 | } | ||
145 | } | ||
146 | |||
147 | impl SymbolIndex { | ||
148 | fn new(mut symbols: Vec<FileSymbol>) -> SymbolIndex { | ||
149 | fn cmp_key<'a>(s1: &'a FileSymbol) -> impl Ord + 'a { | ||
150 | unicase::Ascii::new(s1.name.as_str()) | ||
151 | } | ||
152 | #[cfg(not(feature = "wasm"))] | ||
153 | symbols.par_sort_by(|s1, s2| cmp_key(s1).cmp(&cmp_key(s2))); | ||
154 | |||
155 | #[cfg(feature = "wasm")] | ||
156 | symbols.sort_by(|s1, s2| cmp_key(s1).cmp(&cmp_key(s2))); | ||
157 | |||
158 | let mut builder = fst::MapBuilder::memory(); | ||
159 | |||
160 | let mut last_batch_start = 0; | ||
161 | |||
162 | for idx in 0..symbols.len() { | ||
163 | if symbols.get(last_batch_start).map(cmp_key) == symbols.get(idx + 1).map(cmp_key) { | ||
164 | continue; | ||
165 | } | ||
166 | |||
167 | let start = last_batch_start; | ||
168 | let end = idx + 1; | ||
169 | last_batch_start = end; | ||
170 | |||
171 | let key = symbols[start].name.as_str().to_lowercase(); | ||
172 | let value = SymbolIndex::range_to_map_value(start, end); | ||
173 | |||
174 | builder.insert(key, value).unwrap(); | ||
175 | } | ||
176 | |||
177 | let map = fst::Map::from_bytes(builder.into_inner().unwrap()).unwrap(); | ||
178 | SymbolIndex { symbols, map } | ||
179 | } | ||
180 | |||
181 | pub(crate) fn len(&self) -> usize { | ||
182 | self.symbols.len() | ||
183 | } | ||
184 | |||
185 | pub(crate) fn memory_size(&self) -> usize { | ||
186 | self.map.as_fst().size() + self.symbols.len() * mem::size_of::<FileSymbol>() | ||
187 | } | ||
188 | |||
189 | #[cfg(not(feature = "wasm"))] | ||
190 | pub(crate) fn for_files( | ||
191 | files: impl ParallelIterator<Item = (FileId, Parse<ast::SourceFile>)>, | ||
192 | ) -> SymbolIndex { | ||
193 | let symbols = files | ||
194 | .flat_map(|(file_id, file)| source_file_to_file_symbols(&file.tree(), file_id)) | ||
195 | .collect::<Vec<_>>(); | ||
196 | SymbolIndex::new(symbols) | ||
197 | } | ||
198 | |||
199 | #[cfg(feature = "wasm")] | ||
200 | pub(crate) fn for_files( | ||
201 | files: impl Iterator<Item = (FileId, Parse<ast::SourceFile>)>, | ||
202 | ) -> SymbolIndex { | ||
203 | let symbols = files | ||
204 | .flat_map(|(file_id, file)| source_file_to_file_symbols(&file.tree(), file_id)) | ||
205 | .collect::<Vec<_>>(); | ||
206 | SymbolIndex::new(symbols) | ||
207 | } | ||
208 | |||
209 | fn range_to_map_value(start: usize, end: usize) -> u64 { | ||
210 | debug_assert![start <= (std::u32::MAX as usize)]; | ||
211 | debug_assert![end <= (std::u32::MAX as usize)]; | ||
212 | |||
213 | ((start as u64) << 32) | end as u64 | ||
214 | } | ||
215 | |||
216 | fn map_value_to_range(value: u64) -> (usize, usize) { | ||
217 | let end = value as u32 as usize; | ||
218 | let start = (value >> 32) as usize; | ||
219 | (start, end) | ||
220 | } | ||
221 | } | ||
222 | |||
223 | impl Query { | ||
224 | pub(crate) fn search(self, indices: &[Arc<SymbolIndex>]) -> Vec<FileSymbol> { | ||
225 | let mut op = fst::map::OpBuilder::new(); | ||
226 | for file_symbols in indices.iter() { | ||
227 | let automaton = fst::automaton::Subsequence::new(&self.lowercased); | ||
228 | op = op.add(file_symbols.map.search(automaton)) | ||
229 | } | ||
230 | let mut stream = op.union(); | ||
231 | let mut res = Vec::new(); | ||
232 | while let Some((_, indexed_values)) = stream.next() { | ||
233 | if res.len() >= self.limit { | ||
234 | break; | ||
235 | } | ||
236 | for indexed_value in indexed_values { | ||
237 | let symbol_index = &indices[indexed_value.index]; | ||
238 | let (start, end) = SymbolIndex::map_value_to_range(indexed_value.value); | ||
239 | |||
240 | for symbol in &symbol_index.symbols[start..end] { | ||
241 | if self.only_types && !is_type(symbol.ptr.kind()) { | ||
242 | continue; | ||
243 | } | ||
244 | if self.exact && symbol.name != self.query { | ||
245 | continue; | ||
246 | } | ||
247 | res.push(symbol.clone()); | ||
248 | } | ||
249 | } | ||
250 | } | ||
251 | res | ||
252 | } | ||
253 | } | ||
254 | |||
255 | fn is_type(kind: SyntaxKind) -> bool { | ||
256 | match kind { | ||
257 | STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => true, | ||
258 | _ => false, | ||
259 | } | ||
260 | } | ||
261 | |||
262 | /// The actual data that is stored in the index. It should be as compact as | ||
263 | /// possible. | ||
264 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
265 | pub(crate) struct FileSymbol { | ||
266 | pub(crate) file_id: FileId, | ||
267 | pub(crate) name: SmolStr, | ||
268 | pub(crate) ptr: SyntaxNodePtr, | ||
269 | pub(crate) name_range: Option<TextRange>, | ||
270 | pub(crate) container_name: Option<SmolStr>, | ||
271 | } | ||
272 | |||
273 | fn source_file_to_file_symbols(source_file: &SourceFile, file_id: FileId) -> Vec<FileSymbol> { | ||
274 | let mut symbols = Vec::new(); | ||
275 | let mut stack = Vec::new(); | ||
276 | |||
277 | for event in source_file.syntax().preorder() { | ||
278 | match event { | ||
279 | WalkEvent::Enter(node) => { | ||
280 | if let Some(mut symbol) = to_file_symbol(&node, file_id) { | ||
281 | symbol.container_name = stack.last().cloned(); | ||
282 | |||
283 | stack.push(symbol.name.clone()); | ||
284 | symbols.push(symbol); | ||
285 | } | ||
286 | } | ||
287 | |||
288 | WalkEvent::Leave(node) => { | ||
289 | if to_symbol(&node).is_some() { | ||
290 | stack.pop(); | ||
291 | } | ||
292 | } | ||
293 | } | ||
294 | } | ||
295 | |||
296 | symbols | ||
297 | } | ||
298 | |||
299 | fn to_symbol(node: &SyntaxNode) -> Option<(SmolStr, SyntaxNodePtr, TextRange)> { | ||
300 | fn decl<N: NameOwner>(node: N) -> Option<(SmolStr, SyntaxNodePtr, TextRange)> { | ||
301 | let name = node.name()?; | ||
302 | let name_range = name.syntax().text_range(); | ||
303 | let name = name.text().clone(); | ||
304 | let ptr = SyntaxNodePtr::new(node.syntax()); | ||
305 | |||
306 | Some((name, ptr, name_range)) | ||
307 | } | ||
308 | match_ast! { | ||
309 | match node { | ||
310 | ast::FnDef(it) => { decl(it) }, | ||
311 | ast::StructDef(it) => { decl(it) }, | ||
312 | ast::EnumDef(it) => { decl(it) }, | ||
313 | ast::TraitDef(it) => { decl(it) }, | ||
314 | ast::Module(it) => { decl(it) }, | ||
315 | ast::TypeAliasDef(it) => { decl(it) }, | ||
316 | ast::ConstDef(it) => { decl(it) }, | ||
317 | ast::StaticDef(it) => { decl(it) }, | ||
318 | _ => None, | ||
319 | } | ||
320 | } | ||
321 | } | ||
322 | |||
323 | fn to_file_symbol(node: &SyntaxNode, file_id: FileId) -> Option<FileSymbol> { | ||
324 | to_symbol(node).map(move |(name, ptr, name_range)| FileSymbol { | ||
325 | name, | ||
326 | ptr, | ||
327 | file_id, | ||
328 | name_range: Some(name_range), | ||
329 | container_name: None, | ||
330 | }) | ||
331 | } | ||
332 | |||
333 | #[cfg(test)] | ||
334 | mod tests { | ||
335 | use crate::{display::NavigationTarget, mock_analysis::single_file, Query}; | ||
336 | use ra_syntax::{ | ||
337 | SmolStr, | ||
338 | SyntaxKind::{FN_DEF, STRUCT_DEF}, | ||
339 | }; | ||
340 | |||
341 | #[test] | ||
342 | fn test_world_symbols_with_no_container() { | ||
343 | let code = r#" | ||
344 | enum FooInner { } | ||
345 | "#; | ||
346 | |||
347 | let mut symbols = get_symbols_matching(code, "FooInner"); | ||
348 | |||
349 | let s = symbols.pop().unwrap(); | ||
350 | |||
351 | assert_eq!(s.name(), "FooInner"); | ||
352 | assert!(s.container_name().is_none()); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn test_world_symbols_include_container_name() { | ||
357 | let code = r#" | ||
358 | fn foo() { | ||
359 | enum FooInner { } | ||
360 | } | ||
361 | "#; | ||
362 | |||
363 | let mut symbols = get_symbols_matching(code, "FooInner"); | ||
364 | |||
365 | let s = symbols.pop().unwrap(); | ||
366 | |||
367 | assert_eq!(s.name(), "FooInner"); | ||
368 | assert_eq!(s.container_name(), Some(&SmolStr::new("foo"))); | ||
369 | |||
370 | let code = r#" | ||
371 | mod foo { | ||
372 | struct FooInner; | ||
373 | } | ||
374 | "#; | ||
375 | |||
376 | let mut symbols = get_symbols_matching(code, "FooInner"); | ||
377 | |||
378 | let s = symbols.pop().unwrap(); | ||
379 | |||
380 | assert_eq!(s.name(), "FooInner"); | ||
381 | assert_eq!(s.container_name(), Some(&SmolStr::new("foo"))); | ||
382 | } | ||
383 | |||
384 | #[test] | ||
385 | fn test_world_symbols_are_case_sensitive() { | ||
386 | let code = r#" | ||
387 | fn foo() {} | ||
388 | |||
389 | struct Foo; | ||
390 | "#; | ||
391 | |||
392 | let symbols = get_symbols_matching(code, "Foo"); | ||
393 | |||
394 | let fn_match = symbols.iter().find(|s| s.name() == "foo").map(|s| s.kind()); | ||
395 | let struct_match = symbols.iter().find(|s| s.name() == "Foo").map(|s| s.kind()); | ||
396 | |||
397 | assert_eq!(fn_match, Some(FN_DEF)); | ||
398 | assert_eq!(struct_match, Some(STRUCT_DEF)); | ||
399 | } | ||
400 | |||
401 | fn get_symbols_matching(text: &str, query: &str) -> Vec<NavigationTarget> { | ||
402 | let (analysis, _) = single_file(text); | ||
403 | analysis.symbol_search(Query::new(query.into())).unwrap() | ||
404 | } | ||
405 | } | ||
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs new file mode 100644 index 000000000..9a3e4c82f --- /dev/null +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -0,0 +1,341 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
4 | |||
5 | use hir::{Name, Source}; | ||
6 | use ra_db::SourceDatabase; | ||
7 | use ra_prof::profile; | ||
8 | use ra_syntax::{ast, AstNode, Direction, SyntaxElement, SyntaxKind, SyntaxKind::*, TextRange, T}; | ||
9 | |||
10 | use crate::{ | ||
11 | db::RootDatabase, | ||
12 | references::{ | ||
13 | classify_name, classify_name_ref, | ||
14 | NameKind::{self, *}, | ||
15 | }, | ||
16 | FileId, | ||
17 | }; | ||
18 | |||
19 | #[derive(Debug)] | ||
20 | pub struct HighlightedRange { | ||
21 | pub range: TextRange, | ||
22 | pub tag: &'static str, | ||
23 | pub binding_hash: Option<u64>, | ||
24 | } | ||
25 | |||
26 | fn is_control_keyword(kind: SyntaxKind) -> bool { | ||
27 | match kind { | ||
28 | T![for] | ||
29 | | T![loop] | ||
30 | | T![while] | ||
31 | | T![continue] | ||
32 | | T![break] | ||
33 | | T![if] | ||
34 | | T![else] | ||
35 | | T![match] | ||
36 | | T![return] => true, | ||
37 | _ => false, | ||
38 | } | ||
39 | } | ||
40 | |||
41 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { | ||
42 | let _p = profile("highlight"); | ||
43 | let parse = db.parse(file_id); | ||
44 | let root = parse.tree().syntax().clone(); | ||
45 | |||
46 | fn calc_binding_hash(file_id: FileId, name: &Name, shadow_count: u32) -> u64 { | ||
47 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { | ||
48 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | ||
49 | |||
50 | let mut hasher = DefaultHasher::new(); | ||
51 | x.hash(&mut hasher); | ||
52 | hasher.finish() | ||
53 | } | ||
54 | |||
55 | hash((file_id, name, shadow_count)) | ||
56 | } | ||
57 | |||
58 | // Visited nodes to handle highlighting priorities | ||
59 | // FIXME: retain only ranges here | ||
60 | let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default(); | ||
61 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); | ||
62 | |||
63 | let mut res = Vec::new(); | ||
64 | for node in root.descendants_with_tokens() { | ||
65 | if highlighted.contains(&node) { | ||
66 | continue; | ||
67 | } | ||
68 | let mut binding_hash = None; | ||
69 | let tag = match node.kind() { | ||
70 | FN_DEF => { | ||
71 | bindings_shadow_count.clear(); | ||
72 | continue; | ||
73 | } | ||
74 | COMMENT => "comment", | ||
75 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | ||
76 | ATTR => "attribute", | ||
77 | NAME_REF => { | ||
78 | if node.ancestors().any(|it| it.kind() == ATTR) { | ||
79 | continue; | ||
80 | } | ||
81 | |||
82 | let name_ref = node.as_node().cloned().and_then(ast::NameRef::cast).unwrap(); | ||
83 | let name_kind = | ||
84 | classify_name_ref(db, Source::new(file_id.into(), &name_ref)).map(|d| d.kind); | ||
85 | |||
86 | if let Some(Local(local)) = &name_kind { | ||
87 | if let Some(name) = local.name(db) { | ||
88 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | ||
89 | binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count)) | ||
90 | } | ||
91 | }; | ||
92 | |||
93 | name_kind.map_or("text", |it| highlight_name(db, it)) | ||
94 | } | ||
95 | NAME => { | ||
96 | let name = node.as_node().cloned().and_then(ast::Name::cast).unwrap(); | ||
97 | let name_kind = | ||
98 | classify_name(db, Source::new(file_id.into(), &name)).map(|d| d.kind); | ||
99 | |||
100 | if let Some(Local(local)) = &name_kind { | ||
101 | if let Some(name) = local.name(db) { | ||
102 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | ||
103 | *shadow_count += 1; | ||
104 | binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count)) | ||
105 | } | ||
106 | }; | ||
107 | |||
108 | match name_kind { | ||
109 | Some(name_kind) => highlight_name(db, name_kind), | ||
110 | None => name.syntax().parent().map_or("function", |x| match x.kind() { | ||
111 | TYPE_PARAM | STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => "type", | ||
112 | RECORD_FIELD_DEF => "field", | ||
113 | _ => "function", | ||
114 | }), | ||
115 | } | ||
116 | } | ||
117 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | ||
118 | LIFETIME => "parameter", | ||
119 | T![unsafe] => "keyword.unsafe", | ||
120 | k if is_control_keyword(k) => "keyword.control", | ||
121 | k if k.is_keyword() => "keyword", | ||
122 | _ => { | ||
123 | if let Some(macro_call) = node.as_node().cloned().and_then(ast::MacroCall::cast) { | ||
124 | if let Some(path) = macro_call.path() { | ||
125 | if let Some(segment) = path.segment() { | ||
126 | if let Some(name_ref) = segment.name_ref() { | ||
127 | highlighted.insert(name_ref.syntax().clone().into()); | ||
128 | let range_start = name_ref.syntax().text_range().start(); | ||
129 | let mut range_end = name_ref.syntax().text_range().end(); | ||
130 | for sibling in path.syntax().siblings_with_tokens(Direction::Next) { | ||
131 | match sibling.kind() { | ||
132 | T![!] | IDENT => range_end = sibling.text_range().end(), | ||
133 | _ => (), | ||
134 | } | ||
135 | } | ||
136 | res.push(HighlightedRange { | ||
137 | range: TextRange::from_to(range_start, range_end), | ||
138 | tag: "macro", | ||
139 | binding_hash: None, | ||
140 | }) | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | continue; | ||
146 | } | ||
147 | }; | ||
148 | res.push(HighlightedRange { range: node.text_range(), tag, binding_hash }) | ||
149 | } | ||
150 | res | ||
151 | } | ||
152 | |||
153 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { | ||
154 | let parse = db.parse(file_id); | ||
155 | |||
156 | fn rainbowify(seed: u64) -> String { | ||
157 | use rand::prelude::*; | ||
158 | let mut rng = SmallRng::seed_from_u64(seed); | ||
159 | format!( | ||
160 | "hsl({h},{s}%,{l}%)", | ||
161 | h = rng.gen_range::<u16, _, _>(0, 361), | ||
162 | s = rng.gen_range::<u16, _, _>(42, 99), | ||
163 | l = rng.gen_range::<u16, _, _>(40, 91), | ||
164 | ) | ||
165 | } | ||
166 | |||
167 | let mut ranges = highlight(db, file_id); | ||
168 | ranges.sort_by_key(|it| it.range.start()); | ||
169 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges | ||
170 | let mut frontier = 0; | ||
171 | let mut could_intersect: Vec<&HighlightedRange> = Vec::new(); | ||
172 | |||
173 | let mut buf = String::new(); | ||
174 | buf.push_str(&STYLE); | ||
175 | buf.push_str("<pre><code>"); | ||
176 | let tokens = parse.tree().syntax().descendants_with_tokens().filter_map(|it| it.into_token()); | ||
177 | for token in tokens { | ||
178 | could_intersect.retain(|it| token.text_range().start() <= it.range.end()); | ||
179 | while let Some(r) = ranges.get(frontier) { | ||
180 | if r.range.start() <= token.text_range().end() { | ||
181 | could_intersect.push(r); | ||
182 | frontier += 1; | ||
183 | } else { | ||
184 | break; | ||
185 | } | ||
186 | } | ||
187 | let text = html_escape(&token.text()); | ||
188 | let ranges = could_intersect | ||
189 | .iter() | ||
190 | .filter(|it| token.text_range().is_subrange(&it.range)) | ||
191 | .collect::<Vec<_>>(); | ||
192 | if ranges.is_empty() { | ||
193 | buf.push_str(&text); | ||
194 | } else { | ||
195 | let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" "); | ||
196 | let binding_hash = ranges.first().and_then(|x| x.binding_hash); | ||
197 | let color = match (rainbow, binding_hash) { | ||
198 | (true, Some(hash)) => format!( | ||
199 | " data-binding-hash=\"{}\" style=\"color: {};\"", | ||
200 | hash, | ||
201 | rainbowify(hash) | ||
202 | ), | ||
203 | _ => "".into(), | ||
204 | }; | ||
205 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text)); | ||
206 | } | ||
207 | } | ||
208 | buf.push_str("</code></pre>"); | ||
209 | buf | ||
210 | } | ||
211 | |||
212 | fn highlight_name(db: &RootDatabase, name_kind: NameKind) -> &'static str { | ||
213 | match name_kind { | ||
214 | Macro(_) => "macro", | ||
215 | Field(_) => "field", | ||
216 | AssocItem(hir::AssocItem::Function(_)) => "function", | ||
217 | AssocItem(hir::AssocItem::Const(_)) => "constant", | ||
218 | AssocItem(hir::AssocItem::TypeAlias(_)) => "type", | ||
219 | Def(hir::ModuleDef::Module(_)) => "module", | ||
220 | Def(hir::ModuleDef::Function(_)) => "function", | ||
221 | Def(hir::ModuleDef::Adt(_)) => "type", | ||
222 | Def(hir::ModuleDef::EnumVariant(_)) => "constant", | ||
223 | Def(hir::ModuleDef::Const(_)) => "constant", | ||
224 | Def(hir::ModuleDef::Static(_)) => "constant", | ||
225 | Def(hir::ModuleDef::Trait(_)) => "type", | ||
226 | Def(hir::ModuleDef::TypeAlias(_)) => "type", | ||
227 | Def(hir::ModuleDef::BuiltinType(_)) => "type", | ||
228 | SelfType(_) => "type", | ||
229 | GenericParam(_) => "type", | ||
230 | Local(local) => { | ||
231 | if local.is_mut(db) { | ||
232 | "variable.mut" | ||
233 | } else if local.ty(db).is_mutable_reference() { | ||
234 | "variable.mut" | ||
235 | } else { | ||
236 | "variable" | ||
237 | } | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | //FIXME: like, real html escaping | ||
243 | fn html_escape(text: &str) -> String { | ||
244 | text.replace("<", "<").replace(">", ">") | ||
245 | } | ||
246 | |||
247 | const STYLE: &str = " | ||
248 | <style> | ||
249 | body { margin: 0; } | ||
250 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
251 | |||
252 | .comment { color: #7F9F7F; } | ||
253 | .string { color: #CC9393; } | ||
254 | .function { color: #93E0E3; } | ||
255 | .parameter { color: #94BFF3; } | ||
256 | .builtin { color: #DD6718; } | ||
257 | .text { color: #DCDCCC; } | ||
258 | .attribute { color: #94BFF3; } | ||
259 | .literal { color: #BFEBBF; } | ||
260 | .macro { color: #94BFF3; } | ||
261 | .variable { color: #DCDCCC; } | ||
262 | .variable\\.mut { color: #DCDCCC; text-decoration: underline; } | ||
263 | |||
264 | .keyword { color: #F0DFAF; } | ||
265 | .keyword\\.unsafe { color: #DFAF8F; } | ||
266 | .keyword\\.control { color: #F0DFAF; font-weight: bold; } | ||
267 | </style> | ||
268 | "; | ||
269 | |||
270 | #[cfg(test)] | ||
271 | mod tests { | ||
272 | use crate::mock_analysis::single_file; | ||
273 | use test_utils::{assert_eq_text, project_dir, read_text}; | ||
274 | |||
275 | #[test] | ||
276 | fn test_highlighting() { | ||
277 | let (analysis, file_id) = single_file( | ||
278 | r#" | ||
279 | #[derive(Clone, Debug)] | ||
280 | struct Foo { | ||
281 | pub x: i32, | ||
282 | pub y: i32, | ||
283 | } | ||
284 | |||
285 | fn foo<T>() -> T { | ||
286 | unimplemented!(); | ||
287 | foo::<i32>(); | ||
288 | } | ||
289 | |||
290 | // comment | ||
291 | fn main() { | ||
292 | println!("Hello, {}!", 92); | ||
293 | |||
294 | let mut vec = Vec::new(); | ||
295 | if true { | ||
296 | vec.push(Foo { x: 0, y: 1 }); | ||
297 | } | ||
298 | unsafe { vec.set_len(0); } | ||
299 | |||
300 | let mut x = 42; | ||
301 | let y = &mut x; | ||
302 | let z = &y; | ||
303 | |||
304 | y; | ||
305 | } | ||
306 | "# | ||
307 | .trim(), | ||
308 | ); | ||
309 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html"); | ||
310 | let actual_html = &analysis.highlight_as_html(file_id, false).unwrap(); | ||
311 | let expected_html = &read_text(&dst_file); | ||
312 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
313 | assert_eq_text!(expected_html, actual_html); | ||
314 | } | ||
315 | |||
316 | #[test] | ||
317 | fn test_rainbow_highlighting() { | ||
318 | let (analysis, file_id) = single_file( | ||
319 | r#" | ||
320 | fn main() { | ||
321 | let hello = "hello"; | ||
322 | let x = hello.to_string(); | ||
323 | let y = hello.to_string(); | ||
324 | |||
325 | let x = "other color please!"; | ||
326 | let y = x.to_string(); | ||
327 | } | ||
328 | |||
329 | fn bar() { | ||
330 | let mut hello = "hello"; | ||
331 | } | ||
332 | "# | ||
333 | .trim(), | ||
334 | ); | ||
335 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html"); | ||
336 | let actual_html = &analysis.highlight_as_html(file_id, true).unwrap(); | ||
337 | let expected_html = &read_text(&dst_file); | ||
338 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
339 | assert_eq_text!(expected_html, actual_html); | ||
340 | } | ||
341 | } | ||
diff --git a/crates/ra_ide/src/syntax_tree.rs b/crates/ra_ide/src/syntax_tree.rs new file mode 100644 index 000000000..4d0f0fc47 --- /dev/null +++ b/crates/ra_ide/src/syntax_tree.rs | |||
@@ -0,0 +1,359 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use crate::db::RootDatabase; | ||
4 | use ra_db::SourceDatabase; | ||
5 | use ra_syntax::{ | ||
6 | algo, AstNode, NodeOrToken, SourceFile, | ||
7 | SyntaxKind::{RAW_STRING, STRING}, | ||
8 | SyntaxToken, TextRange, | ||
9 | }; | ||
10 | |||
11 | pub use ra_db::FileId; | ||
12 | |||
13 | pub(crate) fn syntax_tree( | ||
14 | db: &RootDatabase, | ||
15 | file_id: FileId, | ||
16 | text_range: Option<TextRange>, | ||
17 | ) -> String { | ||
18 | let parse = db.parse(file_id); | ||
19 | if let Some(text_range) = text_range { | ||
20 | let node = match algo::find_covering_element(parse.tree().syntax(), text_range) { | ||
21 | NodeOrToken::Node(node) => node, | ||
22 | NodeOrToken::Token(token) => { | ||
23 | if let Some(tree) = syntax_tree_for_string(&token, text_range) { | ||
24 | return tree; | ||
25 | } | ||
26 | token.parent() | ||
27 | } | ||
28 | }; | ||
29 | |||
30 | format!("{:#?}", node) | ||
31 | } else { | ||
32 | format!("{:#?}", parse.tree().syntax()) | ||
33 | } | ||
34 | } | ||
35 | |||
36 | /// Attempts parsing the selected contents of a string literal | ||
37 | /// as rust syntax and returns its syntax tree | ||
38 | fn syntax_tree_for_string(token: &SyntaxToken, text_range: TextRange) -> Option<String> { | ||
39 | // When the range is inside a string | ||
40 | // we'll attempt parsing it as rust syntax | ||
41 | // to provide the syntax tree of the contents of the string | ||
42 | match token.kind() { | ||
43 | STRING | RAW_STRING => syntax_tree_for_token(token, text_range), | ||
44 | _ => None, | ||
45 | } | ||
46 | } | ||
47 | |||
48 | fn syntax_tree_for_token(node: &SyntaxToken, text_range: TextRange) -> Option<String> { | ||
49 | // Range of the full node | ||
50 | let node_range = node.text_range(); | ||
51 | let text = node.text().to_string(); | ||
52 | |||
53 | // We start at some point inside the node | ||
54 | // Either we have selected the whole string | ||
55 | // or our selection is inside it | ||
56 | let start = text_range.start() - node_range.start(); | ||
57 | |||
58 | // how many characters we have selected | ||
59 | let len = text_range.len().to_usize(); | ||
60 | |||
61 | let node_len = node_range.len().to_usize(); | ||
62 | |||
63 | let start = start.to_usize(); | ||
64 | |||
65 | // We want to cap our length | ||
66 | let len = len.min(node_len); | ||
67 | |||
68 | // Ensure our slice is inside the actual string | ||
69 | let end = if start + len < text.len() { start + len } else { text.len() - start }; | ||
70 | |||
71 | let text = &text[start..end]; | ||
72 | |||
73 | // Remove possible extra string quotes from the start | ||
74 | // and the end of the string | ||
75 | let text = text | ||
76 | .trim_start_matches('r') | ||
77 | .trim_start_matches('#') | ||
78 | .trim_start_matches('"') | ||
79 | .trim_end_matches('#') | ||
80 | .trim_end_matches('"') | ||
81 | .trim() | ||
82 | // Remove custom markers | ||
83 | .replace("<|>", ""); | ||
84 | |||
85 | let parsed = SourceFile::parse(&text); | ||
86 | |||
87 | // If the "file" parsed without errors, | ||
88 | // return its syntax | ||
89 | if parsed.errors().is_empty() { | ||
90 | return Some(format!("{:#?}", parsed.tree().syntax())); | ||
91 | } | ||
92 | |||
93 | None | ||
94 | } | ||
95 | |||
96 | #[cfg(test)] | ||
97 | mod tests { | ||
98 | use test_utils::assert_eq_text; | ||
99 | |||
100 | use crate::mock_analysis::{single_file, single_file_with_range}; | ||
101 | |||
102 | #[test] | ||
103 | fn test_syntax_tree_without_range() { | ||
104 | // Basic syntax | ||
105 | let (analysis, file_id) = single_file(r#"fn foo() {}"#); | ||
106 | let syn = analysis.syntax_tree(file_id, None).unwrap(); | ||
107 | |||
108 | assert_eq_text!( | ||
109 | syn.trim(), | ||
110 | r#" | ||
111 | SOURCE_FILE@[0; 11) | ||
112 | FN_DEF@[0; 11) | ||
113 | FN_KW@[0; 2) "fn" | ||
114 | WHITESPACE@[2; 3) " " | ||
115 | NAME@[3; 6) | ||
116 | IDENT@[3; 6) "foo" | ||
117 | PARAM_LIST@[6; 8) | ||
118 | L_PAREN@[6; 7) "(" | ||
119 | R_PAREN@[7; 8) ")" | ||
120 | WHITESPACE@[8; 9) " " | ||
121 | BLOCK_EXPR@[9; 11) | ||
122 | BLOCK@[9; 11) | ||
123 | L_CURLY@[9; 10) "{" | ||
124 | R_CURLY@[10; 11) "}" | ||
125 | "# | ||
126 | .trim() | ||
127 | ); | ||
128 | |||
129 | let (analysis, file_id) = single_file( | ||
130 | r#" | ||
131 | fn test() { | ||
132 | assert!(" | ||
133 | fn foo() { | ||
134 | } | ||
135 | ", ""); | ||
136 | }"# | ||
137 | .trim(), | ||
138 | ); | ||
139 | let syn = analysis.syntax_tree(file_id, None).unwrap(); | ||
140 | |||
141 | assert_eq_text!( | ||
142 | syn.trim(), | ||
143 | r#" | ||
144 | SOURCE_FILE@[0; 60) | ||
145 | FN_DEF@[0; 60) | ||
146 | FN_KW@[0; 2) "fn" | ||
147 | WHITESPACE@[2; 3) " " | ||
148 | NAME@[3; 7) | ||
149 | IDENT@[3; 7) "test" | ||
150 | PARAM_LIST@[7; 9) | ||
151 | L_PAREN@[7; 8) "(" | ||
152 | R_PAREN@[8; 9) ")" | ||
153 | WHITESPACE@[9; 10) " " | ||
154 | BLOCK_EXPR@[10; 60) | ||
155 | BLOCK@[10; 60) | ||
156 | L_CURLY@[10; 11) "{" | ||
157 | WHITESPACE@[11; 16) "\n " | ||
158 | EXPR_STMT@[16; 58) | ||
159 | MACRO_CALL@[16; 57) | ||
160 | PATH@[16; 22) | ||
161 | PATH_SEGMENT@[16; 22) | ||
162 | NAME_REF@[16; 22) | ||
163 | IDENT@[16; 22) "assert" | ||
164 | EXCL@[22; 23) "!" | ||
165 | TOKEN_TREE@[23; 57) | ||
166 | L_PAREN@[23; 24) "(" | ||
167 | STRING@[24; 52) "\"\n fn foo() {\n ..." | ||
168 | COMMA@[52; 53) "," | ||
169 | WHITESPACE@[53; 54) " " | ||
170 | STRING@[54; 56) "\"\"" | ||
171 | R_PAREN@[56; 57) ")" | ||
172 | SEMI@[57; 58) ";" | ||
173 | WHITESPACE@[58; 59) "\n" | ||
174 | R_CURLY@[59; 60) "}" | ||
175 | "# | ||
176 | .trim() | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | #[test] | ||
181 | fn test_syntax_tree_with_range() { | ||
182 | let (analysis, range) = single_file_with_range(r#"<|>fn foo() {}<|>"#.trim()); | ||
183 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
184 | |||
185 | assert_eq_text!( | ||
186 | syn.trim(), | ||
187 | r#" | ||
188 | FN_DEF@[0; 11) | ||
189 | FN_KW@[0; 2) "fn" | ||
190 | WHITESPACE@[2; 3) " " | ||
191 | NAME@[3; 6) | ||
192 | IDENT@[3; 6) "foo" | ||
193 | PARAM_LIST@[6; 8) | ||
194 | L_PAREN@[6; 7) "(" | ||
195 | R_PAREN@[7; 8) ")" | ||
196 | WHITESPACE@[8; 9) " " | ||
197 | BLOCK_EXPR@[9; 11) | ||
198 | BLOCK@[9; 11) | ||
199 | L_CURLY@[9; 10) "{" | ||
200 | R_CURLY@[10; 11) "}" | ||
201 | "# | ||
202 | .trim() | ||
203 | ); | ||
204 | |||
205 | let (analysis, range) = single_file_with_range( | ||
206 | r#"fn test() { | ||
207 | <|>assert!(" | ||
208 | fn foo() { | ||
209 | } | ||
210 | ", "");<|> | ||
211 | }"# | ||
212 | .trim(), | ||
213 | ); | ||
214 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
215 | |||
216 | assert_eq_text!( | ||
217 | syn.trim(), | ||
218 | r#" | ||
219 | EXPR_STMT@[16; 58) | ||
220 | MACRO_CALL@[16; 57) | ||
221 | PATH@[16; 22) | ||
222 | PATH_SEGMENT@[16; 22) | ||
223 | NAME_REF@[16; 22) | ||
224 | IDENT@[16; 22) "assert" | ||
225 | EXCL@[22; 23) "!" | ||
226 | TOKEN_TREE@[23; 57) | ||
227 | L_PAREN@[23; 24) "(" | ||
228 | STRING@[24; 52) "\"\n fn foo() {\n ..." | ||
229 | COMMA@[52; 53) "," | ||
230 | WHITESPACE@[53; 54) " " | ||
231 | STRING@[54; 56) "\"\"" | ||
232 | R_PAREN@[56; 57) ")" | ||
233 | SEMI@[57; 58) ";" | ||
234 | "# | ||
235 | .trim() | ||
236 | ); | ||
237 | } | ||
238 | |||
239 | #[test] | ||
240 | fn test_syntax_tree_inside_string() { | ||
241 | let (analysis, range) = single_file_with_range( | ||
242 | r#"fn test() { | ||
243 | assert!(" | ||
244 | <|>fn foo() { | ||
245 | }<|> | ||
246 | fn bar() { | ||
247 | } | ||
248 | ", ""); | ||
249 | }"# | ||
250 | .trim(), | ||
251 | ); | ||
252 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
253 | assert_eq_text!( | ||
254 | syn.trim(), | ||
255 | r#" | ||
256 | SOURCE_FILE@[0; 12) | ||
257 | FN_DEF@[0; 12) | ||
258 | FN_KW@[0; 2) "fn" | ||
259 | WHITESPACE@[2; 3) " " | ||
260 | NAME@[3; 6) | ||
261 | IDENT@[3; 6) "foo" | ||
262 | PARAM_LIST@[6; 8) | ||
263 | L_PAREN@[6; 7) "(" | ||
264 | R_PAREN@[7; 8) ")" | ||
265 | WHITESPACE@[8; 9) " " | ||
266 | BLOCK_EXPR@[9; 12) | ||
267 | BLOCK@[9; 12) | ||
268 | L_CURLY@[9; 10) "{" | ||
269 | WHITESPACE@[10; 11) "\n" | ||
270 | R_CURLY@[11; 12) "}" | ||
271 | "# | ||
272 | .trim() | ||
273 | ); | ||
274 | |||
275 | // With a raw string | ||
276 | let (analysis, range) = single_file_with_range( | ||
277 | r###"fn test() { | ||
278 | assert!(r#" | ||
279 | <|>fn foo() { | ||
280 | }<|> | ||
281 | fn bar() { | ||
282 | } | ||
283 | "#, ""); | ||
284 | }"### | ||
285 | .trim(), | ||
286 | ); | ||
287 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
288 | assert_eq_text!( | ||
289 | syn.trim(), | ||
290 | r#" | ||
291 | SOURCE_FILE@[0; 12) | ||
292 | FN_DEF@[0; 12) | ||
293 | FN_KW@[0; 2) "fn" | ||
294 | WHITESPACE@[2; 3) " " | ||
295 | NAME@[3; 6) | ||
296 | IDENT@[3; 6) "foo" | ||
297 | PARAM_LIST@[6; 8) | ||
298 | L_PAREN@[6; 7) "(" | ||
299 | R_PAREN@[7; 8) ")" | ||
300 | WHITESPACE@[8; 9) " " | ||
301 | BLOCK_EXPR@[9; 12) | ||
302 | BLOCK@[9; 12) | ||
303 | L_CURLY@[9; 10) "{" | ||
304 | WHITESPACE@[10; 11) "\n" | ||
305 | R_CURLY@[11; 12) "}" | ||
306 | "# | ||
307 | .trim() | ||
308 | ); | ||
309 | |||
310 | // With a raw string | ||
311 | let (analysis, range) = single_file_with_range( | ||
312 | r###"fn test() { | ||
313 | assert!(r<|>#" | ||
314 | fn foo() { | ||
315 | } | ||
316 | fn bar() { | ||
317 | }"<|>#, ""); | ||
318 | }"### | ||
319 | .trim(), | ||
320 | ); | ||
321 | let syn = analysis.syntax_tree(range.file_id, Some(range.range)).unwrap(); | ||
322 | assert_eq_text!( | ||
323 | syn.trim(), | ||
324 | r#" | ||
325 | SOURCE_FILE@[0; 25) | ||
326 | FN_DEF@[0; 12) | ||
327 | FN_KW@[0; 2) "fn" | ||
328 | WHITESPACE@[2; 3) " " | ||
329 | NAME@[3; 6) | ||
330 | IDENT@[3; 6) "foo" | ||
331 | PARAM_LIST@[6; 8) | ||
332 | L_PAREN@[6; 7) "(" | ||
333 | R_PAREN@[7; 8) ")" | ||
334 | WHITESPACE@[8; 9) " " | ||
335 | BLOCK_EXPR@[9; 12) | ||
336 | BLOCK@[9; 12) | ||
337 | L_CURLY@[9; 10) "{" | ||
338 | WHITESPACE@[10; 11) "\n" | ||
339 | R_CURLY@[11; 12) "}" | ||
340 | WHITESPACE@[12; 13) "\n" | ||
341 | FN_DEF@[13; 25) | ||
342 | FN_KW@[13; 15) "fn" | ||
343 | WHITESPACE@[15; 16) " " | ||
344 | NAME@[16; 19) | ||
345 | IDENT@[16; 19) "bar" | ||
346 | PARAM_LIST@[19; 21) | ||
347 | L_PAREN@[19; 20) "(" | ||
348 | R_PAREN@[20; 21) ")" | ||
349 | WHITESPACE@[21; 22) " " | ||
350 | BLOCK_EXPR@[22; 25) | ||
351 | BLOCK@[22; 25) | ||
352 | L_CURLY@[22; 23) "{" | ||
353 | WHITESPACE@[23; 24) "\n" | ||
354 | R_CURLY@[24; 25) "}" | ||
355 | "# | ||
356 | .trim() | ||
357 | ); | ||
358 | } | ||
359 | } | ||
diff --git a/crates/ra_ide/src/test_utils.rs b/crates/ra_ide/src/test_utils.rs new file mode 100644 index 000000000..8adb214d4 --- /dev/null +++ b/crates/ra_ide/src/test_utils.rs | |||
@@ -0,0 +1,21 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use ra_syntax::{SourceFile, TextUnit}; | ||
4 | use ra_text_edit::TextEdit; | ||
5 | |||
6 | pub use test_utils::*; | ||
7 | |||
8 | pub fn check_action<F: Fn(&SourceFile, TextUnit) -> Option<TextEdit>>( | ||
9 | before: &str, | ||
10 | after: &str, | ||
11 | f: F, | ||
12 | ) { | ||
13 | let (before_cursor_pos, before) = extract_offset(before); | ||
14 | let file = SourceFile::parse(&before).ok().unwrap(); | ||
15 | let result = f(&file, before_cursor_pos).expect("code action is not applicable"); | ||
16 | let actual = result.apply(&before); | ||
17 | let actual_cursor_pos = | ||
18 | result.apply_to_offset(before_cursor_pos).expect("cursor position is affected by the edit"); | ||
19 | let actual = add_cursor(&actual, actual_cursor_pos); | ||
20 | assert_eq_text!(after, &actual); | ||
21 | } | ||
diff --git a/crates/ra_ide/src/typing.rs b/crates/ra_ide/src/typing.rs new file mode 100644 index 000000000..21e5be9b3 --- /dev/null +++ b/crates/ra_ide/src/typing.rs | |||
@@ -0,0 +1,490 @@ | |||
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 | use ra_db::{FilePosition, SourceDatabase}; | ||
17 | use ra_fmt::leading_indent; | ||
18 | use ra_syntax::{ | ||
19 | algo::find_node_at_offset, | ||
20 | ast::{self, AstToken}, | ||
21 | AstNode, SmolStr, SourceFile, | ||
22 | SyntaxKind::*, | ||
23 | SyntaxToken, TextRange, TextUnit, TokenAtOffset, | ||
24 | }; | ||
25 | use ra_text_edit::TextEdit; | ||
26 | |||
27 | use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit}; | ||
28 | |||
29 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> { | ||
30 | let parse = db.parse(position.file_id); | ||
31 | let file = parse.tree(); | ||
32 | let comment = file | ||
33 | .syntax() | ||
34 | .token_at_offset(position.offset) | ||
35 | .left_biased() | ||
36 | .and_then(ast::Comment::cast)?; | ||
37 | |||
38 | if comment.kind().shape.is_block() { | ||
39 | return None; | ||
40 | } | ||
41 | |||
42 | let prefix = comment.prefix(); | ||
43 | let comment_range = comment.syntax().text_range(); | ||
44 | if position.offset < comment_range.start() + TextUnit::of_str(prefix) + TextUnit::from(1) { | ||
45 | return None; | ||
46 | } | ||
47 | |||
48 | // Continuing non-doc line comments (like this one :) ) is annoying | ||
49 | if prefix == "//" && comment_range.end() == position.offset { | ||
50 | return None; | ||
51 | } | ||
52 | |||
53 | let indent = node_indent(&file, comment.syntax())?; | ||
54 | let inserted = format!("\n{}{} ", indent, prefix); | ||
55 | let cursor_position = position.offset + TextUnit::of_str(&inserted); | ||
56 | let edit = TextEdit::insert(position.offset, inserted); | ||
57 | |||
58 | Some( | ||
59 | SourceChange::source_file_edit( | ||
60 | "on enter", | ||
61 | SourceFileEdit { edit, file_id: position.file_id }, | ||
62 | ) | ||
63 | .with_cursor(FilePosition { offset: cursor_position, file_id: position.file_id }), | ||
64 | ) | ||
65 | } | ||
66 | |||
67 | fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> { | ||
68 | let ws = match file.syntax().token_at_offset(token.text_range().start()) { | ||
69 | TokenAtOffset::Between(l, r) => { | ||
70 | assert!(r == *token); | ||
71 | l | ||
72 | } | ||
73 | TokenAtOffset::Single(n) => { | ||
74 | assert!(n == *token); | ||
75 | return Some("".into()); | ||
76 | } | ||
77 | TokenAtOffset::None => unreachable!(), | ||
78 | }; | ||
79 | if ws.kind() != WHITESPACE { | ||
80 | return None; | ||
81 | } | ||
82 | let text = ws.text(); | ||
83 | let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0); | ||
84 | Some(text[pos..].into()) | ||
85 | } | ||
86 | |||
87 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | ||
88 | |||
89 | pub(crate) fn on_char_typed( | ||
90 | db: &RootDatabase, | ||
91 | position: FilePosition, | ||
92 | char_typed: char, | ||
93 | ) -> Option<SourceChange> { | ||
94 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
95 | let file = &db.parse(position.file_id).tree(); | ||
96 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | ||
97 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | ||
98 | Some(single_file_change.into_source_change(position.file_id)) | ||
99 | } | ||
100 | |||
101 | fn on_char_typed_inner( | ||
102 | file: &SourceFile, | ||
103 | offset: TextUnit, | ||
104 | char_typed: char, | ||
105 | ) -> Option<SingleFileChange> { | ||
106 | assert!(TRIGGER_CHARS.contains(char_typed)); | ||
107 | match char_typed { | ||
108 | '.' => on_dot_typed(file, offset), | ||
109 | '=' => on_eq_typed(file, offset), | ||
110 | '>' => on_arrow_typed(file, offset), | ||
111 | _ => unreachable!(), | ||
112 | } | ||
113 | } | ||
114 | |||
115 | /// Returns an edit which should be applied after `=` was typed. Primarily, | ||
116 | /// this works when adding `let =`. | ||
117 | // FIXME: use a snippet completion instead of this hack here. | ||
118 | fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
119 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | ||
120 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | ||
121 | if let_stmt.has_semi() { | ||
122 | return None; | ||
123 | } | ||
124 | if let Some(expr) = let_stmt.initializer() { | ||
125 | let expr_range = expr.syntax().text_range(); | ||
126 | if expr_range.contains(offset) && offset != expr_range.start() { | ||
127 | return None; | ||
128 | } | ||
129 | if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') { | ||
130 | return None; | ||
131 | } | ||
132 | } else { | ||
133 | return None; | ||
134 | } | ||
135 | let offset = let_stmt.syntax().text_range().end(); | ||
136 | Some(SingleFileChange { | ||
137 | label: "add semicolon".to_string(), | ||
138 | edit: TextEdit::insert(offset, ";".to_string()), | ||
139 | cursor_position: None, | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | ||
144 | fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
145 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | ||
146 | let whitespace = | ||
147 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | ||
148 | |||
149 | let current_indent = { | ||
150 | let text = whitespace.text(); | ||
151 | let newline = text.rfind('\n')?; | ||
152 | &text[newline + 1..] | ||
153 | }; | ||
154 | let current_indent_len = TextUnit::of_str(current_indent); | ||
155 | |||
156 | // Make sure dot is a part of call chain | ||
157 | let field_expr = ast::FieldExpr::cast(whitespace.syntax().parent())?; | ||
158 | let prev_indent = leading_indent(field_expr.syntax())?; | ||
159 | let target_indent = format!(" {}", prev_indent); | ||
160 | let target_indent_len = TextUnit::of_str(&target_indent); | ||
161 | if current_indent_len == target_indent_len { | ||
162 | return None; | ||
163 | } | ||
164 | |||
165 | Some(SingleFileChange { | ||
166 | label: "reindent dot".to_string(), | ||
167 | edit: TextEdit::replace( | ||
168 | TextRange::from_to(offset - current_indent_len, offset), | ||
169 | target_indent, | ||
170 | ), | ||
171 | cursor_position: Some( | ||
172 | offset + target_indent_len - current_indent_len + TextUnit::of_char('.'), | ||
173 | ), | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | ||
178 | fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> { | ||
179 | let file_text = file.syntax().text(); | ||
180 | assert_eq!(file_text.char_at(offset), Some('>')); | ||
181 | let after_arrow = offset + TextUnit::of_char('>'); | ||
182 | if file_text.char_at(after_arrow) != Some('{') { | ||
183 | return None; | ||
184 | } | ||
185 | if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() { | ||
186 | return None; | ||
187 | } | ||
188 | |||
189 | Some(SingleFileChange { | ||
190 | label: "add space after return type".to_string(), | ||
191 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
192 | cursor_position: Some(after_arrow), | ||
193 | }) | ||
194 | } | ||
195 | |||
196 | #[cfg(test)] | ||
197 | mod tests { | ||
198 | use test_utils::{add_cursor, assert_eq_text, extract_offset}; | ||
199 | |||
200 | use crate::mock_analysis::single_file; | ||
201 | |||
202 | use super::*; | ||
203 | |||
204 | #[test] | ||
205 | fn test_on_enter() { | ||
206 | fn apply_on_enter(before: &str) -> Option<String> { | ||
207 | let (offset, before) = extract_offset(before); | ||
208 | let (analysis, file_id) = single_file(&before); | ||
209 | let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; | ||
210 | |||
211 | assert_eq!(result.source_file_edits.len(), 1); | ||
212 | let actual = result.source_file_edits[0].edit.apply(&before); | ||
213 | let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); | ||
214 | Some(actual) | ||
215 | } | ||
216 | |||
217 | fn do_check(before: &str, after: &str) { | ||
218 | let actual = apply_on_enter(before).unwrap(); | ||
219 | assert_eq_text!(after, &actual); | ||
220 | } | ||
221 | |||
222 | fn do_check_noop(text: &str) { | ||
223 | assert!(apply_on_enter(text).is_none()) | ||
224 | } | ||
225 | |||
226 | do_check( | ||
227 | r" | ||
228 | /// Some docs<|> | ||
229 | fn foo() { | ||
230 | } | ||
231 | ", | ||
232 | r" | ||
233 | /// Some docs | ||
234 | /// <|> | ||
235 | fn foo() { | ||
236 | } | ||
237 | ", | ||
238 | ); | ||
239 | do_check( | ||
240 | r" | ||
241 | impl S { | ||
242 | /// Some<|> docs. | ||
243 | fn foo() {} | ||
244 | } | ||
245 | ", | ||
246 | r" | ||
247 | impl S { | ||
248 | /// Some | ||
249 | /// <|> docs. | ||
250 | fn foo() {} | ||
251 | } | ||
252 | ", | ||
253 | ); | ||
254 | do_check( | ||
255 | r" | ||
256 | fn main() { | ||
257 | // Fix<|> me | ||
258 | let x = 1 + 1; | ||
259 | } | ||
260 | ", | ||
261 | r" | ||
262 | fn main() { | ||
263 | // Fix | ||
264 | // <|> me | ||
265 | let x = 1 + 1; | ||
266 | } | ||
267 | ", | ||
268 | ); | ||
269 | do_check_noop( | ||
270 | r" | ||
271 | fn main() { | ||
272 | // Fix me<|> | ||
273 | let x = 1 + 1; | ||
274 | } | ||
275 | ", | ||
276 | ); | ||
277 | |||
278 | do_check_noop(r"<|>//! docz"); | ||
279 | } | ||
280 | |||
281 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | ||
282 | let (offset, before) = extract_offset(before); | ||
283 | let edit = TextEdit::insert(offset, char_typed.to_string()); | ||
284 | let before = edit.apply(&before); | ||
285 | let parse = SourceFile::parse(&before); | ||
286 | on_char_typed_inner(&parse.tree(), offset, char_typed) | ||
287 | .map(|it| (it.edit.apply(&before), it)) | ||
288 | } | ||
289 | |||
290 | fn type_char(char_typed: char, before: &str, after: &str) { | ||
291 | let (actual, file_change) = do_type_char(char_typed, before) | ||
292 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); | ||
293 | |||
294 | if after.contains("<|>") { | ||
295 | let (offset, after) = extract_offset(after); | ||
296 | assert_eq_text!(&after, &actual); | ||
297 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
298 | } else { | ||
299 | assert_eq_text!(after, &actual); | ||
300 | } | ||
301 | } | ||
302 | |||
303 | fn type_char_noop(char_typed: char, before: &str) { | ||
304 | let file_change = do_type_char(char_typed, before); | ||
305 | assert!(file_change.is_none()) | ||
306 | } | ||
307 | |||
308 | #[test] | ||
309 | fn test_on_eq_typed() { | ||
310 | // do_check(r" | ||
311 | // fn foo() { | ||
312 | // let foo =<|> | ||
313 | // } | ||
314 | // ", r" | ||
315 | // fn foo() { | ||
316 | // let foo =; | ||
317 | // } | ||
318 | // "); | ||
319 | type_char( | ||
320 | '=', | ||
321 | r" | ||
322 | fn foo() { | ||
323 | let foo <|> 1 + 1 | ||
324 | } | ||
325 | ", | ||
326 | r" | ||
327 | fn foo() { | ||
328 | let foo = 1 + 1; | ||
329 | } | ||
330 | ", | ||
331 | ); | ||
332 | // do_check(r" | ||
333 | // fn foo() { | ||
334 | // let foo =<|> | ||
335 | // let bar = 1; | ||
336 | // } | ||
337 | // ", r" | ||
338 | // fn foo() { | ||
339 | // let foo =; | ||
340 | // let bar = 1; | ||
341 | // } | ||
342 | // "); | ||
343 | } | ||
344 | |||
345 | #[test] | ||
346 | fn indents_new_chain_call() { | ||
347 | type_char( | ||
348 | '.', | ||
349 | r" | ||
350 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
351 | self.child_impl(db, name) | ||
352 | <|> | ||
353 | } | ||
354 | ", | ||
355 | r" | ||
356 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
357 | self.child_impl(db, name) | ||
358 | . | ||
359 | } | ||
360 | ", | ||
361 | ); | ||
362 | type_char_noop( | ||
363 | '.', | ||
364 | r" | ||
365 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
366 | self.child_impl(db, name) | ||
367 | <|> | ||
368 | } | ||
369 | ", | ||
370 | ) | ||
371 | } | ||
372 | |||
373 | #[test] | ||
374 | fn indents_new_chain_call_with_semi() { | ||
375 | type_char( | ||
376 | '.', | ||
377 | r" | ||
378 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
379 | self.child_impl(db, name) | ||
380 | <|>; | ||
381 | } | ||
382 | ", | ||
383 | r" | ||
384 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
385 | self.child_impl(db, name) | ||
386 | .; | ||
387 | } | ||
388 | ", | ||
389 | ); | ||
390 | type_char_noop( | ||
391 | '.', | ||
392 | r" | ||
393 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
394 | self.child_impl(db, name) | ||
395 | <|>; | ||
396 | } | ||
397 | ", | ||
398 | ) | ||
399 | } | ||
400 | |||
401 | #[test] | ||
402 | fn indents_continued_chain_call() { | ||
403 | type_char( | ||
404 | '.', | ||
405 | r" | ||
406 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
407 | self.child_impl(db, name) | ||
408 | .first() | ||
409 | <|> | ||
410 | } | ||
411 | ", | ||
412 | r" | ||
413 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
414 | self.child_impl(db, name) | ||
415 | .first() | ||
416 | . | ||
417 | } | ||
418 | ", | ||
419 | ); | ||
420 | type_char_noop( | ||
421 | '.', | ||
422 | r" | ||
423 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
424 | self.child_impl(db, name) | ||
425 | .first() | ||
426 | <|> | ||
427 | } | ||
428 | ", | ||
429 | ); | ||
430 | } | ||
431 | |||
432 | #[test] | ||
433 | fn indents_middle_of_chain_call() { | ||
434 | type_char( | ||
435 | '.', | ||
436 | r" | ||
437 | fn source_impl() { | ||
438 | let var = enum_defvariant_list().unwrap() | ||
439 | <|> | ||
440 | .nth(92) | ||
441 | .unwrap(); | ||
442 | } | ||
443 | ", | ||
444 | r" | ||
445 | fn source_impl() { | ||
446 | let var = enum_defvariant_list().unwrap() | ||
447 | . | ||
448 | .nth(92) | ||
449 | .unwrap(); | ||
450 | } | ||
451 | ", | ||
452 | ); | ||
453 | type_char_noop( | ||
454 | '.', | ||
455 | r" | ||
456 | fn source_impl() { | ||
457 | let var = enum_defvariant_list().unwrap() | ||
458 | <|> | ||
459 | .nth(92) | ||
460 | .unwrap(); | ||
461 | } | ||
462 | ", | ||
463 | ); | ||
464 | } | ||
465 | |||
466 | #[test] | ||
467 | fn dont_indent_freestanding_dot() { | ||
468 | type_char_noop( | ||
469 | '.', | ||
470 | r" | ||
471 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
472 | <|> | ||
473 | } | ||
474 | ", | ||
475 | ); | ||
476 | type_char_noop( | ||
477 | '.', | ||
478 | r" | ||
479 | pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { | ||
480 | <|> | ||
481 | } | ||
482 | ", | ||
483 | ); | ||
484 | } | ||
485 | |||
486 | #[test] | ||
487 | fn adds_space_after_return_type() { | ||
488 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") | ||
489 | } | ||
490 | } | ||
diff --git a/crates/ra_ide/src/wasm_shims.rs b/crates/ra_ide/src/wasm_shims.rs new file mode 100644 index 000000000..088cc9be4 --- /dev/null +++ b/crates/ra_ide/src/wasm_shims.rs | |||
@@ -0,0 +1,19 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | #[cfg(not(feature = "wasm"))] | ||
4 | pub use std::time::Instant; | ||
5 | |||
6 | #[cfg(feature = "wasm")] | ||
7 | #[derive(Clone, Copy, Debug)] | ||
8 | pub struct Instant; | ||
9 | |||
10 | #[cfg(feature = "wasm")] | ||
11 | impl Instant { | ||
12 | pub fn now() -> Self { | ||
13 | Self | ||
14 | } | ||
15 | |||
16 | pub fn elapsed(&self) -> std::time::Duration { | ||
17 | std::time::Duration::new(0, 0) | ||
18 | } | ||
19 | } | ||