Skip to content

Commit df37ef6

Browse files
Merge pull request #21996 from Shourya742/2026-04-08-migrate-extract-struct-from-enum-variant
Migrate extract struct from enum variant to new SyntaxEditor and Port whitespace heuristics to SyntaxEditor
2 parents 7d23c32 + 9fd0deb commit df37ef6

3 files changed

Lines changed: 258 additions & 101 deletions

File tree

crates/ide-assists/src/handlers/extract_struct_from_enum_variant.rs

Lines changed: 141 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use ide_db::{
66
FxHashSet, RootDatabase,
77
defs::Definition,
88
helpers::mod_path_to_ast,
9-
imports::insert_use::{ImportScope, InsertUseConfig, insert_use},
9+
imports::insert_use::{ImportScope, InsertUseConfig, insert_use_with_editor},
1010
path_transform::PathTransform,
1111
search::FileReference,
1212
};
@@ -16,12 +16,14 @@ use syntax::{
1616
SyntaxKind::*,
1717
SyntaxNode, T,
1818
ast::{
19-
self, AstNode, HasAttrs, HasGenericParams, HasName, HasVisibility, edit::AstNodeEdit, make,
19+
self, AstNode, HasAttrs, HasGenericParams, HasName, HasVisibility, edit::AstNodeEdit,
20+
syntax_factory::SyntaxFactory,
2021
},
21-
match_ast, ted,
22+
match_ast,
23+
syntax_editor::{Position, SyntaxEditor},
2224
};
2325

24-
use crate::{AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder};
26+
use crate::{AssistContext, AssistId, Assists};
2527

2628
// Assist: extract_struct_from_enum_variant
2729
//
@@ -58,6 +60,8 @@ pub(crate) fn extract_struct_from_enum_variant(
5860
"Extract struct from enum variant",
5961
target,
6062
|builder| {
63+
let make = SyntaxFactory::with_mappings();
64+
let mut editor = builder.make_editor(variant.syntax());
6165
let edition = enum_hir.krate(ctx.db()).edition(ctx.db());
6266
let variant_hir_name = variant_hir.name(ctx.db());
6367
let enum_module_def = ModuleDef::from(enum_hir);
@@ -73,40 +77,56 @@ pub(crate) fn extract_struct_from_enum_variant(
7377
def_file_references = Some(references);
7478
continue;
7579
}
76-
builder.edit_file(file_id.file_id(ctx.db()));
7780
let processed = process_references(
7881
ctx,
79-
builder,
8082
&mut visited_modules_set,
8183
&enum_module_def,
8284
&variant_hir_name,
8385
references,
8486
);
87+
if processed.is_empty() {
88+
continue;
89+
}
90+
let mut file_editor = builder.make_editor(processed[0].0.syntax());
8591
processed.into_iter().for_each(|(path, node, import)| {
86-
apply_references(ctx.config.insert_use, path, node, import, edition)
92+
apply_references(
93+
ctx.config.insert_use,
94+
path,
95+
node,
96+
import,
97+
edition,
98+
&mut file_editor,
99+
&make,
100+
)
87101
});
102+
file_editor.add_mappings(make.take());
103+
builder.add_file_edits(file_id.file_id(ctx.db()), file_editor);
88104
}
89-
builder.edit_file(ctx.vfs_file_id());
90105

91-
let variant = builder.make_mut(variant.clone());
92106
if let Some(references) = def_file_references {
93107
let processed = process_references(
94108
ctx,
95-
builder,
96109
&mut visited_modules_set,
97110
&enum_module_def,
98111
&variant_hir_name,
99112
references,
100113
);
101114
processed.into_iter().for_each(|(path, node, import)| {
102-
apply_references(ctx.config.insert_use, path, node, import, edition)
115+
apply_references(
116+
ctx.config.insert_use,
117+
path,
118+
node,
119+
import,
120+
edition,
121+
&mut editor,
122+
&make,
123+
)
103124
});
104125
}
105126

106-
let generic_params = enum_ast
107-
.generic_param_list()
108-
.and_then(|known_generics| extract_generic_params(&known_generics, &field_list));
109-
let generics = generic_params.as_ref().map(|generics| generics.clone_for_update());
127+
let generic_params = enum_ast.generic_param_list().and_then(|known_generics| {
128+
extract_generic_params(&make, &known_generics, &field_list)
129+
});
110130

111131
// resolve GenericArg in field_list to actual type
112132
let field_list = if let Some((target_scope, source_scope)) =
@@ -124,25 +144,45 @@ pub(crate) fn extract_struct_from_enum_variant(
124144
}
125145
}
126146
} else {
127-
field_list.clone_for_update()
147+
field_list.clone()
128148
};
129149

