Skip to main content

ide_assists/handlers/
reorder_fields.rs

1use either::Either;
2use ide_db::FxHashMap;
3use itertools::Itertools;
4use syntax::{AstNode, SmolStr, SyntaxElement, ToSmolStr, ast, syntax_editor::SyntaxEditor};
5
6use crate::{AssistContext, AssistId, Assists};
7
8// Assist: reorder_fields
9//
10// Reorder the fields of record literals and record patterns in the same order as in
11// the definition.
12//
13// ```
14// struct Foo {foo: i32, bar: i32};
15// const test: Foo = $0Foo {bar: 0, foo: 1}
16// ```
17// ->
18// ```
19// struct Foo {foo: i32, bar: i32};
20// const test: Foo = Foo {foo: 1, bar: 0}
21// ```
22pub(crate) fn reorder_fields(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
23    let path = ctx.find_node_at_offset::<ast::Path>()?;
24    let record =
25        path.syntax().parent().and_then(<Either<ast::RecordExpr, ast::RecordPat>>::cast)?;
26
27    let parent_node = match ctx.covering_element() {
28        SyntaxElement::Node(n) => n,
29        SyntaxElement::Token(t) => t.parent()?,
30    };
31
32    let ranks = compute_fields_ranks(&path, ctx)?;
33    let get_rank_of_field = |of: Option<SmolStr>| {
34        *ranks.get(of.unwrap_or_default().trim_start_matches("r#")).unwrap_or(&usize::MAX)
35    };
36
37    let field_list = match &record {
38        Either::Left(it) => Either::Left(it.record_expr_field_list()?),
39        Either::Right(it) => Either::Right(it.record_pat_field_list()?),
40    };
41    let fields = match field_list {
42        Either::Left(it) => Either::Left((
43            it.fields()
44                .sorted_unstable_by_key(|field| {
45                    get_rank_of_field(field.field_name().map(|it| it.to_smolstr()))
46                })
47                .collect::<Vec<_>>(),
48            it,
49        )),
50        Either::Right(it) => Either::Right((
51            it.fields()
52                .sorted_unstable_by_key(|field| {
53                    get_rank_of_field(field.field_name().map(|it| it.to_smolstr()))
54                })
55                .collect::<Vec<_>>(),
56            it,
57        )),
58    };
59
60    let is_sorted = fields.as_ref().either(
61        |(sorted, field_list)| field_list.fields().zip(sorted).all(|(a, b)| a == *b),
62        |(sorted, field_list)| field_list.fields().zip(sorted).all(|(a, b)| a == *b),
63    );
64    if is_sorted {
65        cov_mark::hit!(reorder_sorted_fields);
66        return None;
67    }
68    let target = record.as_ref().either(AstNode::syntax, AstNode::syntax).text_range();
69    acc.add(
70        AssistId::refactor_rewrite("reorder_fields"),
71        "Reorder record fields",
72        target,
73        |builder| {
74            let editor = builder.make_editor(&parent_node);
75
76            match fields {
77                Either::Left((sorted, field_list)) => replace(&editor, field_list.fields(), sorted),
78                Either::Right((sorted, field_list)) => {
79                    replace(&editor, field_list.fields(), sorted)
80                }
81            }
82
83            builder.add_file_edits(ctx.vfs_file_id(), editor);
84        },
85    )
86}
87
88fn replace<T: AstNode + PartialEq>(
89    editor: &SyntaxEditor,
90    fields: impl Iterator<Item = T>,
91    sorted_fields: impl IntoIterator<Item = T>,
92) {
93    fields
94        .zip(sorted_fields)
95        .for_each(|(field, sorted_field)| editor.replace(field.syntax(), sorted_field.syntax()));
96}
97
98fn compute_fields_ranks(
99    path: &ast::Path,
100    ctx: &AssistContext<'_, '_>,
101) -> Option<FxHashMap<String, usize>> {
102    let strukt = match ctx.sema.resolve_path(path) {
103        Some(hir::PathResolution::Def(hir::ModuleDef::Adt(hir::Adt::Struct(it)))) => it,
104        _ => return None,
105    };
106
107    let res = strukt
108        .fields(ctx.db())
109        .into_iter()
110        .enumerate()
111        .map(|(idx, field)| (field.name(ctx.db()).as_str().to_owned(), idx))
112        .collect();
113
114    Some(res)
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::tests::{check_assist, check_assist_not_applicable};
120
121    use super::*;
122
123    #[test]
124    fn reorder_sorted_fields() {
125        cov_mark::check!(reorder_sorted_fields);
126        check_assist_not_applicable(
127            reorder_fields,
128            r#"
129struct Foo { foo: i32, bar: i32 }
130const test: Foo = $0Foo { foo: 0, bar: 0 };
131"#,
132        )
133    }
134
135    #[test]
136    fn trivial_empty_fields() {
137        check_assist_not_applicable(
138            reorder_fields,
139            r#"
140struct Foo {}
141const test: Foo = $0Foo {};
142"#,
143        )
144    }
145
146    #[test]
147    fn reorder_struct_fields() {
148        check_assist(
149            reorder_fields,
150            r#"
151struct Foo { foo: i32, bar: i32 }
152const test: Foo = $0Foo { bar: 0, foo: 1 };
153"#,
154            r#"
155struct Foo { foo: i32, bar: i32 }
156const test: Foo = Foo { foo: 1, bar: 0 };
157"#,
158        )
159    }
160    #[test]
161    fn reorder_struct_pattern() {
162        check_assist(
163            reorder_fields,
164            r#"
165struct Foo { foo: i64, bar: i64, baz: i64 }
166
167fn f(f: Foo) -> {
168    match f {
169        $0Foo { baz: 0, ref mut bar, .. } => (),
170        _ => ()
171    }
172}
173"#,
174            r#"
175struct Foo { foo: i64, bar: i64, baz: i64 }
176
177fn f(f: Foo) -> {
178    match f {
179        Foo { ref mut bar, baz: 0, .. } => (),
180        _ => ()
181    }
182}
183"#,
184        )
185    }
186
187    #[test]
188    fn reorder_with_extra_field() {
189        check_assist(
190            reorder_fields,
191            r#"
192struct Foo { foo: String, bar: String }
193
194impl Foo {
195    fn new() -> Foo {
196        let foo = String::new();
197        $0Foo {
198            bar: foo.clone(),
199            extra: "Extra field",
200            foo,
201        }
202    }
203}
204"#,
205            r#"
206struct Foo { foo: String, bar: String }
207
208impl Foo {
209    fn new() -> Foo {
210        let foo = String::new();
211        Foo {
212            foo,
213            bar: foo.clone(),
214            extra: "Extra field",
215        }
216    }
217}
218"#,
219        )
220    }
221}