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