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((parent as InterfaceDeclaration).members, node); 206 case SyntaxKind.ModuleDeclaration: 207 const body = (parent as ModuleDeclaration).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((parent as Block).statements, node); 213 case SyntaxKind.CatchClause: 214 return rangeContainsRange((parent as CatchClause).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: node.pos, end: node.end }; 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 previousRangeTriviaEnd: number; 408 let previousRange: TextRangeWithKind; 409 let previousParent: Node; 410 let previousRangeStartLine: number; 411 412 let lastIndentedLine: number; 413 let indentationOnLastIndentedLine = Constants.Unknown; 414 415 const edits: TextChange[] = []; 416 417 formattingScanner.advance(); 418 419 if (formattingScanner.isOnToken()) { 420 const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; 421 let undecoratedStartLine = startLine; 422 if (hasDecorators(enclosingNode)) { 423 undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; 424 } 425 426 processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); 427 } 428 429 if (!formattingScanner.isOnToken()) { 430 const indentation = SmartIndenter.nodeWillIndentChild(options, enclosingNode, /*child*/ undefined, sourceFile, /*indentByDefault*/ false) 431 ? initialIndentation + options.indentSize! 432 : initialIndentation; 433 const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); 434 if (leadingTrivia) { 435 indentTriviaItems(leadingTrivia, indentation, /*indentNextTokenOrTrivia*/ false, 436 item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); 437 if (options.trimTrailingWhitespace !== false) { 438 trimTrailingWhitespacesForRemainingRange(leadingTrivia); 439 } 440 } 441 } 442 443 if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) { 444 // Formatting edits happen by looking at pairs of contiguous tokens (see `processPair`), 445 // typically inserting or deleting whitespace between them. The recursive `processNode` 446 // logic above bails out as soon as it encounters a token that is beyond the end of the 447 // range we're supposed to format (or if we reach the end of the file). But this potentially 448 // leaves out an edit that would occur *inside* the requested range but cannot be discovered 449 // without looking at one token *beyond* the end of the range: consider the line `x = { }` 450 // with a selection from the beginning of the line to the space inside the curly braces, 451 // inclusive. We would expect a format-selection would delete the space (if rules apply), 452 // but in order to do that, we need to process the pair ["{", "}"], but we stopped processing 453 // just before getting there. This block handles this trailing edit. 454 const tokenInfo = 455 formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() : 456 formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token : 457 undefined; 458 459 if (tokenInfo && tokenInfo.pos === previousRangeTriviaEnd!) { 460 // We need to check that tokenInfo and previousRange are contiguous: the `originalRange` 461 // may have ended in the middle of a token, which means we will have stopped formatting 462 // on that token, leaving `previousRange` pointing to the token before it, but already 463 // having moved the formatting scanner (where we just got `tokenInfo`) to the next token. 464 // If this happens, our supposed pair [previousRange, tokenInfo] actually straddles the 465 // token that intersects the end of the range we're supposed to format, so the pair will 466 // produce bogus edits if we try to `processPair`. Recall that the point of this logic is 467 // to perform a trailing edit at the end of the selection range: but there can be no valid 468 // edit in the middle of a token where the range ended, so if we have a non-contiguous 469 // pair here, we're already done and we can ignore it. 470 const parent = findPrecedingToken(tokenInfo.end, sourceFile, enclosingNode)?.parent || previousParent!; 471 processPair( 472 tokenInfo, 473 sourceFile.getLineAndCharacterOfPosition(tokenInfo.pos).line, 474 parent, 475 previousRange, 476 previousRangeStartLine!, 477 previousParent!, 478 parent, 479 /*dynamicIndentation*/ undefined); 480 } 481 } 482 483 return edits; 484 485 // local functions 486 487 /** Tries to compute the indentation for a list element. 488 * If list element is not in range then 489 * function will pick its actual indentation 490 * so it can be pushed downstream as inherited indentation. 491 * If list element is in the range - its indentation will be equal 492 * to inherited indentation from its predecessors. 493 */ 494 function tryComputeIndentationForListItem(startPos: number, 495 endPos: number, 496 parentStartLine: number, 497 range: TextRange, 498 inheritedIndentation: number): number { 499 500 if (rangeOverlapsWithStartEnd(range, startPos, endPos) || 501 rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { 502 503 if (inheritedIndentation !== Constants.Unknown) { 504 return inheritedIndentation; 505 } 506 } 507 else { 508 const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; 509 const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile); 510 const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); 511 if (startLine !== parentStartLine || startPos === column) { 512 // Use the base indent size if it is greater than 513 // the indentation of the inherited predecessor. 514 const baseIndentSize = SmartIndenter.getBaseIndentation(options); 515 return baseIndentSize > column ? baseIndentSize : column; 516 } 517 } 518 519 return Constants.Unknown; 520 } 521 522 function computeIndentation( 523 node: TextRangeWithKind, 524 startLine: number, 525 inheritedIndentation: number, 526 parent: Node, 527 parentDynamicIndentation: DynamicIndentation, 528 effectiveParentStartLine: number 529 ): { indentation: number, delta: number; } { 530 const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; 531 532 if (effectiveParentStartLine === startLine) { 533 // if node is located on the same line with the parent 534 // - inherit indentation from the parent 535 // - push children if either parent of node itself has non-zero delta 536 return { 537 indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(), 538 delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta) 539 }; 540 } 541 else if (inheritedIndentation === Constants.Unknown) { 542 if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) { 543 // the is used for chaining methods formatting 544 // - we need to get the indentation on last line and the delta of parent 545 return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) }; 546 } 547 else if ( 548 SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile) || 549 SmartIndenter.childIsUnindentedBranchOfConditionalExpression(parent, node, startLine, sourceFile) || 550 SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile) 551 ) { 552 return { indentation: parentDynamicIndentation.getIndentation(), delta }; 553 } 554 else { 555 return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; 556 } 557 } 558 else { 559 return { indentation: inheritedIndentation, delta }; 560 } 561 } 562 563 function getFirstNonDecoratorTokenOfNode(node: Node) { 564 if (canHaveModifiers(node)) { 565 const modifier = find(node.modifiers, isModifier, findIndex(node.modifiers, isDecorator)); 566 if (modifier) return modifier.kind; 567 } 568 569 switch (node.kind) { 570 case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword; 571 case SyntaxKind.StructDeclaration: return SyntaxKind.StructKeyword; 572 case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword; 573 case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword; 574 case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration; 575 case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword; 576 case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword; 577 case SyntaxKind.MethodDeclaration: 578 if ((node as MethodDeclaration).asteriskToken) { 579 return SyntaxKind.AsteriskToken; 580 } 581 // falls through 582 583 case SyntaxKind.PropertyDeclaration: 584 case SyntaxKind.Parameter: 585 const name = getNameOfDeclaration(node as Declaration); 586 if (name) { 587 return name.kind; 588 } 589 } 590 } 591 592 function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { 593 return { 594 getIndentationForComment: (kind, tokenIndentation, container) => { 595 switch (kind) { 596 // preceding comment to the token that closes the indentation scope inherits the indentation from the scope 597 // .. { 598 // // comment 599 // } 600 case SyntaxKind.CloseBraceToken: 601 case SyntaxKind.CloseBracketToken: 602 case SyntaxKind.CloseParenToken: 603 return indentation + getDelta(container); 604 } 605 return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation; 606 }, 607 // if list end token is LessThanToken '>' then its delta should be explicitly suppressed 608 // so that LessThanToken as a binary operator can still be indented. 609 // foo.then 610 // < 611 // number, 612 // string, 613 // >(); 614 // vs 615 // var a = xValue 616 // > yValue; 617 getIndentationForToken: (line, kind, container, suppressDelta) => 618 !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation, 619 getIndentation: () => indentation, 620 getDelta, 621 recomputeIndentation: (lineAdded, parent) => { 622 if (SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) { 623 indentation += lineAdded ? options.indentSize! : -options.indentSize!; 624 delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; 625 } 626 } 627 }; 628 629 function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean { 630 switch (kind) { 631 // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent 632 case SyntaxKind.OpenBraceToken: 633 case SyntaxKind.CloseBraceToken: 634 case SyntaxKind.CloseParenToken: 635 case SyntaxKind.ElseKeyword: 636 case SyntaxKind.WhileKeyword: 637 case SyntaxKind.AtToken: 638 return false; 639 case SyntaxKind.SlashToken: 640 case SyntaxKind.GreaterThanToken: 641 switch (container.kind) { 642 case SyntaxKind.JsxOpeningElement: 643 case SyntaxKind.JsxClosingElement: 644 case SyntaxKind.JsxSelfClosingElement: 645 return false; 646 } 647 break; 648 case SyntaxKind.OpenBracketToken: 649 case SyntaxKind.CloseBracketToken: 650 if (container.kind !== SyntaxKind.MappedType) { 651 return false; 652 } 653 break; 654 } 655 // if token line equals to the line of containing node (this is a first token in the node) - use node indentation 656 return nodeStartLine !== line 657 // if this token is the first token following the list of decorators, we do not need to indent 658 && !(hasDecorators(node) && kind === getFirstNonDecoratorTokenOfNode(node)); 659 } 660 661 function getDelta(child: TextRangeWithKind) { 662 // Delta value should be zero when the node explicitly prevents indentation of the child node 663 return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; 664 } 665 } 666 667 function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { 668 if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { 669 return; 670 } 671 672 const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta); 673 674 // a useful observations when tracking context node 675 // / 676 // [a] 677 // / | \ 678 // [b] [c] [d] 679 // node 'a' is a context node for nodes 'b', 'c', 'd' 680 // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a' 681 // this rule can be applied recursively to child nodes of 'a'. 682 // 683 // context node is set to parent node value after processing every child node 684 // context node is set to parent of the token after processing every token 685 686 let childContextNode = contextNode; 687 688 // if there are any tokens that logically belong to node and interleave child nodes 689 // such tokens will be consumed in processChildNode for the child that follows them 690 forEachChild( 691 node, 692 child => { 693 processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false); 694 }, 695 nodes => { 696 processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation); 697 }); 698 699 // proceed any tokens in the node that are located after child nodes 700 while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { 701 const tokenInfo = formattingScanner.readTokenInfo(node); 702 if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) { 703 break; 704 } 705 consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); 706 } 707 708 function processChildNode( 709 child: Node, 710 inheritedIndentation: number, 711 parent: Node, 712 parentDynamicIndentation: DynamicIndentation, 713 parentStartLine: number, 714 undecoratedParentStartLine: number, 715 isListItem: boolean, 716 isFirstListItem?: boolean): number { 717 Debug.assert(!nodeIsSynthesized(child)); 718 719 if (nodeIsMissing(child)) { 720 return inheritedIndentation; 721 } 722 723 const childStartPos = child.getStart(sourceFile); 724 725 const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; 726 727 let undecoratedChildStartLine = childStartLine; 728 if (hasDecorators(child)) { 729 undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line; 730 } 731 732 // if child is a list item - try to get its indentation, only if parent is within the original range. 733 let childIndentationAmount = Constants.Unknown; 734 735 if (isListItem && rangeContainsRange(originalRange, parent)) { 736 childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); 737 if (childIndentationAmount !== Constants.Unknown) { 738 inheritedIndentation = childIndentationAmount; 739 } 740 } 741 742 // child node is outside the target range - do not dive inside 743 if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { 744 if (child.end < originalRange.pos) { 745 formattingScanner.skipToEndOf(child); 746 } 747 return inheritedIndentation; 748 } 749 750 if (child.getFullWidth() === 0) { 751 return inheritedIndentation; 752 } 753 754 while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { 755 // proceed any parent tokens that are located prior to child.getStart() 756 const tokenInfo = formattingScanner.readTokenInfo(node); 757 if (tokenInfo.token.end > originalRange.end) { 758 return inheritedIndentation; 759 } 760 if (tokenInfo.token.end > childStartPos) { 761 if (tokenInfo.token.pos > childStartPos) { 762 formattingScanner.skipToStartOf(child); 763 } 764 // stop when formatting scanner advances past the beginning of the child 765 break; 766 } 767 768 consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); 769 } 770 771 if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) { 772 return inheritedIndentation; 773 } 774 775 if (isToken(child)) { 776 // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules 777 const tokenInfo = formattingScanner.readTokenInfo(child); 778 // JSX text shouldn't affect indenting 779 if (child.kind !== SyntaxKind.JsxText) { 780 Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); 781 consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); 782 return inheritedIndentation; 783 } 784 } 785 786 const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; 787 const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); 788 789 processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); 790 791 childContextNode = node; 792 793 if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { 794 inheritedIndentation = childIndentation.indentation; 795 } 796 797 return inheritedIndentation; 798 } 799 800 function processChildNodes(nodes: NodeArray<Node>, 801 parent: Node, 802 parentStartLine: number, 803 parentDynamicIndentation: DynamicIndentation): void { 804 Debug.assert(isNodeArray(nodes)); 805 Debug.assert(!nodeIsSynthesized(nodes)); 806 807 const listStartToken = getOpenTokenForList(parent, nodes); 808 809 let listDynamicIndentation = parentDynamicIndentation; 810 let startLine = parentStartLine; 811 // node range is outside the target range - do not dive inside 812 if (!rangeOverlapsWithStartEnd(originalRange, nodes.pos, nodes.end)) { 813 if (nodes.end < originalRange.pos) { 814 formattingScanner.skipToEndOf(nodes); 815 } 816 return; 817 } 818 819 if (listStartToken !== SyntaxKind.Unknown) { 820 // introduce a new indentation scope for lists (including list start and end tokens) 821 while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { 822 const tokenInfo = formattingScanner.readTokenInfo(parent); 823 if (tokenInfo.token.end > nodes.pos) { 824 // stop when formatting scanner moves past the beginning of node list 825 break; 826 } 827 else if (tokenInfo.token.kind === listStartToken) { 828 // consume list start token 829 startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; 830 831 consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); 832 833 let indentationOnListStartToken: number; 834 if (indentationOnLastIndentedLine !== Constants.Unknown) { 835 // scanner just processed list start token so consider last indentation as list indentation 836 // function foo(): { // last indentation was 0, list item will be indented based on this value 837 // foo: number; 838 // }: {}; 839 indentationOnListStartToken = indentationOnLastIndentedLine; 840 } 841 else { 842 const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); 843 indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); 844 } 845 846 listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 847 } 848 else { 849 // consume any tokens that precede the list as child elements of 'node' using its indentation scope 850 consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); 851 } 852 } 853 } 854 855 let inheritedIndentation = Constants.Unknown; 856 for (let i = 0; i < nodes.length; i++) { 857 const child = nodes[i]; 858 inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0); 859 } 860 861 const listEndToken = getCloseTokenForOpenToken(listStartToken); 862 if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) { 863 let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); 864 if (tokenInfo.token.kind === SyntaxKind.CommaToken) { 865 // consume the comma 866 consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent); 867 tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; 868 } 869 870 // consume the list end token only if it is still belong to the parent 871 // there might be the case when current token matches end token but does not considered as one 872 // function (x: function) <-- 873 // without this check close paren will be interpreted as list end token for function expression which is wrong 874 if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) { 875 // consume list end token 876 consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); 877 } 878 } 879 } 880 881 function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void { 882 Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); 883 884 const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); 885 let indentToken = false; 886 887 if (currentTokenInfo.leadingTrivia) { 888 processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); 889 } 890 891 let lineAction = LineAction.None; 892 const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); 893 894 const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos); 895 if (isTokenInRange) { 896 const rangeHasError = rangeContainsError(currentTokenInfo.token); 897 // save previousRange since processRange will overwrite this value with current one 898 const savePreviousRange = previousRange; 899 lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation); 900 // do not indent comments\token if token range overlaps with some error 901 if (!rangeHasError) { 902 if (lineAction === LineAction.None) { 903 // indent token only if end line of previous range does not match start line of the token 904 const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line; 905 indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine; 906 } 907 else { 908 indentToken = lineAction === LineAction.LineAdded; 909 } 910 } 911 } 912 913 if (currentTokenInfo.trailingTrivia) { 914 previousRangeTriviaEnd = last(currentTokenInfo.trailingTrivia).end; 915 processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); 916 } 917 918 if (indentToken) { 919 const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ? 920 dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) : 921 Constants.Unknown; 922 923 let indentNextTokenOrTrivia = true; 924 if (currentTokenInfo.leadingTrivia) { 925 const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container); 926 indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, 927 item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false)); 928 } 929 930 // indent token only if is it is in target range and does not overlap with any error ranges 931 if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) { 932 insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded); 933 934 lastIndentedLine = tokenStart.line; 935 indentationOnLastIndentedLine = tokenIndentation; 936 } 937 } 938 939 formattingScanner.advance(); 940 941 childContextNode = parent; 942 } 943 } 944 945 function indentTriviaItems( 946 trivia: TextRangeWithKind[], 947 commentIndentation: number, 948 indentNextTokenOrTrivia: boolean, 949 indentSingleLine: (item: TextRangeWithKind) => void) { 950 for (const triviaItem of trivia) { 951 const triviaInRange = rangeContainsRange(originalRange, triviaItem); 952 switch (triviaItem.kind) { 953 case SyntaxKind.MultiLineCommentTrivia: 954 if (triviaInRange) { 955 indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); 956 } 957 indentNextTokenOrTrivia = false; 958 break; 959 case SyntaxKind.SingleLineCommentTrivia: 960 if (indentNextTokenOrTrivia && triviaInRange) { 961 indentSingleLine(triviaItem); 962 } 963 indentNextTokenOrTrivia = false; 964 break; 965 case SyntaxKind.NewLineTrivia: 966 indentNextTokenOrTrivia = true; 967 break; 968 } 969 } 970 return indentNextTokenOrTrivia; 971 } 972 973 function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void { 974 for (const triviaItem of trivia) { 975 if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { 976 const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); 977 processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); 978 } 979 } 980 } 981 982 function processRange(range: TextRangeWithKind, 983 rangeStart: LineAndCharacter, 984 parent: Node, 985 contextNode: Node, 986 dynamicIndentation: DynamicIndentation): LineAction { 987 988 const rangeHasError = rangeContainsError(range); 989 let lineAction = LineAction.None; 990 if (!rangeHasError) { 991 if (!previousRange) { 992 // trim whitespaces starting from the beginning of the span up to the current line 993 const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos); 994 trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); 995 } 996 else { 997 lineAction = 998 processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation); 999 } 1000 } 1001 1002 previousRange = range; 1003 previousRangeTriviaEnd = range.end; 1004 previousParent = parent; 1005 previousRangeStartLine = rangeStart.line; 1006 1007 return lineAction; 1008 } 1009 1010 function processPair(currentItem: TextRangeWithKind, 1011 currentStartLine: number, 1012 currentParent: Node, 1013 previousItem: TextRangeWithKind, 1014 previousStartLine: number, 1015 previousParent: Node, 1016 contextNode: Node, 1017 dynamicIndentation: DynamicIndentation | undefined): LineAction { 1018 1019 formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); 1020 1021 const rules = getRules(formattingContext); 1022 1023 let trimTrailingWhitespaces = formattingContext.options.trimTrailingWhitespace !== false; 1024 let lineAction = LineAction.None; 1025 if (rules) { 1026 // Apply rules in reverse order so that higher priority rules (which are first in the array) 1027 // win in a conflict with lower priority rules. 1028 forEachRight(rules, rule => { 1029 lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); 1030 if (dynamicIndentation) { 1031 switch (lineAction) { 1032 case LineAction.LineRemoved: 1033 // Handle the case where the next line is moved to be the end of this line. 1034 // In this case we don't indent the next line in the next pass. 1035 if (currentParent.getStart(sourceFile) === currentItem.pos) { 1036 dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode); 1037 } 1038 break; 1039 case LineAction.LineAdded: 1040 // Handle the case where token2 is moved to the new line. 1041 // In this case we indent token2 in the next pass but we set 1042 // sameLineIndent flag to notify the indenter that the indentation is within the line. 1043 if (currentParent.getStart(sourceFile) === currentItem.pos) { 1044 dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode); 1045 } 1046 break; 1047 default: 1048 Debug.assert(lineAction === LineAction.None); 1049 } 1050 } 1051 1052 // 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 1053 trimTrailingWhitespaces = trimTrailingWhitespaces && !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines; 1054 }); 1055 } 1056 else { 1057 trimTrailingWhitespaces = trimTrailingWhitespaces && currentItem.kind !== SyntaxKind.EndOfFileToken; 1058 } 1059 1060 if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { 1061 // 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 1062 trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); 1063 } 1064 1065 return lineAction; 1066 } 1067 1068 function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void { 1069 const indentationString = getIndentationString(indentation, options); 1070 if (lineAdded) { 1071 // new line is added before the token by the formatting rules 1072 // insert indentation string at the very beginning of the token 1073 recordReplace(pos, 0, indentationString); 1074 } 1075 else { 1076 const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos); 1077 const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); 1078 if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { 1079 recordReplace(startLinePosition, tokenStart.character, indentationString); 1080 } 1081 } 1082 } 1083 1084 function characterToColumn(startLinePosition: number, characterInLine: number): number { 1085 let column = 0; 1086 for (let i = 0; i < characterInLine; i++) { 1087 if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) { 1088 column += options.tabSize! - column % options.tabSize!; 1089 } 1090 else { 1091 column++; 1092 } 1093 } 1094 return column; 1095 } 1096 1097 function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { 1098 return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); 1099 } 1100 1101 function indentMultilineComment(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true) { 1102 // split comment in lines 1103 let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line; 1104 const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line; 1105 if (startLine === endLine) { 1106 if (!firstLineIsIndented) { 1107 // treat as single line comment 1108 insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false); 1109 } 1110 return; 1111 } 1112 1113 const parts: TextRange[] = []; 1114 let startPos = commentRange.pos; 1115 for (let line = startLine; line < endLine; line++) { 1116 const endOfLine = getEndLinePosition(line, sourceFile); 1117 parts.push({ pos: startPos, end: endOfLine }); 1118 startPos = getStartPositionOfLine(line + 1, sourceFile); 1119 } 1120 1121 if (indentFinalLine) { 1122 parts.push({ pos: startPos, end: commentRange.end }); 1123 } 1124 1125 if (parts.length === 0) return; 1126 1127 const startLinePos = getStartPositionOfLine(startLine, sourceFile); 1128 1129 const nonWhitespaceColumnInFirstPart = 1130 SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); 1131 1132 let startIndex = 0; 1133 if (firstLineIsIndented) { 1134 startIndex = 1; 1135 startLine++; 1136 } 1137 1138 // shift all parts on the delta size 1139 const delta = indentation - nonWhitespaceColumnInFirstPart.column; 1140 for (let i = startIndex; i < parts.length; i++ , startLine++) { 1141 const startLinePos = getStartPositionOfLine(startLine, sourceFile); 1142 const nonWhitespaceCharacterAndColumn = 1143 i === 0 1144 ? nonWhitespaceColumnInFirstPart 1145 : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); 1146 const newIndentation = nonWhitespaceCharacterAndColumn.column + delta; 1147 if (newIndentation > 0) { 1148 const indentationString = getIndentationString(newIndentation, options); 1149 recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString); 1150 } 1151 else { 1152 recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character); 1153 } 1154 } 1155 } 1156 1157 function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { 1158 for (let line = line1; line < line2; line++) { 1159 const lineStartPosition = getStartPositionOfLine(line, sourceFile); 1160 const lineEndPosition = getEndLinePosition(line, sourceFile); 1161 1162 // do not trim whitespaces in comments or template expression 1163 if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { 1164 continue; 1165 } 1166 1167 const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); 1168 if (whitespaceStart !== -1) { 1169 Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); 1170 recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); 1171 } 1172 } 1173 } 1174 1175 /** 1176 * @param start The position of the first character in range 1177 * @param end The position of the last character in range 1178 */ 1179 function getTrailingWhitespaceStartPosition(start: number, end: number) { 1180 let pos = end; 1181 while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { 1182 pos--; 1183 } 1184 if (pos !== end) { 1185 return pos + 1; 1186 } 1187 return -1; 1188 } 1189 1190 /** 1191 * Trimming will be done for lines after the previous range. 1192 * Exclude comments as they had been previously processed. 1193 */ 1194 function trimTrailingWhitespacesForRemainingRange(trivias: TextRangeWithKind<SyntaxKind>[]) { 1195 let startPos = previousRange ? previousRange.end : originalRange.pos; 1196 for (const trivia of trivias) { 1197 if (isComment(trivia.kind)) { 1198 if (startPos < trivia.pos) { 1199 trimTrailingWitespacesForPositions(startPos, trivia.pos - 1, previousRange); 1200 } 1201 1202 startPos = trivia.end + 1; 1203 } 1204 } 1205 1206 if (startPos < originalRange.end) { 1207 trimTrailingWitespacesForPositions(startPos, originalRange.end, previousRange); 1208 } 1209 } 1210 1211 function trimTrailingWitespacesForPositions(startPos: number, endPos: number, previousRange: TextRangeWithKind) { 1212 const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; 1213 const endLine = sourceFile.getLineAndCharacterOfPosition(endPos).line; 1214 1215 trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); 1216 } 1217 1218 function recordDelete(start: number, len: number) { 1219 if (len) { 1220 edits.push(createTextChangeFromStartLength(start, len, "")); 1221 } 1222 } 1223 1224 function recordReplace(start: number, len: number, newText: string) { 1225 if (len || newText) { 1226 edits.push(createTextChangeFromStartLength(start, len, newText)); 1227 } 1228 } 1229 1230 function recordInsert(start: number, text: string) { 1231 if (text) { 1232 edits.push(createTextChangeFromStartLength(start, 0, text)); 1233 } 1234 } 1235 1236 function applyRuleEdits(rule: Rule, 1237 previousRange: TextRangeWithKind, 1238 previousStartLine: number, 1239 currentRange: TextRangeWithKind, 1240 currentStartLine: number 1241 ): LineAction { 1242 const onLaterLine = currentStartLine !== previousStartLine; 1243 switch (rule.action) { 1244 case RuleAction.StopProcessingSpaceActions: 1245 // no action required 1246 return LineAction.None; 1247 case RuleAction.DeleteSpace: 1248 if (previousRange.end !== currentRange.pos) { 1249 // delete characters starting from t1.end up to t2.pos exclusive 1250 recordDelete(previousRange.end, currentRange.pos - previousRange.end); 1251 return onLaterLine ? LineAction.LineRemoved : LineAction.None; 1252 } 1253 break; 1254 case RuleAction.DeleteToken: 1255 recordDelete(previousRange.pos, previousRange.end - previousRange.pos); 1256 break; 1257 case RuleAction.InsertNewLine: 1258 // exit early if we on different lines and rule cannot change number of newlines 1259 // if line1 and line2 are on subsequent lines then no edits are required - ok to exit 1260 // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines 1261 if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { 1262 return LineAction.None; 1263 } 1264 1265 // edit should not be applied if we have one line feed between elements 1266 const lineDelta = currentStartLine - previousStartLine; 1267 if (lineDelta !== 1) { 1268 recordReplace(previousRange.end, currentRange.pos - previousRange.end, getNewLineOrDefaultFromHost(host, options)); 1269 return onLaterLine ? LineAction.None : LineAction.LineAdded; 1270 } 1271 break; 1272 case RuleAction.InsertSpace: 1273 // exit early if we on different lines and rule cannot change number of newlines 1274 if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { 1275 return LineAction.None; 1276 } 1277 1278 const posDelta = currentRange.pos - previousRange.end; 1279 if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { 1280 recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); 1281 return onLaterLine ? LineAction.LineRemoved : LineAction.None; 1282 } 1283 break; 1284 case RuleAction.InsertTrailingSemicolon: 1285 recordInsert(previousRange.end, ";"); 1286 } 1287 return LineAction.None; 1288 } 1289 } 1290 1291 const enum LineAction { None, LineAdded, LineRemoved } 1292 1293 /** 1294 * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. 1295 */ 1296 export function getRangeOfEnclosingComment( 1297 sourceFile: SourceFile, 1298 position: number, 1299 precedingToken?: Node | null, 1300 tokenAtPosition = getTokenAtPosition(sourceFile, position), 1301 ): CommentRange | undefined { 1302 const jsdoc = findAncestor(tokenAtPosition, isJSDoc); 1303 if (jsdoc) tokenAtPosition = jsdoc.parent; 1304 const tokenStart = tokenAtPosition.getStart(sourceFile); 1305 if (tokenStart <= position && position < tokenAtPosition.getEnd()) { 1306 return undefined; 1307 } 1308 1309 // eslint-disable-next-line no-null/no-null 1310 precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken; 1311 1312 // Between two consecutive tokens, all comments are either trailing on the former 1313 // or leading on the latter (and none are in both lists). 1314 const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); 1315 const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); 1316 const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); 1317 return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) || 1318 // The end marker of a single-line comment does not include the newline character. 1319 // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position): 1320 // 1321 // // asdf ^\n 1322 // 1323 // But for closed multi-line comments, we don't want to be inside the comment in the following case: 1324 // 1325 // /* asdf */^ 1326 // 1327 // However, unterminated multi-line comments *do* contain their end. 1328 // 1329 // Internally, we represent the end of the comment at the newline and closing '/', respectively. 1330 // 1331 position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); 1332 } 1333 1334 function getOpenTokenForList(node: Node, list: readonly Node[]) { 1335 switch (node.kind) { 1336 case SyntaxKind.Constructor: 1337 case SyntaxKind.FunctionDeclaration: 1338 case SyntaxKind.FunctionExpression: 1339 case SyntaxKind.MethodDeclaration: 1340 case SyntaxKind.MethodSignature: 1341 case SyntaxKind.ArrowFunction: 1342 case SyntaxKind.CallSignature: 1343 case SyntaxKind.ConstructSignature: 1344 case SyntaxKind.FunctionType: 1345 case SyntaxKind.ConstructorType: 1346 case SyntaxKind.GetAccessor: 1347 case SyntaxKind.SetAccessor: 1348 if ((node as FunctionDeclaration).typeParameters === list) { 1349 return SyntaxKind.LessThanToken; 1350 } 1351 else if ((node as FunctionDeclaration).parameters === list) { 1352 return SyntaxKind.OpenParenToken; 1353 } 1354 break; 1355 case SyntaxKind.CallExpression: 1356 case SyntaxKind.NewExpression: 1357 if ((node as CallExpression).typeArguments === list) { 1358 return SyntaxKind.LessThanToken; 1359 } 1360 else if ((node as CallExpression).arguments === list) { 1361 return SyntaxKind.OpenParenToken; 1362 } 1363 break; 1364 case SyntaxKind.ClassDeclaration: 1365 case SyntaxKind.ClassExpression: 1366 case SyntaxKind.InterfaceDeclaration: 1367 case SyntaxKind.TypeAliasDeclaration: 1368 if ((node as ClassDeclaration).typeParameters === list) { 1369 return SyntaxKind.LessThanToken; 1370 } 1371 break; 1372 case SyntaxKind.TypeReference: 1373 case SyntaxKind.TaggedTemplateExpression: 1374 case SyntaxKind.TypeQuery: 1375 case SyntaxKind.ExpressionWithTypeArguments: 1376 case SyntaxKind.ImportType: 1377 if ((node as TypeReferenceNode).typeArguments === list) { 1378 return SyntaxKind.LessThanToken; 1379 } 1380 break; 1381 case SyntaxKind.TypeLiteral: 1382 return SyntaxKind.OpenBraceToken; 1383 } 1384 1385 return SyntaxKind.Unknown; 1386 } 1387 1388 function getCloseTokenForOpenToken(kind: SyntaxKind) { 1389 switch (kind) { 1390 case SyntaxKind.OpenParenToken: 1391 return SyntaxKind.CloseParenToken; 1392 case SyntaxKind.LessThanToken: 1393 return SyntaxKind.GreaterThanToken; 1394 case SyntaxKind.OpenBraceToken: 1395 return SyntaxKind.CloseBraceToken; 1396 } 1397 1398 return SyntaxKind.Unknown; 1399 } 1400 1401 let internedSizes: { tabSize: number; indentSize: number; }; 1402 let internedTabsIndentation: string[] | undefined; 1403 let internedSpacesIndentation: string[] | undefined; 1404 1405 export function getIndentationString(indentation: number, options: EditorSettings): string { 1406 // reset interned strings if FormatCodeOptions were changed 1407 const resetInternedStrings = 1408 !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); 1409 1410 if (resetInternedStrings) { 1411 internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! }; 1412 internedTabsIndentation = internedSpacesIndentation = undefined; 1413 } 1414 1415 if (!options.convertTabsToSpaces) { 1416 const tabs = Math.floor(indentation / options.tabSize!); 1417 const spaces = indentation - tabs * options.tabSize!; 1418 1419 let tabString: string; 1420 if (!internedTabsIndentation) { 1421 internedTabsIndentation = []; 1422 } 1423 1424 if (internedTabsIndentation[tabs] === undefined) { 1425 internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs); 1426 } 1427 else { 1428 tabString = internedTabsIndentation[tabs]; 1429 } 1430 1431 return spaces ? tabString + repeatString(" ", spaces) : tabString; 1432 } 1433 else { 1434 let spacesString: string; 1435 const quotient = Math.floor(indentation / options.indentSize!); 1436 const remainder = indentation % options.indentSize!; 1437 if (!internedSpacesIndentation) { 1438 internedSpacesIndentation = []; 1439 } 1440 1441 if (internedSpacesIndentation[quotient] === undefined) { 1442 spacesString = repeatString(" ", options.indentSize! * quotient); 1443 internedSpacesIndentation[quotient] = spacesString; 1444 } 1445 else { 1446 spacesString = internedSpacesIndentation[quotient]; 1447 } 1448 1449 return remainder ? spacesString + repeatString(" ", remainder) : spacesString; 1450 } 1451 } 1452} 1453