Skip to content

feat: Add a "Unmerge match arm" assist to split or-patterns inside match expressions #13145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions crates/ide-assists/src/handlers/unmerge_match_arm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
use syntax::{
algo::neighbor,
ast::{self, edit::IndentLevel, make, AstNode},
ted::{self, Position},
Direction, SyntaxKind, T,
};

use crate::{AssistContext, AssistId, AssistKind, Assists};

// Assist: unmerge_match_arm
//
// Splits the current match with a `|` pattern into two arms with identical bodies.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move(..) $0| Action::Stop => foo(),
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move(..) => foo(),
// Action::Stop => foo(),
// }
// }
// ```
pub(crate) fn unmerge_match_arm(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let pipe_token = ctx.find_token_syntax_at_offset(T![|])?;
let or_pat = ast::OrPat::cast(pipe_token.parent()?)?.clone_for_update();
let match_arm = ast::MatchArm::cast(or_pat.syntax().parent()?)?;
let match_arm_body = match_arm.expr()?;

// We don't need to check for leading pipe because it is directly under `MatchArm`
// without `OrPat`.

let new_parent = match_arm.syntax().parent()?;
let old_parent_range = new_parent.text_range();

acc.add(
AssistId("unmerge_match_arm", AssistKind::RefactorRewrite),
"Unmerge match arm",
pipe_token.text_range(),
|edit| {
let pats_after = pipe_token
.siblings_with_tokens(Direction::Next)
.filter_map(|it| ast::Pat::cast(it.into_node()?));
// FIXME: We should add a leading pipe if the original arm has one.
let new_match_arm = make::match_arm(
pats_after,
match_arm.guard().and_then(|guard| guard.condition()),
match_arm_body,
)
.clone_for_update();

let mut pipe_index = pipe_token.index();
if pipe_token
.prev_sibling_or_token()
.map_or(false, |it| it.kind() == SyntaxKind::WHITESPACE)
{
pipe_index -= 1;
}
or_pat.syntax().splice_children(
pipe_index..or_pat.syntax().children_with_tokens().count(),
Vec::new(),
);

let mut insert_after_old_arm = Vec::new();

// A comma can be:
// - After the arm. In this case we always want to insert a comma after the newly
// inserted arm.
// - Missing after the arm, with no arms after. In this case we want to insert a
// comma before the newly inserted arm. It can not be necessary if there arm
// body is a block, but we don't bother to check that.
// - Missing after the arm with arms after, if the arm body is a block. In this case
// we don't want to insert a comma at all.
let has_comma_after =
std::iter::successors(match_arm.syntax().last_child_or_token(), |it| {
it.prev_sibling_or_token()
})
.map(|it| it.kind())
.skip_while(|it| it.is_trivia())
.next()
== Some(T![,]);
let has_arms_after = neighbor(&match_arm, Direction::Next).is_some();
if !has_comma_after && !has_arms_after {
insert_after_old_arm.push(make::token(T![,]).into());
}

let indent = IndentLevel::from_node(match_arm.syntax());
insert_after_old_arm.push(make::tokens::whitespace(&format!("\n{indent}")).into());

insert_after_old_arm.push(new_match_arm.syntax().clone().into());

ted::insert_all_raw(Position::after(match_arm.syntax()), insert_after_old_arm);

if has_comma_after {
ted::insert_raw(
Position::last_child_of(new_match_arm.syntax()),
make::token(T![,]),
);
}

edit.replace(old_parent_range, new_parent.to_string());
},
)
}

#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};

use super::*;

#[test]
fn unmerge_match_arm_single_pipe() {
check_assist(
unmerge_match_arm,
r#"
#[derive(Debug)]
enum X { A, B, C }

fn main() {
let x = X::A;
let y = match x {
X::A $0| X::B => { 1i32 }
X::C => { 2i32 }
};
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C }

fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32 }
X::B => { 1i32 }
X::C => { 2i32 }
};
}
"#,
);
}

#[test]
fn unmerge_match_arm_guard() {
check_assist(
unmerge_match_arm,
r#"
#[derive(Debug)]
enum X { A, B, C }

fn main() {
let x = X::A;
let y = match x {
X::A $0| X::B if true => { 1i32 }
_ => { 2i32 }
};
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C }

fn main() {
let x = X::A;
let y = match x {
X::A if true => { 1i32 }
X::B if true => { 1i32 }
_ => { 2i32 }
};
}
"#,
);
}

#[test]
fn unmerge_match_arm_leading_pipe() {
check_assist_not_applicable(
unmerge_match_arm,
r#"

fn main() {
let y = match 0 {
|$0 0 => { 1i32 }
1 => { 2i32 }
};
}
"#,
);
}

#[test]
fn unmerge_match_arm_multiple_pipes() {
check_assist(
unmerge_match_arm,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }

fn main() {
let x = X::A;
let y = match x {
X::A | X::B |$0 X::C | X::D => 1i32,
X::E => 2i32,
};
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }

fn main() {
let x = X::A;
let y = match x {
X::A | X::B => 1i32,
X::C | X::D => 1i32,
X::E => 2i32,
};
}
"#,
);
}

#[test]
fn unmerge_match_arm_inserts_comma_if_required() {
check_assist(
unmerge_match_arm,
r#"
#[derive(Debug)]
enum X { A, B }

fn main() {
let x = X::A;
let y = match x {
X::A $0| X::B => 1i32
};
}
"#,
r#"
#[derive(Debug)]
enum X { A, B }

fn main() {
let x = X::A;
let y = match x {
X::A => 1i32,
X::B => 1i32
};
}
"#,
);
}

#[test]
fn unmerge_match_arm_inserts_comma_if_had_after() {
check_assist(
unmerge_match_arm,
r#"
#[derive(Debug)]
enum X { A, B }

fn main() {
let x = X::A;
match x {
X::A $0| X::B => {},
}
}
"#,
r#"
#[derive(Debug)]
enum X { A, B }

fn main() {
let x = X::A;
match x {
X::A => {},
X::B => {},
}
}
"#,
);
}
}
2 changes: 2 additions & 0 deletions crates/ide-assists/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ mod handlers {
mod replace_string_with_char;
mod replace_turbofish_with_explicit_type;
mod split_import;
mod unmerge_match_arm;
mod sort_items;
mod toggle_ignore;
mod unmerge_use;
Expand Down Expand Up @@ -278,6 +279,7 @@ mod handlers {
sort_items::sort_items,
split_import::split_import,
toggle_ignore::toggle_ignore,
unmerge_match_arm::unmerge_match_arm,
unmerge_use::unmerge_use,
unnecessary_async::unnecessary_async,
unwrap_block::unwrap_block,
Expand Down
26 changes: 26 additions & 0 deletions crates/ide-assists/src/tests/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,32 @@ fn arithmetics {
)
}

#[test]
fn doctest_unmerge_match_arm() {
check_doc_test(
"unmerge_match_arm",
r#####"
enum Action { Move { distance: u32 }, Stop }

fn handle(action: Action) {
match action {
Action::Move(..) $0| Action::Stop => foo(),
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }

fn handle(action: Action) {
match action {
Action::Move(..) => foo(),
Action::Stop => foo(),
}
}
"#####,
)
}

#[test]
fn doctest_unmerge_use() {
check_doc_test(
Expand Down