]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/highlight_related.rs
Improve feature docs for highlight_related
[rust.git] / crates / ide / src / highlight_related.rs
1 use hir::Semantics;
2 use ide_db::{
3     base_db::FilePosition,
4     defs::Definition,
5     helpers::pick_best_token,
6     search::{FileReference, ReferenceAccess, SearchScope},
7     RootDatabase,
8 };
9 use syntax::{
10     ast, match_ast, AstNode,
11     SyntaxKind::{ASYNC_KW, AWAIT_KW, QUESTION, RETURN_KW, THIN_ARROW},
12     SyntaxNode, SyntaxToken, TextRange, WalkEvent,
13 };
14
15 use crate::{display::TryToNav, references, NavigationTarget};
16
17 pub struct DocumentHighlight {
18     pub range: TextRange,
19     pub access: Option<ReferenceAccess>,
20 }
21
22 // Feature: Highlight Related
23 //
24 // Highlights constructs related to the thing under the cursor:
25 // - if on an identifier, highlights all references to that identifier in the current file
26 // - if on an `async` or `await token, highlights all yield points for that async context
27 // - if on a `return` token, `?` character or `->` return type arrow, highlights all exit points for that context
28 pub(crate) fn highlight_related(
29     sema: &Semantics<RootDatabase>,
30     position: FilePosition,
31 ) -> Option<Vec<DocumentHighlight>> {
32     let _p = profile::span("document_highlight");
33     let syntax = sema.parse(position.file_id).syntax().clone();
34
35     let token = pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind {
36         QUESTION => 2, // prefer `?` when the cursor is sandwiched like `await$0?`
37         AWAIT_KW | ASYNC_KW | THIN_ARROW | RETURN_KW => 1,
38         _ => 0,
39     })?;
40
41     match token.kind() {
42         QUESTION | RETURN_KW | THIN_ARROW => highlight_exit_points(sema, token),
43         AWAIT_KW | ASYNC_KW => highlight_yield_points(token),
44         _ => highlight_references(sema, &syntax, position),
45     }
46 }
47
48 fn highlight_references(
49     sema: &Semantics<RootDatabase>,
50     syntax: &SyntaxNode,
51     FilePosition { offset, file_id }: FilePosition,
52 ) -> Option<Vec<DocumentHighlight>> {
53     let def = references::find_def(sema, syntax, offset)?;
54     let usages = def.usages(sema).set_scope(Some(SearchScope::single_file(file_id))).all();
55
56     let declaration = match def {
57         Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
58             Some(NavigationTarget::from_module_to_decl(sema.db, module))
59         }
60         def => def.try_to_nav(sema.db),
61     }
62     .filter(|decl| decl.file_id == file_id)
63     .and_then(|decl| {
64         let range = decl.focus_range?;
65         let access = references::decl_access(&def, syntax, range);
66         Some(DocumentHighlight { range, access })
67     });
68
69     let file_refs = usages.references.get(&file_id).map_or(&[][..], Vec::as_slice);
70     let mut res = Vec::with_capacity(file_refs.len() + 1);
71     res.extend(declaration);
72     res.extend(
73         file_refs
74             .iter()
75             .map(|&FileReference { access, range, .. }| DocumentHighlight { range, access }),
76     );
77     Some(res)
78 }
79
80 fn highlight_exit_points(
81     sema: &Semantics<RootDatabase>,
82     token: SyntaxToken,
83 ) -> Option<Vec<DocumentHighlight>> {
84     fn hl(
85         sema: &Semantics<RootDatabase>,
86         body: Option<ast::Expr>,
87     ) -> Option<Vec<DocumentHighlight>> {
88         let mut highlights = Vec::new();
89         let body = body?;
90         walk(&body, |node| {
91             match_ast! {
92                 match node {
93                     ast::ReturnExpr(expr) => if let Some(token) = expr.return_token() {
94                         highlights.push(DocumentHighlight {
95                             access: None,
96                             range: token.text_range(),
97                         });
98                     },
99                     ast::TryExpr(try_) => if let Some(token) = try_.question_mark_token() {
100                         highlights.push(DocumentHighlight {
101                             access: None,
102                             range: token.text_range(),
103                         });
104                     },
105                     ast::Expr(expr) => match expr {
106                         ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroCall(_) => {
107                             if sema.type_of_expr(&expr).map_or(false, |ty| ty.is_never()) {
108                                 highlights.push(DocumentHighlight {
109                                     access: None,
110                                     range: expr.syntax().text_range(),
111                                 });
112                             }
113                         },
114                         ast::Expr::EffectExpr(effect) => return effect.async_token().is_some() || effect.try_token().is_some(),
115                         ast::Expr::ClosureExpr(_) => return true,
116                         _ => (),
117                     },
118                     ast::Item(__) => return true,
119                     // Don't look into const args
120                     ast::Path(__) => return true,
121                     _ => (),
122                 }
123             }
124             false
125         });
126         let tail = match body {
127             ast::Expr::BlockExpr(b) => b.tail_expr(),
128             e => Some(e),
129         };
130         if let Some(tail) = tail {
131             highlights.push(DocumentHighlight { access: None, range: tail.syntax().text_range() });
132         }
133         Some(highlights)
134     }
135     for anc in token.ancestors() {
136         return match_ast! {
137             match anc {
138                 ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)),
139                 ast::ClosureExpr(closure) => hl(sema, closure.body()),
140                 ast::EffectExpr(effect) => if effect.async_token().is_some() || effect.try_token().is_some() {
141                     hl(sema, effect.block_expr().map(ast::Expr::BlockExpr))
142                 } else {
143                     continue;
144                 },
145                 _ => continue,
146             }
147         };
148     }
149     None
150 }
151
152 fn highlight_yield_points(token: SyntaxToken) -> Option<Vec<DocumentHighlight>> {
153     fn hl(
154         async_token: Option<SyntaxToken>,
155         body: Option<ast::Expr>,
156     ) -> Option<Vec<DocumentHighlight>> {
157         let mut highlights = Vec::new();
158         highlights.push(DocumentHighlight { access: None, range: async_token?.text_range() });
159         if let Some(body) = body {
160             walk(&body, |node| {
161                 match_ast! {
162                     match node {
163                         ast::AwaitExpr(expr) => if let Some(token) = expr.await_token() {
164                             highlights.push(DocumentHighlight {
165                                 access: None,
166                                 range: token.text_range(),
167                             });
168                         },
169                         // All the following are different contexts so skip them
170                         ast::EffectExpr(effect) => return effect.async_token().is_some() || effect.try_token().is_some(),
171                         ast::ClosureExpr(__) => return true,
172                         ast::Item(__) => return true,
173                         // Don't look into const args
174                         ast::Path(__) => return true,
175                         _ => (),
176                     }
177                 }
178                 false
179             });
180         }
181         Some(highlights)
182     }
183     for anc in token.ancestors() {
184         return match_ast! {
185             match anc {
186                 ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),
187                 ast::EffectExpr(effect) => hl(effect.async_token(), effect.block_expr().map(ast::Expr::BlockExpr)),
188                 ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()),
189                 _ => continue,
190             }
191         };
192     }
193     None
194 }
195
196 /// Preorder walk the expression node skipping a node's subtrees if the callback returns `true` for the node.
197 fn walk(expr: &ast::Expr, mut cb: impl FnMut(SyntaxNode) -> bool) {
198     let mut preorder = expr.syntax().preorder();
199     while let Some(event) = preorder.next() {
200         let node = match event {
201             WalkEvent::Enter(node) => node,
202             WalkEvent::Leave(_) => continue,
203         };
204         if cb(node) {
205             preorder.skip_subtree();
206         }
207     }
208 }
209
210 #[cfg(test)]
211 mod tests {
212     use crate::fixture;
213
214     use super::*;
215
216     fn check(ra_fixture: &str) {
217         let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
218         let hls = analysis.highlight_related(pos).unwrap().unwrap();
219
220         let mut expected = annotations
221             .into_iter()
222             .map(|(r, access)| (r.range, (!access.is_empty()).then(|| access)))
223             .collect::<Vec<_>>();
224
225         let mut actual = hls
226             .into_iter()
227             .map(|hl| {
228                 (
229                     hl.range,
230                     hl.access.map(|it| {
231                         match it {
232                             ReferenceAccess::Read => "read",
233                             ReferenceAccess::Write => "write",
234                         }
235                         .to_string()
236                     }),
237                 )
238             })
239             .collect::<Vec<_>>();
240         actual.sort_by_key(|(range, _)| range.start());
241         expected.sort_by_key(|(range, _)| range.start());
242
243         assert_eq!(expected, actual);
244     }
245
246     #[test]
247     fn test_hl_module() {
248         check(
249             r#"
250 //- /lib.rs
251 mod foo$0;
252  // ^^^
253 //- /foo.rs
254 struct Foo;
255 "#,
256         );
257     }
258
259     #[test]
260     fn test_hl_self_in_crate_root() {
261         check(
262             r#"
263 use self$0;
264 "#,
265         );
266     }
267
268     #[test]
269     fn test_hl_self_in_module() {
270         check(
271             r#"
272 //- /lib.rs
273 mod foo;
274 //- /foo.rs
275 use self$0;
276 "#,
277         );
278     }
279
280     #[test]
281     fn test_hl_local() {
282         check(
283             r#"
284 fn foo() {
285     let mut bar = 3;
286          // ^^^ write
287     bar$0;
288  // ^^^ read
289 }
290 "#,
291         );
292     }
293
294     #[test]
295     fn test_hl_yield_points() {
296         check(
297             r#"
298 pub async fn foo() {
299  // ^^^^^
300     let x = foo()
301         .await$0
302       // ^^^^^
303         .await;
304       // ^^^^^
305     || { 0.await };
306     (async { 0.await }).await
307                      // ^^^^^
308 }
309 "#,
310         );
311     }
312
313     #[test]
314     fn test_hl_yield_points2() {
315         check(
316             r#"
317 pub async$0 fn foo() {
318  // ^^^^^
319     let x = foo()
320         .await
321       // ^^^^^
322         .await;
323       // ^^^^^
324     || { 0.await };
325     (async { 0.await }).await
326                      // ^^^^^
327 }
328 "#,
329         );
330     }
331
332     #[test]
333     fn test_hl_yield_nested_fn() {
334         check(
335             r#"
336 async fn foo() {
337     async fn foo2() {
338  // ^^^^^
339         async fn foo3() {
340             0.await
341         }
342         0.await$0
343        // ^^^^^
344     }
345     0.await
346 }
347 "#,
348         );
349     }
350
351     #[test]
352     fn test_hl_yield_nested_async_blocks() {
353         check(
354             r#"
355 async fn foo() {
356     (async {
357   // ^^^^^
358         (async {
359            0.await
360         }).await$0 }
361         // ^^^^^
362     ).await;
363 }
364 "#,
365         );
366     }
367
368     #[test]
369     fn test_hl_exit_points() {
370         check(
371             r#"
372 fn foo() -> u32 {
373     if true {
374         return$0 0;
375      // ^^^^^^
376     }
377
378     0?;
379   // ^
380     0xDEAD_BEEF
381  // ^^^^^^^^^^^
382 }
383 "#,
384         );
385     }
386
387     #[test]
388     fn test_hl_exit_points2() {
389         check(
390             r#"
391 fn foo() ->$0 u32 {
392     if true {
393         return 0;
394      // ^^^^^^
395     }
396
397     0?;
398   // ^
399     0xDEAD_BEEF
400  // ^^^^^^^^^^^
401 }
402 "#,
403         );
404     }
405
406     #[test]
407     fn test_hl_prefer_ref_over_tail_exit() {
408         check(
409             r#"
410 fn foo() -> u32 {
411 // ^^^
412     if true {
413         return 0;
414     }
415
416     0?;
417
418     foo$0()
419  // ^^^
420 }
421 "#,
422         );
423     }
424
425     #[test]
426     fn test_hl_never_call_is_exit_point() {
427         check(
428             r#"
429 struct Never;
430 impl Never {
431     fn never(self) -> ! { loop {} }
432 }
433 macro_rules! never {
434     () => { never() }
435 }
436 fn never() -> ! { loop {} }
437 fn foo() ->$0 u32 {
438     never();
439  // ^^^^^^^
440     never!();
441  // FIXME sema doesnt give us types for macrocalls
442
443     Never.never();
444  // ^^^^^^^^^^^^^
445
446     0
447  // ^
448 }
449 "#,
450         );
451     }
452 }