]> git.lizzy.rs Git - rust.git/blobdiff - crates/ide/src/highlight_related.rs
Unnest ide::display::navigation_target module
[rust.git] / crates / ide / src / highlight_related.rs
index 2d27fb45e341bb7dc6ad93112309638508a23246..357b0d2455cd02598557e62c51a4416d6eaf340c 100644 (file)
@@ -1,21 +1,36 @@
 use hir::Semantics;
 use ide_db::{
-    base_db::FilePosition,
+    base_db::{FileId, FilePosition},
     defs::Definition,
-    helpers::{for_each_break_expr, for_each_tail_expr, pick_best_token},
-    search::{FileReference, ReferenceAccess, SearchScope},
+    helpers::{for_each_break_expr, for_each_tail_expr, node_ext::walk_expr, pick_best_token},
+    search::{FileReference, ReferenceCategory, SearchScope},
     RootDatabase,
 };
+use rustc_hash::FxHashSet;
 use syntax::{
-    ast::{self, LoopBodyOwner},
-    match_ast, AstNode, SyntaxNode, SyntaxToken, TextRange, T,
+    ast::{self, HasLoopBody},
+    match_ast, AstNode,
+    SyntaxKind::IDENT,
+    SyntaxNode, SyntaxToken, TextRange, T,
 };
 
-use crate::{display::TryToNav, references, NavigationTarget};
+use crate::{references, NavigationTarget, TryToNav};
 
+#[derive(PartialEq, Eq, Hash)]
 pub struct HighlightedRange {
     pub range: TextRange,
-    pub access: Option<ReferenceAccess>,
+    // FIXME: This needs to be more precise. Reference category makes sense only
+    // for references, but we also have defs. And things like exit points are
+    // neither.
+    pub category: Option<ReferenceCategory>,
+}
+
+#[derive(Default, Clone)]
+pub struct HighlightRelatedConfig {
+    pub references: bool,
+    pub exit_points: bool,
+    pub break_points: bool,
+    pub yield_points: bool,
 }
 
 // Feature: Highlight Related
