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