Skip to content

Commit 75ab60f

Browse files
author
Andy
authored
Improve ChangeTracker#deleteNodeInList (#24221)
1 parent 3800d7b commit 75ab60f

File tree

5 files changed

+59
-31
lines changed

5 files changed

+59
-31
lines changed

src/compiler/core.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,15 @@ namespace ts {
320320
return -1;
321321
}
322322

323+
export function findLastIndex<T>(array: ReadonlyArray<T>, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
324+
for (let i = startIndex === undefined ? array.length - 1 : startIndex; i >= 0; i--) {
325+
if (predicate(array[i], i)) {
326+
return i;
327+
}
328+
}
329+
return -1;
330+
}
331+
323332
/**
324333
* Returns the first truthy result of `callback`, or else fails.
325334
* This is like `forEach`, but never returns undefined.

src/services/textChanges.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ namespace ts.textChanges {
212212
export class ChangeTracker {
213213
private readonly changes: Change[] = [];
214214
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`.
216216
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
217217

218218
public static fromContext(context: TextChangesContext): ChangeTracker {
@@ -262,35 +262,15 @@ namespace ts.textChanges {
262262
this.deleteNode(sourceFile, node);
263263
return this;
264264
}
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+
});
294274
return this;
295275
}
296276

@@ -683,6 +663,19 @@ namespace ts.textChanges {
683663
});
684664
}
685665

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+
686679
/**
687680
* Note: after calling this, the TextChanges object must be discarded!
688681
* @param validate only for tests
@@ -691,6 +684,7 @@ namespace ts.textChanges {
691684
*/
692685
public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
693686
this.finishClassesWithNodesInsertedAtStart();
687+
this.finishTrailingCommaAfterDeletingNodesInList();
694688
const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
695689
for (const { oldFile, fileName, statements } of this.newFiles) {
696690
changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter));
@@ -703,6 +697,11 @@ namespace ts.textChanges {
703697
}
704698
}
705699

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+
706705
function getClassBraceEnds(cls: ClassLikeDeclaration, sourceFile: SourceFile): [number, number] {
707706
return [findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile).end, findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile).end];
708707
}

src/services/utilities.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,23 @@ namespace ts {
12801280
}
12811281
return propSymbol;
12821282
}
1283+
1284+
export class NodeSet {
1285+
private map = createMap<Node>();
1286+
1287+
add(node: Node): void {
1288+
this.map.set(String(getNodeId(node)), node);
1289+
}
1290+
has(node: Node): boolean {
1291+
return this.map.has(String(getNodeId(node)));
1292+
}
1293+
forEach(cb: (node: Node) => void): void {
1294+
this.map.forEach(cb);
1295+
}
1296+
some(pred: (node: Node) => boolean): boolean {
1297+
return forEachEntry(this.map, pred) || false;
1298+
}
1299+
}
12831300
}
12841301

12851302
// Display-part writer helpers

tests/cases/fourslash/codeFixUnusedIdentifier_all_delete.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
////function f(a, b) {
77
//// const x = 0;
88
////}
9+
////function g(a, b, c) { return a; }
910

1011
verify.codeFixAll({
1112
fixId: "unusedIdentifier_delete",
1213
fixAllDescription: "Delete all unused declarations",
1314
newFileContent:
1415
`function f() {
15-
}`,
16+
}
17+
function g(a) { return a; }`,
1618
});

tests/cases/fourslash/incompleteFunctionCallCodefix2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55

66
verify.codeFix({
77
description: "Prefix 'C' with an underscore",
8+
index: 1,
89
newFileContent: "function f(new _C(100, 3, undefined)",
910
});

0 commit comments

Comments
 (0)