Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #21817 from A4-Tacks/conv-struct-to-tuple-uses-self
fix: Fix overlap edit on record to tuple assist uses self
A4-Tacks 8 weeks ago
parent 3c90ed6 · parent 34839f6 · commit 76de1de
-rw-r--r--crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs298
-rw-r--r--crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs274
-rw-r--r--crates/ide-assists/src/utils.rs14
-rw-r--r--crates/syntax/src/algo.rs16
4 files changed, 452 insertions, 150 deletions
diff --git a/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs b/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
index e518c39dab..42fceb8533 100644
--- a/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
+++ b/crates/ide-assists/src/handlers/convert_named_struct_to_tuple_struct.rs
@@ -1,14 +1,18 @@
use either::Either;
use ide_db::{defs::Definition, search::FileReference};
-use itertools::Itertools;
use syntax::{
- SyntaxKind,
- ast::{self, AstNode, HasAttrs, HasGenericParams, HasVisibility},
+ NodeOrToken, SyntaxKind, SyntaxNode, T,
+ algo::next_non_trivia_token,
+ ast::{
+ self, AstNode, HasAttrs, HasGenericParams, HasVisibility, syntax_factory::SyntaxFactory,
+ },
match_ast,
- syntax_editor::{Position, SyntaxEditor},
+ syntax_editor::{Element, Position, SyntaxEditor},
};
-use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
+use crate::{
+ AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::cover_edit_range,
+};
// Assist: convert_named_struct_to_tuple_struct
//
@@ -81,17 +85,17 @@ pub(crate) fn convert_named_struct_to_tuple_struct(
AssistId::refactor_rewrite("convert_named_struct_to_tuple_struct"),
"Convert to tuple struct",
strukt_or_variant.syntax().text_range(),
- |edit| {
- edit_field_references(ctx, edit, record_fields.fields());
- edit_struct_references(ctx, edit, strukt_def);
- edit_struct_def(ctx, edit, &strukt_or_variant, record_fields);
+ |builder| {
+ edit_field_references(ctx, builder, record_fields.fields());
+ edit_struct_references(ctx, builder, strukt_def);
+ edit_struct_def(ctx, builder, &strukt_or_variant, record_fields);
},
)
}
fn edit_struct_def(
ctx: &AssistContext<'_>,
- edit: &mut SourceChangeBuilder,
+ builder: &mut SourceChangeBuilder,
strukt: &Either<ast::Struct, ast::Variant>,
record_fields: ast::RecordFieldList,
) {
@@ -108,24 +112,23 @@ fn edit_struct_def(
let field = ast::TupleField::cast(field_syntax)?;
Some(field)
});
- let tuple_fields = ast::make::tuple_field_list(tuple_fields);
- let record_fields_text_range = record_fields.syntax().text_range();
- edit.edit_file(ctx.vfs_file_id());
- edit.replace(record_fields_text_range, tuple_fields.syntax().text());
+ let make = SyntaxFactory::without_mappings();
+ let mut edit = builder.make_editor(strukt.syntax());
+
+ let tuple_fields = make.tuple_field_list(tuple_fields);
+ let mut elements = vec![tuple_fields.syntax().clone().into()];
if let Either::Left(strukt) = strukt {
if let Some(w) = strukt.where_clause() {
- let mut where_clause = w.to_string();
- if where_clause.ends_with(',') {
- where_clause.pop();
- }
- where_clause.push(';');
+ edit.delete(w.syntax());
- edit.delete(w.syntax().text_range());
- edit.insert(record_fields_text_range.end(), ast::make::tokens::single_newline().text());
- edit.insert(record_fields_text_range.end(), where_clause);
- edit.insert(record_fields_text_range.end(), ast::make::tokens::single_newline().text());
+ elements.extend([
+ make.whitespace("\n").into(),
+ remove_trailing_comma(w).into(),
+ make.token(T![;]).into(),
+ make.whitespace("\n").into(),
+ ]);
if let Some(tok) = strukt
.generic_param_list()
@@ -133,25 +136,28 @@ fn edit_struct_def(
.and_then(|tok| tok.next_token())
.filter(|tok| tok.kind() == SyntaxKind::WHITESPACE)
{
- edit.delete(tok.text_range());
+ edit.delete(tok);
}
} else {
- edit.insert(record_fields_text_range.end(), ";");
+ elements.push(make.token(T![;]).into());
}
}
+ edit.replace_with_many(record_fields.syntax(), elements);
if let Some(tok) = record_fields
.l_curly_token()
.and_then(|tok| tok.prev_token())
.filter(|tok| tok.kind() == SyntaxKind::WHITESPACE)
{
- edit.delete(tok.text_range())
+ edit.delete(tok)
}
+
+ builder.add_file_edits(ctx.vfs_file_id(), edit);
}
fn edit_struct_references(
ctx: &AssistContext<'_>,
- edit: &mut SourceChangeBuilder,
+ builder: &mut SourceChangeBuilder,
strukt: Either<hir::Struct, hir::Variant>,
) {
let strukt_def = match strukt {
@@ -161,17 +167,20 @@ fn edit_struct_references(
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()));
+ let source = ctx.sema.parse(file_id);
+ let mut edit = builder.make_editor(source.syntax());
for r in refs {
- process_struct_name_reference(ctx, r, edit);
+ process_struct_name_reference(ctx, r, &mut edit, &source);
}
+ builder.add_file_edits(file_id.file_id(ctx.db()), edit);
}
}
fn process_struct_name_reference(
ctx: &AssistContext<'_>,
r: FileReference,
- edit: &mut SourceChangeBuilder,
+ edit: &mut SyntaxEditor,
+ source: &ast::SourceFile,
) -> Option<()> {
// First check if it's the last semgnet of a path that directly belongs to a record
// expression/pattern.
@@ -192,36 +201,26 @@ fn process_struct_name_reference(
match_ast! {
match parent {
ast::RecordPat(record_struct_pat) => {
- // When we failed to get the original range for the whole struct expression node,
+ // When we failed to get the original range for the whole struct pattern node,
// we can't provide any reasonable edit. Leave it untouched.
- let file_range = ctx.sema.original_range_opt(record_struct_pat.syntax())?;
- edit.replace(
- file_range.range,
- ast::make::tuple_struct_pat(
- record_struct_pat.path()?,
- record_struct_pat
- .record_pat_field_list()?
- .fields()
- .filter_map(|pat| pat.pat())
- .chain(record_struct_pat.record_pat_field_list()?
- .rest_pat()
- .map(Into::into))
- )
- .to_string()
+ record_to_tuple_struct_like(
+ ctx,
+ source,
+ edit,
+ record_struct_pat.record_pat_field_list()?,
+ |it| it.fields().filter_map(|it| it.name_ref()),
);
},
ast::RecordExpr(record_expr) => {
- // When we failed to get the original range for the whole struct pattern node,
+ // When we failed to get the original range for the whole struct expression node,
// we can't provide any reasonable edit. Leave it untouched.
- let file_range = ctx.sema.original_range_opt(record_expr.syntax())?;
- let path = record_expr.path()?;
- let args = record_expr
- .record_expr_field_list()?
- .fields()
- .filter_map(|f| f.expr())
- .join(", ");
-
- edit.replace(file_range.range, format!("{path}({args})"));
+ record_to_tuple_struct_like(
+ ctx,
+ source,
+ edit,
+ record_expr.record_expr_field_list()?,
+ |it| it.fields().filter_map(|it| it.name_ref()),
+ );
},
_ => {}
}
@@ -230,11 +229,67 @@ fn process_struct_name_reference(
Some(())
}
+fn record_to_tuple_struct_like<T, I>(
+ ctx: &AssistContext<'_>,
+ source: &ast::SourceFile,
+ edit: &mut SyntaxEditor,
+ field_list: T,
+ fields: impl FnOnce(&T) -> I,
+) -> Option<()>
+where
+ T: AstNode,
+ I: IntoIterator<Item = ast::NameRef>,
+{
+ let make = SyntaxFactory::without_mappings();
+ let orig = ctx.sema.original_range_opt(field_list.syntax())?;
+ let list_range = cover_edit_range(source, orig.range);
+
+ let l_curly = match list_range.start() {
+ NodeOrToken::Node(node) => node.first_token()?,
+ NodeOrToken::Token(t) => t.clone(),
+ };
+ let r_curly = match list_range.end() {
+ NodeOrToken::Node(node) => node.last_token()?,
+ NodeOrToken::Token(t) => t.clone(),
+ };
+
+ if l_curly.kind() == T!['{'] {
+ delete_whitespace(edit, l_curly.prev_token());
+ delete_whitespace(edit, l_curly.next_token());
+ edit.replace(l_curly, make.token(T!['(']));
+ }
+ if r_curly.kind() == T!['}'] {
+ delete_whitespace(edit, r_curly.prev_token());
+ edit.replace(r_curly, make.token(T![')']));
+ }
+
+ for name_ref in fields(&field_list) {
+ let Some(orig) = ctx.sema.original_range_opt(name_ref.syntax()) else { continue };
+ let name_range = cover_edit_range(source, orig.range);
+
+ if let Some(colon) = next_non_trivia_token(name_range.end().clone())
+ && colon.kind() == T![:]
+ {
+ edit.delete(&colon);
+ edit.delete_all(name_range);
+
+ if let Some(next) = next_non_trivia_token(colon.clone())
+ && next.kind() != T!['}']
+ {
+ // Avoid overlapping delete whitespace on `{ field: }`
+ delete_whitespace(edit, colon.next_token());
+ }
+ }
+ }
+ Some(())
+}
+
fn edit_field_references(
ctx: &AssistContext<'_>,
- edit: &mut SourceChangeBuilder,
+ builder: &mut SourceChangeBuilder,
fields: impl Iterator<Item = ast::RecordField>,
) {
+ let make = SyntaxFactory::without_mappings();
for (index, field) in fields.enumerate() {
let field = match ctx.sema.to_def(&field) {
Some(it) => it,
@@ -243,19 +298,46 @@ fn edit_field_references(
let def = Definition::Field(field);
let usages = def.usages(&ctx.sema).all();
for (file_id, refs) in usages {
- edit.edit_file(file_id.file_id(ctx.db()));
+ let source = ctx.sema.parse(file_id);
+ let mut edit = builder.make_editor(source.syntax());
+
for r in refs {
if let Some(name_ref) = r.name.as_name_ref() {
// Only edit the field reference if it's part of a `.field` access
if name_ref.syntax().parent().and_then(ast::FieldExpr::cast).is_some() {
- edit.replace(r.range, index.to_string());
+ edit.replace_all(
+ cover_edit_range(&source, r.range),
+ vec![make.name_ref(&index.to_string()).syntax().clone().into()],
+ );
}
}
}
+
+ builder.add_file_edits(file_id.file_id(ctx.db()), edit);
}
}
}
+fn delete_whitespace(edit: &mut SyntaxEditor, whitespace: Option<impl Element>) {
+ let Some(whitespace) = whitespace else { return };
+ let NodeOrToken::Token(token) = whitespace.syntax_element() else { return };
+
+ if token.kind() == SyntaxKind::WHITESPACE && !token.text().contains('\n') {
+ edit.delete(token);
+ }
+}
+
+fn remove_trailing_comma(w: ast::WhereClause) -> SyntaxNode {
+ let w = w.syntax().clone_subtree();
+ let mut editor = SyntaxEditor::new(w.clone());
+ if let Some(last) = w.last_child_or_token()
+ && last.kind() == T![,]
+ {
+ editor.delete(last);
+ }
+ editor.finish().new_root().clone()
+}
+
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -678,6 +760,102 @@ where
}
#[test]
+ fn convert_constructor_expr_uses_self() {
+ // regression test for #21595
+ check_assist(
+ convert_named_struct_to_tuple_struct,
+ r#"
+struct $0Foo { field1: u32 }
+impl Foo {
+ fn clone(&self) -> Self {
+ Self { field1: self.field1 }
+ }
+}"#,
+ r#"
+struct Foo(u32);
+impl Foo {
+ fn clone(&self) -> Self {
+ Self(self.0)
+ }
+}"#,
+ );
+
+ check_assist(
+ convert_named_struct_to_tuple_struct,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+struct $0Foo { field1: u32 }
+impl Foo {
+ fn clone(&self) -> Self {
+ id!(Self { field1: self.field1 })
+ }
+}"#,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+struct Foo(u32);
+impl Foo {
+ fn clone(&self) -> Self {
+ id!(Self(self.0))
+ }
+}"#,
+ );
+ }
+
+ #[test]
+ fn convert_pat_uses_self() {
+ // regression test for #21595
+ check_assist(
+ convert_named_struct_to_tuple_struct,
+ r#"
+enum Foo {
+ $0Value { field: &'static Foo },
+ Nil,
+}
+fn foo(foo: &Foo) {
+ if let Foo::Value { field: Foo::Value { field } } = foo {}
+}"#,
+ r#"
+enum Foo {
+ Value(&'static Foo),
+ Nil,
+}
+fn foo(foo: &Foo) {
+ if let Foo::Value(Foo::Value(field)) = foo {}
+}"#,
+ );
+
+ check_assist(
+ convert_named_struct_to_tuple_struct,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+enum Foo {
+ $0Value { field: &'static Foo },
+ Nil,
+}
+fn foo(foo: &Foo) {
+ if let id!(Foo::Value { field: Foo::Value { field } }) = foo {}
+}"#,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+enum Foo {
+ Value(&'static Foo),
+ Nil,
+}
+fn foo(foo: &Foo) {
+ if let id!(Foo::Value(Foo::Value(field))) = foo {}
+}"#,
+ );
+ }
+
+ #[test]
fn not_applicable_other_than_record_variant() {
check_assist_not_applicable(
convert_named_struct_to_tuple_struct,
@@ -1042,7 +1220,9 @@ struct Struct(i32);
fn test() {
id! {
- let s = Struct(42);
+ let s = Struct(
+ 42,
+ );
let Struct(value) = s;
let Struct(inner) = s;
}
diff --git a/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs b/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
index f8b9bb68db..f1eae83866 100644
--- a/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
+++ b/crates/ide-assists/src/handlers/convert_tuple_struct_to_named_struct.rs
@@ -1,17 +1,21 @@
use either::Either;
-use hir::FileRangeWrapper;
-use ide_db::defs::{Definition, NameRefClass};
-use std::ops::RangeInclusive;
+use ide_db::{
+ defs::{Definition, NameRefClass},
+ search::FileReference,
+};
use syntax::{
- SyntaxElement, SyntaxKind, SyntaxNode, T, TextSize,
+ SyntaxKind, T,
ast::{
- self, AstNode, HasAttrs, HasGenericParams, HasVisibility, syntax_factory::SyntaxFactory,
+ self, AstNode, HasArgList, HasAttrs, HasGenericParams, HasVisibility,
+ syntax_factory::SyntaxFactory,
},
match_ast,
syntax_editor::{Element, Position, SyntaxEditor},
};
-use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
+use crate::{
+ AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::cover_edit_range,
+};
// Assist: convert_tuple_struct_to_named_struct
//
@@ -147,93 +151,121 @@ fn edit_struct_references(
};
let usages = strukt_def.usages(&ctx.sema).include_self_refs().all();
- let edit_node = |node: SyntaxNode| -> Option<SyntaxNode> {
- let make = SyntaxFactory::without_mappings();
- match_ast! {
- match node {
- ast::TupleStructPat(tuple_struct_pat) => {
- Some(make.record_pat_with_fields(
- tuple_struct_pat.path()?,
- generate_record_pat_list(&tuple_struct_pat, names),
- ).syntax().clone())
- },
- // for tuple struct creations like Foo(42)
- ast::CallExpr(call_expr) => {
- let path = call_expr.syntax().descendants().find_map(ast::PathExpr::cast).and_then(|expr| expr.path())?;
-
- // this also includes method calls like Foo::new(42), we should skip them
- if let Some(name_ref) = path.segment().and_then(|s| s.name_ref()) {
- match NameRefClass::classify(&ctx.sema, &name_ref) {
- Some(NameRefClass::Definition(Definition::SelfType(_), _)) => {},
- Some(NameRefClass::Definition(def, _)) if def == strukt_def => {},
- _ => return None,
- };
- }
+ for (file_id, refs) in usages {
+ let source = ctx.sema.parse(file_id);
+ let mut editor = edit.make_editor(source.syntax());
- let arg_list = call_expr.syntax().descendants().find_map(ast::ArgList::cast)?;
- Some(
- make.record_expr(
- path,
- ast::make::record_expr_field_list(arg_list.args().zip(names).map(
- |(expr, name)| {
- ast::make::record_expr_field(
- ast::make::name_ref(&name.to_string()),
- Some(expr),
- )
- },
- )),
- ).syntax().clone()
- )
- },
- _ => None,
- }
+ for r in refs {
+ process_struct_name_reference(ctx, r, &mut editor, &source, &strukt_def, names);
}
- };
- for (file_id, refs) in usages {
- let source = ctx.sema.parse(file_id);
- let source = source.syntax();
-
- let mut editor = edit.make_editor(source);
- for r in refs.iter().rev() {
- if let Some((old_node, new_node)) = r
- .name
- .syntax()
- .ancestors()
- .find_map(|node| Some((node.clone(), edit_node(node.clone())?)))
- {
- if let Some(old_node) = ctx.sema.original_syntax_node_rooted(&old_node) {
- editor.replace(old_node, new_node);
- } else {
- let FileRangeWrapper { file_id: _, range } = ctx.sema.original_range(&old_node);
- let parent = source.covering_element(range);
- match parent {
- SyntaxElement::Token(token) => {
- editor.replace(token, new_node.syntax_element());
- }
- SyntaxElement::Node(parent_node) => {
- // replace the part of macro
- // ```
- // foo!(a, Test::A(0));
- // ^^^^^^^^^^^^^^^ // parent_node
- // ^^^^^^^^^^ // replace_range
- // ```
- let start = parent_node
- .children_with_tokens()
- .find(|t| t.text_range().contains(range.start()));
- let end = parent_node
- .children_with_tokens()
- .find(|t| t.text_range().contains(range.end() - TextSize::new(1)));
- if let (Some(start), Some(end)) = (start, end) {
- let replace_range = RangeInclusive::new(start, end);
- editor.replace_all(replace_range, vec![new_node.into()]);
- }
- }
+ edit.add_file_edits(file_id.file_id(ctx.db()), editor);
+ }
+}
+
+fn process_struct_name_reference(
+ ctx: &AssistContext<'_>,
+ r: FileReference,
+ editor: &mut SyntaxEditor,
+ source: &ast::SourceFile,
+ strukt_def: &Definition,
+ names: &[ast::Name],
+) -> Option<()> {
+ let make = SyntaxFactory::without_mappings();
+ let name_ref = r.name.as_name_ref()?;
+ let path_segment = name_ref.syntax().parent().and_then(ast::PathSegment::cast)?;
+ let full_path = path_segment.syntax().parent().and_then(ast::Path::cast)?.top_path();
+
+ if full_path.segment()?.name_ref()? != *name_ref {
+ // `name_ref` isn't the last segment of the path, so `full_path` doesn't point to the
+ // struct we want to edit.
+ return None;
+ }
+
+ let parent = full_path.syntax().parent()?;
+ match_ast! {
+ match parent {
+ ast::TupleStructPat(tuple_struct_pat) => {
+ let range = ctx.sema.original_range_opt(tuple_struct_pat.syntax())?.range;
+ let new = make.record_pat_with_fields(
+ full_path,
+ generate_record_pat_list(&tuple_struct_pat, names),
+ );
+ editor.replace_all(cover_edit_range(source, range), vec![new.syntax().clone().into()]);
+ },
+ ast::PathExpr(path_expr) => {
+ let call_expr = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
+
+ // this also includes method calls like Foo::new(42), we should skip them
+ match NameRefClass::classify(&ctx.sema, name_ref) {
+ Some(NameRefClass::Definition(Definition::SelfType(_), _)) => {},
+ Some(NameRefClass::Definition(def, _)) if def == *strukt_def => {},
+ _ => return None,
+ }
+
+ let arg_list = call_expr.arg_list()?;
+ let mut first_insert = vec![];
+ for (expr, name) in arg_list.args().zip(names) {
+ let range = ctx.sema.original_range_opt(expr.syntax())?.range;
+ let place = cover_edit_range(source, range);
+ let elements = vec![
+ make.name_ref(&name.text()).syntax().clone().into(),
+ make.token(T![:]).into(),
+ make.whitespace(" ").into(),
+ ];
+ if first_insert.is_empty() {
+ // XXX: SyntaxEditor cannot insert after deleted element
+ first_insert = elements;
+ } else {
+ editor.insert_all(Position::before(place.start()), elements);
}
}
- }
+ process_delimiter(ctx, source, editor, &arg_list, first_insert);
+ },
+ _ => {}
}
- edit.add_file_edits(file_id.file_id(ctx.db()), editor);
+ }
+ Some(())
+}
+
+fn process_delimiter(
+ ctx: &AssistContext<'_>,
+ source: &ast::SourceFile,
+ editor: &mut SyntaxEditor,
+ list: &impl AstNode,
+ first_insert: Vec<syntax::SyntaxElement>,
+) {
+ let Some(range) = ctx.sema.original_range_opt(list.syntax()) else { return };
+ let place = cover_edit_range(source, range.range);
+
+ let l_paren = match place.start() {
+ syntax::NodeOrToken::Node(node) => node.first_token(),
+ syntax::NodeOrToken::Token(t) => Some(t.clone()),
+ };
+ let r_paren = match place.end() {
+ syntax::NodeOrToken::Node(node) => node.last_token(),
+ syntax::NodeOrToken::Token(t) => Some(t.clone()),
+ };
+
+ let make = SyntaxFactory::without_mappings();
+ if let Some(l_paren) = l_paren
+ && l_paren.kind() == T!['(']
+ {
+ let mut open_delim = vec![
+ make.whitespace(" ").into(),
+ make.token(T!['{']).into(),
+ make.whitespace(" ").into(),
+ ];
+ open_delim.extend(first_insert);
+ editor.replace_with_many(l_paren, open_delim);
+ }
+ if let Some(r_paren) = r_paren
+ && r_paren.kind() == T![')']
+ {
+ editor.replace_with_many(
+ r_paren,
+ vec![make.whitespace(" ").into(), make.token(T!['}']).into()],
+ );
}
}
@@ -252,13 +284,15 @@ fn edit_field_references(
let usages = def.usages(&ctx.sema).all();
for (file_id, refs) in usages {
let source = ctx.sema.parse(file_id);
- let source = source.syntax();
- let mut editor = edit.make_editor(source);
+ let mut editor = edit.make_editor(source.syntax());
for r in refs {
if let Some(name_ref) = r.name.as_name_ref()
- && let Some(original) = ctx.sema.original_ast_node(name_ref.clone())
+ && let Some(original) = ctx.sema.original_range_opt(name_ref.syntax())
{
- editor.replace(original.syntax(), name.syntax());
+ editor.replace_all(
+ cover_edit_range(&source, original.range),
+ vec![name.syntax().clone().into()],
+ );
}
}
edit.add_file_edits(file_id.file_id(ctx.db()), editor);
@@ -739,6 +773,64 @@ where
"#,
);
}
+
+ #[test]
+ fn convert_expr_uses_self() {
+ check_assist(
+ convert_tuple_struct_to_named_struct,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+struct T$0(u8);
+fn test(t: T) {
+ T(t.0);
+ id!(T(t.0));
+}"#,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+struct T { field1: u8 }
+fn test(t: T) {
+ T { field1: t.field1 };
+ id!(T { field1: t.field1 });
+}"#,
+ );
+ }
+
+ #[test]
+ #[ignore = "FIXME overlap edits in nested uses self"]
+ fn convert_pat_uses_self() {
+ check_assist(
+ convert_tuple_struct_to_named_struct,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+enum T {
+ $0Value(&'static T),
+ Nil,
+}
+fn test(t: T) {
+ if let T::Value(T::Value(t)) = t {}
+ if let id!(T::Value(T::Value(t))) = t {}
+}"#,
+ r#"
+macro_rules! id {
+ ($($t:tt)*) => { $($t)* }
+}
+enum T {
+ Value { field1: &'static T },
+ Nil,
+}
+fn test(t: T) {
+ if let T::Value { field1: T::Value { field1: t } } = t {}
+ if let id!(T::Value { field1: T::Value { field1: t } }) = t {}
+}"#,
+ );
+ }
+
#[test]
fn not_applicable_other_than_tuple_variant() {
check_assist_not_applicable(
diff --git a/crates/ide-assists/src/utils.rs b/crates/ide-assists/src/utils.rs
index ef178cdc70..0657e7243a 100644
--- a/crates/ide-assists/src/utils.rs
+++ b/crates/ide-assists/src/utils.rs
@@ -1217,6 +1217,20 @@ pub(crate) fn cover_let_chain(mut expr: ast::Expr, range: TextRange) -> Option<a
}
}
+pub(crate) fn cover_edit_range(
+ source: &impl AstNode,
+ range: TextRange,
+) -> std::ops::RangeInclusive<syntax::SyntaxElement> {
+ let node = match source.syntax().covering_element(range) {
+ NodeOrToken::Node(node) => node,
+ NodeOrToken::Token(t) => t.parent().unwrap(),
+ };
+ let mut iter = node.children_with_tokens().filter(|it| range.contains_range(it.text_range()));
+ let first = iter.next().unwrap_or(node.into());
+ let last = iter.last().unwrap_or_else(|| first.clone());
+ first..=last
+}
+
pub(crate) fn is_selected(
it: &impl AstNode,
selection: syntax::TextRange,
diff --git a/crates/syntax/src/algo.rs b/crates/syntax/src/algo.rs
index 3ab9c90262..c679921b3f 100644
--- a/crates/syntax/src/algo.rs
+++ b/crates/syntax/src/algo.rs
@@ -132,3 +132,19 @@ pub fn previous_non_trivia_token(e: impl Into<SyntaxElement>) -> Option<SyntaxTo
}
None
}
+
+pub fn next_non_trivia_token(e: impl Into<SyntaxElement>) -> Option<SyntaxToken> {
+ let mut token = match e.into() {
+ SyntaxElement::Node(n) => n.last_token()?,
+ SyntaxElement::Token(t) => t,
+ }
+ .next_token();
+ while let Some(inner) = token {
+ if !inner.kind().is_trivia() {
+ return Some(inner);
+ } else {
+ token = inner.next_token();
+ }
+ }
+ None
+}