130-
let def =
131-
create_struct_def(variant_name.clone(), &variant, &field_list, generics, &enum_ast);
150+
let (comments_for_struct, comments_to_delete) =
151+
collect_variant_comments(&make, variant.syntax());
152+
for element in &comments_to_delete {
153+
editor.delete(element.clone());
154+
}
155+
156+
let def = create_struct_def(
157+
&make,
158+
variant_name.clone(),
159+
&field_list,
160+
generic_params.clone(),
161+
&enum_ast,
162+
);
132163

133164
let enum_ast = variant.parent_enum();
134165
let indent = enum_ast.indent_level();
135166
let def = def.indent(indent);
136167

137-
ted::insert_all(
138-
ted::Position::before(enum_ast.syntax()),
139-
vec![
140-
def.syntax().clone().into(),
141-
make::tokens::whitespace(&format!("\n\n{indent}")).into(),
142-
],
168+
let mut insert_items: Vec<SyntaxElement> = Vec::new();
169+
for attr in enum_ast.attrs() {
170+
insert_items.push(attr.syntax().clone().into());
171+
insert_items.push(make.whitespace("\n").into());
172+
}
173+
insert_items.extend(comments_for_struct);
174+
insert_items.push(def.syntax().clone().into());
175+
insert_items.push(make.whitespace(&format!("\n\n{indent}")).into());
176+
editor.insert_all_with_whitespace(
177+
Position::before(enum_ast.syntax()),
178+
insert_items,
179+
&make,
143180
);
144181

145-
update_variant(&variant, generic_params.map(|g| g.clone_for_update()));
182+
update_variant(&make, &mut editor, &variant, generic_params);
183+
184+
editor.add_mappings(make.finish_with_mappings());
185+
builder.add_file_edits(ctx.vfs_file_id(), editor);
146186
},
147187
)
148188
}
@@ -184,6 +224,7 @@ fn existing_definition(db: &RootDatabase, variant_name: &ast::Name, variant: &En
184224
}
185225

186226
fn extract_generic_params(
227+
make: &SyntaxFactory,
187228
known_generics: &ast::GenericParamList,
188229
field_list: &Either<ast::RecordFieldList, ast::TupleFieldList>,
189230
) -> Option<ast::GenericParamList> {
@@ -201,7 +242,7 @@ fn extract_generic_params(
201242
};
202243

203244
let generics = generics.into_iter().filter_map(|(param, tag)| tag.then_some(param));
204-
tagged_one.then(|| make::generic_param_list(generics))
245+
tagged_one.then(|| make.generic_param_list(generics))
205246
}
206247

