@@ -18,6 +18,8 @@ import {
18
18
getLineAndCharacterOfPosition ,
19
19
getLocaleSpecificMessage ,
20
20
getNameFromPropertyName ,
21
+ getNewLineCharacter ,
22
+ getPrecedingNonSpaceCharacterPosition ,
21
23
getRefactorContextSpan ,
22
24
getRenameLocation ,
23
25
getTokenAtPosition ,
@@ -28,6 +30,7 @@ import {
28
30
isIdentifier ,
29
31
isInferTypeNode ,
30
32
isIntersectionTypeNode ,
33
+ isJSDoc ,
31
34
isJSDocTypeExpression ,
32
35
isParenthesizedTypeNode ,
33
36
isSourceFileJS ,
@@ -45,6 +48,7 @@ import {
45
48
JSDocTemplateTag ,
46
49
Node ,
47
50
nodeOverlapsWithStartEnd ,
51
+ Program ,
48
52
pushIfUnique ,
49
53
rangeContainsStartEnd ,
50
54
RefactorContext ,
@@ -53,7 +57,6 @@ import {
53
57
setTextRange ,
54
58
skipTrivia ,
55
59
SourceFile ,
56
- Statement ,
57
60
SymbolFlags ,
58
61
textChanges ,
59
62
TextRange ,
@@ -120,7 +123,7 @@ registerRefactor(refactorName, {
120
123
return emptyArray ;
121
124
} ,
122
125
getEditsForAction : function getRefactorEditsToExtractType ( context , actionName ) : RefactorEditInfo {
123
- const { file, } = context ;
126
+ const { file, program } = context ;
124
127
const info = getRangeToExtract ( context ) ;
125
128
Debug . assert ( info && ! isRefactorErrorInfo ( info ) , "Expected to find a range to extract" ) ;
126
129
@@ -132,7 +135,7 @@ registerRefactor(refactorName, {
132
135
return doTypeAliasChange ( changes , file , name , info ) ;
133
136
case extractToTypeDefAction . name :
134
137
Debug . assert ( info . isJS , "Invalid actionName/JS combo" ) ;
135
- return doTypedefChange ( changes , file , name , info ) ;
138
+ return doTypedefChange ( changes , program , file , name , info ) ;
136
139
case extractToInterfaceAction . name :
137
140
Debug . assert ( ! info . isJS && ! ! info . typeElements , "Invalid actionName/JS combo" ) ;
138
141
return doInterfaceChange ( changes , file , name , info as InterfaceInfo ) ;
@@ -148,11 +151,11 @@ registerRefactor(refactorName, {
148
151
} ) ;
149
152
150
153
interface TypeAliasInfo {
151
- isJS : boolean ; selection : TypeNode ; firstStatement : Statement ; typeParameters : readonly TypeParameterDeclaration [ ] ; typeElements ?: readonly TypeElement [ ] ;
154
+ isJS : boolean ; selection : TypeNode ; enclosingNode : Node ; typeParameters : readonly TypeParameterDeclaration [ ] ; typeElements ?: readonly TypeElement [ ] ;
152
155
}
153
156
154
157
interface InterfaceInfo {
155
- isJS : boolean ; selection : TypeNode ; firstStatement : Statement ; typeParameters : readonly TypeParameterDeclaration [ ] ; typeElements : readonly TypeElement [ ] ;
158
+ isJS : boolean ; selection : TypeNode ; enclosingNode : Node ; typeParameters : readonly TypeParameterDeclaration [ ] ; typeElements : readonly TypeElement [ ] ;
156
159
}
157
160
158
161
type ExtractInfo = TypeAliasInfo | InterfaceInfo ;
@@ -169,12 +172,14 @@ function getRangeToExtract(context: RefactorContext, considerEmptySpans = true):
169
172
if ( ! selection || ! isTypeNode ( selection ) ) return { error : getLocaleSpecificMessage ( Diagnostics . Selection_is_not_a_valid_type_node ) } ;
170
173
171
174
const checker = context . program . getTypeChecker ( ) ;
172
- const firstStatement = Debug . checkDefined ( findAncestor ( selection , isStatement ) , "Should find a statement" ) ;
173
- const typeParameters = collectTypeParameters ( checker , selection , firstStatement , file ) ;
175
+ const enclosingNode = getEnclosingNode ( selection , isJS ) ;
176
+ if ( enclosingNode === undefined ) return { error : getLocaleSpecificMessage ( Diagnostics . No_type_could_be_extracted_from_this_type_node ) } ;
177
+
178
+ const typeParameters = collectTypeParameters ( checker , selection , enclosingNode , file ) ;
174
179
if ( ! typeParameters ) return { error : getLocaleSpecificMessage ( Diagnostics . No_type_could_be_extracted_from_this_type_node ) } ;
175
180
176
181
const typeElements = flattenTypeLiteralNodeReference ( checker , selection ) ;
177
- return { isJS, selection, firstStatement , typeParameters, typeElements } ;
182
+ return { isJS, selection, enclosingNode , typeParameters, typeElements } ;
178
183
}
179
184
180
185
function flattenTypeLiteralNodeReference ( checker : TypeChecker , node : TypeNode | undefined ) : readonly TypeElement [ ] | undefined {
@@ -205,7 +210,7 @@ function rangeContainsSkipTrivia(r1: TextRange, node: Node, file: SourceFile): b
205
210
return rangeContainsStartEnd ( r1 , skipTrivia ( file . text , node . pos ) , node . end ) ;
206
211
}
207
212
208
- function collectTypeParameters ( checker : TypeChecker , selection : TypeNode , statement : Statement , file : SourceFile ) : TypeParameterDeclaration [ ] | undefined {
213
+ function collectTypeParameters ( checker : TypeChecker , selection : TypeNode , enclosingNode : Node , file : SourceFile ) : TypeParameterDeclaration [ ] | undefined {
209
214
const result : TypeParameterDeclaration [ ] = [ ] ;
210
215
return visitor ( selection ) ? undefined : result ;
211
216
@@ -222,7 +227,7 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statem
222
227
return true ;
223
228
}
224
229
225
- if ( rangeContainsSkipTrivia ( statement , decl , file ) && ! rangeContainsSkipTrivia ( selection , decl , file ) ) {
230
+ if ( rangeContainsSkipTrivia ( enclosingNode , decl , file ) && ! rangeContainsSkipTrivia ( selection , decl , file ) ) {
226
231
pushIfUnique ( result , decl ) ;
227
232
break ;
228
233
}
@@ -245,7 +250,7 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statem
245
250
else if ( isTypeQueryNode ( node ) ) {
246
251
if ( isIdentifier ( node . exprName ) ) {
247
252
const symbol = checker . resolveName ( node . exprName . text , node . exprName , SymbolFlags . Value , /* excludeGlobals */ false ) ;
248
- if ( symbol ?. valueDeclaration && rangeContainsSkipTrivia ( statement , symbol . valueDeclaration , file ) && ! rangeContainsSkipTrivia ( selection , symbol . valueDeclaration , file ) ) {
253
+ if ( symbol ?. valueDeclaration && rangeContainsSkipTrivia ( enclosingNode , symbol . valueDeclaration , file ) && ! rangeContainsSkipTrivia ( selection , symbol . valueDeclaration , file ) ) {
249
254
return true ;
250
255
}
251
256
}
@@ -265,20 +270,20 @@ function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statem
265
270
}
266
271
267
272
function doTypeAliasChange ( changes : textChanges . ChangeTracker , file : SourceFile , name : string , info : TypeAliasInfo ) {
268
- const { firstStatement , selection, typeParameters } = info ;
273
+ const { enclosingNode , selection, typeParameters } = info ;
269
274
270
275
const newTypeNode = factory . createTypeAliasDeclaration (
271
276
/* modifiers */ undefined ,
272
277
name ,
273
278
typeParameters . map ( id => factory . updateTypeParameterDeclaration ( id , id . modifiers , id . name , id . constraint , /* defaultType */ undefined ) ) ,
274
279
selection
275
280
) ;
276
- changes . insertNodeBefore ( file , firstStatement , ignoreSourceNewlines ( newTypeNode ) , /* blankLineBetween */ true ) ;
281
+ changes . insertNodeBefore ( file , enclosingNode , ignoreSourceNewlines ( newTypeNode ) , /* blankLineBetween */ true ) ;
277
282
changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /* typeArguments */ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
278
283
}
279
284
280
285
function doInterfaceChange ( changes : textChanges . ChangeTracker , file : SourceFile , name : string , info : InterfaceInfo ) {
281
- const { firstStatement , selection, typeParameters, typeElements } = info ;
286
+ const { enclosingNode , selection, typeParameters, typeElements } = info ;
282
287
283
288
const newTypeNode = factory . createInterfaceDeclaration (
284
289
/* modifiers */ undefined ,
@@ -288,12 +293,12 @@ function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile,
288
293
typeElements
289
294
) ;
290
295
setTextRange ( newTypeNode , typeElements [ 0 ] ?. parent ) ;
291
- changes . insertNodeBefore ( file , firstStatement , ignoreSourceNewlines ( newTypeNode ) , /* blankLineBetween */ true ) ;
296
+ changes . insertNodeBefore ( file , enclosingNode , ignoreSourceNewlines ( newTypeNode ) , /* blankLineBetween */ true ) ;
292
297
changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /* typeArguments */ undefined ) ) ) , { leadingTriviaOption : textChanges . LeadingTriviaOption . Exclude , trailingTriviaOption : textChanges . TrailingTriviaOption . ExcludeWhitespace } ) ;
293
298
}
294
299
295
- function doTypedefChange ( changes : textChanges . ChangeTracker , file : SourceFile , name : string , info : ExtractInfo ) {
296
- const { firstStatement , selection, typeParameters } = info ;
300
+ function doTypedefChange ( changes : textChanges . ChangeTracker , program : Program , file : SourceFile , name : string , info : ExtractInfo ) {
301
+ const { enclosingNode , selection, typeParameters } = info ;
297
302
298
303
setEmitFlags ( selection , EmitFlags . NoComments | EmitFlags . NoNestedComments ) ;
299
304
@@ -314,6 +319,20 @@ function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, n
314
319
templates . push ( template ) ;
315
320
} ) ;
316
321
317
- changes . insertNodeBefore ( file , firstStatement , factory . createJSDocComment ( /* comment */ undefined , factory . createNodeArray ( concatenate < JSDocTag > ( templates , [ node ] ) ) ) , /* blankLineBetween */ true ) ;
322
+ const jsDoc = factory . createJSDocComment ( /* comment */ undefined , factory . createNodeArray ( concatenate < JSDocTag > ( templates , [ node ] ) ) ) ;
323
+ if ( isJSDoc ( enclosingNode ) ) {
324
+ const pos = enclosingNode . getStart ( file ) ;
325
+ const newLineCharacter = getNewLineCharacter ( program . getCompilerOptions ( ) ) ;
326
+ changes . insertNodeAt ( file , enclosingNode . getStart ( file ) , jsDoc , {
327
+ suffix : newLineCharacter + newLineCharacter + file . text . slice ( getPrecedingNonSpaceCharacterPosition ( file . text , pos - 1 ) , pos )
328
+ } ) ;
329
+ }
330
+ else {
331
+ changes . insertNodeBefore ( file , enclosingNode , jsDoc , /* blankLineBetween */ true ) ;
332
+ }
318
333
changes . replaceNode ( file , selection , factory . createTypeReferenceNode ( name , typeParameters . map ( id => factory . createTypeReferenceNode ( id . name , /* typeArguments */ undefined ) ) ) ) ;
319
334
}
335
+
336
+ function getEnclosingNode ( node : Node , isJS : boolean ) {
337
+ return findAncestor ( node , isStatement ) || ( isJS ? findAncestor ( node , isJSDoc ) : undefined ) ;
338
+ }
0 commit comments