Skip to main content

ide_assists/handlers/
unmerge_match_arm.rs

1use syntax::{
2    Direction, SyntaxKind, T,
3    ast::{self, AstNode, edit::IndentLevel},
4    syntax_editor::{Element, Position},
5};
6
7use crate::{AssistContext, AssistId, Assists};
8
9// Assist: unmerge_match_arm
10//
11// Splits the current match with a `|` pattern into two arms with identical bodies.
12//
13// ```
14// enum Action { Move { distance: u32 }, Stop }
15//
16// fn handle(action: Action) {
17//     match action {
18//         Action::Move(..) $0| Action::Stop => foo(),
19//     }
20// }
21// ```
22// ->
23// ```
24// enum Action { Move { distance: u32 }, Stop }
25//
26// fn handle(action: Action) {
27//     match action {
28//         Action::Move(..) => foo(),
29//         Action::Stop => foo(),
30//     }
31// }
32// ```
33pub(crate) fn unmerge_match_arm(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
34    let pipe_token = ctx.find_token_syntax_at_offset(T![|])?;
35    let or_pat = ast::OrPat::cast(pipe_token.parent()?)?;
36    if or_pat.leading_pipe().is_some_and(|it| it == pipe_token) {
37        return None;
38    }
39    let match_arm = ast::MatchArm::cast(or_pat.syntax().parent()?)?;
40    let match_arm_body = match_arm.expr()?;
41    let pats_after = pipe_token
42        .siblings_with_tokens(Direction::Next)
43        .filter_map(|it| ast::Pat::cast(it.into_node()?))
44        .collect::<Vec<_>>();
45
46    // We don't need to check for leading pipe because it is directly under `MatchArm`
47    // without `OrPat`.
48
49    let new_parent = match_arm.syntax().parent()?;
50    if pats_after.is_empty() {
51        return None;
52    }
53
54    acc.add(
55        AssistId::refactor_rewrite("unmerge_match_arm"),
56        "Unmerge match arm",
57        pipe_token.text_range(),
58        |edit| {
59            let editor = edit.make_editor(&new_parent);
60            let make = editor.make();
61            // It is guaranteed that `pats_after` has at least one element
62            let new_pat = if pats_after.len() == 1 {
63                pats_after[0].clone()
64            } else {
65                make.or_pat(pats_after, or_pat.leading_pipe().is_some()).into()
66            };
67            let new_match_arm = make.match_arm(new_pat, match_arm.guard(), match_arm_body);
68            let mut pipe_index = pipe_token.index();
69            if pipe_token
70                .prev_sibling_or_token()
71                .is_some_and(|it| it.kind() == SyntaxKind::WHITESPACE)
72            {
73                pipe_index -= 1;
74            }
75            for child in or_pat
76                .syntax()
77                .children_with_tokens()
78                .skip_while(|child| child.index() < pipe_index)
79            {
80                editor.delete(child.syntax_element());
81            }
82
83            let mut insert_after_old_arm = Vec::new();
84
85            // A comma can be:
86            //  - After the arm. In this case we always want to insert a comma after the newly
87            //    inserted arm.
88            //  - Missing after the arm, with no arms after. In this case we want to insert a
89            //    comma before the newly inserted arm. It can not be necessary if there arm
90            //    body is a block, but we don't bother to check that.
91            //  - Missing after the arm with arms after, if the arm body is a block. In this case
92            //    we don't want to insert a comma at all.
93            let has_comma_after = match_arm.comma_token().is_some();
94            if !has_comma_after && !match_arm.expr().unwrap().is_block_like() {
95                insert_after_old_arm.push(make.token(T![,]).into());
96            }
97
98            let indent = IndentLevel::from_node(match_arm.syntax());
99            insert_after_old_arm.push(make.whitespace(&format!("\n{indent}")).into());
100
101            insert_after_old_arm.push(new_match_arm.syntax().clone().into());
102
103            editor.insert_all(Position::after(match_arm.syntax()), insert_after_old_arm);
104            edit.add_file_edits(ctx.vfs_file_id(), editor);
105        },
106    )
107}
108
109#[cfg(test)]
110mod tests {
111    use crate::tests::{check_assist, check_assist_not_applicable};
112
113    use super::*;
114
115    #[test]
116    fn unmerge_match_arm_single_pipe() {
117        check_assist(
118            unmerge_match_arm,
119            r#"
120#[derive(Debug)]
121enum X { A, B, C }
122
123fn main() {
124    let x = X::A;
125    let y = match x {
126        X::A $0| X::B => { 1i32 }
127        X::C => { 2i32 }
128    };
129}
130"#,
131            r#"
132#[derive(Debug)]
133enum X { A, B, C }
134
135fn main() {
136    let x = X::A;
137    let y = match x {
138        X::A => { 1i32 }
139        X::B => { 1i32 }
140        X::C => { 2i32 }
141    };
142}
143"#,
144        );
145    }
146
147    #[test]
148    fn unmerge_match_arm_guard() {
149        check_assist(
150            unmerge_match_arm,
151            r#"
152#[derive(Debug)]
153enum X { A, B, C }
154
155fn main() {
156    let x = X::A;
157    let y = match x {
158        X::A $0| X::B if true => { 1i32 }
159        _ => { 2i32 }
160    };
161}
162"#,
163            r#"
164#[derive(Debug)]
165enum X { A, B, C }
166
167fn main() {
168    let x = X::A;
169    let y = match x {
170        X::A if true => { 1i32 }
171        X::B if true => { 1i32 }
172        _ => { 2i32 }
173    };
174}
175"#,
176        );
177    }
178
179    #[test]
180    fn unmerge_match_arm_leading_pipe() {
181        check_assist_not_applicable(
182            unmerge_match_arm,
183            r#"
184
185fn main() {
186    let y = match 0 {
187        |$0 0 => { 1i32 }
188        1 => { 2i32 }
189    };
190}
191"#,
192        );
193    }
194
195    #[test]
196    fn unmerge_match_arm_trailing_pipe() {
197        check_assist_not_applicable(
198            unmerge_match_arm,
199            r#"
200fn main() {
201    let y = match 0 {
202        0 |$0 => { 1i32 }
203        1 => { 2i32 }
204    };
205}
206"#,
207        );
208    }
209
210    #[test]
211    fn unmerge_match_arm_multiple_pipes() {
212        check_assist(
213            unmerge_match_arm,
214            r#"
215#[derive(Debug)]
216enum X { A, B, C, D, E }
217
218fn main() {
219    let x = X::A;
220    let y = match x {
221        X::A | X::B |$0 X::C | X::D => 1i32,
222        X::E => 2i32,
223    };
224}
225"#,
226            r#"
227#[derive(Debug)]
228enum X { A, B, C, D, E }
229
230fn main() {
231    let x = X::A;
232    let y = match x {
233        X::A | X::B => 1i32,
234        X::C | X::D => 1i32,
235        X::E => 2i32,
236    };
237}
238"#,
239        );
240    }
241
242    #[test]
243    fn unmerge_match_arm_inserts_comma_if_required() {
244        check_assist(
245            unmerge_match_arm,
246            r#"
247#[derive(Debug)]
248enum X { A, B }
249
250fn main() {
251    let x = X::A;
252    let y = match x {
253        X::A $0| X::B => 1i32
254    };
255}
256"#,
257            r#"
258#[derive(Debug)]
259enum X { A, B }
260
261fn main() {
262    let x = X::A;
263    let y = match x {
264        X::A => 1i32,
265        X::B => 1i32,
266    };
267}
268"#,
269        );
270    }
271
272    #[test]
273    fn unmerge_match_arm_inserts_comma_if_had_after() {
274        check_assist(
275            unmerge_match_arm,
276            r#"
277#[derive(Debug)]
278enum X { A, B }
279
280fn main() {
281    let x = X::A;
282    match x {
283        X::A $0| X::B => {}
284    }
285}
286"#,
287            r#"
288#[derive(Debug)]
289enum X { A, B }
290
291fn main() {
292    let x = X::A;
293    match x {
294        X::A => {}
295        X::B => {}
296    }
297}
298"#,
299        );
300    }
301}