diff --git a/crates/ide-assists/src/handlers/convert_record_struct_to_unit_struct.rs b/crates/ide-assists/src/handlers/convert_record_struct_to_unit_struct.rs new file mode 100644 index 000000000000..348a4d99d678 --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_record_struct_to_unit_struct.rs @@ -0,0 +1,231 @@ +use either::Either; +use ide_db::{defs::Definition, search::FileReference}; +use syntax::{ + SyntaxKind, + ast::{self, AstNode}, + match_ast, +}; + +use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder}; + +// Assist: convert_record_struct_to_unit_struct +// +// Converts an empty record struct or enum variant into a unit form and updates +// usages accordingly. +// +// ``` +// struct Foo$0 {} +// +// impl Foo { +// fn new() -> Self { +// Foo {} +// } +// } +// ``` +// -> +// ``` +// struct Foo; +// +// impl Foo { +// fn new() -> Self { +// Foo +// } +// } +// ``` +pub(crate) fn convert_record_struct_to_unit_struct( + acc: &mut Assists, + ctx: &AssistContext<'_>, +) -> Option<()> { + let strukt_or_variant = ctx + .find_node_at_offset::() + .map(Either::Left) + .or_else(|| ctx.find_node_at_offset::().map(Either::Right))?; + + let field_list = strukt_or_variant.as_ref().either(|s| s.field_list(), |v| v.field_list())?; + let record_fields = match field_list { + ast::FieldList::RecordFieldList(list) => list, + _ => return None, + }; + + if record_fields.fields().next().is_some() { + return None; + } + + if ctx.offset() > record_fields.syntax().text_range().start() { + return None; + } + + let strukt_def = match &strukt_or_variant { + Either::Left(s) => Either::Left(ctx.sema.to_def(s)?), + Either::Right(v) => Either::Right(ctx.sema.to_def(v)?), + }; + + let target = strukt_or_variant.as_ref().either(|s| s.syntax(), |v| v.syntax()).text_range(); + + acc.add( + AssistId::refactor_rewrite("convert_record_struct_to_unit_struct"), + "Convert to unit struct", + target, + |edit| { + edit_struct_references(ctx, edit, strukt_def); + edit_struct_def(ctx, edit, &strukt_or_variant, &record_fields); + }, + ) +} + +fn edit_struct_def( + ctx: &AssistContext<'_>, + edit: &mut SourceChangeBuilder, + strukt: &Either, + record_fields: &ast::RecordFieldList, +) { + edit.edit_file(ctx.vfs_file_id()); + + let newline_before = record_fields + .l_curly_token() + .and_then(|tok| tok.prev_token()) + .filter(|tok| tok.kind() == SyntaxKind::WHITESPACE) + .map(|tok| { + let has_newline = tok.text().contains('\n'); + edit.delete(tok.text_range()); + has_newline + }) + .unwrap_or(false); + + match strukt { + Either::Left(_) => { + let replacement = if newline_before { "\n;" } else { ";" }; + edit.replace(record_fields.syntax().text_range(), replacement); + } + Either::Right(_) => { + edit.delete(record_fields.syntax().text_range()); + } + } +} + +fn edit_struct_references( + ctx: &AssistContext<'_>, + edit: &mut SourceChangeBuilder, + strukt: Either, +) { + let strukt_def = match strukt { + Either::Left(s) => Definition::Adt(hir::Adt::Struct(s)), + Either::Right(v) => Definition::Variant(v), + }; + + let usages = strukt_def.usages(&ctx.sema).include_self_refs().all(); + + for (file_id, refs) in usages { + edit.edit_file(file_id.file_id(ctx.db())); + for r in refs { + process_reference(ctx, r, edit); + } + } +} + +fn process_reference( + ctx: &AssistContext<'_>, + reference: FileReference, + edit: &mut SourceChangeBuilder, +) -> Option<()> { + let name_ref = reference.name.as_name_ref()?; + let path_segment = name_ref.syntax().parent().and_then(ast::PathSegment::cast)?; + let full_path = + path_segment.syntax().parent()?.ancestors().map_while(ast::Path::cast).last()?; + + if full_path.segment()?.name_ref()? != *name_ref { + return None; + } + + let parent = full_path.syntax().parent()?; + match_ast! { + match parent { + ast::RecordExpr(record_expr) => { + let file_range = ctx.sema.original_range_opt(record_expr.syntax())?; + let path = record_expr.path()?; + edit.replace(file_range.range, path.syntax().text().to_string()); + }, + ast::RecordPat(record_pat) => { + let file_range = ctx.sema.original_range_opt(record_pat.syntax())?; + let path = record_pat.path()?; + edit.replace(file_range.range, path.syntax().text().to_string()); + }, + _ => return None, + } + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::convert_record_struct_to_unit_struct; + + #[test] + fn not_applicable_non_empty_record_struct() { + check_assist_not_applicable( + convert_record_struct_to_unit_struct, + r#"struct Foo$0 { field: u32 }"#, + ); + } + + #[test] + fn convert_empty_struct() { + check_assist( + convert_record_struct_to_unit_struct, + r#" +struct Foo$0 {} + +impl Foo { + fn new() -> Self { + Foo {} + } + + fn take(self) { + let Foo {} = self; + } +} +"#, + r#" +struct Foo; + +impl Foo { + fn new() -> Self { + Foo + } + + fn take(self) { + let Foo = self; + } +} +"#, + ); + } + + #[test] + fn convert_record_variant() { + check_assist( + convert_record_struct_to_unit_struct, + r#" +enum E { + $0Foo {} +} + +fn make() -> E { + E::Foo {} +} +"#, + r#" +enum E { + Foo +} + +fn make() -> E { + E::Foo +} +"#, + ); + } +} diff --git a/crates/ide-assists/src/handlers/convert_unit_struct_to_record_struct.rs b/crates/ide-assists/src/handlers/convert_unit_struct_to_record_struct.rs new file mode 100644 index 000000000000..37de3be4f351 --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_unit_struct_to_record_struct.rs @@ -0,0 +1,266 @@ +use either::Either; +use ide_db::{defs::Definition, search::FileReference}; +use syntax::{ + SyntaxKind, + ast::{self, AstNode, HasGenericParams}, + match_ast, +}; + +use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder}; + +// Assist: convert_unit_struct_to_record_struct +// +// Converts a unit struct or enum variant into an empty record form and updates +// usages accordingly. +// +// ``` +// struct Foo$0; +// +// impl Foo { +// fn new() -> Self { +// Foo +// } +// } +// ``` +// -> +// ``` +// struct Foo {} +// +// impl Foo { +// fn new() -> Self { +// Foo {} +// } +// } +// ``` +pub(crate) fn convert_unit_struct_to_record_struct( + acc: &mut Assists, + ctx: &AssistContext<'_>, +) -> Option<()> { + let strukt_or_variant = ctx + .find_node_at_offset::() + .map(Either::Left) + .or_else(|| ctx.find_node_at_offset::().map(Either::Right))?; + + match &strukt_or_variant { + Either::Left(strukt) => { + let semicolon = strukt.semicolon_token()?; + if strukt.field_list().is_some() { + return None; + } + if ctx.offset() > semicolon.text_range().start() { + return None; + } + } + Either::Right(variant) => { + if variant.field_list().is_some() || variant.expr().is_some() { + return None; + } + } + } + + let strukt_def = match &strukt_or_variant { + Either::Left(s) => Either::Left(ctx.sema.to_def(s)?), + Either::Right(v) => Either::Right(ctx.sema.to_def(v)?), + }; + + let target = strukt_or_variant.as_ref().either(|s| s.syntax(), |v| v.syntax()).text_range(); + + acc.add( + AssistId::refactor_rewrite("convert_unit_struct_to_record_struct"), + "Convert to record struct", + target, + |edit| { + edit_struct_references(ctx, edit, strukt_def); + edit_struct_def(ctx, edit, &strukt_or_variant); + }, + ) +} + +fn edit_struct_def( + ctx: &AssistContext<'_>, + edit: &mut SourceChangeBuilder, + strukt: &Either, +) { + edit.edit_file(ctx.vfs_file_id()); + + match strukt { + Either::Left(strukt) => { + let semicolon = match strukt.semicolon_token() { + Some(it) => it, + None => return, + }; + + let ws_text = semicolon + .prev_token() + .filter(|tok| tok.kind() == SyntaxKind::WHITESPACE) + .map(|tok| { + let text = tok.text().to_owned(); + edit.delete(tok.text_range()); + text + }); + + let mut replacement = String::new(); + if let Some(ref text) = ws_text { + replacement.push_str(text); + } else if strukt.where_clause().is_some() { + replacement.push('\n'); + } else { + replacement.push(' '); + } + replacement.push_str("{}"); + edit.replace(semicolon.text_range(), replacement); + } + Either::Right(variant) => { + let insert_at = variant.syntax().text_range().end(); + edit.insert(insert_at, " {}"); + } + } +} + +fn edit_struct_references( + ctx: &AssistContext<'_>, + edit: &mut SourceChangeBuilder, + strukt: Either, +) { + let strukt_def = match strukt { + Either::Left(s) => Definition::Adt(hir::Adt::Struct(s)), + Either::Right(v) => Definition::Variant(v), + }; + + let usages = strukt_def.usages(&ctx.sema).include_self_refs().all(); + + for (file_id, refs) in usages { + edit.edit_file(file_id.file_id(ctx.db())); + for r in &refs { + process_reference(ctx, r, edit); + } + } +} + +fn process_reference( + ctx: &AssistContext<'_>, + reference: &FileReference, + edit: &mut SourceChangeBuilder, +) -> Option<()> { + let name_like = reference.name.clone().into_name_like()?; + match name_like { + ast::NameLike::NameRef(name_ref) => { + let full_path = name_ref.syntax().ancestors().find_map(ast::Path::cast)?; + let segment_name = full_path.segment()?.name_ref()?; + if segment_name.syntax().text_range() != name_ref.syntax().text_range() { + return None; + } + let parent = full_path.syntax().parent()?; + match_ast! { + match parent { + ast::PathExpr(path_expr) => { + let file_range = ctx.sema.original_range_opt(path_expr.syntax())?; + let path_text = full_path.syntax().text().to_string(); + edit.replace(file_range.range, format!("{path_text} {{}}")); + }, + ast::PathPat(path_pat) => { + let file_range = ctx.sema.original_range_opt(path_pat.syntax())?; + let path = path_pat.path()?; + edit.replace(file_range.range, format!("{path} {{}}")); + }, + _ => return None, + } + } + Some(()) + } + ast::NameLike::Name(name) => { + let ident_pat = name.syntax().ancestors().find_map(ast::IdentPat::cast)?; + let file_range = ctx.sema.original_range_opt(ident_pat.syntax())?; + let path_text = name.text().to_string(); + edit.replace(file_range.range, format!("{path_text} {{}}")); + Some(()) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::convert_unit_struct_to_record_struct; + + #[test] + fn not_applicable_tuple_struct() { + check_assist_not_applicable(convert_unit_struct_to_record_struct, r#"struct Foo$0(u32);"#); + } + + #[test] + fn convert_unit_struct() { + check_assist( + convert_unit_struct_to_record_struct, + r#" +struct Foo$0; + +impl Foo { + fn new() -> Foo { + Foo + } + + fn take(self) { + match self { + Foo => {} + } + } +} +"#, + r#" +struct Foo {} + +impl Foo { + fn new() -> Foo { + Foo {} + } + + fn take(self) { + match self { + Foo {} => {} + } + } +} +"#, + ); + } + + #[test] + fn convert_unit_variant() { + check_assist( + convert_unit_struct_to_record_struct, + r#" +enum E { + $0Foo, +} + +fn make() -> E { + E::Foo +} +"#, + r#" +enum E { + Foo {}, +} + +fn make() -> E { + E::Foo {} +} +"#, + ); + } + + #[test] + fn not_applicable_variant_with_discriminant() { + check_assist_not_applicable( + convert_unit_struct_to_record_struct, + r#" +enum E { + $0Foo = 1, +} +"#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 4b4aa9427955..755ae328774d 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -132,10 +132,12 @@ mod handlers { mod convert_named_struct_to_tuple_struct; mod convert_nested_function_to_closure; mod convert_range_for_to_while; + mod convert_record_struct_to_unit_struct; mod convert_to_guarded_return; mod convert_tuple_return_type_to_struct; mod convert_tuple_struct_to_named_struct; mod convert_two_arm_bool_match_to_matches_macro; + mod convert_unit_struct_to_record_struct; mod convert_while_to_loop; mod destructure_struct_binding; mod destructure_tuple_binding; @@ -270,9 +272,11 @@ mod handlers { convert_named_struct_to_tuple_struct::convert_named_struct_to_tuple_struct, convert_nested_function_to_closure::convert_nested_function_to_closure, convert_range_for_to_while::convert_range_for_to_while, + convert_record_struct_to_unit_struct::convert_record_struct_to_unit_struct, convert_to_guarded_return::convert_to_guarded_return, convert_tuple_return_type_to_struct::convert_tuple_return_type_to_struct, convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct, + convert_unit_struct_to_record_struct::convert_unit_struct_to_record_struct, convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro, convert_while_to_loop::convert_while_to_loop, destructure_struct_binding::destructure_struct_binding, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 160b31af0ae9..fd172c72e52a 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -754,6 +754,31 @@ fn foo() { ) } +#[test] +fn doctest_convert_record_struct_to_unit_struct() { + check_doc_test( + "convert_record_struct_to_unit_struct", + r#####" +struct Foo$0 {} + +impl Foo { + fn new() -> Self { + Foo {} + } +} +"#####, + r#####" +struct Foo; + +impl Foo { + fn new() -> Self { + Foo + } +} +"#####, + ) +} + #[test] fn doctest_convert_to_guarded_return() { check_doc_test( @@ -866,6 +891,31 @@ fn main() { ) } +#[test] +fn doctest_convert_unit_struct_to_record_struct() { + check_doc_test( + "convert_unit_struct_to_record_struct", + r#####" +struct Foo$0; + +impl Foo { + fn new() -> Self { + Foo + } +} +"#####, + r#####" +struct Foo {} + +impl Foo { + fn new() -> Self { + Foo {} + } +} +"#####, + ) +} + #[test] fn doctest_convert_while_to_loop() { check_doc_test(