aboutsummaryrefslogtreecommitdiff
path: root/crates/assists/src/handlers/extract_struct_from_enum_variant.rs
diff options
context:
space:
mode:
authorZac Pullar-Strecker <[email protected]>2020-08-24 10:19:53 +0100
committerZac Pullar-Strecker <[email protected]>2020-08-24 10:20:13 +0100
commit7bbca7a1b3f9293d2f5cc5745199bc5f8396f2f0 (patch)
treebdb47765991cb973b2cd5481a088fac636bd326c /crates/assists/src/handlers/extract_struct_from_enum_variant.rs
parentca464650eeaca6195891199a93f4f76cf3e7e697 (diff)
parente65d48d1fb3d4d91d9dc1148a7a836ff5c9a3c87 (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.rs322
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 @@
1use base_db::FileId;
2use hir::{EnumVariant, Module, ModuleDef, Name};
3use ide_db::{defs::Definition, search::Reference, RootDatabase};
4use rustc_hash::FxHashSet;
5use syntax::{
6 algo::find_node_at_offset,
7 ast::{self, edit::IndentLevel, ArgListOwner, AstNode, NameOwner, VisibilityOwner},
8 SourceFile, TextRange, TextSize,
9};
10
11use 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// ```
29pub(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
87fn 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
96fn 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.
120fn 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
149fn 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
164fn 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
198fn 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)]
211mod 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
227enum 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
238enum 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
249pub 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
273fn 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
278mod 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
298fn 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}