1/* @internal */ 2namespace ts.formatting { 3 export interface FormatContext { 4 readonly options: FormatCodeSettings; 5 readonly getRules: RulesMap; 6 readonly host: FormattingHost; 7 } 8 9 export interface TextRangeWithKind<T extends SyntaxKind = SyntaxKind> extends TextRange { 10 kind: T; 11 } 12 13 export type TextRangeWithTriviaKind = TextRangeWithKind<TriviaSyntaxKind>; 14 15 export interface TokenInfo { 16 leadingTrivia: TextRangeWithTriviaKind[] | undefined; 17 token: TextRangeWithKind; 18 trailingTrivia: TextRangeWithTriviaKind[] | undefined; 19 } 20 21 export function createTextRangeWithKind<T extends SyntaxKind>(pos: number, end: number, kind: T): TextRangeWithKind<T> { 22 const textRangeWithKind: TextRangeWithKind<T> = { pos, end, kind }; 23 if (Debug.isDebugging) { 24 Object.defineProperty(textRangeWithKind, "__debugKind", { 25 get: () => Debug.formatSyntaxKind(kind), 26 }); 27 } 28 return textRangeWithKind; 29 } 30 31 const enum Constants { 32 Unknown = -1 33 } 34 35 /* 36 * Indentation for the scope that can be dynamically recomputed. 37 * i.e 38 * while(true) 39 * { let x; 40 * } 41 * Normally indentation is applied only to the first token in line so at glance 'let' should not be touched. 42 * However if some format rule adds new line between '}' and 'let' 'let' will become 43 * the first token in line so it should be indented 44 */ 45 interface DynamicIndentation { 46 getIndentationForToken(tokenLine: number, tokenKind: SyntaxKind, container: Node, suppressDelta: boolean): number; 47 getIndentationForComment(owningToken: SyntaxKind, tokenIndentation: number, container: Node): number; 48 /** 49 * Indentation for open and close tokens of the node if it is block or another node that needs special indentation 50 * ... { 51 * .........<child> 52 * ....} 53 * ____ - indentation 54 * ____ - delta 55 */ 56 getIndentation(): number; 57 /** 58 * Prefered relative indentation for child nodes. 59 * Delta is used to carry the indentation info 60 * foo(bar({ 61 * $ 62 * })) 63 * Both 'foo', 'bar' introduce new indentation with delta = 4, but total indentation in $ is not 8. 64 * foo: { indentation: 0, delta: 4 } 65 * bar: { indentation: foo.indentation + foo.delta = 4, delta: 4} however 'foo' and 'bar' are on the same line 66 * so bar inherits indentation from foo and bar.delta will be 4 67 * 68 */ 69 getDelta(child: TextRangeWithKind): number; 70 /** 71 * Formatter calls this function when rule adds or deletes new lines from the text 72 * so indentation scope can adjust values of indentation and delta. 73 */ 74 recomputeIndentation(lineAddedByFormatting: boolean, parent: Node): void; 75 } 76 77 export function formatOnEnter(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 78 const line = sourceFile.getLineAndCharacterOfPosition(position).line; 79 if (line === 0) { 80 return []; 81 } 82 // After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters. 83 // If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as 84 // trailing whitespaces. So the end of the formatting span should be the later one between: 85 // 1. the end of the previous line 86 // 2. the last non-whitespace character in the current line 87 let endOfFormatSpan = getEndLinePosition(line, sourceFile); 88 while (isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(endOfFormatSpan))) { 89 endOfFormatSpan--; 90 } 91 // if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to 92 // touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the 93 // previous character before the end of format span is line break character as well. 94 if (isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) { 95 endOfFormatSpan--; 96 } 97 const span = { 98 // get start position for the previous line 99 pos: getStartPositionOfLine(line - 1, sourceFile), 100 // end value is exclusive so add 1 to the result 101 end: endOfFormatSpan + 1 102 }; 103 return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatOnEnter); 104 } 105 106 export function formatOnSemicolon(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 107 const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile); 108 return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, FormattingRequestKind.FormatOnSemicolon); 109 } 110 111 export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 112 const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile); 113 if (!openingCurly) { 114 return []; 115 } 116 const curlyBraceRange = openingCurly.parent; 117 const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange); 118 119 /** 120 * We limit the span to end at the opening curly to handle the case where 121 * the brace matched to that just typed will be incorrect after further edits. 122 * For example, we could type the opening curly for the following method 123 * body without brace-matching activated: 124 * ``` 125 * class C { 126 * foo() 127 * } 128 * ``` 129 * and we wouldn't want to move the closing brace. 130 */ 131 const textRange: TextRange = { 132 pos: getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), // TODO: GH#18217 133 end: position 134 }; 135 136 return formatSpan(textRange, sourceFile, formatContext, FormattingRequestKind.FormatOnOpeningCurlyBrace); 137 } 138 139 export function formatOnClosingCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 140 const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile); 141 return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, FormattingRequestKind.FormatOnClosingCurlyBrace); 142 } 143 144 export function formatDocument(sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 145 const span = { 146 pos: 0, 147 end: sourceFile.text.length 148 }; 149 return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatDocument); 150 } 151 152 export function formatSelection(start: number, end: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { 153 // format from the beginning of the line 154 const span = { 155 pos: getLineStartPositionForPosition(start, sourceFile), 156 end, 157 }; 158 return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatSelection); 159 } 160 161 /** 162 * Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment). 163 * @param expectedTokenKind The kind of the last token constituting the desired parent node. 164 */ 165 function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined { 166 const precedingToken = findPrecedingToken(end, sourceFile); 167 168 return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ? 169 precedingToken : 170 undefined; 171 } 172 173 /** 174 * Finds the highest node enclosing `node` at the same list level as `node` 175 * and whose end does not exceed `node.end`. 176 * 177 * Consider typing the following 178 * ``` 179 * let x = 1; 180 * while (true) { 181 * } 182 * ``` 183 * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding 184 * variable declaration. 185 */ 186 function findOutermostNodeWithinListLevel(node: Node | undefined) { 187 let current = node; 188 while (current && 189 current.parent && 190 current.parent.end === node!.end && 191 !isListElement(current.parent, current)) { 192 current = current.parent; 193 } 194 195 return current; 196 } 197 198 // Returns true if node is a element in some list in parent 199 // i.e. parent is class declaration with the list of members and node is one of members. 200 function isListElement(parent: Node, node: Node): boolean { 201 switch (parent.kind) { 202 case SyntaxKind.ClassDeclaration: 203 case SyntaxKind.StructDeclaration: 204 case SyntaxKind.InterfaceDeclaration: 205 return rangeContainsRange((<InterfaceDeclaration>parent).members, node); 206 case SyntaxKind.ModuleDeclaration: 207 const body = (<ModuleDeclaration>parent).body; 208 return !!body && body.kind === SyntaxKind.ModuleBlock && rangeContainsRange(body.statements, node); 209 case SyntaxKind.SourceFile: 210 case SyntaxKind.Block: 211 case SyntaxKind.ModuleBlock: 212 return rangeContainsRange((<Block>parent).statements, node); 213 case SyntaxKind.CatchClause: 214 return rangeContainsRange((<CatchClause>parent).block.statements, node); 215 } 216 217 return false; 218 } 219 220 /** find node that fully contains given text range */ 221 function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { 222 return find(sourceFile); 223 224 function find(n: Node): Node { 225 const candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); 226 if (candidate) { 227 const result = find(candidate); 228 if (result) { 229 return result; 230 } 231 } 232 233 return n; 234 } 235 } 236 237 /** formatting is not applied to ranges that contain parse errors. 238 * This function will return a predicate that for a given text range will tell 239 * if there are any parse errors that overlap with the range. 240 */ 241 function prepareRangeContainsErrorFunction(errors: readonly Diagnostic[], originalRange: TextRange): (r: TextRange) => boolean { 242 if (!errors.length) { 243 return rangeHasNoErrors; 244 } 245 246 // pick only errors that fall in range 247 const sorted = errors 248 .filter(d => rangeOverlapsWithStartEnd(originalRange, d.start!, d.start! + d.length!)) // TODO: GH#18217 249 .sort((e1, e2) => e1.start! - e2.start!); 250 251 if (!sorted.length) { 252 return rangeHasNoErrors; 253 } 254 255 let index = 0; 256 257 return r => { 258 // in current implementation sequence of arguments [r1, r2...] is monotonically increasing. 259 // 'index' tracks the index of the most recent error that was checked. 260 while (true) { 261 if (index >= sorted.length) { 262 // all errors in the range were already checked -> no error in specified range 263 return false; 264 } 265 266 const error = sorted[index]; 267 if (r.end <= error.start!) { 268 // specified range ends before the error referred by 'index' - no error in range 269 return false; 270 } 271 272 if (startEndOverlapsWithStartEnd(r.pos, r.end, error.start!, error.start! + error.length!)) { 273 // specified range overlaps with error range 274 return true; 275 } 276 277 index++; 278 } 279 }; 280 281 function rangeHasNoErrors(): boolean { 282 return false; 283 } 284 } 285 286 /** 287 * Start of the original range might fall inside the comment - scanner will not yield appropriate results 288 * This function will look for token that is located before the start of target range 289 * and return its end as start position for the scanner. 290 */ 291 function getScanStartPosition(enclosingNode: Node, originalRange: TextRange, sourceFile: SourceFile): number { 292 const start = enclosingNode.getStart(sourceFile); 293 if (start === originalRange.pos && enclosingNode.end === originalRange.end) { 294 return start; 295 } 296 297 const precedingToken = findPrecedingToken(originalRange.pos, sourceFile); 298 if (!precedingToken) { 299 // no preceding token found - start from the beginning of enclosing node 300 return enclosingNode.pos; 301 } 302 303 // preceding token ends after the start of original range (i.e when originalRange.pos falls in the middle of literal) 304 // start from the beginning of enclosingNode to handle the entire 'originalRange' 305 if (precedingToken.end >= originalRange.pos) { 306 return enclosingNode.pos; 307 } 308 309 return precedingToken.end; 310 } 311 312 /* 313 * For cases like 314 * if (a || 315 * b ||$ 316 * c) {...} 317 * If we hit Enter at $ we want line ' b ||' to be indented. 318 * Formatting will be applied to the last two lines. 319 * Node that fully encloses these lines is binary expression 'a ||...'. 320 * Initial indentation for this node will be 0. 321 * Binary expressions don't introduce new indentation scopes, however it is possible 322 * that some parent node on the same line does - like if statement in this case. 323 * Note that we are considering parents only from the same line with initial node - 324 * if parent is on the different line - its delta was already contributed 325 * to the initial indentation. 326 */ 327 function getOwnOrInheritedDelta(n: Node, options: FormatCodeSettings, sourceFile: SourceFile): number { 328 let previousLine = Constants.Unknown; 329 let child: Node | undefined; 330 while (n) { 331 const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line; 332 if (previousLine !== Constants.Unknown && line !== previousLine) { 333 break; 334 } 335 336 if (SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) { 337 return options.indentSize!; 338 } 339 340 previousLine = line; 341 child = n; 342 n = n.parent; 343 } 344 return 0; 345 } 346 347 export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] { 348 const range = { pos: 0, end: sourceFileLike.text.length }; 349 return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker( 350 range, 351 node, 352 initialIndentation, 353 delta, 354 scanner, 355 formatContext, 356 FormattingRequestKind.FormatSelection, 357 _ => false, // assume that node does not have any errors 358 sourceFileLike)); 359 } 360 361 function formatNodeLines(node: Node | undefined, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { 362 if (!node) { 363 return []; 364 } 365 366 const span = { 367 pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile), 368 end: node.end 369 }; 370 371 return formatSpan(span, sourceFile, formatContext, requestKind); 372 } 373 374 function formatSpan(originalRange: TextRange, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { 375 // find the smallest node that fully wraps the range and compute the initial indentation for the node 376 const enclosingNode = findEnclosingNode(originalRange, sourceFile); 377 return getFormattingScanner( 378 sourceFile.text, 379 sourceFile.languageVariant, 380 getScanStartPosition(enclosingNode, originalRange, sourceFile), 381 originalRange.end, 382 scanner => formatSpanWorker( 383 originalRange, 384 enclosingNode, 385 SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options), 386 getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile), 387 scanner, 388 formatContext, 389 requestKind, 390 prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), 391 sourceFile)); 392 } 393 394 function formatSpanWorker( 395 originalRange: TextRange, 396 enclosingNode: Node, 397 initialIndentation: number, 398 delta: number, 399 formattingScanner: FormattingScanner, 400 { options, getRules, host }: FormatContext, 401 requestKind: FormattingRequestKind, 402 rangeContainsError: (r: TextRange) => boolean, 403 sourceFile: SourceFileLike): TextChange[] { 404 405 // formatting context is used by rules provider 406 const formattingContext = new FormattingContext(sourceFile, requestKind, options); 407 let previousRange: TextRangeWithKind; 408 let previousParent: Node; 409 let previousRangeStartLine: number; 410 411 let lastIndentedLine: number; 412 let indentationOnLastIndentedLine = Constants.Unknown; 413 414 const edits: TextChange[] = []; 415 416 formattingScanner.advance(); 417 418 if (formattingScanner.isOnToken()) { 419 const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; 420 let undecoratedStartLine = startLine; 421 if (enclosingNode.decorators) { 422 undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; 423 } 424 425 processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); 426 } 427 428 if (!formattingScanner.isOnToken()) { 429 const indentation = SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false) 430 ? initialIndentation + options.indentSize! 431 : initialIndentation; 432 const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); 433 if (leadingTrivia) { 434 indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false, 435 item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); 436 } 437 } 438 439 if (options.trimTrailingWhitespace !== false) { 440 trimTrailingWhitespacesForRemainingRange(); 441 } 442 443 return edits; 444 445 // local functions 446 447 /** Tries to compute the indentation for a list element. 448 * If list element is not in range then 449 * function will pick its actual indentation 450 * so it can be pushed downstream as inherited indentation. 451 * If list element is in the range - its indentation will be equal 452 * to inherited indentation from its predecessors. 453 */ 454 function tryComputeIndentationForListItem(startPos: number, 455 endPos: number, 456 parentStartLine: number, 457 range: TextRange, 458 inheritedIndentation: number): number { 459 460 if (rangeOverlapsWithStartEnd(range, startPos, endPos) || 461 rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { 462 463 if (inheritedIndentation !== Constants.Unknown) { 464 return inheritedIndentation; 465 } 466 } 467 else { 468 const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; 469 const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile); 470 const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); 471 if (startLine !== parentStartLine || startPos === column) { 472 // Use the base indent size if it is greater than 473 // the indentation of the inherited predecessor. 474 const baseIndentSize = SmartIndenter.getBaseIndentation(options); 475 return baseIndentSize > column ? baseIndentSize : column; 476 } 477 } 478 479 return Constants.Unknown; 480 } 481 482 function computeIndentation( 483 node: TextRangeWithKind, 484 startLine: number, 485 inheritedIndentation: number, 486 parent: Node, 487 parentDynamicIndentation: DynamicIndentation, 488 effectiveParentStartLine: number 489 ): { indentation: number, delta: number; } { 490 const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; 491 492 if (effectiveParentStartLine === startLine) { 493 // if node is located on the same line with the parent 494 // - inherit indentation from the parent 495 // - push children if either parent of node itself has non-zero delta 496 return { 497 indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(), 498 delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta) 499 }; 500 } 501 else if (inheritedIndentation === Constants.Unknown) { 502 if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) { 503 // the is used for chaining methods formatting 504 // - we need to get the indentation on last line and the delta of parent 505 return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) }; 506 } 507 else if ( 508 SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) || 509 SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) || 510 SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile) 511 ) { 512 return { indentation: parentDynamicIndentation.getIndentation(), delta }; 513 } 514 else { 515 return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; 516 } 517 } 518 else { 519 return { indentation: inheritedIndentation, delta }; 520 } 521 } 522 523 function getFirstNonDecoratorTokenOfNode(node: Node) { 524 if (node.modifiers && node.modifiers.length) { 525 return node.modifiers[0].kind; 526 } 527 switch (node.kind) { 528 case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword; 529 case SyntaxKind.StructDeclaration: return SyntaxKind.StructKeyword; 530 case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword; 531 case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword; 532 case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration; 533 case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword; 534 case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword; 535 case SyntaxKind.MethodDeclaration: 536 if ((<MethodDeclaration>node).asteriskToken) { 537 return SyntaxKind.AsteriskToken; 538 } 539 // falls through 540 541 case SyntaxKind.PropertyDeclaration: 542 case SyntaxKind.Parameter: 543 const name = getNameOfDeclaration(<Declaration>node); 544 if (name) { 545 return name.kind; 546 } 547 } 548 } 549 550 function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { 551 return { 552 getIndentationForComment: (kind, tokenIndentation, container) => { 553 switch (kind) { 554 // preceding comment to the token that closes the indentation scope inherits the indentation from the scope 555 // .. { 556 // // comment 557 // } 558 case SyntaxKind.CloseBraceToken: 559 case SyntaxKind.CloseBracketToken: 560 case SyntaxKind.CloseParenToken: 561 return indentation + getDelta(container); 562 } 563 return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation; 564 }, 565 // if list end token is LessThanToken '>' then its delta should be explicitly suppressed 566 // so that LessThanToken as a binary operator can still be indented. 567 // foo.then 568 // < 569 // number, 570 // string, 571 // >(); 572 // vs 573 // var a = xValue 574 // > yValue; 575 getIndentationForToken: (line, kind, container, suppressDelta) => 576 !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation, 577 getIndentation: () => indentation, 578 getDelta, 579 recomputeIndentation: (lineAdded, parent) => { 580 if (SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) { 581 indentation += lineAdded ? options.indentSize! : -options.indentSize!; 582 delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; 583 } 584 } 585 }; 586 587 function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean { 588 switch (kind) { 589 // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent 590 case SyntaxKind.OpenBraceToken: 591 case SyntaxKind.CloseBraceToken: 592 case SyntaxKind.CloseParenToken: 593 case SyntaxKind.ElseKeyword: 594 case SyntaxKind.WhileKeyword: 595 case SyntaxKind.AtToken: 596 return false; 597 case SyntaxKind.SlashToken: 598 case SyntaxKind.GreaterThanToken: 599 switch (container.kind) { 600 case SyntaxKind.JsxOpeningElement: 601 case SyntaxKind.JsxClosingElement: 602 case SyntaxKind.JsxSelfClosingElement: 603 case SyntaxKind.ExpressionWithTypeArguments: 604 return false; 605 } 606 break; 607 case SyntaxKind.OpenBracketToken: 608 case SyntaxKind.CloseBracketToken: 609 if (container.kind !== SyntaxKind.MappedType) { 610 return false; 611 } 612 break; 613 } 614 // if token line equals to the line of containing node (this is a first token in the node) - use node indentation 615 return nodeStartLine !== line 616 // if this token is the first token following the list of decorators, we do not need to indent 617 && !(node.decorators && kind === getFirstNonDecoratorTokenOfNode(node)); 618 } 619 620 function getDelta(child: TextRangeWithKind) { 621 // Delta value should be zero when the node explicitly prevents indentation of the child node 622 return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; 623 } 624 } 625 626 function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { 627 if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { 628 return; 629 } 630 631 const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta); 632 633 // a useful observations when tracking context node 634 // / 635 // [a] 636 // / | \ 637 // [b] [c] [d] 638 // node 'a' is a context node for nodes 'b', 'c', 'd' 639 // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a' 640 // this rule can be applied recursively to child nodes of 'a'. 641 // 642 // context node is set to parent node value after processing every child node 643 // context node is set to parent of the token after processing every token 644 645 let childContextNode = contextNode; 646 647 // if there are any tokens that logically belong to node and interleave child nodes 648 // such tokens will be consumed in processChildNode for the child that follows them 649 forEachChild( 650 node, 651 child => { 652 processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false); 653 }, 654 nodes => { 655 processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation); 656 }); 657 658 // proceed any tokens in the node that are located after child nodes 659 while (formattingScanner.isOnToken()) { 660 const tokenInfo = formattingScanner.readTokenInfo(node); 661 if (tokenInfo.token.end > node.end) { 662 break; 663 } 664 if (node.kind === SyntaxKind.JsxText) { 665 // Intentation rules for jsx text are handled by `indentMultilineCommentOrJsxText` inside `processChildNode`; just fastforward past it here 666 formattingScanner.advance(); 667 continue; 668 } 669 consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); 670 } 671 672 if (!node.parent && formattingScanner.isOnEOF()) { 673 const token = formattingScanner.readEOFTokenRange(); 674 if (token.end <= node.end && previousRange) { 675 processPair( 676 token, 677 sourceFile.getLineAndCharacterOfPosition(token.pos).line, 678 node, 679 previousRange, 680 previousRangeStartLine, 681 previousParent, 682 contextNode, 683 nodeDynamicIndentation); 684 } 685 } 686 687 function processChildNode( 688 child: Node, 689 inheritedIndentation: number, 690 parent: Node, 691 parentDynamicIndentation: DynamicIndentation, 692 parentStartLine: number, 693 undecoratedParentStartLine: number, 694 isListItem: boolean, 695 isFirstListItem?: boolean): number { 696 697 const childStartPos = child.getStart(sourceFile); 698 699 const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; 700 701 let undecoratedChildStartLine = childStartLine; 702 if (child.decorators) { 703 undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line; 704 } 705 706 // if child is a list item - try to get its indentation, only if parent is within the original range. 707 let childIndentationAmount = Constants.Unknown; 708 709 if (isListItem && rangeContainsRange(originalRange, parent)) { 710 childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); 711 if (childIndentationAmount !== Constants.Unknown) { 712 inheritedIndentation = childIndentationAmount; 713 } 714 } 715 716 // child node is outside the target range - do not dive inside 717 if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { 718 if (child.end < originalRange.pos) { 719 formattingScanner.skipToEndOf(child); 720 } 721 return inheritedIndentation; 722 } 723 724 if (child.getFullWidth() === 0) { 725 return inheritedIndentation; 726 } 727 728 while (formattingScanner.isOnToken()) { 729 // proceed any parent tokens that are located prior to child.getStart() 730 const tokenInfo = formattingScanner.readTokenInfo(node); 731 if (tokenInfo.token.end > childStartPos) { 732 if (tokenInfo.token.pos > childStartPos) { 733 formattingScanner.skipToStartOf(child); 734 } 735 // stop when formatting scanner advances past the beginning of the child 736 break; 737 } 738 739 consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); 740 } 741 742 if (!formattingScanner.isOnToken()) { 743 return inheritedIndentation; 744 } 745 746 if (isToken(child)) { 747 // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules 748 const tokenInfo = formattingScanner.readTokenInfo(child); 749 // JSX text shouldn't affect indenting 750 if (child.kind !== SyntaxKind.JsxText) { 751 Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); 752 consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); 753 return inheritedIndentation; 754 } 755 } 756 757 const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; 758 const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); 759 760 processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); 761 if (child.kind === SyntaxKind.JsxText) { 762 const range: TextRange = { pos: child.getStart(), end: child.getEnd() }; 763 if (range.pos !== range.end) { // don't indent zero-width jsx text 764 const siblings = parent.getChildren(sourceFile); 765 const currentIndex = findIndex(siblings, arg => arg.pos === child.pos); 766 const previousNode = siblings[currentIndex - 1]; 767 if (previousNode) { 768 // The jsx text needs no indentation whatsoever if it ends on the same line the previous sibling ends on 769 if (sourceFile.getLineAndCharacterOfPosition(range.end).line !== sourceFile.getLineAndCharacterOfPosition(previousNode.end).line) { 770 // The first line is (already) "indented" if the text starts on the same line as the previous sibling element ends on 771 const firstLineIsIndented = sourceFile.getLineAndCharacterOfPosition(range.pos).line === sourceFile.getLineAndCharacterOfPosition(previousNode.end).line; 772 indentMultilineCommentOrJsxText(range, childIndentation.indentation, firstLineIsIndented, /*indentFinalLine*/ false, /*jsxStyle*/ true); 773 } 774 } 775 } 776 } 777 778 childContextNode = node; 779 780 if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { 781 inheritedIndentation = childIndentation.indentation; 782 } 783 784 return inheritedIndentation; 785 } 786 787 function processChildNodes(nodes: NodeArray<Node>, 788 parent: Node, 789 parentStartLine: number, 790 parentDynamicIndentation: DynamicIndentation): void { 791 Debug.assert(isNodeArray(nodes)); 792 793 const listStartToken = getOpenTokenForList(parent, nodes); 794 795 let listDynamicIndentation = parentDynamicIndentation; 796 let startLine = parentStartLine; 797 798 if (listStartToken !== SyntaxKind.Unknown) { 799 // introduce a new indentation scope for lists (including list start and end tokens) 800 while (formattingScanner.isOnToken()) { 801 const tokenInfo = formattingScanner.readTokenInfo(parent); 802 if (tokenInfo.token.end > nodes.pos) { 803 // stop when formatting scanner moves past the beginning of node list 804 break; 805 } 806 else if (tokenInfo.token.kind === listStartToken) { 807 // consume list start token 808 startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; 809 810 consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); 811 812 let indentationOnListStartToken: number; 813 if (indentationOnLastIndentedLine !== Constants.Unknown) { 814 // scanner just processed list start token so consider last indentation as list indentation 815 // function foo(): { // last indentation was 0, list item will be indented based on this value 816 // foo: number; 817 // }: {}; 818 indentationOnListStartToken = indentationOnLastIndentedLine; 819 } 820 else { 821 const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); 822 indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); 823 } 824 825 listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 826 } 827 else { 828 // consume any tokens that precede the list as child elements of 'node' using its indentation scope 829 consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); 830 } 831 } 832 } 833 834 let inheritedIndentation = Constants.Unknown; 835 for (let i = 0; i < nodes.length; i++) { 836 const child = nodes[i]; 837 inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0); 838 } 839 840 const listEndToken = getCloseTokenForOpenToken(listStartToken); 841 if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) { 842 let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); 843 if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) { 844 const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; 845 if (startLine !== commaTokenLine) { 846 formattingScanner.advance(); 847 tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; 848 } 849 } 850 851 // consume the list end token only if it is still belong to the parent 852 // there might be the case when current token matches end token but does not considered as one 853 // function (x: function) <-- 854 // without this check close paren will be interpreted as list end token for function expression which is wrong 855 if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) { 856 // consume list end token 857 consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); 858 } 859 } 860 } 861 862 function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void { 863 Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); 864 865 const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); 866 let indentToken = false; 867 868 if (currentTokenInfo.leadingTrivia) { 869 processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); 870 } 871 872 let lineAction = LineAction.None; 873 const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); 874 875 const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos); 876 if (isTokenInRange) { 877 const rangeHasError = rangeContainsError(currentTokenInfo.token); 878 // save previousRange since processRange will overwrite this value with current one 879 const savePreviousRange = previousRange; 880 lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation); 881 // do not indent comments\token if token range overlaps with some error 882 if (!rangeHasError) { 883 if (lineAction === LineAction.None) { 884 // indent token only if end line of previous range does not match start line of the token 885 const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line; 886 indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine; 887 } 888 else { 889 indentToken = lineAction === LineAction.LineAdded; 890 } 891 } 892 } 893 894 if (currentTokenInfo.trailingTrivia) { 895 processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); 896 } 897 898 if (indentToken) { 899 const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ? 900 dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) : 901 Constants.Unknown; 902 903 let indentNextTokenOrTrivia = true; 904 if (currentTokenInfo.leadingTrivia) { 905 const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container); 906 indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, 907 item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false)); 908 } 909 910 // indent token only if is it is in target range and does not overlap with any error ranges 911 if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) { 912 insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded); 913 914 lastIndentedLine = tokenStart.line; 915 indentationOnLastIndentedLine = tokenIndentation; 916 } 917 } 918 919 formattingScanner.advance(); 920 921 childContextNode = parent; 922 } 923 } 924 925 function indentTriviaItems( 926 trivia: TextRangeWithKind[], 927 commentIndentation: number, 928 indentNextTokenOrTrivia: boolean, 929 indentSingleLine: (item: TextRangeWithKind) => void) { 930 for (const triviaItem of trivia) { 931 const triviaInRange = rangeContainsRange(originalRange, triviaItem); 932 switch (triviaItem.kind) { 933 case SyntaxKind.MultiLineCommentTrivia: 934 if (triviaInRange) { 935 indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); 936 } 937 indentNextTokenOrTrivia = false; 938 break; 939 case SyntaxKind.SingleLineCommentTrivia: 940 if (indentNextTokenOrTrivia && triviaInRange) { 941 indentSingleLine(triviaItem); 942 } 943 indentNextTokenOrTrivia = false; 944 break; 945 case SyntaxKind.NewLineTrivia: 946 indentNextTokenOrTrivia = true; 947 break; 948 } 949 } 950 return indentNextTokenOrTrivia; 951 } 952 953 function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void { 954 for (const triviaItem of trivia) { 955 if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { 956 const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); 957 processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); 958 } 959 } 960 } 961 962 function processRange(range: TextRangeWithKind, 963 rangeStart: LineAndCharacter, 964 parent: Node, 965 contextNode: Node, 966 dynamicIndentation: DynamicIndentation): LineAction { 967 968 const rangeHasError = rangeContainsError(range); 969 let lineAction = LineAction.None; 970 if (!rangeHasError) { 971 if (!previousRange) { 972 // trim whitespaces starting from the beginning of the span up to the current line 973 const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos); 974 trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); 975 } 976 else { 977 lineAction = 978 processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation); 979 } 980 } 981 982 previousRange = range; 983 previousParent = parent; 984 previousRangeStartLine = rangeStart.line; 985 986 return lineAction; 987 } 988 989 function processPair(currentItem: TextRangeWithKind, 990 currentStartLine: number, 991 currentParent: Node, 992 previousItem: TextRangeWithKind, 993 previousStartLine: number, 994 previousParent: Node, 995 contextNode: Node, 996 dynamicIndentation: DynamicIndentation): LineAction { 997 998 formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); 999 1000 const rules = getRules(formattingContext); 1001 1002 let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== false; 1003 let lineAction = LineAction.None; 1004 if (rules) { 1005 // Apply rules in reverse order so that higher priority rules (which are first in the array) 1006 // win in a conflict with lower priority rules. 1007 forEachRight(rules, rule => { 1008 lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); 1009 switch (lineAction) { 1010 case LineAction.LineRemoved: 1011 // Handle the case where the next line is moved to be the end of this line. 1012 // In this case we don't indent the next line in the next pass. 1013 if (currentParent.getStart(sourceFile) === currentItem.pos) { 1014 dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode); 1015 } 1016 break; 1017 case LineAction.LineAdded: 1018 // Handle the case where token2 is moved to the new line. 1019 // In this case we indent token2 in the next pass but we set 1020 // sameLineIndent flag to notify the indenter that the indentation is within the line. 1021 if (currentParent.getStart(sourceFile) === currentItem.pos) { 1022 dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode); 1023 } 1024 break; 1025 default: 1026 Debug.assert(lineAction === LineAction.None); 1027 } 1028 1029 // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line 1030 trimTrailingWhitespaces = trimTrailingWhitespaces && !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines; 1031 }); 1032 } 1033 else { 1034 trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== SyntaxKind.EndOfFileToken; 1035 } 1036 1037 if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { 1038 // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line 1039 trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); 1040 } 1041 1042 return lineAction; 1043 } 1044 1045 function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void { 1046 const indentationString = getIndentationString(indentation, options); 1047 if (lineAdded) { 1048 // new line is added before the token by the formatting rules 1049 // insert indentation string at the very beginning of the token 1050 recordReplace(pos, 0, indentationString); 1051 } 1052 else { 1053 const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos); 1054 const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); 1055 if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { 1056 recordReplace(startLinePosition, tokenStart.character, indentationString); 1057 } 1058 } 1059 } 1060 1061 function characterToColumn(startLinePosition: number, characterInLine: number): number { 1062 let column = 0; 1063 for (let i = 0; i < characterInLine; i++) { 1064 if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) { 1065 column += options.tabSize! - column % options.tabSize!; 1066 } 1067 else { 1068 column++; 1069 } 1070 } 1071 return column; 1072 } 1073 1074 function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { 1075 return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); 1076 } 1077 1078 function indentMultilineCommentOrJsxText(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true, jsxTextStyleIndent?: boolean) { 1079 // split comment in lines 1080 let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line; 1081 const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line; 1082 if (startLine === endLine) { 1083 if (!firstLineIsIndented) { 1084 // treat as single line comment 1085 insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false); 1086 } 1087 return; 1088 } 1089 1090 const parts: TextRange[] = []; 1091 let startPos = commentRange.pos; 1092 for (let line = startLine; line < endLine; line++) { 1093 const endOfLine = getEndLinePosition(line, sourceFile); 1094 parts.push({ pos: startPos, end: endOfLine }); 1095 startPos = getStartPositionOfLine(line + 1, sourceFile); 1096 } 1097 1098 if (indentFinalLine) { 1099 parts.push({ pos: startPos, end: commentRange.end }); 1100 } 1101 1102 if (parts.length === 0) return; 1103 1104 const startLinePos = getStartPositionOfLine(startLine, sourceFile); 1105 1106 const nonWhitespaceColumnInFirstPart = 1107 SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); 1108 1109 let startIndex = 0; 1110 if (firstLineIsIndented) { 1111 startIndex = 1; 1112 startLine++; 1113 } 1114 1115 // shift all parts on the delta size 1116 let delta = indentation - nonWhitespaceColumnInFirstPart.column; 1117 for (let i = startIndex; i < parts.length; i++ , startLine++) { 1118 const startLinePos = getStartPositionOfLine(startLine, sourceFile); 1119 const nonWhitespaceCharacterAndColumn = 1120 i === 0 1121 ? nonWhitespaceColumnInFirstPart 1122 : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); 1123 if (jsxTextStyleIndent) { 1124 // skip adding indentation to blank lines 1125 if (isLineBreak(sourceFile.text.charCodeAt(getStartPositionOfLine(startLine, sourceFile)))) continue; 1126 // reset delta on every line 1127 delta = indentation - nonWhitespaceCharacterAndColumn.column; 1128 } 1129 const newIndentation = nonWhitespaceCharacterAndColumn.column + delta; 1130 if (newIndentation > 0) { 1131 const indentationString = getIndentationString(newIndentation, options); 1132 recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString); 1133 } 1134 else { 1135 recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character); 1136 } 1137 } 1138 } 1139 1140 function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { 1141 for (let line = line1; line < line2; line++) { 1142 const lineStartPosition = getStartPositionOfLine(line, sourceFile); 1143 const lineEndPosition = getEndLinePosition(line, sourceFile); 1144 1145 // do not trim whitespaces in comments or template expression 1146 if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { 1147 continue; 1148 } 1149 1150 const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); 1151 if (whitespaceStart !== -1) { 1152 Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); 1153 recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); 1154 } 1155 } 1156 } 1157 1158 /** 1159 * @param start The position of the first character in range 1160 * @param end The position of the last character in range 1161 */ 1162 function getTrailingWhitespaceStartPosition(start: number, end: number) { 1163 let pos = end; 1164 while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { 1165 pos--; 1166 } 1167 if (pos !== end) { 1168 return pos + 1; 1169 } 1170 return -1; 1171 } 1172 1173 /** 1174 * Trimming will be done for lines after the previous range 1175 */ 1176 function trimTrailingWhitespacesForRemainingRange() { 1177 const startPosition = previousRange ? previousRange.end : originalRange.pos; 1178 1179 const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line; 1180 const endLine = sourceFile.getLineAndCharacterOfPosition(originalRange.end).line; 1181 1182 trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); 1183 } 1184 1185 function recordDelete(start: number, len: number) { 1186 if (len) { 1187 edits.push(createTextChangeFromStartLength(start, len, "")); 1188 } 1189 } 1190 1191 function recordReplace(start: number, len: number, newText: string) { 1192 if (len || newText) { 1193 edits.push(createTextChangeFromStartLength(start, len, newText)); 1194 } 1195 } 1196 1197 function recordInsert(start: number, text: string) { 1198 if (text) { 1199 edits.push(createTextChangeFromStartLength(start, 0, text)); 1200 } 1201 } 1202 1203 function applyRuleEdits(rule: Rule, 1204 previousRange: TextRangeWithKind, 1205 previousStartLine: number, 1206 currentRange: TextRangeWithKind, 1207 currentStartLine: number 1208 ): LineAction { 1209 const onLaterLine = currentStartLine !== previousStartLine; 1210 switch (rule.action) { 1211 case RuleAction.StopProcessingSpaceActions: 1212 // no action required 1213 return LineAction.None; 1214 case RuleAction.DeleteSpace: 1215 if (previousRange.end !== currentRange.pos) { 1216 // delete characters starting from t1.end up to t2.pos exclusive 1217 recordDelete(previousRange.end, currentRange.pos - previousRange.end); 1218 return onLaterLine ? LineAction.LineRemoved : LineAction.None; 1219 } 1220 break; 1221 case RuleAction.DeleteToken: 1222 recordDelete(previousRange.pos, previousRange.end - previousRange.pos); 1223 break; 1224 case RuleAction.InsertNewLine: 1225 // exit early if we on different lines and rule cannot change number of newlines 1226 // if line1 and line2 are on subsequent lines then no edits are required - ok to exit 1227 // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines 1228 if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { 1229 return LineAction.None; 1230 } 1231 1232 // edit should not be applied if we have one line feed between elements 1233 const lineDelta = currentStartLine - previousStartLine; 1234 if (lineDelta !== 1) { 1235 recordReplace(previousRange.end, currentRange.pos - previousRange.end, getNewLineOrDefaultFromHost(host, options)); 1236 return onLaterLine ? LineAction.None : LineAction.LineAdded; 1237 } 1238 break; 1239 case RuleAction.InsertSpace: 1240 // exit early if we on different lines and rule cannot change number of newlines 1241 if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { 1242 return LineAction.None; 1243 } 1244 1245 const posDelta = currentRange.pos - previousRange.end; 1246 if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { 1247 recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); 1248 return onLaterLine ? LineAction.LineRemoved : LineAction.None; 1249 } 1250 break; 1251 case RuleAction.InsertTrailingSemicolon: 1252 recordInsert(previousRange.end, ";"); 1253 } 1254 return LineAction.None; 1255 } 1256 } 1257 1258 const enum LineAction { None, LineAdded, LineRemoved } 1259 1260 /** 1261 * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. 1262 */ 1263 export function getRangeOfEnclosingComment( 1264 sourceFile: SourceFile, 1265 position: number, 1266 precedingToken?: Node | null, 1267 tokenAtPosition = getTokenAtPosition(sourceFile, position), 1268 ): CommentRange | undefined { 1269 const jsdoc = findAncestor(tokenAtPosition, isJSDoc); 1270 if (jsdoc) tokenAtPosition = jsdoc.parent; 1271 const tokenStart = tokenAtPosition.getStart(sourceFile); 1272 if (tokenStart <= position && position < tokenAtPosition.getEnd()) { 1273 return undefined; 1274 } 1275 1276 // eslint-disable-next-line no-null/no-null 1277 precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken; 1278 1279 // Between two consecutive tokens, all comments are either trailing on the former 1280 // or leading on the latter (and none are in both lists). 1281 const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); 1282 const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); 1283 const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); 1284 return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) || 1285 // The end marker of a single-line comment does not include the newline character. 1286 // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position): 1287 // 1288 // // asdf ^\n 1289 // 1290 // But for closed multi-line comments, we don't want to be inside the comment in the following case: 1291 // 1292 // /* asdf */^ 1293 // 1294 // However, unterminated multi-line comments *do* contain their end. 1295 // 1296 // Internally, we represent the end of the comment at the newline and closing '/', respectively. 1297 // 1298 position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); 1299 } 1300 1301 function getOpenTokenForList(node: Node, list: readonly Node[]) { 1302 switch (node.kind) { 1303 case SyntaxKind.Constructor: 1304 case SyntaxKind.FunctionDeclaration: 1305 case SyntaxKind.FunctionExpression: 1306 case SyntaxKind.MethodDeclaration: 1307 case SyntaxKind.MethodSignature: 1308 case SyntaxKind.ArrowFunction: 1309 if ((<FunctionDeclaration>node).typeParameters === list) { 1310 return SyntaxKind.LessThanToken; 1311 } 1312 else if ((<FunctionDeclaration>node).parameters === list) { 1313 return SyntaxKind.OpenParenToken; 1314 } 1315 break; 1316 case SyntaxKind.CallExpression: 1317 case SyntaxKind.NewExpression: 1318 if ((<CallExpression>node).typeArguments === list) { 1319 return SyntaxKind.LessThanToken; 1320 } 1321 else if ((<CallExpression>node).arguments === list) { 1322 return SyntaxKind.OpenParenToken; 1323 } 1324 break; 1325 case SyntaxKind.TypeReference: 1326 if ((<TypeReferenceNode>node).typeArguments === list) { 1327 return SyntaxKind.LessThanToken; 1328 } 1329 break; 1330 case SyntaxKind.TypeLiteral: 1331 return SyntaxKind.OpenBraceToken; 1332 } 1333 1334 return SyntaxKind.Unknown; 1335 } 1336 1337 function getCloseTokenForOpenToken(kind: SyntaxKind) { 1338 switch (kind) { 1339 case SyntaxKind.OpenParenToken: 1340 return SyntaxKind.CloseParenToken; 1341 case SyntaxKind.LessThanToken: 1342 return SyntaxKind.GreaterThanToken; 1343 case SyntaxKind.OpenBraceToken: 1344 return SyntaxKind.CloseBraceToken; 1345 } 1346 1347 return SyntaxKind.Unknown; 1348 } 1349 1350 let internedSizes: { tabSize: number; indentSize: number; }; 1351 let internedTabsIndentation: string[] | undefined; 1352 let internedSpacesIndentation: string[] | undefined; 1353 1354 export function getIndentationString(indentation: number, options: EditorSettings): string { 1355 // reset interned strings if FormatCodeOptions were changed 1356 const resetInternedStrings = 1357 !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); 1358 1359 if (resetInternedStrings) { 1360 internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! }; 1361 internedTabsIndentation = internedSpacesIndentation = undefined; 1362 } 1363 1364 if (!options.convertTabsToSpaces) { 1365 const tabs = Math.floor(indentation / options.tabSize!); 1366 const spaces = indentation - tabs * options.tabSize!; 1367 1368 let tabString: string; 1369 if (!internedTabsIndentation) { 1370 internedTabsIndentation = []; 1371 } 1372 1373 if (internedTabsIndentation[tabs] === undefined) { 1374 internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs); 1375 } 1376 else { 1377 tabString = internedTabsIndentation[tabs]; 1378 } 1379 1380 return spaces ? tabString + repeatString(" ", spaces) : tabString; 1381 } 1382 else { 1383 let spacesString: string; 1384 const quotient = Math.floor(indentation / options.indentSize!); 1385 const remainder = indentation % options.indentSize!; 1386 if (!internedSpacesIndentation) { 1387 internedSpacesIndentation = []; 1388 } 1389 1390 if (internedSpacesIndentation[quotient] === undefined) { 1391 spacesString = repeatString(" ", options.indentSize! * quotient); 1392 internedSpacesIndentation[quotient] = spacesString; 1393 } 1394 else { 1395 spacesString = internedSpacesIndentation[quotient]; 1396 } 1397 1398 return remainder ? spacesString + repeatString(" ", remainder) : spacesString; 1399 } 1400 } 1401} 1402