ide_assists/handlers/
flip_comma.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
use syntax::{
    algo::non_trivia_sibling,
    ast::{self, syntax_factory::SyntaxFactory},
    syntax_editor::{Element, SyntaxMapping},
    AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxToken, T,
};

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

// Assist: flip_comma
//
// Flips two comma-separated items.
//
// ```
// fn main() {
//     ((1, 2),$0 (3, 4));
// }
// ```
// ->
// ```
// fn main() {
//     ((3, 4), (1, 2));
// }
// ```
pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
    let comma = ctx.find_token_syntax_at_offset(T![,])?;
    let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?;
    let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?;

    // Don't apply a "flip" in case of a last comma
    // that typically comes before punctuation
    if next.kind().is_punct() {
        return None;
    }

    // Don't apply a "flip" inside the macro call
    // since macro input are just mere tokens
    if comma.parent_ancestors().any(|it| it.kind() == SyntaxKind::MACRO_CALL) {
        return None;
    }

    let prev = match prev {
        SyntaxElement::Node(node) => node.syntax_element(),
        _ => prev,
    };
    let next = match next {
        SyntaxElement::Node(node) => node.syntax_element(),
        _ => next,
    };

    acc.add(
        AssistId("flip_comma", AssistKind::RefactorRewrite),
        "Flip comma",
        comma.text_range(),
        |builder| {
            let parent = comma.parent().unwrap();
            let mut editor = builder.make_editor(&parent);

            if let Some(parent) = ast::TokenTree::cast(parent) {
                // An attribute. It often contains a path followed by a
                // token tree (e.g. `align(2)`), so we have to be smarter.
                let (new_tree, mapping) = flip_tree(parent.clone(), comma);
                editor.replace(parent.syntax(), new_tree.syntax());
                editor.add_mappings(mapping);
            } else {
                editor.replace(prev.clone(), next.clone());
                editor.replace(next.clone(), prev.clone());
            }

            builder.add_file_edits(ctx.file_id(), editor);
        },
    )
}

fn flip_tree(tree: ast::TokenTree, comma: SyntaxToken) -> (ast::TokenTree, SyntaxMapping) {
    let mut tree_iter = tree.token_trees_and_tokens();
    let before: Vec<_> =
        tree_iter.by_ref().take_while(|it| it.as_token() != Some(&comma)).collect();
    let after: Vec<_> = tree_iter.collect();

    let not_ws = |element: &NodeOrToken<_, SyntaxToken>| match element {
        NodeOrToken::Token(token) => token.kind() != SyntaxKind::WHITESPACE,
        NodeOrToken::Node(_) => true,
    };

    let is_comma = |element: &NodeOrToken<_, SyntaxToken>| match element {
        NodeOrToken::Token(token) => token.kind() == T![,],
        NodeOrToken::Node(_) => false,
    };

    let prev_start_untrimmed = match before.iter().rposition(is_comma) {
        Some(pos) => pos + 1,
        None => 1,
    };
    let prev_end = 1 + before.iter().rposition(not_ws).unwrap();
    let prev_start = prev_start_untrimmed
        + before[prev_start_untrimmed..prev_end].iter().position(not_ws).unwrap();

    let next_start = after.iter().position(not_ws).unwrap();
    let next_end_untrimmed = match after.iter().position(is_comma) {
        Some(pos) => pos,
        None => after.len() - 1,
    };
    let next_end = 1 + after[..next_end_untrimmed].iter().rposition(not_ws).unwrap();

    let result = [
        &before[1..prev_start],
        &after[next_start..next_end],
        &before[prev_end..],
        &[NodeOrToken::Token(comma)],
        &after[..next_start],
        &before[prev_start..prev_end],
        &after[next_end..after.len() - 1],
    ]
    .concat();

    let make = SyntaxFactory::new();
    let new_token_tree = make.token_tree(tree.left_delimiter_token().unwrap().kind(), result);
    (new_token_tree, make.finish_with_mappings())
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};

    #[test]
    fn flip_comma_works_for_function_parameters() {
        check_assist(
            flip_comma,
            r#"fn foo(x: i32,$0 y: Result<(), ()>) {}"#,
            r#"fn foo(y: Result<(), ()>, x: i32) {}"#,
        )
    }

    #[test]
    fn flip_comma_target() {
        check_assist_target(flip_comma, r#"fn foo(x: i32,$0 y: Result<(), ()>) {}"#, ",")
    }

    #[test]
    fn flip_comma_before_punct() {
        // See https://github.com/rust-lang/rust-analyzer/issues/1619
        // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct
        // declaration body.
        check_assist_not_applicable(flip_comma, "pub enum Test { A,$0 }");
        check_assist_not_applicable(flip_comma, "pub struct Test { foo: usize,$0 }");
    }

    #[test]
    fn flip_comma_works() {
        check_assist(
            flip_comma,
            r#"fn main() {((1, 2),$0 (3, 4));}"#,
            r#"fn main() {((3, 4), (1, 2));}"#,
        )
    }

    #[test]
    fn flip_comma_not_applicable_for_macro_input() {
        // "Flip comma" assist shouldn't be applicable inside the macro call
        // See https://github.com/rust-lang/rust-analyzer/issues/7693
        check_assist_not_applicable(flip_comma, r#"bar!(a,$0 b)"#);
    }

    #[test]
    fn flip_comma_attribute() {
        check_assist(
            flip_comma,
            r#"#[repr(align(2),$0 C)] struct Foo;"#,
            r#"#[repr(C, align(2))] struct Foo;"#,
        );
        check_assist(
            flip_comma,
            r#"#[foo(bar, baz(1 + 1),$0 qux, other)] struct Foo;"#,
            r#"#[foo(bar, qux, baz(1 + 1), other)] struct Foo;"#,
        );
    }

    #[test]
    fn flip_comma_attribute_incomplete() {
        check_assist_not_applicable(flip_comma, r#"#[repr(align(2),$0)] struct Foo;"#);
    }
}