@@ -23,66 +38,86 @@ pub struct HighlightedRange {
 // Highlights constructs related to the thing under the cursor:
 // - if on an identifier, highlights all references to that identifier in the current file
 // - if on an `async` or `await token, highlights all yield points for that async context
-// - if on a `return` token, `?` character or `->` return type arrow, highlights all exit points for that context
+// - if on a `return` or `fn` keyword, `?` character or `->` return type arrow, highlights all exit points for that context
 // - if on a `break`, `loop`, `while` or `for` token, highlights all break points for that loop or block context
+//
+// Note: `?` and `->` do not currently trigger this behavior in the VSCode editor.
 pub(crate) fn highlight_related(
     sema: &Semantics<RootDatabase>,
-    position: FilePosition,
+    config: HighlightRelatedConfig,
+    FilePosition { offset, file_id }: FilePosition,
 ) -> Option<Vec<HighlightedRange>> {
     let _p = profile::span("highlight_related");
-    let syntax = sema.parse(position.file_id).syntax().clone();
-
-    let token = pick_best_token(syntax.token_at_offset(position.offset), |kind| match kind {
-        T![?] => 2, // prefer `?` when the cursor is sandwiched like `await$0?`
-        T![await]
-        | T![async]
-        | T![return]
-        | T![break]
-        | T![loop]
-        | T![for]
-        | T![while]
-        | T![->] => 1,
+    let syntax = sema.parse(file_id).syntax().clone();
+
+    let token = pick_best_token(syntax.token_at_offset(offset), |kind| match kind {
+        T![?] => 4, // prefer `?` when the cursor is sandwiched like in `await$0?`
+        T![->] => 3,
+        kind if kind.is_keyword() => 2,
+        IDENT => 1,
         _ => 0,
     })?;
 
     match token.kind() {
-        T![return] | T![?] | T![->] => highlight_exit_points(sema, token),
-        T![await] | T![async] => highlight_yield_points(token),
-        T![break] | T![loop] | T![for] | T![while] => highlight_break_points(token),
-        _ => highlight_references(sema, &syntax, position),
+        T![?] if config.exit_points && token.parent().and_then(ast::TryExpr::cast).is_some() => {
+            highlight_exit_points(sema, token)
+        }
+        T![fn] | T![return] | T![->] if config.exit_points => highlight_exit_points(sema, token),
+        T![await] | T![async] if config.yield_points => highlight_yield_points(token),
+        T![for] if config.break_points && token.parent().and_then(ast::ForExpr::cast).is_some() => {
+            highlight_break_points(token)
+        }
+        T![break] | T![loop] | T![while] if config.break_points => highlight_break_points(token),
+        _ if config.references => highlight_references(sema, &syntax, token, file_id),
+        _ => None,
     }
 }
 
 fn highlight_references(
     sema: &Semantics<RootDatabase>,
-    syntax: &SyntaxNode,
-    FilePosition { offset, file_id }: FilePosition,
+    node: &SyntaxNode,
+    token: SyntaxToken,
+    file_id: FileId,
 ) -> Option<Vec<HighlightedRange>> {
-    let def = references::find_def(sema, syntax, offset)?;
-    let usages = def.usages(sema).set_scope(Some(SearchScope::single_file(file_id))).all();
+    let defs = find_defs(sema, token.clone());
+    let usages = defs
+        .iter()
+        .filter_map(|&d| {
+            d.usages(sema)
+                .set_scope(Some(SearchScope::single_file(file_id)))
+                .include_self_refs()
+                .all()
+                .references
+                .remove(&file_id)
+        })
+        .flatten()
+        .map(|FileReference { category: access, range, .. }| HighlightedRange {
+            range,
+            category: access,
+        });
 
-    let declaration = match def {
-        Definition::ModuleDef(hir::ModuleDef::Module(module)) => {
-            Some(NavigationTarget::from_module_to_decl(sema.db, module))
+    let declarations = defs.iter().flat_map(|def| {
+        match def {
+            &Definition::Module(module) => {
+                Some(NavigationTarget::from_module_to_decl(sema.db, module))
+            }
+            def => def.try_to_nav(sema.db),
         }
-        def => def.try_to_nav(sema.db),
-    }
-    .filter(|decl| decl.file_id == file_id)
-    .and_then(|decl| {
-        let range = decl.focus_range?;
-        let access = references::decl_access(&def, syntax, range);
-        Some(HighlightedRange { range, access })
+        .filter(|decl| decl.file_id == file_id)
+        .and_then(|decl| {
+            let range = decl.focus_range?;
+            let category =
+                references::decl_mutability(&def, node, range).then(|| ReferenceCategory::Write);
+            Some(HighlightedRange { range, category })
+        })
     });
 
-    let file_refs = usages.references.get(&file_id).map_or(&[][..], Vec::as_slice);
-    let mut res = Vec::with_capacity(file_refs.len() + 1);
-    res.extend(declaration);
-    res.extend(
-        file_refs
-            .iter()
-            .map(|&FileReference { access, range, .. }| HighlightedRange { range, access }),
-    );
-    Some(res)
+    let res: FxHashSet<_> = declarations.chain(usages).collect();
+    if res.is_empty() {
+        None
+    } else {
+        Some(res.into_iter().collect())
+    }
 }
 
 fn highlight_exit_points(
@@ -95,21 +130,23 @@ fn hl(
     ) -> Option<Vec<HighlightedRange>> {
         let mut highlights = Vec::new();
         let body = body?;
-        body.walk(&mut |expr| match expr {
+        walk_expr(&body, &mut |expr| match expr {
             ast::Expr::ReturnExpr(expr) => {
                 if let Some(token) = expr.return_token() {
-                    highlights.push(HighlightedRange { access: None, range: token.text_range() });
+                    highlights.push(HighlightedRange { category: None, range: token.text_range() });
                 }
             }
             ast::Expr::TryExpr(try_) => {
                 if let Some(token) = try_.question_mark_token() {
-                    highlights.push(HighlightedRange { access: None, range: token.text_range() });
+                    highlights.push(HighlightedRange { category: None, range: token.text_range() });
                 }
             }
             ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_) | ast::Expr::MacroCall(_) => {
-                if sema.type_of_expr(&expr).map_or(false, |ty| ty.is_never()) {
-                    highlights
-                        .push(HighlightedRange { access: None, range: expr.syntax().text_range() });
+                if sema.type_of_expr(&expr).map_or(false, |ty| ty.original.is_never()) {
+                    highlights.push(HighlightedRange {
+                        category: None,
+                        range: expr.syntax().text_range(),
+                    });
                 }
             }
             _ => (),
@@ -127,7 +164,7 @@ fn hl(
                         .map_or_else(|| tail.syntax().text_range(), |tok| tok.text_range()),
                     _ => tail.syntax().text_range(),
                 };
-                highlights.push(HighlightedRange { access: None, range })
+                highlights.push(HighlightedRange { category: None, range })
             });
         }
         Some(highlights)
@@ -137,8 +174,8 @@ fn hl(
             match anc {
                 ast::Fn(fn_) => hl(sema, fn_.body().map(ast::Expr::BlockExpr)),
                 ast::ClosureExpr(closure) => hl(sema, closure.body()),
-                ast::EffectExpr(effect) => if matches!(effect.effect(), ast::Effect::Async(_) | ast::Effect::Try(_)| ast::Effect::Const(_)) {
-                    hl(sema, effect.block_expr().map(ast::Expr::BlockExpr))
+                ast::BlockExpr(block_expr) => if matches!(block_expr.modifier(), Some(ast::BlockModifier::Async(_) | ast::BlockModifier::Try(_)| ast::BlockModifier::Const(_))) {
+                    hl(sema, Some(block_expr.into()))
                 } else {
                     continue;
                 },
@@ -153,20 +190,20 @@ fn highlight_break_points(token: SyntaxToken) -> Option<Vec<HighlightedRange>> {
     fn hl(
         token: Option<SyntaxToken>,
         label: Option<ast::Label>,
-        body: Option<ast::BlockExpr>,
+        body: Option<ast::StmtList>,
     ) -> Option<Vec<HighlightedRange>> {
         let mut highlights = Vec::new();
         let range = cover_range(
             token.map(|tok| tok.text_range()),
             label.as_ref().map(|it| it.syntax().text_range()),
         );
-        highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
+        highlights.extend(range.map(|range| HighlightedRange { category: None, range }));
         for_each_break_expr(label, body, &mut |break_| {
             let range = cover_range(
                 break_.break_token().map(|it| it.text_range()),
                 break_.lifetime().map(|it| it.syntax().text_range()),
             );
-            highlights.extend(range.map(|range| HighlightedRange { access: None, range }));
+            highlights.extend(range.map(|range| HighlightedRange { category: None, range }));
         });
         Some(highlights)
     }
@@ -177,7 +214,7 @@ fn hl(
             ast::LoopExpr(l) => l.label().and_then(|it| it.lifetime()),
             ast::ForExpr(f) => f.label().and_then(|it| it.lifetime()),
             ast::WhileExpr(w) => w.label().and_then(|it| it.lifetime()),
-            ast::EffectExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
+            ast::BlockExpr(b) => Some(b.label().and_then(|it| it.lifetime())?),
             _ => return None,
         }
     };
@@ -191,16 +228,16 @@ fn hl(
     for anc in token.ancestors().flat_map(ast::Expr::cast) {
         return match anc {
             ast::Expr::LoopExpr(l) if label_matches(l.label()) => {
-                hl(l.loop_token(), l.label(), l.loop_body())
+                hl(l.loop_token(), l.label(), l.loop_body().and_then(|it| it.stmt_list()))
             }
             ast::Expr::ForExpr(f) if label_matches(f.label()) => {
-                hl(f.for_token(), f.label(), f.loop_body())
+                hl(f.for_token(), f.label(), f.loop_body().and_then(|it| it.stmt_list()))
             }
             ast::Expr::WhileExpr(w) if label_matches(w.label()) => {
-                hl(w.while_token(), w.label(), w.loop_body())
+                hl(w.while_token(), w.label(), w.loop_body().and_then(|it| it.stmt_list()))
             }
-            ast::Expr::EffectExpr(e) if e.label().is_some() && label_matches(e.label()) => {
-                hl(None, e.label(), e.block_expr())
+            ast::Expr::BlockExpr(e) if e.label().is_some() && label_matches(e.label()) => {
+                hl(None, e.label(), e.stmt_list())
             }
             _ => continue,
         };
@@ -213,14 +250,14 @@ fn hl(
         async_token: Option<SyntaxToken>,
         body: Option<ast::Expr>,
     ) -> Option<Vec<HighlightedRange>> {
-        let mut highlights = Vec::new();
-        highlights.push(HighlightedRange { access: None, range: async_token?.text_range() });
+        let mut highlights =
+            vec![HighlightedRange { category: None, range: async_token?.text_range() }];
         if let Some(body) = body {
-            body.walk(&mut |expr| {
+            walk_expr(&body, &mut |expr| {
                 if let ast::Expr::AwaitExpr(expr) = expr {
                     if let Some(token) = expr.await_token() {
                         highlights
-                            .push(HighlightedRange { access: None, range: token.text_range() });
+                            .push(HighlightedRange { category: None, range: token.text_range() });
                     }
                 }
             });
@@ -231,7 +268,12 @@ fn hl(
         return match_ast! {
             match anc {
                 ast::Fn(fn_) => hl(fn_.async_token(), fn_.body().map(ast::Expr::BlockExpr)),
-                ast::EffectExpr(effect) => hl(effect.async_token(), effect.block_expr().map(ast::Expr::BlockExpr)),
+                ast::BlockExpr(block_expr) => {
+                    if block_expr.async_token().is_none() {
+                        continue;
+                    }
+                    hl(block_expr.async_token(), Some(block_expr.into()))
+                },
                 ast::ClosureExpr(closure) => hl(closure.async_token(), closure.body()),
                 _ => continue,
             }
@@ -249,6 +291,13 @@ fn cover_range(r0: Option<TextRange>, r1: Option<TextRange>) -> Option<TextRange
     }
 }
 
+fn find_defs(sema: &Semantics<RootDatabase>, token: SyntaxToken) -> FxHashSet<Definition> {
+    sema.descend_into_macros(token)
+        .into_iter()
+        .flat_map(|token| Definition::from_token(sema, &token))
+        .collect()
+}
+
 #[cfg(test)]
 mod tests {
     use crate::fixture;
@@ -256,8 +305,20 @@ mod tests {
     use super::*;
 
     fn check(ra_fixture: &str) {
+        let config = HighlightRelatedConfig {
+            break_points: true,
+            exit_points: true,
+            references: true,
+            yield_points: true,
+        };
+
+        check_with_config(ra_fixture, config);
+    }
+
+    fn check_with_config(ra_fixture: &str, config: HighlightRelatedConfig) {
         let (analysis, pos, annotations) = fixture::annotations(ra_fixture);
-        let hls = analysis.highlight_related(pos).unwrap().unwrap();
+
+        let hls = analysis.highlight_related(config, pos).unwrap().unwrap_or_default();
 
         let mut expected = annotations
             .into_iter()
@@ -269,10 +330,10 @@ fn check(ra_fixture: &str) {
             .map(|hl| {
                 (
                     hl.range,
-                    hl.access.map(|it| {
+                    hl.category.map(|it| {
                         match it {
-                            ReferenceAccess::Read => "read",
-                            ReferenceAccess::Write => "write",
+                            ReferenceCategory::Read => "read",
+                            ReferenceCategory::Write => "write",
                         }
                         .to_string()
                     }),
@@ -315,6 +376,7 @@ fn test_hl_self_in_module() {
 mod foo;
 //- /foo.rs
 use self$0;
+ // ^^^^
 "#,
         );
     }
@@ -333,6 +395,61 @@ fn foo() {
         );
     }
 
+    #[test]
+    fn test_hl_local_in_attr() {
+        check(
+            r#"
+//- proc_macros: identity
+#[proc_macros::identity]
+fn foo() {
+    let mut bar = 3;
+         // ^^^ write
+    bar$0;
+ // ^^^ read
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn test_multi_macro_usage() {
+        check(
+            r#"
+macro_rules! foo {
+    ($ident:ident) => {
+        fn $ident() -> $ident { loop {} }
+        struct $ident;
+    }
+}
+
+foo!(bar$0);
+  // ^^^
+fn foo() {
+    let bar: bar = bar();
+          // ^^^
+                // ^^^
+}
+"#,
+        );
+        check(
+            r#"
+macro_rules! foo {
+    ($ident:ident) => {
+        fn $ident() -> $ident { loop {} }
+        struct $ident;
+    }
+}
+
+foo!(bar);
+  // ^^^
+fn foo() {
+    let bar: bar$0 = bar();
+          // ^^^
+}
+"#,
+        );
+    }
+
     #[test]
     fn test_hl_yield_points() {
         check(
@@ -445,6 +562,25 @@ fn foo() ->$0 u32 {
         );
     }
 
+    #[test]
+    fn test_hl_exit_points3() {
+        check(
+            r#"
+fn$0 foo() -> u32 {
+    if true {
+        return 0;
+     // ^^^^^^
+    }
+
+    0?;
+  // ^
+    0xDEAD_BEEF
+ // ^^^^^^^^^^^
+}
+"#,
+        );
+    }
+
     #[test]
     fn test_hl_prefer_ref_over_tail_exit() {
         check(
@@ -504,6 +640,9 @@ fn foo() ->$0 u32 {
             5
          // ^
         }
+    } else if false {
+        0
+     // ^
     } else {
         match 5 {
             6 => 100,
@@ -721,4 +860,202 @@ fn foo() {
 "#,
         );
     }
+
+    #[test]
+    fn test_hl_field_shorthand() {
+        check(
+            r#"
+struct Struct { field: u32 }
+              //^^^^^
+fn function(field: u32) {
+          //^^^^^
+    Struct { field$0 }
+           //^^^^^ read
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn test_hl_disabled_ref_local() {
+        let config = HighlightRelatedConfig {
+            references: false,
+            break_points: true,
+            exit_points: true,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+fn foo() {
+    let x$0 = 5;
+    let y = x * 2;
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_ref_local_preserved_break() {
+        let config = HighlightRelatedConfig {
+            references: false,
+            break_points: true,
+            exit_points: true,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+fn foo() {
+    let x$0 = 5;
+    let y = x * 2;
+
+    loop {
+        break;
+    }
+}"#;
+
+        check_with_config(ra_fixture, config.clone());
+
+        let ra_fixture = r#"
+fn foo() {
+    let x = 5;
+    let y = x * 2;
+
+    loop$0 {
+//  ^^^^
+        break;
+//      ^^^^^
+    }
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_ref_local_preserved_yield() {
+        let config = HighlightRelatedConfig {
+            references: false,
+            break_points: true,
+            exit_points: true,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+async fn foo() {
+    let x$0 = 5;
+    let y = x * 2;
+
+    0.await;
+}"#;
+
+        check_with_config(ra_fixture, config.clone());
+
+        let ra_fixture = r#"
+    async fn foo() {
+//  ^^^^^
+        let x = 5;
+        let y = x * 2;
+
+        0.await$0;
+//        ^^^^^
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_ref_local_preserved_exit() {
+        let config = HighlightRelatedConfig {
+            references: false,
+            break_points: true,
+            exit_points: true,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+fn foo() -> i32 {
+    let x$0 = 5;
+    let y = x * 2;
+
+    if true {
+        return y;
+    }
+
+    0?
+}"#;
+
+        check_with_config(ra_fixture, config.clone());
+
+        let ra_fixture = r#"
+fn foo() ->$0 i32 {
+    let x = 5;
+    let y = x * 2;
+
+    if true {
+        return y;
+//      ^^^^^^
+    }
+
+    0?
+//   ^
+"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_break() {
+        let config = HighlightRelatedConfig {
+            references: true,
+            break_points: false,
+            exit_points: true,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+fn foo() {
+    loop {
+        break$0;
+    }
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_yield() {
+        let config = HighlightRelatedConfig {
+            references: true,
+            break_points: true,
+            exit_points: true,
+            yield_points: false,
+        };
+
+        let ra_fixture = r#"
+async$0 fn foo() {
+    0.await;
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
+
+    #[test]
+    fn test_hl_disabled_exit() {
+        let config = HighlightRelatedConfig {
+            references: true,
+            break_points: true,
+            exit_points: false,
+            yield_points: true,
+        };
+
+        let ra_fixture = r#"
+fn foo() ->$0 i32 {
+    if true {
+        return -1;
+    }
+
+    42
+}"#;
+
+        check_with_config(ra_fixture, config);
+    }
 }