diff options
author | Zac Pullar-Strecker <[email protected]> | 2020-08-24 10:19:53 +0100 |
---|---|---|
committer | Zac Pullar-Strecker <[email protected]> | 2020-08-24 10:20:13 +0100 |
commit | 7bbca7a1b3f9293d2f5cc5745199bc5f8396f2f0 (patch) | |
tree | bdb47765991cb973b2cd5481a088fac636bd326c /crates/assists/src/handlers/extract_struct_from_enum_variant.rs | |
parent | ca464650eeaca6195891199a93f4f76cf3e7e697 (diff) | |
parent | e65d48d1fb3d4d91d9dc1148a7a836ff5c9a3c87 (diff) |
Merge remote-tracking branch 'upstream/master' into 503-hover-doc-links
Diffstat (limited to 'crates/assists/src/handlers/extract_struct_from_enum_variant.rs')
-rw-r--r-- | crates/assists/src/handlers/extract_struct_from_enum_variant.rs | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/crates/assists/src/handlers/extract_struct_from_enum_variant.rs b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs new file mode 100644 index 000000000..c1124b9e2 --- /dev/null +++ b/crates/assists/src/handlers/extract_struct_from_enum_variant.rs | |||
@@ -0,0 +1,322 @@ | |||
1 | use base_db::FileId; | ||
2 | use hir::{EnumVariant, Module, ModuleDef, Name}; | ||
3 | use ide_db::{defs::Definition, search::Reference, RootDatabase}; | ||
4 | use rustc_hash::FxHashSet; | ||
5 | use syntax::{ | ||
6 | algo::find_node_at_offset, | ||
7 | ast::{self, edit::IndentLevel, ArgListOwner, AstNode, NameOwner, VisibilityOwner}, | ||
8 | SourceFile, TextRange, TextSize, | ||
9 | }; | ||
10 | |||
11 | use crate::{ | ||
12 | assist_context::AssistBuilder, utils::insert_use_statement, AssistContext, AssistId, | ||
13 | AssistKind, Assists, | ||
14 | }; | ||
15 | |||
16 | // Assist: extract_struct_from_enum_variant | ||
17 | // | ||
18 | // Extracts a struct from enum variant. | ||
19 | // | ||
20 | // ``` | ||
21 | // enum A { <|>One(u32, u32) } | ||
22 | // ``` | ||
23 | // -> | ||
24 | // ``` | ||
25 | // struct One(pub u32, pub u32); | ||
26 | // | ||
27 | // enum A { One(One) } | ||
28 | // ``` | ||
29 | pub(crate) fn extract_struct_from_enum_variant( | ||
30 | acc: &mut Assists, | ||
31 | ctx: &AssistContext, | ||
32 | ) -> Option<()> { | ||
33 | let variant = ctx.find_node_at_offset::<ast::Variant>()?; | ||
34 | let field_list = match variant.kind() { | ||
35 | ast::StructKind::Tuple(field_list) => field_list, | ||
36 | _ => return None, | ||
37 | }; | ||
38 | let variant_name = variant.name()?.to_string(); | ||
39 | let variant_hir = ctx.sema.to_def(&variant)?; | ||
40 | if existing_struct_def(ctx.db(), &variant_name, &variant_hir) { | ||
41 | return None; | ||
42 | } | ||
43 | let enum_ast = variant.parent_enum(); | ||
44 | let visibility = enum_ast.visibility(); | ||
45 | let enum_hir = ctx.sema.to_def(&enum_ast)?; | ||
46 | let variant_hir_name = variant_hir.name(ctx.db()); | ||
47 | let enum_module_def = ModuleDef::from(enum_hir); | ||
48 | let current_module = enum_hir.module(ctx.db()); | ||
49 | let target = variant.syntax().text_range(); | ||
50 | acc.add( | ||
51 | AssistId("extract_struct_from_enum_variant", AssistKind::RefactorRewrite), | ||
52 | "Extract struct from enum variant", | ||
53 | target, | ||
54 | |builder| { | ||
55 | let definition = Definition::ModuleDef(ModuleDef::EnumVariant(variant_hir)); | ||
56 | let res = definition.usages(&ctx.sema).all(); | ||
57 | let start_offset = variant.parent_enum().syntax().text_range().start(); | ||
58 | let mut visited_modules_set = FxHashSet::default(); | ||
59 | visited_modules_set.insert(current_module); | ||
60 | for reference in res { | ||
61 | let source_file = ctx.sema.parse(reference.file_range.file_id); | ||
62 | update_reference( | ||
63 | ctx, | ||
64 | builder, | ||
65 | reference, | ||
66 | &source_file, | ||
67 | &enum_module_def, | ||
68 | &variant_hir_name, | ||
69 | &mut visited_modules_set, | ||
70 | ); | ||
71 | } | ||
72 | extract_struct_def( | ||
73 | builder, | ||
74 | &enum_ast, | ||
75 | &variant_name, | ||
76 | &field_list.to_string(), | ||
77 | start_offset, | ||
78 | ctx.frange.file_id, | ||
79 | &visibility, | ||
80 | ); | ||
81 | let list_range = field_list.syntax().text_range(); | ||
82 | update_variant(builder, &variant_name, ctx.frange.file_id, list_range); | ||
83 | }, | ||
84 | ) | ||
85 | } | ||
86 | |||
87 | fn existing_struct_def(db: &RootDatabase, variant_name: &str, variant: &EnumVariant) -> bool { | ||
88 | variant | ||
89 | .parent_enum(db) | ||
90 | .module(db) | ||
91 | .scope(db, None) | ||
92 | .into_iter() | ||
93 | .any(|(name, _)| name.to_string() == variant_name.to_string()) | ||
94 | } | ||
95 | |||
96 | fn insert_import( | ||
97 | ctx: &AssistContext, | ||
98 | builder: &mut AssistBuilder, | ||
99 | path: &ast::PathExpr, | ||
100 | module: &Module, | ||
101 | enum_module_def: &ModuleDef, | ||
102 | variant_hir_name: &Name, | ||
103 | ) -> Option<()> { | ||
104 | let db = ctx.db(); | ||
105 | let mod_path = module.find_use_path(db, enum_module_def.clone()); | ||
106 | if let Some(mut mod_path) = mod_path { | ||
107 | mod_path.segments.pop(); | ||
108 | mod_path.segments.push(variant_hir_name.clone()); | ||
109 | insert_use_statement( | ||
110 | path.syntax(), | ||
111 | &mod_path.to_string(), | ||
112 | ctx, | ||
113 | builder.text_edit_builder(), | ||
114 | ); | ||
115 | } | ||
116 | Some(()) | ||
117 | } | ||
118 | |||
119 | // FIXME: this should use strongly-typed `make`, rather than string manipulation. | ||
120 | fn extract_struct_def( | ||
121 | builder: &mut AssistBuilder, | ||
122 | enum_: &ast::Enum, | ||
123 | variant_name: &str, | ||
124 | variant_list: &str, | ||
125 | start_offset: TextSize, | ||
126 | file_id: FileId, | ||
127 | visibility: &Option<ast::Visibility>, | ||
128 | ) -> Option<()> { | ||
129 | let visibility_string = if let Some(visibility) = visibility { | ||
130 | format!("{} ", visibility.to_string()) | ||
131 | } else { | ||
132 | "".to_string() | ||
133 | }; | ||
134 | let indent = IndentLevel::from_node(enum_.syntax()); | ||
135 | let struct_def = format!( | ||
136 | r#"{}struct {}{}; | ||
137 | |||
138 | {}"#, | ||
139 | visibility_string, | ||
140 | variant_name, | ||
141 | list_with_visibility(variant_list), | ||
142 | indent | ||
143 | ); | ||
144 | builder.edit_file(file_id); | ||
145 | builder.insert(start_offset, struct_def); | ||
146 | Some(()) | ||
147 | } | ||
148 | |||
149 | fn update_variant( | ||
150 | builder: &mut AssistBuilder, | ||
151 | variant_name: &str, | ||
152 | file_id: FileId, | ||
153 | list_range: TextRange, | ||
154 | ) -> Option<()> { | ||
155 | let inside_variant_range = TextRange::new( | ||
156 | list_range.start().checked_add(TextSize::from(1))?, | ||
157 | list_range.end().checked_sub(TextSize::from(1))?, | ||
158 | ); | ||
159 | builder.edit_file(file_id); | ||
160 | builder.replace(inside_variant_range, variant_name); | ||
161 | Some(()) | ||
162 | } | ||
163 | |||
164 | fn update_reference( | ||
165 | ctx: &AssistContext, | ||
166 | builder: &mut AssistBuilder, | ||
167 | reference: Reference, | ||
168 | source_file: &SourceFile, | ||
169 | enum_module_def: &ModuleDef, | ||
170 | variant_hir_name: &Name, | ||
171 | visited_modules_set: &mut FxHashSet<Module>, | ||
172 | ) -> Option<()> { | ||
173 | let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>( | ||
174 | source_file.syntax(), | ||
175 | reference.file_range.range.start(), | ||
176 | )?; | ||
177 | let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?; | ||
178 | let list = call.arg_list()?; | ||
179 | let segment = path_expr.path()?.segment()?; | ||
180 | let module = ctx.sema.scope(&path_expr.syntax()).module()?; | ||
181 | let list_range = list.syntax().text_range(); | ||
182 | let inside_list_range = TextRange::new( | ||
183 | list_range.start().checked_add(TextSize::from(1))?, | ||
184 | list_range.end().checked_sub(TextSize::from(1))?, | ||
185 | ); | ||
186 | builder.edit_file(reference.file_range.file_id); | ||
187 | if !visited_modules_set.contains(&module) { | ||
188 | if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name) | ||
189 | .is_some() | ||
190 | { | ||
191 | visited_modules_set.insert(module); | ||
192 | } | ||
193 | } | ||
194 | builder.replace(inside_list_range, format!("{}{}", segment, list)); | ||
195 | Some(()) | ||
196 | } | ||
197 | |||
198 | fn list_with_visibility(list: &str) -> String { | ||
199 | list.split(',') | ||
200 | .map(|part| { | ||
201 | let index = if part.chars().next().unwrap() == '(' { 1usize } else { 0 }; | ||
202 | let mut mod_part = part.trim().to_string(); | ||
203 | mod_part.insert_str(index, "pub "); | ||
204 | mod_part | ||
205 | }) | ||
206 | .collect::<Vec<String>>() | ||
207 | .join(", ") | ||
208 | } | ||
209 | |||
210 | #[cfg(test)] | ||
211 | mod tests { | ||
212 | |||
213 | use crate::{ | ||
214 | tests::{check_assist, check_assist_not_applicable}, | ||
215 | utils::FamousDefs, | ||
216 | }; | ||
217 | |||
218 | use super::*; | ||
219 | |||
220 | #[test] | ||
221 | fn test_extract_struct_several_fields() { | ||
222 | check_assist( | ||
223 | extract_struct_from_enum_variant, | ||
224 | "enum A { <|>One(u32, u32) }", | ||
225 | r#"struct One(pub u32, pub u32); | ||
226 | |||
227 | enum A { One(One) }"#, | ||
228 | ); | ||
229 | } | ||
230 | |||
231 | #[test] | ||
232 | fn test_extract_struct_one_field() { | ||
233 | check_assist( | ||
234 | extract_struct_from_enum_variant, | ||
235 | "enum A { <|>One(u32) }", | ||
236 | r#"struct One(pub u32); | ||
237 | |||
238 | enum A { One(One) }"#, | ||
239 | ); | ||
240 | } | ||
241 | |||
242 | #[test] | ||
243 | fn test_extract_struct_pub_visibility() { | ||
244 | check_assist( | ||
245 | extract_struct_from_enum_variant, | ||
246 | "pub enum A { <|>One(u32, u32) }", | ||
247 | r#"pub struct One(pub u32, pub u32); | ||
248 | |||
249 | pub enum A { One(One) }"#, | ||
250 | ); | ||
251 | } | ||
252 | |||
253 | #[test] | ||
254 | fn test_extract_struct_with_complex_imports() { | ||
255 | check_assist( | ||
256 | extract_struct_from_enum_variant, | ||
257 | r#"mod my_mod { | ||
258 | fn another_fn() { | ||
259 | let m = my_other_mod::MyEnum::MyField(1, 1); | ||
260 | } | ||
261 | |||
262 | pub mod my_other_mod { | ||
263 | fn another_fn() { | ||
264 | let m = MyEnum::MyField(1, 1); | ||
265 | } | ||
266 | |||
267 | pub enum MyEnum { | ||
268 | <|>MyField(u8, u8), | ||
269 | } | ||
270 | } | ||
271 | } | ||
272 | |||
273 | fn another_fn() { | ||
274 | let m = my_mod::my_other_mod::MyEnum::MyField(1, 1); | ||
275 | }"#, | ||
276 | r#"use my_mod::my_other_mod::MyField; | ||
277 | |||
278 | mod my_mod { | ||
279 | use my_other_mod::MyField; | ||
280 | |||
281 | fn another_fn() { | ||
282 | let m = my_other_mod::MyEnum::MyField(MyField(1, 1)); | ||
283 | } | ||
284 | |||
285 | pub mod my_other_mod { | ||
286 | fn another_fn() { | ||
287 | let m = MyEnum::MyField(MyField(1, 1)); | ||
288 | } | ||
289 | |||
290 | pub struct MyField(pub u8, pub u8); | ||
291 | |||
292 | pub enum MyEnum { | ||
293 | MyField(MyField), | ||
294 | } | ||
295 | } | ||
296 | } | ||
297 | |||
298 | fn another_fn() { | ||
299 | let m = my_mod::my_other_mod::MyEnum::MyField(MyField(1, 1)); | ||
300 | }"#, | ||
301 | ); | ||
302 | } | ||
303 | |||
304 | fn check_not_applicable(ra_fixture: &str) { | ||
305 | let fixture = | ||
306 | format!("//- /main.rs crate:main deps:core\n{}\n{}", ra_fixture, FamousDefs::FIXTURE); | ||
307 | check_assist_not_applicable(extract_struct_from_enum_variant, &fixture) | ||
308 | } | ||
309 | |||
310 | #[test] | ||
311 | fn test_extract_enum_not_applicable_for_element_with_no_fields() { | ||
312 | check_not_applicable("enum A { <|>One }"); | ||
313 | } | ||
314 | |||
315 | #[test] | ||
316 | fn test_extract_enum_not_applicable_if_struct_exists() { | ||
317 | check_not_applicable( | ||
318 | r#"struct One; | ||
319 | enum A { <|>One(u8) }"#, | ||
320 | ); | ||
321 | } | ||
322 | } | ||