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