Skip to content

Commit 8d4be82

Browse files
committed
Add convert tuple struct to named struct assist
1 parent d3a112d commit 8d4be82

File tree

5 files changed

+397
-9
lines changed

5 files changed

+397
-9
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
use hir::{Adt, ModuleDef};
2+
use ide_db::defs::Definition;
3+
use syntax::{
4+
ast::{self, AstNode, GenericParamsOwner, VisibilityOwner},
5+
match_ast,
6+
};
7+
8+
use crate::{assist_context::AssistBuilder, AssistContext, AssistId, AssistKind, Assists};
9+
10+
// Assist: convert_tuple_struct_to_named_struct
11+
//
12+
// Converts tuple struct to struct with named fields.
13+
//
14+
// ```
15+
// struct Inner;
16+
// struct A$0(Inner);
17+
// ```
18+
// ->
19+
// ```
20+
// struct Inner;
21+
// struct A { field1: Inner }
22+
// ```
23+
pub(crate) fn convert_tuple_struct_to_named_struct(
24+
acc: &mut Assists,
25+
ctx: &AssistContext,
26+
) -> Option<()> {
27+
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
28+
let tuple_fields = match strukt.field_list()? {
29+
ast::FieldList::TupleFieldList(it) => it,
30+
ast::FieldList::RecordFieldList(_) => return None,
31+
};
32+
33+
let target = strukt.syntax().text_range();
34+
acc.add(
35+
AssistId("convert_tuple_struct_to_named_struct", AssistKind::RefactorRewrite),
36+
"Convert to named struct",
37+
target,
38+
|edit| {
39+
let names = generate_names(tuple_fields.fields());
40+
edit_field_references(ctx, edit, tuple_fields.fields(), &names);
41+
edit_struct_references(ctx, edit, &strukt, &names);
42+
edit_struct_def(ctx, edit, &strukt, tuple_fields, names);
43+
},
44+
)
45+
}
46+
47+
fn edit_struct_def(
48+
ctx: &AssistContext,
49+
edit: &mut AssistBuilder,
50+
strukt: &ast::Struct,
51+
tuple_fields: ast::TupleFieldList,
52+
names: Vec<ast::Name>,
53+
) {
54+
let record_fields = tuple_fields
55+
.fields()
56+
.zip(names)
57+
.map(|(f, name)| ast::make::record_field(f.visibility(), name, f.ty().unwrap()));
58+
let record_fields = ast::make::record_field_list(record_fields);
59+
let tuple_fields_text_range = tuple_fields.syntax().text_range();
60+
61+
edit.edit_file(ctx.frange.file_id);
62+
63+
if let Some(w) = strukt.where_clause() {
64+
edit.delete(w.syntax().text_range());
65+
edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_newline().text());
66+
edit.insert(tuple_fields_text_range.start(), w.syntax().text());
67+
edit.insert(tuple_fields_text_range.start(), ",");
68+
edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_newline().text());
69+
} else {
70+
edit.insert(tuple_fields_text_range.start(), ast::make::tokens::single_space().text());
71+
}
72+
73+
edit.replace(tuple_fields_text_range, record_fields.to_string());
74+
strukt.semicolon_token().map(|t| edit.delete(t.text_range()));
75+
}
76+
77+
fn edit_struct_references(
78+
ctx: &AssistContext,
79+
edit: &mut AssistBuilder,
80+
strukt: &ast::Struct,
81+
names: &[ast::Name],
82+
) {
83+
let strukt_def = ctx.sema.to_def(strukt).unwrap();
84+
let usages = Definition::ModuleDef(ModuleDef::Adt(Adt::Struct(strukt_def)))
85+
.usages(&ctx.sema)
86+
.include_self_kw_refs(true)
87+
.all();
88+
89+
for (file_id, refs) in usages {
90+
edit.edit_file(file_id);
91+
for r in refs {
92+
for node in r.name.syntax().ancestors() {
93+
match_ast! {
94+
match node {
95+
ast::TupleStructPat(tuple_struct_pat) => {
96+
edit.replace(
97+
tuple_struct_pat.syntax().text_range(),
98+
ast::make::record_pat_with_fields(
99+
tuple_struct_pat.path().unwrap(),
100+
ast::make::record_pat_field_list(tuple_struct_pat.fields().zip(names).map(
101+
|(pat, name)| {
102+
ast::make::record_pat_field(
103+
ast::make::name_ref(&name.to_string()),
104+
pat,
105+
)
106+
},
107+
)),
108+
)
109+
.to_string(),
110+
);
111+
},
112+
// for tuple struct creations like: Foo(42)
113+
ast::CallExpr(call_expr) => {
114+
let path = call_expr.syntax().descendants().find_map(ast::PathExpr::cast).unwrap();
115+
let arg_list =
116+
call_expr.syntax().descendants().find_map(ast::ArgList::cast).unwrap();
117+
118+
edit.replace(
119+
call_expr.syntax().text_range(),
120+
ast::make::record_expr(
121+
path.path().unwrap(),
122+
ast::make::record_expr_field_list(arg_list.args().zip(names).map(
123+
|(expr, name)| {
124+
ast::make::record_expr_field(
125+
ast::make::name_ref(&name.to_string()),
126+
Some(expr),
127+
)
128+
},
129+
)),
130+
)
131+
.to_string(),
132+
);
133+
},
134+
_ => ()
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
142+
fn edit_field_references(
143+
ctx: &AssistContext,
144+
edit: &mut AssistBuilder,
145+
fields: impl Iterator<Item = ast::TupleField>,
146+
names: &[ast::Name],
147+
) {
148+
for (field, name) in fields.zip(names) {
149+
let field = match ctx.sema.to_def(&field) {
150+
Some(it) => it,
151+
None => continue,
152+
};
153+
let def = Definition::Field(field);
154+
let usages = def.usages(&ctx.sema).all();
155+
for (file_id, refs) in usages {
156+
edit.edit_file(file_id);
157+
for r in refs {
158+
if let Some(name_ref) = r.name.as_name_ref() {
159+
edit.replace(name_ref.syntax().text_range(), name.text());
160+
}
161+
}
162+
}
163+
}
164+
}
165+
166+
fn generate_names(fields: impl Iterator<Item = ast::TupleField>) -> Vec<ast::Name> {
167+
fields.enumerate().map(|(i, _)| ast::make::name(&format!("field{}", i + 1))).collect()
168+
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use crate::tests::{check_assist, check_assist_not_applicable};
173+
174+
use super::*;
175+
176+
#[test]
177+
fn not_applicable_other_than_tuple_struct() {
178+
check_assist_not_applicable(
179+
convert_tuple_struct_to_named_struct,
180+
r#"struct Foo$0 { bar: u32 };"#,
181+
);
182+
check_assist_not_applicable(convert_tuple_struct_to_named_struct, r#"struct Foo$0;"#);
183+
}
184+
185+
#[test]
186+
fn convert_simple_struct() {
187+
check_assist(
188+
convert_tuple_struct_to_named_struct,
189+
r#"
190+
struct Inner;
191+
struct A$0(Inner);
192+
193+
impl A {
194+
fn new() -> A {
195+
A(Inner)
196+
}
197+
198+
fn into_inner(self) -> Inner {
199+
self.0
200+
}
201+
}"#,
202+
r#"
203+
struct Inner;
204+
struct A { field1: Inner }
205+
206+
impl A {
207+
fn new() -> A {
208+
A { field1: Inner }
209+
}
210+
211+
fn into_inner(self) -> Inner {
212+
self.field1
213+
}
214+
}"#,
215+
);
216+
}
217+
218+
#[test]
219+
fn convert_struct_referenced_via_self_kw() {
220+
check_assist(
221+
convert_tuple_struct_to_named_struct,
222+
r#"
223+
struct Inner;
224+
struct A$0(Inner);
225+
226+
impl A {
227+
fn new() -> Self {
228+
Self(Inner)
229+
}
230+
231+
fn into_inner(self) -> Inner {
232+
self.0
233+
}
234+
}"#,
235+
r#"
236+
struct Inner;
237+
struct A { field1: Inner }
238+
239+
impl A {
240+
fn new() -> Self {
241+
Self { field1: Inner }
242+
}
243+
244+
fn into_inner(self) -> Inner {
245+
self.field1
246+
}
247+
}"#,
248+
);
249+
}
250+
251+
#[test]
252+
fn convert_destructured_struct() {
253+
check_assist(
254+
convert_tuple_struct_to_named_struct,
255+
r#"
256+
struct Inner;
257+
struct A$0(Inner);
258+
259+
impl A {
260+
fn into_inner(self) -> Inner {
261+
let A(first) = self;
262+
first
263+
}
264+
265+
fn into_inner_via_self(self) -> Inner {
266+
let Self(first) = self;
267+
first
268+
}
269+
}"#,
270+
r#"
271+
struct Inner;
272+
struct A { field1: Inner }
273+
274+
impl A {
275+
fn into_inner(self) -> Inner {
276+
let A { field1: first } = self;
277+
first
278+
}
279+
280+
fn into_inner_via_self(self) -> Inner {
281+
let Self { field1: first } = self;
282+
first
283+
}
284+
}"#,
285+
);
286+
}
287+
288+
#[test]
289+
fn convert_struct_with_visibility() {
290+
check_assist(
291+
convert_tuple_struct_to_named_struct,
292+
r#"
293+
struct A$0(pub u32, pub(crate) u64);
294+
295+
impl A {
296+
fn new() -> A {
297+
A(42, 42)
298+
}
299+
300+
fn into_first(self) -> u32 {
301+
self.0
302+
}
303+
304+
fn into_second(self) -> u64 {
305+
self.1
306+
}
307+
}"#,
308+
r#"
309+
struct A { pub field1: u32, pub(crate) field2: u64 }
310+
311+
impl A {
312+
fn new() -> A {
313+
A { field1: 42, field2: 42 }
314+
}
315+
316+
fn into_first(self) -> u32 {
317+
self.field1
318+
}
319+
320+
fn into_second(self) -> u64 {
321+
self.field2
322+
}
323+
}"#,
324+
);
325+
}
326+
327+
#[test]
328+
fn convert_struct_with_where_clause() {
329+
check_assist(
330+
convert_tuple_struct_to_named_struct,
331+
r#"
332+
struct Wrap$0<T>(T)
333+
where
334+
T: Display;
335+
"#,
336+
r#"
337+
struct Wrap<T>
338+
where
339+
T: Display,
340+
{ field1: T }
341+
342+
"#,
343+
);
344+
}
345+
}

crates/ide_assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ mod handlers {
118118
mod convert_comment_block;
119119
mod convert_iter_for_each_to_for;
120120
mod convert_into_to_from;
121+
mod convert_tuple_struct_to_named_struct;
121122
mod early_return;
122123
mod expand_glob_import;
123124
mod extract_function;
@@ -187,6 +188,7 @@ mod handlers {
187188
convert_comment_block::convert_comment_block,
188189
convert_iter_for_each_to_for::convert_iter_for_each_to_for,
189190
convert_into_to_from::convert_into_to_from,
191+
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
190192
early_return::convert_to_guarded_return,
191193
expand_glob_import::expand_glob_import,
192194
extract_struct_from_enum_variant::extract_struct_from_enum_variant,

crates/ide_assists/src/tests/generated.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,21 @@ fn main() {
291291
)
292292
}
293293

294+
#[test]
295+
fn doctest_convert_tuple_struct_to_named_struct() {
296+
check_doc_test(
297+
"convert_tuple_struct_to_named_struct",
298+
r#####"
299+
struct Inner;
300+
struct A$0(Inner);
301+
"#####,
302+
r#####"
303+
struct Inner;
304+
struct A { field1: Inner }
305+
"#####,
306+
)
307+
}
308+
294309
#[test]
295310
fn doctest_expand_glob_import() {
296311
check_doc_test(

0 commit comments

Comments
 (0)