Skip to main content

ide_assists/handlers/
flip_comma.rs

1use syntax::{
2    AstNode, Direction, NodeOrToken, SyntaxKind, SyntaxToken, T,
3    algo::non_trivia_sibling,
4    ast::{self, syntax_factory::SyntaxFactory},
5};
6
7use crate::{AssistContext, AssistId, Assists};
8
9// Assist: flip_comma
10//
11// Flips two comma-separated items.
12//
13// ```
14// fn main() {
15//     ((1, 2),$0 (3, 4));
16// }
17// ```
18// ->
19// ```
20// fn main() {
21//     ((3, 4), (1, 2));
22// }
23// ```
24pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
25    let comma = ctx.find_token_syntax_at_offset(T![,])?;
26    let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?;
27    let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?;
28
29    // Don't apply a "flip" in case of a last comma
30    // that typically comes before punctuation
31    if next.kind().is_punct() {
32        return None;
33    }
34
35    // Don't apply a "flip" inside the macro call
36    // since macro input are just mere tokens
37    if comma.parent_ancestors().any(|it| it.kind() == SyntaxKind::MACRO_CALL) {
38        return None;
39    }
40
41    let target = comma.text_range();
42    acc.add(AssistId::refactor_rewrite("flip_comma"), "Flip comma", target, |builder| {
43        let parent = comma.parent().unwrap();
44        let editor = builder.make_editor(&parent);
45
46        if let Some(parent) = ast::TokenTree::cast(parent) {
47            // An attribute. It often contains a path followed by a
48            // token tree (e.g. `align(2)`), so we have to be smarter.
49            let new_tree = flip_tree(parent.clone(), comma, editor.make());
50            editor.replace(parent.syntax(), new_tree.syntax());
51        } else {
52            editor.replace(prev.clone(), next.clone());
53            editor.replace(next.clone(), prev.clone());
54        }
55
56        builder.add_file_edits(ctx.vfs_file_id(), editor);
57    })
58}
59
60fn flip_tree(tree: ast::TokenTree, comma: SyntaxToken, make: &SyntaxFactory) -> ast::TokenTree {
61    let mut tree_iter = tree.token_trees_and_tokens();
62    let before: Vec<_> =
63        tree_iter.by_ref().take_while(|it| it.as_token() != Some(&comma)).collect();
64    let after: Vec<_> = tree_iter.collect();
65
66    let not_ws = |element: &NodeOrToken<_, SyntaxToken>| match element {
67        NodeOrToken::Token(token) => token.kind() != SyntaxKind::WHITESPACE,
68        NodeOrToken::Node(_) => true,
69    };
70
71    let is_comma = |element: &NodeOrToken<_, SyntaxToken>| match element {
72        NodeOrToken::Token(token) => token.kind() == T![,],
73        NodeOrToken::Node(_) => false,
74    };
75
76    let prev_start_untrimmed = match before.iter().rposition(is_comma) {
77        Some(pos) => pos + 1,
78        None => 1,
79    };
80    let prev_end = 1 + before.iter().rposition(not_ws).unwrap();
81    let prev_start = prev_start_untrimmed
82        + before[prev_start_untrimmed..prev_end].iter().position(not_ws).unwrap();
83
84    let next_start = after.iter().position(not_ws).unwrap();
85    let next_end_untrimmed = match after.iter().position(is_comma) {
86        Some(pos) => pos,
87        None => after.len() - 1,
88    };
89    let next_end = 1 + after[..next_end_untrimmed].iter().rposition(not_ws).unwrap();
90
91    let result = [
92        &before[1..prev_start],
93        &after[next_start..next_end],
94        &before[prev_end..],
95        &[NodeOrToken::Token(comma)],
96        &after[..next_start],
97        &before[prev_start..prev_end],
98        &after[next_end..after.len() - 1],
99    ]
100    .concat();
101    make.token_tree(tree.left_delimiter_token().unwrap().kind(), result)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
109
110    #[test]
111    fn flip_comma_works_for_function_parameters() {
112        check_assist(
113            flip_comma,
114            r#"fn foo(x: i32,$0 y: Result<(), ()>) {}"#,
115            r#"fn foo(y: Result<(), ()>, x: i32) {}"#,
116        )
117    }
118
119    #[test]
120    fn flip_comma_target() {
121        check_assist_target(flip_comma, r#"fn foo(x: i32,$0 y: Result<(), ()>) {}"#, ",")
122    }
123
124    #[test]
125    fn flip_comma_before_punct() {
126        // See https://github.com/rust-lang/rust-analyzer/issues/1619
127        // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct
128        // declaration body.
129        check_assist_not_applicable(flip_comma, "pub enum Test { A,$0 }");
130        check_assist_not_applicable(flip_comma, "pub struct Test { foo: usize,$0 }");
131    }
132
133    #[test]
134    fn flip_comma_works() {
135        check_assist(
136            flip_comma,
137            r#"fn main() {((1, 2),$0 (3, 4));}"#,
138            r#"fn main() {((3, 4), (1, 2));}"#,
139        )
140    }
141
142    #[test]
143    fn flip_comma_not_applicable_for_macro_input() {
144        // "Flip comma" assist shouldn't be applicable inside the macro call
145        // See https://github.com/rust-lang/rust-analyzer/issues/7693
146        check_assist_not_applicable(flip_comma, r#"bar!(a,$0 b)"#);
147    }
148
149    #[test]
150    fn flip_comma_attribute() {
151        check_assist(
152            flip_comma,
153            r#"#[repr(align(2),$0 C)] struct Foo;"#,
154            r#"#[repr(C, align(2))] struct Foo;"#,
155        );
156        check_assist(
157            flip_comma,
158            r#"#[foo(bar, baz(1 + 1),$0 qux, other)] struct Foo;"#,
159            r#"#[foo(bar, qux, baz(1 + 1), other)] struct Foo;"#,
160        );
161    }
162
163    #[test]
164    fn flip_comma_attribute_incomplete() {
165        check_assist_not_applicable(flip_comma, r#"#[repr(align(2),$0)] struct Foo;"#);
166    }
167}