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