Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/ide-assists/src/handlers/destructure_struct_binding.rs231
-rw-r--r--crates/ide-assists/src/handlers/destructure_tuple_binding.rs1
2 files changed, 198 insertions, 34 deletions
diff --git a/crates/ide-assists/src/handlers/destructure_struct_binding.rs b/crates/ide-assists/src/handlers/destructure_struct_binding.rs
index bb5d112210..4c4cee1d78 100644
--- a/crates/ide-assists/src/handlers/destructure_struct_binding.rs
+++ b/crates/ide-assists/src/handlers/destructure_struct_binding.rs
@@ -1,15 +1,19 @@
-use hir::HasVisibility;
+use hir::{HasVisibility, Semantics};
use ide_db::{
- FxHashMap, FxHashSet,
+ FxHashMap, FxHashSet, RootDatabase,
assists::AssistId,
defs::Definition,
helpers::mod_path_to_ast,
search::{FileReference, SearchScope},
};
use itertools::Itertools;
-use syntax::ast::{HasName, syntax_factory::SyntaxFactory};
use syntax::syntax_editor::SyntaxEditor;
use syntax::{AstNode, Edition, SmolStr, SyntaxNode, ToSmolStr, ast};
+use syntax::{
+ SyntaxToken,
+ ast::{HasName, edit::IndentLevel, syntax_factory::SyntaxFactory},
+ syntax_editor::Position,
+};
use crate::{
assist_context::{AssistContext, Assists, SourceChangeBuilder},
@@ -44,33 +48,90 @@ use crate::{
// }
// ```
pub(crate) fn destructure_struct_binding(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
- let ident_pat = ctx.find_node_at_offset::<ast::IdentPat>()?;
- let data = collect_data(ident_pat, ctx)?;
+ let target = ctx.find_node_at_offset::<Target>()?;
+ let data = collect_data(target, ctx)?;
acc.add(
AssistId::refactor_rewrite("destructure_struct_binding"),
"Destructure struct binding",
- data.ident_pat.syntax().text_range(),
+ data.target.syntax().text_range(),
|edit| destructure_struct_binding_impl(ctx, edit, &data),
);
Some(())
}
+enum Target {
+ IdentPat(ast::IdentPat),
+ SelfParam { param: ast::SelfParam, insert_after: SyntaxToken },
+}
+
+impl Target {
+ fn ty<'db>(&self, sema: &Semantics<'db, RootDatabase>) -> Option<hir::Type<'db>> {
+ match self {
+ Target::IdentPat(pat) => sema.type_of_binding_in_pat(pat),
+ Target::SelfParam { param, .. } => sema.type_of_self(param),
+ }
+ }
+
+ fn is_ref(&self) -> bool {
+ match self {
+ Target::IdentPat(ident_pat) => ident_pat.ref_token().is_some(),
+ Target::SelfParam { .. } => false,
+ }
+ }
+
+ fn is_mut(&self) -> bool {
+ match self {
+ Target::IdentPat(ident_pat) => ident_pat.mut_token().is_some(),
+ Target::SelfParam { param, .. } => {
+ param.mut_token().is_some() && param.amp_token().is_none()
+ }
+ }
+ }
+}
+
+impl HasName for Target {}
+
+impl AstNode for Target {
+ fn cast(node: SyntaxNode) -> Option<Self> {
+ if ast::IdentPat::can_cast(node.kind()) {
+ ast::IdentPat::cast(node).map(Self::IdentPat)
+ } else {
+ let param = ast::SelfParam::cast(node)?;
+ let param_list = param.syntax().parent().and_then(ast::ParamList::cast)?;
+ let block = param_list.syntax().parent()?.children().find_map(ast::BlockExpr::cast)?;
+ let insert_after = block.stmt_list()?.l_curly_token()?;
+ Some(Self::SelfParam { param, insert_after })
+ }
+ }
+
+ fn can_cast(kind: syntax::SyntaxKind) -> bool {
+ ast::IdentPat::can_cast(kind) || ast::SelfParam::can_cast(kind)
+ }
+
+ fn syntax(&self) -> &SyntaxNode {
+ match self {
+ Target::IdentPat(ident_pat) => ident_pat.syntax(),
+ Target::SelfParam { param, .. } => param.syntax(),
+ }
+ }
+}
+
fn destructure_struct_binding_impl(
ctx: &AssistContext<'_>,
builder: &mut SourceChangeBuilder,
data: &StructEditData,
) {
let field_names = generate_field_names(ctx, data);
- let mut editor = builder.make_editor(data.ident_pat.syntax());
+ let mut editor = builder.make_editor(data.target.syntax());
destructure_pat(ctx, &mut editor, data, &field_names);
update_usages(ctx, &mut editor, data, &field_names.into_iter().collect());
builder.add_file_edits(ctx.vfs_file_id(), editor);
}
struct StructEditData {
- ident_pat: ast::IdentPat,
+ target: Target,
name: ast::Name,
kind: hir::StructKind,
struct_def_path: hir::ModPath,
@@ -83,11 +144,44 @@ struct StructEditData {
edition: Edition,
}
-fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<StructEditData> {
- let ty = ctx.sema.type_of_binding_in_pat(&ident_pat)?;
+impl StructEditData {
+ fn apply_to_destruct(
+ &self,
+ new_pat: ast::Pat,
+ editor: &mut SyntaxEditor,
+ make: &SyntaxFactory,
+ ) {
+ match &self.target {
+ Target::IdentPat(pat) => {
+ // If the binding is nested inside a record, we need to wrap the new
+ // destructured pattern in a non-shorthand record field
+ if self.need_record_field_name {
+ let new_pat =
+ make.record_pat_field(make.name_ref(&self.name.to_string()), new_pat);
+ editor.replace(pat.syntax(), new_pat.syntax())
+ } else {
+ editor.replace(pat.syntax(), new_pat.syntax())
+ }
+ }
+ Target::SelfParam { insert_after, .. } => {
+ let indent = IndentLevel::from_token(insert_after) + 1;
+ let newline = make.whitespace(&format!("\n{indent}"));
+ let initializer = make.expr_path(make.ident_path("self"));
+ let let_stmt = make.let_stmt(new_pat, None, Some(initializer));
+ editor.insert_all(
+ Position::after(insert_after),
+ vec![newline.into(), let_stmt.syntax().clone().into()],
+ );
+ }
+ }
+ }
+}
+
+fn collect_data(target: Target, ctx: &AssistContext<'_>) -> Option<StructEditData> {
+ let ty = target.ty(&ctx.sema)?;
let hir::Adt::Struct(struct_type) = ty.strip_references().as_adt()? else { return None };
- let module = ctx.sema.scope(ident_pat.syntax())?.module();
+ let module = ctx.sema.scope(target.syntax())?.module();
let cfg = ctx.config.find_path_config(ctx.sema.is_nightly(module.krate(ctx.db())));
let struct_def = hir::ModuleDef::from(struct_type);
let kind = struct_type.kind(ctx.db());
@@ -116,15 +210,17 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
}
let is_ref = ty.is_reference();
- let need_record_field_name = ident_pat
+ let need_record_field_name = target
.syntax()
.parent()
.and_then(ast::RecordPatField::cast)
.is_some_and(|field| field.colon_token().is_none());
- let usages = ctx
- .sema
- .to_def(&ident_pat)
+ let def = match &target {
+ Target::IdentPat(pat) => ctx.sema.to_def(pat),
+ Target::SelfParam { param, .. } => ctx.sema.to_def(param),
+ };
+ let usages = def
.and_then(|def| {
Definition::Local(def)
.usages(&ctx.sema)
@@ -136,11 +232,11 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
})
.unwrap_or_default();
- let names_in_scope = get_names_in_scope(ctx, &ident_pat, &usages).unwrap_or_default();
+ let names_in_scope = get_names_in_scope(ctx, &target, &usages).unwrap_or_default();
Some(StructEditData {
- name: ident_pat.name()?,
- ident_pat,
+ name: target.name()?,
+ target,
kind,
struct_def_path,
usages,
@@ -155,7 +251,7 @@ fn collect_data(ident_pat: ast::IdentPat, ctx: &AssistContext<'_>) -> Option<Str
fn get_names_in_scope(
ctx: &AssistContext<'_>,
- ident_pat: &ast::IdentPat,
+ target: &Target,
usages: &[FileReference],
) -> Option<FxHashSet<SmolStr>> {
fn last_usage(usages: &[FileReference]) -> Option<SyntaxNode> {
@@ -165,7 +261,7 @@ fn get_names_in_scope(
// If available, find names visible to the last usage of the binding
// else, find names visible to the binding itself
let last_usage = last_usage(usages);
- let node = last_usage.as_ref().unwrap_or(ident_pat.syntax());
+ let node = last_usage.as_ref().unwrap_or(target.syntax());
let scope = ctx.sema.scope(node)?;
let mut names = FxHashSet::default();
@@ -183,12 +279,9 @@ fn destructure_pat(
data: &StructEditData,
field_names: &[(SmolStr, SmolStr)],
) {
- let ident_pat = &data.ident_pat;
- let name = &data.name;
-
let struct_path = mod_path_to_ast(&data.struct_def_path, data.edition);
- let is_ref = ident_pat.ref_token().is_some();
- let is_mut = ident_pat.mut_token().is_some();
+ let is_ref = data.target.is_ref();
+ let is_mut = data.target.is_mut();
let make = SyntaxFactory::with_mappings();
let new_pat = match data.kind {
@@ -221,16 +314,8 @@ fn destructure_pat(
hir::StructKind::Unit => make.path_pat(struct_path),
};
- // If the binding is nested inside a record, we need to wrap the new
- // destructured pattern in a non-shorthand record field
- let destructured_pat = if data.need_record_field_name {
- make.record_pat_field(make.name_ref(&name.to_string()), new_pat).syntax().clone()
- } else {
- new_pat.syntax().clone()
- };
-
+ data.apply_to_destruct(new_pat, editor, &make);
editor.add_mappings(make.finish_with_mappings());
- editor.replace(data.ident_pat.syntax(), destructured_pat);
}
fn generate_field_names(ctx: &AssistContext<'_>, data: &StructEditData) -> Vec<(SmolStr, SmolStr)> {
@@ -699,6 +784,84 @@ mod tests {
}
#[test]
+ fn mut_self_param() {
+ check_assist(
+ destructure_struct_binding,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(mut $0self) {
+ self.bar = 5;
+ }
+ }
+ "#,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(mut self) {
+ let Foo { mut bar, mut baz } = self;
+ bar = 5;
+ }
+ }
+ "#,
+ )
+ }
+
+ #[test]
+ fn ref_mut_self_param() {
+ check_assist(
+ destructure_struct_binding,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(&mut $0self) {
+ self.bar = 5;
+ }
+ }
+ "#,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(&mut self) {
+ let Foo { bar, baz } = self;
+ *bar = 5;
+ }
+ }
+ "#,
+ )
+ }
+
+ #[test]
+ fn ref_self_param() {
+ check_assist(
+ destructure_struct_binding,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(&$0self) -> &i32 {
+ &self.bar
+ }
+ }
+ "#,
+ r#"
+ struct Foo { bar: i32, baz: i32 }
+
+ impl Foo {
+ fn foo(&self) -> &i32 {
+ let Foo { bar, baz } = self;
+ bar
+ }
+ }
+ "#,
+ )
+ }
+
+ #[test]
fn ref_not_add_parenthesis_and_deref_record() {
check_assist(
destructure_struct_binding,
diff --git a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
index b8dc59f87d..d51a3a26a3 100644
--- a/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
+++ b/crates/ide-assists/src/handlers/destructure_tuple_binding.rs
@@ -164,6 +164,7 @@ enum RefType {
Mutable,
}
struct TupleData {
+ // FIXME: After removing ted, it may be possible to reuse destructure_struct_binding::Target
ident_pat: IdentPat,
ref_type: Option<RefType>,
field_names: Vec<String>,