use itertools::Itertools; use rustc_hash::FxHashMap; use hir::{Adt, ModuleDef, PathResolution, Semantics, Struct}; use ra_ide_db::RootDatabase; use ra_syntax::{algo, ast, match_ast, AstNode, SyntaxKind, SyntaxKind::*, SyntaxNode}; use crate::{AssistContext, AssistId, AssistKind, Assists}; // Assist: reorder_fields // // Reorder the fields of record literals and record patterns in the same order as in // the definition. // // ``` // struct Foo {foo: i32, bar: i32}; // const test: Foo = <|>Foo {bar: 0, foo: 1} // ``` // -> // ``` // struct Foo {foo: i32, bar: i32}; // const test: Foo = Foo {foo: 1, bar: 0} // ``` // pub(crate) fn reorder_fields(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { reorder::<ast::RecordLit>(acc, ctx).or_else(|| reorder::<ast::RecordPat>(acc, ctx)) } fn reorder<R: AstNode>(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { let record = ctx.find_node_at_offset::<R>()?; let path = record.syntax().children().find_map(ast::Path::cast)?; let ranks = compute_fields_ranks(&path, &ctx)?; let fields = get_fields(&record.syntax()); let sorted_fields = sorted_by_rank(&fields, |node| { *ranks.get(&get_field_name(node)).unwrap_or(&usize::max_value()) }); if sorted_fields == fields { return None; } let target = record.syntax().text_range(); acc.add( AssistId("reorder_fields", AssistKind::RefactorRewrite), "Reorder record fields", target, |edit| { for (old, new) in fields.iter().zip(&sorted_fields) { algo::diff(old, new).into_text_edit(edit.text_edit_builder()); } }, ) } fn get_fields_kind(node: &SyntaxNode) -> Vec<SyntaxKind> { match node.kind() { RECORD_LIT => vec![RECORD_FIELD], RECORD_PAT => vec![RECORD_FIELD_PAT, BIND_PAT], _ => vec![], } } fn get_field_name(node: &SyntaxNode) -> String { let res = match_ast! { match node { ast::RecordField(field) => field.field_name().map(|it| it.to_string()), ast::RecordFieldPat(field) => field.field_name().map(|it| it.to_string()), _ => None, } }; res.unwrap_or_default() } fn get_fields(record: &SyntaxNode) -> Vec<SyntaxNode> { let kinds = get_fields_kind(record); record.children().flat_map(|n| n.children()).filter(|n| kinds.contains(&n.kind())).collect() } fn sorted_by_rank( fields: &[SyntaxNode], get_rank: impl Fn(&SyntaxNode) -> usize, ) -> Vec<SyntaxNode> { fields.iter().cloned().sorted_by_key(get_rank).collect() } fn struct_definition(path: &ast::Path, sema: &Semantics<RootDatabase>) -> Option<Struct> { match sema.resolve_path(path) { Some(PathResolution::Def(ModuleDef::Adt(Adt::Struct(s)))) => Some(s), _ => None, } } fn compute_fields_ranks(path: &ast::Path, ctx: &AssistContext) -> Option<FxHashMap<String, usize>> { Some( struct_definition(path, &ctx.sema)? .fields(ctx.db()) .iter() .enumerate() .map(|(idx, field)| (field.name(ctx.db()).to_string(), idx)) .collect(), ) } #[cfg(test)] mod tests { use crate::tests::{check_assist, check_assist_not_applicable}; use super::*; #[test] fn not_applicable_if_sorted() { check_assist_not_applicable( reorder_fields, r#" struct Foo { foo: i32, bar: i32, } const test: Foo = <|>Foo { foo: 0, bar: 0 }; "#, ) } #[test] fn trivial_empty_fields() { check_assist_not_applicable( reorder_fields, r#" struct Foo {}; const test: Foo = <|>Foo {} "#, ) } #[test] fn reorder_struct_fields() { check_assist( reorder_fields, r#" struct Foo {foo: i32, bar: i32}; const test: Foo = <|>Foo {bar: 0, foo: 1} "#, r#" struct Foo {foo: i32, bar: i32}; const test: Foo = Foo {foo: 1, bar: 0} "#, ) } #[test] fn reorder_struct_pattern() { check_assist( reorder_fields, r#" struct Foo { foo: i64, bar: i64, baz: i64 } fn f(f: Foo) -> { match f { <|>Foo { baz: 0, ref mut bar, .. } => (), _ => () } } "#, r#" struct Foo { foo: i64, bar: i64, baz: i64 } fn f(f: Foo) -> { match f { Foo { ref mut bar, baz: 0, .. } => (), _ => () } } "#, ) } #[test] fn reorder_with_extra_field() { check_assist( reorder_fields, r#" struct Foo { foo: String, bar: String, } impl Foo { fn new() -> Foo { let foo = String::new(); <|>Foo { bar: foo.clone(), extra: "Extra field", foo, } } } "#, r#" struct Foo { foo: String, bar: String, } impl Foo { fn new() -> Foo { let foo = String::new(); Foo { foo, bar: foo.clone(), extra: "Extra field", } } } "#, ) } }