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