@@ -212,7 +212,7 @@ namespace ts.textChanges {
212
212
export class ChangeTracker {
213
213
private readonly changes : Change [ ] = [ ] ;
214
214
private readonly newFiles : { readonly oldFile : SourceFile , readonly fileName : string , readonly statements : ReadonlyArray < Statement > } [ ] = [ ] ;
215
- private readonly deletedNodesInLists : true [ ] = [ ] ; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
215
+ private readonly deletedNodesInLists = new NodeSet ( ) ; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
216
216
private readonly classesWithNodesInsertedAtStart = createMap < ClassDeclaration > ( ) ; // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
217
217
218
218
public static fromContext ( context : TextChangesContext ) : ChangeTracker {
@@ -262,35 +262,15 @@ namespace ts.textChanges {
262
262
this . deleteNode ( sourceFile , node ) ;
263
263
return this ;
264
264
}
265
- const id = getNodeId ( node ) ;
266
- Debug . assert ( ! this . deletedNodesInLists [ id ] , "Deleting a node twice" ) ;
267
- this . deletedNodesInLists [ id ] = true ;
268
- if ( index !== containingList . length - 1 ) {
269
- const nextToken = getTokenAtPosition ( sourceFile , node . end , /*includeJsDocComment*/ false ) ;
270
- if ( nextToken && isSeparator ( node , nextToken ) ) {
271
- // find first non-whitespace position in the leading trivia of the node
272
- const startPosition = skipTrivia ( sourceFile . text , getAdjustedStartPosition ( sourceFile , node , { } , Position . FullStart ) , /*stopAfterLineBreak*/ false , /*stopAtComments*/ true ) ;
273
- const nextElement = containingList [ index + 1 ] ;
274
- /// find first non-whitespace position in the leading trivia of the next node
275
- const endPosition = skipTrivia ( sourceFile . text , getAdjustedStartPosition ( sourceFile , nextElement , { } , Position . FullStart ) , /*stopAfterLineBreak*/ false , /*stopAtComments*/ true ) ;
276
- // shift next node so its first non-whitespace position will be moved to the first non-whitespace position of the deleted node
277
- this . deleteRange ( sourceFile , { pos : startPosition , end : endPosition } ) ;
278
- }
279
- }
280
- else {
281
- const prev = containingList [ index - 1 ] ;
282
- if ( this . deletedNodesInLists [ getNodeId ( prev ) ] ) {
283
- const pos = skipTrivia ( sourceFile . text , getAdjustedStartPosition ( sourceFile , node , { } , Position . FullStart ) , /*stopAfterLineBreak*/ false , /*stopAtComments*/ true ) ;
284
- const end = getAdjustedEndPosition ( sourceFile , node , { } ) ;
285
- this . deleteRange ( sourceFile , { pos, end } ) ;
286
- }
287
- else {
288
- const previousToken = getTokenAtPosition ( sourceFile , containingList [ index - 1 ] . end , /*includeJsDocComment*/ false ) ;
289
- if ( previousToken && isSeparator ( node , previousToken ) ) {
290
- this . deleteNodeRange ( sourceFile , previousToken , node ) ;
291
- }
292
- }
293
- }
265
+
266
+ // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
267
+ // That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`.
268
+ Debug . assert ( ! this . deletedNodesInLists . has ( node ) , "Deleting a node twice" ) ;
269
+ this . deletedNodesInLists . add ( node ) ;
270
+ this . deleteRange ( sourceFile , {
271
+ pos : startPositionToDeleteNodeInList ( sourceFile , node ) ,
272
+ end : index === containingList . length - 1 ? getAdjustedEndPosition ( sourceFile , node , { } ) : startPositionToDeleteNodeInList ( sourceFile , containingList [ index + 1 ] ) ,
273
+ } ) ;
294
274
return this ;
295
275
}
296
276
@@ -683,6 +663,19 @@ namespace ts.textChanges {
683
663
} ) ;
684
664
}
685
665
666
+ private finishTrailingCommaAfterDeletingNodesInList ( ) {
667
+ this . deletedNodesInLists . forEach ( node => {
668
+ const sourceFile = node . getSourceFile ( ) ;
669
+ const list = formatting . SmartIndenter . getContainingList ( node , sourceFile ) ;
670
+ if ( node !== last ( list ) ) return ;
671
+
672
+ const lastNonDeletedIndex = findLastIndex ( list , n => ! this . deletedNodesInLists . has ( n ) , list . length - 2 ) ;
673
+ if ( lastNonDeletedIndex !== - 1 ) {
674
+ this . deleteRange ( sourceFile , { pos : list [ lastNonDeletedIndex ] . end , end : startPositionToDeleteNodeInList ( sourceFile , list [ lastNonDeletedIndex + 1 ] ) } ) ;
675
+ }
676
+ } ) ;
677
+ }
678
+
686
679
/**
687
680
* Note: after calling this, the TextChanges object must be discarded!
688
681
* @param validate only for tests
@@ -691,6 +684,7 @@ namespace ts.textChanges {
691
684
*/
692
685
public getChanges ( validate ?: ValidateNonFormattedText ) : FileTextChanges [ ] {
693
686
this . finishClassesWithNodesInsertedAtStart ( ) ;
687
+ this . finishTrailingCommaAfterDeletingNodesInList ( ) ;
694
688
const changes = changesToText . getTextChangesFromChanges ( this . changes , this . newLineCharacter , this . formatContext , validate ) ;
695
689
for ( const { oldFile, fileName, statements } of this . newFiles ) {
696
690
changes . push ( changesToText . newFileChanges ( oldFile , fileName , statements , this . newLineCharacter ) ) ;
@@ -703,6 +697,11 @@ namespace ts.textChanges {
703
697
}
704
698
}
705
699
700
+ // find first non-whitespace position in the leading trivia of the node
701
+ function startPositionToDeleteNodeInList ( sourceFile : SourceFile , node : Node ) : number {
702
+ return skipTrivia ( sourceFile . text , getAdjustedStartPosition ( sourceFile , node , { } , Position . FullStart ) , /*stopAfterLineBreak*/ false , /*stopAtComments*/ true ) ;
703
+ }
704
+
706
705
function getClassBraceEnds ( cls : ClassLikeDeclaration , sourceFile : SourceFile ) : [ number , number ] {
707
706
return [ findChildOfKind ( cls , SyntaxKind . OpenBraceToken , sourceFile ) . end , findChildOfKind ( cls , SyntaxKind . CloseBraceToken , sourceFile ) . end ] ;
708
707
}
0 commit comments