• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.formatting {
3    export interface FormatContext {
4        readonly options: FormatCodeSettings;
5        readonly getRules: RulesMap;
6        readonly host: FormattingHost;
7    }
8
9    export interface TextRangeWithKind<T extends SyntaxKind = SyntaxKind> extends TextRange {
10        kind: T;
11    }
12
13    export type TextRangeWithTriviaKind = TextRangeWithKind<TriviaSyntaxKind>;
14
15    export interface TokenInfo {
16        leadingTrivia: TextRangeWithTriviaKind[] | undefined;
17        token: TextRangeWithKind;
18        trailingTrivia: TextRangeWithTriviaKind[] | undefined;
19    }
20
21    export function createTextRangeWithKind<T extends SyntaxKind>(pos: number, end: number, kind: T): TextRangeWithKind<T> {
22        const textRangeWithKind: TextRangeWithKind<T> = { pos, end, kind };
23        if (Debug.isDebugging) {
24            Object.defineProperty(textRangeWithKind, "__debugKind", {
25                get: () => Debug.formatSyntaxKind(kind),
26            });
27        }
28        return textRangeWithKind;
29    }
30
31    const enum Constants {
32        Unknown = -1
33    }
34
35    /*
36     * Indentation for the scope that can be dynamically recomputed.
37     * i.e
38     * while(true)
39     * { let x;
40     * }
41     * Normally indentation is applied only to the first token in line so at glance 'let' should not be touched.
42     * However if some format rule adds new line between '}' and 'let' 'let' will become
43     * the first token in line so it should be indented
44     */
45    interface DynamicIndentation {
46        getIndentationForToken(tokenLine: number, tokenKind: SyntaxKind, container: Node, suppressDelta: boolean): number;
47        getIndentationForComment(owningToken: SyntaxKind, tokenIndentation: number, container: Node): number;
48        /**
49         * Indentation for open and close tokens of the node if it is block or another node that needs special indentation
50         * ... {
51         * .........<child>
52         * ....}
53         *  ____ - indentation
54         *      ____ - delta
55         */
56        getIndentation(): number;
57        /**
58         * Prefered relative indentation for child nodes.
59         * Delta is used to carry the indentation info
60         * foo(bar({
61         *     $
62         * }))
63         * Both 'foo', 'bar' introduce new indentation with delta = 4, but total indentation in $ is not 8.
64         * foo: { indentation: 0, delta: 4 }
65         * bar: { indentation: foo.indentation + foo.delta = 4, delta: 4} however 'foo' and 'bar' are on the same line
66         * so bar inherits indentation from foo and bar.delta will be 4
67         *
68         */
69        getDelta(child: TextRangeWithKind): number;
70        /**
71         * Formatter calls this function when rule adds or deletes new lines from the text
72         * so indentation scope can adjust values of indentation and delta.
73         */
74        recomputeIndentation(lineAddedByFormatting: boolean, parent: Node): void;
75    }
76
77    export function formatOnEnter(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
78        const line = sourceFile.getLineAndCharacterOfPosition(position).line;
79        if (line === 0) {
80            return [];
81        }
82        // After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters.
83        // If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as
84        // trailing whitespaces. So the end of the formatting span should be the later one between:
85        //  1. the end of the previous line
86        //  2. the last non-whitespace character in the current line
87        let endOfFormatSpan = getEndLinePosition(line, sourceFile);
88        while (isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(endOfFormatSpan))) {
89            endOfFormatSpan--;
90        }
91        // if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to
92        // touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the
93        // previous character before the end of format span is line break character as well.
94        if (isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) {
95            endOfFormatSpan--;
96        }
97        const span = {
98            // get start position for the previous line
99            pos: getStartPositionOfLine(line - 1, sourceFile),
100            // end value is exclusive so add 1 to the result
101            end: endOfFormatSpan + 1
102        };
103        return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatOnEnter);
104    }
105
106    export function formatOnSemicolon(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
107        const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile);
108        return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, FormattingRequestKind.FormatOnSemicolon);
109    }
110
111    export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
112        const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile);
113        if (!openingCurly) {
114            return [];
115        }
116        const curlyBraceRange = openingCurly.parent;
117        const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange);
118
119        /**
120         * We limit the span to end at the opening curly to handle the case where
121         * the brace matched to that just typed will be incorrect after further edits.
122         * For example, we could type the opening curly for the following method
123         * body without brace-matching activated:
124         * ```
125         * class C {
126         *     foo()
127         * }
128         * ```
129         * and we wouldn't want to move the closing brace.
130         */
131        const textRange: TextRange = {
132            pos: getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), // TODO: GH#18217
133            end: position
134        };
135
136        return formatSpan(textRange, sourceFile, formatContext, FormattingRequestKind.FormatOnOpeningCurlyBrace);
137    }
138
139    export function formatOnClosingCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
140        const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile);
141        return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, FormattingRequestKind.FormatOnClosingCurlyBrace);
142    }
143
144    export function formatDocument(sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
145        const span = {
146            pos: 0,
147            end: sourceFile.text.length
148        };
149        return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatDocument);
150    }
151
152    export function formatSelection(start: number, end: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] {
153        // format from the beginning of the line
154        const span = {
155            pos: getLineStartPositionForPosition(start, sourceFile),
156            end,
157        };
158        return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatSelection);
159    }
160
161    /**
162     * Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment).
163     * @param expectedTokenKind The kind of the last token constituting the desired parent node.
164     */
165    function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined {
166        const precedingToken = findPrecedingToken(end, sourceFile);
167
168        return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ?
169            precedingToken :
170            undefined;
171    }
172
173    /**
174     * Finds the highest node enclosing `node` at the same list level as `node`
175     * and whose end does not exceed `node.end`.
176     *
177     * Consider typing the following
178     * ```
179     * let x = 1;
180     * while (true) {
181     * }
182     * ```
183     * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding
184     * variable declaration.
185     */
186    function findOutermostNodeWithinListLevel(node: Node | undefined) {
187        let current = node;
188        while (current &&
189            current.parent &&
190            current.parent.end === node!.end &&
191            !isListElement(current.parent, current)) {
192            current = current.parent;
193        }
194
195        return current;
196    }
197
198    // Returns true if node is a element in some list in parent
199    // i.e. parent is class declaration with the list of members and node is one of members.
200    function isListElement(parent: Node, node: Node): boolean {
201        switch (parent.kind) {
202            case SyntaxKind.ClassDeclaration:
203            case SyntaxKind.StructDeclaration:
204            case SyntaxKind.InterfaceDeclaration:
205                return rangeContainsRange((parent as InterfaceDeclaration).members, node);
206            case SyntaxKind.ModuleDeclaration:
207                const body = (parent as ModuleDeclaration).body;
208                return !!body && body.kind === SyntaxKind.ModuleBlock && rangeContainsRange(body.statements, node);
209            case SyntaxKind.SourceFile:
210            case SyntaxKind.Block:
211            case SyntaxKind.ModuleBlock:
212                return rangeContainsRange((parent as Block).statements, node);
213            case SyntaxKind.CatchClause:
214                return rangeContainsRange((parent as CatchClause).block.statements, node);
215        }
216
217        return false;
218    }
219
220    /** find node that fully contains given text range */
221    function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node {
222        return find(sourceFile);
223
224        function find(n: Node): Node {
225            const candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c);
226            if (candidate) {
227                const result = find(candidate);
228                if (result) {
229                    return result;
230                }
231            }
232
233            return n;
234        }
235    }
236
237    /** formatting is not applied to ranges that contain parse errors.
238     * This function will return a predicate that for a given text range will tell
239     * if there are any parse errors that overlap with the range.
240     */
241    function prepareRangeContainsErrorFunction(errors: readonly Diagnostic[], originalRange: TextRange): (r: TextRange) => boolean {
242        if (!errors.length) {
243            return rangeHasNoErrors;
244        }
245
246        // pick only errors that fall in range
247        const sorted = errors
248            .filter(d => rangeOverlapsWithStartEnd(originalRange, d.start!, d.start! + d.length!)) // TODO: GH#18217
249            .sort((e1, e2) => e1.start! - e2.start!);
250
251        if (!sorted.length) {
252            return rangeHasNoErrors;
253        }
254
255        let index = 0;
256
257        return r => {
258            // in current implementation sequence of arguments [r1, r2...] is monotonically increasing.
259            // 'index' tracks the index of the most recent error that was checked.
260            while (true) {
261                if (index >= sorted.length) {
262                    // all errors in the range were already checked -> no error in specified range
263                    return false;
264                }
265
266                const error = sorted[index];
267                if (r.end <= error.start!) {
268                    // specified range ends before the error referred by 'index' - no error in range
269                    return false;
270                }
271
272                if (startEndOverlapsWithStartEnd(r.pos, r.end, error.start!, error.start! + error.length!)) {
273                    // specified range overlaps with error range
274                    return true;
275                }
276
277                index++;
278            }
279        };
280
281        function rangeHasNoErrors(): boolean {
282            return false;
283        }
284    }
285
286    /**
287     * Start of the original range might fall inside the comment - scanner will not yield appropriate results
288     * This function will look for token that is located before the start of target range
289     * and return its end as start position for the scanner.
290     */
291    function getScanStartPosition(enclosingNode: Node, originalRange: TextRange, sourceFile: SourceFile): number {
292        const start = enclosingNode.getStart(sourceFile);
293        if (start === originalRange.pos && enclosingNode.end === originalRange.end) {
294            return start;
295        }
296
297        const precedingToken = findPrecedingToken(originalRange.pos, sourceFile);
298        if (!precedingToken) {
299            // no preceding token found - start from the beginning of enclosing node
300            return enclosingNode.pos;
301        }
302
303        // preceding token ends after the start of original range (i.e when originalRange.pos falls in the middle of literal)
304        // start from the beginning of enclosingNode to handle the entire 'originalRange'
305        if (precedingToken.end >= originalRange.pos) {
306            return enclosingNode.pos;
307        }
308
309        return precedingToken.end;
310    }
311
312    /*
313     * For cases like
314     * if (a ||
315     *     b ||$
316     *     c) {...}
317     * If we hit Enter at $ we want line '    b ||' to be indented.
318     * Formatting will be applied to the last two lines.
319     * Node that fully encloses these lines is binary expression 'a ||...'.
320     * Initial indentation for this node will be 0.
321     * Binary expressions don't introduce new indentation scopes, however it is possible
322     * that some parent node on the same line does - like if statement in this case.
323     * Note that we are considering parents only from the same line with initial node -
324     * if parent is on the different line - its delta was already contributed
325     * to the initial indentation.
326     */
327    function getOwnOrInheritedDelta(n: Node, options: FormatCodeSettings, sourceFile: SourceFile): number {
328        let previousLine = Constants.Unknown;
329        let child: Node | undefined;
330        while (n) {
331            const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line;
332            if (previousLine !== Constants.Unknown && line !== previousLine) {
333                break;
334            }
335
336            if (SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) {
337                return options.indentSize!;
338            }
339
340            previousLine = line;
341            child = n;
342            n = n.parent;
343        }
344        return 0;
345    }
346
347    export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] {
348        const range = { pos: node.pos, end: node.end };
349        return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(
350            range,
351            node,
352            initialIndentation,
353            delta,
354            scanner,
355            formatContext,
356            FormattingRequestKind.FormatSelection,
357            _ => false, // assume that node does not have any errors
358            sourceFileLike));
359    }
360
361    function formatNodeLines(node: Node | undefined, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] {
362        if (!node) {
363            return [];
364        }
365
366        const span = {
367            pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile),
368            end: node.end
369        };
370
371        return formatSpan(span, sourceFile, formatContext, requestKind);
372    }
373
374    function formatSpan(originalRange: TextRange, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] {
375        // find the smallest node that fully wraps the range and compute the initial indentation for the node
376        const enclosingNode = findEnclosingNode(originalRange, sourceFile);
377        return getFormattingScanner(
378            sourceFile.text,
379            sourceFile.languageVariant,
380            getScanStartPosition(enclosingNode, originalRange, sourceFile),
381            originalRange.end,
382            scanner => formatSpanWorker(
383                originalRange,
384                enclosingNode,
385                SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options),
386                getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile),
387                scanner,
388                formatContext,
389                requestKind,
390                prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange),
391                sourceFile));
392    }
393
394    function formatSpanWorker(
395        originalRange: TextRange,
396        enclosingNode: Node,
397        initialIndentation: number,
398        delta: number,
399        formattingScanner: FormattingScanner,
400        { options, getRules, host }: FormatContext,
401        requestKind: FormattingRequestKind,
402        rangeContainsError: (r: TextRange) => boolean,
403        sourceFile: SourceFileLike): TextChange[] {
404
405        // formatting context is used by rules provider
406        const formattingContext = new FormattingContext(sourceFile, requestKind, options);
407        let previousRangeTriviaEnd: number;
408        let previousRange: TextRangeWithKind;
409        let previousParent: Node;
410        let previousRangeStartLine: number;
411
412        let lastIndentedLine: number;
413        let indentationOnLastIndentedLine = Constants.Unknown;
414
415        const edits: TextChange[] = [];
416
417        formattingScanner.advance();
418
419        if (formattingScanner.isOnToken()) {
420            const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line;
421            let undecoratedStartLine = startLine;
422            if (hasDecorators(enclosingNode)) {
423                undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line;
424            }
425
426            processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta);
427        }
428
429        if (!formattingScanner.isOnToken()) {
430            const indentation = SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false)
431                ? initialIndentation + options.indentSize!
432                : initialIndentation;
433            const leadingTrivia = formattingScanner.getCurrentLeadingTrivia();
434            if (leadingTrivia) {
435                indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false,
436                    item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!));
437                if (options.trimTrailingWhitespace !== false) {
438                    trimTrailingWhitespacesForRemainingRange(leadingTrivia);
439                }
440            }
441        }
442
443        if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) {
444            // Formatting edits happen by looking at pairs of contiguous tokens (see `processPair`),
445            // typically inserting or deleting whitespace between them. The recursive `processNode`
446            // logic above bails out as soon as it encounters a token that is beyond the end of the
447            // range we're supposed to format (or if we reach the end of the file). But this potentially
448            // leaves out an edit that would occur *inside* the requested range but cannot be discovered
449            // without looking at one token *beyond* the end of the range: consider the line `x = { }`
450            // with a selection from the beginning of the line to the space inside the curly braces,
451            // inclusive. We would expect a format-selection would delete the space (if rules apply),
452            // but in order to do that, we need to process the pair ["{", "}"], but we stopped processing
453            // just before getting there. This block handles this trailing edit.
454            const tokenInfo =
455                formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() :
456                formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token :
457                undefined;
458
459            if (tokenInfo && tokenInfo.pos === previousRangeTriviaEnd!) {
460                // We need to check that tokenInfo and previousRange are contiguous: the `originalRange`
461                // may have ended in the middle of a token, which means we will have stopped formatting
462                // on that token, leaving `previousRange` pointing to the token before it, but already
463                // having moved the formatting scanner (where we just got `tokenInfo`) to the next token.
464                // If this happens, our supposed pair [previousRange, tokenInfo] actually straddles the
465                // token that intersects the end of the range we're supposed to format, so the pair will
466                // produce bogus edits if we try to `processPair`. Recall that the point of this logic is
467                // to perform a trailing edit at the end of the selection range: but there can be no valid
468                // edit in the middle of a token where the range ended, so if we have a non-contiguous
469                // pair here, we're already done and we can ignore it.
470                const parent = findPrecedingToken(tokenInfo.end, sourceFile, enclosingNode)?.parent || previousParent!;
471                processPair(
472                    tokenInfo,
473                    sourceFile.getLineAndCharacterOfPosition(tokenInfo.pos).line,
474                    parent,
475                    previousRange,
476                    previousRangeStartLine!,
477                    previousParent!,
478                    parent,
479                    /*dynamicIndentation*/ undefined);
480            }
481        }
482
483        return edits;
484
485        // local functions
486
487        /** Tries to compute the indentation for a list element.
488         * If list element is not in range then
489         * function will pick its actual indentation
490         * so it can be pushed downstream as inherited indentation.
491         * If list element is in the range - its indentation will be equal
492         * to inherited indentation from its predecessors.
493         */
494        function tryComputeIndentationForListItem(startPos: number,
495            endPos: number,
496            parentStartLine: number,
497            range: TextRange,
498            inheritedIndentation: number): number {
499
500            if (rangeOverlapsWithStartEnd(range, startPos, endPos) ||
501                rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) {
502
503                if (inheritedIndentation !== Constants.Unknown) {
504                    return inheritedIndentation;
505                }
506            }
507            else {
508                const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line;
509                const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile);
510                const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options);
511                if (startLine !== parentStartLine || startPos === column) {
512                    // Use the base indent size if it is greater than
513                    // the indentation of the inherited predecessor.
514                    const baseIndentSize = SmartIndenter.getBaseIndentation(options);
515                    return baseIndentSize > column ? baseIndentSize : column;
516                }
517            }
518
519            return Constants.Unknown;
520        }
521
522        function computeIndentation(
523            node: TextRangeWithKind,
524            startLine: number,
525            inheritedIndentation: number,
526            parent: Node,
527            parentDynamicIndentation: DynamicIndentation,
528            effectiveParentStartLine: number
529        ): { indentation: number, delta: number; } {
530            const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0;
531
532            if (effectiveParentStartLine === startLine) {
533                // if node is located on the same line with the parent
534                // - inherit indentation from the parent
535                // - push children if either parent of node itself has non-zero delta
536                return {
537                    indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(),
538                    delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta)
539                };
540            }
541            else if (inheritedIndentation === Constants.Unknown) {
542                if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) {
543                    // the is used for chaining methods formatting
544                    // - we need to get the indentation on last line and the delta of parent
545                    return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) };
546                }
547                else if (
548                    SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) ||
549                    SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) ||
550                    SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)
551                ) {
552                    return { indentation: parentDynamicIndentation.getIndentation(), delta };
553                }
554                else {
555                    return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta };
556                }
557            }
558            else {
559                return { indentation: inheritedIndentation, delta };
560            }
561        }
562
563        function getFirstNonDecoratorTokenOfNode(node: Node) {
564            if (canHaveModifiers(node)) {
565                const modifier = find(node.modifiers, isModifier, findIndex(node.modifiers, isDecorator));
566                if (modifier) return modifier.kind;
567            }
568
569            switch (node.kind) {
570                case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword;
571                case SyntaxKind.StructDeclaration: return SyntaxKind.StructKeyword;
572                case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword;
573                case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword;
574                case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration;
575                case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword;
576                case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword;
577                case SyntaxKind.MethodDeclaration:
578                    if ((node as MethodDeclaration).asteriskToken) {
579                        return SyntaxKind.AsteriskToken;
580                    }
581                    // falls through
582
583                case SyntaxKind.PropertyDeclaration:
584                case SyntaxKind.Parameter:
585                    const name = getNameOfDeclaration(node as Declaration);
586                    if (name) {
587                        return name.kind;
588                    }
589            }
590        }
591
592        function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation {
593            return {
594                getIndentationForComment: (kind, tokenIndentation, container) => {
595                    switch (kind) {
596                        // preceding comment to the token that closes the indentation scope inherits the indentation from the scope
597                        // ..  {
598                        //     // comment
599                        // }
600                        case SyntaxKind.CloseBraceToken:
601                        case SyntaxKind.CloseBracketToken:
602                        case SyntaxKind.CloseParenToken:
603                            return indentation + getDelta(container);
604                    }
605                    return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation;
606                },
607                // if list end token is LessThanToken '>' then its delta should be explicitly suppressed
608                // so that LessThanToken as a binary operator can still be indented.
609                // foo.then
610                //     <
611                //         number,
612                //         string,
613                //     >();
614                // vs
615                // var a = xValue
616                //     > yValue;
617                getIndentationForToken: (line, kind, container, suppressDelta) =>
618                    !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation,
619                getIndentation: () => indentation,
620                getDelta,
621                recomputeIndentation: (lineAdded, parent) => {
622                    if (SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) {
623                        indentation += lineAdded ? options.indentSize! : -options.indentSize!;
624                        delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0;
625                    }
626                }
627            };
628
629            function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean {
630                switch (kind) {
631                    // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent
632                    case SyntaxKind.OpenBraceToken:
633                    case SyntaxKind.CloseBraceToken:
634                    case SyntaxKind.CloseParenToken:
635                    case SyntaxKind.ElseKeyword:
636                    case SyntaxKind.WhileKeyword:
637                    case SyntaxKind.AtToken:
638                        return false;
639                    case SyntaxKind.SlashToken:
640                    case SyntaxKind.GreaterThanToken:
641                        switch (container.kind) {
642                            case SyntaxKind.JsxOpeningElement:
643                            case SyntaxKind.JsxClosingElement:
644                            case SyntaxKind.JsxSelfClosingElement:
645                                return false;
646                        }
647                        break;
648                    case SyntaxKind.OpenBracketToken:
649                    case SyntaxKind.CloseBracketToken:
650                        if (container.kind !== SyntaxKind.MappedType) {
651                            return false;
652                        }
653                        break;
654                }
655                // if token line equals to the line of containing node (this is a first token in the node) - use node indentation
656                return nodeStartLine !== line
657                    // if this token is the first token following the list of decorators, we do not need to indent
658                    && !(hasDecorators(node) && kind === getFirstNonDecoratorTokenOfNode(node));
659            }
660
661            function getDelta(child: TextRangeWithKind) {
662                // Delta value should be zero when the node explicitly prevents indentation of the child node
663                return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0;
664            }
665        }
666
667        function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) {
668            if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) {
669                return;
670            }
671
672            const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta);
673
674            // a useful observations when tracking context node
675            //        /
676            //      [a]
677            //   /   |   \
678            //  [b] [c] [d]
679            // node 'a' is a context node for nodes 'b', 'c', 'd'
680            // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a'
681            // this rule can be applied recursively to child nodes of 'a'.
682            //
683            // context node is set to parent node value after processing every child node
684            // context node is set to parent of the token after processing every token
685
686            let childContextNode = contextNode;
687
688            // if there are any tokens that logically belong to node and interleave child nodes
689            // such tokens will be consumed in processChildNode for the child that follows them
690            forEachChild(
691                node,
692                child => {
693                    processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false);
694                },
695                nodes => {
696                    processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation);
697                });
698
699            // proceed any tokens in the node that are located after child nodes
700            while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
701                const tokenInfo = formattingScanner.readTokenInfo(node);
702                if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) {
703                    break;
704                }
705                consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node);
706            }
707
708            function processChildNode(
709                child: Node,
710                inheritedIndentation: number,
711                parent: Node,
712                parentDynamicIndentation: DynamicIndentation,
713                parentStartLine: number,
714                undecoratedParentStartLine: number,
715                isListItem: boolean,
716                isFirstListItem?: boolean): number {
717                Debug.assert(!nodeIsSynthesized(child));
718
719                if (nodeIsMissing(child)) {
720                    return inheritedIndentation;
721                }
722
723                const childStartPos = child.getStart(sourceFile);
724
725                const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line;
726
727                let undecoratedChildStartLine = childStartLine;
728                if (hasDecorators(child)) {
729                    undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line;
730                }
731
732                // if child is a list item - try to get its indentation, only if parent is within the original range.
733                let childIndentationAmount = Constants.Unknown;
734
735                if (isListItem && rangeContainsRange(originalRange, parent)) {
736                    childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation);
737                    if (childIndentationAmount !== Constants.Unknown) {
738                        inheritedIndentation = childIndentationAmount;
739                    }
740                }
741
742                // child node is outside the target range - do not dive inside
743                if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) {
744                    if (child.end < originalRange.pos) {
745                        formattingScanner.skipToEndOf(child);
746                    }
747                    return inheritedIndentation;
748                }
749
750                if (child.getFullWidth() === 0) {
751                    return inheritedIndentation;
752                }
753
754                while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
755                    // proceed any parent tokens that are located prior to child.getStart()
756                    const tokenInfo = formattingScanner.readTokenInfo(node);
757                    if (tokenInfo.token.end > originalRange.end) {
758                        return inheritedIndentation;
759                    }
760                    if (tokenInfo.token.end > childStartPos) {
761                        if (tokenInfo.token.pos > childStartPos) {
762                            formattingScanner.skipToStartOf(child);
763                        }
764                        // stop when formatting scanner advances past the beginning of the child
765                        break;
766                    }
767
768                    consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node);
769                }
770
771                if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) {
772                    return inheritedIndentation;
773                }
774
775                if (isToken(child)) {
776                    // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules
777                    const tokenInfo = formattingScanner.readTokenInfo(child);
778                    // JSX text shouldn't affect indenting
779                    if (child.kind !== SyntaxKind.JsxText) {
780                        Debug.assert(tokenInfo.token.end === child.end, "Token end is child end");
781                        consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child);
782                        return inheritedIndentation;
783                    }
784                }
785
786                const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine;
787                const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine);
788
789                processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta);
790
791                childContextNode = node;
792
793                if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) {
794                    inheritedIndentation = childIndentation.indentation;
795                }
796
797                return inheritedIndentation;
798            }
799
800            function processChildNodes(nodes: NodeArray<Node>,
801                parent: Node,
802                parentStartLine: number,
803                parentDynamicIndentation: DynamicIndentation): void {
804                Debug.assert(isNodeArray(nodes));
805                Debug.assert(!nodeIsSynthesized(nodes));
806
807                const listStartToken = getOpenTokenForList(parent, nodes);
808
809                let listDynamicIndentation = parentDynamicIndentation;
810                let startLine = parentStartLine;
811                // node range is outside the target range - do not dive inside
812                if (!rangeOverlapsWithStartEnd(originalRange, nodes.pos, nodes.end)) {
813                    if (nodes.end < originalRange.pos) {
814                        formattingScanner.skipToEndOf(nodes);
815                    }
816                    return;
817                }
818
819                if (listStartToken !== SyntaxKind.Unknown) {
820                    // introduce a new indentation scope for lists (including list start and end tokens)
821                    while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
822                        const tokenInfo = formattingScanner.readTokenInfo(parent);
823                        if (tokenInfo.token.end > nodes.pos) {
824                            // stop when formatting scanner moves past the beginning of node list
825                            break;
826                        }
827                        else if (tokenInfo.token.kind === listStartToken) {
828                            // consume list start token
829                            startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line;
830
831                            consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent);
832
833                            let indentationOnListStartToken: number;
834                            if (indentationOnLastIndentedLine !== Constants.Unknown) {
835                                // scanner just processed list start token so consider last indentation as list indentation
836                                // function foo(): { // last indentation was 0, list item will be indented based on this value
837                                //   foo: number;
838                                // }: {};
839                                indentationOnListStartToken = indentationOnLastIndentedLine;
840                            }
841                            else {
842                                const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile);
843                                indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options);
844                            }
845
846                            listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217
847                        }
848                        else {
849                            // consume any tokens that precede the list as child elements of 'node' using its indentation scope
850                            consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent);
851                        }
852                    }
853                }
854
855                let inheritedIndentation = Constants.Unknown;
856                for (let i = 0; i < nodes.length; i++) {
857                    const child = nodes[i];
858                    inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0);
859                }
860
861                const listEndToken = getCloseTokenForOpenToken(listStartToken);
862                if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
863                    let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent);
864                    if (tokenInfo.token.kind === SyntaxKind.CommaToken) {
865                        // consume the comma
866                        consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent);
867                        tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined;
868                    }
869
870                    // consume the list end token only if it is still belong to the parent
871                    // there might be the case when current token matches end token but does not considered as one
872                    // function (x: function) <--
873                    // without this check close paren will be interpreted as list end token for function expression which is wrong
874                    if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) {
875                        // consume list end token
876                        consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true);
877                    }
878                }
879            }
880
881            function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void {
882                Debug.assert(rangeContainsRange(parent, currentTokenInfo.token));
883
884                const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine();
885                let indentToken = false;
886
887                if (currentTokenInfo.leadingTrivia) {
888                    processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation);
889                }
890
891                let lineAction = LineAction.None;
892                const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token);
893
894                const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos);
895                if (isTokenInRange) {
896                    const rangeHasError = rangeContainsError(currentTokenInfo.token);
897                    // save previousRange since processRange will overwrite this value with current one
898                    const savePreviousRange = previousRange;
899                    lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation);
900                    // do not indent comments\token if token range overlaps with some error
901                    if (!rangeHasError) {
902                        if (lineAction === LineAction.None) {
903                            // indent token only if end line of previous range does not match start line of the token
904                            const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line;
905                            indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine;
906                        }
907                        else {
908                            indentToken = lineAction === LineAction.LineAdded;
909                        }
910                    }
911                }
912
913                if (currentTokenInfo.trailingTrivia) {
914                    previousRangeTriviaEnd = last(currentTokenInfo.trailingTrivia).end;
915                    processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation);
916                }
917
918                if (indentToken) {
919                    const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ?
920                        dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) :
921                        Constants.Unknown;
922
923                    let indentNextTokenOrTrivia = true;
924                    if (currentTokenInfo.leadingTrivia) {
925                        const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container);
926                        indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia,
927                            item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false));
928                    }
929
930                    // indent token only if is it is in target range and does not overlap with any error ranges
931                    if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) {
932                        insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded);
933
934                        lastIndentedLine = tokenStart.line;
935                        indentationOnLastIndentedLine = tokenIndentation;
936                    }
937                }
938
939                formattingScanner.advance();
940
941                childContextNode = parent;
942            }
943        }
944
945        function indentTriviaItems(
946            trivia: TextRangeWithKind[],
947            commentIndentation: number,
948            indentNextTokenOrTrivia: boolean,
949            indentSingleLine: (item: TextRangeWithKind) => void) {
950            for (const triviaItem of trivia) {
951                const triviaInRange = rangeContainsRange(originalRange, triviaItem);
952                switch (triviaItem.kind) {
953                    case SyntaxKind.MultiLineCommentTrivia:
954                        if (triviaInRange) {
955                            indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia);
956                        }
957                        indentNextTokenOrTrivia = false;
958                        break;
959                    case SyntaxKind.SingleLineCommentTrivia:
960                        if (indentNextTokenOrTrivia && triviaInRange) {
961                            indentSingleLine(triviaItem);
962                        }
963                        indentNextTokenOrTrivia = false;
964                        break;
965                    case SyntaxKind.NewLineTrivia:
966                        indentNextTokenOrTrivia = true;
967                        break;
968                }
969            }
970            return indentNextTokenOrTrivia;
971        }
972
973        function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void {
974            for (const triviaItem of trivia) {
975                if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) {
976                    const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos);
977                    processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation);
978                }
979            }
980        }
981
982        function processRange(range: TextRangeWithKind,
983            rangeStart: LineAndCharacter,
984            parent: Node,
985            contextNode: Node,
986            dynamicIndentation: DynamicIndentation): LineAction {
987
988            const rangeHasError = rangeContainsError(range);
989            let lineAction = LineAction.None;
990            if (!rangeHasError) {
991                if (!previousRange) {
992                    // trim whitespaces starting from the beginning of the span up to the current line
993                    const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos);
994                    trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line);
995                }
996                else {
997                    lineAction =
998                        processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation);
999                }
1000            }
1001
1002            previousRange = range;
1003            previousRangeTriviaEnd = range.end;
1004            previousParent = parent;
1005            previousRangeStartLine = rangeStart.line;
1006
1007            return lineAction;
1008        }
1009
1010        function processPair(currentItem: TextRangeWithKind,
1011            currentStartLine: number,
1012            currentParent: Node,
1013            previousItem: TextRangeWithKind,
1014            previousStartLine: number,
1015            previousParent: Node,
1016            contextNode: Node,
1017            dynamicIndentation: DynamicIndentation | undefined): LineAction {
1018
1019            formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode);
1020
1021            const rules = getRules(formattingContext);
1022
1023            let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== false;
1024            let lineAction = LineAction.None;
1025            if (rules) {
1026                // Apply rules in reverse order so that higher priority rules (which are first in the array)
1027                // win in a conflict with lower priority rules.
1028                forEachRight(rules, rule => {
1029                    lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine);
1030                    if (dynamicIndentation) {
1031                        switch (lineAction) {
1032                            case LineAction.LineRemoved:
1033                                // Handle the case where the next line is moved to be the end of this line.
1034                                // In this case we don't indent the next line in the next pass.
1035                                if (currentParent.getStart(sourceFile) === currentItem.pos) {
1036                                    dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
1037                                }
1038                                break;
1039                            case LineAction.LineAdded:
1040                                // Handle the case where token2 is moved to the new line.
1041                                // In this case we indent token2 in the next pass but we set
1042                                // sameLineIndent flag to notify the indenter that the indentation is within the line.
1043                                if (currentParent.getStart(sourceFile) === currentItem.pos) {
1044                                    dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
1045                                }
1046                                break;
1047                            default:
1048                                Debug.assert(lineAction === LineAction.None);
1049                        }
1050                    }
1051
1052                    // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line
1053                    trimTrailingWhitespaces = trimTrailingWhitespaces && !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines;
1054                });
1055            }
1056            else {
1057                trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== SyntaxKind.EndOfFileToken;
1058            }
1059
1060            if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) {
1061                // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line
1062                trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem);
1063            }
1064
1065            return lineAction;
1066        }
1067
1068        function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void {
1069            const indentationString = getIndentationString(indentation, options);
1070            if (lineAdded) {
1071                // new line is added before the token by the formatting rules
1072                // insert indentation string at the very beginning of the token
1073                recordReplace(pos, 0, indentationString);
1074            }
1075            else {
1076                const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos);
1077                const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile);
1078                if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) {
1079                    recordReplace(startLinePosition, tokenStart.character, indentationString);
1080                }
1081            }
1082        }
1083
1084        function characterToColumn(startLinePosition: number, characterInLine: number): number {
1085            let column = 0;
1086            for (let i = 0; i < characterInLine; i++) {
1087                if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) {
1088                    column += options.tabSize! - column % options.tabSize!;
1089                }
1090                else {
1091                    column++;
1092                }
1093            }
1094            return column;
1095        }
1096
1097        function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean {
1098            return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length);
1099        }
1100
1101        function indentMultilineComment(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true) {
1102            // split comment in lines
1103            let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line;
1104            const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line;
1105            if (startLine === endLine) {
1106                if (!firstLineIsIndented) {
1107                    // treat as single line comment
1108                    insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false);
1109                }
1110                return;
1111            }
1112
1113            const parts: TextRange[] = [];
1114            let startPos = commentRange.pos;
1115            for (let line = startLine; line < endLine; line++) {
1116                const endOfLine = getEndLinePosition(line, sourceFile);
1117                parts.push({ pos: startPos, end: endOfLine });
1118                startPos = getStartPositionOfLine(line + 1, sourceFile);
1119            }
1120
1121            if (indentFinalLine) {
1122                parts.push({ pos: startPos, end: commentRange.end });
1123            }
1124
1125            if (parts.length === 0) return;
1126
1127            const startLinePos = getStartPositionOfLine(startLine, sourceFile);
1128
1129            const nonWhitespaceColumnInFirstPart =
1130                SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options);
1131
1132            let startIndex = 0;
1133            if (firstLineIsIndented) {
1134                startIndex = 1;
1135                startLine++;
1136            }
1137
1138            // shift all parts on the delta size
1139            const delta = indentation - nonWhitespaceColumnInFirstPart.column;
1140            for (let i = startIndex; i < parts.length; i++ , startLine++) {
1141                const startLinePos = getStartPositionOfLine(startLine, sourceFile);
1142                const nonWhitespaceCharacterAndColumn =
1143                    i === 0
1144                        ? nonWhitespaceColumnInFirstPart
1145                        : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options);
1146                const newIndentation = nonWhitespaceCharacterAndColumn.column + delta;
1147                if (newIndentation > 0) {
1148                    const indentationString = getIndentationString(newIndentation, options);
1149                    recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString);
1150                }
1151                else {
1152                    recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character);
1153                }
1154            }
1155        }
1156
1157        function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) {
1158            for (let line = line1; line < line2; line++) {
1159                const lineStartPosition = getStartPositionOfLine(line, sourceFile);
1160                const lineEndPosition = getEndLinePosition(line, sourceFile);
1161
1162                // do not trim whitespaces in comments or template expression
1163                if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) {
1164                    continue;
1165                }
1166
1167                const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition);
1168                if (whitespaceStart !== -1) {
1169                    Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1)));
1170                    recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart);
1171                }
1172            }
1173        }
1174
1175        /**
1176         * @param start The position of the first character in range
1177         * @param end The position of the last character in range
1178         */
1179        function getTrailingWhitespaceStartPosition(start: number, end: number) {
1180            let pos = end;
1181            while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) {
1182                pos--;
1183            }
1184            if (pos !== end) {
1185                return pos + 1;
1186            }
1187            return -1;
1188        }
1189
1190        /**
1191         * Trimming will be done for lines after the previous range.
1192         * Exclude comments as they had been previously processed.
1193         */
1194        function trimTrailingWhitespacesForRemainingRange(trivias: TextRangeWithKind<SyntaxKind>[]) {
1195            let startPos = previousRange ? previousRange.end : originalRange.pos;
1196            for (const trivia of trivias) {
1197                if (isComment(trivia.kind)) {
1198                    if (startPos < trivia.pos) {
1199                        trimTrailingWitespacesForPositions(startPos, trivia.pos - 1, previousRange);
1200                    }
1201
1202                    startPos = trivia.end + 1;
1203                }
1204            }
1205
1206            if (startPos < originalRange.end) {
1207                trimTrailingWitespacesForPositions(startPos, originalRange.end, previousRange);
1208            }
1209        }
1210
1211        function trimTrailingWitespacesForPositions(startPos: number, endPos: number, previousRange: TextRangeWithKind) {
1212            const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line;
1213            const endLine = sourceFile.getLineAndCharacterOfPosition(endPos).line;
1214
1215            trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange);
1216        }
1217
1218        function recordDelete(start: number, len: number) {
1219            if (len) {
1220                edits.push(createTextChangeFromStartLength(start, len, ""));
1221            }
1222        }
1223
1224        function recordReplace(start: number, len: number, newText: string) {
1225            if (len || newText) {
1226                edits.push(createTextChangeFromStartLength(start, len, newText));
1227            }
1228        }
1229
1230        function recordInsert(start: number, text: string) {
1231            if (text) {
1232                edits.push(createTextChangeFromStartLength(start, 0, text));
1233            }
1234        }
1235
1236        function applyRuleEdits(rule: Rule,
1237            previousRange: TextRangeWithKind,
1238            previousStartLine: number,
1239            currentRange: TextRangeWithKind,
1240            currentStartLine: number
1241        ): LineAction {
1242            const onLaterLine = currentStartLine !== previousStartLine;
1243            switch (rule.action) {
1244                case RuleAction.StopProcessingSpaceActions:
1245                    // no action required
1246                    return LineAction.None;
1247                case RuleAction.DeleteSpace:
1248                    if (previousRange.end !== currentRange.pos) {
1249                        // delete characters starting from t1.end up to t2.pos exclusive
1250                        recordDelete(previousRange.end, currentRange.pos - previousRange.end);
1251                        return onLaterLine ? LineAction.LineRemoved : LineAction.None;
1252                    }
1253                    break;
1254                case RuleAction.DeleteToken:
1255                    recordDelete(previousRange.pos, previousRange.end - previousRange.pos);
1256                    break;
1257                case RuleAction.InsertNewLine:
1258                    // exit early if we on different lines and rule cannot change number of newlines
1259                    // if line1 and line2 are on subsequent lines then no edits are required - ok to exit
1260                    // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines
1261                    if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) {
1262                        return LineAction.None;
1263                    }
1264
1265                    // edit should not be applied if we have one line feed between elements
1266                    const lineDelta = currentStartLine - previousStartLine;
1267                    if (lineDelta !== 1) {
1268                        recordReplace(previousRange.end, currentRange.pos - previousRange.end, getNewLineOrDefaultFromHost(host, options));
1269                        return onLaterLine ? LineAction.None : LineAction.LineAdded;
1270                    }
1271                    break;
1272                case RuleAction.InsertSpace:
1273                    // exit early if we on different lines and rule cannot change number of newlines
1274                    if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) {
1275                        return LineAction.None;
1276                    }
1277
1278                    const posDelta = currentRange.pos - previousRange.end;
1279                    if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) {
1280                        recordReplace(previousRange.end, currentRange.pos - previousRange.end, " ");
1281                        return onLaterLine ? LineAction.LineRemoved : LineAction.None;
1282                    }
1283                    break;
1284                case RuleAction.InsertTrailingSemicolon:
1285                    recordInsert(previousRange.end, ";");
1286            }
1287            return LineAction.None;
1288        }
1289    }
1290
1291    const enum LineAction { None, LineAdded, LineRemoved }
1292
1293    /**
1294     * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`.
1295     */
1296    export function getRangeOfEnclosingComment(
1297        sourceFile: SourceFile,
1298        position: number,
1299        precedingToken?: Node | null,
1300        tokenAtPosition = getTokenAtPosition(sourceFile, position),
1301    ): CommentRange | undefined {
1302        const jsdoc = findAncestor(tokenAtPosition, isJSDoc);
1303        if (jsdoc) tokenAtPosition = jsdoc.parent;
1304        const tokenStart = tokenAtPosition.getStart(sourceFile);
1305        if (tokenStart <= position && position < tokenAtPosition.getEnd()) {
1306            return undefined;
1307        }
1308
1309        // eslint-disable-next-line no-null/no-null
1310        precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken;
1311
1312        // Between two consecutive tokens, all comments are either trailing on the former
1313        // or leading on the latter (and none are in both lists).
1314        const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end);
1315        const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile);
1316        const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken);
1317        return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) ||
1318            // The end marker of a single-line comment does not include the newline character.
1319            // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position):
1320            //
1321            //    // asdf   ^\n
1322            //
1323            // But for closed multi-line comments, we don't want to be inside the comment in the following case:
1324            //
1325            //    /* asdf */^
1326            //
1327            // However, unterminated multi-line comments *do* contain their end.
1328            //
1329            // Internally, we represent the end of the comment at the newline and closing '/', respectively.
1330            //
1331            position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth()));
1332    }
1333
1334    function getOpenTokenForList(node: Node, list: readonly Node[]) {
1335        switch (node.kind) {
1336            case SyntaxKind.Constructor:
1337            case SyntaxKind.FunctionDeclaration:
1338            case SyntaxKind.FunctionExpression:
1339            case SyntaxKind.MethodDeclaration:
1340            case SyntaxKind.MethodSignature:
1341            case SyntaxKind.ArrowFunction:
1342            case SyntaxKind.CallSignature:
1343            case SyntaxKind.ConstructSignature:
1344            case SyntaxKind.FunctionType:
1345            case SyntaxKind.ConstructorType:
1346            case SyntaxKind.GetAccessor:
1347            case SyntaxKind.SetAccessor:
1348                if ((node as FunctionDeclaration).typeParameters === list) {
1349                    return SyntaxKind.LessThanToken;
1350                }
1351                else if ((node as FunctionDeclaration).parameters === list) {
1352                    return SyntaxKind.OpenParenToken;
1353                }
1354                break;
1355            case SyntaxKind.CallExpression:
1356            case SyntaxKind.NewExpression:
1357                if ((node as CallExpression).typeArguments === list) {
1358                    return SyntaxKind.LessThanToken;
1359                }
1360                else if ((node as CallExpression).arguments === list) {
1361                    return SyntaxKind.OpenParenToken;
1362                }
1363                break;
1364            case SyntaxKind.ClassDeclaration:
1365            case SyntaxKind.ClassExpression:
1366            case SyntaxKind.InterfaceDeclaration:
1367            case SyntaxKind.TypeAliasDeclaration:
1368                if ((node as ClassDeclaration).typeParameters === list) {
1369                    return SyntaxKind.LessThanToken;
1370                }
1371                break;
1372            case SyntaxKind.TypeReference:
1373            case SyntaxKind.TaggedTemplateExpression:
1374            case SyntaxKind.TypeQuery:
1375            case SyntaxKind.ExpressionWithTypeArguments:
1376            case SyntaxKind.ImportType:
1377                if ((node as TypeReferenceNode).typeArguments === list) {
1378                    return SyntaxKind.LessThanToken;
1379                }
1380                break;
1381            case SyntaxKind.TypeLiteral:
1382                return SyntaxKind.OpenBraceToken;
1383        }
1384
1385        return SyntaxKind.Unknown;
1386    }
1387
1388    function getCloseTokenForOpenToken(kind: SyntaxKind) {
1389        switch (kind) {
1390            case SyntaxKind.OpenParenToken:
1391                return SyntaxKind.CloseParenToken;
1392            case SyntaxKind.LessThanToken:
1393                return SyntaxKind.GreaterThanToken;
1394            case SyntaxKind.OpenBraceToken:
1395                return SyntaxKind.CloseBraceToken;
1396        }
1397
1398        return SyntaxKind.Unknown;
1399    }
1400
1401    let internedSizes: { tabSize: number; indentSize: number; };
1402    let internedTabsIndentation: string[] | undefined;
1403    let internedSpacesIndentation: string[] | undefined;
1404
1405    export function getIndentationString(indentation: number, options: EditorSettings): string {
1406        // reset interned strings if FormatCodeOptions were changed
1407        const resetInternedStrings =
1408            !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize);
1409
1410        if (resetInternedStrings) {
1411            internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! };
1412            internedTabsIndentation = internedSpacesIndentation = undefined;
1413        }
1414
1415        if (!options.convertTabsToSpaces) {
1416            const tabs = Math.floor(indentation / options.tabSize!);
1417            const spaces = indentation - tabs * options.tabSize!;
1418
1419            let tabString: string;
1420            if (!internedTabsIndentation) {
1421                internedTabsIndentation = [];
1422            }
1423
1424            if (internedTabsIndentation[tabs] === undefined) {
1425                internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs);
1426            }
1427            else {
1428                tabString = internedTabsIndentation[tabs];
1429            }
1430
1431            return spaces ? tabString + repeatString(" ", spaces) : tabString;
1432        }
1433        else {
1434            let spacesString: string;
1435            const quotient = Math.floor(indentation / options.indentSize!);
1436            const remainder = indentation % options.indentSize!;
1437            if (!internedSpacesIndentation) {
1438                internedSpacesIndentation = [];
1439            }
1440
1441            if (internedSpacesIndentation[quotient] === undefined) {
1442                spacesString = repeatString(" ", options.indentSize! * quotient);
1443                internedSpacesIndentation[quotient] = spacesString;
1444            }
1445            else {
1446                spacesString = internedSpacesIndentation[quotient];
1447            }
1448
1449            return remainder ? spacesString + repeatString(" ", remainder) : spacesString;
1450        }
1451    }
1452}
1453