From f87220e22a2dbd35ffe9b0853f858969352e5131 Mon Sep 17 00:00:00 2001 From: roifewu Date: Wed, 9 Apr 2025 11:29:05 +0800 Subject: [PATCH 1/5] feat: highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`) --- crates/ide/src/goto_definition.rs | 210 ++++++++++++++ crates/ide/src/highlight_related.rs | 333 ++++++++++++++++++++++- crates/ide/src/references.rs | 221 ++++++++++++++- crates/rust-analyzer/src/config.rs | 3 + docs/book/src/configuration_generated.md | 7 + editors/code/package.json | 10 + 6 files changed, 775 insertions(+), 9 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 574803fb9e..0efc6cfe9b 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -298,6 +298,7 @@ fn handle_control_flow_keywords( 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_branches(sema, token), _ => None, } } @@ -407,6 +408,64 @@ fn nav_for_exit_points( Some(navs) } +fn nav_for_branches( + sema: &Semantics<'_, RootDatabase>, + token: &SyntaxToken, +) -> Option> { + let db = sema.db; + + let navs = match token.kind() { + T![match] => sema + .descend_into_macros(token.clone()) + .into_iter() + .filter_map(|token| { + let match_expr = + sema.token_ancestors_with_macros(token).find_map(ast::MatchExpr::cast)?; + 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![=>] => sema + .descend_into_macros(token.clone()) + .into_iter() + .filter_map(|token| { + let match_arm = + sema.token_ancestors_with_macros(token).find_map(ast::MatchArm::cast)?; + 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] => sema + .descend_into_macros(token.clone()) + .into_iter() + .filter_map(|token| { + let if_expr = + sema.token_ancestors_with_macros(token).find_map(ast::IfExpr::cast)?; + 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, @@ -3614,4 +3673,155 @@ fn foo() { "#, ); } + + #[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 aa947921a9..8d93cd6328 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -37,8 +37,11 @@ pub struct HighlightRelatedConfig { pub break_points: bool, pub closure_captures: bool, pub yield_points: bool, + pub branches: bool, } +type HighlightMap = FxHashMap>; + // Feature: Highlight Related // // Highlights constructs related to the thing under the cursor: @@ -64,7 +67,7 @@ pub(crate) fn highlight_related( 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(sema.db)) => 3, IDENT | INT_NUMBER => 2, T![|] => 1, @@ -78,6 +81,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.branches => { + highlight_branches(sema, token).remove(&file_id) + } T![await] | T![async] if config.yield_points => { highlight_yield_points(sema, token).remove(&file_id) } @@ -300,11 +306,122 @@ fn highlight_references( if res.is_empty() { None } else { Some(res.into_iter().collect()) } } +pub(crate) fn highlight_branches( + 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); + }); + }; + + match token.kind() { + T![match] => { + for token in sema.descend_into_macros(token.clone()) { + let Some(match_expr) = + sema.token_ancestors_with_macros(token).find_map(ast::MatchExpr::cast) + else { + continue; + }; + + 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 token in sema.descend_into_macros(token.clone()) { + let Some(arm) = + sema.token_ancestors_with_macros(token).find_map(ast::MatchArm::cast) + else { + continue; + }; + 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 tok in sema.descend_into_macros(token.clone()) { + let Some(if_expr) = + sema.token_ancestors_with_macros(tok).find_map(ast::IfExpr::cast) + else { + continue; + }; + + // Find the root of the if expression + let mut if_to_process = 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() + .or(Some(if_expr)); + + 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| { @@ -411,7 +528,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| { @@ -504,7 +621,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| { @@ -597,10 +714,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; }; @@ -750,6 +864,7 @@ mod tests { references: true, closure_captures: true, yield_points: true, + branches: true, }; #[track_caller] @@ -2134,6 +2249,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 { branches: false, ..ENABLED_CONFIG }; + check_with_config( + r#" +fn main() { + match$0 0 { + 0 => 1, + _ => 2, + } +} +"#, + config, + ); + } + #[test] fn asm() { check( @@ -2164,6 +2335,152 @@ 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 labeled_block_tail_expr() { check( diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index c6a323d408..900f49910d 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -397,7 +397,10 @@ fn handle_control_flow_keywords( .attach_first_edition(file_id) .map(|it| it.edition(sema.db)) .unwrap_or(Edition::CURRENT); - let token = file.syntax().token_at_offset(offset).find(|t| t.kind().is_keyword(edition))?; + let token = file + .syntax() + .token_at_offset(offset) + .find(|t| t.kind().is_keyword(edition) || t.kind() == T![=>])?; let references = match token.kind() { T![fn] | T![return] | T![try] => highlight_related::highlight_exit_points(sema, token), @@ -408,6 +411,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_branches(sema, token), _ => return None, } .into_iter() @@ -1344,6 +1348,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) } @@ -2867,4 +3024,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 05e1b832cd..cc8711f2c0 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -94,6 +94,8 @@ config_data! { + /// Enables highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`). + highlightRelated_branches_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. @@ -1629,6 +1631,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(), + branches: self.highlightRelated_branches_enable().to_owned(), } } diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md index 9404b1454a..587c09234c 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -612,6 +612,13 @@ Default: `"client"` Controls file watching implementation. +## rust-analyzer.highlightRelated.branches.enable {#highlightRelated.branches.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 {#highlightRelated.breakPoints.enable} Default: `true` diff --git a/editors/code/package.json b/editors/code/package.json index 26a21c1468..c792ef0ffd 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -1529,6 +1529,16 @@ } } }, + { + "title": "highlightRelated", + "properties": { + "rust-analyzer.highlightRelated.branches.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": { From 9fa0543504417a68e2f47bc2aab0d72eb19e1fe9 Mon Sep 17 00:00:00 2001 From: roifewu Date: Wed, 9 Apr 2025 13:26:57 +0800 Subject: [PATCH 2/5] refactor: improve macro handling in navigation for control-flow kws --- crates/ide/src/goto_definition.rs | 18 ++++++++----- crates/ide/src/highlight_related.rs | 42 ++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 0efc6cfe9b..35eb18e3c7 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -419,8 +419,10 @@ fn nav_for_branches( .descend_into_macros(token.clone()) .into_iter() .filter_map(|token| { - let match_expr = - sema.token_ancestors_with_macros(token).find_map(ast::MatchExpr::cast)?; + let match_expr = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::MatchExpr::cast)?; 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()); @@ -433,8 +435,10 @@ fn nav_for_branches( .descend_into_macros(token.clone()) .into_iter() .filter_map(|token| { - let match_arm = - sema.token_ancestors_with_macros(token).find_map(ast::MatchArm::cast)?; + let match_arm = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::MatchArm::cast)?; let match_expr = sema .ancestors_with_macros(match_arm.syntax().clone()) .find_map(ast::MatchExpr::cast)?; @@ -450,8 +454,10 @@ fn nav_for_branches( .descend_into_macros(token.clone()) .into_iter() .filter_map(|token| { - let if_expr = - sema.token_ancestors_with_macros(token).find_map(ast::IfExpr::cast)?; + let if_expr = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::IfExpr::cast)?; 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()); diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index 8d93cd6328..28447005c1 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -334,8 +334,10 @@ pub(crate) fn highlight_branches( match token.kind() { T![match] => { for token in sema.descend_into_macros(token.clone()) { - let Some(match_expr) = - sema.token_ancestors_with_macros(token).find_map(ast::MatchExpr::cast) + let Some(match_expr) = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::MatchExpr::cast) else { continue; }; @@ -355,11 +357,14 @@ pub(crate) fn highlight_branches( } T![=>] => { for token in sema.descend_into_macros(token.clone()) { - let Some(arm) = - sema.token_ancestors_with_macros(token).find_map(ast::MatchArm::cast) + let Some(arm) = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::MatchArm::cast) else { continue; }; + 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); @@ -368,9 +373,11 @@ pub(crate) fn highlight_branches( } } T![if] => { - for tok in sema.descend_into_macros(token.clone()) { - let Some(if_expr) = - sema.token_ancestors_with_macros(tok).find_map(ast::IfExpr::cast) + for token in sema.descend_into_macros(token.clone()) { + let Some(if_expr) = sema + .token_ancestors_with_macros(token) + .take_while(|node| !ast::MacroCall::can_cast(node.kind())) + .find_map(ast::IfExpr::cast) else { continue; }; @@ -2481,6 +2488,27 @@ fn main() { ) } + #[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( From c36758def492ef410259493121b7355af93c75c8 Mon Sep 17 00:00:00 2001 From: roifewu Date: Tue, 15 Apr 2025 11:02:14 +0800 Subject: [PATCH 3/5] refactor: rename `branches` to `branch_exit_points` in highlight_related --- crates/ide/src/highlight_related.rs | 12 ++++++------ crates/ide/src/references.rs | 2 +- crates/rust-analyzer/src/config.rs | 4 ++-- docs/book/src/configuration_generated.md | 2 +- editors/code/package.json | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index 28447005c1..77b8599fd0 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -37,7 +37,7 @@ pub struct HighlightRelatedConfig { pub break_points: bool, pub closure_captures: bool, pub yield_points: bool, - pub branches: bool, + pub branch_exit_points: bool, } type HighlightMap = FxHashMap>; @@ -81,8 +81,8 @@ 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.branches => { - highlight_branches(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) @@ -306,7 +306,7 @@ fn highlight_references( if res.is_empty() { None } else { Some(res.into_iter().collect()) } } -pub(crate) fn highlight_branches( +pub(crate) fn highlight_branch_exit_points( sema: &Semantics<'_, RootDatabase>, token: SyntaxToken, ) -> FxHashMap> { @@ -871,7 +871,7 @@ mod tests { references: true, closure_captures: true, yield_points: true, - branches: true, + branch_exit_points: true, }; #[track_caller] @@ -2298,7 +2298,7 @@ fn main() { #[test] fn no_branches_when_disabled() { - let config = HighlightRelatedConfig { branches: false, ..ENABLED_CONFIG }; + let config = HighlightRelatedConfig { branch_exit_points: false, ..ENABLED_CONFIG }; check_with_config( r#" fn main() { diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 900f49910d..205e93d7a2 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -411,7 +411,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_branches(sema, token), + T![if] | T![=>] | T![match] => highlight_related::highlight_branch_exit_points(sema, token), _ => return None, } .into_iter() diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index cc8711f2c0..e716d14075 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -95,7 +95,7 @@ config_data! { /// Enables highlighting of related return values while the cursor is on any `match`, `if`, or match arm arrow (`=>`). - highlightRelated_branches_enable: bool = true, + 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. @@ -1631,7 +1631,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(), - branches: self.highlightRelated_branches_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 587c09234c..ebac26e1d6 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -612,7 +612,7 @@ Default: `"client"` Controls file watching implementation. -## rust-analyzer.highlightRelated.branches.enable {#highlightRelated.branches.enable} +## rust-analyzer.highlightRelated.branchExitPoints.enable {#highlightRelated.branchExitPoints.enable} Default: `true` diff --git a/editors/code/package.json b/editors/code/package.json index c792ef0ffd..3cb4c21ee1 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -1532,7 +1532,7 @@ { "title": "highlightRelated", "properties": { - "rust-analyzer.highlightRelated.branches.enable": { + "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" From 79de21bd9da36d17236051594eebdb9d685db411 Mon Sep 17 00:00:00 2001 From: roifewu Date: Tue, 15 Apr 2025 11:04:46 +0800 Subject: [PATCH 4/5] refactor: simplify functions related to branch_exit_points in highlight_related --- crates/ide/src/goto_definition.rs | 90 ++++++++++++++++++++--------- crates/ide/src/highlight_related.rs | 46 ++------------- crates/ide/src/references.rs | 10 ++-- 3 files changed, 74 insertions(+), 72 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 35eb18e3c7..c90a544a71 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -291,14 +291,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_branches(sema, token), + T![match] | T![=>] | T![if] => nav_for_branch_exit_points(sema, token), _ => None, } } @@ -408,22 +408,66 @@ fn nav_for_exit_points( Some(navs) } -fn nav_for_branches( +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] => sema - .descend_into_macros(token.clone()) + T![match] => find_branch_root(sema, token) .into_iter() - .filter_map(|token| { - let match_expr = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::MatchExpr::cast)?; - let file_id = sema.hir_file_for(match_expr.syntax()); + .filter_map(|node| { + let file_id = sema.hir_file_for(&node); + let match_expr = ast::MatchExpr::cast(node)?; 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))) @@ -431,14 +475,10 @@ fn nav_for_branches( .flatten() .collect_vec(), - T![=>] => sema - .descend_into_macros(token.clone()) + T![=>] => find_branch_root(sema, token) .into_iter() - .filter_map(|token| { - let match_arm = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::MatchArm::cast)?; + .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)?; @@ -450,15 +490,11 @@ fn nav_for_branches( .flatten() .collect_vec(), - T![if] => sema - .descend_into_macros(token.clone()) + T![if] => find_branch_root(sema, token) .into_iter() - .filter_map(|token| { - let if_expr = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::IfExpr::cast)?; - let file_id = sema.hir_file_for(if_expr.syntax()); + .filter_map(|node| { + let file_id = sema.hir_file_for(&node); + let if_expr = ast::IfExpr::cast(node)?; 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))) @@ -3785,9 +3821,9 @@ fn main() { r#" fn main() { if true { + // ^^ () } else if$0 false { - // ^^ () } else { () diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index 77b8599fd0..6aa17f90ef 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -331,17 +331,10 @@ pub(crate) fn highlight_branch_exit_points( }); }; + let nodes = goto_definition::find_branch_root(sema, &token).into_iter(); match token.kind() { T![match] => { - for token in sema.descend_into_macros(token.clone()) { - let Some(match_expr) = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::MatchExpr::cast) - else { - continue; - }; - + 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); @@ -349,22 +342,13 @@ pub(crate) fn highlight_branch_exit_points( 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 token in sema.descend_into_macros(token.clone()) { - let Some(arm) = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::MatchArm::cast) - else { - continue; - }; - + 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); @@ -373,27 +357,7 @@ pub(crate) fn highlight_branch_exit_points( } } T![if] => { - for token in sema.descend_into_macros(token.clone()) { - let Some(if_expr) = sema - .token_ancestors_with_macros(token) - .take_while(|node| !ast::MacroCall::can_cast(node.kind())) - .find_map(ast::IfExpr::cast) - else { - continue; - }; - - // Find the root of the if expression - let mut if_to_process = 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() - .or(Some(if_expr)); - + 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()); @@ -415,7 +379,7 @@ pub(crate) fn highlight_branch_exit_points( } } } - _ => unreachable!(), + _ => {} } highlights diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index 205e93d7a2..fe874bc99b 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -21,6 +21,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; @@ -397,10 +398,11 @@ fn handle_control_flow_keywords( .attach_first_edition(file_id) .map(|it| it.edition(sema.db)) .unwrap_or(Edition::CURRENT); - let token = file - .syntax() - .token_at_offset(offset) - .find(|t| t.kind().is_keyword(edition) || t.kind() == T![=>])?; + 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), From 15e70c21d8e76d8cb32fdbef0038492e022be61b Mon Sep 17 00:00:00 2001 From: roifewu Date: Thu, 26 Jun 2025 13:40:24 +0800 Subject: [PATCH 5/5] refactor: enhance highlighting for control flow kws in macros --- crates/ide/src/goto_definition.rs | 51 ++++++++++------------------- crates/ide/src/highlight_related.rs | 27 +++++++++++++++ 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index c90a544a71..fd465f31d4 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -412,45 +412,30 @@ 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 - } + let find_nodes = |node_filter: fn(SyntaxNode) -> Option| { + sema.descend_into_macros(token.clone()) + .into_iter() + .filter_map(|token| node_filter(token.parent()?)) + .collect_vec() + }; 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| { + T![match] => find_nodes(|node| Some(ast::MatchExpr::cast(node)?.syntax().clone())), + T![=>] => find_nodes(|node| Some(ast::MatchArm::cast(node)?.syntax().clone())), + T![if] => find_nodes(|node| { let if_expr = ast::IfExpr::cast(node)?; - iter::successors(Some(if_expr.clone()), |if_expr| { + let root_if = 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 - } + let ast::ElseBranch::IfExpr(else_branch) = parent_if.else_branch()? else { + return None; + }; + + (else_branch.syntax() == if_expr.syntax()).then_some(parent_if) }) - .last() - .map(|if_expr| if_expr.syntax().clone()) + .last()?; + + Some(root_if.syntax().clone()) }), _ => vec![], } diff --git a/crates/ide/src/highlight_related.rs b/crates/ide/src/highlight_related.rs index 6aa17f90ef..356bd69aa4 100644 --- a/crates/ide/src/highlight_related.rs +++ b/crates/ide/src/highlight_related.rs @@ -2356,6 +2356,33 @@ fn main() { ) } + #[test] + fn match_in_macro_highlight_2() { + check( + r#" +macro_rules! match_ast { + (match $node:ident { $($tt:tt)* }) => { $crate::match_ast!(match ($node) { $($tt)* }) }; + + (match ($node:expr) { + $( $( $path:ident )::+ ($it:pat) => $res:expr, )* + _ => $catch_all:expr $(,)? + }) => {{ + $( if let Some($it) = $($path::)+cast($node.clone()) { $res } else )* + { $catch_all } + }}; +} + +fn main() { + match_ast! { + match$0 Some(1) { + Some(x) => x, + } + } +} + "#, + ); + } + #[test] fn nested_if_else() { check(