diff options
author | Mikhail Rakhmanov <[email protected]> | 2020-05-22 21:28:30 +0100 |
---|---|---|
committer | Mikhail Rakhmanov <[email protected]> | 2020-05-22 21:28:30 +0100 |
commit | 5cd4eb6dd6d8c733077a6aeea5d2cc0812ded096 (patch) | |
tree | dffc23552eb6e77c50966fa155ed8a56b418c5f0 /crates/ra_assists/src | |
parent | d4daca9f02ec46be1beef79e9ed647a3a24e2434 (diff) |
Add preliminary implementation of extract struct from enum variant
Diffstat (limited to 'crates/ra_assists/src')
-rw-r--r-- | crates/ra_assists/src/assist_context.rs | 62 | ||||
-rw-r--r-- | crates/ra_assists/src/handlers/extract_struct_from_enum_variant.rs | 338 | ||||
-rw-r--r-- | crates/ra_assists/src/lib.rs | 2 |
3 files changed, 401 insertions, 1 deletions
diff --git a/crates/ra_assists/src/assist_context.rs b/crates/ra_assists/src/assist_context.rs index 5b1a4680b..6291c68de 100644 --- a/crates/ra_assists/src/assist_context.rs +++ b/crates/ra_assists/src/assist_context.rs | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | use algo::find_covering_element; | 3 | use algo::find_covering_element; |
4 | use hir::Semantics; | 4 | use hir::Semantics; |
5 | use ra_db::{FileId, FileRange}; | 5 | use ra_db::{FileId, FileRange, FilePosition}; |
6 | use ra_fmt::{leading_indent, reindent}; | 6 | use ra_fmt::{leading_indent, reindent}; |
7 | use ra_ide_db::{ | 7 | use ra_ide_db::{ |
8 | source_change::{SourceChange, SourceFileEdit}, | 8 | source_change::{SourceChange, SourceFileEdit}, |
@@ -19,6 +19,7 @@ use crate::{ | |||
19 | assist_config::{AssistConfig, SnippetCap}, | 19 | assist_config::{AssistConfig, SnippetCap}, |
20 | Assist, AssistId, GroupLabel, ResolvedAssist, | 20 | Assist, AssistId, GroupLabel, ResolvedAssist, |
21 | }; | 21 | }; |
22 | use rustc_hash::FxHashMap; | ||
22 | 23 | ||
23 | /// `AssistContext` allows to apply an assist or check if it could be applied. | 24 | /// `AssistContext` allows to apply an assist or check if it could be applied. |
24 | /// | 25 | /// |
@@ -138,6 +139,16 @@ impl Assists { | |||
138 | let label = Assist::new(id, label.into(), None, target); | 139 | let label = Assist::new(id, label.into(), None, target); |
139 | self.add_impl(label, f) | 140 | self.add_impl(label, f) |
140 | } | 141 | } |
142 | pub(crate) fn add_in_multiple_files( | ||
143 | &mut self, | ||
144 | id: AssistId, | ||
145 | label: impl Into<String>, | ||
146 | target: TextRange, | ||
147 | f: impl FnOnce(&mut AssistDirector), | ||
148 | ) -> Option<()> { | ||
149 | let label = Assist::new(id, label.into(), None, target); | ||
150 | self.add_impl_multiple_files(label, f) | ||
151 | } | ||
141 | pub(crate) fn add_group( | 152 | pub(crate) fn add_group( |
142 | &mut self, | 153 | &mut self, |
143 | group: &GroupLabel, | 154 | group: &GroupLabel, |
@@ -162,6 +173,27 @@ impl Assists { | |||
162 | Some(()) | 173 | Some(()) |
163 | } | 174 | } |
164 | 175 | ||
176 | fn add_impl_multiple_files(&mut self, label: Assist, f: impl FnOnce(&mut AssistDirector)) -> Option<()> { | ||
177 | let change_label = label.label.clone(); | ||
178 | if !self.resolve { | ||
179 | return None | ||
180 | } | ||
181 | let mut director = AssistDirector::new(change_label.clone()); | ||
182 | f(&mut director); | ||
183 | let changes = director.finish(); | ||
184 | let file_edits: Vec<SourceFileEdit> = changes.into_iter() | ||
185 | .map(|mut change| change.source_file_edits.pop().unwrap()).collect(); | ||
186 | |||
187 | let source_change = SourceChange { | ||
188 | source_file_edits: file_edits, | ||
189 | file_system_edits: vec![], | ||
190 | is_snippet: false, | ||
191 | }; | ||
192 | |||
193 | self.buf.push((label, Some(source_change))); | ||
194 | Some(()) | ||
195 | } | ||
196 | |||
165 | fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> { | 197 | fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> { |
166 | self.buf.sort_by_key(|(label, _edit)| label.target.len()); | 198 | self.buf.sort_by_key(|(label, _edit)| label.target.len()); |
167 | self.buf | 199 | self.buf |
@@ -255,3 +287,31 @@ impl AssistBuilder { | |||
255 | res | 287 | res |
256 | } | 288 | } |
257 | } | 289 | } |
290 | |||
291 | pub(crate) struct AssistDirector { | ||
292 | source_changes: Vec<SourceChange>, | ||
293 | builders: FxHashMap<FileId, AssistBuilder>, | ||
294 | change_label: String | ||
295 | } | ||
296 | |||
297 | impl AssistDirector { | ||
298 | fn new(change_label: String) -> AssistDirector { | ||
299 | AssistDirector { | ||
300 | source_changes: vec![], | ||
301 | builders: FxHashMap::default(), | ||
302 | change_label | ||
303 | } | ||
304 | } | ||
305 | |||
306 | pub(crate) fn perform(&mut self, file_id: FileId, f: impl FnOnce(&mut AssistBuilder)) { | ||
307 | let mut builder = self.builders.entry(file_id).or_insert(AssistBuilder::new(file_id)); | ||
308 | f(&mut builder); | ||
309 | } | ||
310 | |||
311 | fn finish(mut self) -> Vec<SourceChange> { | ||
312 | for (file_id, builder) in self.builders.into_iter().collect::<Vec<(FileId, AssistBuilder)>>() { | ||
313 | self.source_changes.push(builder.finish()); | ||
314 | } | ||
315 | self.source_changes | ||
316 | } | ||
317 | } | ||
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 @@ | |||
1 | use hir_expand::name::AsName; | ||
2 | use ra_ide_db::{ | ||
3 | defs::Definition, imports_locator::ImportsLocator, search::Reference, RootDatabase, | ||
4 | }; | ||
5 | use ra_syntax::{ | ||
6 | algo::find_node_at_offset, | ||
7 | ast::{self, AstNode, NameOwner}, | ||
8 | SourceFile, SyntaxNode, TextRange, TextSize, | ||
9 | }; | ||
10 | use stdx::format_to; | ||
11 | |||
12 | use crate::{ | ||
13 | assist_context::{AssistBuilder, AssistDirector}, | ||
14 | utils::insert_use_statement, | ||
15 | AssistContext, AssistId, Assists, | ||
16 | }; | ||
17 | use ast::{ArgListOwner, VisibilityOwner}; | ||
18 | use hir::{EnumVariant, Module, ModuleDef}; | ||
19 | use ra_fmt::leading_indent; | ||
20 | use rustc_hash::FxHashSet; | ||
21 | use 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 | // ``` | ||
36 | pub(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 | |||
90 | fn 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 | |||
100 | fn 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 | |||
104 | fn 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 | |||
122 | fn 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 | |||
159 | fn 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 | |||
176 | fn 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 | |||
217 | fn 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)] | ||
230 | mod 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 | |||
243 | enum 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 | |||
254 | enum 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 | |||
265 | pub 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 | |||
289 | fn 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 | |||
294 | mod 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 | |||
314 | fn 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 | } | ||
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 464bc03dd..9933f7a50 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -115,6 +115,7 @@ mod handlers { | |||
115 | mod change_return_type_to_result; | 115 | mod change_return_type_to_result; |
116 | mod change_visibility; | 116 | mod change_visibility; |
117 | mod early_return; | 117 | mod early_return; |
118 | mod extract_struct_from_enum_variant; | ||
118 | mod fill_match_arms; | 119 | mod fill_match_arms; |
119 | mod fix_visibility; | 120 | mod fix_visibility; |
120 | mod flip_binexpr; | 121 | mod flip_binexpr; |
@@ -154,6 +155,7 @@ mod handlers { | |||
154 | change_return_type_to_result::change_return_type_to_result, | 155 | change_return_type_to_result::change_return_type_to_result, |
155 | change_visibility::change_visibility, | 156 | change_visibility::change_visibility, |
156 | early_return::convert_to_guarded_return, | 157 | early_return::convert_to_guarded_return, |
158 | extract_struct_from_enum_variant::extract_struct_from_enum, | ||
157 | fill_match_arms::fill_match_arms, | 159 | fill_match_arms::fill_match_arms, |
158 | fix_visibility::fix_visibility, | 160 | fix_visibility::fix_visibility, |
159 | flip_binexpr::flip_binexpr, | 161 | flip_binexpr::flip_binexpr, |