• 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((<InterfaceDeclaration>parent).members, node);
206            case SyntaxKind.ModuleDeclaration:
207                const body = (<ModuleDeclaration>parent).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((<Block>parent).statements, node);
213            case SyntaxKind.CatchClause:
214                return rangeContainsRange((<CatchClause>parent).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: 0, end: sourceFileLike.text.length };
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 previousRange: TextRangeWithKind;
408        let previousParent: Node;
409        let previousRangeStartLine: number;
410
411        let lastIndentedLine: number;
412        let indentationOnLastIndentedLine = Constants.Unknown;
413
414        const edits: TextChange[] = [];
415
416        formattingScanner.advance();
417
418        if (formattingScanner.isOnToken()) {
419            const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line;
420            let undecoratedStartLine = startLine;
421            if (enclosingNode.decorators) {
422                undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line;
423            }
424
425            processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta);
426        }
427
428        if (!formattingScanner.isOnToken()) {
429            const indentation = SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false)
430                ? initialIndentation + options.indentSize!
431                : initialIndentation;
432            const leadingTrivia = formattingScanner.getCurrentLeadingTrivia();
433            if (leadingTrivia) {
434                indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false,
435                    item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!));
436            }
437        }
438
439        if (options.trimTrailingWhitespace !== false) {
440            trimTrailingWhitespacesForRemainingRange();
441        }
442
443        return edits;
444
445        // local functions
446
447        /** Tries to compute the indentation for a list element.
448         * If list element is not in range then
449         * function will pick its actual indentation
450         * so it can be pushed downstream as inherited indentation.
451         * If list element is in the range - its indentation will be equal
452         * to inherited indentation from its predecessors.
453         */
454        function tryComputeIndentationForListItem(startPos: number,
455            endPos: number,
456            parentStartLine: number,
457            range: TextRange,
458            inheritedIndentation: number): number {
459
460            if (rangeOverlapsWithStartEnd(range, startPos, endPos) ||
461                rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) {
462
463                if (inheritedIndentation !== Constants.Unknown) {
464                    return inheritedIndentation;
465                }
466            }
467            else {
468                const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line;
469                const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile);
470                const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options);
471                if (startLine !== parentStartLine || startPos === column) {
472                    // Use the base indent size if it is greater than
473                    // the indentation of the inherited predecessor.
474                    const baseIndentSize = SmartIndenter.getBaseIndentation(options);
475                    return baseIndentSize > column ? baseIndentSize : column;
476                }
477            }
478
479            return Constants.Unknown;
480        }
481
482        function computeIndentation(
483            node: TextRangeWithKind,
484            startLine: number,
485            inheritedIndentation: number,
486            parent: Node,
487            parentDynamicIndentation: DynamicIndentation,
488            effectiveParentStartLine: number
489        ): { indentation: number, delta: number; } {
490            const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0;
491
492            if (effectiveParentStartLine === startLine) {
493                // if node is located on the same line with the parent
494                // - inherit indentation from the parent
495                // - push children if either parent of node itself has non-zero delta
496                return {
497                    indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(),
498                    delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta)
499                };
500            }
501            else if (inheritedIndentation === Constants.Unknown) {
502                if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) {
503                    // the is used for chaining methods formatting
504                    // - we need to get the indentation on last line and the delta of parent
505                    return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) };
506                }
507                else if (
508                    SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) ||
509                    SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) ||
510                    SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)
511                ) {
512                    return { indentation: parentDynamicIndentation.getIndentation(), delta };
513                }
514                else {
515                    return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta };
516                }
517            }
518            else {
519                return { indentation: inheritedIndentation, delta };
520            }
521        }
522
523        function getFirstNonDecoratorTokenOfNode(node: Node) {
524            if (node.modifiers && node.modifiers.length) {
525                return node.modifiers[0].kind;
526            }
527            switch (node.kind) {
528                case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword;
529                case SyntaxKind.StructDeclaration: return SyntaxKind.StructKeyword;
530                case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword;
531                case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword;
532                case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration;
533                case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword;
534                case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword;
535                case SyntaxKind.MethodDeclaration:
536                    if ((<MethodDeclaration>node).asteriskToken) {
537                        return SyntaxKind.AsteriskToken;
538                    }
539                    // falls through
540
541                case SyntaxKind.PropertyDeclaration:
542                case SyntaxKind.Parameter:
543                    const name = getNameOfDeclaration(<Declaration>node);
544                    if (name) {
545                        return name.kind;
546                    }
547            }
548        }
549
550        function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation {
551            return {
552                getIndentationForComment: (kind, tokenIndentation, container) => {
553                    switch (kind) {
554                        // preceding comment to the token that closes the indentation scope inherits the indentation from the scope
555                        // ..  {
556                        //     // comment
557                        // }
558                        case SyntaxKind.CloseBraceToken:
559                        case SyntaxKind.CloseBracketToken:
560                        case SyntaxKind.CloseParenToken:
561                            return indentation + getDelta(container);
562                    }
563                    return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation;
564                },
565                // if list end token is LessThanToken '>' then its delta should be explicitly suppressed
566                // so that LessThanToken as a binary operator can still be indented.
567                // foo.then
568                //     <
569                //         number,
570                //         string,
571                //     >();
572                // vs
573                // var a = xValue
574                //     > yValue;
575                getIndentationForToken: (line, kind, container, suppressDelta) =>
576                    !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation,
577                getIndentation: () => indentation,
578                getDelta,
579                recomputeIndentation: (lineAdded, parent) => {
580                    if (SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) {
581                        indentation += lineAdded ? options.indentSize! : -options.indentSize!;
582                        delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0;
583                    }
584                }
585            };
586
587            function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean {
588                switch (kind) {
589                    // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent
590                    case SyntaxKind.OpenBraceToken:
591                    case SyntaxKind.CloseBraceToken:
592                    case SyntaxKind.CloseParenToken:
593                    case SyntaxKind.ElseKeyword:
594                    case SyntaxKind.WhileKeyword:
595                    case SyntaxKind.AtToken:
596                        return false;
597                    case SyntaxKind.SlashToken:
598                    case SyntaxKind.GreaterThanToken:
599                        switch (container.kind) {
600                            case SyntaxKind.JsxOpeningElement:
601                            case SyntaxKind.JsxClosingElement:
602                            case SyntaxKind.JsxSelfClosingElement:
603                            case SyntaxKind.ExpressionWithTypeArguments:
604                                return false;
605                        }
606                        break;
607                    case SyntaxKind.OpenBracketToken:
608                    case SyntaxKind.CloseBracketToken:
609                        if (container.kind !== SyntaxKind.MappedType) {
610                            return false;
611                        }
612                        break;
613                }
614                // if token line equals to the line of containing node (this is a first token in the node) - use node indentation
615                return nodeStartLine !== line
616                    // if this token is the first token following the list of decorators, we do not need to indent
617                    && !(node.decorators && kind === getFirstNonDecoratorTokenOfNode(node));
618            }
619
620            function getDelta(child: TextRangeWithKind) {
621                // Delta value should be zero when the node explicitly prevents indentation of the child node
622                return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0;
623            }
624        }
625
626        function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) {
627            if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) {
628                return;
629            }
630
631            const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta);
632
633            // a useful observations when tracking context node
634            //        /
635            //      [a]
636            //   /   |   \
637            //  [b] [c] [d]
638            // node 'a' is a context node for nodes 'b', 'c', 'd'
639            // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a'
640            // this rule can be applied recursively to child nodes of 'a'.
641            //
642            // context node is set to parent node value after processing every child node
643            // context node is set to parent of the token after processing every token
644
645            let childContextNode = contextNode;
646
647            // if there are any tokens that logically belong to node and interleave child nodes
648            // such tokens will be consumed in processChildNode for the child that follows them
649            forEachChild(
650                node,
651                child => {
652                    processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false);
653                },
654                nodes => {
655                    processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation);
656                });
657
658            // proceed any tokens in the node that are located after child nodes
659            while (formattingScanner.isOnToken()) {
660                const tokenInfo = formattingScanner.readTokenInfo(node);
661                if (tokenInfo.token.end > node.end) {
662                    break;
663                }
664                if (node.kind === SyntaxKind.JsxText) {
665                    // Intentation rules for jsx text are handled by `indentMultilineCommentOrJsxText` inside `processChildNode`; just fastforward past it here
666                    formattingScanner.advance();
667                    continue;
668                }
669                consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node);
670            }
671
672            if (!node.parent && formattingScanner.isOnEOF()) {
673                const token = formattingScanner.readEOFTokenRange();
674                if (token.end <= node.end && previousRange) {
675                    processPair(
676                        token,
677                        sourceFile.getLineAndCharacterOfPosition(token.pos).line,
678                        node,
679                        previousRange,
680                        previousRangeStartLine,
681                        previousParent,
682                        contextNode,
683                        nodeDynamicIndentation);
684                }
685            }
686
687            function processChildNode(
688                child: Node,
689                inheritedIndentation: number,
690                parent: Node,
691                parentDynamicIndentation: DynamicIndentation,
692                parentStartLine: number,
693                undecoratedParentStartLine: number,
694                isListItem: boolean,
695                isFirstListItem?: boolean): number {
696
697                const childStartPos = child.getStart(sourceFile);
698
699                const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line;
700
701                let undecoratedChildStartLine = childStartLine;
702                if (child.decorators) {
703                    undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line;
704                }
705
706                // if child is a list item - try to get its indentation, only if parent is within the original range.
707                let childIndentationAmount = Constants.Unknown;
708
709                if (isListItem && rangeContainsRange(originalRange, parent)) {
710                    childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation);
711                    if (childIndentationAmount !== Constants.Unknown) {
712                        inheritedIndentation = childIndentationAmount;
713                    }
714                }
715
716                // child node is outside the target range - do not dive inside
717                if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) {
718                    if (child.end < originalRange.pos) {
719                        formattingScanner.skipToEndOf(child);
720                    }
721                    return inheritedIndentation;
722                }
723
724                if (child.getFullWidth() === 0) {
725                    return inheritedIndentation;
726                }
727
728                while (formattingScanner.isOnToken()) {
729                    // proceed any parent tokens that are located prior to child.getStart()
730                    const tokenInfo = formattingScanner.readTokenInfo(node);
731                    if (tokenInfo.token.end > childStartPos) {
732                        if (tokenInfo.token.pos > childStartPos) {
733                            formattingScanner.skipToStartOf(child);
734                        }
735                        // stop when formatting scanner advances past the beginning of the child
736                        break;
737                    }
738
739                    consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node);
740                }
741
742                if (!formattingScanner.isOnToken()) {
743                    return inheritedIndentation;
744                }
745
746                if (isToken(child)) {
747                    // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules
748                    const tokenInfo = formattingScanner.readTokenInfo(child);
749                    // JSX text shouldn't affect indenting
750                    if (child.kind !== SyntaxKind.JsxText) {
751                        Debug.assert(tokenInfo.token.end === child.end, "Token end is child end");
752                        consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child);
753                        return inheritedIndentation;
754                    }
755                }
756
757                const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine;
758                const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine);
759
760                processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta);
761                if (child.kind === SyntaxKind.JsxText) {
762                    const range: TextRange = { pos: child.getStart(), end: child.getEnd() };
763                    if (range.pos !== range.end) { // don't indent zero-width jsx text
764                        const siblings = parent.getChildren(sourceFile);
765                        const currentIndex = findIndex(siblings, arg => arg.pos === child.pos);
766                        const previousNode = siblings[currentIndex - 1];
767                        if (previousNode) {
768                            // The jsx text needs no indentation whatsoever if it ends on the same line the previous sibling ends on
769                            if (sourceFile.getLineAndCharacterOfPosition(range.end).line !== sourceFile.getLineAndCharacterOfPosition(previousNode.end).line) {
770                                // The first line is (already) "indented" if the text starts on the same line as the previous sibling element ends on
771                                const firstLineIsIndented = sourceFile.getLineAndCharacterOfPosition(range.pos).line === sourceFile.getLineAndCharacterOfPosition(previousNode.end).line;
772                                indentMultilineCommentOrJsxText(range, childIndentation.indentation, firstLineIsIndented, /*indentFinalLine*/ false, /*jsxStyle*/ true);
773                            }
774                        }
775                    }
776                }
777
778                childContextNode = node;
779
780                if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) {
781                    inheritedIndentation = childIndentation.indentation;
782                }
783
784                return inheritedIndentation;
785            }
786
787            function processChildNodes(nodes: NodeArray<Node>,
788                parent: Node,
789                parentStartLine: number,
790                parentDynamicIndentation: DynamicIndentation): void {
791                Debug.assert(isNodeArray(nodes));
792
793                const listStartToken = getOpenTokenForList(parent, nodes);
794
795                let listDynamicIndentation = parentDynamicIndentation;
796                let startLine = parentStartLine;
797
798                if (listStartToken !== SyntaxKind.Unknown) {
799                    // introduce a new indentation scope for lists (including list start and end tokens)
800                    while (formattingScanner.isOnToken()) {
801                        const tokenInfo = formattingScanner.readTokenInfo(parent);
802                        if (tokenInfo.token.end > nodes.pos) {
803                            // stop when formatting scanner moves past the beginning of node list
804                            break;
805                        }
806                        else if (tokenInfo.token.kind === listStartToken) {
807                            // consume list start token
808                            startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line;
809
810                            consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent);
811
812                            let indentationOnListStartToken: number;
813                            if (indentationOnLastIndentedLine !== Constants.Unknown) {
814                                // scanner just processed list start token so consider last indentation as list indentation
815                                // function foo(): { // last indentation was 0, list item will be indented based on this value
816                                //   foo: number;
817                                // }: {};
818                                indentationOnListStartToken = indentationOnLastIndentedLine;
819                            }
820                            else {
821                                const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile);
822                                indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options);
823                            }
824
825                            listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217
826                        }
827                        else {
828                            // consume any tokens that precede the list as child elements of 'node' using its indentation scope
829                            consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent);
830                        }
831                    }
832                }
833
834                let inheritedIndentation = Constants.Unknown;
835                for (let i = 0; i < nodes.length; i++) {
836                    const child = nodes[i];
837                    inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0);
838                }
839
840                const listEndToken = getCloseTokenForOpenToken(listStartToken);
841                if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) {
842                    let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent);
843                    if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) {
844                        const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line;
845                        if (startLine !== commaTokenLine) {
846                            formattingScanner.advance();
847                            tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined;
848                        }
849                    }
850
851                    // consume the list end token only if it is still belong to the parent
852                    // there might be the case when current token matches end token but does not considered as one
853                    // function (x: function) <--
854                    // without this check close paren will be interpreted as list end token for function expression which is wrong
855                    if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) {
856                        // consume list end token
857                        consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true);
858                    }
859                }
860            }
861
862            function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void {
863                Debug.assert(rangeContainsRange(parent, currentTokenInfo.token));
864
865                const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine();
866                let indentToken = false;
867
868                if (currentTokenInfo.leadingTrivia) {
869                    processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation);
870                }
871
872                let lineAction = LineAction.None;
873                const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token);
874
875                const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos);
876                if (isTokenInRange) {
877                    const rangeHasError = rangeContainsError(currentTokenInfo.token);
878                    // save previousRange since processRange will overwrite this value with current one
879                    const savePreviousRange = previousRange;
880                    lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation);
881                    // do not indent comments\token if token range overlaps with some error
882                    if (!rangeHasError) {
883                        if (lineAction === LineAction.None) {
884                            // indent token only if end line of previous range does not match start line of the token
885                            const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line;
886                            indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine;
887                        }
888                        else {
889                            indentToken = lineAction === LineAction.LineAdded;
890                        }
891                    }
892                }
893
894                if (currentTokenInfo.trailingTrivia) {
895                    processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation);
896                }
897
898                if (indentToken) {
899                    const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ?
900                        dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) :
901                        Constants.Unknown;
902
903                    let indentNextTokenOrTrivia = true;
904                    if (currentTokenInfo.leadingTrivia) {
905                        const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container);
906                        indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia,
907                            item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false));
908                    }
909
910                    // indent token only if is it is in target range and does not overlap with any error ranges
911                    if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) {
912                        insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded);
913
914                        lastIndentedLine = tokenStart.line;
915                        indentationOnLastIndentedLine = tokenIndentation;
916                    }
917                }
918
919                formattingScanner.advance();
920
921                childContextNode = parent;
922            }
923        }
924
925        function indentTriviaItems(
926            trivia: TextRangeWithKind[],
927            commentIndentation: number,
928            indentNextTokenOrTrivia: boolean,
929            indentSingleLine: (item: TextRangeWithKind) => void) {
930            for (const triviaItem of trivia) {
931                const triviaInRange = rangeContainsRange(originalRange, triviaItem);
932                switch (triviaItem.kind) {
933                    case SyntaxKind.MultiLineCommentTrivia:
934                        if (triviaInRange) {
935                            indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia);
936                        }
937                        indentNextTokenOrTrivia = false;
938                        break;
939                    case SyntaxKind.SingleLineCommentTrivia:
940                        if (indentNextTokenOrTrivia && triviaInRange) {
941                            indentSingleLine(triviaItem);
942                        }
943                        indentNextTokenOrTrivia = false;
944                        break;
945                    case SyntaxKind.NewLineTrivia:
946                        indentNextTokenOrTrivia = true;
947                        break;
948                }
949            }
950            return indentNextTokenOrTrivia;
951        }
952
953        function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void {
954            for (const triviaItem of trivia) {
955                if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) {
956                    const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos);
957                    processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation);
958                }
959            }
960        }
961
962        function processRange(range: TextRangeWithKind,
963            rangeStart: LineAndCharacter,
964            parent: Node,
965            contextNode: Node,
966            dynamicIndentation: DynamicIndentation): LineAction {
967
968            const rangeHasError = rangeContainsError(range);
969            let lineAction = LineAction.None;
970            if (!rangeHasError) {
971                if (!previousRange) {
972                    // trim whitespaces starting from the beginning of the span up to the current line
973                    const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos);
974                    trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line);
975                }
976                else {
977                    lineAction =
978                        processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation);
979                }
980            }
981
982            previousRange = range;
983            previousParent = parent;
984            previousRangeStartLine = rangeStart.line;
985
986            return lineAction;
987        }
988
989        function processPair(currentItem: TextRangeWithKind,
990            currentStartLine: number,
991            currentParent: Node,
992            previousItem: TextRangeWithKind,
993            previousStartLine: number,
994            previousParent: Node,
995            contextNode: Node,
996            dynamicIndentation: DynamicIndentation): LineAction {
997
998            formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode);
999
1000            const rules = getRules(formattingContext);
1001
1002            let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== false;
1003            let lineAction = LineAction.None;
1004            if (rules) {
1005                // Apply rules in reverse order so that higher priority rules (which are first in the array)
1006                // win in a conflict with lower priority rules.
1007                forEachRight(rules, rule => {
1008                    lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine);
1009                    switch (lineAction) {
1010                        case LineAction.LineRemoved:
1011                            // Handle the case where the next line is moved to be the end of this line.
1012                            // In this case we don't indent the next line in the next pass.
1013                            if (currentParent.getStart(sourceFile) === currentItem.pos) {
1014                                dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
1015                            }
1016                            break;
1017                        case LineAction.LineAdded:
1018                            // Handle the case where token2 is moved to the new line.
1019                            // In this case we indent token2 in the next pass but we set
1020                            // sameLineIndent flag to notify the indenter that the indentation is within the line.
1021                            if (currentParent.getStart(sourceFile) === currentItem.pos) {
1022                                dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
1023                            }
1024                            break;
1025                        default:
1026                            Debug.assert(lineAction === LineAction.None);
1027                    }
1028
1029                    // 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
1030                    trimTrailingWhitespaces = trimTrailingWhitespaces && !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines;
1031                });
1032            }
1033            else {
1034                trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== SyntaxKind.EndOfFileToken;
1035            }
1036
1037            if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) {
1038                // 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
1039                trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem);
1040            }
1041
1042            return lineAction;
1043        }
1044
1045        function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void {
1046            const indentationString = getIndentationString(indentation, options);
1047            if (lineAdded) {
1048                // new line is added before the token by the formatting rules
1049                // insert indentation string at the very beginning of the token
1050                recordReplace(pos, 0, indentationString);
1051            }
1052            else {
1053                const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos);
1054                const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile);
1055                if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) {
1056                    recordReplace(startLinePosition, tokenStart.character, indentationString);
1057                }
1058            }
1059        }
1060
1061        function characterToColumn(startLinePosition: number, characterInLine: number): number {
1062            let column = 0;
1063            for (let i = 0; i < characterInLine; i++) {
1064                if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) {
1065                    column += options.tabSize! - column % options.tabSize!;
1066                }
1067                else {
1068                    column++;
1069                }
1070            }
1071            return column;
1072        }
1073
1074        function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean {
1075            return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length);
1076        }
1077
1078        function indentMultilineCommentOrJsxText(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true, jsxTextStyleIndent?: boolean) {
1079            // split comment in lines
1080            let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line;
1081            const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line;
1082            if (startLine === endLine) {
1083                if (!firstLineIsIndented) {
1084                    // treat as single line comment
1085                    insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false);
1086                }
1087                return;
1088            }
1089
1090            const parts: TextRange[] = [];
1091            let startPos = commentRange.pos;
1092            for (let line = startLine; line < endLine; line++) {
1093                const endOfLine = getEndLinePosition(line, sourceFile);
1094                parts.push({ pos: startPos, end: endOfLine });
1095                startPos = getStartPositionOfLine(line + 1, sourceFile);
1096            }
1097
1098            if (indentFinalLine) {
1099                parts.push({ pos: startPos, end: commentRange.end });
1100            }
1101
1102            if (parts.length === 0) return;
1103
1104            const startLinePos = getStartPositionOfLine(startLine, sourceFile);
1105
1106            const nonWhitespaceColumnInFirstPart =
1107                SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options);
1108
1109            let startIndex = 0;
1110            if (firstLineIsIndented) {
1111                startIndex = 1;
1112                startLine++;
1113            }
1114
1115            // shift all parts on the delta size
1116            let delta = indentation - nonWhitespaceColumnInFirstPart.column;
1117            for (let i = startIndex; i < parts.length; i++ , startLine++) {
1118                const startLinePos = getStartPositionOfLine(startLine, sourceFile);
1119                const nonWhitespaceCharacterAndColumn =
1120                    i === 0
1121                        ? nonWhitespaceColumnInFirstPart
1122                        : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options);
1123                if (jsxTextStyleIndent) {
1124                    // skip adding indentation to blank lines
1125                    if (isLineBreak(sourceFile.text.charCodeAt(getStartPositionOfLine(startLine, sourceFile)))) continue;
1126                    // reset delta on every line
1127                    delta = indentation - nonWhitespaceCharacterAndColumn.column;
1128                }
1129                const newIndentation = nonWhitespaceCharacterAndColumn.column + delta;
1130                if (newIndentation > 0) {
1131                    const indentationString = getIndentationString(newIndentation, options);
1132                    recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString);
1133                }
1134                else {
1135                    recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character);
1136                }
1137            }
1138        }
1139
1140        function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) {
1141            for (let line = line1; line < line2; line++) {
1142                const lineStartPosition = getStartPositionOfLine(line, sourceFile);
1143                const lineEndPosition = getEndLinePosition(line, sourceFile);
1144
1145                // do not trim whitespaces in comments or template expression
1146                if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) {
1147                    continue;
1148                }
1149
1150                const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition);
1151                if (whitespaceStart !== -1) {
1152                    Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1)));
1153                    recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart);
1154                }
1155            }
1156        }
1157
1158        /**
1159         * @param start The position of the first character in range
1160         * @param end The position of the last character in range
1161         */
1162        function getTrailingWhitespaceStartPosition(start: number, end: number) {
1163            let pos = end;
1164            while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) {
1165                pos--;
1166            }
1167            if (pos !== end) {
1168                return pos + 1;
1169            }
1170            return -1;
1171        }
1172
1173        /**
1174         * Trimming will be done for lines after the previous range
1175         */
1176        function trimTrailingWhitespacesForRemainingRange() {
1177            const startPosition = previousRange ? previousRange.end : originalRange.pos;
1178
1179            const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line;
1180            const endLine = sourceFile.getLineAndCharacterOfPosition(originalRange.end).line;
1181
1182            trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange);
1183        }
1184
1185        function recordDelete(start: number, len: number) {
1186            if (len) {
1187                edits.push(createTextChangeFromStartLength(start, len, ""));
1188            }
1189        }
1190
1191        function recordReplace(start: number, len: number, newText: string) {
1192            if (len || newText) {
1193                edits.push(createTextChangeFromStartLength(start, len, newText));
1194            }
1195        }
1196
1197        function recordInsert(start: number, text: string) {
1198            if (text) {
1199                edits.push(createTextChangeFromStartLength(start, 0, text));
1200            }
1201        }
1202
1203        function applyRuleEdits(rule: Rule,
1204            previousRange: TextRangeWithKind,
1205            previousStartLine: number,
1206            currentRange: TextRangeWithKind,
1207            currentStartLine: number
1208        ): LineAction {
1209            const onLaterLine = currentStartLine !== previousStartLine;
1210            switch (rule.action) {
1211                case RuleAction.StopProcessingSpaceActions:
1212                    // no action required
1213                    return LineAction.None;
1214                case RuleAction.DeleteSpace:
1215                    if (previousRange.end !== currentRange.pos) {
1216                        // delete characters starting from t1.end up to t2.pos exclusive
1217                        recordDelete(previousRange.end, currentRange.pos - previousRange.end);
1218                        return onLaterLine ? LineAction.LineRemoved : LineAction.None;
1219                    }
1220                    break;
1221                case RuleAction.DeleteToken:
1222                    recordDelete(previousRange.pos, previousRange.end - previousRange.pos);
1223                    break;
1224                case RuleAction.InsertNewLine:
1225                    // exit early if we on different lines and rule cannot change number of newlines
1226                    // if line1 and line2 are on subsequent lines then no edits are required - ok to exit
1227                    // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines
1228                    if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) {
1229                        return LineAction.None;
1230                    }
1231
1232                    // edit should not be applied if we have one line feed between elements
1233                    const lineDelta = currentStartLine - previousStartLine;
1234                    if (lineDelta !== 1) {
1235                        recordReplace(previousRange.end, currentRange.pos - previousRange.end, getNewLineOrDefaultFromHost(host, options));
1236                        return onLaterLine ? LineAction.None : LineAction.LineAdded;
1237                    }
1238                    break;
1239                case RuleAction.InsertSpace:
1240                    // exit early if we on different lines and rule cannot change number of newlines
1241                    if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) {
1242                        return LineAction.None;
1243                    }
1244
1245                    const posDelta = currentRange.pos - previousRange.end;
1246                    if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) {
1247                        recordReplace(previousRange.end, currentRange.pos - previousRange.end, " ");
1248                        return onLaterLine ? LineAction.LineRemoved : LineAction.None;
1249                    }
1250                    break;
1251                case RuleAction.InsertTrailingSemicolon:
1252                    recordInsert(previousRange.end, ";");
1253            }
1254            return LineAction.None;
1255        }
1256    }
1257
1258    const enum LineAction { None, LineAdded, LineRemoved }
1259
1260    /**
1261     * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`.
1262     */
1263    export function getRangeOfEnclosingComment(
1264        sourceFile: SourceFile,
1265        position: number,
1266        precedingToken?: Node | null,
1267        tokenAtPosition = getTokenAtPosition(sourceFile, position),
1268    ): CommentRange | undefined {
1269        const jsdoc = findAncestor(tokenAtPosition, isJSDoc);
1270        if (jsdoc) tokenAtPosition = jsdoc.parent;
1271        const tokenStart = tokenAtPosition.getStart(sourceFile);
1272        if (tokenStart <= position && position < tokenAtPosition.getEnd()) {
1273            return undefined;
1274        }
1275
1276        // eslint-disable-next-line no-null/no-null
1277        precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken;
1278
1279        // Between two consecutive tokens, all comments are either trailing on the former
1280        // or leading on the latter (and none are in both lists).
1281        const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end);
1282        const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile);
1283        const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken);
1284        return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) ||
1285            // The end marker of a single-line comment does not include the newline character.
1286            // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position):
1287            //
1288            //    // asdf   ^\n
1289            //
1290            // But for closed multi-line comments, we don't want to be inside the comment in the following case:
1291            //
1292            //    /* asdf */^
1293            //
1294            // However, unterminated multi-line comments *do* contain their end.
1295            //
1296            // Internally, we represent the end of the comment at the newline and closing '/', respectively.
1297            //
1298            position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth()));
1299    }
1300
1301    function getOpenTokenForList(node: Node, list: readonly Node[]) {
1302        switch (node.kind) {
1303            case SyntaxKind.Constructor:
1304            case SyntaxKind.FunctionDeclaration:
1305            case SyntaxKind.FunctionExpression:
1306            case SyntaxKind.MethodDeclaration:
1307            case SyntaxKind.MethodSignature:
1308            case SyntaxKind.ArrowFunction:
1309                if ((<FunctionDeclaration>node).typeParameters === list) {
1310                    return SyntaxKind.LessThanToken;
1311                }
1312                else if ((<FunctionDeclaration>node).parameters === list) {
1313                    return SyntaxKind.OpenParenToken;
1314                }
1315                break;
1316            case SyntaxKind.CallExpression:
1317            case SyntaxKind.NewExpression:
1318                if ((<CallExpression>node).typeArguments === list) {
1319                    return SyntaxKind.LessThanToken;
1320                }
1321                else if ((<CallExpression>node).arguments === list) {
1322                    return SyntaxKind.OpenParenToken;
1323                }
1324                break;
1325            case SyntaxKind.TypeReference:
1326                if ((<TypeReferenceNode>node).typeArguments === list) {
1327                    return SyntaxKind.LessThanToken;
1328                }
1329                break;
1330            case SyntaxKind.TypeLiteral:
1331                return SyntaxKind.OpenBraceToken;
1332        }
1333
1334        return SyntaxKind.Unknown;
1335    }
1336
1337    function getCloseTokenForOpenToken(kind: SyntaxKind) {
1338        switch (kind) {
1339            case SyntaxKind.OpenParenToken:
1340                return SyntaxKind.CloseParenToken;
1341            case SyntaxKind.LessThanToken:
1342                return SyntaxKind.GreaterThanToken;
1343            case SyntaxKind.OpenBraceToken:
1344                return SyntaxKind.CloseBraceToken;
1345        }
1346
1347        return SyntaxKind.Unknown;
1348    }
1349
1350    let internedSizes: { tabSize: number; indentSize: number; };
1351    let internedTabsIndentation: string[] | undefined;
1352    let internedSpacesIndentation: string[] | undefined;
1353
1354    export function getIndentationString(indentation: number, options: EditorSettings): string {
1355        // reset interned strings if FormatCodeOptions were changed
1356        const resetInternedStrings =
1357            !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize);
1358
1359        if (resetInternedStrings) {
1360            internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! };
1361            internedTabsIndentation = internedSpacesIndentation = undefined;
1362        }
1363
1364        if (!options.convertTabsToSpaces) {
1365            const tabs = Math.floor(indentation / options.tabSize!);
1366            const spaces = indentation - tabs * options.tabSize!;
1367
1368            let tabString: string;
1369            if (!internedTabsIndentation) {
1370                internedTabsIndentation = [];
1371            }
1372
1373            if (internedTabsIndentation[tabs] === undefined) {
1374                internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs);
1375            }
1376            else {
1377                tabString = internedTabsIndentation[tabs];
1378            }
1379
1380            return spaces ? tabString + repeatString(" ", spaces) : tabString;
1381        }
1382        else {
1383            let spacesString: string;
1384            const quotient = Math.floor(indentation / options.indentSize!);
1385            const remainder = indentation % options.indentSize!;
1386            if (!internedSpacesIndentation) {
1387                internedSpacesIndentation = [];
1388            }
1389
1390            if (internedSpacesIndentation[quotient] === undefined) {
1391                spacesString = repeatString(" ", options.indentSize! * quotient);
1392                internedSpacesIndentation[quotient] = spacesString;
1393            }
1394            else {
1395                spacesString = internedSpacesIndentation[quotient];
1396            }
1397
1398            return remainder ? spacesString + repeatString(" ", remainder) : spacesString;
1399        }
1400    }
1401}
1402