diff options
author | unexge <[email protected]> | 2021-04-03 22:04:31 +0100 |
---|---|---|
committer | unexge <[email protected]> | 2021-04-04 18:52:43 +0100 |
commit | 8d4be829e09dae4af7b4a82b379fdb8700b0929f (patch) | |
tree | 860e45f32fc34b5f443addd04583658118ad2daa /crates | |
parent | d3a112d68c74cbd02630f6c909071c94872c193f (diff) |
Add convert tuple struct to named struct assist
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ide_assists/src/handlers/convert_tuple_struct_to_named_struct.rs | 345 | ||||
-rw-r--r-- | crates/ide_assists/src/lib.rs | 2 | ||||
-rw-r--r-- | crates/ide_assists/src/tests/generated.rs | 15 | ||||
-rw-r--r-- | crates/ide_db/src/search.rs | 18 | ||||
-rw-r--r-- | crates/syntax/src/ast/make.rs | 26 |
5 files changed, 397 insertions, 9 deletions
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 new file mode 100644 index 000000000..b2f7be011 --- /dev/null +++ b/crates/ide_assists/src/handlers/convert_tuple_struct_to_named_struct.rs | |||
@@ -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 | } | ||
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs index 3e2c82dac..1c55b9fbf 100644 --- a/crates/ide_assists/src/lib.rs +++ b/crates/ide_assists/src/lib.rs | |||
@@ -118,6 +118,7 @@ mod handlers { | |||
118 | mod convert_comment_block; | 118 | mod convert_comment_block; |
119 | mod convert_iter_for_each_to_for; | 119 | mod convert_iter_for_each_to_for; |
120 | mod convert_into_to_from; | 120 | mod convert_into_to_from; |
121 | mod convert_tuple_struct_to_named_struct; | ||
121 | mod early_return; | 122 | mod early_return; |
122 | mod expand_glob_import; | 123 | mod expand_glob_import; |
123 | mod extract_function; | 124 | mod extract_function; |
@@ -187,6 +188,7 @@ mod handlers { | |||
187 | convert_comment_block::convert_comment_block, | 188 | convert_comment_block::convert_comment_block, |
188 | convert_iter_for_each_to_for::convert_iter_for_each_to_for, | 189 | convert_iter_for_each_to_for::convert_iter_for_each_to_for, |
189 | convert_into_to_from::convert_into_to_from, | 190 | convert_into_to_from::convert_into_to_from, |
191 | convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct, | ||
190 | early_return::convert_to_guarded_return, | 192 | early_return::convert_to_guarded_return, |
191 | expand_glob_import::expand_glob_import, | 193 | expand_glob_import::expand_glob_import, |
192 | extract_struct_from_enum_variant::extract_struct_from_enum_variant, | 194 | extract_struct_from_enum_variant::extract_struct_from_enum_variant, |
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs index 27a22ca10..53f455adf 100644 --- a/crates/ide_assists/src/tests/generated.rs +++ b/crates/ide_assists/src/tests/generated.rs | |||
@@ -292,6 +292,21 @@ fn main() { | |||
292 | } | 292 | } |
293 | 293 | ||
294 | #[test] | 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 | |||
309 | #[test] | ||
295 | fn doctest_expand_glob_import() { | 310 | fn doctest_expand_glob_import() { |
296 | check_doc_test( | 311 | check_doc_test( |
297 | "expand_glob_import", | 312 | "expand_glob_import", |
diff --git a/crates/ide_db/src/search.rs b/crates/ide_db/src/search.rs index 02f5e514b..90e4e7b03 100644 --- a/crates/ide_db/src/search.rs +++ b/crates/ide_db/src/search.rs | |||
@@ -430,6 +430,15 @@ impl<'a> FindUsages<'a> { | |||
430 | sink: &mut dyn FnMut(FileId, FileReference) -> bool, | 430 | sink: &mut dyn FnMut(FileId, FileReference) -> bool, |
431 | ) -> bool { | 431 | ) -> bool { |
432 | match NameRefClass::classify(self.sema, &name_ref) { | 432 | match NameRefClass::classify(self.sema, &name_ref) { |
433 | Some(NameRefClass::Definition(def)) if &def == self.def => { | ||
434 | let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax()); | ||
435 | let reference = FileReference { | ||
436 | range, | ||
437 | name: ast::NameLike::NameRef(name_ref.clone()), | ||
438 | access: reference_access(&def, &name_ref), | ||
439 | }; | ||
440 | sink(file_id, reference) | ||
441 | } | ||
433 | Some(NameRefClass::Definition(Definition::SelfType(impl_))) => { | 442 | Some(NameRefClass::Definition(Definition::SelfType(impl_))) => { |
434 | let ty = impl_.self_ty(self.sema.db); | 443 | let ty = impl_.self_ty(self.sema.db); |
435 | 444 | ||
@@ -448,15 +457,6 @@ impl<'a> FindUsages<'a> { | |||
448 | 457 | ||
449 | false | 458 | false |
450 | } | 459 | } |
451 | Some(NameRefClass::Definition(def)) if &def == self.def => { | ||
452 | let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax()); | ||
453 | let reference = FileReference { | ||
454 | range, | ||
455 | name: ast::NameLike::NameRef(name_ref.clone()), | ||
456 | access: reference_access(&def, &name_ref), | ||
457 | }; | ||
458 | sink(file_id, reference) | ||
459 | } | ||
460 | Some(NameRefClass::FieldShorthand { local_ref: local, field_ref: field }) => { | 460 | Some(NameRefClass::FieldShorthand { local_ref: local, field_ref: field }) => { |
461 | let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax()); | 461 | let FileRange { file_id, range } = self.sema.original_range(name_ref.syntax()); |
462 | let reference = match self.def { | 462 | let reference = match self.def { |
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index c6a7b99b7..3a588e540 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs | |||
@@ -133,6 +133,17 @@ pub fn use_(visibility: Option<ast::Visibility>, use_tree: ast::UseTree) -> ast: | |||
133 | ast_from_text(&format!("{}use {};", visibility, use_tree)) | 133 | ast_from_text(&format!("{}use {};", visibility, use_tree)) |
134 | } | 134 | } |
135 | 135 | ||
136 | pub fn record_expr(path: ast::Path, fields: ast::RecordExprFieldList) -> ast::RecordExpr { | ||
137 | ast_from_text(&format!("fn f() {{ {} {} }}", path, fields)) | ||
138 | } | ||
139 | |||
140 | pub fn record_expr_field_list( | ||
141 | fields: impl IntoIterator<Item = ast::RecordExprField>, | ||
142 | ) -> ast::RecordExprFieldList { | ||
143 | let fields = fields.into_iter().join(", "); | ||
144 | ast_from_text(&format!("fn f() {{ S {{ {} }} }}", fields)) | ||
145 | } | ||
146 | |||
136 | pub fn record_expr_field(name: ast::NameRef, expr: Option<ast::Expr>) -> ast::RecordExprField { | 147 | pub fn record_expr_field(name: ast::NameRef, expr: Option<ast::Expr>) -> ast::RecordExprField { |
137 | return match expr { | 148 | return match expr { |
138 | Some(expr) => from_text(&format!("{}: {}", name, expr)), | 149 | Some(expr) => from_text(&format!("{}: {}", name, expr)), |
@@ -325,6 +336,21 @@ pub fn record_pat(path: ast::Path, pats: impl IntoIterator<Item = ast::Pat>) -> | |||
325 | } | 336 | } |
326 | } | 337 | } |
327 | 338 | ||
339 | pub fn record_pat_with_fields(path: ast::Path, fields: ast::RecordPatFieldList) -> ast::RecordPat { | ||
340 | ast_from_text(&format!("fn f({} {}: ()))", path, fields)) | ||
341 | } | ||
342 | |||
343 | pub fn record_pat_field_list( | ||
344 | fields: impl IntoIterator<Item = ast::RecordPatField>, | ||
345 | ) -> ast::RecordPatFieldList { | ||
346 | let fields = fields.into_iter().join(", "); | ||
347 | ast_from_text(&format!("fn f(S {{ {} }}: ()))", fields)) | ||
348 | } | ||
349 | |||
350 | pub fn record_pat_field(name_ref: ast::NameRef, pat: ast::Pat) -> ast::RecordPatField { | ||
351 | ast_from_text(&format!("fn f(S {{ {}: {} }}: ()))", name_ref, pat)) | ||
352 | } | ||
353 | |||
328 | /// Returns a `BindPat` if the path has just one segment, a `PathPat` otherwise. | 354 | /// Returns a `BindPat` if the path has just one segment, a `PathPat` otherwise. |
329 | pub fn path_pat(path: ast::Path) -> ast::Pat { | 355 | pub fn path_pat(path: ast::Path) -> ast::Pat { |
330 | return from_text(&path.to_string()); | 356 | return from_text(&path.to_string()); |