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