diff --git a/crates/ra_assists/src/handlers/reorder_fields.rs b/crates/ra_assists/src/handlers/reorder_fields.rs new file mode 100644 index 0000000000..c17c1288cd --- /dev/null +++ b/crates/ra_assists/src/handlers/reorder_fields.rs @@ -0,0 +1,207 @@ +use std::collections::HashMap; + +use itertools::Itertools; + +use hir::{Adt, ModuleDef, PathResolution, Semantics, Struct}; +use ra_ide_db::RootDatabase; +use ra_syntax::ast::{Name, Pat}; +use ra_syntax::{ + ast, + ast::{Path, RecordField, RecordLit, RecordPat}, + AstNode, +}; + +use crate::{ + assist_ctx::{Assist, AssistCtx}, + AssistId, +}; + +pub(crate) fn reorder_fields(ctx: AssistCtx) -> Option { + reorder_struct(ctx.clone()).or_else(|| reorder_struct_pat(ctx)) +} + +fn reorder_struct(ctx: AssistCtx) -> Option { + let record: RecordLit = ctx.find_node_at_offset()?; + reorder(ctx, &record, &record.path()?, field_name) +} + +fn field_name(r: &RecordField) -> String { + r.name_ref() + .map(|name| name.syntax().text()) + .unwrap_or_else(|| r.expr().unwrap().syntax().text()) + .to_string() +} + +fn reorder_struct_pat(ctx: AssistCtx) -> Option { + let record: RecordPat = ctx.find_node_at_offset()?; + reorder(ctx, &record, &record.path()?, field_pat_name) +} + +fn field_pat_name(field: &Pat) -> String { + field.syntax().children().find_map(Name::cast).map(|n| n.to_string()).unwrap_or_default() +} + +fn reorder( + ctx: AssistCtx, + record: &R, + path: &Path, + field_name: fn(&F) -> String, +) -> Option { + let ranks = compute_fields_ranks(path, &ctx)?; + let fields: Vec = get_fields(record); + let sorted_fields: Vec = + sort_by_rank(&fields, |f| *ranks.get(&field_name(f)).unwrap_or(&usize::max_value())); + + if sorted_fields == fields { + return None; + } + + ctx.add_assist(AssistId("reorder_fields"), "Reorder record fields", |edit| { + for (old, new) in fields.into_iter().zip(sorted_fields) { + edit.replace_ast(old, new); + } + edit.target(record.syntax().text_range()) + }) +} + +fn get_fields(record: &R) -> Vec { + record.syntax().children().flat_map(|n1| n1.children()).filter_map(|n3| F::cast(n3)).collect() +} + +fn sort_by_rank(fields: &[F], get_rank: impl FnMut(&F) -> usize) -> Vec { + fields.iter().cloned().sorted_by_key(get_rank).collect() +} + +fn struct_definition(path: &ast::Path, sema: &Semantics) -> Option { + match sema.resolve_path(path) { + Some(PathResolution::Def(ModuleDef::Adt(Adt::Struct(s)))) => Some(s), + _ => None, + } +} + +fn compute_fields_ranks(path: &Path, ctx: &AssistCtx) -> Option> { + 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::helpers::{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", + } + } + } + "#, + ) + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 5ba5254fd0..a00136da1c 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -129,6 +129,7 @@ mod handlers { mod replace_unwrap_with_match; mod split_import; mod add_from_impl_for_enum; + mod reorder_fields; pub(crate) fn all() -> &'static [AssistHandler] { &[ @@ -170,6 +171,7 @@ mod handlers { // These are manually sorted for better priorities add_missing_impl_members::add_missing_impl_members, add_missing_impl_members::add_missing_default_members, + reorder_fields::reorder_fields, ] } } diff --git a/crates/ra_hir_def/src/body/lower.rs b/crates/ra_hir_def/src/body/lower.rs index b0d71eb3de..80492b733b 100644 --- a/crates/ra_hir_def/src/body/lower.rs +++ b/crates/ra_hir_def/src/body/lower.rs @@ -689,9 +689,10 @@ impl ExprCollector<'_> { Pat::Missing } } - // FIXME: implement ast::Pat::BoxPat(_) | ast::Pat::RangePat(_) | ast::Pat::MacroPat(_) => Pat::Missing, + // FIXME: implement + ast::Pat::RecordFieldPat(_) => Pat::Missing, }; let ptr = AstPtr::new(&pat); self.alloc_pat(pattern, Either::Left(ptr)) diff --git a/crates/ra_syntax/src/ast/generated/nodes.rs b/crates/ra_syntax/src/ast/generated/nodes.rs index 20f6630467..79b2256227 100644 --- a/crates/ra_syntax/src/ast/generated/nodes.rs +++ b/crates/ra_syntax/src/ast/generated/nodes.rs @@ -3256,6 +3256,7 @@ pub enum Pat { RangePat(RangePat), LiteralPat(LiteralPat), MacroPat(MacroPat), + RecordFieldPat(RecordFieldPat), } impl From for Pat { fn from(node: OrPat) -> Pat { Pat::OrPat(node) } @@ -3302,12 +3303,15 @@ impl From for Pat { impl From for Pat { fn from(node: MacroPat) -> Pat { Pat::MacroPat(node) } } +impl From for Pat { + fn from(node: RecordFieldPat) -> Pat { Pat::RecordFieldPat(node) } +} impl AstNode for Pat { fn can_cast(kind: SyntaxKind) -> bool { match kind { OR_PAT | PAREN_PAT | REF_PAT | BOX_PAT | BIND_PAT | PLACEHOLDER_PAT | DOT_DOT_PAT | PATH_PAT | RECORD_PAT | TUPLE_STRUCT_PAT | TUPLE_PAT | SLICE_PAT | RANGE_PAT - | LITERAL_PAT | MACRO_PAT => true, + | LITERAL_PAT | MACRO_PAT | RECORD_FIELD_PAT => true, _ => false, } } @@ -3328,6 +3332,7 @@ impl AstNode for Pat { RANGE_PAT => Pat::RangePat(RangePat { syntax }), LITERAL_PAT => Pat::LiteralPat(LiteralPat { syntax }), MACRO_PAT => Pat::MacroPat(MacroPat { syntax }), + RECORD_FIELD_PAT => Pat::RecordFieldPat(RecordFieldPat { syntax }), _ => return None, }; Some(res) @@ -3349,6 +3354,7 @@ impl AstNode for Pat { Pat::RangePat(it) => &it.syntax, Pat::LiteralPat(it) => &it.syntax, Pat::MacroPat(it) => &it.syntax, + Pat::RecordFieldPat(it) => &it.syntax, } } } diff --git a/xtask/src/ast_src.rs b/xtask/src/ast_src.rs index bb97b13fe5..eb5c3abf75 100644 --- a/xtask/src/ast_src.rs +++ b/xtask/src/ast_src.rs @@ -741,6 +741,7 @@ pub(crate) const AST_SRC: AstSrc = AstSrc { RangePat, LiteralPat, MacroPat, + RecordFieldPat, } enum RecordInnerPat {