@@ -3,10 +3,12 @@ use ide_db::{
3
3
defs:: Definition ,
4
4
search:: { FileReference , SearchScope , UsageSearchResult } ,
5
5
} ;
6
+ use itertools:: Itertools ;
6
7
use syntax:: {
7
- ast:: { self , AstNode , FieldExpr , HasName , IdentPat , MethodCallExpr } ,
8
- TextRange ,
8
+ ast:: { self , make , AstNode , FieldExpr , HasName , IdentPat , MethodCallExpr } ,
9
+ ted , T ,
9
10
} ;
11
+ use text_edit:: TextRange ;
10
12
11
13
use crate :: assist_context:: { AssistContext , Assists , SourceChangeBuilder } ;
12
14
@@ -61,27 +63,36 @@ pub(crate) fn destructure_tuple_binding_impl(
61
63
acc. add (
62
64
AssistId ( "destructure_tuple_binding_in_sub_pattern" , AssistKind :: RefactorRewrite ) ,
63
65
"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 ) ,
69
68
) ;
70
69
}
71
70
72
71
acc. add (
73
72
AssistId ( "destructure_tuple_binding" , AssistKind :: RefactorRewrite ) ,
74
73
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 ) ,
80
76
) ;
81
77
82
78
Some ( ( ) )
83
79
}
84
80
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
+
85
96
fn collect_data ( ident_pat : IdentPat , ctx : & AssistContext < ' _ > ) -> Option < TupleData > {
86
97
if ident_pat. at_token ( ) . is_some ( ) {
87
98
// Cannot destructure pattern with sub-pattern:
@@ -109,7 +120,6 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
109
120
}
110
121
111
122
let name = ident_pat. name ( ) ?. to_string ( ) ;
112
- let range = ident_pat. syntax ( ) . text_range ( ) ;
113
123
114
124
let usages = ctx. sema . to_def ( & ident_pat) . map ( |def| {
115
125
Definition :: Local ( def)
@@ -122,7 +132,7 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
122
132
. map ( |i| generate_name ( ctx, i, & name, & ident_pat, & usages) )
123
133
. collect :: < Vec < _ > > ( ) ;
124
134
125
- Some ( TupleData { ident_pat, range , ref_type, field_names, usages } )
135
+ Some ( TupleData { ident_pat, ref_type, field_names, usages } )
126
136
}
127
137
128
138
fn generate_name (
@@ -142,98 +152,121 @@ enum RefType {
142
152
}
143
153
struct TupleData {
144
154
ident_pat : IdentPat ,
145
- // name: String,
146
- range : TextRange ,
147
155
ref_type : Option < RefType > ,
148
156
field_names : Vec < String > ,
149
- // field_types: Vec<Type>,
150
157
usages : Option < UsageSearchResult > ,
151
158
}
152
159
fn edit_tuple_assignment (
153
160
ctx : & AssistContext < ' _ > ,
154
- builder : & mut SourceChangeBuilder ,
161
+ edit : & mut SourceChangeBuilder ,
155
162
data : & TupleData ,
156
163
in_sub_pattern : bool ,
157
- ) {
164
+ ) -> AssignmentEdit {
165
+ let ident_pat = edit. make_mut ( data. ident_pat . clone ( ) ) ;
166
+
158
167
let tuple_pat = {
159
168
let original = & data. ident_pat ;
160
169
let is_ref = original. ref_token ( ) . is_some ( ) ;
161
170
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 ( )
172
176
} ;
173
177
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 ! ( )
192
182
} ;
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
+ }
193
210
}
194
211
}
195
212
196
213
fn edit_tuple_usages (
197
214
data : & TupleData ,
198
- builder : & mut SourceChangeBuilder ,
215
+ edit : & mut SourceChangeBuilder ,
199
216
ctx : & AssistContext < ' _ > ,
200
217
in_sub_pattern : bool ,
201
- ) {
218
+ ) -> Option < Vec < EditTupleUsage > > {
219
+ let mut current_file_usages = None ;
220
+
202
221
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
+ }
205
237
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 ;
208
241
}
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) )
209
251
}
210
252
}
253
+
254
+ current_file_usages
211
255
}
212
256
fn edit_tuple_usage (
213
257
ctx : & AssistContext < ' _ > ,
214
258
builder : & mut SourceChangeBuilder ,
215
259
usage : & FileReference ,
216
260
data : & TupleData ,
217
261
in_sub_pattern : bool ,
218
- ) {
262
+ ) -> Option < EditTupleUsage > {
219
263
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 ;
236
268
}
269
+ None => Some ( EditTupleUsage :: NoIndex ( usage. range ) ) ,
237
270
}
238
271
}
239
272
@@ -242,19 +275,47 @@ fn edit_tuple_field_usage(
242
275
builder : & mut SourceChangeBuilder ,
243
276
data : & TupleData ,
244
277
index : TupleIndex ,
245
- ) {
278
+ ) -> EditTupleUsage {
246
279
let field_name = & data. field_names [ index. index ] ;
280
+ let field_name = make:: expr_path ( make:: ext:: ident_path ( field_name) ) ;
247
281
248
282
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) )
251
286
} 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
+ }
253
314
}
254
315
}
316
+
255
317
struct TupleIndex {
256
318
index : usize ,
257
- range : TextRange ,
258
319
field_expr : FieldExpr ,
259
320
}
260
321
fn detect_tuple_index ( usage : & FileReference , data : & TupleData ) -> Option < TupleIndex > {
@@ -296,7 +357,7 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
296
357
return None ;
297
358
}
298
359
299
- Some ( TupleIndex { index : idx, range : field_expr . syntax ( ) . text_range ( ) , field_expr } )
360
+ Some ( TupleIndex { index : idx, field_expr } )
300
361
} else {
301
362
// tuple index out of range
302
363
None
@@ -307,32 +368,34 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
307
368
}
308
369
309
370
struct RefData {
310
- range : TextRange ,
311
371
needs_deref : bool ,
312
372
needs_parentheses : bool ,
313
373
}
314
374
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) ;
321
378
}
379
+
380
+ if self . needs_parentheses {
381
+ expr = make:: expr_paren ( expr) ;
382
+ }
383
+
384
+ return expr;
322
385
}
323
386
}
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 ) {
325
388
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 ( ) ;
328
391
329
392
let parent = match s. parent ( ) . map ( ast:: Expr :: cast) {
330
393
Some ( Some ( parent) ) => parent,
331
394
Some ( None ) => {
332
395
ref_data. needs_parentheses = false ;
333
- return ref_data;
396
+ return ( target_node , ref_data) ;
334
397
}
335
- None => return ref_data,
398
+ None => return ( target_node , ref_data) ,
336
399
} ;
337
400
338
401
match parent {
@@ -342,7 +405,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
342
405
// there might be a ref outside: `&(t.0)` -> can be removed
343
406
if let Some ( it) = it. syntax ( ) . parent ( ) . and_then ( ast:: RefExpr :: cast) {
344
407
ref_data. needs_deref = false ;
345
- ref_data . range = it. syntax ( ) . text_range ( ) ;
408
+ target_node = it. into ( ) ;
346
409
}
347
410
}
348
411
ast:: Expr :: RefExpr ( it) => {
@@ -351,8 +414,8 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
351
414
ref_data. needs_parentheses = false ;
352
415
// might be surrounded by parens -> can be removed too
353
416
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 ( ) ,
356
419
} ;
357
420
}
358
421
// higher precedence than deref `*`
@@ -414,7 +477,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
414
477
}
415
478
} ;
416
479
417
- ref_data
480
+ ( target_node , ref_data)
418
481
}
419
482
420
483
#[ cfg( test) ]
0 commit comments