feat: highlighting of related return values while the cursor is on any match, if, or match arm arrow (=>)

This commit is contained in:
roifewu 2025-04-09 11:29:05 +08:00
parent c6633fa1f2
commit f87220e22a
6 changed files with 775 additions and 9 deletions

View file

@ -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<Vec<NavigationTarget>> {
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!(),
_ => {},
}
}
"#,
);
}
}

View file

@ -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<EditionedFileId, FxHashSet<HighlightedRange>>;
// 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<EditionedFileId, Vec<HighlightedRange>> {
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<ast::Expr>, 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<SyntaxToken>,
body: ast::Expr,
) -> Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>> {
) -> Option<HighlightMap> {
let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = FxHashMap::default();
let mut push_to_highlights = |file_id, range| {
@ -411,7 +528,7 @@ pub(crate) fn highlight_break_points(
loop_token: Option<SyntaxToken>,
label: Option<ast::Label>,
expr: ast::Expr,
) -> Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>> {
) -> Option<HighlightMap> {
let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = 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<SyntaxToken>,
body: Option<ast::Expr>,
) -> Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>> {
) -> Option<HighlightMap> {
let mut highlights: FxHashMap<EditionedFileId, FxHashSet<_>> = 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<EditionedFileId, FxHashSet<HighlightedRange>>,
new: Option<FxHashMap<EditionedFileId, FxHashSet<HighlightedRange>>>,
) {
fn merge_map(res: &mut HighlightMap, new: Option<HighlightMap>) {
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(

View file

@ -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
"#]],
);
}
}

View file

@ -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(),
}
}

View file

@ -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`

View file

@ -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": {