diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index f61d4403eec..c9672b87cd1 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -139,7 +139,7 @@ module ts { function writeLiteral(s: string) { if (s && s.length) { write(s); - var lineStartsOfS = getLineStarts(s); + var lineStartsOfS = computeLineStarts(s); if (lineStartsOfS.length > 1) { lineCount = lineCount + lineStartsOfS.length - 1; linePos = output.length - s.length + lineStartsOfS[lineStartsOfS.length - 1]; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index c7818f2e1bb..d43bff7984a 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -948,18 +948,16 @@ module ts { }; })(); - function getLineAndCharacterlFromSourcePosition(position: number) { - if (!lineStarts) { - lineStarts = getLineStarts(sourceText); - } - return getLineAndCharacterOfPosition(lineStarts, position); + function getLineStarts(): number[] { + return lineStarts || (lineStarts = computeLineStarts(sourceText)); + } + + function getLineAndCharacterFromSourcePosition(position: number) { + return getLineAndCharacterOfPosition(getLineStarts(), position); } function getPositionFromSourceLineAndCharacter(line: number, character: number): number { - if (!lineStarts) { - lineStarts = getLineStarts(sourceText); - } - return getPositionFromLineAndCharacter(lineStarts, line, character); + return getPositionFromLineAndCharacter(getLineStarts(), line, character); } function error(message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): void { @@ -1002,7 +1000,9 @@ module ts { ? file.syntacticErrors[file.syntacticErrors.length - 1].start : -1; if (start !== lastErrorPos) { - file.syntacticErrors.push(createFileDiagnostic(file, start, length, message, arg0, arg1, arg2)); + var diagnostic = createFileDiagnostic(file, start, length, message, arg0, arg1, arg2); + diagnostic.isParseError = true; + file.syntacticErrors.push(diagnostic); } if (lookAheadMode === LookAheadMode.NoErrorYet) { @@ -4273,8 +4273,9 @@ module ts { file = createRootNode(SyntaxKind.SourceFile, 0, sourceText.length, rootNodeFlags); file.filename = normalizePath(filename); file.text = sourceText; - file.getLineAndCharacterFromPosition = getLineAndCharacterlFromSourcePosition; + file.getLineAndCharacterFromPosition = getLineAndCharacterFromSourcePosition; file.getPositionFromLineAndCharacter = getPositionFromSourceLineAndCharacter; + file.getLineStarts = getLineStarts; file.syntacticErrors = []; file.semanticErrors = []; var referenceComments = processReferenceComments(); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 29593ddf92e..24ff3378d59 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -246,7 +246,7 @@ module ts { return tokenStrings[t]; } - export function getLineStarts(text: string): number[] { + export function computeLineStarts(text: string): number[] { var result: number[] = new Array(); var pos = 0; var lineStart = 0; @@ -294,7 +294,7 @@ module ts { } export function positionToLineAndCharacter(text: string, pos: number) { - var lineStarts = getLineStarts(text); + var lineStarts = computeLineStarts(text); return getLineAndCharacterOfPosition(lineStarts, pos); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d7d4ba48b08..e9293070766 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -245,7 +245,11 @@ module ts { FirstLiteralToken = NumericLiteral, LastLiteralToken = NoSubstitutionTemplateLiteral, FirstTemplateToken = NoSubstitutionTemplateLiteral, - LastTemplateToken = TemplateTail + LastTemplateToken = TemplateTail, + FirstOperator = SemicolonToken, + LastOperator = CaretEqualsToken, + FirstBinaryOperator = LessThanToken, + LastBinaryOperator = CaretEqualsToken } export const enum NodeFlags { @@ -627,8 +631,9 @@ module ts { export interface SourceFile extends Block { filename: string; text: string; - getLineAndCharacterFromPosition(position: number): { line: number; character: number }; + getLineAndCharacterFromPosition(position: number): LineAndCharacter; getPositionFromLineAndCharacter(line: number, character: number): number; + getLineStarts(): number[]; amdDependencies: string[]; referencedFiles: FileReference[]; syntacticErrors: Diagnostic[]; @@ -1121,7 +1126,16 @@ module ts { messageText: string; category: DiagnosticCategory; code: number; + /** + * Early error - any error (can be produced at parsing\binding\typechecking step) that blocks emit + */ isEarly?: boolean; + /** + * Parse error - error produced by parser when it scanner returns a token + * that parser does not understand in its current state + * (as opposed to grammar error when parser can interpret the token but interpretation is not legal from the grammar perespective) + */ + isParseError?: boolean; } export enum DiagnosticCategory { @@ -1343,4 +1357,4 @@ module ts { useCaseSensitiveFileNames(): boolean; getNewLine(): string; } -} +} diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index eb338f87027..ffb0ed16cb8 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1021,7 +1021,7 @@ module FourSlash { var resultString = "SpanInfo: " + JSON.stringify(spanInfo); if (spanInfo) { var spanString = this.activeFile.content.substr(spanInfo.start(), spanInfo.length()); - var spanLineMap = ts.getLineStarts(spanString); + var spanLineMap = ts.computeLineStarts(spanString); for (var i = 0; i < spanLineMap.length; i++) { if (!i) { resultString += "\n"; @@ -1035,7 +1035,7 @@ module FourSlash { } private baselineCurrentFileLocations(getSpanAtPos: (pos: number) => TypeScript.TextSpan): string { - var fileLineMap = ts.getLineStarts(this.activeFile.content); + var fileLineMap = ts.computeLineStarts(this.activeFile.content); var nextLine = 0; var resultString = ""; var currentLine: string; diff --git a/src/harness/harness.ts b/src/harness/harness.ts index d385dd2afc7..ab20e561851 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -957,7 +957,7 @@ module Harness { // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so // we have to string-based splitting instead and try to figure out the delimiting chars - var lineStarts = ts.getLineStarts(inputFile.content); + var lineStarts = ts.computeLineStarts(inputFile.content); var lines = inputFile.content.split('\n'); lines.forEach((line, lineIndex) => { if (line.length > 0 && line.charAt(line.length - 1) === '\r') { diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts index f7a6bdbf3e5..f7d6bcfeaa1 100644 --- a/src/harness/sourceMapRecorder.ts +++ b/src/harness/sourceMapRecorder.ts @@ -223,7 +223,7 @@ module Harness.SourceMapRecoder { sourceMapNames = sourceMapData.sourceMapNames; jsFile = currentJsFile; - jsLineMap = ts.getLineStarts(jsFile.code); + jsLineMap = ts.computeLineStarts(jsFile.code); spansOnSingleLine = []; prevWrittenSourcePos = 0; @@ -294,7 +294,7 @@ module Harness.SourceMapRecoder { sourceMapRecoder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]); sourceMapRecoder.WriteLine("-------------------------------------------------------------------"); - tsLineMap = ts.getLineStarts(newSourceFileCode); + tsLineMap = ts.computeLineStarts(newSourceFileCode); tsCode = newSourceFileCode; prevWrittenSourcePos = 0; } @@ -390,7 +390,7 @@ module Harness.SourceMapRecoder { } } - var tsCodeLineMap = ts.getLineStarts(sourceText); + var tsCodeLineMap = ts.computeLineStarts(sourceText); for (var i = 0; i < tsCodeLineMap.length; i++) { writeSourceMapIndent(prevEmittedCol, i == 0 ? markerIds[index] : " >"); sourceMapRecoder.Write(getTextOfLine(i, tsCodeLineMap, sourceText)); diff --git a/src/services/formatting.ts b/src/services/formatting.ts new file mode 100644 index 00000000000..74ff9a11209 --- /dev/null +++ b/src/services/formatting.ts @@ -0,0 +1,953 @@ +/// +/// +/// +/// + +module ts.formatting { + + export interface TextRangeWithKind extends TextRange { + kind: SyntaxKind; + } + + export interface TokenInfo { + leadingTrivia: TextRangeWithKind[]; + token: TextRangeWithKind; + trailingTrivia: TextRangeWithKind[]; + } + + const enum Constants { + 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 adds new line between '}' and 'var' '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; + /** + * Indentation for open and close tokens of the node if it is block or another node that needs special indentation + * ... { + * ......... + * ....} + * ____ - indentation + * ____ - delta + **/ + getIndentation(): number; + /** + * Prefered relative indentation for child nodes. + * Delta is used to carry the indentation info + * foo(bar({ + * $ + * })) + * Both 'foo', 'bar' introduce new indentation with delta = 4, but total indentation in $ is not 8. + * foo: { indentation: 0, delta: 4 } + * bar: { indentation: foo.indentation + foo.delta = 4, delta: 4} however 'foo' and 'bar' are on the same line + * so bar inherits indentation from foo and bar.delta will be 4 + * + */ + getDelta(): number; + /** + * Formatter calls this function when rule adds or deletes new lines from the text + * so indentation scope can adjust values of indentation and delta. + */ + recomputeIndentation(lineAddedByFormatting: boolean): void; + } + + interface Indentation { + indentation: number; + delta: number + } + + export function formatOnEnter(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + var line = sourceFile.getLineAndCharacterFromPosition(position).line; + Debug.assert(line >= 2); + // get the span for the previous\current line + var span = { + // get start position for the previous line + pos: getStartPositionOfLine(line - 1, sourceFile), + // get end position for the current line (end value is exclusive so add 1 to the result) + end: getEndLinePosition(line, sourceFile) + 1 + } + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnEnter); + } + + export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon); + } + + export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace); + } + + export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + var span = { + pos: 0, + end: sourceFile.text.length + }; + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatDocument); + } + + export function formatSelection(start: number, end: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + // format from the beginning of the line + var span = { + pos: getStartLinePositionForPosition(start, sourceFile), + end: end + }; + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection); + } + + function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeOptions, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { + var parent = findOutermostParent(position, expectedLastToken, sourceFile); + if (!parent) { + return []; + } + var span = { + pos: getStartLinePositionForPosition(parent.getStart(sourceFile), sourceFile), + end: parent.end + }; + return formatSpan(span, sourceFile, options, rulesProvider, requestKind); + } + + function findOutermostParent(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node { + var precedingToken = findPrecedingToken(position, sourceFile); + if (!precedingToken || precedingToken.kind !== expectedTokenKind) { + return undefined; + } + + // walk up and search for the parent node that ends at the same position with precedingToken. + // for cases like this + // + // var x = 1; + // while (true) { + // } + // after typing close curly in while statement we want to reformat just the while statement. + // However if we just walk upwards searching for the parent that has the same end value - + // we'll end up with the whole source file. isListElement allows to stop on the list element level + var current = precedingToken; + while (current && + current.parent && + current.parent.end === precedingToken.end && + !isListElement(current.parent, current)) { + current = current.parent; + } + + 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: + case SyntaxKind.InterfaceDeclaration: + return rangeContainsRange((parent).members, node); + case SyntaxKind.ModuleDeclaration: + var body = (parent).body; + return body && body.kind === SyntaxKind.Block && rangeContainsRange((body).statements, node); + case SyntaxKind.SourceFile: + case SyntaxKind.Block: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.ModuleBlock: + return rangeContainsRange((parent).statements, node) + } + + return false; + } + + /** find node that fully contains given text range */ + function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { + return find(sourceFile); + + function find(n: Node): Node { + var candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); + if (candidate) { + var result = find(candidate); + if (result) { + return result; + } + } + + return n; + } + } + + /** 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; + } + + // pick only errors that fall in range + var sorted = errors + .filter(d => d.isParseError && rangeOverlapsWithStartEnd(originalRange, d.start, d.start + d.length)) + .sort((e1, e2) => e1.start - e2.start); + + if (!sorted.length) { + return rangeHasNoErrors; + } + + 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; + } + + index++; + } + }; + + function rangeHasNoErrors(r: TextRange): boolean { + return false; + } + } + + /** + * Start of the original range might fall inside the comment - scanner will not yield appropriate results + * This function will look for token that is located before the start of target range + * and return its end as start position for the scanner. + */ + function getScanStartPosition(enclosingNode: Node, originalRange: TextRange, sourceFile: SourceFile): number { + var start = enclosingNode.getStart(sourceFile); + if (start === originalRange.pos && enclosingNode.end === originalRange.end) { + return start; + } + + var precedingToken = findPrecedingToken(enclosingNode.pos, sourceFile); + // no preceding token found - start from the beginning of enclosing node + return precedingToken ? precedingToken.end : enclosingNode.pos; + } + + function formatSpan(originalRange: TextRange, + sourceFile: SourceFile, + options: FormatCodeOptions, + rulesProvider: RulesProvider, + requestKind: FormattingRequestKind): TextChange[] { + + var rangeContainsError = prepareRangeContainsErrorFunction(sourceFile.syntacticErrors, originalRange); + + // formatting context is used by rules provider + var formattingContext = new FormattingContext(sourceFile, requestKind); + + // find the smallest node that fully wraps the range and compute the initial indentation for the node + var enclosingNode = findEnclosingNode(originalRange, sourceFile); + + var formattingScanner = getFormattingScanner(sourceFile, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end); + + var initialIndentation = SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, options); + + var previousRangeHasError: boolean; + var previousRange: TextRangeWithKind; + var previousParent: Node; + var previousRangeStartLine: number; + + var edits: TextChange[] = []; + + formattingScanner.advance(); + + if (formattingScanner.isOnToken()) { + var startLine = sourceFile.getLineAndCharacterFromPosition(enclosingNode.getStart(sourceFile)).line; + var delta = SmartIndenter.shouldIndentChildNode(enclosingNode.kind, SyntaxKind.Unknown) ? options.IndentSize : 0; + processNode(enclosingNode, enclosingNode, startLine, initialIndentation, delta); + } + + formattingScanner.close(); + + return edits; + + // local functions + + /** 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; + } + } + else { + var startLine = sourceFile.getLineAndCharacterFromPosition(startPos).line; + var startLinePosition = getStartLinePositionForPosition(startPos, sourceFile); + var column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); + if (startLine !== parentStartLine || startPos === column) { + return column + } + } + + return Constants.Unknown; + } + + function computeIndentation( + node: TextRangeWithKind, + startLine: number, + inheritedIndentation: number, + parent: Node, + parentDynamicIndentation: DynamicIndentation, + effectiveParentStartLine: number): Indentation { + + var indentation = inheritedIndentation; + if (indentation === Constants.Unknown) { + if (isSomeBlock(node.kind)) { + // blocks should be indented in + // - other blocks + // - source file + // - switch\default clauses + if (isSomeBlock(parent.kind) || + parent.kind === SyntaxKind.SourceFile || + parent.kind === SyntaxKind.CaseClause || + parent.kind === SyntaxKind.DefaultClause) { + + indentation = parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(); + } + else { + indentation = parentDynamicIndentation.getIndentation(); + } + } + else { + if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { + indentation = parentDynamicIndentation.getIndentation(); + } + else { + indentation = parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(); + } + } + } + + var delta = SmartIndenter.shouldIndentChildNode(node.kind, SyntaxKind.Unknown) ? options.IndentSize : 0; + + 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 = parentDynamicIndentation.getIndentation(); + delta = Math.min(options.IndentSize, parentDynamicIndentation.getDelta() + delta); + } + return { + indentation: indentation, + delta: delta + } + } + + function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { + return { + getIndentationForComment: kind => { + switch (kind) { + // preceding comment to the token that closes the indentation scope inherits the indentation from the scope + // .. { + // // comment + // } + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CloseBracketToken: + return indentation + delta; + } + return indentation; + }, + getIndentationForToken: (line, kind) => { + switch (kind) { + // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent + case SyntaxKind.OpenBraceToken: + case SyntaxKind.CloseBraceToken: + case SyntaxKind.OpenBracketToken: + case SyntaxKind.CloseBracketToken: + case SyntaxKind.ElseKeyword: + case SyntaxKind.WhileKeyword: + return indentation; + default: + // if token line equals to the line of containing node (this is a first token in the node) - use node indentation + return nodeStartLine !== line ? indentation + delta : indentation; + } + }, + getIndentation: () => indentation, + getDelta: () => delta, + recomputeIndentation: lineAdded => { + if (node.parent && SmartIndenter.shouldIndentChildNode(node.parent.kind, node.kind)) { + if (lineAdded) { + indentation += options.IndentSize; + } + else { + indentation -= options.IndentSize; + } + + if (SmartIndenter.shouldIndentChildNode(node.kind, SyntaxKind.Unknown)) { + delta = options.IndentSize; + } + else { + delta = 0; + } + } + }, + } + } + + function processNode(node: Node, contextNode: Node, nodeStartLine: number, indentation: number, delta: number) { + if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { + return; + } + + var nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta); + + // a useful observations when tracking context node + // / + // [a] + // / | \ + // [b] [c] [d] + // node 'a' is a context node for nodes 'b', 'c', 'd' + // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a' + // this rule can be applied recursively to child nodes of 'a'. + // + // context node is set to parent node value after processing every child node + // context node is set to parent of the token after processing every token + + var childContextNode = contextNode; + + // if there are any tokens that logically belong to node and interleave child nodes + // such tokens will be consumed in processChildNode for for the child that follows them + forEachChild( + node, + child => { + processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, /*isListElement*/ false) + }, + (nodes: NodeArray) => { + processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation); + }); + + // 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) { + break; + } + consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation); + } + + function processChildNode( + child: Node, + inheritedIndentation: number, + parent: Node, + parentDynamicIndentation: DynamicIndentation, + parentStartLine: number, + isListItem: boolean): number { + + var childStartPos = child.getStart(sourceFile); + + var childStart = sourceFile.getLineAndCharacterFromPosition(childStartPos); + + // if child is a list item - try to get its indentation + var childIndentationAmount = Constants.Unknown; + if (isListItem) { + childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); + if (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; + } + + if (child.kind === SyntaxKind.Missing) { + return inheritedIndentation; + } + + while (formattingScanner.isOnToken()) { + // proceed any parent tokens that are located prior to child.getStart() + var tokenInfo = formattingScanner.readTokenInfo(node); + if (tokenInfo.token.end > childStartPos) { + // stop when formatting scanner advances past the beginning of the child + break; + } + + consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation); + } + + if (!formattingScanner.isOnToken()) { + return inheritedIndentation; + } + + 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, parentDynamicIndentation); + return inheritedIndentation; + } + + var childIndentation = computeIndentation(child, childStart.line, childIndentationAmount, node, parentDynamicIndentation, parentStartLine); + + processNode(child, childContextNode, childStart.line, childIndentation.indentation, childIndentation.delta); + + childContextNode = node; + + return inheritedIndentation; + } + + function processChildNodes(nodes: NodeArray, + parent: Node, + parentStartLine: number, + parentDynamicIndentation: DynamicIndentation): void { + + var listStartToken = getOpenTokenForList(parent, nodes); + var listEndToken = getCloseTokenForOpenToken(listStartToken); + + var listDynamicIndentation = parentDynamicIndentation; + var startLine = parentStartLine; + + if (listStartToken !== SyntaxKind.Unknown) { + // introduce a new indentation scope for lists (including list start and end tokens) + while (formattingScanner.isOnToken()) { + var tokenInfo = formattingScanner.readTokenInfo(parent); + if (tokenInfo.token.end > nodes.pos) { + // stop when formatting scanner moves past the beginning of node list + break; + } + else if (tokenInfo.token.kind === listStartToken) { + // consume list start token + startLine = sourceFile.getLineAndCharacterFromPosition(tokenInfo.token.pos).line; + var indentation = + computeIndentation(tokenInfo.token, startLine, Constants.Unknown, parent, parentDynamicIndentation, startLine); + + listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentation.indentation, indentation.delta); + consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation); + } + else { + // consume any tokens that precede the list as child elements of 'node' using its indentation scope + consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation); + } + } + } + + var inheritedIndentation = Constants.Unknown; + for (var i = 0, len = nodes.length; i < len; ++i) { + inheritedIndentation = processChildNode(nodes[i], inheritedIndentation, node, listDynamicIndentation, startLine, /*isListElement*/ true) + } + + if (listEndToken !== SyntaxKind.Unknown) { + if (formattingScanner.isOnToken()) { + var tokenInfo = formattingScanner.readTokenInfo(parent); + if (tokenInfo.token.kind === listEndToken) { + // consume list end token + consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation); + } + } + } + } + + function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation): void { + Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); + + var lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); + var indentToken = false; + + if (currentTokenInfo.leadingTrivia) { + processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); + } + + var lineAdded: boolean; + var isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); + + 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, dynamicIndentation); + if (lineAdded !== undefined) { + indentToken = lineAdded; + } + else { + indentToken = lastTriviaWasNewLine && tokenStart.line !== prevStartLine; + } + } + + if (currentTokenInfo.trailingTrivia) { + processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); + } + + if (indentToken) { + var indentNextTokenOrTrivia = true; + if (currentTokenInfo.leadingTrivia) { + for (var i = 0, len = currentTokenInfo.leadingTrivia.length; i < len; ++i) { + var triviaItem = currentTokenInfo.leadingTrivia[i]; + if (!rangeContainsRange(originalRange, triviaItem)) { + continue; + } + + var triviaStartLine = sourceFile.getLineAndCharacterFromPosition(triviaItem.pos).line; + switch (triviaItem.kind) { + case SyntaxKind.MultiLineCommentTrivia: + var commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind); + indentMultilineComment(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); + indentNextTokenOrTrivia = false; + break; + case SyntaxKind.SingleLineCommentTrivia: + if (indentNextTokenOrTrivia) { + var commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind); + insertIndentation(triviaItem.pos, commentIndentation, /*lineAdded*/ false); + indentNextTokenOrTrivia = false; + } + break; + case SyntaxKind.NewLineTrivia: + indentNextTokenOrTrivia = true; + break; + } + } + } + + // indent token only if is it is in target range and does not overlap with any error ranges + if (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) { + var tokenIndentation = dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind); + insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAdded); + } + } + + formattingScanner.advance(); + + childContextNode = parent; + } + } + + function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void { + for (var i = 0, len = trivia.length; i < len; ++i) { + var triviaItem = trivia[i]; + if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { + var triviaItemStart = sourceFile.getLineAndCharacterFromPosition(triviaItem.pos); + processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); + } + } + } + + function processRange(range: TextRangeWithKind, + rangeStart: LineAndCharacter, + parent: Node, + contextNode: Node, + dynamicIndentation: DynamicIndentation): boolean { + + var rangeHasError = rangeContainsError(range); + var lineAdded: boolean; + if (!rangeHasError && !previousRangeHasError) { + if (!previousRange) { + // trim whitespaces starting from the beginning of the span up to the current line + var originalStart = sourceFile.getLineAndCharacterFromPosition(originalRange.pos); + trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); + } + else { + lineAdded = + processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation) + } + } + + previousRange = range; + previousParent = parent; + previousRangeStartLine = rangeStart.line; + previousRangeHasError = rangeHasError; + + return lineAdded; + } + + function processPair(currentItem: TextRangeWithKind, + currentStartLine: number, + currentParent: Node, + previousItem: TextRangeWithKind, + previousStartLine: number, + previousParent: Node, + contextNode: Node, + dynamicIndentation: DynamicIndentation): boolean { + + formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); + + var rule = rulesProvider.getRulesMap().GetRule(formattingContext); + + var trimTrailingWhitespaces: boolean; + var lineAdded: boolean; + if (rule) { + applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); + + 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. + if (currentParent.getStart(sourceFile) === currentItem.pos) { + lineAdded = false; + } + } + else if (rule.Operation.Action & RuleAction.NewLine && currentStartLine === previousStartLine) { + // 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. + if (currentParent.getStart(sourceFile) === currentItem.pos) { + lineAdded = true; + } + } + + if (lineAdded !== undefined) { + dynamicIndentation.recomputeIndentation(lineAdded); + } + + // 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 + trimTrailingWhitespaces = + (rule.Operation.Action & (RuleAction.NewLine | RuleAction.Space)) && + rule.Flag !== RuleFlags.CanDeleteNewLines; + } + else { + trimTrailingWhitespaces = true; + } + + if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { + // 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 + trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); + } + + return lineAdded; + } + + 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 tokenStart = sourceFile.getLineAndCharacterFromPosition(pos); + if (indentation !== tokenStart.character - 1) { + var startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); + recordReplace(startLinePosition, tokenStart.character - 1, indentationString); + } + } + } + + function indentMultilineComment(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean) { + // split comment in lines + var startLine = sourceFile.getLineAndCharacterFromPosition(commentRange.pos).line; + var endLine = sourceFile.getLineAndCharacterFromPosition(commentRange.end).line; + + if (startLine === endLine) { + if (!firstLineIsIndented) { + // treat as single line comment + insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false); + } + return; + } + else { + var parts: TextRange[] = []; + var startPos = commentRange.pos; + for (var line = startLine; line < endLine; ++line) { + var endOfLine = getEndLinePosition(line, sourceFile); + parts.push({ pos: startPos, end: endOfLine }); + startPos = getStartPositionOfLine(line + 1, sourceFile); + } + + parts.push({ pos: startPos, end: commentRange.end }); + } + + var startLinePos = getStartPositionOfLine(startLine, sourceFile); + + var nonWhitespaceColumnInFirstPart = + SmartIndenter.findFirstNonWhitespaceColumn(startLinePos, parts[0].pos, sourceFile, options); + + if (indentation === nonWhitespaceColumnInFirstPart) { + return; + } + + var startIndex = 0; + if (firstLineIsIndented) { + startIndex = 1; + startLine++; + } + + // shift all parts on the delta size + var delta = indentation - nonWhitespaceColumnInFirstPart; + for (var i = startIndex, len = parts.length; i < len; ++i, ++startLine) { + var startLinePos = getStartPositionOfLine(startLine, sourceFile); + var nonWhitespaceColumn = + i === 0 + ? nonWhitespaceColumnInFirstPart + : SmartIndenter.findFirstNonWhitespaceColumn(parts[i].pos, parts[i].end, sourceFile, options); + + var newIndentation = nonWhitespaceColumn + delta; + if (newIndentation > 0) { + var indentationString = getIndentationString(newIndentation, options); + recordReplace(startLinePos, nonWhitespaceColumn, indentationString); + } + else { + recordDelete(startLinePos, nonWhitespaceColumn); + } + } + } + + function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { + for (var line = line1; line < line2; ++line) { + var lineStartPosition = getStartPositionOfLine(line, sourceFile); + var lineEndPosition = getEndLinePosition(line, sourceFile); + + // do not trim whitespaces in comments + if (range && isComment(range.kind) && range.pos <= lineEndPosition && range.end > lineEndPosition) { + continue; + } + + var pos = lineEndPosition; + while (pos >= lineStartPosition && isWhiteSpace(sourceFile.text.charCodeAt(pos))) { + pos--; + } + if (pos !== lineEndPosition) { + Debug.assert(pos === lineStartPosition || !isWhiteSpace(sourceFile.text.charCodeAt(pos))); + recordDelete(pos + 1, lineEndPosition - pos); + } + } + } + + function newTextChange(start: number, len: number, newText: string): TextChange { + return { span: new TypeScript.TextSpan(start, len), newText: newText } + } + + function recordDelete(start: number, len: number) { + if (len) { + edits.push(newTextChange(start, len, "")); + } + } + + function recordReplace(start: number, len: number, newText: string) { + if (len || newText) { + edits.push(newTextChange(start, len, newText)); + } + } + + function applyRuleEdits(rule: Rule, + previousRange: TextRangeWithKind, + previousStartLine: number, + currentRange: TextRangeWithKind, + currentStartLine: number): void { + + var between: TextRange; + switch (rule.Operation.Action) { + case RuleAction.Ignore: + // no action required + return; + case RuleAction.Delete: + if (previousRange.end !== currentRange.pos) { + // delete characters starting from t1.end up to t2.pos exclusive + recordDelete(previousRange.end, currentRange.pos - previousRange.end); + } + break; + case RuleAction.NewLine: + // exit early if we on different lines and rule cannot change number of newlines + // if line1 and line2 are on subsequent lines then no edits are required - ok to exit + // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines + if (rule.Flag !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return; + } + + // edit should not be applied only if we have one line feed between elements + var lineDelta = currentStartLine - previousStartLine; + if (lineDelta !== 1) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.NewLineCharacter); + } + break; + case RuleAction.Space: + // exit early if we on different lines and rule cannot change number of newlines + if (rule.Flag !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return; + } + + var posDelta = currentRange.pos - previousRange.end; + if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); + } + break; + } + } + } + + function isSomeBlock(kind: SyntaxKind): boolean { + switch (kind) { + case SyntaxKind.Block: + case SyntaxKind.FunctionBlock: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.ModuleBlock: + return true; + } + return false; + } + + function getOpenTokenForList(node: Node, list: Node[]) { + switch (node.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Method: + case SyntaxKind.ArrowFunction: + if ((node).typeParameters === list) { + return SyntaxKind.LessThanToken; + } + else if ((node).parameters === list) { + return SyntaxKind.OpenParenToken; + } + break; + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + if ((node).typeArguments === list) { + return SyntaxKind.LessThanToken; + } + else if ((node).arguments === list) { + return SyntaxKind.OpenParenToken; + } + break; + case SyntaxKind.TypeReference: + if ((node).typeArguments === list) { + return SyntaxKind.LessThanToken; + } + } + + return SyntaxKind.Unknown; + } + + function getCloseTokenForOpenToken(kind: SyntaxKind) { + switch (kind) { + case SyntaxKind.OpenParenToken: + return SyntaxKind.CloseParenToken; + case SyntaxKind.LessThanToken: + return SyntaxKind.GreaterThanToken; + } + + return SyntaxKind.Unknown; + } +} \ No newline at end of file diff --git a/src/services/formatting/formatter.ts b/src/services/formatting/formatter.ts deleted file mode 100644 index 35e93f84cb8..00000000000 --- a/src/services/formatting/formatter.ts +++ /dev/null @@ -1,315 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class Formatter extends MultipleTokenIndenter { - private previousTokenSpan: TokenSpan = null; - private previousTokenParent: IndentationNodeContext = null; - - // TODO: implement it with skipped tokens in Fidelity - private scriptHasErrors: boolean = false; - - private rulesProvider: RulesProvider; - private formattingRequestKind: FormattingRequestKind; - private formattingContext: FormattingContext; - - constructor(textSpan: TextSpan, - sourceUnit: SourceUnitSyntax, - indentFirstToken: boolean, - options: FormattingOptions, - snapshot: ITextSnapshot, - rulesProvider: RulesProvider, - formattingRequestKind: FormattingRequestKind) { - - super(textSpan, sourceUnit, snapshot, indentFirstToken, options); - - this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); - - this.rulesProvider = rulesProvider; - this.formattingRequestKind = formattingRequestKind; - this.formattingContext = new FormattingContext(this.snapshot(), this.formattingRequestKind); - } - - public static getEdits(textSpan: TextSpan, - sourceUnit: SourceUnitSyntax, - options: FormattingOptions, - indentFirstToken: boolean, - snapshot: ITextSnapshot, - rulesProvider: RulesProvider, - formattingRequestKind: FormattingRequestKind): TextEditInfo[] { - var walker = new Formatter(textSpan, sourceUnit, indentFirstToken, options, snapshot, rulesProvider, formattingRequestKind); - walker.walk(sourceUnit); - return walker.edits(); - } - - public visitTokenInSpan(token: ISyntaxToken): void { - if (token.fullWidth() !== 0) { - var tokenSpan = new TextSpan(this.position() + token.leadingTriviaWidth(), width(token)); - if (this.textSpan().containsTextSpan(tokenSpan)) { - this.processToken(token); - } - } - - // Call the base class to process the token and indent it if needed - super.visitTokenInSpan(token); - } - - private processToken(token: ISyntaxToken): void { - var position = this.position(); - - // Extract any leading comments - if (token.leadingTriviaWidth() !== 0) { - this.processTrivia(token.leadingTrivia(), position); - position += token.leadingTriviaWidth(); - } - - // Push the token - var currentTokenSpan = new TokenSpan(token.kind, position, width(token)); - if (!this.parent().hasSkippedOrMissingTokenChild()) { - if (this.previousTokenSpan) { - // Note that formatPair calls TrimWhitespaceInLineRange in between the 2 tokens - this.formatPair(this.previousTokenSpan, this.previousTokenParent, currentTokenSpan, this.parent()); - } - else { - // We still want to trim whitespace even if it is the first trivia of the first token. Trim from the beginning of the span to the trivia - this.trimWhitespaceInLineRange(this.getLineNumber(this.textSpan()), this.getLineNumber(currentTokenSpan)); - } - } - this.previousTokenSpan = currentTokenSpan; - if (this.previousTokenParent) { - // Make sure to clear the previous parent before assigning a new value to it - this.indentationNodeContextPool().releaseNode(this.previousTokenParent, /* recursive */true); - } - this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); - position += width(token); - } - - private processTrivia(triviaList: ISyntaxTriviaList, fullStart: number) { - var position = fullStart; - - for (var i = 0, n = triviaList.count(); i < n ; i++) { - var trivia = triviaList.syntaxTriviaAt(i); - // For a comment, format it like it is a token. For skipped text, eat it up as a token, but skip the formatting - if (trivia.isComment() || trivia.isSkippedToken()) { - var currentTokenSpan = new TokenSpan(trivia.kind, position, trivia.fullWidth()); - if (this.textSpan().containsTextSpan(currentTokenSpan)) { - if (trivia.isComment() && this.previousTokenSpan) { - // Note that formatPair calls TrimWhitespaceInLineRange in between the 2 tokens - this.formatPair(this.previousTokenSpan, this.previousTokenParent, currentTokenSpan, this.parent()); - } - else { - // We still want to trim whitespace even if it is the first trivia of the first token. Trim from the beginning of the span to the trivia - var startLine = this.getLineNumber(this.previousTokenSpan || this.textSpan()); - this.trimWhitespaceInLineRange(startLine, this.getLineNumber(currentTokenSpan)); - } - this.previousTokenSpan = currentTokenSpan; - if (this.previousTokenParent) { - // Make sure to clear the previous parent before assigning a new value to it - this.indentationNodeContextPool().releaseNode(this.previousTokenParent, /* recursive */true); - } - this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); - } - } - - position += trivia.fullWidth(); - } - } - - private findCommonParents(parent1: IndentationNodeContext, parent2: IndentationNodeContext): IndentationNodeContext { - // TODO: disable debug assert message - - var shallowParent: IndentationNodeContext; - var shallowParentDepth: number; - var deepParent: IndentationNodeContext; - var deepParentDepth: number; - - if (parent1.depth() < parent2.depth()) { - shallowParent = parent1; - shallowParentDepth = parent1.depth(); - deepParent = parent2; - deepParentDepth = parent2.depth(); - } - else { - shallowParent = parent2; - shallowParentDepth = parent2.depth(); - deepParent = parent1; - deepParentDepth = parent1.depth(); - } - - Debug.assert(shallowParentDepth >= 0, "Expected shallowParentDepth >= 0"); - Debug.assert(deepParentDepth >= 0, "Expected deepParentDepth >= 0"); - Debug.assert(deepParentDepth >= shallowParentDepth, "Expected deepParentDepth >= shallowParentDepth"); - - while (deepParentDepth > shallowParentDepth) { - deepParent = deepParent.parent(); - deepParentDepth--; - } - - Debug.assert(deepParentDepth === shallowParentDepth, "Expected deepParentDepth === shallowParentDepth"); - - while (deepParent.node() && shallowParent.node()) { - if (deepParent.node() === shallowParent.node()) { - return deepParent; - } - deepParent = deepParent.parent(); - shallowParent = shallowParent.parent(); - } - - // The root should be the first element in the parent chain, we can not be here unless something wrong - // happened along the way - throw Errors.invalidOperation(); - } - - private formatPair(t1: TokenSpan, t1Parent: IndentationNodeContext, t2: TokenSpan, t2Parent: IndentationNodeContext): void { - var token1Line = this.getLineNumber(t1); - var token2Line = this.getLineNumber(t2); - - // Find common parent - var commonParent= this.findCommonParents(t1Parent, t2Parent); - - // Update the context - this.formattingContext.updateContext(t1, t1Parent, t2, t2Parent, commonParent); - - // Find rules matching the current context - var rule = this.rulesProvider.getRulesMap().GetRule(this.formattingContext); - - if (rule != null) { - // Record edits from the rule - this.RecordRuleEdits(rule, t1, t2); - - // 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. - if ((rule.Operation.Action == RuleAction.Space || rule.Operation.Action == RuleAction.Delete) && - token1Line != token2Line) { - this.forceSkipIndentingNextToken(t2.start()); - } - - // 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. - if (rule.Operation.Action == RuleAction.NewLine && token1Line == token2Line) { - this.forceIndentNextToken(t2.start()); - } - } - - // 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 - if (token1Line != token2Line && (!rule || (rule.Operation.Action != RuleAction.Delete && rule.Flag != RuleFlags.CanDeleteNewLines))) { - this.trimWhitespaceInLineRange(token1Line, token2Line, t1); - } - } - - private getLineNumber(span: TextSpan): number { - return this.snapshot().getLineNumberFromPosition(span.start()); - } - - private trimWhitespaceInLineRange(startLine: number, endLine: number, token?: TokenSpan): void { - for (var lineNumber = startLine; lineNumber < endLine; ++lineNumber) { - var line = this.snapshot().getLineFromLineNumber(lineNumber); - - this.trimWhitespace(line, token); - } - } - - private trimWhitespace(line: ITextSnapshotLine, token?: TokenSpan): void { - // Don't remove the trailing spaces inside comments (this includes line comments and block comments) - if (token && (token.kind == SyntaxKind.MultiLineCommentTrivia || token.kind == SyntaxKind.SingleLineCommentTrivia) && token.start() <= line.endPosition() && token.end() >= line.endPosition()) - return; - - var text = line.getText(); - var index = 0; - - for (index = text.length - 1; index >= 0; --index) { - if (!CharacterInfo.isWhitespace(text.charCodeAt(index))) { - break; - } - } - - ++index; - - if (index < text.length) { - this.recordEdit(line.startPosition() + index, line.length() - index, ""); - } - } - - private RecordRuleEdits(rule: Rule, t1: TokenSpan, t2: TokenSpan): void { - if (rule.Operation.Action == RuleAction.Ignore) { - return; - } - - var betweenSpan: TextSpan; - - switch (rule.Operation.Action) { - case RuleAction.Delete: - { - betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); - - if (betweenSpan.length() > 0) { - this.recordEdit(betweenSpan.start(), betweenSpan.length(), ""); - return; - } - } - break; - - case RuleAction.NewLine: - { - if (!(rule.Flag == RuleFlags.CanDeleteNewLines || this.getLineNumber(t1) == this.getLineNumber(t2))) { - return; - } - - betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); - - var doEdit = false; - var betweenText = this.snapshot().getText(betweenSpan); - - var lineFeedLoc = betweenText.indexOf(this.options.newLineCharacter); - if (lineFeedLoc < 0) { - // no linefeeds, do the edit - doEdit = true; - } - else { - // We only require one line feed. If there is another one, do the edit - lineFeedLoc = betweenText.indexOf(this.options.newLineCharacter, lineFeedLoc + 1); - if (lineFeedLoc >= 0) { - doEdit = true; - } - } - - if (doEdit) { - this.recordEdit(betweenSpan.start(), betweenSpan.length(), this.options.newLineCharacter); - return; - } - } - break; - - case RuleAction.Space: - { - if (!(rule.Flag == RuleFlags.CanDeleteNewLines || this.getLineNumber(t1) == this.getLineNumber(t2))) { - return; - } - - betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); - - if (betweenSpan.length() > 1 || this.snapshot().getText(betweenSpan) != " ") { - this.recordEdit(betweenSpan.start(), betweenSpan.length(), " "); - return; - } - } - break; - } - } - } -} \ No newline at end of file diff --git a/src/services/formatting/formattingContext.ts b/src/services/formatting/formattingContext.ts index 4f46341ef05..2bfb155921e 100644 --- a/src/services/formatting/formattingContext.ts +++ b/src/services/formatting/formattingContext.ts @@ -13,48 +13,48 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class FormattingContext { - public currentTokenSpan: TokenSpan = null; - public nextTokenSpan: TokenSpan = null; - public contextNode: IndentationNodeContext = null; - public currentTokenParent: IndentationNodeContext = null; - public nextTokenParent: IndentationNodeContext = null; + public currentTokenSpan: TextRangeWithKind; + public nextTokenSpan: TextRangeWithKind; + public contextNode: Node; + public currentTokenParent: Node; + public nextTokenParent: Node; - private contextNodeAllOnSameLine: boolean = null; - private nextNodeAllOnSameLine: boolean = null; - private tokensAreOnSameLine: boolean = null; - private contextNodeBlockIsOnOneLine: boolean = null; - private nextNodeBlockIsOnOneLine: boolean = null; + private contextNodeAllOnSameLine: boolean; + private nextNodeAllOnSameLine: boolean; + private tokensAreOnSameLine: boolean; + private contextNodeBlockIsOnOneLine: boolean; + private nextNodeBlockIsOnOneLine: boolean; - constructor(private snapshot: ITextSnapshot, public formattingRequestKind: FormattingRequestKind) { - Debug.assert(this.snapshot != null, "snapshot is null"); + constructor(private sourceFile: SourceFile, public formattingRequestKind: FormattingRequestKind) { } - public updateContext(currentTokenSpan: TokenSpan, currentTokenParent: IndentationNodeContext, nextTokenSpan: TokenSpan, nextTokenParent: IndentationNodeContext, commonParent: IndentationNodeContext) { - Debug.assert(currentTokenSpan != null, "currentTokenSpan is null"); - Debug.assert(currentTokenParent != null, "currentTokenParent is null"); - Debug.assert(nextTokenSpan != null, "nextTokenSpan is null"); - Debug.assert(nextTokenParent != null, "nextTokenParent is null"); - Debug.assert(commonParent != null, "commonParent is null"); + public updateContext(currentRange: TextRangeWithKind, currentTokenParent: Node, nextRange: TextRangeWithKind, nextTokenParent: Node, commonParent: Node) { + Debug.assert(currentRange !== undefined, "currentTokenSpan is null"); + Debug.assert(currentTokenParent !== undefined, "currentTokenParent is null"); + Debug.assert(nextRange !== undefined, "nextTokenSpan is null"); + Debug.assert(nextTokenParent !== undefined, "nextTokenParent is null"); + Debug.assert(commonParent !== undefined, "commonParent is null"); - this.currentTokenSpan = currentTokenSpan; + this.currentTokenSpan = currentRange; this.currentTokenParent = currentTokenParent; - this.nextTokenSpan = nextTokenSpan; + this.nextTokenSpan = nextRange; this.nextTokenParent = nextTokenParent; this.contextNode = commonParent; - this.contextNodeAllOnSameLine = null; - this.nextNodeAllOnSameLine = null; - this.tokensAreOnSameLine = null; - this.contextNodeBlockIsOnOneLine = null; - this.nextNodeBlockIsOnOneLine = null; + // drop cached results + this.contextNodeAllOnSameLine = undefined; + this.nextNodeAllOnSameLine = undefined; + this.tokensAreOnSameLine = undefined; + this.contextNodeBlockIsOnOneLine = undefined; + this.nextNodeBlockIsOnOneLine = undefined; } public ContextNodeAllOnSameLine(): boolean { - if (this.contextNodeAllOnSameLine === null) { + if (this.contextNodeAllOnSameLine === undefined) { this.contextNodeAllOnSameLine = this.NodeIsOnOneLine(this.contextNode); } @@ -62,7 +62,7 @@ module TypeScript.Services.Formatting { } public NextNodeAllOnSameLine(): boolean { - if (this.nextNodeAllOnSameLine === null) { + if (this.nextNodeAllOnSameLine === undefined) { this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); } @@ -70,10 +70,9 @@ module TypeScript.Services.Formatting { } public TokensAreOnSameLine(): boolean { - if (this.tokensAreOnSameLine === null) { - var startLine = this.snapshot.getLineNumberFromPosition(this.currentTokenSpan.start()); - var endLine = this.snapshot.getLineNumberFromPosition(this.nextTokenSpan.start()); - + if (this.tokensAreOnSameLine === undefined) { + var startLine = this.sourceFile.getLineAndCharacterFromPosition(this.currentTokenSpan.pos).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(this.nextTokenSpan.pos).line; this.tokensAreOnSameLine = (startLine == endLine); } @@ -81,7 +80,7 @@ module TypeScript.Services.Formatting { } public ContextNodeBlockIsOnOneLine() { - if (this.contextNodeBlockIsOnOneLine === null) { + if (this.contextNodeBlockIsOnOneLine === undefined) { this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); } @@ -89,28 +88,28 @@ module TypeScript.Services.Formatting { } public NextNodeBlockIsOnOneLine() { - if (this.nextNodeBlockIsOnOneLine === null) { + if (this.nextNodeBlockIsOnOneLine === undefined) { this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); } return this.nextNodeBlockIsOnOneLine; } - public NodeIsOnOneLine(node: IndentationNodeContext): boolean { - var startLine = this.snapshot.getLineNumberFromPosition(node.start()); - var endLine = this.snapshot.getLineNumberFromPosition(node.end()); - + private NodeIsOnOneLine(node: Node): boolean { + var startLine = this.sourceFile.getLineAndCharacterFromPosition(node.getStart(this.sourceFile)).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(node.getEnd()).line; return startLine == endLine; } - // Now we know we have a block (or a fake block represented by some other kind of node with an open and close brace as children). - // IMPORTANT!!! This relies on the invariant that IsBlockContext must return true ONLY for nodes with open and close braces as immediate children - public BlockIsOnOneLine(node: IndentationNodeContext): boolean { - var block = node.node(); - - // Now check if they are on the same line - return this.snapshot.getLineNumberFromPosition(fullEnd(block.openBraceToken)) === - this.snapshot.getLineNumberFromPosition(start(block.closeBraceToken)); + private BlockIsOnOneLine(node: Node): boolean { + var openBrace = findChildOfKind(node, SyntaxKind.OpenBraceToken, this.sourceFile); + var closeBrace = findChildOfKind(node, SyntaxKind.CloseBraceToken, this.sourceFile); + if (openBrace && closeBrace) { + var startLine = this.sourceFile.getLineAndCharacterFromPosition(openBrace.getEnd()).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(closeBrace.getStart(this.sourceFile)).line; + return startLine === endLine; + } + return false; } } } \ No newline at end of file diff --git a/src/services/formatting/formattingManager.ts b/src/services/formatting/formattingManager.ts deleted file mode 100644 index 40bc3fdeb6d..00000000000 --- a/src/services/formatting/formattingManager.ts +++ /dev/null @@ -1,125 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class FormattingManager { - private options: FormattingOptions; - - constructor(private syntaxTree: SyntaxTree, - private snapshot: ITextSnapshot, - private rulesProvider: RulesProvider, - editorOptions: ts.EditorOptions) { - // - // TODO: convert to use FormattingOptions instead of EditorOptions - this.options = new FormattingOptions(!editorOptions.ConvertTabsToSpaces, editorOptions.TabSize, editorOptions.IndentSize, editorOptions.NewLineCharacter) - } - - public formatSelection(minChar: number, limChar: number): ts.TextChange[] { - var span = TextSpan.fromBounds(minChar, limChar); - return this.formatSpan(span, FormattingRequestKind.FormatSelection); - } - - public formatDocument(): ts.TextChange[] { - var span = TextSpan.fromBounds(0, this.snapshot.getLength()); - return this.formatSpan(span, FormattingRequestKind.FormatDocument); - } - - public formatOnSemicolon(caretPosition: number): ts.TextChange[] { - var sourceUnit = this.syntaxTree.sourceUnit(); - var semicolonPositionedToken = findToken(sourceUnit, caretPosition - 1); - - if (semicolonPositionedToken.kind === SyntaxKind.SemicolonToken) { - // Find the outer most parent that this semicolon terminates - var current: ISyntaxElement = semicolonPositionedToken; - while (current.parent !== null && - fullEnd(current.parent) === fullEnd(semicolonPositionedToken) && - current.parent.kind !== SyntaxKind.List) { - current = current.parent; - } - - // Compute the span - var span = new TextSpan(fullStart(current), fullWidth(current)); - - // Format the span - return this.formatSpan(span, FormattingRequestKind.FormatOnSemicolon); - } - - return []; - } - - public formatOnClosingCurlyBrace(caretPosition: number): ts.TextChange[] { - var sourceUnit = this.syntaxTree.sourceUnit(); - var closeBracePositionedToken = findToken(sourceUnit, caretPosition - 1); - - if (closeBracePositionedToken.kind === SyntaxKind.CloseBraceToken) { - // Find the outer most parent that this closing brace terminates - var current: ISyntaxElement = closeBracePositionedToken; - while (current.parent !== null && - fullEnd(current.parent) === fullEnd(closeBracePositionedToken) && - current.parent.kind !== SyntaxKind.List) { - current = current.parent; - } - - // Compute the span - var span = new TextSpan(fullStart(current), fullWidth(current)); - - // Format the span - return this.formatSpan(span, FormattingRequestKind.FormatOnClosingCurlyBrace); - } - - return []; - } - - public formatOnEnter(caretPosition: number): ts.TextChange[] { - var lineNumber = this.snapshot.getLineNumberFromPosition(caretPosition); - - if (lineNumber > 0) { - // Format both lines - var prevLine = this.snapshot.getLineFromLineNumber(lineNumber - 1); - var currentLine = this.snapshot.getLineFromLineNumber(lineNumber); - var span = TextSpan.fromBounds(prevLine.startPosition(), currentLine.endPosition()); - - // Format the span - return this.formatSpan(span, FormattingRequestKind.FormatOnEnter); - - } - - return []; - } - - private formatSpan(span: TextSpan, formattingRequestKind: FormattingRequestKind): ts.TextChange[] { - // Always format from the beginning of the line - var startLine = this.snapshot.getLineFromPosition(span.start()); - span = TextSpan.fromBounds(startLine.startPosition(), span.end()); - - var result: ts.TextChange[] = []; - - var formattingEdits = Formatter.getEdits(span, this.syntaxTree.sourceUnit(), this.options, true, this.snapshot, this.rulesProvider, formattingRequestKind); - - // - // TODO: Change the ILanguageService interface to return TextEditInfo (with start, and length) instead of TextEdit (with minChar and limChar) - formattingEdits.forEach(item => { - result.push({ - span: new TextSpan(item.position, item.length), - newText: item.replaceWith - }); - }); - - return result; - } - } -} \ No newline at end of file diff --git a/src/services/formatting/formattingRequestKind.ts b/src/services/formatting/formattingRequestKind.ts index b766058e1ca..a66169c1e7c 100644 --- a/src/services/formatting/formattingRequestKind.ts +++ b/src/services/formatting/formattingRequestKind.ts @@ -13,15 +13,14 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { - export enum FormattingRequestKind { +module ts.formatting { + export const enum FormattingRequestKind { FormatDocument, FormatSelection, FormatOnEnter, FormatOnSemicolon, - FormatOnClosingCurlyBrace, - FormatOnPaste + FormatOnClosingCurlyBrace } } \ No newline at end of file diff --git a/src/services/formatting/formattingScanner.ts b/src/services/formatting/formattingScanner.ts new file mode 100644 index 00000000000..d774e34da41 --- /dev/null +++ b/src/services/formatting/formattingScanner.ts @@ -0,0 +1,210 @@ +/// + +module ts.formatting { + var scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false); + + export interface FormattingScanner { + advance(): void; + isOnToken(): boolean; + readTokenInfo(n: Node): TokenInfo; + lastTrailingTriviaWasNewLine(): boolean; + close(): void; + } + + const enum ScanAction{ + Scan, + RescanGreaterThanToken, + RescanSlashToken + } + + export function getFormattingScanner(sourceFile: SourceFile, startPos: number, endPos: number): FormattingScanner { + + scanner.setText(sourceFile.text); + scanner.setTextPos(startPos); + + var wasNewLine: boolean = true; + var leadingTrivia: TextRangeWithKind[]; + var trailingTrivia: TextRangeWithKind[]; + + var savedPos: number; + var lastScanAction: ScanAction; + var lastTokenInfo: TokenInfo; + + return { + advance: advance, + readTokenInfo: readTokenInfo, + isOnToken: isOnToken, + lastTrailingTriviaWasNewLine: () => wasNewLine, + close: () => { + lastTokenInfo = undefined; + scanner.setText(undefined); + } + } + + function advance(): void { + lastTokenInfo = undefined; + var isStarted = scanner.getStartPos() !== startPos; + + if (isStarted) { + if (trailingTrivia) { + Debug.assert(trailingTrivia.length !== 0); + wasNewLine = trailingTrivia[trailingTrivia.length - 1].kind === SyntaxKind.NewLineTrivia; + } + else { + wasNewLine = false; + } + } + + leadingTrivia = undefined; + trailingTrivia = undefined; + + if (!isStarted) { + scanner.scan(); + } + + var t: SyntaxKind; + var pos = scanner.getStartPos(); + + // Read leading trivia and token + while (pos < endPos) { + var t = scanner.getToken(); + if (!isTrivia(t)) { + break; + } + + // consume leading trivia + scanner.scan(); + var item = { + pos: pos, + end: scanner.getStartPos(), + kind: t + } + + pos = scanner.getStartPos(); + + if (!leadingTrivia) { + leadingTrivia = []; + } + leadingTrivia.push(item); + } + + savedPos = scanner.getStartPos(); + } + + function shouldRescanGreaterThanToken(container: Node): boolean { + if (container.kind !== SyntaxKind.BinaryExpression) { + return false; + } + switch ((container).operator) { + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + return true; + } + + return false; + } + + function shouldRescanSlashToken(container: Node): boolean { + return container.kind === SyntaxKind.RegularExpressionLiteral; + } + + function startsWithSlashToken(t: SyntaxKind): boolean { + return t === SyntaxKind.SlashToken || t === SyntaxKind.SlashEqualsToken; + } + + 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 + : shouldRescanSlashToken(n) + ? ScanAction.RescanSlashToken + : 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(); + } + + var currentToken = scanner.getToken(); + + if (expectedScanAction === ScanAction.RescanGreaterThanToken && currentToken === SyntaxKind.GreaterThanToken) { + currentToken = scanner.reScanGreaterToken(); + Debug.assert((n).operator === currentToken); + lastScanAction = ScanAction.RescanGreaterThanToken; + } + else if (expectedScanAction === ScanAction.RescanSlashToken && startsWithSlashToken(currentToken)) { + currentToken = scanner.reScanSlashToken(); + Debug.assert(n.kind === currentToken); + lastScanAction = ScanAction.RescanSlashToken; + } + else { + lastScanAction = ScanAction.Scan; + } + + var token: TextRangeWithKind = { + pos: scanner.getStartPos(), + end: scanner.getTextPos(), + kind: currentToken + } + + // consume trailing trivia + while(scanner.getStartPos() < endPos) { + currentToken = scanner.scan(); + if (!isTrivia(currentToken)) { + break; + } + var trivia = { + pos: scanner.getStartPos(), + end: scanner.getTextPos(), + kind: currentToken + }; + + if (!trailingTrivia) { + trailingTrivia = []; + } + + trailingTrivia.push(trivia); + + if (currentToken === SyntaxKind.NewLineTrivia) { + // move past new line + scanner.scan(); + break; + } + } + + return lastTokenInfo = { + leadingTrivia: leadingTrivia, + trailingTrivia: trailingTrivia, + token: token + } + } + + function isOnToken(): boolean { + var current = (lastTokenInfo && lastTokenInfo.token.kind) || scanner.getToken(); + var startPos = (lastTokenInfo && lastTokenInfo.token.pos) || scanner.getStartPos(); + return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/indentation.ts b/src/services/formatting/indentation.ts new file mode 100644 index 00000000000..f6f8fc0319f --- /dev/null +++ b/src/services/formatting/indentation.ts @@ -0,0 +1,54 @@ +module ts.formatting { + + var internedTabsIndentation: string[]; + var internedSpacesIndentation: string[]; + + export function getIndentationString(indentation: number, options: FormatCodeOptions): string { + if (!options.ConvertTabsToSpaces) { + var tabs = Math.floor(indentation / options.TabSize); + var spaces = indentation - tabs * options.TabSize; + + var tabString: string; + if (!internedTabsIndentation) { + internedTabsIndentation = []; + } + + if (internedTabsIndentation[tabs] === undefined) { + internedTabsIndentation[tabs] = tabString = repeat('\t', tabs); + } + else { + tabString = internedTabsIndentation[tabs]; + } + + return spaces ? tabString + repeat(" ", spaces) : tabString; + } + else { + var spacesString: string; + var quotient = Math.floor(indentation / options.IndentSize); + var remainder = indentation % options.IndentSize; + if (!internedSpacesIndentation) { + internedSpacesIndentation = []; + } + + if (internedSpacesIndentation[quotient] === undefined) { + spacesString = repeat(" ", options.IndentSize * quotient); + internedSpacesIndentation[quotient] = spacesString; + } + else { + spacesString = internedSpacesIndentation[quotient]; + } + + + return remainder ? spacesString + repeat(" ", remainder) : spacesString; + } + + function repeat(value: string, count: number): string { + var s = ""; + for (var i = 0; i < count; ++i) { + s += value; + } + + return s; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/indentationNodeContext.ts b/src/services/formatting/indentationNodeContext.ts deleted file mode 100644 index 7aa10d2f9b9..00000000000 --- a/src/services/formatting/indentationNodeContext.ts +++ /dev/null @@ -1,103 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class IndentationNodeContext { - private _node: ISyntaxNode; - private _parent: IndentationNodeContext; - private _fullStart: number; - private _indentationAmount: number; - private _childIndentationAmountDelta: number; - private _depth: number; - private _hasSkippedOrMissingTokenChild: boolean; - - constructor(parent: IndentationNodeContext, node: ISyntaxNode, fullStart: number, indentationAmount: number, childIndentationAmountDelta: number) { - this.update(parent, node, fullStart, indentationAmount, childIndentationAmountDelta); - } - - public parent(): IndentationNodeContext { - return this._parent; - } - - public node(): ISyntaxNode { - return this._node; - } - - public fullStart(): number { - return this._fullStart; - } - - public fullWidth(): number { - return fullWidth(this._node); - } - - public start(): number { - return this._fullStart + leadingTriviaWidth(this._node); - } - - public end(): number { - return this._fullStart + leadingTriviaWidth(this._node) + width(this._node); - } - - public indentationAmount(): number { - return this._indentationAmount; - } - - public childIndentationAmountDelta(): number { - return this._childIndentationAmountDelta; - } - - public depth(): number { - return this._depth; - } - - public kind(): SyntaxKind { - return this._node.kind; - } - - public hasSkippedOrMissingTokenChild(): boolean { - if (this._hasSkippedOrMissingTokenChild === null) { - this._hasSkippedOrMissingTokenChild = Syntax.nodeHasSkippedOrMissingTokens(this._node); - } - return this._hasSkippedOrMissingTokenChild; - } - - public clone(pool: IndentationNodeContextPool): IndentationNodeContext { - var parent: IndentationNodeContext = null; - if (this._parent) { - parent = this._parent.clone(pool); - } - return pool.getNode(parent, this._node, this._fullStart, this._indentationAmount, this._childIndentationAmountDelta); - } - - public update(parent: IndentationNodeContext, node: ISyntaxNode, fullStart: number, indentationAmount: number, childIndentationAmountDelta: number) { - this._parent = parent; - this._node = node; - this._fullStart = fullStart; - this._indentationAmount = indentationAmount; - this._childIndentationAmountDelta = childIndentationAmountDelta; - this._hasSkippedOrMissingTokenChild = null; - - if (parent) { - this._depth = parent.depth() + 1; - } - else { - this._depth = 0; - } - } - } -} \ No newline at end of file diff --git a/src/services/formatting/indentationNodeContextPool.ts b/src/services/formatting/indentationNodeContextPool.ts deleted file mode 100644 index ea5b26277b5..00000000000 --- a/src/services/formatting/indentationNodeContextPool.ts +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class IndentationNodeContextPool { - private nodes: IndentationNodeContext[] = []; - - public getNode(parent: IndentationNodeContext, node: ISyntaxNode, fullStart: number, indentationLevel: number, childIndentationLevelDelta: number): IndentationNodeContext { - if (this.nodes.length > 0) { - var cachedNode = this.nodes.pop(); - cachedNode.update(parent, node, fullStart, indentationLevel, childIndentationLevelDelta); - return cachedNode; - } - - return new IndentationNodeContext(parent, node, fullStart, indentationLevel, childIndentationLevelDelta); - } - - public releaseNode(node: IndentationNodeContext, recursive: boolean = false): void { - this.nodes.push(node); - - if (recursive) { - var parent = node.parent(); - if (parent) { - this.releaseNode(parent, recursive); - } - } - } - } -} \ No newline at end of file diff --git a/src/services/formatting/indentationTrackingWalker.ts b/src/services/formatting/indentationTrackingWalker.ts deleted file mode 100644 index 8e86c67f40f..00000000000 --- a/src/services/formatting/indentationTrackingWalker.ts +++ /dev/null @@ -1,371 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class IndentationTrackingWalker { - private _position: number = 0; - private _parent: IndentationNodeContext = null; - private _textSpan: TextSpan; - private _snapshot: ITextSnapshot; - private _lastTriviaWasNewLine: boolean; - private _indentationNodeContextPool: IndentationNodeContextPool; - private _text: ISimpleText; - - constructor(textSpan: TextSpan, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, indentFirstToken: boolean, public options: FormattingOptions) { - // Create a pool object to manage context nodes while walking the tree - this._indentationNodeContextPool = new IndentationNodeContextPool(); - - this._textSpan = textSpan; - this._text = sourceUnit.syntaxTree.text; - this._snapshot = snapshot; - this._parent = this._indentationNodeContextPool.getNode(null, sourceUnit, 0, 0, 0); - - // Is the first token in the span at the start of a new line. - this._lastTriviaWasNewLine = indentFirstToken; - } - - public position(): number { - return this._position; - } - - public parent(): IndentationNodeContext { - return this._parent; - } - - public textSpan(): TextSpan { - return this._textSpan; - } - - public snapshot(): ITextSnapshot { - return this._snapshot; - } - - public indentationNodeContextPool(): IndentationNodeContextPool { - return this._indentationNodeContextPool; - } - - public forceIndentNextToken(tokenStart: number): void { - this._lastTriviaWasNewLine = true; - this.forceRecomputeIndentationOfParent(tokenStart, true); - } - - public forceSkipIndentingNextToken(tokenStart: number): void { - this._lastTriviaWasNewLine = false; - this.forceRecomputeIndentationOfParent(tokenStart, false); - } - - public indentToken(token: ISyntaxToken, indentationAmount: number, commentIndentationAmount: number): void { - throw Errors.abstract(); - } - - public visitTokenInSpan(token: ISyntaxToken): void { - if (this._lastTriviaWasNewLine) { - // Compute the indentation level at the current token - var indentationAmount = this.getTokenIndentationAmount(token); - var commentIndentationAmount = this.getCommentIndentationAmount(token); - - // Process the token - this.indentToken(token, indentationAmount, commentIndentationAmount); - } - } - - public visitToken(token: ISyntaxToken): void { - var tokenSpan = new TextSpan(this._position, token.fullWidth()); - - if (tokenSpan.intersectsWithTextSpan(this._textSpan)) { - this.visitTokenInSpan(token); - - // Only track new lines on tokens within the range. Make sure to check that the last trivia is a newline, and not just one of the trivia - var _nextToken = nextToken(token); - if (_nextToken && _nextToken.hasLeadingTrivia()) { - var trivia = _nextToken.leadingTrivia(); - this._lastTriviaWasNewLine = trivia.hasNewLine(); - } - else { - this._lastTriviaWasNewLine = false; - } - } - - // Update the position - this._position += token.fullWidth(); - } - - public walk(element: ISyntaxElement) { - if (element) { - if (isToken(element)) { - this.visitToken(element); - } - else if (element.kind === SyntaxKind.List) { - for (var i = 0, n = childCount(element); i < n; i++) { - this.walk(childAt(element, i)); - } - } - else { - this.visitNode(element); - } - } - } - - private visitNode(node: ISyntaxNode): void { - var nodeSpan = new TextSpan(this._position, fullWidth(node)); - - if (nodeSpan.intersectsWithTextSpan(this._textSpan)) { - // Update indentation level - var indentation = this.getNodeIndentation(node); - - // Update the parent - var currentParent = this._parent; - this._parent = this._indentationNodeContextPool.getNode(currentParent, node, this._position, indentation.indentationAmount, indentation.indentationAmountDelta); - - // Visit node - for (var i = 0, n = childCount(node); i < n; i++) { - this.walk(childAt(node, i)); - } - - // Reset state - this._indentationNodeContextPool.releaseNode(this._parent); - this._parent = currentParent; - } - else { - // We're skipping the node, so update our position accordingly. - this._position += fullWidth(node); - } - } - - private getTokenIndentationAmount(token: ISyntaxToken): number { - // If this is the first token of a node, it should follow the node indentation and not the child indentation; - // (e.g.class in a class declaration or module in module declariotion). - // Open and close braces should follow the indentation of thier parent as well(e.g. - // class { - // } - // Also in a do-while statement, the while should be indented like the parent. - if (firstToken(this._parent.node()) === token || - token.kind === SyntaxKind.OpenBraceToken || token.kind === SyntaxKind.CloseBraceToken || - token.kind === SyntaxKind.OpenBracketToken || token.kind === SyntaxKind.CloseBracketToken || - (token.kind === SyntaxKind.WhileKeyword && this._parent.node().kind == SyntaxKind.DoStatement)) { - return this._parent.indentationAmount(); - } - - return (this._parent.indentationAmount() + this._parent.childIndentationAmountDelta()); - } - - private getCommentIndentationAmount(token: ISyntaxToken): number { - // If this is token terminating an indentation scope, leading comments should be indented to follow the children - // indentation level and not the node - - if (token.kind === SyntaxKind.CloseBraceToken || token.kind === SyntaxKind.CloseBracketToken) { - return (this._parent.indentationAmount() + this._parent.childIndentationAmountDelta()); - } - return this._parent.indentationAmount(); - } - - private getNodeIndentation(node: ISyntaxNode, newLineInsertedByFormatting?: boolean): { indentationAmount: number; indentationAmountDelta: number; } { - var parent = this._parent; - - // We need to get the parent's indentation, which could be one of 2 things. If first token of the parent is in the span, use the parent's computed indentation. - // If the parent was outside the span, use the actual indentation of the parent. - var parentIndentationAmount: number; - if (this._textSpan.containsPosition(parent.start())) { - parentIndentationAmount = parent.indentationAmount(); - } - else { - if (parent.kind() === SyntaxKind.Block && !this.shouldIndentBlockInParent(this._parent.parent())) { - // Blocks preserve the indentation of their containing node (unless they're a - // standalone block in a list). i.e. if you have: - // - // function foo( - // a: number) { - // - // Then we expect the indentation of the block to be tied to the function, not to - // the line that the block is defined on. If we were to do the latter, then the - // indentation would be here: - // - // function foo( - // a: number) { - // | - // - // Instead of: - // - // function foo( - // a: number) { - // | - parent = this._parent.parent(); - } - - var line = this._snapshot.getLineFromPosition(parent.start()).getText(); - var firstNonWhiteSpacePosition = Indentation.firstNonWhitespacePosition(line); - parentIndentationAmount = Indentation.columnForPositionInString(line, firstNonWhiteSpacePosition, this.options); - } - var parentIndentationAmountDelta = parent.childIndentationAmountDelta(); - - // The indentation level of the node - var indentationAmount: number; - - // The delta it adds to its children. - var indentationAmountDelta: number; - var parentNode = parent.node(); - - switch (node.kind) { - default: - // General case - // This node should follow the child indentation set by its parent - // This node does not introduce any new indentation scope, indent any decendants of this node (tokens or child nodes) - // using the same indentation level - indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); - indentationAmountDelta = 0; - break; - - // Statements introducing {} - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ObjectType: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.SwitchStatement: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MemberFunctionDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.IndexMemberDeclaration: - case SyntaxKind.CatchClause: - // Statements introducing [] - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.ArrayType: - case SyntaxKind.ElementAccessExpression: - case SyntaxKind.IndexSignature: - // Other statements - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.CaseSwitchClause: - case SyntaxKind.DefaultSwitchClause: - case SyntaxKind.ReturnStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.SimpleArrowFunctionExpression: - case SyntaxKind.ParenthesizedArrowFunctionExpression: - case SyntaxKind.VariableDeclaration: - case SyntaxKind.ExportAssignment: - - // Expressions which have argument lists or parameter lists - case SyntaxKind.InvocationExpression: - case SyntaxKind.ObjectCreationExpression: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - - // These nodes should follow the child indentation set by its parent; - // they introduce a new indenation scope; children should be indented at one level deeper - indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); - indentationAmountDelta = this.options.indentSpaces; - break; - - case SyntaxKind.IfStatement: - if (parent.kind() === SyntaxKind.ElseClause && - !SyntaxUtilities.isLastTokenOnLine((parentNode).elseKeyword, this._text)) { - // This is an else if statement with the if on the same line as the else, do not indent the if statmement. - // Note: Children indentation has already been set by the parent if statement, so no need to increment - indentationAmount = parentIndentationAmount; - } - else { - // Otherwise introduce a new indenation scope; children should be indented at one level deeper - indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); - } - indentationAmountDelta = this.options.indentSpaces; - break; - - case SyntaxKind.ElseClause: - // Else should always follow its parent if statement indentation. - // Note: Children indentation has already been set by the parent if statement, so no need to increment - indentationAmount = parentIndentationAmount; - indentationAmountDelta = this.options.indentSpaces; - break; - - - case SyntaxKind.Block: - // Check if the block is a member in a list of statements (if the parent is a source unit, module, or block, or switch clause) - if (this.shouldIndentBlockInParent(parent)) { - indentationAmount = parentIndentationAmount + parentIndentationAmountDelta; - } - else { - indentationAmount = parentIndentationAmount; - } - - indentationAmountDelta = this.options.indentSpaces; - break; - } - - // If the parent happens to start on the same line as this node, then override the current node indenation with that - // of the parent. This avoid having to add an extra level of indentation for the children. e.g.: - // return { - // a:1 - // }; - // instead of: - // return { - // a:1 - // }; - // We also need to pass the delta (if it is nonzero) to the children, so that subsequent lines get indented. Essentially, if any node starting on the given line - // has a nonzero delta , the resulting delta should be inherited from this node. This is to indent cases like the following: - // return a - // || b; - // Lastly, it is possible the node indentation needs to be recomputed because the formatter inserted a newline before its first token. - // If this is the case, we know the node no longer starts on the same line as its parent (or at least we shouldn't treat it as such). - if (parentNode) { - if (!newLineInsertedByFormatting /*This could be false or undefined here*/) { - var parentStartLine = this._snapshot.getLineNumberFromPosition(parent.start()); - var currentNodeStartLine = this._snapshot.getLineNumberFromPosition(this._position + leadingTriviaWidth(node)); - if (parentStartLine === currentNodeStartLine || newLineInsertedByFormatting === false /*meaning a new line was removed and we are force recomputing*/) { - indentationAmount = parentIndentationAmount; - indentationAmountDelta = Math.min(this.options.indentSpaces, parentIndentationAmountDelta + indentationAmountDelta); - } - } - } - - return { - indentationAmount: indentationAmount, - indentationAmountDelta: indentationAmountDelta - }; - } - - private shouldIndentBlockInParent(parent: IndentationNodeContext): boolean { - switch (parent.kind()) { - case SyntaxKind.SourceUnit: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.Block: - case SyntaxKind.CaseSwitchClause: - case SyntaxKind.DefaultSwitchClause: - return true; - - default: - return false; - } - } - - private forceRecomputeIndentationOfParent(tokenStart: number, newLineAdded: boolean /*as opposed to removed*/): void { - var parent = this._parent; - if (start(parent.node()) === tokenStart) { - // Temporarily pop the parent before recomputing - this._parent = parent.parent(); - var indentation = this.getNodeIndentation(parent.node(), /* newLineInsertedByFormatting */ newLineAdded); - parent.update(parent.parent(), parent.node(), parent.fullStart(), indentation.indentationAmount, indentation.indentationAmountDelta); - this._parent = parent; - } - } - } -} \ No newline at end of file diff --git a/src/services/formatting/multipleTokenIndenter.ts b/src/services/formatting/multipleTokenIndenter.ts deleted file mode 100644 index ad74952ad3d..00000000000 --- a/src/services/formatting/multipleTokenIndenter.ts +++ /dev/null @@ -1,221 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class MultipleTokenIndenter extends IndentationTrackingWalker { - private _edits: TextEditInfo[] = []; - - constructor(textSpan: TextSpan, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, indentFirstToken: boolean, options: FormattingOptions) { - super(textSpan, sourceUnit, snapshot, indentFirstToken, options); - } - - public indentToken(token: ISyntaxToken, indentationAmount: number, commentIndentationAmount: number): void { - // Ignore generated tokens - if (token.fullWidth() === 0) { - return; - } - - // If we have any skipped tokens as children, do not process this node for indentation or formatting - if (this.parent().hasSkippedOrMissingTokenChild()) { - return; - } - - // Be strict, and only consider nodes that fall inside the span. This avoids indenting a multiline string - // on enter at the end of, as the whole token was not included in the span - var tokenSpan = new TextSpan(this.position() + token.leadingTriviaWidth(), width(token)); - if (!this.textSpan().containsTextSpan(tokenSpan)) { - return; - } - - // Compute an indentation string for this token - var indentationString = Indentation.indentationString(indentationAmount, this.options); - - var commentIndentationString = Indentation.indentationString(commentIndentationAmount, this.options); - - // Record any needed indentation edits - this.recordIndentationEditsForToken(token, indentationString, commentIndentationString); - } - - public edits(): TextEditInfo[]{ - return this._edits; - } - - public recordEdit(position: number, length: number, replaceWith: string): void { - this._edits.push(new TextEditInfo(position, length, replaceWith)); - } - - private recordIndentationEditsForToken(token: ISyntaxToken, indentationString: string, commentIndentationString: string) { - var position = this.position(); - var indentNextTokenOrTrivia = true; - var leadingWhiteSpace = ""; // We need to track the whitespace before a multiline comment - - // Process any leading trivia if any - var triviaList = token.leadingTrivia(); - if (triviaList) { - var seenNewLine = position === 0; - - for (var i = 0, length = triviaList.count(); i < length; i++, position += trivia.fullWidth()) { - var trivia = triviaList.syntaxTriviaAt(i); - - // Skip all trivia up to the first newline we see. We consider this trivia to - // 'belong' to the previous token. - if (!seenNewLine) { - if (trivia.kind !== SyntaxKind.NewLineTrivia) { - continue; - } - else { - seenNewLine = true; - continue; - } - } - - // Skip this trivia if it is not in the span - if (!this.textSpan().containsTextSpan(new TextSpan(position, trivia.fullWidth()))) { - continue; - } - - switch (trivia.kind) { - case SyntaxKind.MultiLineCommentTrivia: - // We will only indent the first line of the multiline comment if we were planning to indent the next trivia. However, - // subsequent lines will always be indented - this.recordIndentationEditsForMultiLineComment(trivia, position, commentIndentationString, leadingWhiteSpace, !indentNextTokenOrTrivia /* already indented first line */); - indentNextTokenOrTrivia = false; - leadingWhiteSpace = ""; - break; - - case SyntaxKind.SingleLineCommentTrivia: - case SyntaxKind.SkippedTokenTrivia: - if (indentNextTokenOrTrivia) { - this.recordIndentationEditsForSingleLineOrSkippedText(trivia, position, commentIndentationString); - indentNextTokenOrTrivia = false; - } - break; - - case SyntaxKind.WhitespaceTrivia: - // If the next trivia is a comment, use the comment indentation level instead of the regular indentation level - // If the next trivia is a newline, this whole line is just whitespace, so don't do anything (trimming will take care of it) - var nextTrivia = length > i + 1 && triviaList.syntaxTriviaAt(i + 1); - var whiteSpaceIndentationString = nextTrivia && nextTrivia.isComment() ? commentIndentationString : indentationString; - if (indentNextTokenOrTrivia) { - if (!(nextTrivia && nextTrivia.isNewLine())) { - this.recordIndentationEditsForWhitespace(trivia, position, whiteSpaceIndentationString); - } - indentNextTokenOrTrivia = false; - } - leadingWhiteSpace += trivia.fullText(); - break; - - case SyntaxKind.NewLineTrivia: - // We hit a newline processing the trivia. We need to add the indentation to the - // next line as well. Note: don't bother indenting the newline itself. This will - // just insert ugly whitespace that most users probably will not want. - indentNextTokenOrTrivia = true; - leadingWhiteSpace = ""; - break; - - default: - throw Errors.invalidOperation(); - } - } - - } - - if (token.kind !== SyntaxKind.EndOfFileToken && indentNextTokenOrTrivia) { - // If the last trivia item was a new line, or no trivia items were encounterd record the - // indentation edit at the token position - if (indentationString.length > 0) { - this.recordEdit(position, 0, indentationString); - } - } - } - - private recordIndentationEditsForSingleLineOrSkippedText(trivia: ISyntaxTrivia, fullStart: number, indentationString: string): void { - // Record the edit - if (indentationString.length > 0) { - this.recordEdit(fullStart, 0, indentationString); - } - } - - private recordIndentationEditsForWhitespace(trivia: ISyntaxTrivia, fullStart: number, indentationString: string): void { - var text = trivia.fullText(); - - // Check if the current indentation matches the desired indentation or not - if (indentationString === text) { - return; - } - - // Record the edit - this.recordEdit(fullStart, text.length, indentationString); - } - - private recordIndentationEditsForMultiLineComment(trivia: ISyntaxTrivia, fullStart: number, indentationString: string, leadingWhiteSpace: string, firstLineAlreadyIndented: boolean): void { - // If the multiline comment spans multiple lines, we need to add the right indent amount to - // each successive line segment as well. - var position = fullStart; - var segments = Syntax.splitMultiLineCommentTriviaIntoMultipleLines(trivia); - - if (segments.length <= 1) { - if (!firstLineAlreadyIndented) { - // Process the one-line multiline comment just like a single line comment - this.recordIndentationEditsForSingleLineOrSkippedText(trivia, fullStart, indentationString); - } - return; - } - - // Find number of columns in first segment - var whiteSpaceColumnsInFirstSegment = Indentation.columnForPositionInString(leadingWhiteSpace, leadingWhiteSpace.length, this.options); - - var indentationColumns = Indentation.columnForPositionInString(indentationString, indentationString.length, this.options); - var startIndex = 0; - if (firstLineAlreadyIndented) { - startIndex = 1; - position += segments[0].length; - } - for (var i = startIndex; i < segments.length; i++) { - var segment = segments[i]; - this.recordIndentationEditsForSegment(segment, position, indentationColumns, whiteSpaceColumnsInFirstSegment); - position += segment.length; - } - } - - private recordIndentationEditsForSegment(segment: string, fullStart: number, indentationColumns: number, whiteSpaceColumnsInFirstSegment: number): void { - // Indent subsequent lines using a column delta of the actual indentation relative to the first line - var firstNonWhitespacePosition = Indentation.firstNonWhitespacePosition(segment); - var leadingWhiteSpaceColumns = Indentation.columnForPositionInString(segment, firstNonWhitespacePosition, this.options); - var deltaFromFirstSegment = leadingWhiteSpaceColumns - whiteSpaceColumnsInFirstSegment; - var finalColumns = indentationColumns + deltaFromFirstSegment; - if (finalColumns < 0) { - finalColumns = 0; - } - var indentationString = Indentation.indentationString(finalColumns, this.options); - - if (firstNonWhitespacePosition < segment.length && - CharacterInfo.isLineTerminator(segment.charCodeAt(firstNonWhitespacePosition))) { - // If this segment was just a newline, then don't bother indenting it. That will just - // leave the user with an ugly indent in their output that they probably do not want. - return; - } - - if (indentationString === segment.substring(0, firstNonWhitespacePosition)) { - return; - } - - // Record the edit - this.recordEdit(fullStart, firstNonWhitespacePosition, indentationString); - } - } -} \ No newline at end of file diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/references.ts similarity index 65% rename from src/services/formatting/formatting.ts rename to src/services/formatting/references.ts index 7570ef936c7..3d19b33d821 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/references.ts @@ -14,11 +14,7 @@ // /// -/// -/// -/// /// -/// /// /// /// @@ -28,12 +24,5 @@ /// /// /// -/// -/// /// -/// -/// -/// -/// -/// -/// \ No newline at end of file +/// \ No newline at end of file diff --git a/src/services/formatting/rule.ts b/src/services/formatting/rule.ts index 273f0590ce7..356720d2330 100644 --- a/src/services/formatting/rule.ts +++ b/src/services/formatting/rule.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class Rule { constructor( public Descriptor: RuleDescriptor, diff --git a/src/services/formatting/ruleAction.ts b/src/services/formatting/ruleAction.ts index 32c67c950ca..d2890d8e080 100644 --- a/src/services/formatting/ruleAction.ts +++ b/src/services/formatting/ruleAction.ts @@ -13,13 +13,13 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { - export enum RuleAction { - Ignore, - Space, - NewLine, - Delete +module ts.formatting { + export const enum RuleAction { + Ignore = 0x00000001, + Space = 0x00000002, + NewLine = 0x00000004, + Delete = 0x00000008 } } \ No newline at end of file diff --git a/src/services/formatting/ruleDescriptor.ts b/src/services/formatting/ruleDescriptor.ts index 1e7d822d57b..e5b7d6f3186 100644 --- a/src/services/formatting/ruleDescriptor.ts +++ b/src/services/formatting/ruleDescriptor.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class RuleDescriptor { constructor(public LeftTokenRange: Shared.TokenRange, public RightTokenRange: Shared.TokenRange) { } @@ -34,7 +34,6 @@ module TypeScript.Services.Formatting { } static create3(left: SyntaxKind, right: Shared.TokenRange): RuleDescriptor - //: this(TokenRange.FromToken(left), right) { return RuleDescriptor.create4(Shared.TokenRange.FromToken(left), right); } diff --git a/src/services/formatting/ruleFlag.ts b/src/services/formatting/ruleFlag.ts index d815537dfd9..aaf70639e01 100644 --- a/src/services/formatting/ruleFlag.ts +++ b/src/services/formatting/ruleFlag.ts @@ -13,10 +13,10 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { - export enum RuleFlags { +module ts.formatting { + export const enum RuleFlags { None, CanDeleteNewLines } diff --git a/src/services/formatting/ruleOperation.ts b/src/services/formatting/ruleOperation.ts index 1f74aea0784..c73e3b6bcf7 100644 --- a/src/services/formatting/ruleOperation.ts +++ b/src/services/formatting/ruleOperation.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class RuleOperation { public Context: RuleOperationContext; public Action: RuleAction; diff --git a/src/services/formatting/ruleOperationContext.ts b/src/services/formatting/ruleOperationContext.ts index a1e349210e4..d037f8e70d7 100644 --- a/src/services/formatting/ruleOperationContext.ts +++ b/src/services/formatting/ruleOperationContext.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class RuleOperationContext { private customContextChecks: { (context: FormattingContext): boolean; }[]; diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 2bb8be11e7a..4b42591936e 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class Rules { public getRuleName(rule: Rule) { var o: ts.Map = this; @@ -241,7 +241,7 @@ module TypeScript.Services.Formatting { this.SpaceBeforeOpenBraceInFunction = new Rule(RuleDescriptor.create2(this.FunctionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines); // Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc) - this.TypeScriptOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.MultiLineCommentTrivia]); + this.TypeScriptOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia]); this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsTypeScriptDeclWithBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines); // Place a space before open brace in a control flow construct @@ -299,7 +299,7 @@ module TypeScript.Services.Formatting { // get x() {} // set x(val) {} - this.SpaceAfterGetSetInMember = new Rule(RuleDescriptor.create2(Shared.TokenRange.FromTokens([SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]), SyntaxKind.IdentifierName), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Space)); + this.SpaceAfterGetSetInMember = new Rule(RuleDescriptor.create2(Shared.TokenRange.FromTokens([SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]), SyntaxKind.Identifier), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Space)); // Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options. this.SpaceBeforeBinaryKeywordOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.BinaryKeywordOperators), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); @@ -324,7 +324,7 @@ module TypeScript.Services.Formatting { this.SpaceAfterArrow = new Rule(RuleDescriptor.create3(SyntaxKind.EqualsGreaterThanToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); // Optional parameters and var args - this.NoSpaceAfterEllipsis = new Rule(RuleDescriptor.create1(SyntaxKind.DotDotDotToken, SyntaxKind.IdentifierName), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterEllipsis = new Rule(RuleDescriptor.create1(SyntaxKind.DotDotDotToken, SyntaxKind.Identifier), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); this.NoSpaceAfterOptionalParameters = new Rule(RuleDescriptor.create3(SyntaxKind.QuestionToken, Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.CommaToken])), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Delete)); // generics @@ -437,7 +437,7 @@ module TypeScript.Services.Formatting { /// static IsForContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.ForStatement; + return context.contextNode.kind === SyntaxKind.ForStatement; } static IsNotForContext(context: FormattingContext): boolean { @@ -446,8 +446,7 @@ module TypeScript.Services.Formatting { static IsBinaryOpContext(context: FormattingContext): boolean { - switch (context.contextNode.kind()) { - // binary expressions + switch (context.contextNode.kind) { case SyntaxKind.BinaryExpression: case SyntaxKind.ConditionalExpression: return true; @@ -455,8 +454,11 @@ module TypeScript.Services.Formatting { // equal in import a = module('a'); case SyntaxKind.ImportDeclaration: // equal in var a = 0; - case SyntaxKind.VariableDeclarator: - case SyntaxKind.EqualsValueClause: + case SyntaxKind.VariableDeclaration: + // equal in p = 0; + case SyntaxKind.Parameter: + case SyntaxKind.EnumMember: + case SyntaxKind.Property: return context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; // "in" keyword in for (var x in []) { } case SyntaxKind.ForInStatement: @@ -512,16 +514,21 @@ module TypeScript.Services.Formatting { } // IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children - static NodeIsBlockContext(node: IndentationNodeContext): boolean { + static NodeIsBlockContext(node: Node): boolean { if (Rules.NodeIsTypeScriptDeclWithBlockContext(node)) { // This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc). return true; } - switch (node.kind()) { + switch (node.kind) { case SyntaxKind.Block: case SyntaxKind.SwitchStatement: - case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.FunctionBlock: + case SyntaxKind.ModuleBlock: return true; } @@ -529,17 +536,20 @@ module TypeScript.Services.Formatting { } static IsFunctionDeclContext(context: FormattingContext): boolean { - switch (context.contextNode.kind()) { + switch (context.contextNode.kind) { case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MemberFunctionDeclaration: + case SyntaxKind.Method: + //case SyntaxKind.MemberFunctionDeclaration: case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: - case SyntaxKind.MethodSignature: + ///case SyntaxKind.MethodSignature: case SyntaxKind.CallSignature: case SyntaxKind.FunctionExpression: - case SyntaxKind.ConstructorDeclaration: - case SyntaxKind.SimpleArrowFunctionExpression: - case SyntaxKind.ParenthesizedArrowFunctionExpression: + case SyntaxKind.Constructor: + case SyntaxKind.ArrowFunction: + //case SyntaxKind.ConstructorDeclaration: + //case SyntaxKind.SimpleArrowFunctionExpression: + //case SyntaxKind.ParenthesizedArrowFunctionExpression: case SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one return true; } @@ -551,11 +561,12 @@ module TypeScript.Services.Formatting { return Rules.NodeIsTypeScriptDeclWithBlockContext(context.contextNode); } - static NodeIsTypeScriptDeclWithBlockContext(node: IndentationNodeContext): boolean { - switch (node.kind()) { + static NodeIsTypeScriptDeclWithBlockContext(node: Node): boolean { + switch (node.kind) { case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: - case SyntaxKind.ObjectType: + case SyntaxKind.TypeLiteral: case SyntaxKind.ModuleDeclaration: return true; } @@ -564,11 +575,16 @@ module TypeScript.Services.Formatting { } static IsAfterCodeBlockContext(context: FormattingContext): boolean { - switch (context.currentTokenParent.kind()) { + switch (context.currentTokenParent.kind) { case SyntaxKind.ClassDeclaration: case SyntaxKind.ModuleDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.Block: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.FunctionBlock: + case SyntaxKind.ModuleBlock: case SyntaxKind.SwitchStatement: return true; } @@ -576,7 +592,7 @@ module TypeScript.Services.Formatting { } static IsControlDeclContext(context: FormattingContext): boolean { - switch (context.contextNode.kind()) { + switch (context.contextNode.kind) { case SyntaxKind.IfStatement: case SyntaxKind.SwitchStatement: case SyntaxKind.ForStatement: @@ -585,9 +601,10 @@ module TypeScript.Services.Formatting { case SyntaxKind.TryStatement: case SyntaxKind.DoStatement: case SyntaxKind.WithStatement: - case SyntaxKind.ElseClause: - case SyntaxKind.CatchClause: - case SyntaxKind.FinallyClause: + // TODO + // case SyntaxKind.ElseClause: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: return true; default: @@ -596,15 +613,15 @@ module TypeScript.Services.Formatting { } static IsObjectContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.ObjectLiteralExpression; + return context.contextNode.kind === SyntaxKind.ObjectLiteral; } static IsFunctionCallContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.InvocationExpression; + return context.contextNode.kind === SyntaxKind.CallExpression; } static IsNewContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.ObjectCreationExpression; + return context.contextNode.kind === SyntaxKind.NewExpression; } static IsFunctionCallOrNewContext(context: FormattingContext): boolean { @@ -620,25 +637,43 @@ module TypeScript.Services.Formatting { } static IsModuleDeclContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.ModuleDeclaration; + return context.contextNode.kind === SyntaxKind.ModuleDeclaration; } static IsObjectTypeContext(context: FormattingContext): boolean { - return context.contextNode.kind() === SyntaxKind.ObjectType && context.contextNode.parent().kind() !== SyntaxKind.InterfaceDeclaration; + return context.contextNode.kind === SyntaxKind.TypeLiteral;// && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; } - static IsTypeArgumentOrParameter(tokenKind: SyntaxKind, parentKind: SyntaxKind): boolean { - return ((tokenKind === SyntaxKind.LessThanToken || tokenKind === SyntaxKind.GreaterThanToken) && - (parentKind === SyntaxKind.TypeParameterList || parentKind === SyntaxKind.TypeArgumentList)); + static IsTypeArgumentOrParameter(token: TextRangeWithKind, parent: Node): boolean { + if (token.kind !== SyntaxKind.LessThanToken && token.kind !== SyntaxKind.GreaterThanToken) { + return false; + } + switch (parent.kind) { + case SyntaxKind.TypeReference: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.Method: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + return true; + default: + return false; + + } } static IsTypeArgumentOrParameterContext(context: FormattingContext): boolean { - return Rules.IsTypeArgumentOrParameter(context.currentTokenSpan.kind, context.currentTokenParent.kind()) || - Rules.IsTypeArgumentOrParameter(context.nextTokenSpan.kind, context.nextTokenParent.kind()); + return Rules.IsTypeArgumentOrParameter(context.currentTokenSpan, context.currentTokenParent) || + Rules.IsTypeArgumentOrParameter(context.nextTokenSpan, context.nextTokenParent); } static IsVoidOpContext(context: FormattingContext): boolean { - return context.currentTokenSpan.kind === SyntaxKind.VoidKeyword && context.currentTokenParent.kind() === SyntaxKind.VoidExpression; + return context.currentTokenSpan.kind === SyntaxKind.VoidKeyword && context.currentTokenParent.kind === SyntaxKind.PrefixOperator; } } } \ No newline at end of file diff --git a/src/services/formatting/rulesMap.ts b/src/services/formatting/rulesMap.ts index 0fbe81d16c9..d25320f16a8 100644 --- a/src/services/formatting/rulesMap.ts +++ b/src/services/formatting/rulesMap.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class RulesMap { public map: RulesBucket[]; public mapRowLength: number; diff --git a/src/services/formatting/rulesProvider.ts b/src/services/formatting/rulesProvider.ts index 90b11f01a9b..1469e86971b 100644 --- a/src/services/formatting/rulesProvider.ts +++ b/src/services/formatting/rulesProvider.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export class RulesProvider { private globalRules: Rules; private options: ts.FormatCodeOptions; diff --git a/src/services/formatting/snapshotPoint.ts b/src/services/formatting/snapshotPoint.ts deleted file mode 100644 index 762748dbc70..00000000000 --- a/src/services/formatting/snapshotPoint.ts +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - - export class SnapshotPoint { - constructor(public snapshot: ITextSnapshot, public position: number) { - } - public getContainingLine(): ITextSnapshotLine { - return this.snapshot.getLineFromPosition(this.position); - } - public add(offset: number): SnapshotPoint { - return new SnapshotPoint(this.snapshot, this.position + offset); - } - } -} \ No newline at end of file diff --git a/src/services/formatting/textEditInfo.ts b/src/services/formatting/textEditInfo.ts deleted file mode 100644 index bdcbdd0ddc8..00000000000 --- a/src/services/formatting/textEditInfo.ts +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export class TextEditInfo { - - constructor(public position: number, public length: number, public replaceWith: string) { - } - - public toString() { - return "[ position: " + this.position + ", length: " + this.length + ", replaceWith: '" + this.replaceWith + "' ]"; - } - } -} \ No newline at end of file diff --git a/src/services/formatting/textSnapshot.ts b/src/services/formatting/textSnapshot.ts deleted file mode 100644 index 4f2904dd76f..00000000000 --- a/src/services/formatting/textSnapshot.ts +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export interface ITextSnapshot { - getLength(): number; - getText(span: TextSpan): string; - getLineNumberFromPosition(position: number): number; - getLineFromPosition(position: number): ITextSnapshotLine; - getLineFromLineNumber(lineNumber: number): ITextSnapshotLine; - } - - export class TextSnapshot implements ITextSnapshot { - private lines: TextSnapshotLine[]; - - constructor(private snapshot: ISimpleText) { - this.lines = []; - } - - public getLength(): number { - return this.snapshot.length(); - } - - public getText(span: TextSpan): string { - return this.snapshot.substr(span.start(), span.length()); - } - - public getLineNumberFromPosition(position: number): number { - return this.snapshot.lineMap().getLineNumberFromPosition(position); - } - - public getLineFromPosition(position: number): ITextSnapshotLine { - var lineNumber = this.getLineNumberFromPosition(position); - return this.getLineFromLineNumber(lineNumber); - } - - public getLineFromLineNumber(lineNumber: number): ITextSnapshotLine { - var line = this.lines[lineNumber]; - if (line === undefined) { - line = this.getLineFromLineNumberWorker(lineNumber); - this.lines[lineNumber] = line; - } - return line; - } - - private getLineFromLineNumberWorker(lineNumber: number): ITextSnapshotLine { - var lineMap = this.snapshot.lineMap().lineStarts(); - var lineMapIndex = lineNumber; //Note: lineMap is 0-based - if (lineMapIndex < 0 || lineMapIndex >= lineMap.length) - throw new Error(TypeScript.getDiagnosticMessage(TypeScript.DiagnosticCode.Invalid_line_number_0, [lineMapIndex])); - var start = lineMap[lineMapIndex]; - - var end: number; - var endIncludingLineBreak: number; - var lineBreak = ""; - if (lineMapIndex == lineMap.length) { - end = endIncludingLineBreak = this.snapshot.length(); - } - else { - endIncludingLineBreak = (lineMapIndex >= lineMap.length - 1 ? this.snapshot.length() : lineMap[lineMapIndex + 1]); - for (var p = endIncludingLineBreak - 1; p >= start; p--) { - var c = this.snapshot.substr(p, 1); - //TODO: Other ones? - if (c != "\r" && c != "\n") { - break; - } - } - end = p + 1; - lineBreak = this.snapshot.substr(end, endIncludingLineBreak - end); - } - var result = new TextSnapshotLine(this, lineNumber, start, end, lineBreak); - return result; - } - } -} \ No newline at end of file diff --git a/src/services/formatting/textSnapshotLine.ts b/src/services/formatting/textSnapshotLine.ts deleted file mode 100644 index df2d9eff237..00000000000 --- a/src/services/formatting/textSnapshotLine.ts +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// - -module TypeScript.Services.Formatting { - export interface ITextSnapshotLine { - snapshot(): ITextSnapshot; - - start(): SnapshotPoint; - startPosition(): number; - - end(): SnapshotPoint; - endPosition(): number; - - endIncludingLineBreak(): SnapshotPoint; - endIncludingLineBreakPosition(): number; - - length(): number; - lineNumber(): number; - getText(): string; - } - - export class TextSnapshotLine implements ITextSnapshotLine { - constructor(private _snapshot: ITextSnapshot, private _lineNumber: number, private _start: number, private _end: number, private _lineBreak: string) { - } - - public snapshot() { - return this._snapshot; - } - - public start() { - return new SnapshotPoint(this._snapshot, this._start); - } - - public startPosition() { - return this._start; - } - - public end() { - return new SnapshotPoint(this._snapshot, this._end); - } - - public endPosition() { - return this._end; - } - - public endIncludingLineBreak() { - return new SnapshotPoint(this._snapshot, this._end + this._lineBreak.length); - } - - public endIncludingLineBreakPosition() { - return this._end + this._lineBreak.length; - } - - public length() { - return this._end - this._start; - } - - public lineNumber() { - return this._lineNumber; - } - - public getText(): string { - return this._snapshot.getText(TextSpan.fromBounds(this._start, this._end)); - } - } -} \ No newline at end of file diff --git a/src/services/formatting/tokenRange.ts b/src/services/formatting/tokenRange.ts index 0c06f797025..8d421140e67 100644 --- a/src/services/formatting/tokenRange.ts +++ b/src/services/formatting/tokenRange.ts @@ -13,9 +13,9 @@ // limitations under the License. // -/// +/// -module TypeScript.Services.Formatting { +module ts.formatting { export module Shared { export interface ITokenAccess { GetTokens(): SyntaxKind[]; @@ -41,12 +41,6 @@ module TypeScript.Services.Formatting { public Contains(token: SyntaxKind): boolean { return this.tokens.indexOf(token) >= 0; } - - - public toString(): string { - return "[tokenRangeStart=" + SyntaxKind[this.tokens[0]] + "," + - "tokenRangeEnd=" + SyntaxKind[this.tokens[this.tokens.length - 1]] + "]"; - } } export class TokenValuesAccess implements ITokenAccess { @@ -76,10 +70,6 @@ module TypeScript.Services.Formatting { public Contains(tokenValue: SyntaxKind): boolean { return tokenValue == this.token; } - - public toString(): string { - return "[singleTokenKind=" + SyntaxKind[this.token] + "]"; - } } export class TokenAllAccess implements ITokenAccess { @@ -135,18 +125,18 @@ module TypeScript.Services.Formatting { static Any: TokenRange = TokenRange.AllTokens(); static AnyIncludingMultilineComments = TokenRange.FromTokens(TokenRange.Any.GetTokens().concat([SyntaxKind.MultiLineCommentTrivia])); static Keywords = TokenRange.FromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); - static Operators = TokenRange.FromRange(SyntaxKind.SemicolonToken, SyntaxKind.SlashEqualsToken); - static BinaryOperators = TokenRange.FromRange(SyntaxKind.LessThanToken, SyntaxKind.SlashEqualsToken); + static Operators = TokenRange.FromRange(SyntaxKind.FirstOperator, SyntaxKind.LastOperator); + static BinaryOperators = TokenRange.FromRange(SyntaxKind.FirstBinaryOperator, SyntaxKind.LastBinaryOperator); static BinaryKeywordOperators = TokenRange.FromTokens([SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword]); - static ReservedKeywords = TokenRange.FromRange(SyntaxKind.FirstFutureReservedStrictKeyword, SyntaxKind.LastFutureReservedStrictKeyword); + static ReservedKeywords = TokenRange.FromRange(SyntaxKind.FirstFutureReservedWord, SyntaxKind.LastFutureReservedWord); static UnaryPrefixOperators = TokenRange.FromTokens([SyntaxKind.PlusPlusToken, SyntaxKind.MinusMinusToken, SyntaxKind.TildeToken, SyntaxKind.ExclamationToken]); - static UnaryPrefixExpressions = TokenRange.FromTokens([SyntaxKind.NumericLiteral, SyntaxKind.IdentifierName, SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.OpenBraceToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); - static UnaryPreincrementExpressions = TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); - static UnaryPostincrementExpressions = TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); - static UnaryPredecrementExpressions = TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); - static UnaryPostdecrementExpressions = TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); + static UnaryPrefixExpressions = TokenRange.FromTokens([SyntaxKind.NumericLiteral, SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.OpenBraceToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPreincrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPostincrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); + static UnaryPredecrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPostdecrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); static Comments = TokenRange.FromTokens([SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia]); - static TypeNames = TokenRange.FromTokens([SyntaxKind.IdentifierName, SyntaxKind.NumberKeyword, SyntaxKind.StringKeyword, SyntaxKind.BooleanKeyword, SyntaxKind.VoidKeyword, SyntaxKind.AnyKeyword]); + static TypeNames = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.NumberKeyword, SyntaxKind.StringKeyword, SyntaxKind.BooleanKeyword, SyntaxKind.VoidKeyword, SyntaxKind.AnyKeyword]); } } } \ No newline at end of file diff --git a/src/services/formatting/tokenSpan.ts b/src/services/formatting/tokenSpan.ts index aba9c372f35..95f9bab3eee 100644 --- a/src/services/formatting/tokenSpan.ts +++ b/src/services/formatting/tokenSpan.ts @@ -13,11 +13,10 @@ // limitations under the License. // -/// +/// - -module TypeScript.Services.Formatting { - export class TokenSpan extends TextSpan { +module ts.formatting { + export class TokenSpan extends TypeScript.TextSpan { constructor(public kind: SyntaxKind, start: number, length: number) { super(start, length); } diff --git a/src/services/indentation.ts b/src/services/indentation.ts deleted file mode 100644 index b35a4540fba..00000000000 --- a/src/services/indentation.ts +++ /dev/null @@ -1,49 +0,0 @@ - -module TypeScript.Indentation { - // Returns the column that this input string ends at (assuming it starts at column 0). - export function columnForPositionInString(input: string, position: number, options: FormattingOptions): number { - return columnForPositionInStringWorker(input, position, 0, options); - } - - function columnForPositionInStringWorker(input: string, position: number, startColumn: number, options: FormattingOptions): number { - var column = startColumn; - var spacesPerTab = options.spacesPerTab; - - for (var j = 0; j < position; j++) { - var ch = input.charCodeAt(j); - - if (ch === CharacterCodes.tab) { - column += spacesPerTab - column % spacesPerTab; - } - else { - column++; - } - } - - return column; - } - - export function indentationString(column: number, options: FormattingOptions): string { - var numberOfTabs = 0; - var numberOfSpaces = Math.max(0, column); - - if (options.useTabs) { - numberOfTabs = Math.floor(column / options.spacesPerTab); - numberOfSpaces -= numberOfTabs * options.spacesPerTab; - } - - return StringUtilities.repeat('\t', numberOfTabs) + - StringUtilities.repeat(' ', numberOfSpaces); - } - - export function firstNonWhitespacePosition(value: string): number { - for (var i = 0; i < value.length; i++) { - var ch = value.charCodeAt(i); - if (!CharacterInfo.isWhitespace(ch)) { - return i; - } - } - - return value.length; - } -} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index 800d63738f0..1eed221d403 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -8,11 +8,10 @@ /// /// /// -/// /// /// -/// -/// +/// +/// /// /// @@ -667,6 +666,7 @@ module ts { public text: string; public getLineAndCharacterFromPosition(position: number): { line: number; character: number } { return null; } public getPositionFromLineAndCharacter(line: number, character: number): number { return -1; } + public getLineStarts(): number[] { return undefined; } public amdDependencies: string[]; public referencedFiles: FileReference[]; public syntacticErrors: Diagnostic[]; @@ -2137,7 +2137,7 @@ module ts { export function createLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry): LanguageService { var syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); - var formattingRulesProvider: TypeScript.Services.Formatting.RulesProvider; + var ruleProvider: ts.formatting.RulesProvider; var hostCache: HostCache; // A cache of all the information about the files on the host side. var program: Program; @@ -2168,6 +2168,16 @@ module ts { return fullTypeCheckChecker_doNotAccessDirectly || (fullTypeCheckChecker_doNotAccessDirectly = program.getTypeChecker(/*fullTypeCheck*/ true)); } + function getRuleProvider(options: FormatCodeOptions) { + // Ensure rules are initialized and up to date wrt to formatting options + if (!ruleProvider) { + ruleProvider = new ts.formatting.RulesProvider(host); + } + + ruleProvider.ensureUpToDate(options); + return ruleProvider; + } + function createCompilerHost(): CompilerHost { return { getSourceFile: (filename, languageVersion) => { @@ -5044,7 +5054,7 @@ module ts { } } - if (isPunctuation(token)) { + if (isPunctuation(token.kind)) { // the '=' in a variable declaration is special cased here. if (token.parent.kind === SyntaxKind.BinaryExpression || token.parent.kind === SyntaxKind.VariableDeclaration || @@ -5190,62 +5200,39 @@ module ts { host.log("getIndentationAtPosition: getCurrentSourceFile: " + (new Date().getTime() - start)); var start = new Date().getTime(); - var options = new TypeScript.FormattingOptions(!editorOptions.ConvertTabsToSpaces, editorOptions.TabSize, editorOptions.IndentSize, editorOptions.NewLineCharacter) - var result = formatting.SmartIndenter.getIndentation(position, sourceFile, options); + var result = formatting.SmartIndenter.getIndentation(position, sourceFile, editorOptions); host.log("getIndentationAtPosition: computeIndentation : " + (new Date().getTime() - start)); return result; } - function getFormattingManager(filename: string, options: FormatCodeOptions) { - // Ensure rules are initialized and up to date wrt to formatting options - if (formattingRulesProvider == null) { - formattingRulesProvider = new TypeScript.Services.Formatting.RulesProvider(host); - } - - formattingRulesProvider.ensureUpToDate(options); - - // Get the Syntax Tree - var syntaxTree = getSyntaxTree(filename); - - // Convert IScriptSnapshot to ITextSnapshot - var scriptSnapshot = syntaxTreeCache.getCurrentScriptSnapshot(filename); - var scriptText = TypeScript.SimpleText.fromScriptSnapshot(scriptSnapshot); - var textSnapshot = new TypeScript.Services.Formatting.TextSnapshot(scriptText); - - var manager = new TypeScript.Services.Formatting.FormattingManager(syntaxTree, textSnapshot, formattingRulesProvider, options); - - return manager; - } - function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[] { fileName = normalizeSlashes(fileName); - - var manager = getFormattingManager(fileName, options); - return manager.formatSelection(start, end); + var sourceFile = getCurrentSourceFile(fileName); + return formatting.formatSelection(start, end, sourceFile, getRuleProvider(options), options); } function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { fileName = normalizeSlashes(fileName); - var manager = getFormattingManager(fileName, options); - return manager.formatDocument(); + var sourceFile = getCurrentSourceFile(fileName); + return formatting.formatDocument(sourceFile, getRuleProvider(options), options); } function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[] { fileName = normalizeSlashes(fileName); - var manager = getFormattingManager(fileName, options); + var sourceFile = getCurrentSourceFile(fileName); if (key === "}") { - return manager.formatOnClosingCurlyBrace(position); + return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(options), options); } else if (key === ";") { - return manager.formatOnSemicolon(position); + return formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(options), options); } else if (key === "\n") { - return manager.formatOnEnter(position); + return formatting.formatOnEnter(position, sourceFile, getRuleProvider(options), options); } return []; diff --git a/src/services/formatting/smartIndenter.ts b/src/services/smartIndenter.ts similarity index 70% rename from src/services/formatting/smartIndenter.ts rename to src/services/smartIndenter.ts index 029df425045..7a56ea07be3 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/smartIndenter.ts @@ -1,8 +1,8 @@ -/// +/// module ts.formatting { export module SmartIndenter { - export function getIndentation(position: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorOptions): number { if (position > sourceFile.text.length) { return 0; // past EOF } @@ -13,8 +13,8 @@ module ts.formatting { } // no indentation in string \regex literals - if ((precedingToken.kind === SyntaxKind.StringLiteral || precedingToken.kind === SyntaxKind.RegularExpressionLiteral) && - precedingToken.getStart(sourceFile) <= position && + if ((precedingToken.kind === SyntaxKind.StringLiteral || precedingToken.kind === SyntaxKind.RegularExpressionLiteral) && + precedingToken.getStart(sourceFile) <= position && precedingToken.end > position) { return 0; } @@ -37,14 +37,14 @@ module ts.formatting { var indentationDelta: number; while (current) { - if (positionBelongsToNode(current, position, sourceFile) && nodeContentIsIndented(current, previous)) { + if (positionBelongsToNode(current, position, sourceFile) && shouldIndentChildNode(current.kind, previous ? previous.kind : SyntaxKind.Unknown)) { currentStart = getStartLineAndCharacterForNode(current, sourceFile); if (nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)) { indentationDelta = 0; } else { - indentationDelta = lineAtPosition !== currentStart.line ? options.indentSpaces : 0; + indentationDelta = lineAtPosition !== currentStart.line ? options.IndentSize : 0; } break; @@ -59,12 +59,27 @@ module ts.formatting { previous = current; current = current.parent; } - + if (!current) { // no parent was found - return 0 to be indented on the level of SourceFile return 0; } + return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta, sourceFile, options); + } + + export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: FormatCodeOptions): number { + var start = sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)); + return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, options); + } + + function getIndentationForNodeWorker( + current: Node, + currentStart: LineAndCharacter, + ignoreActualIndentationRange: TextRange, + indentationDelta: number, + sourceFile: SourceFile, + options: EditorOptions): number { var parent: Node = current.parent; var parentStart: LineAndCharacter; @@ -72,26 +87,35 @@ module ts.formatting { // walk upwards and collect indentations for pairs of parent-child nodes // indentation is not added if parent and child nodes start on the same line or if parent is IfStatement and child starts on the same line with 'else clause' while (parent) { - // check if current node is a list item - if yes, take indentation from it - var actualIndentation = getActualIndentationForListItem(current, sourceFile, options); - if (actualIndentation !== -1) { - return actualIndentation + indentationDelta; + var useActualIndentation = true; + if (ignoreActualIndentationRange) { + var start = current.getStart(sourceFile); + useActualIndentation = start < ignoreActualIndentationRange.pos || start > ignoreActualIndentationRange.end; } - parentStart = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)); - var parentAndChildShareLine = - parentStart.line === currentStart.line || + if (useActualIndentation) { + // check if current node is a list item - if yes, take indentation from it + var actualIndentation = getActualIndentationForListItem(current, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation + indentationDelta; + } + } + parentStart = getParentStart(parent, current, sourceFile); + var parentAndChildShareLine = + parentStart.line === currentStart.line || childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStart.line, sourceFile); - // try to fetch actual indentation for current node from source text - var actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options); - if (actualIndentation !== -1) { - return actualIndentation + indentationDelta; + if (useActualIndentation) { + // try to fetch actual indentation for current node from source text + var actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation + indentationDelta; + } } // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line - if (nodeContentIsIndented(parent, current) && !parentAndChildShareLine) { - indentationDelta += options.indentSpaces; + if (shouldIndentChildNode(parent.kind, current.kind) && !parentAndChildShareLine) { + indentationDelta += options.IndentSize; } current = parent; @@ -102,10 +126,20 @@ module ts.formatting { return indentationDelta; } + + function getParentStart(parent: Node, child: Node, sourceFile: SourceFile): LineAndCharacter { + var containingList = getContainingList(child, sourceFile); + if (containingList) { + return sourceFile.getLineAndCharacterFromPosition(containingList.pos); + } + + return sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)); + } + /* * Function returns -1 if indentation cannot be determined - */ - function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + */ + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorOptions): number { // previous token is comma that separates items in list - find the previous item and try to derive indentation from it var commaItemInfo = findListItemInfo(commaToken); Debug.assert(commaItemInfo && commaItemInfo.listItemIndex > 0); @@ -116,20 +150,20 @@ module ts.formatting { /* * Function returns -1 if actual indentation for node should not be used (i.e because node is nested expression) */ - function getActualIndentationForNode(current: Node, - parent: Node, - currentLineAndChar: LineAndCharacter, - parentAndChildShareLine: boolean, - sourceFile: SourceFile, - options: TypeScript.FormattingOptions): number { + function getActualIndentationForNode(current: Node, + parent: Node, + currentLineAndChar: LineAndCharacter, + parentAndChildShareLine: boolean, + sourceFile: SourceFile, + options: EditorOptions): number { // actual indentation is used for statements\declarations if one of cases below is true: // - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually // - parent and child are not on the same line - var useActualIndentation = + var useActualIndentation = (isDeclaration(current) || isStatement(current)) && (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine); - + if (!useActualIndentation) { return -1; } @@ -142,7 +176,7 @@ module ts.formatting { if (!nextToken) { return false; } - + if (nextToken.kind === SyntaxKind.OpenBraceToken) { // open braces are always indented at the parent level return true; @@ -172,30 +206,30 @@ module ts.formatting { return candidate.end > position || !isCompletedNode(candidate, sourceFile); } - function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFile): boolean { + export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFile): boolean { if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { var elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile); Debug.assert(elseKeyword !== undefined); - var elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; + var elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; return elseKeywordStartLine === childStartLine; } + + return false; } - function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function getContainingList(node: Node, sourceFile: SourceFile): NodeArray { if (node.parent) { switch (node.parent.kind) { case SyntaxKind.TypeReference: if ((node.parent).typeArguments) { - return getActualIndentationFromList((node.parent).typeArguments); + return (node.parent).typeArguments; } break; case SyntaxKind.ObjectLiteral: - return getActualIndentationFromList((node.parent).properties); - case SyntaxKind.TypeLiteral: - return getActualIndentationFromList((node.parent).members); + return (node.parent).properties; case SyntaxKind.ArrayLiteral: - return getActualIndentationFromList((node.parent).elements); + return (node.parent).elements; case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: @@ -203,21 +237,26 @@ module ts.formatting { case SyntaxKind.CallSignature: case SyntaxKind.ConstructSignature: if ((node.parent).typeParameters && node.end < (node.parent).typeParameters.end) { - return getActualIndentationFromList((node.parent).typeParameters); + return (node.parent).typeParameters; } - - return getActualIndentationFromList((node.parent).parameters); + + return (node.parent).parameters; case SyntaxKind.NewExpression: case SyntaxKind.CallExpression: if ((node.parent).typeArguments && node.end < (node.parent).typeArguments.end) { - return getActualIndentationFromList((node.parent).typeArguments); + return (node.parent).typeArguments; } - - return getActualIndentationFromList((node.parent).arguments); + + return (node.parent).arguments; } } - return -1; + return undefined; + } + + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorOptions): number { + var containingList = getContainingList(node, sourceFile); + return containingList ? getActualIndentationFromList(containingList) : -1; function getActualIndentationFromList(list: Node[]): number { var index = indexOf(list, node); @@ -226,13 +265,13 @@ module ts.formatting { } - function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: EditorOptions): number { Debug.assert(index >= 0 && index < list.length); var node = list[index]; // walk toward the start of the list starting from current node and check if the line is the same for all items. // if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i] - var lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile); + var lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile); for (var i = index - 1; i >= 0; --i) { if (list[i].kind === SyntaxKind.CommaToken) { continue; @@ -248,53 +287,34 @@ module ts.formatting { return -1; } - function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorOptions): number { var lineStart = sourceFile.getPositionFromLineAndCharacter(lineAndCharacter.line, 1); + return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options); + } + + export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorOptions): number { var column = 0; - for (var i = 0; i < lineAndCharacter.character; ++i) { - var charCode = sourceFile.text.charCodeAt(lineStart + i); - if (!isWhiteSpace(charCode)) { + for (var pos = startPos; pos < endPos; ++pos) { + var ch = sourceFile.text.charCodeAt(pos); + if (!isWhiteSpace(ch)) { return column; } - if (charCode === CharacterCodes.tab) { - column += options.spacesPerTab; + if (ch === CharacterCodes.tab) { + column += options.TabSize + (column % options.TabSize); } else { column++; } } - return column; } - function nodeContentIsIndented(parent: Node, child: Node): boolean { - switch (parent.kind) { + function nodeContentIsAlwaysIndented(kind: SyntaxKind): boolean { + switch (kind) { case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: - return true; - case SyntaxKind.ModuleDeclaration: - // ModuleBlock should take care of indentation - return false; - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.Method: - case SyntaxKind.FunctionExpression: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.Constructor: - // FunctionBlock should take care of indentation - return false; - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForStatement: - return child && child.kind !== SyntaxKind.Block; - case SyntaxKind.IfStatement: - return child && child.kind !== SyntaxKind.Block; - case SyntaxKind.TryStatement: - // TryBlock\CatchBlock\FinallyBlock should take care of indentation - return false; case SyntaxKind.ArrayLiteral: case SyntaxKind.Block: case SyntaxKind.FunctionBlock: @@ -312,7 +332,32 @@ module ts.formatting { case SyntaxKind.NewExpression: case SyntaxKind.VariableStatement: case SyntaxKind.VariableDeclaration: + case SyntaxKind.ExportAssignment: + case SyntaxKind.ReturnStatement: return true; + } + return false; + } + + export function shouldIndentChildNode(parent: SyntaxKind, child: SyntaxKind): boolean { + if (nodeContentIsAlwaysIndented(parent)) { + return true; + } + switch (parent) { + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.IfStatement: + return child !== SyntaxKind.Block; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Method: + case SyntaxKind.ArrowFunction: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return child !== SyntaxKind.FunctionBlock; default: return false; } @@ -368,7 +413,7 @@ module ts.formatting { if ((n).elseStatement) { return isCompletedNode((n).elseStatement, sourceFile); } - return isCompletedNode((n).thenStatement, sourceFile); + return isCompletedNode((n).thenStatement, sourceFile); case SyntaxKind.ExpressionStatement: return isCompletedNode((n).expression, sourceFile); case SyntaxKind.ArrayLiteral: @@ -384,10 +429,10 @@ module ts.formatting { case SyntaxKind.DoStatement: // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; var hasWhileKeyword = findChildOfKind(n, SyntaxKind.WhileKeyword, sourceFile); - if(hasWhileKeyword) { + if (hasWhileKeyword) { return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile); } - return isCompletedNode((n).statement, sourceFile); + return isCompletedNode((n).statement, sourceFile); default: return true; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 8745421c269..8620f8d1920 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -5,6 +5,66 @@ module ts { list: Node; } + export function getEndLinePosition(line: number, sourceFile: SourceFile): number { + Debug.assert(line >= 1); + var lineStarts = sourceFile.getLineStarts(); + + // lines returned by SourceFile.getLineAndCharacterForPosition are 1-based + var lineIndex = line - 1; + if (lineIndex === lineStarts.length - 1) { + // last line - return EOF + return sourceFile.text.length - 1; + } + else { + // current line start + var start = lineStarts[lineIndex]; + // take the start position of the next line -1 = it should be some line break + var pos = lineStarts[lineIndex + 1] - 1; + Debug.assert(isLineBreak(sourceFile.text.charCodeAt(pos))); + // walk backwards skipping line breaks, stop the the beginning of current line. + // i.e: + // + // $ <- end of line for this position should match the start position + while (start <= pos && isLineBreak(sourceFile.text.charCodeAt(pos))) { + pos--; + } + return pos; + } + } + + export function getStartPositionOfLine(line: number, sourceFile: SourceFile): number { + Debug.assert(line >= 1); + return sourceFile.getLineStarts()[line - 1]; + } + + export function getStartLinePositionForPosition(position: number, sourceFile: SourceFile): number { + var lineStarts = sourceFile.getLineStarts(); + var line = sourceFile.getLineAndCharacterFromPosition(position).line; + return lineStarts[line - 1]; + } + + export function rangeContainsRange(r1: TextRange, r2: TextRange): boolean { + return startEndContainsRange(r1.pos, r1.end, r2); + } + + export function startEndContainsRange(start: number, end: number, range: TextRange): boolean { + return start <= range.pos && end >= range.end; + } + + export function rangeContainsStartEnd(range: TextRange, start: number, end: number): boolean { + return range.pos <= start && range.end >= end; + } + + export function rangeOverlapsWithStartEnd(r1: TextRange, start: number, end: number) { + return startEndOverlapsWithStartEnd(r1.pos, r1.end, start, end); + } + + export function startEndOverlapsWithStartEnd(start1: number, end1: number, start2: number, end2: number) { + var start = Math.max(start1, start2); + var end = Math.min(end1, end2); + return start < end; + } + export function findListItemInfo(node: Node): ListItemInfo { var syntaxList = findContainingList(node); @@ -63,14 +123,14 @@ module ts { * position >= start and (position < end or (position === end && token is keyword or identifier)) */ export function getTouchingWord(sourceFile: SourceFile, position: number): Node { - return getTouchingToken(sourceFile, position, isWord); + return getTouchingToken(sourceFile, position, n => isWord(n.kind)); } /* Gets the token whose text has range [start, end) and position >= start * and (position < end or (position === end && token is keyword or identifier or numeric\string litera)) */ export function getTouchingPropertyName(sourceFile: SourceFile, position: number): Node { - return getTouchingToken(sourceFile, position, isPropertyName); + return getTouchingToken(sourceFile, position, n => isPropertyName(n.kind)); } /** Returns the token if position is in [start, end) or if position === end and includeItemAtEndPosition(token) === true */ @@ -229,7 +289,7 @@ module ts { return n.getWidth() !== 0; } - export function getTypeArgumentOrTypeParameterList(node: Node): NodeArray { + export function getTypeArgumentOrTypeParameterList(node: Node): NodeArray { if (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.CallExpression) { return (node).typeArguments; } @@ -245,19 +305,19 @@ module ts { return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken; } - function isWord(n: Node): boolean { - return n.kind === SyntaxKind.Identifier || isKeyword(n.kind); + function isWord(kind: SyntaxKind): boolean { + return kind === SyntaxKind.Identifier || isKeyword(kind); } - function isPropertyName(n: Node): boolean { - return n.kind === SyntaxKind.StringLiteral || n.kind === SyntaxKind.NumericLiteral || isWord(n); + function isPropertyName(kind: SyntaxKind): boolean { + return kind === SyntaxKind.StringLiteral || kind === SyntaxKind.NumericLiteral || isWord(kind); } - export function isComment(n: Node): boolean { - return n.kind === SyntaxKind.SingleLineCommentTrivia || n.kind === SyntaxKind.MultiLineCommentTrivia; + export function isComment(kind: SyntaxKind): boolean { + return kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia; } - export function isPunctuation(n: Node): boolean { - return SyntaxKind.FirstPunctuation <= n.kind && n.kind <= SyntaxKind.LastPunctuation; + export function isPunctuation(kind: SyntaxKind): boolean { + return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation; } } \ No newline at end of file diff --git a/tests/cases/fourslash/formatInTryCatchFinally.ts b/tests/cases/fourslash/formatInTryCatchFinally.ts new file mode 100644 index 00000000000..dbbf28fb878 --- /dev/null +++ b/tests/cases/fourslash/formatInTryCatchFinally.ts @@ -0,0 +1,13 @@ +/// + +////try +////{ +//// var x = 1/*1*/ +////} +////catch (e) +////{ +////} + +goTo.marker("1"); +edit.insert(";") +verify.currentLineContentIs(" var x = 1;"); diff --git a/tests/cases/fourslash/formattingAfterMultiLineIfCondition.ts b/tests/cases/fourslash/formattingAfterMultiLineIfCondition.ts index 843eea5155b..21812b0b2b3 100644 --- a/tests/cases/fourslash/formattingAfterMultiLineIfCondition.ts +++ b/tests/cases/fourslash/formattingAfterMultiLineIfCondition.ts @@ -11,4 +11,4 @@ goTo.marker(); edit.insert('}'); goTo.marker('comment'); // Comment below multi-line 'if' condition formatting -verify.currentLineContentIs(' // This is a comment'); \ No newline at end of file +verify.currentLineContentIs(' // This is a comment'); \ No newline at end of file diff --git a/tests/cases/fourslash/formattingBlockInCaseClauses.ts b/tests/cases/fourslash/formattingBlockInCaseClauses.ts new file mode 100644 index 00000000000..64cd74ca858 --- /dev/null +++ b/tests/cases/fourslash/formattingBlockInCaseClauses.ts @@ -0,0 +1,12 @@ +/// + +////switch (1) { +//// case 1: +//// { +//// /*1*/ +//// break; +////} + +goTo.marker("1"); +edit.insert("}"); +verify.currentLineContentIs(" }"); diff --git a/tests/cases/fourslash/formattingFatArrowFunctions.ts b/tests/cases/fourslash/formattingFatArrowFunctions.ts index 699e0b53157..73463852a67 100644 --- a/tests/cases/fourslash/formattingFatArrowFunctions.ts +++ b/tests/cases/fourslash/formattingFatArrowFunctions.ts @@ -120,13 +120,13 @@ verify.currentLineContentIs("(arg) => 2;"); goTo.marker("3"); verify.currentLineContentIs("arg => 2;"); goTo.marker("4"); -verify.currentLineContentIs("(arg = 1) => 3;"); +verify.currentLineContentIs("(arg = 1) => 3;"); goTo.marker("5"); verify.currentLineContentIs("(arg?) => 4;"); goTo.marker("6"); verify.currentLineContentIs("(arg: number) => 5;"); goTo.marker("7"); -verify.currentLineContentIs("(arg: number = 0) => 6;"); +verify.currentLineContentIs("(arg: number = 0) => 6;"); goTo.marker("8"); verify.currentLineContentIs("(arg?: number) => 7;"); goTo.marker("9"); @@ -134,13 +134,13 @@ verify.currentLineContentIs("(...arg: number[]) => 8;"); goTo.marker("10"); verify.currentLineContentIs("(arg1, arg2) => 12;"); goTo.marker("11"); -verify.currentLineContentIs("(arg1 = 1, arg2 = 3) => 13;"); +verify.currentLineContentIs("(arg1 = 1, arg2 = 3) => 13;"); goTo.marker("12"); verify.currentLineContentIs("(arg1?, arg2?) => 14;"); goTo.marker("13"); verify.currentLineContentIs("(arg1: number, arg2: number) => 15;"); goTo.marker("14"); -verify.currentLineContentIs("(arg1: number = 0, arg2: number = 1) => 16;"); +verify.currentLineContentIs("(arg1: number = 0, arg2: number = 1) => 16;"); goTo.marker("15"); verify.currentLineContentIs("(arg1?: number, arg2?: number) => 17;"); goTo.marker("16"); @@ -152,13 +152,13 @@ verify.currentLineContentIs("(() => 21);"); goTo.marker("19"); verify.currentLineContentIs("((arg) => 22);"); goTo.marker("20"); -verify.currentLineContentIs("((arg = 1) => 23);"); +verify.currentLineContentIs("((arg = 1) => 23);"); goTo.marker("21"); verify.currentLineContentIs("((arg?) => 24);"); goTo.marker("22"); verify.currentLineContentIs("((arg: number) => 25);"); goTo.marker("23"); -verify.currentLineContentIs("((arg: number = 0) => 26);"); +verify.currentLineContentIs("((arg: number = 0) => 26);"); goTo.marker("24"); verify.currentLineContentIs("((arg?: number) => 27);"); goTo.marker("25"); @@ -170,7 +170,7 @@ verify.currentLineContentIs("false ? () => 41 : null;"); goTo.marker("28"); verify.currentLineContentIs("false ? (arg) => 42 : null;"); goTo.marker("29"); -verify.currentLineContentIs("false ? (arg = 1) => 43 : null;"); +verify.currentLineContentIs("false ? (arg = 1) => 43 : null;"); goTo.marker("30"); verify.currentLineContentIs("false ? (arg?) => 44 : null;"); goTo.marker("31"); @@ -178,7 +178,7 @@ verify.currentLineContentIs("false ? (arg: number) => 45 : null;"); goTo.marker("32"); verify.currentLineContentIs("false ? (arg?: number) => 46 : null;"); goTo.marker("33"); -verify.currentLineContentIs("false ? (arg?: number = 0) => 47 : null;"); +verify.currentLineContentIs("false ? (arg?: number = 0) => 47 : null;"); goTo.marker("34"); verify.currentLineContentIs("false ? (...arg: number[]) => 48 : null;"); goTo.marker("35"); @@ -186,7 +186,7 @@ verify.currentLineContentIs("false ? (() => 51) : null;"); goTo.marker("36"); verify.currentLineContentIs("false ? ((arg) => 52) : null;"); goTo.marker("37"); -verify.currentLineContentIs("false ? ((arg = 1) => 53) : null;"); +verify.currentLineContentIs("false ? ((arg = 1) => 53) : null;"); goTo.marker("38"); verify.currentLineContentIs("false ? ((arg?) => 54) : null;"); goTo.marker("39"); @@ -194,7 +194,7 @@ verify.currentLineContentIs("false ? ((arg: number) => 55) : null;"); goTo.marker("40"); verify.currentLineContentIs("false ? ((arg?: number) => 56) : null;"); goTo.marker("41"); -verify.currentLineContentIs("false ? ((arg?: number = 0) => 57) : null;"); +verify.currentLineContentIs("false ? ((arg?: number = 0) => 57) : null;"); goTo.marker("42"); verify.currentLineContentIs("false ? ((...arg: number[]) => 58) : null;"); goTo.marker("43"); @@ -202,7 +202,7 @@ verify.currentLineContentIs("false ? null : () => 61;"); goTo.marker("44"); verify.currentLineContentIs("false ? null : (arg) => 62;"); goTo.marker("45"); -verify.currentLineContentIs("false ? null : (arg = 1) => 63;"); +verify.currentLineContentIs("false ? null : (arg = 1) => 63;"); goTo.marker("46"); verify.currentLineContentIs("false ? null : (arg?) => 64;"); goTo.marker("47"); @@ -210,7 +210,7 @@ verify.currentLineContentIs("false ? null : (arg: number) => 65;"); goTo.marker("48"); verify.currentLineContentIs("false ? null : (arg?: number) => 66;"); goTo.marker("49"); -verify.currentLineContentIs("false ? null : (arg?: number = 0) => 67;"); +verify.currentLineContentIs("false ? null : (arg?: number = 0) => 67;"); goTo.marker("50"); verify.currentLineContentIs("false ? null : (...arg: number[]) => 68;"); goTo.marker("51"); @@ -220,13 +220,13 @@ verify.currentLineContentIs("((a?) => { return a; }) ? (b) => (c) => 81 : (c) => goTo.marker("53"); verify.currentLineContentIs("((arg) => 90) instanceof Function;"); goTo.marker("54"); -verify.currentLineContentIs("((arg = 1) => 91) instanceof Function;"); +verify.currentLineContentIs("((arg = 1) => 91) instanceof Function;"); goTo.marker("55"); verify.currentLineContentIs("((arg?) => 92) instanceof Function;"); goTo.marker("56"); verify.currentLineContentIs("((arg: number) => 93) instanceof Function;"); goTo.marker("57"); -verify.currentLineContentIs("((arg: number = 1) => 94) instanceof Function;"); +verify.currentLineContentIs("((arg: number = 1) => 94) instanceof Function;"); goTo.marker("58"); verify.currentLineContentIs("((arg?: number) => 95) instanceof Function;"); goTo.marker("59"); @@ -237,13 +237,13 @@ verify.currentLineContentIs("'' + ((arg) => 100);"); goTo.marker("61"); verify.currentLineContentIs("((arg) => 0) + '' + ((arg) => 101);"); goTo.marker("62"); -verify.currentLineContentIs("((arg = 1) => 0) + '' + ((arg = 2) => 102);"); +verify.currentLineContentIs("((arg = 1) => 0) + '' + ((arg = 2) => 102);"); goTo.marker("63"); verify.currentLineContentIs("((arg?) => 0) + '' + ((arg?) => 103);"); goTo.marker("64"); verify.currentLineContentIs("((arg: number) => 0) + '' + ((arg: number) => 104);"); goTo.marker("65"); -verify.currentLineContentIs("((arg: number = 1) => 0) + '' + ((arg: number = 2) => 105);"); +verify.currentLineContentIs("((arg: number = 1) => 0) + '' + ((arg: number = 2) => 105);"); goTo.marker("66"); verify.currentLineContentIs("((arg?: number) => 0) + '' + ((arg?: number) => 106);"); goTo.marker("67"); @@ -273,17 +273,17 @@ verify.currentLineContentIs(" (a, b?) => 114,"); goTo.marker("79"); verify.currentLineContentIs(" (a: number) => 115,"); goTo.marker("80"); -verify.currentLineContentIs(" (a: number = 0) => 116,"); +verify.currentLineContentIs(" (a: number = 0) => 116,"); goTo.marker("81"); -verify.currentLineContentIs(" (a = 0) => 117,"); +verify.currentLineContentIs(" (a = 0) => 117,"); goTo.marker("82"); -verify.currentLineContentIs(" (a: number = 0) => 118,"); +verify.currentLineContentIs(" (a: number = 0) => 118,"); goTo.marker("83"); verify.currentLineContentIs(" (a?, b?: number) => 118,"); goTo.marker("84"); verify.currentLineContentIs(" (...a: number[]) => 119,"); goTo.marker("85"); -verify.currentLineContentIs(" (a, b = 0, ...c: number[]) => 120,"); +verify.currentLineContentIs(" (a, b = 0, ...c: number[]) => 120,"); goTo.marker("86"); verify.currentLineContentIs(" (a) => (b) => (c) => 121,"); goTo.marker("87"); diff --git a/tests/cases/fourslash/formattingIfInElseBlock.ts b/tests/cases/fourslash/formattingIfInElseBlock.ts new file mode 100644 index 00000000000..b4f5246fb35 --- /dev/null +++ b/tests/cases/fourslash/formattingIfInElseBlock.ts @@ -0,0 +1,12 @@ +/// + +////if (true) { +////} +////else { +//// if (true) { +//// /*1*/ +////} + +goTo.marker("1"); +edit.insert("}") +verify.currentLineContentIs(" }"); diff --git a/tests/cases/fourslash/formattingOnStatementsWithNoSemicolon.ts b/tests/cases/fourslash/formattingOnStatementsWithNoSemicolon.ts index 8456182036d..c49152f5011 100644 --- a/tests/cases/fourslash/formattingOnStatementsWithNoSemicolon.ts +++ b/tests/cases/fourslash/formattingOnStatementsWithNoSemicolon.ts @@ -135,9 +135,9 @@ verify.currentLineContentIs(" return 0"); goTo.marker("51"); verify.currentLineContentIs("}).then(function(doc) {"); goTo.marker("52"); -verify.currentLineContentIs(" return 1"); +verify.currentLineContentIs(" return 1"); goTo.marker("53"); -verify.currentLineContentIs(" });"); +verify.currentLineContentIs("});"); goTo.marker("54"); verify.currentLineContentIs("if (1)"); goTo.marker("55"); diff --git a/tests/cases/fourslash/formattingOnTabAfterCloseCurly.ts b/tests/cases/fourslash/formattingOnTabAfterCloseCurly.ts index 3b6d60ef45b..2e7c344d225 100644 --- a/tests/cases/fourslash/formattingOnTabAfterCloseCurly.ts +++ b/tests/cases/fourslash/formattingOnTabAfterCloseCurly.ts @@ -4,7 +4,7 @@ //// export enum NodeType {/*2*/ //// Error,/*3*/ //// Comment,/*4*/ -//// } /*5*/ +//// } /*5*/ //// export enum foob/*6*/ //// { //// Blah=1, Bleah=2/*7*/ @@ -25,7 +25,7 @@ verify.currentLineContentIs(" }"); goTo.marker("6"); verify.currentLineContentIs(" export enum foob {"); goTo.marker("7"); -verify.currentLineContentIs(" Blah= 1, Bleah= 2"); +verify.currentLineContentIs(" Blah = 1, Bleah = 2"); goTo.marker("8"); verify.currentLineContentIs(" }"); goTo.marker("9"); diff --git a/tests/cases/fourslash/formattingSkippedTokens.ts b/tests/cases/fourslash/formattingSkippedTokens.ts index 8904b058191..094612c8939 100644 --- a/tests/cases/fourslash/formattingSkippedTokens.ts +++ b/tests/cases/fourslash/formattingSkippedTokens.ts @@ -15,8 +15,8 @@ verify.currentLineContentIs('foo(): Bar { }'); goTo.marker('2'); verify.currentLineContentIs('function Foo() # { }'); goTo.marker('3'); -verify.currentLineContentIs('4+:5'); +verify.currentLineContentIs('4 +:5'); goTo.marker('4'); verify.currentLineContentIs(' : T) { }'); goTo.marker('5'); -verify.currentLineContentIs('var x ='); \ No newline at end of file +verify.currentLineContentIs('var x ='); diff --git a/tests/cases/fourslash/multilineCommentBeforeOpenBrace.ts b/tests/cases/fourslash/multilineCommentBeforeOpenBrace.ts index 792691d9418..63813c12288 100644 --- a/tests/cases/fourslash/multilineCommentBeforeOpenBrace.ts +++ b/tests/cases/fourslash/multilineCommentBeforeOpenBrace.ts @@ -11,8 +11,8 @@ debugger; format.document(); goTo.marker('1'); -verify.currentLineContentIs('function test() /* %^ */'); +verify.currentLineContentIs('function test() /* %^ */ {'); goTo.marker('2'); -verify.currentLineContentIs(' if (true) /* %^ */'); +verify.currentLineContentIs(' if (true) /* %^ */ {'); goTo.marker('3'); verify.currentLineContentIs('}'); \ No newline at end of file diff --git a/tests/cases/fourslash/semicolonFormatting.ts b/tests/cases/fourslash/semicolonFormatting.ts index da828e8e389..1522e7b32fa 100644 --- a/tests/cases/fourslash/semicolonFormatting.ts +++ b/tests/cases/fourslash/semicolonFormatting.ts @@ -4,4 +4,4 @@ goTo.eof(); edit.insert(';'); -verify.currentLineContentIs('function of1 (b:{ r:{ c: number;'); \ No newline at end of file +verify.currentLineContentIs('function of1(b: { r: { c: number;'); \ No newline at end of file diff --git a/tests/cases/fourslash/semicolonFormattingNestedStatements.ts b/tests/cases/fourslash/semicolonFormattingNestedStatements.ts index 87cdebb3c9b..20d49a35f47 100644 --- a/tests/cases/fourslash/semicolonFormattingNestedStatements.ts +++ b/tests/cases/fourslash/semicolonFormattingNestedStatements.ts @@ -12,11 +12,11 @@ goTo.marker("innermost"); edit.insert(";"); // Adding smicolon should format the innermost statement -verify.currentLineContentIs(' var x = 0;'); +verify.currentLineContentIs(' var x = 0;'); // Also should format any parent statement that is terminated by the semicolon goTo.marker("directParent"); -verify.currentLineContentIs(' if (true)'); +verify.currentLineContentIs(' if (true)'); // But not parents that are not terminated by it goTo.marker("parentOutsideBlock"); diff --git a/tests/cases/fourslash/smartIndentStartLineInLists.ts b/tests/cases/fourslash/smartIndentStartLineInLists.ts new file mode 100644 index 00000000000..0150881ff34 --- /dev/null +++ b/tests/cases/fourslash/smartIndentStartLineInLists.ts @@ -0,0 +1,8 @@ +/// +////foo(function () { +////}).then(function () {/*1*/ +////}) + +goTo.marker("1"); +edit.insert("\r\n"); +verify.indentationIs(4); \ No newline at end of file