207248
fn tag_generics_in_variant(ty: &ast::Type, generics: &mut [(ast::GenericParam, bool)]) -> bool {
@@ -250,118 +291,114 @@ fn tag_generics_in_variant(ty: &ast::Type, generics: &mut [(ast::GenericParam, b
250291
}
251292

252293
fn create_struct_def(
294+
make: &SyntaxFactory,
253295
name: ast::Name,
254-
variant: &ast::Variant,
255296
field_list: &Either<ast::RecordFieldList, ast::TupleFieldList>,
256297
generics: Option<ast::GenericParamList>,
257298
enum_: &ast::Enum,
258299
) -> ast::Struct {
259300
let enum_vis = enum_.visibility();
260301

261-
let insert_vis = |node: &'_ SyntaxNode, vis: &'_ SyntaxNode| {
262-
let vis = vis.clone_for_update();
263-
ted::insert(ted::Position::before(node), vis);
264-
};
265-
266302
// for fields without any existing visibility, use visibility of enum
267303
let field_list: ast::FieldList = match field_list {
268304
Either::Left(field_list) => {
269305
if let Some(vis) = &enum_vis {
270-
field_list
271-
.fields()
272-
.filter(|field| field.visibility().is_none())
273-
.filter_map(|field| field.name())
274-
.for_each(|it| insert_vis(it.syntax(), vis.syntax()));
306+
let new_fields = field_list.fields().map(|field| {
307+
if field.visibility().is_none()
308+
&& let Some(name) = field.name()
309+
&& let Some(ty) = field.ty()
310+
{
311+
make.record_field(Some(vis.clone()), name, ty)
312+
} else {
313+
field
314+
}
315+
});
316+
make.record_field_list(new_fields).into()
317+
} else {
318+
field_list.clone().into()
275319
}
276-
277-
field_list.clone().into()
278320
}
279321
Either::Right(field_list) => {
280322
if let Some(vis) = &enum_vis {
281-
field_list
282-
.fields()
283-
.filter(|field| field.visibility().is_none())
284-
.filter_map(|field| field.ty())
285-
.for_each(|it| insert_vis(it.syntax(), vis.syntax()));
323+
let new_fields = field_list.fields().map(|field| {
324+
if field.visibility().is_none()
325+
&& let Some(ty) = field.ty()
326+
{
327+
make.tuple_field(Some(vis.clone()), ty)
328+
} else {
329+
field
330+
}
331+
});
332+
make.tuple_field_list(new_fields).into()
333+
} else {
334+
field_list.clone().into()
286335
}
287-
288-
field_list.clone().into()
289336
}
290337
};
291338

292-
let strukt = make::struct_(enum_vis, name, generics, field_list).clone_for_update();
293-
294-
// take comments from variant
295-
ted::insert_all(
296-
ted::Position::first_child_of(strukt.syntax()),
297-
take_all_comments(variant.syntax()),
298-
);
299-
300-
// copy attributes from enum
301-
ted::insert_all(
302-
ted::Position::first_child_of(strukt.syntax()),
303-
enum_
304-
.attrs()
305-
.flat_map(|it| {
306-
vec![it.syntax().clone_for_update().into(), make::tokens::single_newline().into()]
307-
})
308-
.collect(),
309-
);
310-
311-
strukt
339+
make.struct_(enum_vis, name, generics, field_list)
312340
}
313341

314-
fn update_variant(variant: &ast::Variant, generics: Option<ast::GenericParamList>) -> Option<()> {
342+
fn update_variant(
343+
make: &SyntaxFactory,
344+
editor: &mut SyntaxEditor,
345+
variant: &ast::Variant,
346+
generics: Option<ast::GenericParamList>,
347+
) -> Option<()> {
315348
let name = variant.name()?;
316349
let generic_args = generics
317350
.filter(|generics| generics.generic_params().count() > 0)
318351
.map(|generics| generics.to_generic_args());
319352
// FIXME: replace with a `ast::make` constructor
320353
let ty = match generic_args {
321-
Some(generic_args) => make::ty(&format!("{name}{generic_args}")),
322-
None => make::ty(&name.text()),
354+
Some(generic_args) => make.ty(&format!("{name}{generic_args}")),
355+
None => make.ty(&name.text()),
323356
};
324357

325358
// change from a record to a tuple field list
326-
let tuple_field = make::tuple_field(None, ty);
327-
let field_list = make::tuple_field_list(iter::once(tuple_field)).clone_for_update();
328-
ted::replace(variant.field_list()?.syntax(), field_list.syntax());
359+
let tuple_field = make.tuple_field(None, ty);
360+
let field_list = make.tuple_field_list(iter::once(tuple_field));
361+
editor.replace(variant.field_list()?.syntax(), field_list.syntax());
329362

330363
// remove any ws after the name
331364
if let Some(ws) = name
332365
.syntax()
333366
.siblings_with_tokens(syntax::Direction::Next)
334367
.find_map(|tok| tok.into_token().filter(|tok| tok.kind() == WHITESPACE))
335368
{
336-
ted::remove(SyntaxElement::Token(ws));
369+
editor.delete(ws);
337370
}
338371

339372
Some(())
340373
}
341374

342-
// Note: this also detaches whitespace after comments,
343-
// since `SyntaxNode::splice_children` (and by extension `ted::insert_all_raw`)
344-
// detaches nodes. If we only took the comments, we'd leave behind the old whitespace.
345-
fn take_all_comments(node: &SyntaxNode) -> Vec<SyntaxElement> {
346-
let mut remove_next_ws = false;
347-
node.children_with_tokens()
348-
.filter_map(move |child| match child.kind() {
375+
fn collect_variant_comments(
376+
make: &SyntaxFactory,
377+
node: &SyntaxNode,
378+
) -> (Vec<SyntaxElement>, Vec<SyntaxElement>) {
379+
let mut to_insert: Vec<SyntaxElement> = Vec::new();
380+
let mut to_delete: Vec<SyntaxElement> = Vec::new();
381+
let mut after_comment = false;
382+
383+
for child in node.children_with_tokens() {
384+
match child.kind() {
349385
COMMENT => {
350-
remove_next_ws = true;
351-
child.detach();
352-
Some(child)
386+
after_comment = true;
387+
to_insert.push(child.clone());
388+
to_delete.push(child);
353389
}
354-
WHITESPACE if remove_next_ws => {
355-
remove_next_ws = false;
356-
child.detach();
357-
Some(make::tokens::single_newline().into())
390+
WHITESPACE if after_comment => {
391+
after_comment = false;
392+
to_insert.push(make.whitespace("\n").into());
393+
to_delete.push(child);
358394
}
359395
_ => {
360-
remove_next_ws = false;
361-
None
396+
after_comment = false;
362397
}
363-
})
364-
.collect()
398+
}
399+
}
400+
401+
(to_insert, to_delete)
365402
}
366403

367404
fn apply_references(
@@ -370,20 +407,27 @@ fn apply_references(
370407
node: SyntaxNode,
371408
import: Option<(ImportScope, hir::ModPath)>,
372409
edition: Edition,
410+
editor: &mut SyntaxEditor,
411+
make: &SyntaxFactory,
373412
) {
374413
if let Some((scope, path)) = import {
375-
insert_use(&scope, mod_path_to_ast(&path, edition), &insert_use_cfg);
414+
insert_use_with_editor(
415+
&scope,
416+
mod_path_to_ast(&path, edition),
417+
&insert_use_cfg,
418+
editor,
419+
make,
420+
);
376421
}
377422
// deep clone to prevent cycle
378-
let path = make::path_from_segments(iter::once(segment.clone_subtree()), false);
379-
ted::insert_raw(ted::Position::before(segment.syntax()), path.clone_for_update().syntax());
380-
ted::insert_raw(ted::Position::before(segment.syntax()), make::token(T!['(']));
381-
ted::insert_raw(ted::Position::after(&node), make::token(T![')']));
423+
let path = make.path_from_segments(iter::once(segment.clone()), false);
424+
editor.insert(Position::before(segment.syntax()), make.token(T!['(']));
425+
editor.insert(Position::before(segment.syntax()), path.syntax());
426+
editor.insert(Position::after(&node), make.token(T![')']));
382427
}
383428

384429
fn process_references(
385430
ctx: &AssistContext<'_>,
386-
builder: &mut SourceChangeBuilder,
387431
visited_modules: &mut FxHashSet<Module>,
388432
enum_module_def: &ModuleDef,
389433
variant_hir_name: &Name,
@@ -394,8 +438,6 @@ fn process_references(
394438
refs.into_iter()
395439
.flat_map(|reference| {
396440
let (segment, scope_node, module) = reference_to_node(&ctx.sema, reference)?;
397-
let segment = builder.make_mut(segment);
398-
let scope_node = builder.make_syntax_mut(scope_node);
399441
if !visited_modules.contains(&module) {
400442
let cfg =
401443
ctx.config.find_path_config(ctx.sema.is_nightly(module.krate(ctx.sema.db)));

0 commit comments

Comments
 (0)