From e1a3849b66e0374fcb222234ae80e9d005db832b Mon Sep 17 00:00:00 2001 From: A4-Tacks Date: Sat, 2 Aug 2025 18:59:33 +0800 Subject: [PATCH] Add ide-assist: extract_impl_items Extract selected impl items into new impl. ```rust struct Foo; impl Foo { fn foo() {} $0fn bar() {} fn baz() {}$0 } ``` -> ```rust struct Foo; impl Foo { fn foo() {} } impl Foo { fn bar() {} fn baz() {} } ``` --- .../src/handlers/extract_impl_items.rs | 264 ++++++++++++++++++ crates/ide-assists/src/lib.rs | 2 + crates/ide-assists/src/tests/generated.rs | 27 ++ 3 files changed, 293 insertions(+) create mode 100644 crates/ide-assists/src/handlers/extract_impl_items.rs diff --git a/crates/ide-assists/src/handlers/extract_impl_items.rs b/crates/ide-assists/src/handlers/extract_impl_items.rs new file mode 100644 index 000000000000..0df2acd3f923 --- /dev/null +++ b/crates/ide-assists/src/handlers/extract_impl_items.rs @@ -0,0 +1,264 @@ +use crate::assist_context::{AssistContext, Assists}; +use ide_db::assists::AssistId; +use syntax::{ + AstNode, SyntaxKind, TextRange, + ast::{self, edit_in_place::Indent, make}, + syntax_editor::Position, + ted, +}; + +// Assist: extract_impl_items +// +// Extract selected impl items into new impl. +// +// ``` +// struct Foo; +// impl Foo { +// fn foo() {} +// $0fn bar() {} +// fn baz() {}$0 +// } +// ``` +// -> +// ``` +// struct Foo; +// impl Foo { +// fn foo() {} +// } +// +// impl Foo { +// fn bar() {} +// +// fn baz() {} +// } +// ``` +pub(crate) fn extract_impl_items(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + if ctx.has_empty_selection() { + return None; + } + let item = ctx.find_node_at_trimmed_offset::()?; + let impl_ = ast::Impl::cast(item.syntax().parent()?.parent()?)?; + let indent = impl_.indent_level(); + let selection = ctx.selection_trimmed(); + let items = selection_items(&impl_, selection)?; + + let target = TextRange::new( + items.first()?.syntax().text_range().start(), + items.last()?.syntax().text_range().end(), + ); + acc.add( + AssistId::refactor_extract("extract_impl_items"), + "Extract items into new impl block", + target, + |builder| { + let mut edit = builder.make_editor(impl_.syntax()); + let new_impl = impl_.clone_for_update(); + + for origin_item in &items { + if let Some(token) = origin_item.syntax().prev_sibling_or_token() + && token.kind() == SyntaxKind::WHITESPACE + { + edit.delete(token); + } + edit.delete(origin_item.syntax()); + } + + if let Some(assoc_item_list) = new_impl.assoc_item_list() { + let new_item_list = make::assoc_item_list(None); + ted::replace(assoc_item_list.syntax(), new_item_list.clone_for_update().syntax()); + } + + let assoc_item_list = new_impl.get_or_create_assoc_item_list(); + + for item in items { + assoc_item_list.add_item(item.clone_for_update()); + } + + edit.insert_all( + Position::after(impl_.syntax()), + vec![ + make::tokens::whitespace(&format!("\n\n{indent}")).into(), + new_impl.syntax().clone().into(), + ], + ); + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn selection_items(impl_: &ast::Impl, selection: TextRange) -> Option> { + let items = impl_ + .assoc_item_list()? + .assoc_items() + .filter(|item| { + let item_range = item.syntax().text_range(); + selection == item_range + || selection.intersect(item_range).is_some_and(|range| !range.is_empty()) + && !item_range.contains_range(selection) + }) + .collect::>(); + Some(items) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{check_assist, check_assist_not_applicable}; + + #[test] + fn test_extract_impl_items() { + check_assist( + extract_impl_items, + r#" +struct Foo; +impl Foo { + fn other_1() {} + + /// some docs + + /// some docs + fn $0extract_1() {} + + // some comment + + fn extract_2() {} + + const EXTRACT_3: u32 = 2;$0 + + fn other_2() {} +} + "#, + r#" +struct Foo; +impl Foo { + fn other_1() {} + + // some comment + + fn other_2() {} +} + +impl Foo { + /// some docs + + /// some docs + fn extract_1() {} + + fn extract_2() {} + + const EXTRACT_3: u32 = 2; +} + "#, + ); + } + + #[test] + fn test_extract_impl_items_with_generics() { + check_assist( + extract_impl_items, + r#" +struct Foo(T); +impl Foo +where + T: Clone, +{ + fn other_1() {} + + /// some docs + fn $0extract_1() {} + + // some comment + fn extract_2() {} + + const EXTRACT_3: u32 = 2;$0 + + fn other_2() {} +} + "#, + r#" +struct Foo(T); +impl Foo +where + T: Clone, +{ + fn other_1() {} + + fn other_2() {} +} + +impl Foo +where + T: Clone, +{ + /// some docs + fn extract_1() {} + + // some comment + fn extract_2() {} + + const EXTRACT_3: u32 = 2; +} + "#, + ); + } + + #[test] + fn test_extract_impl_items_with_indent() { + check_assist( + extract_impl_items, + r#" +mod foo { + mod bar { + struct Foo; + impl Foo { + fn other_1() { + todo!() + } + + $0fn extract_1() { + todo!() + }$0 + } + } +} + "#, + r#" +mod foo { + mod bar { + struct Foo; + impl Foo { + fn other_1() { + todo!() + } + } + + impl Foo { + fn extract_1() { + todo!() + } + } + } +} + "#, + ); + } + #[test] + fn test_extract_impl_items_in_body_not_application() { + check_assist_not_applicable( + extract_impl_items, + r#" +struct Foo; +impl Foo { + fn other_1() { + todo!() + } + + fn other_1() { + $0()$0 + } +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index cde0d875e0d6..a023eefad8ce 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -144,6 +144,7 @@ mod handlers { mod expand_rest_pattern; mod extract_expressions_from_format_string; mod extract_function; + mod extract_impl_items; mod extract_module; mod extract_struct_from_enum_variant; mod extract_type_alias; @@ -278,6 +279,7 @@ mod handlers { expand_glob_import::expand_glob_import, expand_glob_import::expand_glob_reexport, expand_rest_pattern::expand_rest_pattern, + extract_impl_items::extract_impl_items, extract_expressions_from_format_string::extract_expressions_from_format_string, extract_struct_from_enum_variant::extract_struct_from_enum_variant, extract_type_alias::extract_type_alias, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index fc1c6928ff31..e20fa2286e39 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -1127,6 +1127,33 @@ fn $0fun_name(n: i32) { ) } +#[test] +fn doctest_extract_impl_items() { + check_doc_test( + "extract_impl_items", + r#####" +struct Foo; +impl Foo { + fn foo() {} + $0fn bar() {} + fn baz() {}$0 +} +"#####, + r#####" +struct Foo; +impl Foo { + fn foo() {} +} + +impl Foo { + fn bar() {} + + fn baz() {} +} +"#####, + ) +} + #[test] fn doctest_extract_module() { check_doc_test(