diff --git a/src/services/formatting.ts b/src/services/formatting.ts index 44068e744a0..f4acd193b02 100644 --- a/src/services/formatting.ts +++ b/src/services/formatting.ts @@ -19,6 +19,16 @@ module ts.formatting { Unknown = -1 } + /* + * Indentation for the scope that can be dynamically recomputed. + * i.e + * while(true) + * { var x; + * } + * Normally indentation is applied only to the first token in line so at glance 'var' should not be touched. + * However if some format rule removes new line between ')' and '{' 'var' will become + * the first token in line so it should be indented + */ interface DynamicIndentation { getIndentationForToken(tokenLine: number, tokenKind: SyntaxKind): number; getIndentationForComment(owningToken: SyntaxKind): number; @@ -88,7 +98,7 @@ module ts.formatting { return undefined; } - // walk up and search for the parent node that ends at the same position with precedingToken + // walk up and search for the parent node that ends at the same position with precedingToken. var current = precedingToken; while (current && current.parent && @@ -99,7 +109,9 @@ module ts.formatting { return current; } - + + // Returns true if node is a element in some list in parent + // i.e. parent is class declaration with the list of members and node is one of members. function isListElement(parent: Node, node: Node): boolean { switch (parent.kind) { case SyntaxKind.ClassDeclaration: @@ -120,6 +132,7 @@ module ts.formatting { return false; } + /** find node that fully contains given text range */ function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { return find(sourceFile); @@ -129,6 +142,10 @@ module ts.formatting { } } + /** formatting is not applied to ranges that contain parse errors. + * This function will return a predicate that for a given text range will tell + * if there are any parse errors that overlap with the range. + */ function prepareRangeContainsErrorFunction(errors: Diagnostic[], originalRange: TextRange): (r: TextRange) => boolean { if (!errors.length) { return rangeHasNoErrors; @@ -136,7 +153,7 @@ module ts.formatting { // pick only errors that fall in range var sorted = errors - .filter(d => d.isParseError && (d.start >= originalRange.pos && d.start + d.length < originalRange.end)) + .filter(d => d.isParseError && rangeOverlapsWithStartEnd(originalRange, d.start, d.start + d.length)) .sort((e1, e2) => e1.start - e2.start); if (!sorted.length) { @@ -146,17 +163,22 @@ module ts.formatting { var index = 0; return r => { + // in current implementation sequence of arguments [r1, r2...] is monotonically increasing. + // 'index' tracks the index of the most recent error that was checked. while (true) { if (index >= sorted.length) { + // all errors in the range were already checked -> no error in specified range return false; } var error = sorted[index]; if (r.end <= error.start) { + // specified range ends before the error refered by 'index' - no error in range return false; } if (startEndOverlapsWithStartEnd(r.pos, r.end, error.start, error.start + error.length)) { + // specified range overlaps with error range return true; } @@ -177,11 +199,12 @@ module ts.formatting { var rangeContainsError = prepareRangeContainsErrorFunction(sourceFile.syntacticErrors, originalRange); - // formatting context to be used by rules provider to get rules + // formatting context is used by rules provider var formattingContext = new FormattingContext(sourceFile, requestKind); var formattingScanner = getFormattingScanner(sourceFile, originalRange.pos, originalRange.end); + // find the smallest node that fully wraps the range and compute the initial indentation for the node var enclosingNode = findEnclosingNode(originalRange, sourceFile); var initialIndentation = SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, options); @@ -191,7 +214,6 @@ module ts.formatting { var previousRangeStartLine: number; var edits: TextChange[] = []; - var lastTriviaWasNewLine: boolean; formattingScanner.advance(); @@ -207,7 +229,17 @@ module ts.formatting { // local functions - function tryComputeIndentationForListItem(startPos: number, endPos: number, effectiveParentStartLine: number, range: TextRange, inheritedIndentation: number): number { + /** Tries to compute the indentation for a list element. + * If list element is not in range then function will pick its actual indentation + * so it can be pushed downstream as inherited indentation. + * If list element is in the range - its indentation will be equal to inherited indentation from its predecessors. + */ + function tryComputeIndentationForListItem(startPos: number, + endPos: number, + parentStartLine: number, + range: TextRange, + inheritedIndentation: number): number { + if (rangeOverlapsWithStartEnd(range, startPos, endPos)) { if (inheritedIndentation !== Constants.Unknown) { return inheritedIndentation; @@ -217,7 +249,7 @@ module ts.formatting { var startLine = sourceFile.getLineAndCharacterFromPosition(startPos).line; var startLinePosition = getStartLinePositionForPosition(startPos, sourceFile); var column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); - if (startLine !== effectiveParentStartLine || startPos === column) { + if (startLine !== parentStartLine || startPos === column) { return column } } @@ -238,6 +270,10 @@ module ts.formatting { if (indentation === Constants.Unknown) { if (isSomeBlock(node.kind)) { delta = options.IndentSize; + // blocks should be indented in + // - other blocks + // - source file + // - switch\default clauses if (isSomeBlock(parent.kind) || parent.kind === SyntaxKind.SourceFile || parent.kind === SyntaxKind.CaseClause || @@ -250,7 +286,7 @@ module ts.formatting { } } else { - if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { + if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { indentation = parentIndentation.getIndentation(); } else { @@ -264,6 +300,9 @@ module ts.formatting { } if (effectiveParentStartLine === startLine) { + // if node is located on the same line with the parent + // - inherit indentation from the parent + // - push children if either parent of node itself has non-zero delta indentation = parentIndentation.getIndentation(); delta = Math.min(options.IndentSize, parentIndentation.getDelta() + delta); } @@ -275,7 +314,7 @@ module ts.formatting { function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { return { - getIndentationForComment: (kind) => { + getIndentationForComment: kind => { switch (kind) { // preceding comment to the token that closes the indentation scope inherits the indentation from the scope // .. { @@ -303,7 +342,7 @@ module ts.formatting { }, getIndentation: () => indentation, getDelta: () => delta, - recomputeIndentation: (lineAdded) => { + recomputeIndentation: lineAdded => { if (node.parent && SmartIndenter.shouldIndentChildNode(node.parent.kind, node.kind)) { if (lineAdded) { indentation += options.IndentSize; @@ -311,6 +350,7 @@ module ts.formatting { else { indentation -= options.IndentSize; } + if (SmartIndenter.shouldIndentChildNode(node.kind, SyntaxKind.Unknown)) { delta = options.IndentSize; } @@ -340,43 +380,48 @@ module ts.formatting { var listEndToken = getCloseTokenForOpenToken(listStartToken); var listIndentation = nodeIndentation; + var startLine = nodeStartLine; if (listStartToken !== SyntaxKind.Unknown) { - // try to consume open token + // introduce a new indentation scope for lists (including list start and end tokens) while (formattingScanner.isOnToken()) { var tokenInfo = formattingScanner.readTokenInfo(node); if (tokenInfo.token.end > nodes.pos) { break; } else if (tokenInfo.token.kind === listStartToken) { - var tokenStartLine = sourceFile.getLineAndCharacterFromPosition(tokenInfo.token.pos).line; - var indentation = computeIndentation(tokenInfo.token, tokenStartLine, Constants.Unknown, node, nodeIndentation, tokenStartLine); + // consume list start token + startLine = sourceFile.getLineAndCharacterFromPosition(tokenInfo.token.pos).line; + var indentation = + computeIndentation(tokenInfo.token, startLine, Constants.Unknown, node, nodeIndentation, startLine); + listIndentation = getDynamicIndentation(node, nodeStartLine, indentation.indentation, indentation.delta); consumeTokenAndAdvanceScanner(tokenInfo, node, listIndentation); } else { + // consume any tokens that precede the list as child elements of 'node' using its indentation scope consumeTokenAndAdvanceScanner(tokenInfo, node, nodeIndentation); } } } var inheritedIndentation = Constants.Unknown; - var effectiveStartLine = tokenStartLine || nodeStartLine; for (var i = 0, len = nodes.length; i < len; ++i) { - inheritedIndentation = processChildNode(nodes[i], inheritedIndentation, listIndentation, effectiveStartLine, /*isListElement*/ true) + inheritedIndentation = processChildNode(nodes[i], inheritedIndentation, listIndentation, startLine, /*isListElement*/ true) } if (listEndToken !== SyntaxKind.Unknown) { if (formattingScanner.isOnToken()) { var tokenInfo = formattingScanner.readTokenInfo(node); if (tokenInfo.token.kind === listEndToken) { + // consume list end token consumeTokenAndAdvanceScanner(tokenInfo, node, listIndentation); } } } }); - // this eats up last tokens in the node + // proceed any tokens in the node that are located after child nodes while (formattingScanner.isOnToken()) { var tokenInfo = formattingScanner.readTokenInfo(node); if (tokenInfo.token.end > node.end) { @@ -389,7 +434,7 @@ module ts.formatting { child: Node, inheritedIndentation: number, nodeIndentation: DynamicIndentation, - effectiveParentStartLine: number, + parentStartLine: number, isListItem: boolean): number { var childStartPos = child.getStart(sourceFile); @@ -397,13 +442,14 @@ module ts.formatting { var childStart = sourceFile.getLineAndCharacterFromPosition(childStartPos); var childIndentationAmount = isListItem - ? tryComputeIndentationForListItem(childStartPos, child.end, effectiveParentStartLine, originalRange, inheritedIndentation) + ? tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation) : Constants.Unknown; if (isListItem && childIndentationAmount !== Constants.Unknown) { inheritedIndentation = childIndentationAmount; } + // child node is outside the target range - do not dive inside if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { return inheritedIndentation; } @@ -413,6 +459,7 @@ module ts.formatting { } while (formattingScanner.isOnToken()) { + // proceed any parent tokens that are located prior to child.getStart() var tokenInfo = formattingScanner.readTokenInfo(node); if (tokenInfo.token.end > childStartPos) { break; @@ -426,13 +473,14 @@ module ts.formatting { } if (isToken(child)) { + // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules var tokenInfo = formattingScanner.readTokenInfo(node); Debug.assert(tokenInfo.token.end === child.end); consumeTokenAndAdvanceScanner(tokenInfo, node, nodeIndentation); return inheritedIndentation; } - var childIndentation = computeIndentation(child, childStart.line, childIndentationAmount, node, nodeIndentation, effectiveParentStartLine); + var childIndentation = computeIndentation(child, childStart.line, childIndentationAmount, node, nodeIndentation, parentStartLine); processNode(child, childContextNode, childStart.line, childIndentation.indentation, childIndentation.delta); @@ -444,7 +492,8 @@ module ts.formatting { function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, indentation: DynamicIndentation): void { Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); - lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); + var lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); + var indentToken = false; if (currentTokenInfo.leadingTrivia) { processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, indentation); @@ -452,17 +501,17 @@ module ts.formatting { var lineAdded: boolean; var isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); - var indentToken: boolean = true; var tokenStart = sourceFile.getLineAndCharacterFromPosition(currentTokenInfo.token.pos); if (isTokenInRange) { + // save prevStartLine since processRange will overwrite this value with current ones var prevStartLine = previousRangeStartLine; lineAdded = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, indentation); if (lineAdded !== undefined) { indentToken = lineAdded; } else { - indentToken = tokenStart.line !== prevStartLine; + indentToken = lastTriviaWasNewLine && tokenStart.line !== prevStartLine; } } @@ -470,7 +519,7 @@ module ts.formatting { processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, indentation); } - if (lastTriviaWasNewLine && indentToken) { + if (indentToken) { var indentNextTokenOrTrivia = true; if (currentTokenInfo.leadingTrivia) { for (var i = 0, len = currentTokenInfo.leadingTrivia.length; i < len; ++i) { @@ -482,12 +531,14 @@ module ts.formatting { var triviaStartLine = sourceFile.getLineAndCharacterFromPosition(triviaItem.pos).line; switch (triviaItem.kind) { case SyntaxKind.MultiLineCommentTrivia: - indentMultilineComment(triviaItem, indentation.getIndentationForComment(currentTokenInfo.token.kind), /*firstLineIsIndented*/ !indentNextTokenOrTrivia); + var commentIndentation = indentation.getIndentationForComment(currentTokenInfo.token.kind); + indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); indentNextTokenOrTrivia = false; break; case SyntaxKind.SingleLineCommentTrivia: if (indentNextTokenOrTrivia) { - insertIndentation(triviaItem.pos, indentation.getIndentationForComment(currentTokenInfo.token.kind), /*lineAdded*/ false); + var commentIndentation = indentation.getIndentationForComment(currentTokenInfo.token.kind); + insertIndentation(triviaItem.pos, commentIndentation, /*lineAdded*/ false); indentNextTokenOrTrivia = false; } break; @@ -498,8 +549,10 @@ module ts.formatting { } } + // indent token only if is it is in target range and does not overlap with any error ranges if (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) { - insertIndentation(currentTokenInfo.token.pos, indentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind), lineAdded); + var tokenIndentation = indentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind); + insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAdded); } } @@ -529,7 +582,8 @@ module ts.formatting { trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); } else { - lineAdded = processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, indentation) + lineAdded = + processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, indentation) } } @@ -562,7 +616,6 @@ module ts.formatting { if (rule.Operation.Action & (RuleAction.Space | RuleAction.Delete) && currentStartLine !== previousStartLine) { // Handle the case where the next line is moved to be the end of this line. // In this case we don't indent the next line in the next pass. - lastTriviaWasNewLine = false; if (currentParent.getStart(sourceFile) === currentItem.pos) { lineAdded = false; } @@ -571,7 +624,6 @@ module ts.formatting { // Handle the case where token2 is moved to the new line. // In this case we indent token2 in the next pass but we set // sameLineIndent flag to notify the indenter that the indentation is within the line. - lastTriviaWasNewLine = true; if (currentParent.getStart(sourceFile) === currentItem.pos) { lineAdded = true; } @@ -601,13 +653,15 @@ module ts.formatting { function insertIndentation(pos: number, indentation: number, lineAdded: boolean): void { var indentationString = getIndentationString(indentation, options); if (lineAdded) { + // new line is added before the token by the formatting rules + // insert indentation string at the very beginning of the token recordReplace(pos, 0, indentationString); } else { - var tokenRange = sourceFile.getLineAndCharacterFromPosition(pos); - if (indentation !== tokenRange.character - 1) { - var startLinePosition = getStartPositionOfLine(tokenRange.line, sourceFile); - recordReplace(startLinePosition, tokenRange.character - 1, indentationString); + var tokenStart = sourceFile.getLineAndCharacterFromPosition(pos); + if (indentation !== tokenStart.character - 1) { + var startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); + recordReplace(startLinePosition, tokenStart.character - 1, indentationString); } } } @@ -799,8 +853,8 @@ module ts.formatting { return SyntaxKind.Unknown; } - function getCloseTokenForOpenToken(k: SyntaxKind) { - switch (k) { + function getCloseTokenForOpenToken(kind: SyntaxKind) { + switch (kind) { case SyntaxKind.OpenParenToken: return SyntaxKind.CloseParenToken; case SyntaxKind.LessThanToken: diff --git a/src/services/formatting/formattingScanner.ts b/src/services/formatting/formattingScanner.ts index ed7b84dd4bb..d774e34da41 100644 --- a/src/services/formatting/formattingScanner.ts +++ b/src/services/formatting/formattingScanner.ts @@ -1,7 +1,7 @@ /// module ts.formatting { - var scanner = createScanner(ScriptTarget.ES5, /*skipTrivia*/ false); + var scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false); export interface FormattingScanner { advance(): void; @@ -64,7 +64,8 @@ module ts.formatting { var t: SyntaxKind; var pos = scanner.getStartPos(); - + + // Read leading trivia and token while (pos < endPos) { var t = scanner.getToken(); if (!isTrivia(t)) { @@ -116,14 +117,16 @@ module ts.formatting { function readTokenInfo(n: Node): TokenInfo { if (!isOnToken()) { + // scanner is not on the token (either advance was not called yet or scanner is already past the end position) return { leadingTrivia: leadingTrivia, trailingTrivia: undefined, token: undefined }; - } + // normally scanner returns the smallest available token + // check the kind of context node to determine if scanner should have more greedy behavior and consume more text. var expectedScanAction = shouldRescanGreaterThanToken(n) ? ScanAction.RescanGreaterThanToken @@ -132,10 +135,14 @@ module ts.formatting { : ScanAction.Scan if (lastTokenInfo && expectedScanAction === lastScanAction) { + // readTokenInfo was called before with the same expected scan action. + // No need to re-scan text, return existing 'lastTokenInfo' return lastTokenInfo; } if (scanner.getStartPos() !== savedPos) { + Debug.assert(lastTokenInfo !== undefined); + // readTokenInfo was called before but scan action differs - rescan text scanner.setTextPos(savedPos); scanner.scan(); } @@ -162,6 +169,7 @@ module ts.formatting { kind: currentToken } + // consume trailing trivia while(scanner.getStartPos() < endPos) { currentToken = scanner.scan(); if (!isTrivia(currentToken)) {