From 5f3693783940bb6aa4b200a41625de0cc4672728 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 | 291 ++++++++++++++++++ crates/ide-assists/src/lib.rs | 2 + crates/ide-assists/src/tests/generated.rs | 26 ++ 3 files changed, 319 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 0000000000..a72ceae995 --- /dev/null +++ b/crates/ide-assists/src/handlers/extract_impl_items.rs @@ -0,0 +1,291 @@ +use crate::assist_context::{AssistContext, Assists}; +use ide_db::assists::AssistId; +use syntax::{ + AstNode, Direction, SyntaxElement, SyntaxKind, SyntaxToken, TextRange, + ast::{self, edit_in_place::Indent, make}, + syntax_editor::{Element, Position, SyntaxEditor}, +}; + +// 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 first = items.first()?; + let last = items.last()?; + let skip_items = impl_.assoc_item_list()?.assoc_items().position(|it| it == *first)?; + + let target = + TextRange::new(first.syntax().text_range().start(), 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_subtree(); + let mut tedit = SyntaxEditor::new(new_impl.syntax().clone()); + + if let Some(prev) = first.syntax().prev_sibling_or_token() + && prev.kind() == SyntaxKind::WHITESPACE + { + edit.delete(prev); + } + edit.delete_all(first.syntax().syntax_element()..=last.syntax().syntax_element()); + let _ = exclude_delete(&mut tedit, &new_impl, skip_items, items.len()); + + edit.insert_all( + Position::after(impl_.syntax()), + vec![ + make::tokens::whitespace(&format!("\n\n{indent}")).into(), + tedit.finish().new_root().clone().into(), + ], + ); + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn exclude_delete( + tedit: &mut SyntaxEditor, + new_impl: &ast::Impl, + skip_items: usize, + len: usize, +) -> Option<()> { + let assoc_item_list = new_impl.assoc_item_list()?; + let mut extracted = assoc_item_list.assoc_items().skip(skip_items).take(len); + let first = extracted.next()?; + let last = extracted.last().unwrap_or(first.clone()); + + let l_curly = assoc_item_list.l_curly_token()?; + let r_curly = assoc_item_list.r_curly_token()?; + + if let Some(start) = next_skip_ws(l_curly, Direction::Next) + && let Some(end) = first.syntax().prev_sibling_or_token() + { + tedit.delete_all(start..=end); + } + + if let Some(end) = next_skip_ws(r_curly, Direction::Prev) + && let Some(start) = last.syntax().next_sibling_or_token() + { + tedit.delete_all(start..=end); + } + + Some(()) +} + +fn next_skip_ws(token: SyntaxToken, dir: Direction) -> Option { + token.siblings_with_tokens(dir).skip(1).find(|it| it.kind() != SyntaxKind::WHITESPACE) +} + +fn selection_items(impl_: &ast::Impl, selection: TextRange) -> Option> { + let items = impl_ + .assoc_item_list()? + .assoc_items() + .skip_while(|item| item.syntax().text_range().end() < selection.start()) + .take_while(|item| item.syntax().text_range().start() <= selection.end()) + .collect::>(); + + let any_item_range = items.first()?.syntax().text_range(); + if any_item_range.contains_range(selection) && any_item_range != selection { + return None; + } + 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 comment + + /// some docs + + /// some docs + fn $0extract_1() {} + + // some comment2 + + 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() {} + + // some comment2 + + 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 4682c04732..d0bf4fd123 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 91348be97e..b73e5c3afc 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -1127,6 +1127,32 @@ 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(