ide_assists/handlers/
unmerge_match_arm.rs

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