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