|
|
|
|
@@ -212,7 +212,7 @@ namespace ts.textChanges {
|
|
|
|
|
export class ChangeTracker {
|
|
|
|
|
private readonly changes: Change[] = [];
|
|
|
|
|
private readonly newFiles: { readonly oldFile: SourceFile, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
|
|
|
|
|
private readonly deletedNodesInLists: true[] = []; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
|
|
|
|
|
private readonly deletedNodesInLists = new NodeSet(); // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
|
|
|
|
|
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
|
|
|
|
|
|
|
|
|
|
public static fromContext(context: TextChangesContext): ChangeTracker {
|
|
|
|
|
@@ -262,35 +262,15 @@ namespace ts.textChanges {
|
|
|
|
|
this.deleteNode(sourceFile, node);
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
const id = getNodeId(node);
|
|
|
|
|
Debug.assert(!this.deletedNodesInLists[id], "Deleting a node twice");
|
|
|
|
|
this.deletedNodesInLists[id] = true;
|
|
|
|
|
if (index !== containingList.length - 1) {
|
|
|
|
|
const nextToken = getTokenAtPosition(sourceFile, node.end, /*includeJsDocComment*/ false);
|
|
|
|
|
if (nextToken && isSeparator(node, nextToken)) {
|
|
|
|
|
// find first non-whitespace position in the leading trivia of the node
|
|
|
|
|
const startPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
|
|
|
|
|
const nextElement = containingList[index + 1];
|
|
|
|
|
/// find first non-whitespace position in the leading trivia of the next node
|
|
|
|
|
const endPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, nextElement, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
|
|
|
|
|
// shift next node so its first non-whitespace position will be moved to the first non-whitespace position of the deleted node
|
|
|
|
|
this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
const prev = containingList[index - 1];
|
|
|
|
|
if (this.deletedNodesInLists[getNodeId(prev)]) {
|
|
|
|
|
const pos = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
|
|
|
|
|
const end = getAdjustedEndPosition(sourceFile, node, {});
|
|
|
|
|
this.deleteRange(sourceFile, { pos, end });
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
const previousToken = getTokenAtPosition(sourceFile, containingList[index - 1].end, /*includeJsDocComment*/ false);
|
|
|
|
|
if (previousToken && isSeparator(node, previousToken)) {
|
|
|
|
|
this.deleteNodeRange(sourceFile, previousToken, node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
|
|
|
|
|
// That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`.
|
|
|
|
|
Debug.assert(!this.deletedNodesInLists.has(node), "Deleting a node twice");
|
|
|
|
|
this.deletedNodesInLists.add(node);
|
|
|
|
|
this.deleteRange(sourceFile, {
|
|
|
|
|
pos: startPositionToDeleteNodeInList(sourceFile, node),
|
|
|
|
|
end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : startPositionToDeleteNodeInList(sourceFile, containingList[index + 1]),
|
|
|
|
|
});
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -683,6 +663,19 @@ namespace ts.textChanges {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private finishTrailingCommaAfterDeletingNodesInList() {
|
|
|
|
|
this.deletedNodesInLists.forEach(node => {
|
|
|
|
|
const sourceFile = node.getSourceFile();
|
|
|
|
|
const list = formatting.SmartIndenter.getContainingList(node, sourceFile);
|
|
|
|
|
if (node !== last(list)) return;
|
|
|
|
|
|
|
|
|
|
const lastNonDeletedIndex = findLastIndex(list, n => !this.deletedNodesInLists.has(n), list.length - 2);
|
|
|
|
|
if (lastNonDeletedIndex !== -1) {
|
|
|
|
|
this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Note: after calling this, the TextChanges object must be discarded!
|
|
|
|
|
* @param validate only for tests
|
|
|
|
|
@@ -691,6 +684,7 @@ namespace ts.textChanges {
|
|
|
|
|
*/
|
|
|
|
|
public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
|
|
|
|
|
this.finishClassesWithNodesInsertedAtStart();
|
|
|
|
|
this.finishTrailingCommaAfterDeletingNodesInList();
|
|
|
|
|
const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
|
|
|
|
|
for (const { oldFile, fileName, statements } of this.newFiles) {
|
|
|
|
|
changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter));
|
|
|
|
|
@@ -703,6 +697,11 @@ namespace ts.textChanges {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// find first non-whitespace position in the leading trivia of the node
|
|
|
|
|
function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number {
|
|
|
|
|
return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getClassBraceEnds(cls: ClassLikeDeclaration, sourceFile: SourceFile): [number, number] {
|
|
|
|
|
return [findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile).end, findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile).end];
|
|
|
|
|
}
|
|
|
|
|
|