diff options
Diffstat (limited to 'crates/ra_ide/src/inlay_hints.rs')
-rw-r--r-- | crates/ra_ide/src/inlay_hints.rs | 543 |
1 files changed, 543 insertions, 0 deletions
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 | } | ||