diff --git a/crates/ide-assists/src/handlers/reorder_impl_items.rs b/crates/ide-assists/src/handlers/reorder_impl_items.rs new file mode 100644 index 0000000000..87bee6c121 --- /dev/null +++ b/crates/ide-assists/src/handlers/reorder_impl_items.rs @@ -0,0 +1,270 @@ +use hir::{PathResolution, Semantics}; +use ide_db::{FxHashMap, RootDatabase}; +use itertools::Itertools; +use syntax::{ + ast::{self, HasName}, + ted, AstNode, +}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: reorder_impl_items +// +// Reorder the items of an `impl Trait`. The items will be ordered +// in the same order as in the trait definition. +// +// ``` +// trait Foo { +// type A; +// const B: u8; +// fn c(); +// } +// +// struct Bar; +// $0impl Foo for Bar { +// const B: u8 = 17; +// fn c() {} +// type A = String; +// } +// ``` +// -> +// ``` +// trait Foo { +// type A; +// const B: u8; +// fn c(); +// } +// +// struct Bar; +// impl Foo for Bar { +// type A = String; +// const B: u8 = 17; +// fn c() {} +// } +// ``` +pub(crate) fn reorder_impl_items(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + let impl_ast = ctx.find_node_at_offset::()?; + let items = impl_ast.assoc_item_list()?; + let assoc_items = items.assoc_items().collect::>(); + + // If all items are either function or macro calls, then reorder_impl assist can be used + if assoc_items.iter().all(|i| matches!(i, ast::AssocItem::Fn(_) | ast::AssocItem::MacroCall(_))) + { + cov_mark::hit!(not_applicable_if_all_functions); + return None; + } + + let path = impl_ast + .trait_() + .and_then(|t| match t { + ast::Type::PathType(path) => Some(path), + _ => None, + })? + .path()?; + + let ranks = compute_item_ranks(&path, ctx)?; + let sorted: Vec<_> = assoc_items + .iter() + .cloned() + .sorted_by_key(|i| { + let name = match i { + ast::AssocItem::Const(c) => c.name(), + ast::AssocItem::Fn(f) => f.name(), + ast::AssocItem::TypeAlias(t) => t.name(), + ast::AssocItem::MacroCall(_) => None, + }; + + name.and_then(|n| ranks.get(&n.to_string()).copied()).unwrap_or(usize::max_value()) + }) + .collect(); + + // Don't edit already sorted methods: + if assoc_items == sorted { + cov_mark::hit!(not_applicable_if_sorted); + return None; + } + + let target = items.syntax().text_range(); + acc.add( + AssistId("reorder_impl_items", AssistKind::RefactorRewrite), + "Sort items by trait definition", + target, + |builder| { + let assoc_items = + assoc_items.into_iter().map(|item| builder.make_mut(item)).collect::>(); + assoc_items + .into_iter() + .zip(sorted) + .for_each(|(old, new)| ted::replace(old.syntax(), new.clone_for_update().syntax())); + }, + ) +} + +fn compute_item_ranks(path: &ast::Path, ctx: &AssistContext) -> Option> { + let td = trait_definition(path, &ctx.sema)?; + + Some( + td.items(ctx.db()) + .iter() + .flat_map(|i| i.name(ctx.db())) + .enumerate() + .map(|(idx, name)| (name.to_string(), idx)) + .collect(), + ) +} + +fn trait_definition(path: &ast::Path, sema: &Semantics) -> Option { + match sema.resolve_path(path)? { + PathResolution::Def(hir::ModuleDef::Trait(trait_)) => Some(trait_), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn not_applicable_if_sorted() { + cov_mark::check!(not_applicable_if_sorted); + check_assist_not_applicable( + reorder_impl_items, + r#" +trait Bar { + type T; + const C: (); + fn a() {} + fn z() {} + fn b() {} +} +struct Foo; +$0impl Bar for Foo { + type T = (); + const C: () = (); + fn a() {} + fn z() {} + fn b() {} +} + "#, + ) + } + + #[test] + fn not_applicable_if_all_functions() { + cov_mark::check!(not_applicable_if_all_functions); + check_assist_not_applicable( + reorder_impl_items, + r#" +trait Bar { + fn a() {} + fn z() {} + fn b() {} +} +struct Foo; +$0impl Bar for Foo { + fn a() {} + fn z() {} + fn b() {} +} + "#, + ) + } + + #[test] + fn not_applicable_if_empty() { + check_assist_not_applicable( + reorder_impl_items, + r#" +trait Bar {}; +struct Foo; +$0impl Bar for Foo {} + "#, + ) + } + + #[test] + fn reorder_impl_trait_items() { + check_assist( + reorder_impl_items, + r#" +trait Bar { + fn a() {} + type T0; + fn c() {} + const C1: (); + fn b() {} + type T1; + fn d() {} + const C0: (); +} + +struct Foo; +$0impl Bar for Foo { + type T1 = (); + fn d() {} + fn b() {} + fn c() {} + const C1: () = (); + fn a() {} + type T0 = (); + const C0: () = (); +} + "#, + r#" +trait Bar { + fn a() {} + type T0; + fn c() {} + const C1: (); + fn b() {} + type T1; + fn d() {} + const C0: (); +} + +struct Foo; +impl Bar for Foo { + fn a() {} + type T0 = (); + fn c() {} + const C1: () = (); + fn b() {} + type T1 = (); + fn d() {} + const C0: () = (); +} + "#, + ) + } + + #[test] + fn reorder_impl_trait_items_uneven_ident_lengths() { + check_assist( + reorder_impl_items, + r#" +trait Bar { + type Foo; + type Fooo; +} + +struct Foo; +impl Bar for Foo { + type Fooo = (); + type Foo = ();$0 +}"#, + r#" +trait Bar { + type Foo; + type Fooo; +} + +struct Foo; +impl Bar for Foo { + type Foo = (); + type Fooo = (); +}"#, + ) + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 93a9f3858f..7b2f733567 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -170,6 +170,7 @@ mod handlers { mod remove_unused_param; mod reorder_fields; mod reorder_impl; + mod reorder_impl_items; mod replace_try_expr_with_match; mod replace_derive_with_manual_impl; mod replace_if_let_with_match; @@ -257,6 +258,7 @@ mod handlers { remove_unused_param::remove_unused_param, reorder_fields::reorder_fields, reorder_impl::reorder_impl, + reorder_impl_items::reorder_impl_items, replace_try_expr_with_match::replace_try_expr_with_match, replace_derive_with_manual_impl::replace_derive_with_manual_impl, replace_if_let_with_match::replace_if_let_with_match, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 8a1e95d894..b629364b88 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -1771,6 +1771,41 @@ impl Foo for Bar { ) } +#[test] +fn doctest_reorder_impl_items() { + check_doc_test( + "reorder_impl_items", + r#####" +trait Foo { + type A; + const B: u8; + fn c(); +} + +struct Bar; +$0impl Foo for Bar { + const B: u8 = 17; + fn c() {} + type A = String; +} +"#####, + r#####" +trait Foo { + type A; + const B: u8; + fn c(); +} + +struct Bar; +impl Foo for Bar { + type A = String; + const B: u8 = 17; + fn c() {} +} +"#####, + ) +} + #[test] fn doctest_replace_char_with_string() { check_doc_test(