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