diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs index 64444ee3aa58..6dc049715da5 100644 --- a/crates/ra_assists/src/doc_tests/generated.rs +++ b/crates/ra_assists/src/doc_tests/generated.rs @@ -606,6 +606,21 @@ impl Walrus { ) } +#[test] +fn doctest_reorder_fields() { + check( + "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 doctest_replace_if_let_with_match() { check( 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 000000000000..692dd1315e28 --- /dev/null +++ b/crates/ra_assists/src/handlers/reorder_fields.rs @@ -0,0 +1,230 @@ +use std::collections::HashMap; + +use itertools::Itertools; + +use hir::{Adt, ModuleDef, PathResolution, Semantics, Struct}; +use ra_ide_db::RootDatabase; +use ra_syntax::{ + algo, ast, + ast::{Name, Path, RecordLit, RecordPat}, + AstNode, SyntaxKind, SyntaxNode, +}; + +use crate::{ + assist_ctx::{Assist, AssistCtx}, + AssistId, +}; +use ra_syntax::ast::{Expr, NameRef}; + +// 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(ctx: AssistCtx) -> Option { + reorder::(ctx.clone()).or_else(|| reorder::(ctx)) +} + +fn reorder(ctx: AssistCtx) -> Option { + let record = ctx.find_node_at_offset::()?; + let path = record.syntax().children().find_map(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; + } + + ctx.add_assist(AssistId("reorder_fields"), "Reorder record fields", |edit| { + for (old, new) in fields.iter().zip(&sorted_fields) { + algo::diff(old, new).into_text_edit(edit.text_edit_builder()); + } + edit.target(record.syntax().text_range()) + }) +} + +fn get_fields_kind(node: &SyntaxNode) -> Vec { + use 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 { + use SyntaxKind::*; + match node.kind() { + RECORD_FIELD => { + if let Some(name) = node.children().find_map(NameRef::cast) { + return name.to_string(); + } + node.children().find_map(Expr::cast).map(|expr| expr.to_string()).unwrap_or_default() + } + BIND_PAT | RECORD_FIELD_PAT => { + node.children().find_map(Name::cast).map(|n| n.to_string()).unwrap_or_default() + } + _ => String::new(), + } +} + +fn get_fields(record: &SyntaxNode) -> Vec { + 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 { + 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 5ba5254fd042..a00136da1c14 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 b0d71eb3ded7..e1b08d48fb9e 100644 --- a/crates/ra_hir_def/src/body/lower.rs +++ b/crates/ra_hir_def/src/body/lower.rs @@ -689,7 +689,6 @@ impl ExprCollector<'_> { Pat::Missing } } - // FIXME: implement ast::Pat::BoxPat(_) | ast::Pat::RangePat(_) | ast::Pat::MacroPat(_) => Pat::Missing, }; diff --git a/docs/user/assists.md b/docs/user/assists.md index 754131f6fec1..75404e289000 100644 --- a/docs/user/assists.md +++ b/docs/user/assists.md @@ -582,6 +582,21 @@ impl Walrus { } ``` +## `reorder_fields` + +Reorder the fields of record literals and record patterns in the same order as in +the definition. + +```rust +// BEFORE +struct Foo {foo: i32, bar: i32}; +const test: Foo = ┃Foo {bar: 0, foo: 1} + +// AFTER +struct Foo {foo: i32, bar: i32}; +const test: Foo = Foo {foo: 1, bar: 0} +``` + ## `replace_if_let_with_match` Replaces `if let` with an else branch with a `match` expression.