]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/highlight_related.rs
Merge #9929
[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::{for_each_break_expr, for_each_tail_expr, pick_best_token},
6     search::{FileReference, ReferenceAccess, SearchScope},
7     RootDatabase,
8 };
9 use syntax::{
10     ast::{self, LoopBodyOwner},
11     match_ast, AstNode, SyntaxNode, SyntaxToken, TextRange, T,
12 };
13
14 use crate::{display::TryToNav, references, NavigationTarget};
15
16 pub struct HighlightedRange {
17     pub range: TextRange,
18     pub access: Option<ReferenceAccess>,
19 }
20
21 #[derive(Default, Clone)]
22 pub struct HighlightRelatedConfig {
23     pub references: bool,
24     pub exit_points: bool,
25     pub break_points: bool,
26     pub yield_points: bool,
27 }
28
29 // Feature: Highlight Related
30 //
31 // Highlights constructs related to the thing under the cursor:
32 // - if on an identifier, highlights all references to that identifier in the current file
33 // - if on an `async` or `await token, highlights all yield points for that async context
34 // - if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context
35 // - if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context
36 //
37 // Note: `?` and `->` do not currently trigger this behavior in the VSCode editor.
38 pub(crate) fn highlight_related(
39     sema: &Semantics<RootDatabase>,
40     config: HighlightRelatedConfig,
41     position: FilePosition,
42 ) -> Option<Vec<HighlightedRange>> {
43     let _p = profile::span("highlight_related");
44     let syntax = sema.parse(position.file_id).syntax().clone();
45
46     let token = pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind {
47         T![?] => 3, // prefer `?` when the cursor is sandwiched like in `await$0?`
48         T![->] => 2,
49         kind if kind.is_keyword() => 1,
50         _ => 0,
51     })?;
52
53     match token.kind() {
54         T![fn] | T![return] | T![?] | T![->] if config.exit_points => {
55             highlight_exit_points(sema, token)
56         }
57         T![await] | T![async] if config.yield_points => highlight_yield_points(token),
58         T![break] | T![loop] | T![for] | T![while] if config.break_points => {
59             highlight_break_points(token)
60         }
61         _ if config.references => highlight_references(sema, &syntax, position),
62         _ => None,
63     }
64 }
65
66 fn highlight_references(
67     sema: &Semantics<RootDatabase>,
68     syntax: &SyntaxNode,
69     FilePosition { offset, file_id }: FilePosition,
70 ) -> Option<Vec<HighlightedRange>> {
71     let def = references::find_def(sema, syntax, offset)?;
72     let usages = def
73         .usages(sema)
74         .set_scope(Some(SearchScope::single_file(file_id)))
75         .include_self_refs()
76         .all();
77
78     let declaration = match def {
79         Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
80             Some(NavigationTarget::from_module_to_decl(sema.db, module))
81         }
82         def => def.try_to_nav(sema.db),
83     }
84     .filter(|decl| decl.file_id == file_id)
85     .and_then(|decl| {
86         let range = decl.focus_range?;
87         let access = references::decl_access(&def, syntax, range);
88         Some(HighlightedRange { range, access })
89     });
90
91     let file_refs = usages.references.get(&file_id).map_or(&[][..], Vec::as_slice);
92     let mut res = Vec::with_capacity(file_refs.len() + 1);
93     res.extend(declaration);
94     res.extend(
95         file_refs
96             .iter()
97             .map(|&FileReference { access, range, .. }| HighlightedRange { range, access }),
98     );
99     Some(res)
100 }
101
102 fn highlight_exit_points(
103     sema: &Semantics<RootDatabase>,
104     token: SyntaxToken,
105 ) -> Option<Vec<HighlightedRange>> {
106     fn hl(
107         sema: &Semantics<RootDatabase>,
108         body: Option<ast::Expr>,
109     ) -> Option<Vec<HighlightedRange>> {
110         let mut highlights = Vec::new();
111         let body = body?;
112         body.walk(&mut |expr| match expr {
113             ast::Expr::ReturnExpr(expr) => {
114                 if let Some(token) = expr.return_token() {
115                     highlights.push(HighlightedRange { access: None, range: token.text_range() });
116                 }
117             }
118             ast::Expr::TryExpr(try_) => {
119                 if let Some(token) = try_.question_mark_token() {
120                     highlights.push(HighlightedRange { access: None, range: token.text_range() });
121                 }
122             }
123             ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroCall(_) => {
124                 if sema.type_of_expr(&expr).map_or(false, |ty| ty.original.is_never()) {
125                     highlights
126                         .push(HighlightedRange { access: None, range: expr.syntax().text_range() });
127                 }
128             }
129             _ => (),
130         });
131         let tail = match body {
132             ast::Expr::BlockExpr(b) => b.tail_expr(),
133             e => Some(e),
134         };
135
136         if let Some(tail) = tail {
137             for_each_tail_expr(&tail, &mut |tail| {
138                 let range = match tail {
139                     ast::Expr::BreakExpr(b) => b
140                         .break_token()
141                         .map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),
142                     _ => tail.syntax().text_range(),
143                 };
144                 highlights.push(HighlightedRange { access: None, range })
145             });
146         }
147         Some(highlights)
148     }
149     for anc in token.ancestors() {
150         return match_ast! {
151             match anc {
152                 ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)),
153                 ast::ClosureExpr(closure) => hl(sema, closure.body()),
154                 ast::EffectExpr(effect) => if matches!(effect.effect(), ast::Effect::Async(_) | ast::Effect::Try(_)| ast::Effect::Const(_)) {
155                     hl(sema, effect.block_expr().map(ast::Expr::BlockExpr))
156                 } else {
157                     continue;
158                 },
159                 _ => continue,
160             }
161         };
162     }
163     None
164 }
165
166 fn highlight_break_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> {
167     fn hl(
168         token: Option<SyntaxToken>,
169         label: Option<ast::Label>,
170         body: Option<ast::BlockExpr>,
171     ) -> Option<Vec<HighlightedRange>> {
172         let mut highlights = Vec::new();
173         let range = cover_range(
174             token.map(|tok| tok.text_range()),
175             label.as_ref().map(|it| it.syntax().text_range()),
176         );
177         highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
178         for_each_break_expr(label, body, &mut |break_| {
179             let range = cover_range(
180                 break_.break_token().map(|it| it.text_range()),
181                 break_.lifetime().map(|it| it.syntax().text_range()),
182             );
183             highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
184         });
185         Some(highlights)
186     }
187     let parent = token.parent()?;
188     let lbl = match_ast! {
189         match parent {
190             ast::BreakExpr(b) => b.lifetime(),
191             ast::LoopExpr(l) => l.label().and_then(|it| it.lifetime()),
192             ast::ForExpr(f) => f.label().and_then(|it| it.lifetime()),
193             ast::WhileExpr(w) => w.label().and_then(|it| it.lifetime()),
194             ast::EffectExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
195             _ => return None,
196         }
197     };
198     let lbl = lbl.as_ref();
199     let label_matches = |def_lbl: Option<ast::Label>| match lbl {
200         Some(lbl) => {
201             Some(lbl.text()) == def_lbl.and_then(|it| it.lifetime()).as_ref().map(|it| it.text())
202         }
203         None => true,
204     };
205     for anc in token.ancestors().flat_map(ast::Expr::cast) {
206         return match anc {
207             ast::Expr::LoopExpr(l) if label_matches(l.label()) => {
208                 hl(l.loop_token(), l.label(), l.loop_body())
209             }
210             ast::Expr::ForExpr(f) if label_matches(f.label()) => {
211                 hl(f.for_token(), f.label(), f.loop_body())
212             }
213             ast::Expr::WhileExpr(w) if label_matches(w.label()) => {
214                 hl(w.while_token(), w.label(), w.loop_body())
215             }
216             ast::Expr::EffectExpr(e) if e.label().is_some() && label_matches(e.label()) => {
217                 hl(None, e.label(), e.block_expr())
218             }
219             _ => continue,
220         };
221     }
222     None
223 }
224
225 fn highlight_yield_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> {
226     fn hl(
227         async_token: Option<SyntaxToken>,
228         body: Option<ast::Expr>,
229     ) -> Option<Vec<HighlightedRange>> {
230         let mut highlights =
231             vec![HighlightedRange { access: None, range: async_token?.text_range() }];
232         if let Some(body) = body {
233             body.walk(&mut |expr| {
234                 if let ast::Expr::AwaitExpr(expr) = expr {
235                     if let Some(token) = expr.await_token() {
236                         highlights
237                             .push(HighlightedRange { access: None, range: token.text_range() });
238                     }
239                 }
240             });
241         }
242         Some(highlights)
243     }
244     for anc in token.ancestors() {
245         return match_ast! {
246             match anc {
247                 ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),
248                 ast::EffectExpr(effect) => hl(effect.async_token(), effect.block_expr().map(ast::Expr::BlockExpr)),
249                 ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()),
250                 _ => continue,
251             }
252         };
253     }
254     None
255 }
256
257 fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange> {
258     match (r0, r1) {
259         (Some(r0), Some(r1)) => Some(r0.cover(r1)),
260         (Some(range), None) => Some(range),
261         (None, Some(range)) => Some(range),
262         (None, None) => None,
263     }
264 }
265
266 #[cfg(test)]
267 mod tests {
268     use crate::fixture;
269
270     use super::*;
271
272     fn check(ra_fixture: &str) {
273         let config = HighlightRelatedConfig {
274             break_points: true,
275             exit_points: true,
276             references: true,
277             yield_points: true,
278         };
279
280         check_with_config(ra_fixture, config);
281     }
282
283     fn check_with_config(ra_fixture: &str, config: HighlightRelatedConfig) {
284         let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
285
286         let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or(Vec::default());
287
288         let mut expected = annotations
289             .into_iter()
290             .map(|(r, access)| (r.range, (!access.is_empty()).then(|| access)))
291             .collect::<Vec<_>>();
292
293         let mut actual = hls
294             .into_iter()
295             .map(|hl| {
296                 (
297                     hl.range,
298                     hl.access.map(|it| {
299                         match it {
300                             ReferenceAccess::Read => "read",
301                             ReferenceAccess::Write => "write",
302                         }
303                         .to_string()
304                     }),
305                 )
306             })
307             .collect::<Vec<_>>();
308         actual.sort_by_key(|(range, _)| range.start());
309         expected.sort_by_key(|(range, _)| range.start());
310
311         assert_eq!(expected, actual);
312     }
313
314     #[test]
315     fn test_hl_module() {
316         check(
317             r#"
318 //- /lib.rs
319 mod foo$0;
320  // ^^^
321 //- /foo.rs
322 struct Foo;
323 "#,
324         );
325     }
326
327     #[test]
328     fn test_hl_self_in_crate_root() {
329         check(
330             r#"
331 use self$0;
332 "#,
333         );
334     }
335
336     #[test]
337     fn test_hl_self_in_module() {
338         check(
339             r#"
340 //- /lib.rs
341 mod foo;
342 //- /foo.rs
343 use self$0;
344  // ^^^^
345 "#,
346         );
347     }
348
349     #[test]
350     fn test_hl_local() {
351         check(
352             r#"
353 fn foo() {
354     let mut bar = 3;
355          // ^^^ write
356     bar$0;
357  // ^^^ read
358 }
359 "#,
360         );
361     }
362
363     #[test]
364     fn test_hl_yield_points() {
365         check(
366             r#"
367 pub async fn foo() {
368  // ^^^^^
369     let x = foo()
370         .await$0
371       // ^^^^^
372         .await;
373       // ^^^^^
374     || { 0.await };
375     (async { 0.await }).await
376                      // ^^^^^
377 }
378 "#,
379         );
380     }
381
382     #[test]
383     fn test_hl_yield_points2() {
384         check(
385             r#"
386 pub async$0 fn foo() {
387  // ^^^^^
388     let x = foo()
389         .await
390       // ^^^^^
391         .await;
392       // ^^^^^
393     || { 0.await };
394     (async { 0.await }).await
395                      // ^^^^^
396 }
397 "#,
398         );
399     }
400
401     #[test]
402     fn test_hl_yield_nested_fn() {
403         check(
404             r#"
405 async fn foo() {
406     async fn foo2() {
407  // ^^^^^
408         async fn foo3() {
409             0.await
410         }
411         0.await$0
412        // ^^^^^
413     }
414     0.await
415 }
416 "#,
417         );
418     }
419
420     #[test]
421     fn test_hl_yield_nested_async_blocks() {
422         check(
423             r#"
424 async fn foo() {
425     (async {
426   // ^^^^^
427         (async {
428            0.await
429         }).await$0 }
430         // ^^^^^
431     ).await;
432 }
433 "#,
434         );
435     }
436
437     #[test]
438     fn test_hl_exit_points() {
439         check(
440             r#"
441 fn foo() -> u32 {
442     if true {
443         return$0 0;
444      // ^^^^^^
445     }
446
447     0?;
448   // ^
449     0xDEAD_BEEF
450  // ^^^^^^^^^^^
451 }
452 "#,
453         );
454     }
455
456     #[test]
457     fn test_hl_exit_points2() {
458         check(
459             r#"
460 fn foo() ->$0 u32 {
461     if true {
462         return 0;
463      // ^^^^^^
464     }
465
466     0?;
467   // ^
468     0xDEAD_BEEF
469  // ^^^^^^^^^^^
470 }
471 "#,
472         );
473     }
474
475     #[test]
476     fn test_hl_exit_points3() {
477         check(
478             r#"
479 fn$0 foo() -> u32 {
480     if true {
481         return 0;
482      // ^^^^^^
483     }
484
485     0?;
486   // ^
487     0xDEAD_BEEF
488  // ^^^^^^^^^^^
489 }
490 "#,
491         );
492     }
493
494     #[test]
495     fn test_hl_prefer_ref_over_tail_exit() {
496         check(
497             r#"
498 fn foo() -> u32 {
499 // ^^^
500     if true {
501         return 0;
502     }
503
504     0?;
505
506     foo$0()
507  // ^^^
508 }
509 "#,
510         );
511     }
512
513     #[test]
514     fn test_hl_never_call_is_exit_point() {
515         check(
516             r#"
517 struct Never;
518 impl Never {
519     fn never(self) -> ! { loop {} }
520 }
521 macro_rules! never {
522     () => { never() }
523 }
524 fn never() -> ! { loop {} }
525 fn foo() ->$0 u32 {
526     never();
527  // ^^^^^^^
528     never!();
529  // FIXME sema doesn't give us types for macrocalls
530
531     Never.never();
532  // ^^^^^^^^^^^^^
533
534     0
535  // ^
536 }
537 "#,
538         );
539     }
540
541     #[test]
542     fn test_hl_inner_tail_exit_points() {
543         check(
544             r#"
545 fn foo() ->$0 u32 {
546     if true {
547         unsafe {
548             return 5;
549          // ^^^^^^
550             5
551          // ^
552         }
553     } else if false {
554         0
555      // ^
556     } else {
557         match 5 {
558             6 => 100,
559               // ^^^
560             7 => loop {
561                 break 5;
562              // ^^^^^
563             }
564             8 => 'a: loop {
565                 'b: loop {
566                     break 'a 5;
567                  // ^^^^^
568                     break 'b 5;
569                     break 5;
570                 };
571             }
572             //
573             _ => 500,
574               // ^^^
575         }
576     }
577 }
578 "#,
579         );
580     }
581
582     #[test]
583     fn test_hl_inner_tail_exit_points_labeled_block() {
584         check(
585             r#"
586 fn foo() ->$0 u32 {
587     'foo: {
588         break 'foo 0;
589      // ^^^^^
590         loop {
591             break;
592             break 'foo 0;
593          // ^^^^^
594         }
595         0
596      // ^
597     }
598 }
599 "#,
600         );
601     }
602
603     #[test]
604     fn test_hl_break_loop() {
605         check(
606             r#"
607 fn foo() {
608     'outer: loop {
609  // ^^^^^^^^^^^^
610          break;
611       // ^^^^^
612          'inner: loop {
613             break;
614             'innermost: loop {
615                 break 'outer;
616              // ^^^^^^^^^^^^
617                 break 'inner;
618             }
619             break$0 'outer;
620          // ^^^^^^^^^^^^
621             break;
622         }
623         break;
624      // ^^^^^
625     }
626 }
627 "#,
628         );
629     }
630
631     #[test]
632     fn test_hl_break_loop2() {
633         check(
634             r#"
635 fn foo() {
636     'outer: loop {
637         break;
638         'inner: loop {
639      // ^^^^^^^^^^^^
640             break;
641          // ^^^^^
642             'innermost: loop {
643                 break 'outer;
644                 break 'inner;
645              // ^^^^^^^^^^^^
646             }
647             break 'outer;
648             break$0;
649          // ^^^^^
650         }
651         break;
652     }
653 }
654 "#,
655         );
656     }
657
658     #[test]
659     fn test_hl_break_for() {
660         check(
661             r#"
662 fn foo() {
663     'outer: for _ in () {
664  // ^^^^^^^^^^^
665          break;
666       // ^^^^^
667          'inner: for _ in () {
668             break;
669             'innermost: for _ in () {
670                 break 'outer;
671              // ^^^^^^^^^^^^
672                 break 'inner;
673             }
674             break$0 'outer;
675          // ^^^^^^^^^^^^
676             break;
677         }
678         break;
679      // ^^^^^
680     }
681 }
682 "#,
683         );
684     }
685
686     #[test]
687     fn test_hl_break_while() {
688         check(
689             r#"
690 fn foo() {
691     'outer: while true {
692  // ^^^^^^^^^^^^^
693          break;
694       // ^^^^^
695          'inner: while true {
696             break;
697             'innermost: while true {
698                 break 'outer;
699              // ^^^^^^^^^^^^
700                 break 'inner;
701             }
702             break$0 'outer;
703          // ^^^^^^^^^^^^
704             break;
705         }
706         break;
707      // ^^^^^
708     }
709 }
710 "#,
711         );
712     }
713
714     #[test]
715     fn test_hl_break_labeled_block() {
716         check(
717             r#"
718 fn foo() {
719     'outer: {
720  // ^^^^^^^
721          break;
722       // ^^^^^
723          'inner: {
724             break;
725             'innermost: {
726                 break 'outer;
727              // ^^^^^^^^^^^^
728                 break 'inner;
729             }
730             break$0 'outer;
731          // ^^^^^^^^^^^^
732             break;
733         }
734         break;
735      // ^^^^^
736     }
737 }
738 "#,
739         );
740     }
741
742     #[test]
743     fn test_hl_break_unlabeled_loop() {
744         check(
745             r#"
746 fn foo() {
747     loop {
748  // ^^^^
749         break$0;
750      // ^^^^^
751     }
752 }
753 "#,
754         );
755     }
756
757     #[test]
758     fn test_hl_break_unlabeled_block_in_loop() {
759         check(
760             r#"
761 fn foo() {
762     loop {
763  // ^^^^
764         {
765             break$0;
766          // ^^^^^
767         }
768     }
769 }
770 "#,
771         );
772     }
773
774     #[test]
775     fn test_hl_disabled_ref_local() {
776         let config = HighlightRelatedConfig {
777             references: false,
778             break_points: true,
779             exit_points: true,
780             yield_points: true,
781         };
782
783         let ra_fixture = r#"
784 fn foo() {
785     let x$0 = 5;
786     let y = x * 2;
787 }"#;
788
789         check_with_config(ra_fixture, config);
790     }
791
792     #[test]
793     fn test_hl_disabled_ref_local_preserved_break() {
794         let config = HighlightRelatedConfig {
795             references: false,
796             break_points: true,
797             exit_points: true,
798             yield_points: true,
799         };
800
801         let ra_fixture = r#"
802 fn foo() {
803     let x$0 = 5;
804     let y = x * 2;
805
806     loop {
807         break;
808     }
809 }"#;
810
811         check_with_config(ra_fixture, config.clone());
812
813         let ra_fixture = r#"
814 fn foo() {
815     let x = 5;
816     let y = x * 2;
817
818     loop$0 {
819 //  ^^^^
820         break;
821 //      ^^^^^
822     }
823 }"#;
824
825         check_with_config(ra_fixture, config);
826     }
827
828     #[test]
829     fn test_hl_disabled_ref_local_preserved_yield() {
830         let config = HighlightRelatedConfig {
831             references: false,
832             break_points: true,
833             exit_points: true,
834             yield_points: true,
835         };
836
837         let ra_fixture = r#"
838 async fn foo() {
839     let x$0 = 5;
840     let y = x * 2;
841
842     0.await;
843 }"#;
844
845         check_with_config(ra_fixture, config.clone());
846
847         let ra_fixture = r#"
848     async fn foo() {
849 //  ^^^^^
850         let x = 5;
851         let y = x * 2;
852
853         0.await$0;
854 //        ^^^^^
855 }"#;
856
857         check_with_config(ra_fixture, config);
858     }
859
860     #[test]
861     fn test_hl_disabled_ref_local_preserved_exit() {
862         let config = HighlightRelatedConfig {
863             references: false,
864             break_points: true,
865             exit_points: true,
866             yield_points: true,
867         };
868
869         let ra_fixture = r#"
870 fn foo() -> i32 {
871     let x$0 = 5;
872     let y = x * 2;
873
874     if true {
875         return y;
876     }
877
878     0?
879 }"#;
880
881         check_with_config(ra_fixture, config.clone());
882
883         let ra_fixture = r#"
884 fn foo() ->$0 i32 {
885     let x = 5;
886     let y = x * 2;
887
888     if true {
889         return y;
890 //      ^^^^^^
891     }
892
893     0?
894 //   ^
895 "#;
896
897         check_with_config(ra_fixture, config);
898     }
899
900     #[test]
901     fn test_hl_disabled_break() {
902         let config = HighlightRelatedConfig {
903             references: true,
904             break_points: false,
905             exit_points: true,
906             yield_points: true,
907         };
908
909         let ra_fixture = r#"
910 fn foo() {
911     loop {
912         break$0;
913     }
914 }"#;
915
916         check_with_config(ra_fixture, config);
917     }
918
919     #[test]
920     fn test_hl_disabled_yield() {
921         let config = HighlightRelatedConfig {
922             references: true,
923             break_points: true,
924             exit_points: true,
925             yield_points: false,
926         };
927
928         let ra_fixture = r#"
929 async$0 fn foo() {
930     0.await;
931 }"#;
932
933         check_with_config(ra_fixture, config);
934     }
935
936     #[test]
937     fn test_hl_disabled_exit() {
938         let config = HighlightRelatedConfig {
939             references: true,
940             break_points: true,
941             exit_points: false,
942             yield_points: true,
943         };
944
945         let ra_fixture = r#"
946 fn foo() ->$0 i32 {
947     if true {
948         return -1;
949     }
950
951     42
952 }"#;
953
954         check_with_config(ra_fixture, config);
955     }
956 }