• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    ArrayBindingPattern, ArrayLiteralExpression, CallExpression, CharacterCodes, ClassDeclaration, ClassExpression,
3    CommentRange, contains, Debug, EditorSettings, find, findChildOfKind, findListItemInfo, findNextToken,
4    findPrecedingToken, FormatCodeSettings, GetAccessorDeclaration, getLineAndCharacterOfPosition,
5    getLineStartPositionForPosition, getStartPositionOfLine, getTokenAtPosition, IfStatement, ImportClause, IndentStyle,
6    InterfaceDeclaration, isCallExpression, isCallOrNewExpression, isConditionalExpression, isDeclaration,
7    isStatementButNotDeclaration, isStringOrRegularExpressionOrTemplateLiteral, isWhiteSpaceLike,
8    isWhiteSpaceSingleLine, JSDocTemplateTag, LineAndCharacter, NamedImportsOrExports, Node, NodeArray,
9    ObjectBindingPattern, ObjectLiteralExpression, positionBelongsToNode, rangeContainsRange, rangeContainsStartEnd,
10    SignatureDeclaration, skipTrivia, SourceFile, SourceFileLike, StructDeclaration, SyntaxKind, TextRange,
11    TypeAliasDeclaration, TypeLiteralNode, TypeReferenceNode, VariableDeclarationList,
12} from "../_namespaces/ts";
13import { getRangeOfEnclosingComment, TextRangeWithKind } from "../_namespaces/ts.formatting";
14
15/** @internal */
16export namespace SmartIndenter {
17
18    const enum Value {
19        Unknown = -1
20    }
21
22    /**
23     * @param assumeNewLineBeforeCloseBrace
24     * `false` when called on text from a real source file.
25     * `true` when we need to assume `position` is on a newline.
26     *
27     * This is useful for codefixes. Consider
28     * ```
29     * function f() {
30     * |}
31     * ```
32     * with `position` at `|`.
33     *
34     * When inserting some text after an open brace, we would like to get indentation as if a newline was already there.
35     * By default indentation at `position` will be 0 so 'assumeNewLineBeforeCloseBrace' overrides this behavior.
36     */
37    export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings, assumeNewLineBeforeCloseBrace = false): number {
38        if (position > sourceFile.text.length) {
39            return getBaseIndentation(options); // past EOF
40        }
41
42        // no indentation when the indent style is set to none,
43        // so we can return fast
44        if (options.indentStyle === IndentStyle.None) {
45            return 0;
46        }
47
48        const precedingToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*excludeJsdoc*/ true);
49
50        // eslint-disable-next-line no-null/no-null
51        const enclosingCommentRange = getRangeOfEnclosingComment(sourceFile, position, precedingToken || null);
52        if (enclosingCommentRange && enclosingCommentRange.kind === SyntaxKind.MultiLineCommentTrivia) {
53            return getCommentIndent(sourceFile, position, options, enclosingCommentRange);
54        }
55
56        if (!precedingToken) {
57            return getBaseIndentation(options);
58        }
59
60        // no indentation in string \regex\template literals
61        const precedingTokenIsLiteral = isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind);
62        if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) {
63            return 0;
64        }
65
66        const lineAtPosition = sourceFile.getLineAndCharacterOfPosition(position).line;
67
68        // indentation is first non-whitespace character in a previous line
69        // for block indentation, we should look for a line which contains something that's not
70        // whitespace.
71        const currentToken = getTokenAtPosition(sourceFile, position);
72        // For object literals, we want indentation to work just like with blocks.
73        // If the `{` starts in any position (even in the middle of a line), then
74        // the following indentation should treat `{` as the start of that line (including leading whitespace).
75        // ```
76        //     const a: { x: undefined, y: undefined } = {}       // leading 4 whitespaces and { starts in the middle of line
77        // ->
78        //     const a: { x: undefined, y: undefined } = {
79        //         x: undefined,
80        //         y: undefined,
81        //     }
82        // ---------------------
83        //     const a: {x : undefined, y: undefined } =
84        //      {}
85        // ->
86        //     const a: { x: undefined, y: undefined } =
87        //      {                                                  // leading 5 whitespaces and { starts at 6 column
88        //          x: undefined,
89        //          y: undefined,
90        //      }
91        // ```
92        const isObjectLiteral = currentToken.kind === SyntaxKind.OpenBraceToken && currentToken.parent.kind === SyntaxKind.ObjectLiteralExpression;
93        if (options.indentStyle === IndentStyle.Block || isObjectLiteral) {
94            return getBlockIndent(sourceFile, position, options);
95        }
96
97        if (precedingToken.kind === SyntaxKind.CommaToken && precedingToken.parent.kind !== SyntaxKind.BinaryExpression) {
98            // previous token is comma that separates items in list - find the previous item and try to derive indentation from it
99            const actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options);
100            if (actualIndentation !== Value.Unknown) {
101                return actualIndentation;
102            }
103        }
104
105        const containerList = getListByPosition(position, precedingToken.parent, sourceFile);
106        // use list position if the preceding token is before any list items
107        if (containerList && !rangeContainsRange(containerList, precedingToken)) {
108            const useTheSameBaseIndentation = [SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction].indexOf(currentToken.parent.kind) !== -1;
109            const indentSize = useTheSameBaseIndentation ? 0 : options.indentSize!;
110            return getActualIndentationForListStartLine(containerList, sourceFile, options) + indentSize; // TODO: GH#18217
111        }
112
113        return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options);
114    }
115
116    function getCommentIndent(sourceFile: SourceFile, position: number, options: EditorSettings, enclosingCommentRange: CommentRange): number {
117        const previousLine = getLineAndCharacterOfPosition(sourceFile, position).line - 1;
118        const commentStartLine = getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line;
119
120        Debug.assert(commentStartLine >= 0);
121
122        if (previousLine <= commentStartLine) {
123            return findFirstNonWhitespaceColumn(getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options);
124        }
125
126        const startPositionOfLine = getStartPositionOfLine(previousLine, sourceFile);
127        const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options);
128
129        if (column === 0) {
130            return column;
131        }
132
133        const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character);
134        return firstNonWhitespaceCharacterCode === CharacterCodes.asterisk ? column - 1 : column;
135    }
136
137    function getBlockIndent(sourceFile: SourceFile, position: number, options: EditorSettings): number {
138        // move backwards until we find a line with a non-whitespace character,
139        // then find the first non-whitespace character for that line.
140        let current = position;
141        while (current > 0) {
142            const char = sourceFile.text.charCodeAt(current);
143            if (!isWhiteSpaceLike(char)) {
144                break;
145            }
146            current--;
147        }
148
149        const lineStart = getLineStartPositionForPosition(current, sourceFile);
150        return findFirstNonWhitespaceColumn(lineStart, current, sourceFile, options);
151    }
152
153    function getSmartIndent(sourceFile: SourceFile, position: number, precedingToken: Node, lineAtPosition: number, assumeNewLineBeforeCloseBrace: boolean, options: EditorSettings): number {
154        // try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken'
155        // if such node is found - compute initial indentation for 'position' inside this node
156        let previous: Node | undefined;
157        let current = precedingToken;
158
159        while (current) {
160            if (positionBelongsToNode(current, position, sourceFile) && shouldIndentChildNode(options, current, previous, sourceFile, /*isNextChild*/ true)) {
161                const currentStart = getStartLineAndCharacterForNode(current, sourceFile);
162                const nextTokenKind = nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile);
163                const indentationDelta = nextTokenKind !== NextTokenKind.Unknown
164                    // handle cases when codefix is about to be inserted before the close brace
165                    ? assumeNewLineBeforeCloseBrace && nextTokenKind === NextTokenKind.CloseBrace ? options.indentSize : 0
166                    : lineAtPosition !== currentStart.line ? options.indentSize : 0;
167                return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta!, sourceFile, /*isNextChild*/ true, options); // TODO: GH#18217
168            }
169
170            // check if current node is a list item - if yes, take indentation from it
171            // do not consider parent-child line sharing yet:
172            // function foo(a
173            //    | preceding node 'a' does share line with its parent but indentation is expected
174            const actualIndentation = getActualIndentationForListItem(current, sourceFile, options, /*listIndentsChild*/ true);
175            if (actualIndentation !== Value.Unknown) {
176                return actualIndentation;
177            }
178
179            previous = current;
180            current = current.parent;
181        }
182        // no parent was found - return the base indentation of the SourceFile
183        return getBaseIndentation(options);
184    }
185
186    export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: EditorSettings): number {
187        const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile));
188        return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, /*isNextChild*/ false, options);
189    }
190
191    export function getBaseIndentation(options: EditorSettings) {
192        return options.baseIndentSize || 0;
193    }
194
195    function getIndentationForNodeWorker(
196        current: Node,
197        currentStart: LineAndCharacter,
198        ignoreActualIndentationRange: TextRange | undefined,
199        indentationDelta: number,
200        sourceFile: SourceFile,
201        isNextChild: boolean,
202        options: EditorSettings): number {
203        let parent = current.parent;
204
205        // Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if
206        // * parent and child nodes start on the same line, or
207        // * parent is an IfStatement and child starts on the same line as an 'else clause'.
208        while (parent) {
209            let useActualIndentation = true;
210            if (ignoreActualIndentationRange) {
211                const start = current.getStart(sourceFile);
212                useActualIndentation = start < ignoreActualIndentationRange.pos || start > ignoreActualIndentationRange.end;
213            }
214
215            const containingListOrParentStart = getContainingListOrParentStart(parent, current, sourceFile);
216            const parentAndChildShareLine =
217                containingListOrParentStart.line === currentStart.line ||
218                childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStart.line, sourceFile);
219
220            if (useActualIndentation) {
221                // check if current node is a list item - if yes, take indentation from it
222                const firstListChild = getContainingList(current, sourceFile)?.[0];
223                // A list indents its children if the children begin on a later line than the list itself:
224                //
225                // f1(               L0 - List start
226                //   {               L1 - First child start: indented, along with all other children
227                //     prop: 0
228                //   },
229                //   {
230                //     prop: 1
231                //   }
232                // )
233                //
234                // f2({             L0 - List start and first child start: children are not indented.
235                //   prop: 0             Object properties are indented only one level, because the list
236                // }, {                  itself contributes nothing.
237                //   prop: 1        L3 - The indentation of the second object literal is best understood by
238                // })                    looking at the relationship between the list and *first* list item.
239                const listIndentsChild = !!firstListChild && getStartLineAndCharacterForNode(firstListChild, sourceFile).line > containingListOrParentStart.line;
240                let actualIndentation = getActualIndentationForListItem(current, sourceFile, options, listIndentsChild);
241                if (actualIndentation !== Value.Unknown) {
242                    return actualIndentation + indentationDelta;
243                }
244
245                // try to fetch actual indentation for current node from source text
246                actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options);
247                if (actualIndentation !== Value.Unknown) {
248                    return actualIndentation + indentationDelta;
249                }
250            }
251
252            // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line
253            if (shouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine) {
254                indentationDelta += options.indentSize!;
255            }
256
257            // In our AST, a call argument's `parent` is the call-expression, not the argument list.
258            // We would like to increase indentation based on the relationship between an argument and its argument-list,
259            // so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list.
260            // But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression
261            // to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation.
262            //
263            // Instead, when at an argument, we unspoof the starting position of the enclosing call expression
264            // *after* applying indentation for the argument.
265
266            const useTrueStart =
267                isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStart.line, sourceFile);
268
269            current = parent;
270            parent = current.parent;
271            currentStart = useTrueStart ? sourceFile.getLineAndCharacterOfPosition(current.getStart(sourceFile)) : containingListOrParentStart;
272        }
273
274        return indentationDelta + getBaseIndentation(options);
275    }
276
277    function getContainingListOrParentStart(parent: Node, child: Node, sourceFile: SourceFile): LineAndCharacter {
278        const containingList = getContainingList(child, sourceFile);
279        const startPos = containingList ? containingList.pos : parent.getStart(sourceFile);
280        return sourceFile.getLineAndCharacterOfPosition(startPos);
281    }
282
283    /*
284     * Function returns Value.Unknown if indentation cannot be determined
285     */
286    function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorSettings): number {
287        // previous token is comma that separates items in list - find the previous item and try to derive indentation from it
288        const commaItemInfo = findListItemInfo(commaToken);
289        if (commaItemInfo && commaItemInfo.listItemIndex > 0) {
290            return deriveActualIndentationFromList(commaItemInfo.list.getChildren(), commaItemInfo.listItemIndex - 1, sourceFile, options);
291        }
292        else {
293            // handle broken code gracefully
294            return Value.Unknown;
295        }
296    }
297
298    /*
299     * Function returns Value.Unknown if actual indentation for node should not be used (i.e because node is nested expression)
300     */
301    function getActualIndentationForNode(current: Node,
302        parent: Node,
303        currentLineAndChar: LineAndCharacter,
304        parentAndChildShareLine: boolean,
305        sourceFile: SourceFile,
306        options: EditorSettings): number {
307
308        // actual indentation is used for statements\declarations if one of cases below is true:
309        // - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually
310        // - parent and child are not on the same line
311        const useActualIndentation =
312            (isDeclaration(current) || isStatementButNotDeclaration(current)) &&
313            (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine);
314
315        if (!useActualIndentation) {
316            return Value.Unknown;
317        }
318
319        return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options);
320    }
321
322    const enum NextTokenKind {
323        Unknown,
324        OpenBrace,
325        CloseBrace
326    }
327
328    function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): NextTokenKind {
329        const nextToken = findNextToken(precedingToken, current, sourceFile);
330        if (!nextToken) {
331            return NextTokenKind.Unknown;
332        }
333
334        if (nextToken.kind === SyntaxKind.OpenBraceToken) {
335            // open braces are always indented at the parent level
336            return NextTokenKind.OpenBrace;
337        }
338        else if (nextToken.kind === SyntaxKind.CloseBraceToken) {
339            // close braces are indented at the parent level if they are located on the same line with cursor
340            // this means that if new line will be added at $ position, this case will be indented
341            // class A {
342            //    $
343            // }
344            /// and this one - not
345            // class A {
346            // $}
347
348            const nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line;
349            return lineAtPosition === nextTokenStartLine ? NextTokenKind.CloseBrace : NextTokenKind.Unknown;
350        }
351
352        return NextTokenKind.Unknown;
353    }
354
355    function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFileLike): LineAndCharacter {
356        return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile));
357    }
358
359    export function isArgumentAndStartLineOverlapsExpressionBeingCalled(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFileLike): boolean {
360        if (!(isCallExpression(parent) && contains(parent.arguments, child))) {
361            return false;
362        }
363
364        const expressionOfCallExpressionEnd = parent.expression.getEnd();
365        const expressionOfCallExpressionEndLine = getLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd).line;
366        return expressionOfCallExpressionEndLine === childStartLine;
367    }
368
369    export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean {
370        if (parent.kind === SyntaxKind.IfStatement && (parent as IfStatement).elseStatement === child) {
371            const elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile)!;
372            Debug.assert(elseKeyword !== undefined);
373
374            const elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line;
375            return elseKeywordStartLine === childStartLine;
376        }
377
378        return false;
379    }
380
381    // A multiline conditional typically increases the indentation of its whenTrue and whenFalse children:
382    //
383    // condition
384    //   ? whenTrue
385    //   : whenFalse;
386    //
387    // However, that indentation does not apply if the subexpressions themselves span multiple lines,
388    // applying their own indentation:
389    //
390    // (() => {
391    //   return complexCalculationForCondition();
392    // })() ? {
393    //   whenTrue: 'multiline object literal'
394    // } : (
395    //   whenFalse('multiline parenthesized expression')
396    // );
397    //
398    // In these cases, we must discard the indentation increase that would otherwise be applied to the
399    // whenTrue and whenFalse children to avoid double-indenting their contents. To identify this scenario,
400    // we check for the whenTrue branch beginning on the line that the condition ends, and the whenFalse
401    // branch beginning on the line that the whenTrue branch ends.
402    export function childIsUnindentedBranchOfConditionalExpression(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean {
403        if (isConditionalExpression(parent) && (child === parent.whenTrue || child === parent.whenFalse)) {
404            const conditionEndLine = getLineAndCharacterOfPosition(sourceFile, parent.condition.end).line;
405            if (child === parent.whenTrue) {
406                return childStartLine === conditionEndLine;
407            }
408            else {
409                // On the whenFalse side, we have to look at the whenTrue side, because if that one was
410                // indented, whenFalse must also be indented:
411                //
412                // const y = true
413                //   ? 1 : (          L1: whenTrue indented because it's on a new line
414                //     0              L2: indented two stops, one because whenTrue was indented
415                //   );                   and one because of the parentheses spanning multiple lines
416                const trueStartLine = getStartLineAndCharacterForNode(parent.whenTrue, sourceFile).line;
417                const trueEndLine = getLineAndCharacterOfPosition(sourceFile, parent.whenTrue.end).line;
418                return conditionEndLine === trueStartLine && trueEndLine === childStartLine;
419            }
420        }
421        return false;
422    }
423
424    export function argumentStartsOnSameLineAsPreviousArgument(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean {
425        if (isCallOrNewExpression(parent)) {
426            if (!parent.arguments) return false;
427            const currentNode = find(parent.arguments, arg => arg.pos === child.pos);
428            // If it's not one of the arguments, don't look past this
429            if (!currentNode) return false;
430            const currentIndex = parent.arguments.indexOf(currentNode);
431            if (currentIndex === 0) return false; // Can't look at previous node if first
432
433            const previousNode = parent.arguments[currentIndex - 1];
434            const lineOfPreviousNode = getLineAndCharacterOfPosition(sourceFile, previousNode.getEnd()).line;
435
436            if (childStartLine === lineOfPreviousNode) {
437                return true;
438            }
439        }
440
441        return false;
442    }
443
444    export function getContainingList(node: Node, sourceFile: SourceFile): NodeArray<Node> | undefined {
445        return node.parent && getListByRange(node.getStart(sourceFile), node.getEnd(), node.parent, sourceFile);
446    }
447
448    function getListByPosition(pos: number, node: Node, sourceFile: SourceFile): NodeArray<Node> | undefined {
449        return node && getListByRange(pos, pos, node, sourceFile);
450    }
451
452    function getListByRange(start: number, end: number, node: Node, sourceFile: SourceFile): NodeArray<Node> | undefined {
453        switch (node.kind) {
454            case SyntaxKind.TypeReference:
455                return getList((node as TypeReferenceNode).typeArguments);
456            case SyntaxKind.ObjectLiteralExpression:
457                return getList((node as ObjectLiteralExpression).properties);
458            case SyntaxKind.ArrayLiteralExpression:
459                return getList((node as ArrayLiteralExpression).elements);
460            case SyntaxKind.TypeLiteral:
461                return getList((node as TypeLiteralNode).members);
462            case SyntaxKind.FunctionDeclaration:
463            case SyntaxKind.FunctionExpression:
464            case SyntaxKind.ArrowFunction:
465            case SyntaxKind.MethodDeclaration:
466            case SyntaxKind.MethodSignature:
467            case SyntaxKind.CallSignature:
468            case SyntaxKind.Constructor:
469            case SyntaxKind.ConstructorType:
470            case SyntaxKind.ConstructSignature:
471                return getList((node as SignatureDeclaration).typeParameters) || getList((node as SignatureDeclaration).parameters);
472            case SyntaxKind.GetAccessor:
473                return getList((node as GetAccessorDeclaration).parameters);
474            case SyntaxKind.ClassDeclaration:
475            case SyntaxKind.ClassExpression:
476            case SyntaxKind.StructDeclaration:
477            case SyntaxKind.InterfaceDeclaration:
478            case SyntaxKind.TypeAliasDeclaration:
479            case SyntaxKind.JSDocTemplateTag:
480                return getList((node as ClassDeclaration | ClassExpression | StructDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag).typeParameters);
481            case SyntaxKind.NewExpression:
482            case SyntaxKind.CallExpression:
483                return getList((node as CallExpression).typeArguments) || getList((node as CallExpression).arguments);
484            case SyntaxKind.VariableDeclarationList:
485                return getList((node as VariableDeclarationList).declarations);
486            case SyntaxKind.NamedImports:
487            case SyntaxKind.NamedExports:
488                return getList((node as NamedImportsOrExports).elements);
489            case SyntaxKind.ObjectBindingPattern:
490            case SyntaxKind.ArrayBindingPattern:
491                return getList((node as ObjectBindingPattern | ArrayBindingPattern).elements);
492        }
493
494        function getList(list: NodeArray<Node> | undefined): NodeArray<Node> | undefined {
495            return list && rangeContainsStartEnd(getVisualListRange(node, list, sourceFile), start, end) ? list : undefined;
496        }
497    }
498
499    function getVisualListRange(node: Node, list: TextRange, sourceFile: SourceFile): TextRange {
500        const children = node.getChildren(sourceFile);
501        for (let i = 1; i < children.length - 1; i++) {
502            if (children[i].pos === list.pos && children[i].end === list.end) {
503                return { pos: children[i - 1].end, end: children[i + 1].getStart(sourceFile) };
504            }
505        }
506        return list;
507    }
508
509    function getActualIndentationForListStartLine(list: NodeArray<Node>, sourceFile: SourceFile, options: EditorSettings): number {
510        if (!list) {
511            return Value.Unknown;
512        }
513        return findColumnForFirstNonWhitespaceCharacterInLine(sourceFile.getLineAndCharacterOfPosition(list.pos), sourceFile, options);
514    }
515
516    function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorSettings, listIndentsChild: boolean): number {
517        if (node.parent && node.parent.kind === SyntaxKind.VariableDeclarationList) {
518            // VariableDeclarationList has no wrapping tokens
519            return Value.Unknown;
520        }
521        const containingList = getContainingList(node, sourceFile);
522        if (containingList) {
523            const index = containingList.indexOf(node);
524            if (index !== -1) {
525                const result = deriveActualIndentationFromList(containingList, index, sourceFile, options);
526                if (result !== Value.Unknown) {
527                    return result;
528                }
529            }
530            return getActualIndentationForListStartLine(containingList, sourceFile, options) + (listIndentsChild ? options.indentSize! : 0); // TODO: GH#18217
531        }
532        return Value.Unknown;
533    }
534
535    function deriveActualIndentationFromList(list: readonly Node[], index: number, sourceFile: SourceFile, options: EditorSettings): number {
536        Debug.assert(index >= 0 && index < list.length);
537        const node = list[index];
538
539        // walk toward the start of the list starting from current node and check if the line is the same for all items.
540        // if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i]
541        let lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile);
542        for (let i = index - 1; i >= 0; i--) {
543            if (list[i].kind === SyntaxKind.CommaToken) {
544                continue;
545            }
546            // skip list items that ends on the same line with the current list element
547            const prevEndLine = sourceFile.getLineAndCharacterOfPosition(list[i].end).line;
548            if (prevEndLine !== lineAndCharacter.line) {
549                return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options);
550            }
551
552            lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile);
553        }
554        return Value.Unknown;
555    }
556
557    function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorSettings): number {
558        const lineStart = sourceFile.getPositionOfLineAndCharacter(lineAndCharacter.line, 0);
559        return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options);
560    }
561
562    /**
563     * Character is the actual index of the character since the beginning of the line.
564     * Column - position of the character after expanding tabs to spaces.
565     * "0\t2$"
566     * value of 'character' for '$' is 3
567     * value of 'column' for '$' is 6 (assuming that tab size is 4)
568     */
569    export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) {
570        let character = 0;
571        let column = 0;
572        for (let pos = startPos; pos < endPos; pos++) {
573            const ch = sourceFile.text.charCodeAt(pos);
574            if (!isWhiteSpaceSingleLine(ch)) {
575                break;
576            }
577
578            if (ch === CharacterCodes.tab) {
579                column += options.tabSize! + (column % options.tabSize!);
580            }
581            else {
582                column++;
583            }
584
585            character++;
586        }
587        return { column, character };
588    }
589
590    export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings): number {
591        return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column;
592    }
593
594    export function nodeWillIndentChild(settings: FormatCodeSettings, parent: TextRangeWithKind, child: TextRangeWithKind | undefined, sourceFile: SourceFileLike | undefined, indentByDefault: boolean): boolean {
595        const childKind = child ? child.kind : SyntaxKind.Unknown;
596
597        switch (parent.kind) {
598            case SyntaxKind.ExpressionStatement:
599            case SyntaxKind.ClassDeclaration:
600            case SyntaxKind.ClassExpression:
601            case SyntaxKind.StructDeclaration:
602            case SyntaxKind.InterfaceDeclaration:
603            case SyntaxKind.EnumDeclaration:
604            case SyntaxKind.TypeAliasDeclaration:
605            case SyntaxKind.ArrayLiteralExpression:
606            case SyntaxKind.Block:
607            case SyntaxKind.ModuleBlock:
608            case SyntaxKind.ObjectLiteralExpression:
609            case SyntaxKind.TypeLiteral:
610            case SyntaxKind.MappedType:
611            case SyntaxKind.TupleType:
612            case SyntaxKind.CaseBlock:
613            case SyntaxKind.DefaultClause:
614            case SyntaxKind.CaseClause:
615            case SyntaxKind.ParenthesizedExpression:
616            case SyntaxKind.PropertyAccessExpression:
617            case SyntaxKind.CallExpression:
618            case SyntaxKind.NewExpression:
619            case SyntaxKind.VariableStatement:
620            case SyntaxKind.ExportAssignment:
621            case SyntaxKind.ReturnStatement:
622            case SyntaxKind.ConditionalExpression:
623            case SyntaxKind.ArrayBindingPattern:
624            case SyntaxKind.ObjectBindingPattern:
625            case SyntaxKind.JsxOpeningElement:
626            case SyntaxKind.JsxOpeningFragment:
627            case SyntaxKind.JsxSelfClosingElement:
628            case SyntaxKind.JsxExpression:
629            case SyntaxKind.MethodSignature:
630            case SyntaxKind.CallSignature:
631            case SyntaxKind.ConstructSignature:
632            case SyntaxKind.Parameter:
633            case SyntaxKind.FunctionType:
634            case SyntaxKind.ConstructorType:
635            case SyntaxKind.ParenthesizedType:
636            case SyntaxKind.TaggedTemplateExpression:
637            case SyntaxKind.AwaitExpression:
638            case SyntaxKind.NamedExports:
639            case SyntaxKind.NamedImports:
640            case SyntaxKind.ExportSpecifier:
641            case SyntaxKind.ImportSpecifier:
642            case SyntaxKind.PropertyDeclaration:
643                return true;
644            case SyntaxKind.VariableDeclaration:
645            case SyntaxKind.PropertyAssignment:
646            case SyntaxKind.BinaryExpression:
647                if (!settings.indentMultiLineObjectLiteralBeginningOnBlankLine && sourceFile && childKind === SyntaxKind.ObjectLiteralExpression) { // TODO: GH#18217
648                    return rangeIsOnOneLine(sourceFile, child!);
649                }
650                if (parent.kind === SyntaxKind.BinaryExpression && sourceFile && child && childKind === SyntaxKind.JsxElement) {
651                    const parentStartLine = sourceFile.getLineAndCharacterOfPosition(skipTrivia(sourceFile.text, parent.pos)).line;
652                    const childStartLine = sourceFile.getLineAndCharacterOfPosition(skipTrivia(sourceFile.text, child.pos)).line;
653                    return parentStartLine !== childStartLine;
654                }
655                if (parent.kind !== SyntaxKind.BinaryExpression) {
656                    return true;
657                }
658                break;
659            case SyntaxKind.DoStatement:
660            case SyntaxKind.WhileStatement:
661            case SyntaxKind.ForInStatement:
662            case SyntaxKind.ForOfStatement:
663            case SyntaxKind.ForStatement:
664            case SyntaxKind.IfStatement:
665            case SyntaxKind.FunctionDeclaration:
666            case SyntaxKind.FunctionExpression:
667            case SyntaxKind.MethodDeclaration:
668            case SyntaxKind.Constructor:
669            case SyntaxKind.GetAccessor:
670            case SyntaxKind.SetAccessor:
671                return childKind !== SyntaxKind.Block;
672            case SyntaxKind.ArrowFunction:
673                if (sourceFile && childKind === SyntaxKind.ParenthesizedExpression) {
674                    return rangeIsOnOneLine(sourceFile, child!);
675                }
676                return childKind !== SyntaxKind.Block;
677            case SyntaxKind.ExportDeclaration:
678                return childKind !== SyntaxKind.NamedExports;
679            case SyntaxKind.ImportDeclaration:
680                return childKind !== SyntaxKind.ImportClause ||
681                    (!!(child as ImportClause).namedBindings && (child as ImportClause).namedBindings!.kind !== SyntaxKind.NamedImports);
682            case SyntaxKind.JsxElement:
683                return childKind !== SyntaxKind.JsxClosingElement;
684            case SyntaxKind.JsxFragment:
685                return childKind !== SyntaxKind.JsxClosingFragment;
686            case SyntaxKind.IntersectionType:
687            case SyntaxKind.UnionType:
688                if (childKind === SyntaxKind.TypeLiteral || childKind === SyntaxKind.TupleType) {
689                    return false;
690                }
691                break;
692        }
693        // No explicit rule for given nodes so the result will follow the default value argument
694        return indentByDefault;
695    }
696
697    function isControlFlowEndingStatement(kind: SyntaxKind, parent: TextRangeWithKind): boolean {
698        switch (kind) {
699            case SyntaxKind.ReturnStatement:
700            case SyntaxKind.ThrowStatement:
701            case SyntaxKind.ContinueStatement:
702            case SyntaxKind.BreakStatement:
703                return parent.kind !== SyntaxKind.Block;
704            default:
705                return false;
706        }
707    }
708
709    /**
710     * True when the parent node should indent the given child by an explicit rule.
711     * @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child.
712     */
713    export function shouldIndentChildNode(settings: FormatCodeSettings, parent: TextRangeWithKind, child?: Node, sourceFile?: SourceFileLike, isNextChild = false): boolean {
714        return nodeWillIndentChild(settings, parent, child, sourceFile, /*indentByDefault*/ false)
715            && !(isNextChild && child && isControlFlowEndingStatement(child.kind, parent));
716    }
717
718    function rangeIsOnOneLine(sourceFile: SourceFileLike, range: TextRangeWithKind) {
719        const rangeStart = skipTrivia(sourceFile.text, range.pos);
720        const startLine = sourceFile.getLineAndCharacterOfPosition(rangeStart).line;
721        const endLine = sourceFile.getLineAndCharacterOfPosition(range.end).line;
722        return startLine === endLine;
723    }
724}
725