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