@@ -25,6 +25,7 @@ import {
25
25
getTokenAtPosition ,
26
26
getUniqueName ,
27
27
ignoreSourceNewlines ,
28
+ isArray ,
28
29
isConditionalTypeNode ,
29
30
isFunctionLike ,
30
31
isIdentifier ,
@@ -44,6 +45,7 @@ import {
44
45
isTypePredicateNode ,
45
46
isTypeQueryNode ,
46
47
isTypeReferenceNode ,
48
+ isUnionTypeNode ,
47
49
JSDocTag ,
48
50
JSDocTemplateTag ,
49
51
Node ,
@@ -59,6 +61,7 @@ import {
59
61
SymbolFlags ,
60
62
textChanges ,
61
63
TextRange ,
64
+ toArray ,
62
65
TypeChecker ,
63
66
TypeElement ,
64
67
TypeNode ,
@@ -151,15 +154,15 @@ registerRefactor(refactorName, {
151
154
152
155
interface TypeAliasInfo {
153
156
isJS : boolean ;
154
- selection : TypeNode ;
157
+ selection : TypeNode | TypeNode [ ] ;
155
158
enclosingNode : Node ;
156
159
typeParameters : readonly TypeParameterDeclaration [ ] ;
157
160
typeElements ?: readonly TypeElement [ ] ;
158
161
}
159
162
160
163
interface InterfaceInfo {
161
164
isJS : boolean ;
162
- selection : TypeNode ;
165
+ selection : TypeNode | TypeNode [ ] ;
163
166
enclosingNode : Node ;
164
167
typeParameters : readonly TypeParameterDeclaration [ ] ;
165
168
typeElements : readonly TypeElement [ ] ;
@@ -173,29 +176,54 @@ function getRangeToExtract(context: RefactorContext, considerEmptySpans = true):
173
176
const current = getTokenAtPosition ( file , startPosition ) ;
174
177
const range = createTextRangeFromSpan ( getRefactorContextSpan ( context ) ) ;
175
178
const cursorRequest = range . pos === range . end && considerEmptySpans ;
179
+ const overlappingRange = nodeOverlapsWithStartEnd ( current , file , range . pos , range . end ) ;
176
180
177
- const selection = findAncestor ( current , node =>
181
+ const firstType = findAncestor ( current , node =>
178
182
node . parent && isTypeNode ( node ) && ! rangeContainsSkipTrivia ( range , node . parent , file ) &&
179
- ( cursorRequest || nodeOverlapsWithStartEnd ( current , file , range . pos , range . end ) ) ) ;
180
- if ( ! selection || ! isTypeNode ( selection ) ) return { error : getLocaleSpecificMessage ( Diagnostics . Selection_is_not_a_valid_type_node ) } ;
183
+ ( cursorRequest || overlappingRange ) ) ;
184
+ if ( ! firstType || ! isTypeNode ( firstType ) ) return { error : getLocaleSpecificMessage ( Diagnostics . Selection_is_not_a_valid_type_node ) } ;
181
185
182
186
const checker = context . program . getTypeChecker ( ) ;
183
- const enclosingNode = getEnclosingNode ( selection , isJS ) ;
187
+ const enclosingNode = getEnclosingNode ( firstType , isJS ) ;
184
188
if ( enclosingNode === undefined ) return { error : getLocaleSpecificMessage ( Diagnostics . No_type_could_be_extracted_from_this_type_node ) } ;
185
189
190
+ const expandedFirstType = getExpandedSelectionNode ( firstType , enclosingNode ) ;
191
+ if ( ! isTypeNode ( expandedFirstType ) ) return { error : getLocaleSpecificMessage ( Diagnostics . Selection_is_not_a_valid_type_node ) } ;
192
+
193
+ const typeList : TypeNode [ ] = [ ] ;
194
+ if ( ( isUnionTypeNode ( expandedFirstType . parent ) || isIntersectionTypeNode ( expandedFirstType . parent ) ) && range . end > firstType . end ) {
195
+ // the only extraction cases in which multiple nodes may need to be selected to capture the entire type are union and intersection types
196
+ addRange (
197
+ typeList ,
198
+ expandedFirstType . parent . types . filter ( type => {
199
+ return nodeOverlapsWithStartEnd ( type , file , range . pos , range . end ) ;
200
+ } ) ,
201
+ ) ;
202
+ }
203
+ const selection = typeList . length > 1 ? typeList : expandedFirstType ;
204
+
186
205
const typeParameters = collectTypeParameters ( checker , selection , enclosingNode , file ) ;
187
206
if ( ! typeParameters ) return { error : getLocaleSpecificMessage ( Diagnostics . No_type_could_be_extracted_from_this_type_node ) } ;
188
207
189
208
const typeElements = flattenTypeLiteralNodeReference ( checker , selection ) ;
190
209
return { isJS, selection, enclosingNode, typeParameters, typeElements } ;
191
210
}
192
211
193
- function flattenTypeLiteralNodeReference ( checker : TypeChecker , node : TypeNode | undefined ) : readonly TypeElement [ ] | undefined {
194
- if ( ! node ) return undefined ;
195
- if ( isIntersectionTypeNode ( node ) ) {
212
+ function flattenTypeLiteralNodeReference ( checker : TypeChecker , selection : TypeNode | TypeNode [ ] | undefined ) : readonly TypeElement [ ] | undefined {
213
+ if ( ! selection ) return undefined ;
214
+ if ( isArray ( selection ) ) {
215
+ const result : TypeElement [ ] = [ ] ;
216
+ for ( const type of selection ) {
217
+ const flattenedTypeMembers = flattenTypeLiteralNodeReference ( checker , type ) ;
218
+ if ( ! flattenedTypeMembers ) return undefined ;
219
+ addRange ( result , flattenedTypeMembers ) ;
220
+ }
221
+ return result ;
222
+ }
223
+ if ( isIntersectionTypeNode ( selection ) ) {
196
224
const result : TypeElement [ ] = [ ] ;
197
225
const seen = new Map < string , true > ( ) ;
198
- for ( const type of node . types ) {
226
+ for ( const type of selection . types ) {
199
227
const flattenedTypeMembers = flattenTypeLiteralNodeReference ( checker , type ) ;
200
228
if ( ! flattenedTypeMembers || ! flattenedTypeMembers . every ( type => type . name && addToSeen ( seen , getNameFromPropertyName ( type . name ) as string ) ) ) {
201
229
return undefined ;
@@ -205,22 +233,27 @@ function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode |
205
233
}
206
234
return result ;
207
235
}
208
- else if ( isParenthesizedTypeNode ( node ) ) {
209
- return flattenTypeLiteralNodeReference ( checker , node . type ) ;
236
+ else if ( isParenthesizedTypeNode ( selection ) ) {
237
+ return flattenTypeLiteralNodeReference ( checker , selection . type ) ;
210
238
}
211
- else if ( isTypeLiteralNode ( node ) ) {
212
- return node . members ;
239
+ else if ( isTypeLiteralNode ( selection ) ) {
240
+ return selection . members ;
213
241
}
214
242
return undefined ;
215
243
}
216
244
217
- function rangeContainsSkipTrivia ( r1 : TextRange , node : Node , file : SourceFile ) : boolean {
245
+ function rangeContainsSkipTrivia ( r1 : TextRange , node : TextRange , file : SourceFile ) : boolean {
218
246
return rangeContainsStartEnd ( r1 , skipTrivia ( file . text , node . pos ) , node . end ) ;
219
247
}
220
248
221
- function collectTypeParameters ( checker : TypeChecker , selection : TypeNode , enclosingNode : Node , file : SourceFile ) : TypeParameterDeclaration [ ] | undefined {
249
+ function collectTypeParameters ( checker : TypeChecker , selection : TypeNode | TypeNode [ ] , enclosingNode : Node , file : SourceFile ) : TypeParameterDeclaration [ ] | undefined {
222
250
const result : TypeParameterDeclaration [ ] = [ ] ;
223
- return visitor ( selection ) ? undefined : result ;
251
+ const selectionArray = toArray ( selection ) ;
252
+ const selectionRange = { pos : selectionArray [ 0 ] . pos , end : selectionArray [ selectionArray . length - 1 ] . end } ;
253
+ for ( const t of selectionArray ) {
254
+ if ( visitor ( t ) ) return undefined ;
255
+ }
256
+ return result ;
224
257
225
258
function visitor ( node : Node ) : true | undefined {
226
259
if ( isTypeReferenceNode ( node ) ) {
@@ -231,11 +264,11 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, enclos
231
264
if ( isTypeParameterDeclaration ( decl ) && decl . getSourceFile ( ) === file ) {
232
265
// skip extraction if the type node is in the range of the type parameter declaration.
233
266
// function foo<T extends { a?: /**/T }>(): void;
234
- if ( decl . name . escapedText === typeName . escapedText && rangeContainsSkipTrivia ( decl , selection , file ) ) {
267
+ if ( decl . name . escapedText === typeName . escapedText && rangeContainsSkipTrivia ( decl , selectionRange , file ) ) {
235
268
return true ;
236
269
}
237
270
238
- if ( rangeContainsSkipTrivia ( enclosingNode , decl , file ) && ! rangeContainsSkipTrivia ( selection , decl , file ) ) {
271
+ if ( rangeContainsSkipTrivia ( enclosingNode , decl , file ) && ! rangeContainsSkipTrivia ( selectionRange , decl , file ) ) {
239
272
pushIfUnique ( result , decl ) ;
240
273
break ;
241
274
}
@@ -245,25 +278,25 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, enclos
245
278
}
246
279
else if ( isInferTypeNode ( node ) ) {
247
280
const conditionalTypeNode = findAncestor ( node , n => isConditionalTypeNode ( n ) && rangeContainsSkipTrivia ( n . extendsType , node , file ) ) ;
248
- if ( ! conditionalTypeNode || ! rangeContainsSkipTrivia ( selection , conditionalTypeNode , file ) ) {
281
+ if ( ! conditionalTypeNode || ! rangeContainsSkipTrivia ( selectionRange , conditionalTypeNode , file ) ) {
249
282
return true ;
250
283
}
251
284
}
252
285
else if ( ( isTypePredicateNode ( node ) || isThisTypeNode ( node ) ) ) {
253
286
const functionLikeNode = findAncestor ( node . parent , isFunctionLike ) ;
254
- if ( functionLikeNode && functionLikeNode . type && rangeContainsSkipTrivia ( functionLikeNode . type , node , file ) && ! rangeContainsSkipTrivia ( selection , functionLikeNode , file ) ) {
287
+ if ( functionLikeNode && functionLikeNode . type && rangeContainsSkipTrivia ( functionLikeNode . type , node , file ) && ! rangeContainsSkipTrivia ( selectionRange , functionLikeNode , file ) ) {
255
288
return true ;
256
289
}
257
290
}
258
291
else if ( isTypeQueryNode ( node ) ) {
259
292
if ( isIdentifier ( node . exprName ) ) {
260
293
const symbol = checker . resolveName ( node . exprName . text , node . exprName , SymbolFlags . Value , /*excludeGlobals*/ false ) ;
261
- if ( symbol ?. valueDeclaration && rangeContainsSkipTrivia ( enclosingNode , symbol . valueDeclaration , file ) && ! rangeContainsSkipTrivia ( selection , symbol . valueDeclaration , file ) ) {
294
+ if ( symbol ?. valueDeclaration && rangeContainsSkipTrivia ( enclosingNode , symbol . valueDeclaration , file ) && ! rangeContainsSkipTrivia ( selectionRange , symbol . valueDeclaration , file ) ) {
262
295
return true ;
263
296
}
264
297
}
265
298
else {
266
- if ( isThisIdentifier ( node . exprName . left ) && ! rangeContainsSkipTrivia ( selection , node . parent , file ) ) {
299
+ if ( isThisIdentifier ( node . exprName . left ) && ! rangeContainsSkipTrivia ( selectionRange , node . parent , file ) ) {
267
300
return true ;
268
301
}
269
302
}
@@ -278,20 +311,20 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, enclos
278
311
}
279
312
280
313
function doTypeAliasChange ( changes : textChanges . ChangeTracker , file : SourceFile , name : string , info : TypeAliasInfo ) {
281
- const { enclosingNode, selection , typeParameters } = info ;
282
-
283
- const newTypeNode = factory . createTypeAliasDeclaration (
314
+ const { enclosingNode, typeParameters } = info ;
315
+ const { firstTypeNode , lastTypeNode , newTypeNode } = getNodesToEdit ( info ) ;
316
+ const newTypeDeclaration = factory . createTypeAliasDeclaration (
284
317
/*modifiers*/ undefined ,
285
318
name ,
286
319
typeParameters . map ( id => factory . updateTypeParameterDeclaration ( id , id . modifiers , id . name , id . constraint , /*defaultType*/ undefined ) ) ,
287
- selection ,
320
+ newTypeNode ,
288
321
) ;
289
- changes . insertNodeBefore ( file , enclosingNode , ignoreSourceNewlines ( newTypeNode ) , /*blankLineBetween*/ true ) ;
290
- changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
322
+ changes . insertNodeBefore ( file , enclosingNode , ignoreSourceNewlines ( newTypeDeclaration ) , /*blankLineBetween*/ true ) ;
323
+ changes . replaceNodeRange ( file , firstTypeNode , lastTypeNode , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
291
324
}
292
325
293
326
function doInterfaceChange ( changes : textChanges . ChangeTracker , file : SourceFile , name : string , info : InterfaceInfo ) {
294
- const { enclosingNode, selection , typeParameters, typeElements } = info ;
327
+ const { enclosingNode, typeParameters, typeElements } = info ;
295
328
296
329
const newTypeNode = factory . createInterfaceDeclaration (
297
330
/*modifiers*/ undefined ,
@@ -302,17 +335,21 @@ function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile,
302
335
) ;
303
336
setTextRange ( newTypeNode , typeElements [ 0 ] ?. parent ) ;
304
337
changes . insertNodeBefore ( file , enclosingNode , ignoreSourceNewlines ( newTypeNode ) , /*blankLineBetween*/ true ) ;
305
- changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
338
+
339
+ const { firstTypeNode, lastTypeNode } = getNodesToEdit ( info ) ;
340
+ changes . replaceNodeRange ( file , firstTypeNode , lastTypeNode , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
306
341
}
307
342
308
343
function doTypedefChange ( changes : textChanges . ChangeTracker , context : RefactorContext , file : SourceFile , name : string , info : ExtractInfo ) {
309
- const { enclosingNode, selection, typeParameters } = info ;
310
-
311
- setEmitFlags ( selection , EmitFlags . NoComments | EmitFlags . NoNestedComments ) ;
344
+ toArray ( info . selection ) . forEach ( typeNode => {
345
+ setEmitFlags ( typeNode , EmitFlags . NoComments | EmitFlags . NoNestedComments ) ;
346
+ } ) ;
347
+ const { enclosingNode, typeParameters } = info ;
348
+ const { firstTypeNode, lastTypeNode, newTypeNode } = getNodesToEdit ( info ) ;
312
349
313
350
const node = factory . createJSDocTypedefTag (
314
351
factory . createIdentifier ( "typedef" ) ,
315
- factory . createJSDocTypeExpression ( selection ) ,
352
+ factory . createJSDocTypeExpression ( newTypeNode ) ,
316
353
factory . createIdentifier ( name ) ,
317
354
) ;
318
355
@@ -339,9 +376,36 @@ function doTypedefChange(changes: textChanges.ChangeTracker, context: RefactorCo
339
376
else {
340
377
changes . insertNodeBefore ( file , enclosingNode , jsDoc , /*blankLineBetween*/ true ) ;
341
378
}
342
- changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) ) ;
379
+ changes . replaceNodeRange ( file , firstTypeNode , lastTypeNode , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /*typeArguments*/ undefined ) ) ) ) ;
380
+ }
381
+
382
+ function getNodesToEdit ( info : ExtractInfo ) {
383
+ if ( isArray ( info . selection ) ) {
384
+ return {
385
+ firstTypeNode : info . selection [ 0 ] ,
386
+ lastTypeNode : info . selection [ info . selection . length - 1 ] ,
387
+ newTypeNode : isUnionTypeNode ( info . selection [ 0 ] . parent ) ? factory . createUnionTypeNode ( info . selection ) : factory . createIntersectionTypeNode ( info . selection ) ,
388
+ } ;
389
+ }
390
+ return {
391
+ firstTypeNode : info . selection ,
392
+ lastTypeNode : info . selection ,
393
+ newTypeNode : info . selection ,
394
+ } ;
343
395
}
344
396
345
397
function getEnclosingNode ( node : Node , isJS : boolean ) {
346
398
return findAncestor ( node , isStatement ) || ( isJS ? findAncestor ( node , isJSDoc ) : undefined ) ;
347
399
}
400
+
401
+ function getExpandedSelectionNode ( firstType : Node , enclosingNode : Node ) {
402
+ // intended to capture the entire type in cases where the user selection is not exactly the entire type
403
+ // currently only implemented for union and intersection types
404
+ return findAncestor ( firstType , node => {
405
+ if ( node === enclosingNode ) return "quit" ;
406
+ if ( isUnionTypeNode ( node . parent ) || isIntersectionTypeNode ( node . parent ) ) {
407
+ return true ;
408
+ }
409
+ return false ;
410
+ } ) ?? firstType ;
411
+ }
0 commit comments