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