Skip to content

Commit 4aaa592

Browse files
committed
Migrate destructure_tuple_binding to mutable ast
Due to the way the current tree mutation api works, we need to collect changes before we can apply them to the real syntax tree, and also can only switch to a file once. `destructure_tuple_binding_in_sub_pattern` also gets migrated even though can't be used.
1 parent f3dcc67 commit 4aaa592

File tree

2 files changed

+156
-93
lines changed

2 files changed

+156
-93
lines changed

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

Lines changed: 155 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ use ide_db::{
33
defs::Definition,
44
search::{FileReference, SearchScope, UsageSearchResult},
55
};
6+
use itertools::Itertools;
67
use syntax::{
7-
ast::{self, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
8-
TextRange,
8+
ast::{self, make, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
9+
ted, T,
910
};
11+
use text_edit::TextRange;
1012

1113
use crate::assist_context::{AssistContext, Assists, SourceChangeBuilder};
1214

@@ -61,27 +63,36 @@ pub(crate) fn destructure_tuple_binding_impl(
6163
acc.add(
6264
AssistId("destructure_tuple_binding_in_sub_pattern", AssistKind::RefactorRewrite),
6365
"Destructure tuple in sub-pattern",
64-
data.range,
65-
|builder| {
66-
edit_tuple_assignment(ctx, builder, &data, true);
67-
edit_tuple_usages(&data, builder, ctx, true);
68-
},
66+
data.ident_pat.syntax().text_range(),
67+
|edit| destructure_tuple_edit_impl(ctx, edit, &data, true),
6968
);
7069
}
7170

7271
acc.add(
7372
AssistId("destructure_tuple_binding", AssistKind::RefactorRewrite),
7473
if with_sub_pattern { "Destructure tuple in place" } else { "Destructure tuple" },
75-
data.range,
76-
|builder| {
77-
edit_tuple_assignment(ctx, builder, &data, false);
78-
edit_tuple_usages(&data, builder, ctx, false);
79-
},
74+
data.ident_pat.syntax().text_range(),
75+
|edit| destructure_tuple_edit_impl(ctx, edit, &data, false),
8076
);
8177

8278
Some(())
8379
}
8480

81+
fn destructure_tuple_edit_impl(
82+
ctx: &AssistContext<'_>,
83+
edit: &mut SourceChangeBuilder,
84+
data: &TupleData,
85+
in_sub_pattern: bool,
86+
) {
87+
let assignment_edit = edit_tuple_assignment(ctx, edit, &data, in_sub_pattern);
88+
let current_file_usages_edit = edit_tuple_usages(&data, edit, ctx, in_sub_pattern);
89+
90+
assignment_edit.apply();
91+
if let Some(usages_edit) = current_file_usages_edit {
92+
usages_edit.into_iter().for_each(|usage_edit| usage_edit.apply(edit))
93+
}
94+
}
95+
8596
fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleData> {
8697
if ident_pat.at_token().is_some() {
8798
// Cannot destructure pattern with sub-pattern:
@@ -109,7 +120,6 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
109120
}
110121

111122
let name = ident_pat.name()?.to_string();
112-
let range = ident_pat.syntax().text_range();
113123

114124
let usages = ctx.sema.to_def(&ident_pat).map(|def| {
115125
Definition::Local(def)
@@ -122,7 +132,7 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
122132
.map(|i| generate_name(ctx, i, &name, &ident_pat, &usages))
123133
.collect::<Vec<_>>();
124134

125-
Some(TupleData { ident_pat, range, ref_type, field_names, usages })
135+
Some(TupleData { ident_pat, ref_type, field_names, usages })
126136
}
127137

128138
fn generate_name(
@@ -142,98 +152,121 @@ enum RefType {
142152
}
143153
struct TupleData {
144154
ident_pat: IdentPat,
145-
// name: String,
146-
range: TextRange,
147155
ref_type: Option<RefType>,
148156
field_names: Vec<String>,
149-
// field_types: Vec<Type>,
150157
usages: Option<UsageSearchResult>,
151158
}
152159
fn edit_tuple_assignment(
153160
ctx: &AssistContext<'_>,
154-
builder: &mut SourceChangeBuilder,
161+
edit: &mut SourceChangeBuilder,
155162
data: &TupleData,
156163
in_sub_pattern: bool,
157-
) {
164+
) -> AssignmentEdit {
165+
let ident_pat = edit.make_mut(data.ident_pat.clone());
166+
158167
let tuple_pat = {
159168
let original = &data.ident_pat;
160169
let is_ref = original.ref_token().is_some();
161170
let is_mut = original.mut_token().is_some();
162-
let fields = data.field_names.iter().map(|name| {
163-
ast::Pat::from(ast::make::ident_pat(is_ref, is_mut, ast::make::name(name)))
164-
});
165-
ast::make::tuple_pat(fields)
166-
};
167-
168-
let add_cursor = |text: &str| {
169-
// place cursor on first tuple item
170-
let first_tuple = &data.field_names[0];
171-
text.replacen(first_tuple, &format!("$0{first_tuple}"), 1)
171+
let fields = data
172+
.field_names
173+
.iter()
174+
.map(|name| ast::Pat::from(make::ident_pat(is_ref, is_mut, make::name(name))));
175+
make::tuple_pat(fields).clone_for_update()
172176
};
173177

174-
// with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
175-
if in_sub_pattern {
176-
let text = format!(" @ {tuple_pat}");
177-
match ctx.config.snippet_cap {
178-
Some(cap) => {
179-
let snip = add_cursor(&text);
180-
builder.insert_snippet(cap, data.range.end(), snip);
181-
}
182-
None => builder.insert(data.range.end(), text),
183-
};
184-
} else {
185-
let text = tuple_pat.to_string();
186-
match ctx.config.snippet_cap {
187-
Some(cap) => {
188-
let snip = add_cursor(&text);
189-
builder.replace_snippet(cap, data.range, snip);
190-
}
191-
None => builder.replace(data.range, text),
178+
if let Some(cap) = ctx.config.snippet_cap {
179+
// place cursor on first tuple name
180+
let ast::Pat::IdentPat(first_pat) = tuple_pat.fields().next().unwrap() else {
181+
unreachable!()
192182
};
183+
edit.add_tabstop_before(cap, first_pat.name().unwrap())
184+
}
185+
186+
AssignmentEdit { ident_pat, tuple_pat, in_sub_pattern }
187+
}
188+
struct AssignmentEdit {
189+
ident_pat: ast::IdentPat,
190+
tuple_pat: ast::TuplePat,
191+
in_sub_pattern: bool,
192+
}
193+
194+
impl AssignmentEdit {
195+
fn apply(self) {
196+
// with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
197+
if self.in_sub_pattern {
198+
ted::insert_all_raw(
199+
ted::Position::after(self.ident_pat.syntax()),
200+
vec![
201+
make::tokens::single_space().into(),
202+
make::token(T![@]).into(),
203+
make::tokens::single_space().into(),
204+
self.tuple_pat.syntax().clone().into(),
205+
],
206+
)
207+
} else {
208+
ted::replace(self.ident_pat.syntax(), self.tuple_pat.syntax())
209+
}
193210
}
194211
}
195212

196213
fn edit_tuple_usages(
197214
data: &TupleData,
198-
builder: &mut SourceChangeBuilder,
215+
edit: &mut SourceChangeBuilder,
199216
ctx: &AssistContext<'_>,
200217
in_sub_pattern: bool,
201-
) {
218+
) -> Option<Vec<EditTupleUsage>> {
219+
let mut current_file_usages = None;
220+
202221
if let Some(usages) = data.usages.as_ref() {
203-
for (file_id, refs) in usages.iter() {
204-
builder.edit_file(*file_id);
222+
// We need to collect edits first before actually applying them
223+
// as mapping nodes to their mutable node versions requires an
224+
// unmodified syntax tree.
225+
//
226+
// We also defer editing usages in the current file first since
227+
// tree mutation in the same file breaks when `builder.edit_file`
228+
// is called
229+
230+
if let Some((_, refs)) = usages.iter().find(|(file_id, _)| **file_id == ctx.file_id()) {
231+
current_file_usages = Some(
232+
refs.iter()
233+
.filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
234+
.collect_vec(),
235+
);
236+
}
205237

206-
for r in refs {
207-
edit_tuple_usage(ctx, builder, r, data, in_sub_pattern);
238+
for (file_id, refs) in usages.iter() {
239+
if *file_id == ctx.file_id() {
240+
continue;
208241
}
242+
243+
edit.edit_file(*file_id);
244+
245+
let tuple_edits = refs
246+
.iter()
247+
.filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
248+
.collect_vec();
249+
250+
tuple_edits.into_iter().for_each(|tuple_edit| tuple_edit.apply(edit))
209251
}
210252
}
253+
254+
current_file_usages
211255
}
212256
fn edit_tuple_usage(
213257
ctx: &AssistContext<'_>,
214258
builder: &mut SourceChangeBuilder,
215259
usage: &FileReference,
216260
data: &TupleData,
217261
in_sub_pattern: bool,
218-
) {
262+
) -> Option<EditTupleUsage> {
219263
match detect_tuple_index(usage, data) {
220-
Some(index) => edit_tuple_field_usage(ctx, builder, data, index),
221-
None => {
222-
if in_sub_pattern {
223-
cov_mark::hit!(destructure_tuple_call_with_subpattern);
224-
return;
225-
}
226-
227-
// no index access -> make invalid -> requires handling by user
228-
// -> put usage in block comment
229-
//
230-
// Note: For macro invocations this might result in still valid code:
231-
// When a macro accepts the tuple as argument, as well as no arguments at all,
232-
// uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
233-
// But this is an unlikely case. Usually the resulting macro call will become erroneous.
234-
builder.insert(usage.range.start(), "/*");
235-
builder.insert(usage.range.end(), "*/");
264+
Some(index) => Some(edit_tuple_field_usage(ctx, builder, data, index)),
265+
None if in_sub_pattern => {
266+
cov_mark::hit!(destructure_tuple_call_with_subpattern);
267+
return None;
236268
}
269+
None => Some(EditTupleUsage::NoIndex(usage.range)),
237270
}
238271
}
239272

@@ -242,19 +275,47 @@ fn edit_tuple_field_usage(
242275
builder: &mut SourceChangeBuilder,
243276
data: &TupleData,
244277
index: TupleIndex,
245-
) {
278+
) -> EditTupleUsage {
246279
let field_name = &data.field_names[index.index];
280+
let field_name = make::expr_path(make::ext::ident_path(field_name));
247281

248282
if data.ref_type.is_some() {
249-
let ref_data = handle_ref_field_usage(ctx, &index.field_expr);
250-
builder.replace(ref_data.range, ref_data.format(field_name));
283+
let (replace_expr, ref_data) = handle_ref_field_usage(ctx, &index.field_expr);
284+
let replace_expr = builder.make_mut(replace_expr);
285+
EditTupleUsage::ReplaceExpr(replace_expr, ref_data.wrap_expr(field_name))
251286
} else {
252-
builder.replace(index.range, field_name);
287+
let field_expr = builder.make_mut(index.field_expr);
288+
EditTupleUsage::ReplaceExpr(field_expr.into(), field_name)
289+
}
290+
}
291+
enum EditTupleUsage {
292+
/// no index access -> make invalid -> requires handling by user
293+
/// -> put usage in block comment
294+
///
295+
/// Note: For macro invocations this might result in still valid code:
296+
/// When a macro accepts the tuple as argument, as well as no arguments at all,
297+
/// uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
298+
/// But this is an unlikely case. Usually the resulting macro call will become erroneous.
299+
NoIndex(TextRange),
300+
ReplaceExpr(ast::Expr, ast::Expr),
301+
}
302+
303+
impl EditTupleUsage {
304+
fn apply(self, edit: &mut SourceChangeBuilder) {
305+
match self {
306+
EditTupleUsage::NoIndex(range) => {
307+
edit.insert(range.start(), "/*");
308+
edit.insert(range.end(), "*/");
309+
}
310+
EditTupleUsage::ReplaceExpr(target_expr, replace_with) => {
311+
ted::replace(target_expr.syntax(), replace_with.clone_for_update().syntax())
312+
}
313+
}
253314
}
254315
}
316+
255317
struct TupleIndex {
256318
index: usize,
257-
range: TextRange,
258319
field_expr: FieldExpr,
259320
}
260321
fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIndex> {
@@ -296,7 +357,7 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
296357
return None;
297358
}
298359

299-
Some(TupleIndex { index: idx, range: field_expr.syntax().text_range(), field_expr })
360+
Some(TupleIndex { index: idx, field_expr })
300361
} else {
301362
// tuple index out of range
302363
None
@@ -307,32 +368,34 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
307368
}
308369

309370
struct RefData {
310-
range: TextRange,
311371
needs_deref: bool,
312372
needs_parentheses: bool,
313373
}
314374
impl RefData {
315-
fn format(&self, field_name: &str) -> String {
316-
match (self.needs_deref, self.needs_parentheses) {
317-
(true, true) => format!("(*{field_name})"),
318-
(true, false) => format!("*{field_name}"),
319-
(false, true) => format!("({field_name})"),
320-
(false, false) => field_name.to_string(),
375+
fn wrap_expr(&self, mut expr: ast::Expr) -> ast::Expr {
376+
if self.needs_deref {
377+
expr = make::expr_prefix(T![*], expr);
321378
}
379+
380+
if self.needs_parentheses {
381+
expr = make::expr_paren(expr);
382+
}
383+
384+
return expr;
322385
}
323386
}
324-
fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> RefData {
387+
fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> (ast::Expr, RefData) {
325388
let s = field_expr.syntax();
326-
let mut ref_data =
327-
RefData { range: s.text_range(), needs_deref: true, needs_parentheses: true };
389+
let mut ref_data = RefData { needs_deref: true, needs_parentheses: true };
390+
let mut target_node = field_expr.clone().into();
328391

329392
let parent = match s.parent().map(ast::Expr::cast) {
330393
Some(Some(parent)) => parent,
331394
Some(None) => {
332395
ref_data.needs_parentheses = false;
333-
return ref_data;
396+
return (target_node, ref_data);
334397
}
335-
None => return ref_data,
398+
None => return (target_node, ref_data),
336399
};
337400

338401
match parent {
@@ -342,7 +405,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
342405
// there might be a ref outside: `&(t.0)` -> can be removed
343406
if let Some(it) = it.syntax().parent().and_then(ast::RefExpr::cast) {
344407
ref_data.needs_deref = false;
345-
ref_data.range = it.syntax().text_range();
408+
target_node = it.into();
346409
}
347410
}
348411
ast::Expr::RefExpr(it) => {
@@ -351,8 +414,8 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
351414
ref_data.needs_parentheses = false;
352415
// might be surrounded by parens -> can be removed too
353416
match it.syntax().parent().and_then(ast::ParenExpr::cast) {
354-
Some(parent) => ref_data.range = parent.syntax().text_range(),
355-
None => ref_data.range = it.syntax().text_range(),
417+
Some(parent) => target_node = parent.into(),
418+
None => target_node = it.into(),
356419
};
357420
}
358421
// higher precedence than deref `*`
@@ -414,7 +477,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
414477
}
415478
};
416479

417-
ref_data
480+
(target_node, ref_data)
418481
}
419482

420483
#[cfg(test)]

0 commit comments

Comments
 (0)