• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.textChanges {
3
4    /**
5     * Currently for simplicity we store recovered positions on the node itself.
6     * It can be changed to side-table later if we decide that current design is too invasive.
7     */
8    function getPos(n: TextRange): number {
9        const result = (n as any).__pos;
10        Debug.assert(typeof result === "number");
11        return result;
12    }
13
14    function setPos(n: TextRange, pos: number): void {
15        Debug.assert(typeof pos === "number");
16        (n as any).__pos = pos;
17    }
18
19    function getEnd(n: TextRange): number {
20        const result = (n as any).__end;
21        Debug.assert(typeof result === "number");
22        return result;
23    }
24
25    function setEnd(n: TextRange, end: number): void {
26        Debug.assert(typeof end === "number");
27        (n as any).__end = end;
28    }
29
30    export interface ConfigurableStart {
31        leadingTriviaOption?: LeadingTriviaOption;
32    }
33    export interface ConfigurableEnd {
34        trailingTriviaOption?: TrailingTriviaOption;
35    }
36
37    export enum LeadingTriviaOption {
38        /** Exclude all leading trivia (use getStart()) */
39        Exclude,
40        /** Include leading trivia and,
41         * if there are no line breaks between the node and the previous token,
42         * include all trivia between the node and the previous token
43         */
44        IncludeAll,
45        /**
46         * Include attached JSDoc comments
47         */
48        JSDoc,
49        /**
50         * Only delete trivia on the same line as getStart().
51         * Used to avoid deleting leading comments
52         */
53        StartLine,
54    }
55
56    export enum TrailingTriviaOption {
57        /** Exclude all trailing trivia (use getEnd()) */
58        Exclude,
59        /** Doesn't include whitespace, but does strip comments */
60        ExcludeWhitespace,
61        /** Include trailing trivia */
62        Include,
63    }
64
65    function skipWhitespacesAndLineBreaks(text: string, start: number) {
66        return skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
67    }
68
69    function hasCommentsBeforeLineBreak(text: string, start: number) {
70        let i = start;
71        while (i < text.length) {
72            const ch = text.charCodeAt(i);
73            if (isWhiteSpaceSingleLine(ch)) {
74                i++;
75                continue;
76            }
77            return ch === CharacterCodes.slash;
78        }
79        return false;
80    }
81
82    /**
83     * Usually node.pos points to a position immediately after the previous token.
84     * If this position is used as a beginning of the span to remove - it might lead to removing the trailing trivia of the previous node, i.e:
85     * const x; // this is x
86     *        ^ - pos for the next variable declaration will point here
87     * const y; // this is y
88     *        ^ - end for previous variable declaration
89     * Usually leading trivia of the variable declaration 'y' should not include trailing trivia (whitespace, comment 'this is x' and newline) from the preceding
90     * variable declaration and trailing trivia for 'y' should include (whitespace, comment 'this is y', newline).
91     * By default when removing nodes we adjust start and end positions to respect specification of the trivia above.
92     * If pos\end should be interpreted literally (that is, withouth including leading and trailing trivia), `leadingTriviaOption` should be set to `LeadingTriviaOption.Exclude`
93     * and `trailingTriviaOption` to `TrailingTriviaOption.Exclude`.
94     */
95    export interface ConfigurableStartEnd extends ConfigurableStart, ConfigurableEnd {}
96
97    const useNonAdjustedPositions: ConfigurableStartEnd = {
98        leadingTriviaOption: LeadingTriviaOption.Exclude,
99        trailingTriviaOption: TrailingTriviaOption.Exclude,
100    };
101
102    export interface InsertNodeOptions {
103        /**
104         * Text to be inserted before the new node
105         */
106        prefix?: string;
107        /**
108         * Text to be inserted after the new node
109         */
110        suffix?: string;
111        /**
112         * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node
113         */
114        indentation?: number;
115        /**
116         * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind
117         */
118        delta?: number;
119    }
120
121    export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions {
122        readonly joiner?: string;
123    }
124
125    enum ChangeKind {
126        Remove,
127        ReplaceWithSingleNode,
128        ReplaceWithMultipleNodes,
129        Text,
130    }
131
132    type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText;
133
134    interface BaseChange {
135        readonly sourceFile: SourceFile;
136        readonly range: TextRange;
137    }
138
139    export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions {}
140    interface ReplaceWithSingleNode extends BaseChange {
141        readonly kind: ChangeKind.ReplaceWithSingleNode;
142        readonly node: Node;
143        readonly options?: InsertNodeOptions;
144    }
145
146    interface RemoveNode extends BaseChange {
147        readonly kind: ChangeKind.Remove;
148        readonly node?: never;
149        readonly options?: never;
150    }
151
152    interface ReplaceWithMultipleNodes extends BaseChange {
153        readonly kind: ChangeKind.ReplaceWithMultipleNodes;
154        readonly nodes: readonly Node[];
155        readonly options?: ReplaceWithMultipleNodesOptions;
156    }
157
158    interface ChangeText extends BaseChange {
159        readonly kind: ChangeKind.Text;
160        readonly text: string;
161    }
162
163    function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange {
164        return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) };
165    }
166
167    function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd, hasTrailingComment = false) {
168        const { leadingTriviaOption } = options;
169        if (leadingTriviaOption === LeadingTriviaOption.Exclude) {
170            return node.getStart(sourceFile);
171        }
172        if (leadingTriviaOption === LeadingTriviaOption.StartLine) {
173            const startPos = node.getStart(sourceFile);
174            const pos = getLineStartPositionForPosition(startPos, sourceFile);
175            return rangeContainsPosition(node, pos) ? pos : startPos;
176        }
177        if (leadingTriviaOption === LeadingTriviaOption.JSDoc) {
178            const JSDocComments = getJSDocCommentRanges(node, sourceFile.text);
179            if (JSDocComments?.length) {
180                return getLineStartPositionForPosition(JSDocComments[0].pos, sourceFile);
181            }
182        }
183        const fullStart = node.getFullStart();
184        const start = node.getStart(sourceFile);
185        if (fullStart === start) {
186            return start;
187        }
188        const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile);
189        const startLine = getLineStartPositionForPosition(start, sourceFile);
190        if (startLine === fullStartLine) {
191            // full start and start of the node are on the same line
192            //   a,     b;
193            //    ^     ^
194            //    |   start
195            // fullstart
196            // when b is replaced - we usually want to keep the leading trvia
197            // when b is deleted - we delete it
198            return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start;
199        }
200
201        // if node has a trailing comments, use comment end position as the text has already been included.
202        if (hasTrailingComment) {
203            // Check first for leading comments as if the node is the first import, we want to exclude the trivia;
204            // otherwise we get the trailing comments.
205            const comment = getLeadingCommentRanges(sourceFile.text, fullStart)?.[0] || getTrailingCommentRanges(sourceFile.text, fullStart)?.[0];
206            if (comment) {
207                return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
208            }
209        }
210
211        // get start position of the line following the line that contains fullstart position
212        // (but only if the fullstart isn't the very beginning of the file)
213        const nextLineStart = fullStart > 0 ? 1 : 0;
214        let adjustedStartPosition = getStartPositionOfLine(getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile);
215        // skip whitespaces/newlines
216        adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition);
217        return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile);
218    }
219
220    /** Return the end position of a multiline comment of it is on another line; otherwise returns `undefined`; */
221    function getEndPositionOfMultilineTrailingComment(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number | undefined {
222        const { end } = node;
223        const { trailingTriviaOption } = options;
224        if (trailingTriviaOption === TrailingTriviaOption.Include) {
225            // If the trailing comment is a multiline comment that extends to the next lines,
226            // return the end of the comment and track it for the next nodes to adjust.
227            const comments = getTrailingCommentRanges(sourceFile.text, end);
228            if (comments) {
229                const nodeEndLine = getLineOfLocalPosition(sourceFile, node.end);
230                for (const comment of comments) {
231                    // Single line can break the loop as trivia will only be this line.
232                    // Comments on subsequest lines are also ignored.
233                    if (comment.kind === SyntaxKind.SingleLineCommentTrivia || getLineOfLocalPosition(sourceFile, comment.pos) > nodeEndLine) {
234                        break;
235                    }
236
237                    // Get the end line of the comment and compare against the end line of the node.
238                    // If the comment end line position and the multiline comment extends to multiple lines,
239                    // then is safe to return the end position.
240                    const commentEndLine = getLineOfLocalPosition(sourceFile, comment.end);
241                    if (commentEndLine > nodeEndLine) {
242                        return skipTrivia(sourceFile.text, comment.end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
243                    }
244                }
245            }
246        }
247
248        return undefined;
249    }
250
251    function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd): number {
252        const { end } = node;
253        const { trailingTriviaOption } = options;
254        if (trailingTriviaOption === TrailingTriviaOption.Exclude) {
255            return end;
256        }
257        if (trailingTriviaOption === TrailingTriviaOption.ExcludeWhitespace) {
258            const comments = concatenate(getTrailingCommentRanges(sourceFile.text, end), getLeadingCommentRanges(sourceFile.text, end));
259            const realEnd = comments?.[comments.length - 1]?.end;
260            if (realEnd) {
261                return realEnd;
262            }
263            return end;
264        }
265
266        const multilineEndPosition = getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
267        if (multilineEndPosition) {
268            return multilineEndPosition;
269        }
270
271        const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true);
272
273        return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || isLineBreak(sourceFile.text.charCodeAt(newEnd - 1)))
274            ? newEnd
275            : end;
276    }
277
278    /**
279     * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element
280     */
281    function isSeparator(node: Node, candidate: Node | undefined): candidate is Token<SyntaxKind.CommaToken | SyntaxKind.SemicolonToken> {
282        return !!candidate && !!node.parent && (candidate.kind === SyntaxKind.CommaToken || (candidate.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression));
283    }
284
285    export interface TextChangesContext {
286        host: LanguageServiceHost;
287        formatContext: formatting.FormatContext;
288        preferences: UserPreferences;
289    }
290
291    export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature;
292
293    export type ThisTypeAnnotatable = FunctionDeclaration | FunctionExpression;
294
295    export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): containingFunction is ThisTypeAnnotatable {
296        return isFunctionExpression(containingFunction) || isFunctionDeclaration(containingFunction);
297    }
298
299    export class ChangeTracker {
300        private readonly changes: Change[] = [];
301        private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = [];
302        private readonly classesWithNodesInsertedAtStart = new Map<number, { readonly node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
303        private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];
304
305        public static fromContext(context: TextChangesContext): ChangeTracker {
306            return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext);
307        }
308
309        public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] {
310            const tracker = ChangeTracker.fromContext(context);
311            cb(tracker);
312            return tracker.getChanges();
313        }
314
315        /** Public for tests only. Other callers should use `ChangeTracker.with`. */
316        constructor(private readonly newLineCharacter: string, private readonly formatContext: formatting.FormatContext) {}
317
318        public pushRaw(sourceFile: SourceFile, change: FileTextChanges) {
319            Debug.assertEqual(sourceFile.fileName, change.fileName);
320            for (const c of change.textChanges) {
321                this.changes.push({
322                    kind: ChangeKind.Text,
323                    sourceFile,
324                    text: c.newText,
325                    range: createTextRangeFromSpan(c.span),
326                });
327            }
328        }
329
330        public deleteRange(sourceFile: SourceFile, range: TextRange): void {
331            this.changes.push({ kind: ChangeKind.Remove, sourceFile, range });
332        }
333
334        delete(sourceFile: SourceFile, node: Node | NodeArray<TypeParameterDeclaration>): void {
335            this.deletedNodes.push({ sourceFile, node });
336        }
337
338        /** Stop! Consider using `delete` instead, which has logic for deleting nodes from delimited lists. */
339        public deleteNode(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
340            this.deleteRange(sourceFile, getAdjustedRange(sourceFile, node, node, options));
341        }
342
343        public deleteNodes(sourceFile: SourceFile, nodes: readonly Node[], options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }, hasTrailingComment: boolean): void {
344            // When deleting multiple nodes we need to track if the end position is including multiline trailing comments.
345            for (const node of nodes) {
346                const pos = getAdjustedStartPosition(sourceFile, node, options, hasTrailingComment);
347                const end = getAdjustedEndPosition(sourceFile, node, options);
348
349                this.deleteRange(sourceFile, { pos, end });
350
351                hasTrailingComment = !!getEndPositionOfMultilineTrailingComment(sourceFile, node, options);
352            }
353        }
354
355        public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
356            this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) });
357        }
358
359        public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
360            const startPosition = getAdjustedStartPosition(sourceFile, startNode, options);
361            const endPosition = getAdjustedEndPosition(sourceFile, endNode, options);
362            this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
363        }
364
365        public deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
366            const startPosition = getAdjustedStartPosition(sourceFile, startNode, options);
367            const endPosition = afterEndNode === undefined ? sourceFile.text.length : getAdjustedStartPosition(sourceFile, afterEndNode, options);
368            this.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
369        }
370
371        public replaceRange(sourceFile: SourceFile, range: TextRange, newNode: Node, options: InsertNodeOptions = {}): void {
372            this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode });
373        }
374
375        public replaceNode(sourceFile: SourceFile, oldNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void {
376            this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options);
377        }
378
379        public replaceNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void {
380            this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options);
381        }
382
383        private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void {
384            this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes });
385        }
386
387        public replaceNodeWithNodes(sourceFile: SourceFile, oldNode: Node, newNodes: readonly Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void {
388            this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options);
389        }
390
391        public replaceNodeWithText(sourceFile: SourceFile, oldNode: Node, text: string): void {
392            this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text);
393        }
394
395        public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void {
396            this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options);
397        }
398
399        public nodeHasTrailingComment(sourceFile: SourceFile, oldNode: Node, configurableEnd: ConfigurableEnd = useNonAdjustedPositions): boolean {
400            return !!getEndPositionOfMultilineTrailingComment(sourceFile, oldNode, configurableEnd);
401        }
402
403        private nextCommaToken(sourceFile: SourceFile, node: Node): Node | undefined {
404            const next = findNextToken(node, node.parent, sourceFile);
405            return next && next.kind === SyntaxKind.CommaToken ? next : undefined;
406        }
407
408        public replacePropertyAssignment(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void {
409            const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter);
410            this.replaceNode(sourceFile, oldNode, newNode, { suffix });
411        }
412
413        public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}): void {
414            this.replaceRange(sourceFile, createRange(pos), newNode, options);
415        }
416
417        private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions = {}): void {
418            this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options);
419        }
420
421        public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void {
422            this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween);
423        }
424
425        public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void {
426            this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween);
427        }
428
429        private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void {
430            const pos = getInsertionPositionAtSourceFileTop(sourceFile);
431            const options = {
432                prefix: pos === 0 ? undefined : this.newLineCharacter,
433                suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""),
434            };
435            if (isArray(insert)) {
436                this.insertNodesAt(sourceFile, pos, insert, options);
437            }
438            else {
439                this.insertNodeAt(sourceFile, pos, insert, options);
440            }
441        }
442
443        public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray<ParameterDeclaration>, newParam: ParameterDeclaration): void {
444            const p0 = firstOrUndefined(parameters);
445            if (p0) {
446                this.insertNodeBefore(sourceFile, p0, newParam);
447            }
448            else {
449                this.insertNodeAt(sourceFile, parameters.pos, newParam);
450            }
451        }
452
453        public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween = false, options: ConfigurableStartEnd = {}): void {
454            this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, options), newNode, this.getOptionsForInsertNodeBefore(before, newNode, blankLineBetween));
455        }
456
457        public insertModifierAt(sourceFile: SourceFile, pos: number, modifier: SyntaxKind, options: InsertNodeOptions = {}): void {
458            this.insertNodeAt(sourceFile, pos, factory.createToken(modifier), options);
459        }
460
461        public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void {
462            return this.insertModifierAt(sourceFile, before.getStart(sourceFile), modifier, { suffix: " " });
463        }
464
465        public insertCommentBeforeLine(sourceFile: SourceFile, lineNumber: number, position: number, commentText: string): void {
466            const lineStartPosition = getStartPositionOfLine(lineNumber, sourceFile);
467            const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition);
468            // First try to see if we can put the comment on the previous line.
469            // We need to make sure that we are not in the middle of a string literal or a comment.
470            // If so, we do not want to separate the node from its comment if we can.
471            // Otherwise, add an extra new line immediately before the error span.
472            const insertAtLineStart = isValidLocationToAddComment(sourceFile, startPosition);
473            const token = getTouchingToken(sourceFile, insertAtLineStart ? startPosition : position);
474            const indent = sourceFile.text.slice(lineStartPosition, startPosition);
475            const text = `${insertAtLineStart ? "" : this.newLineCharacter}//${commentText}${this.newLineCharacter}${indent}`;
476            this.insertText(sourceFile, token.getStart(sourceFile), text);
477        }
478
479        public insertJsdocCommentBefore(sourceFile: SourceFile, node: HasJSDoc, tag: JSDoc): void {
480            const fnStart = node.getStart(sourceFile);
481            if (node.jsDoc) {
482                for (const jsdoc of node.jsDoc) {
483                    this.deleteRange(sourceFile, {
484                        pos: getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile),
485                        end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {})
486                    });
487                }
488            }
489            const startPosition = getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1);
490            const indent = sourceFile.text.slice(startPosition, fnStart);
491            this.insertNodeAt(sourceFile, fnStart, tag, { suffix: this.newLineCharacter + indent });
492        }
493
494        private createJSDocText(sourceFile: SourceFile, node: HasJSDoc) {
495            const comments = flatMap(node.jsDoc, jsDoc =>
496                isString(jsDoc.comment) ? factory.createJSDocText(jsDoc.comment) : jsDoc.comment) as JSDocComment[];
497            const jsDoc = singleOrUndefined(node.jsDoc);
498            return jsDoc && positionsAreOnSameLine(jsDoc.pos, jsDoc.end, sourceFile) && length(comments) === 0 ? undefined :
499                factory.createNodeArray(intersperse(comments, factory.createJSDocText("\n")));
500        }
501
502        public replaceJSDocComment(sourceFile: SourceFile, node: HasJSDoc, tags: readonly JSDocTag[]) {
503            this.insertJsdocCommentBefore(sourceFile, updateJSDocHost(node), factory.createJSDocComment(this.createJSDocText(sourceFile, node), factory.createNodeArray(tags)));
504        }
505
506        public addJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, newTags: readonly JSDocTag[]): void {
507            const oldTags = flatMapToMutable(parent.jsDoc, j => j.tags);
508            const unmergedNewTags = newTags.filter(newTag => !oldTags.some((tag, i) => {
509                const merged = tryMergeJsdocTags(tag, newTag);
510                if (merged) oldTags[i] = merged;
511                return !!merged;
512            }));
513            this.replaceJSDocComment(sourceFile, parent, [...oldTags, ...unmergedNewTags]);
514        }
515
516        public filterJSDocTags(sourceFile: SourceFile, parent: HasJSDoc, predicate: (tag: JSDocTag) => boolean): void {
517            this.replaceJSDocComment(sourceFile, parent, filter(flatMapToMutable(parent.jsDoc, j => j.tags), predicate));
518        }
519
520        public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string): void {
521            this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text });
522        }
523
524        public insertText(sourceFile: SourceFile, pos: number, text: string): void {
525            this.replaceRangeWithText(sourceFile, createRange(pos), text);
526        }
527
528        /** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */
529        public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean {
530            let endNode: Node | undefined;
531            if (isFunctionLike(node)) {
532                endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile);
533                if (!endNode) {
534                    if (!isArrowFunction(node)) return false; // Function missing parentheses, give up
535                    // If no `)`, is an arrow function `x => x`, so use the end of the first parameter
536                    endNode = first(node.parameters);
537                }
538            }
539            else {
540                endNode = (node.kind === SyntaxKind.VariableDeclaration ? node.exclamationToken : node.questionToken) ?? node.name;
541            }
542
543            this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " });
544            return true;
545        }
546
547        public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void {
548            const start = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1;
549            const suffix = node.parameters.length ? ", " : "";
550
551            this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix });
552        }
553
554        public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: readonly TypeParameterDeclaration[]): void {
555            // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter
556            const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile);
557            this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">", joiner: ", " });
558        }
559
560        private getOptionsForInsertNodeBefore(before: Node, inserted: Node, blankLineBetween: boolean): InsertNodeOptions {
561            if (isStatement(before) || isClassElement(before)) {
562                return { suffix: blankLineBetween ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter };
563            }
564            else if (isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2;
565                return { suffix: ", " };
566            }
567            else if (isParameter(before)) {
568                return isParameter(inserted) ? { suffix: ", " } : {};
569            }
570            else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) {
571                return { suffix: ", " };
572            }
573            else if (isImportSpecifier(before)) {
574                return { suffix: "," + (blankLineBetween ? this.newLineCharacter : " ") };
575            }
576            return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it
577        }
578
579        public insertNodeAtConstructorStart(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
580            const firstStatement = firstOrUndefined(ctr.body!.statements);
581            if (!firstStatement || !ctr.body!.multiLine) {
582                this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]);
583            }
584            else {
585                this.insertNodeBefore(sourceFile, firstStatement, newStatement);
586            }
587        }
588
589        public insertNodeAtConstructorStartAfterSuperCall(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
590            const superCallStatement = find(ctr.body!.statements, stmt => isExpressionStatement(stmt) && isSuperCall(stmt.expression));
591            if (!superCallStatement || !ctr.body!.multiLine) {
592                this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]);
593            }
594            else {
595                this.insertNodeAfter(sourceFile, superCallStatement, newStatement);
596            }
597        }
598
599        public insertNodeAtConstructorEnd(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void {
600            const lastStatement = lastOrUndefined(ctr.body!.statements);
601            if (!lastStatement || !ctr.body!.multiLine) {
602                this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]);
603            }
604            else {
605                this.insertNodeAfter(sourceFile, lastStatement, newStatement);
606            }
607        }
608
609        private replaceConstructorBody(sourceFile: SourceFile, ctr: ConstructorDeclaration, statements: readonly Statement[]): void {
610            this.replaceNode(sourceFile, ctr.body!, factory.createBlock(statements, /*multiLine*/ true));
611        }
612
613        public insertNodeAtEndOfScope(sourceFile: SourceFile, scope: Node, newNode: Node): void {
614            const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {});
615            this.insertNodeAt(sourceFile, pos, newNode, {
616                prefix: isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter,
617                suffix: this.newLineCharacter
618            });
619        }
620
621        public insertMemberAtStart(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, newElement: ClassElement | PropertySignature | MethodSignature): void {
622            this.insertNodeAtStartWorker(sourceFile, node, newElement);
623        }
624
625        public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void {
626            this.insertNodeAtStartWorker(sourceFile, obj, newElement);
627        }
628
629        private insertNodeAtStartWorker(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, newElement: ClassElement | ObjectLiteralElementLike | PropertySignature | MethodSignature): void {
630            const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node);
631            this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation));
632        }
633
634        /**
635         * Tries to guess the indentation from the existing members of a class/interface/object. All members must be on
636         * new lines and must share the same indentation.
637         */
638        private guessIndentationFromExistingMembers(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
639            let indentation: number | undefined;
640            let lastRange: TextRange = node;
641            for (const member of getMembersOrProperties(node)) {
642                if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) {
643                    // each indented member must be on a new line
644                    return undefined;
645                }
646                const memberStart = member.getStart(sourceFile);
647                const memberIndentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options);
648                if (indentation === undefined) {
649                    indentation = memberIndentation;
650                }
651                else if (memberIndentation !== indentation) {
652                    // indentation of multiple members is not consistent
653                    return undefined;
654                }
655                lastRange = member;
656            }
657            return indentation;
658        }
659
660        private computeIndentationForNewMember(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
661            const nodeStart = node.getStart(sourceFile);
662            return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options)
663                + (this.formatContext.options.indentSize ?? 4);
664        }
665
666        private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, indentation: number): InsertNodeOptions {
667            // Rules:
668            // - Always insert leading newline.
669            // - For object literals:
670            //   - Add a trailing comma if there are existing members in the node, or the source file is not a JSON file
671            //     (because trailing commas are generally illegal in a JSON file).
672            //   - Add a leading comma if the source file is not a JSON file, there are existing insertions,
673            //     and the node is empty (because we didn't add a trailing comma per the previous rule).
674            // - Only insert a trailing newline if body is single-line and there are no other insertions for the node.
675            //   NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`.
676
677            const members = getMembersOrProperties(node);
678            const isEmpty = members.length === 0;
679            const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(node), { node, sourceFile });
680            const insertTrailingComma = isObjectLiteralExpression(node) && (!isJsonSourceFile(sourceFile) || !isEmpty);
681            const insertLeadingComma = isObjectLiteralExpression(node) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
682            return {
683                indentation,
684                prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter,
685                suffix: insertTrailingComma ? "," : isInterfaceDeclaration(node) && isEmpty ? ";" : ""
686            };
687        }
688
689        public insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void {
690            const endPosition = this.insertNodeAfterWorker(sourceFile, this.nextCommaToken(sourceFile, after) || after, newNode);
691            this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after));
692        }
693
694        public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void {
695            const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode);
696            this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after));
697        }
698
699        public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray<Node>, newNode: Node): void {
700            this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " });
701        }
702
703        public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: readonly Node[]): void {
704            const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes));
705            this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after));
706        }
707
708        private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number {
709            if (needSemicolonBetween(after, newNode)) {
710                // check if previous statement ends with semicolon
711                // if not - insert semicolon to preserve the code from changing the meaning due to ASI
712                if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) {
713                    this.replaceRange(sourceFile, createRange(after.end), factory.createToken(SyntaxKind.SemicolonToken));
714                }
715            }
716            const endPosition = getAdjustedEndPosition(sourceFile, after, {});
717            return endPosition;
718        }
719
720        private getInsertNodeAfterOptions(sourceFile: SourceFile, after: Node): InsertNodeOptions {
721            const options = this.getInsertNodeAfterOptionsWorker(after);
722            return {
723                ...options,
724                prefix: after.end === sourceFile.end && isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix,
725            };
726        }
727
728        private getInsertNodeAfterOptionsWorker(node: Node): InsertNodeOptions {
729            switch (node.kind) {
730                case SyntaxKind.ClassDeclaration:
731                case SyntaxKind.StructDeclaration:
732                case SyntaxKind.ModuleDeclaration:
733                    return { prefix: this.newLineCharacter, suffix: this.newLineCharacter };
734
735                case SyntaxKind.VariableDeclaration:
736                case SyntaxKind.StringLiteral:
737                case SyntaxKind.Identifier:
738                    return { prefix: ", " };
739
740                case SyntaxKind.PropertyAssignment:
741                    return { suffix: "," + this.newLineCharacter };
742
743                case SyntaxKind.ExportKeyword:
744                    return { prefix: " " };
745
746                case SyntaxKind.Parameter:
747                    return {};
748
749                default:
750                    Debug.assert(isStatement(node) || isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it
751                    return { suffix: this.newLineCharacter };
752            }
753        }
754
755        public insertName(sourceFile: SourceFile, node: FunctionExpression | ClassExpression | ArrowFunction, name: string): void {
756            Debug.assert(!node.name);
757            if (node.kind === SyntaxKind.ArrowFunction) {
758                const arrow = findChildOfKind(node, SyntaxKind.EqualsGreaterThanToken, sourceFile)!;
759                const lparen = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile);
760                if (lparen) {
761                    // `() => {}` --> `function f() {}`
762                    this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [factory.createToken(SyntaxKind.FunctionKeyword), factory.createIdentifier(name)], { joiner: " " });
763                    deleteNode(this, sourceFile, arrow);
764                }
765                else {
766                    // `x => {}` -> `function f(x) {}`
767                    this.insertText(sourceFile, first(node.parameters).getStart(sourceFile), `function ${name}(`);
768                    // Replacing full range of arrow to get rid of the leading space -- replace ` =>` with `)`
769                    this.replaceRange(sourceFile, arrow, factory.createToken(SyntaxKind.CloseParenToken));
770                }
771
772                if (node.body.kind !== SyntaxKind.Block) {
773                    // `() => 0` => `function f() { return 0; }`
774                    this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [factory.createToken(SyntaxKind.OpenBraceToken), factory.createToken(SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " });
775                    this.insertNodesAt(sourceFile, node.body.end, [factory.createToken(SyntaxKind.SemicolonToken), factory.createToken(SyntaxKind.CloseBraceToken)], { joiner: " " });
776                }
777            }
778            else {
779                const pos = findChildOfKind(node, node.kind === SyntaxKind.FunctionExpression ? SyntaxKind.FunctionKeyword : SyntaxKind.ClassKeyword, sourceFile)!.end;
780                this.insertNodeAt(sourceFile, pos, factory.createIdentifier(name), { prefix: " " });
781            }
782        }
783
784        public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void {
785            this.insertText(sourceFile, node.getStart(sourceFile), "export ");
786        }
787
788        public insertImportSpecifierAtIndex(sourceFile: SourceFile, importSpecifier: ImportSpecifier, namedImports: NamedImports, index: number) {
789            const prevSpecifier = namedImports.elements[index - 1];
790            if (prevSpecifier) {
791                this.insertNodeInListAfter(sourceFile, prevSpecifier, importSpecifier);
792            }
793            else {
794                this.insertNodeBefore(
795                    sourceFile,
796                    namedImports.elements[0],
797                    importSpecifier,
798                    !positionsAreOnSameLine(namedImports.elements[0].getStart(), namedImports.parent.parent.getStart(), sourceFile));
799            }
800        }
801
802        /**
803         * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range,
804         * i.e. arguments in arguments lists, parameters in parameter lists etc.
805         * Note that separators are part of the node in statements and class elements.
806         */
807        public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList = formatting.SmartIndenter.getContainingList(after, sourceFile)): void {
808            if (!containingList) {
809                Debug.fail("node is not a list element");
810                return;
811            }
812            const index = indexOfNode(containingList, after);
813            if (index < 0) {
814                return;
815            }
816            const end = after.getEnd();
817            if (index !== containingList.length - 1) {
818                // any element except the last one
819                // use next sibling as an anchor
820                const nextToken = getTokenAtPosition(sourceFile, after.end);
821                if (nextToken && isSeparator(after, nextToken)) {
822                    // for list
823                    // a, b, c
824                    // create change for adding 'e' after 'a' as
825                    // - find start of next element after a (it is b)
826                    // - use next element start as start and end position in final change
827                    // - build text of change by formatting the text of node + whitespace trivia of b
828
829                    // in multiline case it will work as
830                    //   a,
831                    //   b,
832                    //   c,
833                    // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#')
834                    //   a,
835                    //   insertedtext<separator>#
836                    // ###b,
837                    //   c,
838                    const nextNode = containingList[index + 1];
839                    const startPos = skipWhitespacesAndLineBreaks(sourceFile.text, nextNode.getFullStart());
840
841                    // write separator and leading trivia of the next element as suffix
842                    const suffix = `${tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, startPos)}`;
843                    this.insertNodesAt(sourceFile, startPos, [newNode], { suffix });
844                }
845            }
846            else {
847                const afterStart = after.getStart(sourceFile);
848                const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile);
849
850                let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken | undefined;
851                let multilineList = false;
852
853                // insert element after the last element in the list that has more than one item
854                // pick the element preceding the after element to:
855                // - pick the separator
856                // - determine if list is a multiline
857                if (containingList.length === 1) {
858                    // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise
859                    // i.e. var x = 1 // this is x
860                    //     | new element will be inserted at this position
861                    separator = SyntaxKind.CommaToken;
862                }
863                else {
864                    // element has more than one element, pick separator from the list
865                    const tokenBeforeInsertPosition = findPrecedingToken(after.pos, sourceFile);
866                    separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : SyntaxKind.CommaToken;
867                    // determine if list is multiline by checking lines of after element and element that precedes it.
868                    const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[index - 1].getStart(sourceFile), sourceFile);
869                    multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition;
870                }
871                if (hasCommentsBeforeLineBreak(sourceFile.text, after.end)) {
872                    // in this case we'll always treat containing list as multiline
873                    multilineList = true;
874                }
875                if (multilineList) {
876                    // insert separator immediately following the 'after' node to preserve comments in trailing trivia
877                    this.replaceRange(sourceFile, createRange(end), factory.createToken(separator));
878                    // use the same indentation as 'after' item
879                    const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options);
880                    // insert element before the line break on the line that contains 'after' element
881                    let insertPos = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false);
882                    // find position before "\n" or "\r\n"
883                    while (insertPos !== end && isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) {
884                        insertPos--;
885                    }
886                    this.replaceRange(sourceFile, createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter });
887                }
888                else {
889                    this.replaceRange(sourceFile, createRange(end), newNode, { prefix: `${tokenToString(separator)} ` });
890                }
891            }
892        }
893
894        public parenthesizeExpression(sourceFile: SourceFile, expression: Expression) {
895            this.replaceRange(sourceFile, rangeOfNode(expression), factory.createParenthesizedExpression(expression));
896        }
897
898        private finishClassesWithNodesInsertedAtStart(): void {
899            this.classesWithNodesInsertedAtStart.forEach(({ node, sourceFile }) => {
900                const [openBraceEnd, closeBraceEnd] = getClassOrObjectBraceEnds(node, sourceFile);
901                if (openBraceEnd !== undefined && closeBraceEnd !== undefined) {
902                    const isEmpty = getMembersOrProperties(node).length === 0;
903                    const isSingleLine = positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile);
904                    if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) {
905                        // For `class C { }` remove the whitespace inside the braces.
906                        this.deleteRange(sourceFile, createRange(openBraceEnd, closeBraceEnd - 1));
907                    }
908                    if (isSingleLine) {
909                        this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter);
910                    }
911                }
912            });
913        }
914
915        private finishDeleteDeclarations(): void {
916            const deletedNodesInLists = new Set<Node>(); // Stores nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
917            for (const { sourceFile, node } of this.deletedNodes) {
918                if (!this.deletedNodes.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) {
919                    if (isArray(node)) {
920                        this.deleteRange(sourceFile, rangeOfTypeParameters(sourceFile, node));
921                    }
922                    else {
923                        deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node);
924                    }
925                }
926            }
927
928            deletedNodesInLists.forEach(node => {
929                const sourceFile = node.getSourceFile();
930                const list = formatting.SmartIndenter.getContainingList(node, sourceFile)!;
931                if (node !== last(list)) return;
932
933                const lastNonDeletedIndex = findLastIndex(list, n => !deletedNodesInLists.has(n), list.length - 2);
934                if (lastNonDeletedIndex !== -1) {
935                    this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) });
936                }
937            });
938        }
939
940        /**
941         * Note: after calling this, the TextChanges object must be discarded!
942         * @param validate only for tests
943         *    The reason we must validate as part of this method is that `getNonFormattedText` changes the node's positions,
944         *    so we can only call this once and can't get the non-formatted text separately.
945         */
946        public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
947            this.finishDeleteDeclarations();
948            this.finishClassesWithNodesInsertedAtStart();
949            const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
950            for (const { oldFile, fileName, statements } of this.newFiles) {
951                changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext));
952            }
953            return changes;
954        }
955
956        public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void {
957            this.newFiles.push({ oldFile, fileName, statements });
958        }
959    }
960
961    function updateJSDocHost(parent: HasJSDoc): HasJSDoc {
962        if (parent.kind !== SyntaxKind.ArrowFunction) {
963            return parent;
964        }
965        const jsDocNode = parent.parent.kind === SyntaxKind.PropertyDeclaration ?
966            parent.parent as HasJSDoc :
967            parent.parent.parent as HasJSDoc;
968        jsDocNode.jsDoc = parent.jsDoc;
969        jsDocNode.jsDocCache = parent.jsDocCache;
970        return jsDocNode;
971    }
972
973    function tryMergeJsdocTags(oldTag: JSDocTag, newTag: JSDocTag): JSDocTag | undefined {
974        if (oldTag.kind !== newTag.kind) {
975            return undefined;
976        }
977        switch (oldTag.kind) {
978            case SyntaxKind.JSDocParameterTag: {
979                const oldParam = oldTag as JSDocParameterTag;
980                const newParam = newTag as JSDocParameterTag;
981                return isIdentifier(oldParam.name) && isIdentifier(newParam.name) && oldParam.name.escapedText === newParam.name.escapedText
982                    ? factory.createJSDocParameterTag(/*tagName*/ undefined, newParam.name, /*isBracketed*/ false, newParam.typeExpression, newParam.isNameFirst, oldParam.comment)
983                    : undefined;
984            }
985            case SyntaxKind.JSDocReturnTag:
986                return factory.createJSDocReturnTag(/*tagName*/ undefined, (newTag as JSDocReturnTag).typeExpression, oldTag.comment);
987            case SyntaxKind.JSDocTypeTag:
988                return factory.createJSDocTypeTag(/*tagName*/ undefined, (newTag as JSDocTypeTag).typeExpression, oldTag.comment);
989        }
990    }
991
992    // find first non-whitespace position in the leading trivia of the node
993    function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number {
994        return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
995    }
996
997    function endPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node, prevNode: Node | undefined, nextNode: Node): number {
998        const end = startPositionToDeleteNodeInList(sourceFile, nextNode);
999        if (prevNode === undefined || positionsAreOnSameLine(getAdjustedEndPosition(sourceFile, node, {}), end, sourceFile)) {
1000            return end;
1001        }
1002        const token = findPrecedingToken(nextNode.getStart(sourceFile), sourceFile);
1003        if (isSeparator(node, token)) {
1004            const prevToken = findPrecedingToken(node.getStart(sourceFile), sourceFile);
1005            if (isSeparator(prevNode, prevToken)) {
1006                const pos = skipTrivia(sourceFile.text, token.getEnd(), /*stopAfterLineBreak*/ true, /*stopAtComments*/ true);
1007                if (positionsAreOnSameLine(prevToken.getStart(sourceFile), token.getStart(sourceFile), sourceFile)) {
1008                    return isLineBreak(sourceFile.text.charCodeAt(pos - 1)) ? pos - 1 : pos;
1009                }
1010                if (isLineBreak(sourceFile.text.charCodeAt(pos))) {
1011                    return pos;
1012                }
1013            }
1014        }
1015        return end;
1016    }
1017
1018    function getClassOrObjectBraceEnds(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, sourceFile: SourceFile): [number | undefined, number | undefined] {
1019        const open = findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile);
1020        const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile);
1021        return [open?.end, close?.end];
1022    }
1023    function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode): NodeArray<Node> {
1024        return isObjectLiteralExpression(node) ? node.properties : node.members;
1025    }
1026
1027    export type ValidateNonFormattedText = (node: Node, text: string) => void;
1028
1029    export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: formatting.FormatContext): string {
1030        return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext);
1031    }
1032
1033    namespace changesToText {
1034        export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] {
1035            return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => {
1036                const sourceFile = changesInFile[0].sourceFile;
1037                // order changes by start position
1038                // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa.
1039                const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end));
1040                // verify that change intervals do not overlap, except possibly at end points.
1041                for (let i = 0; i < normalized.length - 1; i++) {
1042                    Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () =>
1043                        `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`);
1044                }
1045
1046                const textChanges = mapDefined(normalized, c => {
1047                    const span = createTextSpanFromRange(c.range);
1048                    const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate);
1049
1050                    // Filter out redundant changes.
1051                    if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) {
1052                        return undefined;
1053                    }
1054
1055                    return createTextChange(span, newText);
1056                });
1057
1058                return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined;
1059            });
1060        }
1061
1062        export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
1063            const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext);
1064            return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true };
1065        }
1066
1067        export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
1068            // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this
1069            const nonFormattedText = statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter);
1070            const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind);
1071            const changes = formatting.formatDocument(sourceFile, formatContext);
1072            return applyChanges(nonFormattedText, changes) + newLineCharacter;
1073        }
1074
1075        function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string {
1076            if (change.kind === ChangeKind.Remove) {
1077                return "";
1078            }
1079            if (change.kind === ChangeKind.Text) {
1080                return change.text;
1081            }
1082
1083            const { options = {}, range: { pos } } = change;
1084            const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate);
1085            const text = change.kind === ChangeKind.ReplaceWithMultipleNodes
1086                ? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options?.joiner || newLineCharacter)
1087                : format(change.node);
1088            // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line
1089            const noIndent = (options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, "");
1090            return (options.prefix || "") + noIndent
1091                 + ((!options.suffix || endsWith(noIndent, options.suffix))
1092                    ? "" : options.suffix);
1093        }
1094
1095        /** Note: this may mutate `nodeIn`. */
1096        function getFormattedTextOfNode(nodeIn: Node, sourceFile: SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string {
1097            const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter);
1098            if (validate) validate(node, text);
1099            const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile);
1100            const initialIndentation =
1101                indentation !== undefined
1102                    ? indentation
1103                    : formatting.SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || getLineStartPositionForPosition(pos, sourceFile) === pos);
1104            if (delta === undefined) {
1105                delta = formatting.SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0;
1106            }
1107
1108            const file: SourceFileLike = {
1109                text,
1110                getLineAndCharacterOfPosition(pos) {
1111                    return getLineAndCharacterOfPosition(this, pos);
1112                }
1113            };
1114            const changes = formatting.formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions });
1115            return applyChanges(text, changes);
1116        }
1117
1118        /** Note: output node may be mutated input node. */
1119        export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } {
1120            const writer = createWriter(newLineCharacter);
1121            const newLine = getNewLineKind(newLineCharacter);
1122            createPrinter({
1123                newLine,
1124                neverAsciiEscape: true,
1125                preserveSourceNewlines: true,
1126                terminateUnterminatedLiterals: true
1127            }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer);
1128            return { text: writer.getText(), node: assignPositionsToNode(node) };
1129        }
1130    }
1131
1132    export function applyChanges(text: string, changes: readonly TextChange[]): string {
1133        for (let i = changes.length - 1; i >= 0; i--) {
1134            const { span, newText } = changes[i];
1135            text = `${text.substring(0, span.start)}${newText}${text.substring(textSpanEnd(span))}`;
1136        }
1137        return text;
1138    }
1139
1140    function isTrivia(s: string) {
1141        return skipTrivia(s, 0) === s.length;
1142    }
1143
1144    // A transformation context that won't perform parenthesization, as some parenthesization rules
1145    // are more aggressive than is strictly necessary.
1146    const textChangesTransformationContext: TransformationContext = {
1147        ...nullTransformationContext,
1148        factory: createNodeFactory(
1149            nullTransformationContext.factory.flags | NodeFactoryFlags.NoParenthesizerRules,
1150            nullTransformationContext.factory.baseFactory),
1151    };
1152
1153    export function assignPositionsToNode(node: Node): Node {
1154        const visited = visitEachChild(node, assignPositionsToNode, textChangesTransformationContext, assignPositionsToNodeArray, assignPositionsToNode);
1155        // create proxy node for non synthesized nodes
1156        const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node;
1157        setTextRangePosEnd(newNode, getPos(node), getEnd(node));
1158        return newNode;
1159    }
1160
1161    function assignPositionsToNodeArray(nodes: NodeArray<any>, visitor: Visitor, test?: (node: Node) => boolean, start?: number, count?: number) {
1162        const visited = visitNodes(nodes, visitor, test, start, count);
1163        if (!visited) {
1164            return visited;
1165        }
1166        // clone nodearray if necessary
1167        const nodeArray = visited === nodes ? factory.createNodeArray(visited.slice(0)) : visited;
1168        setTextRangePosEnd(nodeArray, getPos(nodes), getEnd(nodes));
1169        return nodeArray;
1170    }
1171
1172    interface TextChangesWriter extends EmitTextWriter, PrintHandlers {}
1173
1174    export function createWriter(newLine: string): TextChangesWriter {
1175        let lastNonTriviaPosition = 0;
1176
1177        const writer = createTextWriter(newLine);
1178        const onBeforeEmitNode: PrintHandlers["onBeforeEmitNode"] = node => {
1179            if (node) {
1180                setPos(node, lastNonTriviaPosition);
1181            }
1182        };
1183        const onAfterEmitNode: PrintHandlers["onAfterEmitNode"] = node => {
1184            if (node) {
1185                setEnd(node, lastNonTriviaPosition);
1186            }
1187        };
1188        const onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"] = nodes => {
1189            if (nodes) {
1190                setPos(nodes, lastNonTriviaPosition);
1191            }
1192        };
1193        const onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"] = nodes => {
1194            if (nodes) {
1195                setEnd(nodes, lastNonTriviaPosition);
1196            }
1197        };
1198        const onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"] = node => {
1199            if (node) {
1200                setPos(node, lastNonTriviaPosition);
1201            }
1202        };
1203        const onAfterEmitToken: PrintHandlers["onAfterEmitToken"] = node => {
1204            if (node) {
1205                setEnd(node, lastNonTriviaPosition);
1206            }
1207        };
1208
1209        function setLastNonTriviaPosition(s: string, force: boolean) {
1210            if (force || !isTrivia(s)) {
1211                lastNonTriviaPosition = writer.getTextPos();
1212                let i = 0;
1213                while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) {
1214                    i++;
1215                }
1216                // trim trailing whitespaces
1217                lastNonTriviaPosition -= i;
1218            }
1219        }
1220
1221        function write(s: string): void {
1222            writer.write(s);
1223            setLastNonTriviaPosition(s, /*force*/ false);
1224        }
1225        function writeComment(s: string): void {
1226            writer.writeComment(s);
1227        }
1228        function writeKeyword(s: string): void {
1229            writer.writeKeyword(s);
1230            setLastNonTriviaPosition(s, /*force*/ false);
1231        }
1232        function writeOperator(s: string): void {
1233            writer.writeOperator(s);
1234            setLastNonTriviaPosition(s, /*force*/ false);
1235        }
1236        function writePunctuation(s: string): void {
1237            writer.writePunctuation(s);
1238            setLastNonTriviaPosition(s, /*force*/ false);
1239        }
1240        function writeTrailingSemicolon(s: string): void {
1241            writer.writeTrailingSemicolon(s);
1242            setLastNonTriviaPosition(s, /*force*/ false);
1243        }
1244        function writeParameter(s: string): void {
1245            writer.writeParameter(s);
1246            setLastNonTriviaPosition(s, /*force*/ false);
1247        }
1248        function writeProperty(s: string): void {
1249            writer.writeProperty(s);
1250            setLastNonTriviaPosition(s, /*force*/ false);
1251        }
1252        function writeSpace(s: string): void {
1253            writer.writeSpace(s);
1254            setLastNonTriviaPosition(s, /*force*/ false);
1255        }
1256        function writeStringLiteral(s: string): void {
1257            writer.writeStringLiteral(s);
1258            setLastNonTriviaPosition(s, /*force*/ false);
1259        }
1260        function writeSymbol(s: string, sym: Symbol): void {
1261            writer.writeSymbol(s, sym);
1262            setLastNonTriviaPosition(s, /*force*/ false);
1263        }
1264        function writeLine(force?: boolean): void {
1265            writer.writeLine(force);
1266        }
1267        function increaseIndent(): void {
1268            writer.increaseIndent();
1269        }
1270        function decreaseIndent(): void {
1271            writer.decreaseIndent();
1272        }
1273        function getText(): string {
1274            return writer.getText();
1275        }
1276        function rawWrite(s: string): void {
1277            writer.rawWrite(s);
1278            setLastNonTriviaPosition(s, /*force*/ false);
1279        }
1280        function writeLiteral(s: string): void {
1281            writer.writeLiteral(s);
1282            setLastNonTriviaPosition(s, /*force*/ true);
1283        }
1284        function getTextPos(): number {
1285            return writer.getTextPos();
1286        }
1287        function getLine(): number {
1288            return writer.getLine();
1289        }
1290        function getColumn(): number {
1291            return writer.getColumn();
1292        }
1293        function getIndent(): number {
1294            return writer.getIndent();
1295        }
1296        function isAtStartOfLine(): boolean {
1297            return writer.isAtStartOfLine();
1298        }
1299        function clear(): void {
1300            writer.clear();
1301            lastNonTriviaPosition = 0;
1302        }
1303
1304        return {
1305            onBeforeEmitNode,
1306            onAfterEmitNode,
1307            onBeforeEmitNodeArray,
1308            onAfterEmitNodeArray,
1309            onBeforeEmitToken,
1310            onAfterEmitToken,
1311            write,
1312            writeComment,
1313            writeKeyword,
1314            writeOperator,
1315            writePunctuation,
1316            writeTrailingSemicolon,
1317            writeParameter,
1318            writeProperty,
1319            writeSpace,
1320            writeStringLiteral,
1321            writeSymbol,
1322            writeLine,
1323            increaseIndent,
1324            decreaseIndent,
1325            getText,
1326            rawWrite,
1327            writeLiteral,
1328            getTextPos,
1329            getLine,
1330            getColumn,
1331            getIndent,
1332            isAtStartOfLine,
1333            hasTrailingComment: () => writer.hasTrailingComment(),
1334            hasTrailingWhitespace: () => writer.hasTrailingWhitespace(),
1335            clear
1336        };
1337    }
1338
1339    function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number {
1340        let lastPrologue: PrologueDirective | undefined;
1341        for (const node of sourceFile.statements) {
1342            if (isPrologueDirective(node)) {
1343                lastPrologue = node;
1344            }
1345            else {
1346                break;
1347            }
1348        }
1349
1350        let position = 0;
1351        const text = sourceFile.text;
1352        if (lastPrologue) {
1353            position = lastPrologue.end;
1354            advancePastLineBreak();
1355            return position;
1356        }
1357
1358        const shebang = getShebang(text);
1359        if (shebang !== undefined) {
1360            position = shebang.length;
1361            advancePastLineBreak();
1362        }
1363
1364        const ranges = getLeadingCommentRanges(text, position);
1365        if (!ranges) return position;
1366
1367        // Find the first attached comment to the first node and add before it
1368        let lastComment: { range: CommentRange; pinnedOrTripleSlash: boolean; } | undefined;
1369        let firstNodeLine: number | undefined;
1370        for (const range of ranges) {
1371            if (range.kind === SyntaxKind.MultiLineCommentTrivia) {
1372                if (isPinnedComment(text, range.pos)) {
1373                    lastComment = { range, pinnedOrTripleSlash: true };
1374                    continue;
1375                }
1376            }
1377            else if (isRecognizedTripleSlashComment(text, range.pos, range.end)) {
1378                lastComment = { range, pinnedOrTripleSlash: true };
1379                continue;
1380            }
1381
1382            if (lastComment) {
1383                // Always insert after pinned or triple slash comments
1384                if (lastComment.pinnedOrTripleSlash) break;
1385
1386                // There was a blank line between the last comment and this comment.
1387                // This comment is not part of the copyright comments
1388                const commentLine = sourceFile.getLineAndCharacterOfPosition(range.pos).line;
1389                const lastCommentEndLine = sourceFile.getLineAndCharacterOfPosition(lastComment.range.end).line;
1390                if (commentLine >= lastCommentEndLine + 2) break;
1391            }
1392
1393            if (sourceFile.statements.length) {
1394                if (firstNodeLine === undefined) firstNodeLine = sourceFile.getLineAndCharacterOfPosition(sourceFile.statements[0].getStart()).line;
1395                const commentEndLine = sourceFile.getLineAndCharacterOfPosition(range.end).line;
1396                if (firstNodeLine < commentEndLine + 2) break;
1397            }
1398            lastComment = { range, pinnedOrTripleSlash: false };
1399        }
1400
1401        if (lastComment) {
1402            position = lastComment.range.end;
1403            advancePastLineBreak();
1404        }
1405        return position;
1406
1407        function advancePastLineBreak() {
1408            if (position < text.length) {
1409                const charCode = text.charCodeAt(position);
1410                if (isLineBreak(charCode)) {
1411                    position++;
1412
1413                    if (position < text.length && charCode === CharacterCodes.carriageReturn && text.charCodeAt(position) === CharacterCodes.lineFeed) {
1414                        position++;
1415                    }
1416                }
1417            }
1418        }
1419    }
1420
1421    export function isValidLocationToAddComment(sourceFile: SourceFile, position: number) {
1422        return !isInComment(sourceFile, position) && !isInString(sourceFile, position) && !isInTemplateString(sourceFile, position) && !isInJSXText(sourceFile, position);
1423    }
1424
1425    function needSemicolonBetween(a: Node, b: Node): boolean {
1426        return (isPropertySignature(a) || isPropertyDeclaration(a)) && isClassOrTypeElement(b) && b.name!.kind === SyntaxKind.ComputedPropertyName
1427            || isStatementButNotDeclaration(a) && isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[`
1428    }
1429
1430    namespace deleteDeclaration {
1431        export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: Node): void {
1432            switch (node.kind) {
1433                case SyntaxKind.Parameter: {
1434                    const oldFunction = node.parent;
1435                    if (isArrowFunction(oldFunction) &&
1436                        oldFunction.parameters.length === 1 &&
1437                        !findChildOfKind(oldFunction, SyntaxKind.OpenParenToken, sourceFile)) {
1438                        // Lambdas with exactly one parameter are special because, after removal, there
1439                        // must be an empty parameter list (i.e. `()`) and this won't necessarily be the
1440                        // case if the parameter is simply removed (e.g. in `x => 1`).
1441                        changes.replaceNodeWithText(sourceFile, node, "()");
1442                    }
1443                    else {
1444                        deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1445                    }
1446                    break;
1447                }
1448
1449                case SyntaxKind.ImportDeclaration:
1450                case SyntaxKind.ImportEqualsDeclaration:
1451                    const isFirstImport = sourceFile.imports.length && node === first(sourceFile.imports).parent || node === find(sourceFile.statements, isAnyImportSyntax);
1452                    // For first import, leave header comment in place, otherwise only delete JSDoc comments
1453                    deleteNode(changes, sourceFile, node, {
1454                        leadingTriviaOption: isFirstImport ? LeadingTriviaOption.Exclude : hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine,
1455                    });
1456                    break;
1457
1458                case SyntaxKind.BindingElement:
1459                    const pattern = (node as BindingElement).parent;
1460                    const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && node !== last(pattern.elements);
1461                    if (preserveComma) {
1462                        deleteNode(changes, sourceFile, node);
1463                    }
1464                    else {
1465                        deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1466                    }
1467                    break;
1468
1469                case SyntaxKind.VariableDeclaration:
1470                    deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as VariableDeclaration);
1471                    break;
1472
1473                case SyntaxKind.TypeParameter:
1474                    deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1475                    break;
1476
1477                case SyntaxKind.ImportSpecifier:
1478                    const namedImports = (node as ImportSpecifier).parent;
1479                    if (namedImports.elements.length === 1) {
1480                        deleteImportBinding(changes, sourceFile, namedImports);
1481                    }
1482                    else {
1483                        deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1484                    }
1485                    break;
1486
1487                case SyntaxKind.NamespaceImport:
1488                    deleteImportBinding(changes, sourceFile, node as NamespaceImport);
1489                    break;
1490
1491                case SyntaxKind.SemicolonToken:
1492                    deleteNode(changes, sourceFile, node, { trailingTriviaOption: TrailingTriviaOption.Exclude });
1493                    break;
1494
1495                case SyntaxKind.FunctionKeyword:
1496                    deleteNode(changes, sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.Exclude });
1497                    break;
1498
1499                case SyntaxKind.ClassDeclaration:
1500                case SyntaxKind.StructDeclaration:
1501                case SyntaxKind.FunctionDeclaration:
1502                    deleteNode(changes, sourceFile, node, { leadingTriviaOption: hasJSDocNodes(node) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine });
1503                    break;
1504
1505                default:
1506                    if (!node.parent) {
1507                        // a misbehaving client can reach here with the SourceFile node
1508                        deleteNode(changes, sourceFile, node);
1509                    }
1510                    else if (isImportClause(node.parent) && node.parent.name === node) {
1511                        deleteDefaultImport(changes, sourceFile, node.parent);
1512                    }
1513                    else if (isCallExpression(node.parent) && contains(node.parent.arguments, node)) {
1514                        deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1515                    }
1516                    else {
1517                        deleteNode(changes, sourceFile, node);
1518                    }
1519            }
1520        }
1521
1522        function deleteDefaultImport(changes: ChangeTracker, sourceFile: SourceFile, importClause: ImportClause): void {
1523            if (!importClause.namedBindings) {
1524                // Delete the whole import
1525                deleteNode(changes, sourceFile, importClause.parent);
1526            }
1527            else {
1528                // import |d,| * as ns from './file'
1529                const start = importClause.name!.getStart(sourceFile);
1530                const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end);
1531                if (nextToken && nextToken.kind === SyntaxKind.CommaToken) {
1532                    // shift first non-whitespace position after comma to the start position of the node
1533                    const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true);
1534                    changes.deleteRange(sourceFile, { pos: start, end });
1535                }
1536                else {
1537                    deleteNode(changes, sourceFile, importClause.name!);
1538                }
1539            }
1540        }
1541
1542        function deleteImportBinding(changes: ChangeTracker, sourceFile: SourceFile, node: NamedImportBindings): void {
1543            if (node.parent.name) {
1544                // Delete named imports while preserving the default import
1545                // import d|, * as ns| from './file'
1546                // import d|, { a }| from './file'
1547                const previousToken = Debug.checkDefined(getTokenAtPosition(sourceFile, node.pos - 1));
1548                changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end });
1549            }
1550            else {
1551                // Delete the entire import declaration
1552                // |import * as ns from './file'|
1553                // |import { a } from './file'|
1554                const importDecl = getAncestor(node, SyntaxKind.ImportDeclaration)!;
1555                deleteNode(changes, sourceFile, importDecl);
1556            }
1557        }
1558
1559        function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: VariableDeclaration): void {
1560            const { parent } = node;
1561
1562            if (parent.kind === SyntaxKind.CatchClause) {
1563                // TODO: There's currently no unused diagnostic for this, could be a suggestion
1564                changes.deleteNodeRange(sourceFile, findChildOfKind(parent, SyntaxKind.OpenParenToken, sourceFile)!, findChildOfKind(parent, SyntaxKind.CloseParenToken, sourceFile)!);
1565                return;
1566            }
1567
1568            if (parent.declarations.length !== 1) {
1569                deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
1570                return;
1571            }
1572
1573            const gp = parent.parent;
1574            switch (gp.kind) {
1575                case SyntaxKind.ForOfStatement:
1576                case SyntaxKind.ForInStatement:
1577                    changes.replaceNode(sourceFile, node, factory.createObjectLiteralExpression());
1578                    break;
1579
1580                case SyntaxKind.ForStatement:
1581                    deleteNode(changes, sourceFile, parent);
1582                    break;
1583
1584                case SyntaxKind.VariableStatement:
1585                    deleteNode(changes, sourceFile, gp, { leadingTriviaOption: hasJSDocNodes(gp) ? LeadingTriviaOption.JSDoc : LeadingTriviaOption.StartLine });
1586                    break;
1587
1588                default:
1589                    Debug.assertNever(gp);
1590            }
1591        }
1592    }
1593
1594    /** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */
1595    // Exported for tests only! (TODO: improve tests to not need this)
1596    export function deleteNode(changes: ChangeTracker, sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void {
1597        const startPosition = getAdjustedStartPosition(sourceFile, node, options);
1598        const endPosition = getAdjustedEndPosition(sourceFile, node, options);
1599        changes.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
1600    }
1601
1602    function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: Set<Node>, sourceFile: SourceFile, node: Node): void {
1603        const containingList = Debug.checkDefined(formatting.SmartIndenter.getContainingList(node, sourceFile));
1604        const index = indexOfNode(containingList, node);
1605        Debug.assert(index !== -1);
1606        if (containingList.length === 1) {
1607            deleteNode(changes, sourceFile, node);
1608            return;
1609        }
1610
1611        // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node.
1612        // That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`.
1613        Debug.assert(!deletedNodesInLists.has(node), "Deleting a node twice");
1614        deletedNodesInLists.add(node);
1615
1616        changes.deleteRange(sourceFile, {
1617            pos: startPositionToDeleteNodeInList(sourceFile, node),
1618            end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : endPositionToDeleteNodeInList(sourceFile, node, containingList[index - 1], containingList[index + 1]),
1619        });
1620    }
1621}
1622