diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index a7c8a92f0eda..3e6c0cd0c9c8 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -292,13 +292,14 @@ fn handle_control_flow_keywords( token: &SyntaxToken, ) -> Option> { match token.kind() { - // For `fn` / `loop` / `while` / `for` / `async`, return the keyword it self, + // For `fn` / `loop` / `while` / `for` / `async` / `match`, return the keyword it self, // so that VSCode will find the references when using `ctrl + click` T![fn] | T![async] | T![try] | T![return] => nav_for_exit_points(sema, token), T![loop] | T![while] | T![break] | T![continue] => nav_for_break_points(sema, token), T![for] if token.parent().and_then(ast::ForExpr::cast).is_some() => { nav_for_break_points(sema, token) } + T![match] | T![=>] | T![if] => nav_for_branch_exit_points(sema, token), _ => None, } } @@ -408,6 +409,106 @@ fn nav_for_exit_points( Some(navs) } +pub(crate) fn find_branch_root( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, +) -> Vec { + fn find_root( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, + pred: impl Fn(SyntaxNode) -> Option, + ) -> Vec { + let mut result = Vec::new(); + for token in sema.descend_into_macros(token.clone()) { + for node in sema.token_ancestors_with_macros(token) { + if ast::MacroCall::can_cast(node.kind()) { + break; + } + + if let Some(node) = pred(node) { + result.push(node); + break; + } + } + } + result + } + + match token.kind() { + T![match] => { + find_root(sema, token, |node| Some(ast::MatchExpr::cast(node)?.syntax().clone())) + } + T![=>] => find_root(sema, token, |node| Some(ast::MatchArm::cast(node)?.syntax().clone())), + T![if] => find_root(sema, token, |node| { + let if_expr = ast::IfExpr::cast(node)?; + + iter::successors(Some(if_expr.clone()), |if_expr| { + let parent_if = if_expr.syntax().parent().and_then(ast::IfExpr::cast)?; + if let ast::ElseBranch::IfExpr(nested_if) = parent_if.else_branch()? { + (nested_if.syntax() == if_expr.syntax()).then_some(parent_if) + } else { + None + } + }) + .last() + .map(|if_expr| if_expr.syntax().clone()) + }), + _ => vec![], + } +} + +fn nav_for_branch_exit_points( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, +) -> Option> { + let db = sema.db; + + let navs = match token.kind() { + T![match] => find_branch_root(sema, token) + .into_iter() + .filter_map(|node| { + let match_expr = ast::MatchExpr::cast(node)?; + let file_id = sema.hir_file_for(match_expr.syntax()); + let focus_range = match_expr.match_token()?.text_range(); + let match_expr_in_file = InFile::new(file_id, match_expr.into()); + Some(expr_to_nav(db, match_expr_in_file, Some(focus_range))) + }) + .flatten() + .collect_vec(), + + T![=>] => find_branch_root(sema, token) + .into_iter() + .filter_map(|node| { + let match_arm = ast::MatchArm::cast(node)?; + let match_expr = sema + .ancestors_with_macros(match_arm.syntax().clone()) + .find_map(ast::MatchExpr::cast)?; + let file_id = sema.hir_file_for(match_expr.syntax()); + let focus_range = match_arm.fat_arrow_token()?.text_range(); + let match_expr_in_file = InFile::new(file_id, match_expr.into()); + Some(expr_to_nav(db, match_expr_in_file, Some(focus_range))) + }) + .flatten() + .collect_vec(), + + T![if] => find_branch_root(sema, token) + .into_iter() + .filter_map(|node| { + let if_expr = ast::IfExpr::cast(node)?; + let file_id = sema.hir_file_for(if_expr.syntax()); + let focus_range = if_expr.if_token()?.text_range(); + let if_expr_in_file = InFile::new(file_id, if_expr.into()); + Some(expr_to_nav(db, if_expr_in_file, Some(focus_range))) + }) + .flatten() + .collect_vec(), + + _ => return Some(Vec::new()), + }; + + Some(navs) +} + pub(crate) fn find_loops( sema: &Semantics<'_, RootDatabase>, token: &SyntaxToken, @@ -3474,6 +3575,157 @@ fn main() { fn f1() {} } } +"#, + ); + } + + #[test] + fn goto_def_for_match_keyword() { + check( + r#" +fn main() { + match$0 0 { + // ^^^^^ + 0 => {}, + _ => {}, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_match_arm_fat_arrow() { + check( + r#" +fn main() { + match 0 { + 0 =>$0 {}, + // ^^ + _ => {}, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_if_keyword() { + check( + r#" +fn main() { + if$0 true { + // ^^ + () + } +} +"#, + ); + } + + #[test] + fn goto_def_for_match_nested_in_if() { + check( + r#" +fn main() { + if true { + match$0 0 { + // ^^^^^ + 0 => {}, + _ => {}, + } + } +} +"#, + ); + } + + #[test] + fn goto_def_for_multiple_match_expressions() { + check( + r#" +fn main() { + match 0 { + 0 => {}, + _ => {}, + }; + + match$0 1 { + // ^^^^^ + 1 => {}, + _ => {}, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_nested_match_expressions() { + check( + r#" +fn main() { + match 0 { + 0 => match$0 1 { + // ^^^^^ + 1 => {}, + _ => {}, + }, + _ => {}, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_if_else_chains() { + check( + r#" +fn main() { + if true { + // ^^ + () + } else if$0 false { + () + } else { + () + } +} +"#, + ); + } + + #[test] + fn goto_def_for_match_with_guards() { + check( + r#" +fn main() { + match 42 { + x if x > 0 =>$0 {}, + // ^^ + _ => {}, + } +} +"#, + ); + } + + #[test] + fn goto_def_for_match_with_macro_arm() { + check( + r#" +macro_rules! arm { + () => { 0 => {} }; +} + +fn main() { + match$0 0 { + // ^^^^^ + arm!(), + _ => {}, + } +} "#, ); } diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index bb96c925190b..4d7098656b68 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -39,8 +39,11 @@ pub struct HighlightRelatedConfig { pub break_points: bool, pub closure_captures: bool, pub yield_points: bool, + pub branch_exit_points: bool, } +type HighlightMap = FxHashMap>; + // Feature: Highlight Related // // Highlights constructs related to the thing under the cursor: @@ -66,10 +69,9 @@ pub(crate) fn highlight_related( ide_db::base_db::EditionedFileId::new(sema.db.as_dyn_database(), file_id); let syntax = sema.parse(editioned_file_id_wrapper).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![->] => 4, + T![->] | T![=>] => 4, kind if kind.is_keyword(file_id.edition()) => 3, IDENT | INT_NUMBER => 2, T![|] => 1, @@ -83,6 +85,9 @@ pub(crate) fn highlight_related( T![fn] | T![return] | T![->] if config.exit_points => { highlight_exit_points(sema, token).remove(&file_id) } + T![match] | T![=>] | T![if] if config.branch_exit_points => { + highlight_branch_exit_points(sema, token).remove(&file_id) + } T![await] | T![async] if config.yield_points => { highlight_yield_points(sema, token).remove(&file_id) } @@ -302,11 +307,93 @@ fn highlight_references( if res.is_empty() { None } else { Some(res.into_iter().collect()) } } +pub(crate) fn highlight_branch_exit_points( + sema: &Semantics<'_, RootDatabase>, + token: SyntaxToken, +) -> FxHashMap> { + let mut highlights: HighlightMap = FxHashMap::default(); + + let push_to_highlights = |file_id, range, highlights: &mut HighlightMap| { + if let Some(FileRange { file_id, range }) = original_frange(sema.db, file_id, range) { + let hrange = HighlightedRange { category: ReferenceCategory::empty(), range }; + highlights.entry(file_id).or_default().insert(hrange); + } + }; + + let push_tail_expr = |tail: Option, highlights: &mut HighlightMap| { + let Some(tail) = tail else { + return; + }; + + for_each_tail_expr(&tail, &mut |tail| { + let file_id = sema.hir_file_for(tail.syntax()); + let range = tail.syntax().text_range(); + push_to_highlights(file_id, Some(range), highlights); + }); + }; + + let nodes = goto_definition::find_branch_root(sema, &token).into_iter(); + match token.kind() { + T![match] => { + for match_expr in nodes.filter_map(ast::MatchExpr::cast) { + let file_id = sema.hir_file_for(match_expr.syntax()); + let range = match_expr.match_token().map(|token| token.text_range()); + push_to_highlights(file_id, range, &mut highlights); + + let Some(arm_list) = match_expr.match_arm_list() else { + continue; + }; + for arm in arm_list.arms() { + push_tail_expr(arm.expr(), &mut highlights); + } + } + } + T![=>] => { + for arm in nodes.filter_map(ast::MatchArm::cast) { + let file_id = sema.hir_file_for(arm.syntax()); + let range = arm.fat_arrow_token().map(|token| token.text_range()); + push_to_highlights(file_id, range, &mut highlights); + + push_tail_expr(arm.expr(), &mut highlights); + } + } + T![if] => { + for mut if_to_process in nodes.map(ast::IfExpr::cast) { + while let Some(cur_if) = if_to_process.take() { + let file_id = sema.hir_file_for(cur_if.syntax()); + + let if_kw_range = cur_if.if_token().map(|token| token.text_range()); + push_to_highlights(file_id, if_kw_range, &mut highlights); + + if let Some(then_block) = cur_if.then_branch() { + push_tail_expr(Some(then_block.into()), &mut highlights); + } + + match cur_if.else_branch() { + Some(ast::ElseBranch::Block(else_block)) => { + push_tail_expr(Some(else_block.into()), &mut highlights); + if_to_process = None; + } + Some(ast::ElseBranch::IfExpr(nested_if)) => if_to_process = Some(nested_if), + None => if_to_process = None, + } + } + } + } + _ => unreachable!(), + } + + highlights + .into_iter() + .map(|(file_id, ranges)| (file_id, ranges.into_iter().collect())) + .collect() +} + fn hl_exit_points( sema: &Semantics<'_, RootDatabase>, def_token: Option, body: ast::Expr, -) -> Option>> { +) -> Option { let mut highlights: FxHashMap> = FxHashMap::default(); let mut push_to_highlights = |file_id, range| { @@ -413,7 +500,7 @@ pub(crate) fn highlight_break_points( loop_token: Option, label: Option, expr: ast::Expr, - ) -> Option>> { + ) -> Option { let mut highlights: FxHashMap> = FxHashMap::default(); let mut push_to_highlights = |file_id, range| { @@ -506,7 +593,7 @@ pub(crate) fn highlight_yield_points( sema: &Semantics<'_, RootDatabase>, async_token: Option, body: Option, - ) -> Option>> { + ) -> Option { let mut highlights: FxHashMap> = FxHashMap::default(); let mut push_to_highlights = |file_id, range| { @@ -599,10 +686,7 @@ fn original_frange( InFile::new(file_id, text_range?).original_node_file_range_opt(db).map(|(frange, _)| frange) } -fn merge_map( - res: &mut FxHashMap>, - new: Option>>, -) { +fn merge_map(res: &mut HighlightMap, new: Option) { let Some(new) = new else { return; }; @@ -714,6 +798,7 @@ mod tests { references: true, closure_captures: true, yield_points: true, + branch_exit_points: true, }; #[track_caller] @@ -2072,6 +2157,62 @@ fn main() { ) } + #[test] + fn nested_match() { + check( + r#" +fn main() { + match$0 0 { + // ^^^^^ + 0 => match 1 { + 1 => 2, + // ^ + _ => 3, + // ^ + }, + _ => 4, + // ^ + } +} +"#, + ) + } + + #[test] + fn single_arm_highlight() { + check( + r#" +fn main() { + match 0 { + 0 =>$0 { + // ^^ + let x = 1; + x + // ^ + } + _ => 2, + } +} +"#, + ) + } + + #[test] + fn no_branches_when_disabled() { + let config = HighlightRelatedConfig { branch_exit_points: false, ..ENABLED_CONFIG }; + check_with_config( + r#" +fn main() { + match$0 0 { + 0 => 1, + _ => 2, + } +} +"#, + config, + ); + } + #[test] fn asm() { check( @@ -2102,6 +2243,173 @@ pub unsafe fn bootstrap() -> ! { ) } + #[test] + fn complex_arms_highlight() { + check( + r#" +fn calculate(n: i32) -> i32 { n * 2 } + +fn main() { + match$0 Some(1) { + // ^^^^^ + Some(x) => match x { + 0 => { let y = x; y }, + // ^ + 1 => calculate(x), + //^^^^^^^^^^^^ + _ => (|| 6)(), + // ^^^^^^^^ + }, + None => loop { + break 5; + // ^^^^^^^ + }, + } +} +"#, + ) + } + + #[test] + fn match_in_macro_highlight() { + check( + r#" +macro_rules! M { + ($e:expr) => { $e }; +} + +fn main() { + M!{ + match$0 Some(1) { + // ^^^^^ + Some(x) => x, + // ^ + None => 0, + // ^ + } + } +} +"#, + ) + } + + #[test] + fn nested_if_else() { + check( + r#" +fn main() { + if$0 true { + // ^^ + if false { + 1 + // ^ + } else { + 2 + // ^ + } + } else { + 3 + // ^ + } +} +"#, + ) + } + + #[test] + fn if_else_if_highlight() { + check( + r#" +fn main() { + if$0 true { + // ^^ + 1 + // ^ + } else if false { + // ^^ + 2 + // ^ + } else { + 3 + // ^ + } +} +"#, + ) + } + + #[test] + fn complex_if_branches() { + check( + r#" +fn calculate(n: i32) -> i32 { n * 2 } + +fn main() { + if$0 true { + // ^^ + let x = 5; + calculate(x) + // ^^^^^^^^^^^^ + } else if false { + // ^^ + (|| 10)() + // ^^^^^^^^^ + } else { + loop { + break 15; + // ^^^^^^^^ + } + } +} +"#, + ) + } + + #[test] + fn if_in_macro_highlight() { + check( + r#" +macro_rules! M { + ($e:expr) => { $e }; +} + +fn main() { + M!{ + if$0 true { + // ^^ + 5 + // ^ + } else { + 10 + // ^^ + } + } +} +"#, + ) + } + + #[test] + fn match_in_macro() { + // We should not highlight the outer `match` expression. + check( + r#" +macro_rules! M { + (match) => { 1 }; +} + +fn main() { + match Some(1) { + Some(x) => x, + None => { + M!(match$0) + } + } +} + "#, + ) + } + #[test] fn labeled_block_tail_expr() { check( diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 2487543dcfe2..dd83cd3b1190 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -13,6 +13,7 @@ use hir::{PathResolution, Semantics}; use ide_db::{ FileId, RootDatabase, defs::{Definition, NameClass, NameRefClass}, + helpers::pick_best_token, search::{ReferenceCategory, SearchScope, UsageSearchResult}, }; use itertools::Itertools; @@ -309,7 +310,11 @@ fn handle_control_flow_keywords( let file = sema.parse_guess_edition(file_id); let edition = sema.attach_first_edition(file_id).map(|it| it.edition()).unwrap_or(Edition::CURRENT); - let token = file.syntax().token_at_offset(offset).find(|t| t.kind().is_keyword(edition))?; + let token = pick_best_token(file.syntax().token_at_offset(offset), |kind| match kind { + _ if kind.is_keyword(edition) => 4, + T![=>] => 3, + _ => 1, + })?; let references = match token.kind() { T![fn] | T![return] | T![try] => highlight_related::highlight_exit_points(sema, token), @@ -320,6 +325,7 @@ fn handle_control_flow_keywords( T![for] if token.parent().and_then(ast::ForExpr::cast).is_some() => { highlight_related::highlight_break_points(sema, token) } + T![if] | T![=>] | T![match] => highlight_related::highlight_branch_exit_points(sema, token), _ => return None, } .into_iter() @@ -1254,6 +1260,159 @@ impl Foo { ); } + #[test] + fn test_highlight_if_branches() { + check( + r#" +fn main() { + let x = if$0 true { + 1 + } else if false { + 2 + } else { + 3 + }; + + println!("x: {}", x); +} +"#, + expect![[r#" + FileId(0) 24..26 + FileId(0) 42..43 + FileId(0) 55..57 + FileId(0) 74..75 + FileId(0) 97..98 + "#]], + ); + } + + #[test] + fn test_highlight_match_branches() { + check( + r#" +fn main() { + $0match Some(42) { + Some(x) if x > 0 => println!("positive"), + Some(0) => println!("zero"), + Some(_) => println!("negative"), + None => println!("none"), + }; +} +"#, + expect![[r#" + FileId(0) 16..21 + FileId(0) 61..81 + FileId(0) 102..118 + FileId(0) 139..159 + FileId(0) 177..193 + "#]], + ); + } + + #[test] + fn test_highlight_match_arm_arrow() { + check( + r#" +fn main() { + match Some(42) { + Some(x) if x > 0 $0=> println!("positive"), + Some(0) => println!("zero"), + Some(_) => println!("negative"), + None => println!("none"), + } +} +"#, + expect![[r#" + FileId(0) 58..60 + FileId(0) 61..81 + "#]], + ); + } + + #[test] + fn test_highlight_nested_branches() { + check( + r#" +fn main() { + let x = $0if true { + if false { + 1 + } else { + match Some(42) { + Some(_) => 2, + None => 3, + } + } + } else { + 4 + }; + + println!("x: {}", x); +} +"#, + expect![[r#" + FileId(0) 24..26 + FileId(0) 65..66 + FileId(0) 140..141 + FileId(0) 167..168 + FileId(0) 215..216 + "#]], + ); + } + + #[test] + fn test_highlight_match_with_complex_guards() { + check( + r#" +fn main() { + let x = $0match (x, y) { + (a, b) if a > b && a % 2 == 0 => 1, + (a, b) if a < b || b % 2 == 1 => 2, + (a, _) if a > 40 => 3, + _ => 4, + }; + + println!("x: {}", x); +} +"#, + expect![[r#" + FileId(0) 24..29 + FileId(0) 80..81 + FileId(0) 124..125 + FileId(0) 155..156 + FileId(0) 171..172 + "#]], + ); + } + + #[test] + fn test_highlight_mixed_if_match_expressions() { + check( + r#" +fn main() { + let x = $0if let Some(x) = Some(42) { + 1 + } else if let None = None { + 2 + } else { + match 42 { + 0 => 3, + _ => 4, + } + }; +} +"#, + expect![[r#" + FileId(0) 24..26 + FileId(0) 60..61 + FileId(0) 73..75 + FileId(0) 102..103 + FileId(0) 153..154 + FileId(0) 173..174 + "#]], + ); + } + fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str, expect: Expect) { check_with_scope(ra_fixture, None, expect) } @@ -2776,4 +2935,66 @@ const FOO$0: i32 = 0; "#]], ); } + + #[test] + fn test_highlight_if_let_match_combined() { + check( + r#" +enum MyEnum { A(i32), B(String), C } + +fn main() { + let val = MyEnum::A(42); + + let x = $0if let MyEnum::A(x) = val { + 1 + } else if let MyEnum::B(s) = val { + 2 + } else { + match val { + MyEnum::C => 3, + _ => 4, + } + }; +} +"#, + expect![[r#" + FileId(0) 92..94 + FileId(0) 128..129 + FileId(0) 141..143 + FileId(0) 177..178 + FileId(0) 237..238 + FileId(0) 257..258 + "#]], + ); + } + + #[test] + fn test_highlight_nested_match_expressions() { + check( + r#" +enum Outer { A(Inner), B } +enum Inner { X, Y(i32) } + +fn main() { + let val = Outer::A(Inner::Y(42)); + + $0match val { + Outer::A(inner) => match inner { + Inner::X => println!("Inner::X"), + Inner::Y(n) if n > 0 => println!("Inner::Y positive: {}", n), + Inner::Y(_) => println!("Inner::Y non-positive"), + }, + Outer::B => println!("Outer::B"), + } +} +"#, + expect![[r#" + FileId(0) 108..113 + FileId(0) 185..205 + FileId(0) 243..279 + FileId(0) 308..341 + FileId(0) 374..394 + "#]], + ); + } } diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index aceacff98597..4f74f8281c89 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -93,6 +93,8 @@ config_data! { + /// Enables highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`). + highlightRelated_branchExitPoints_enable: bool = true, /// Enables highlighting of related references while the cursor is on `break`, `loop`, `while`, or `for` keywords. highlightRelated_breakPoints_enable: bool = true, /// Enables highlighting of all captures of a closure while the cursor is on the `|` or move keyword of a closure. @@ -1594,6 +1596,7 @@ impl Config { exit_points: self.highlightRelated_exitPoints_enable().to_owned(), yield_points: self.highlightRelated_yieldPoints_enable().to_owned(), closure_captures: self.highlightRelated_closureCaptures_enable().to_owned(), + branch_exit_points: self.highlightRelated_branchExitPoints_enable().to_owned(), } } diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md index 860d8240fd37..a187acedc2a7 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -492,6 +492,11 @@ also need to add the folders to Code's `files.watcherExclude`. Controls file watching implementation. +**rust-analyzer.highlightRelated.branchExitPoints.enable** (default: true) + + Enables highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`). + + **rust-analyzer.highlightRelated.breakPoints.enable** (default: true) Enables highlighting of related references while the cursor is on `break`, `loop`, `while`, or `for` keywords. diff --git a/editors/code/package.json b/editors/code/package.json index ae05992321a9..24e8873f65dc 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -1518,6 +1518,16 @@ } } }, + { + "title": "highlightRelated", + "properties": { + "rust-analyzer.highlightRelated.branchExitPoints.enable": { + "markdownDescription": "Enables highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`).", + "default": true, + "type": "boolean" + } + } + }, { "title": "highlightRelated", "properties": {