diff --git a/Jakefile b/Jakefile index 79907e8a63..1307601fb8 100644 --- a/Jakefile +++ b/Jakefile @@ -318,7 +318,7 @@ function exec(cmd, completeHandler) { complete(); }) try{ - ex.run(); + ex.run(); } catch(e) { console.log('Exception: ' + e) } @@ -385,7 +385,7 @@ desc("Generates code coverage data via instanbul") task("generate-code-coverage", ["tests", builtLocalDirectory], function () { var cmd = 'istanbul cover node_modules/mocha/bin/_mocha -- -R min -t ' + testTimeout + ' ' + run; console.log(cmd); - exec(cmd); + exec(cmd); }, { async: true }); // Browser tests diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index db8fcace15..40d9722af0 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -50,8 +50,8 @@ module ts { return node.pos; } - export function getTokenPosOfNode(node: Node): number { - return skipTrivia(getSourceFileOfNode(node).text, node.pos); + export function getTokenPosOfNode(node: Node, sourceFile?: SourceFile): number { + return skipTrivia((sourceFile || getSourceFileOfNode(node)).text, node.pos); } export function getSourceTextOfNodeFromSourceText(sourceText: string, node: Node): string { @@ -487,6 +487,32 @@ module ts { return false; } + export function isStatement(n: Node): boolean { + switch(n.kind) { + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + case SyntaxKind.DebuggerStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.EmptyStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.LabeledStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ThrowKeyword: + case SyntaxKind.TryStatement: + case SyntaxKind.VariableStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.ExportAssignment: + return true; + default: + return false; + } + } + // True if the given identifier, string literal, or number literal is the name of a declaration node export function isDeclarationOrFunctionExpressionOrCatchVariableName(name: Node): boolean { if (name.kind !== SyntaxKind.Identifier && name.kind !== SyntaxKind.StringLiteral && name.kind !== SyntaxKind.NumericLiteral) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 529903f303..6955689797 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -223,6 +223,8 @@ module ts { LastTypeNode = TupleType, FirstPunctuation = OpenBraceToken, LastPunctuation = CaretEqualsToken, + FirstToken = EndOfFileToken, + LastToken = StringKeyword } export enum NodeFlags { @@ -988,6 +990,15 @@ module ts { AMD, } + export interface LineAndCharacter { + line: number; + /* + * This value denotes the character position in line and is different from the 'column' because of tab characters. + */ + character: number; + } + + export enum ScriptTarget { ES3, ES5, diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index c334e1864a..7570ef936c 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -36,5 +36,4 @@ /// /// /// -/// /// \ No newline at end of file diff --git a/src/services/formatting/singleTokenIndenter.ts b/src/services/formatting/singleTokenIndenter.ts deleted file mode 100644 index 896c2e8ac0..0000000000 --- a/src/services/formatting/singleTokenIndenter.ts +++ /dev/null @@ -1,46 +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 SingleTokenIndenter extends IndentationTrackingWalker { - private indentationAmount: number = null; - private indentationPosition: number; - - constructor(indentationPosition: number, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, indentFirstToken: boolean, options: FormattingOptions) { - super(new TextSpan(indentationPosition, 1), sourceUnit, snapshot, indentFirstToken, options); - - this.indentationPosition = indentationPosition; - } - - public static getIndentationAmount(position: number, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, options: FormattingOptions): number { - var walker = new SingleTokenIndenter(position, sourceUnit, snapshot, true, options); - visitNodeOrToken(walker, sourceUnit); - return walker.indentationAmount; - } - - public indentToken(token: ISyntaxToken, indentationAmount: number, commentIndentationAmount: number): void { - // Compute an indentation string for this token - if (token.fullWidth() === 0 || (this.indentationPosition - this.position() < token.leadingTriviaWidth())) { - // The position is in the leading trivia, use comment indentation - this.indentationAmount = commentIndentationAmount; - } - else { - this.indentationAmount = indentationAmount; - } - } - } -} \ No newline at end of file diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts new file mode 100644 index 0000000000..ad07f884cc --- /dev/null +++ b/src/services/formatting/smartIndenter.ts @@ -0,0 +1,522 @@ +/// + +module ts.formatting { + export module SmartIndenter { + + export function getIndentation(position: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + if (position > sourceFile.text.length) { + return 0; // past EOF + } + + var precedingToken = findPrecedingToken(position, sourceFile); + if (!precedingToken) { + return 0; + } + + // no indentation in string \regex literals + if ((precedingToken.kind === SyntaxKind.StringLiteral || precedingToken.kind === SyntaxKind.RegularExpressionLiteral) && + precedingToken.getStart(sourceFile) <= position && + precedingToken.end > position) { + return 0; + } + + var lineAtPosition = sourceFile.getLineAndCharacterFromPosition(position).line; + + if (precedingToken.kind === SyntaxKind.CommaToken && precedingToken.parent.kind !== SyntaxKind.BinaryExpression) { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + var actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation; + } + } + + // try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken' + // if such node is found - compute initial indentation for 'position' inside this node + var previous: Node; + var current = precedingToken; + var currentStart: LineAndCharacter; + var indentationDelta: number; + + while (current) { + if (positionBelongsToNode(current, position, sourceFile) && nodeContentIsIndented(current, previous)) { + currentStart = getStartLineAndCharacterForNode(current, sourceFile); + + if (nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)) { + indentationDelta = 0; + } + else { + indentationDelta = lineAtPosition !== currentStart.line ? options.indentSpaces : 0; + } + + break; + } + + // 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; + } + + previous = current; + current = current.parent; + } + + if (!current) { + // no parent was found - return 0 to be indented on the level of SourceFile + return 0; + } + + + var parent: Node = current.parent; + var parentStart: LineAndCharacter; + + // 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; + } + + parentStart = sourceFile.getLineAndCharacterFromPosition(parent.getStart(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; + } + + // 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; + } + + current = parent; + currentStart = parentStart; + parent = current.parent; + } + + return indentationDelta; + } + + /* + * Function returns -1 if indentation cannot be determined + */ + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + var itemInfo = findPrecedingListItem(commaToken); + return deriveActualIndentationFromList(itemInfo.list.getChildren(), itemInfo.listItemIndex, sourceFile, options); + } + + /* + * 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 { + + // 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 = + (isDeclaration(current) || isStatement(current)) && + (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine); + + if (!useActualIndentation) { + return -1; + } + + return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); + } + + function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): boolean { + var nextToken = findNextToken(precedingToken, current); + if (!nextToken) { + return false; + } + + if (nextToken.kind === SyntaxKind.OpenBraceToken) { + // open braces are always indented at the parent level + return true; + } + else if (nextToken.kind === SyntaxKind.CloseBraceToken) { + // close braces are indented at the parent level if they are located on the same line with cursor + // this means that if new line will be added at $ position, this case will be indented + // class A { + // $ + // } + /// and this one - not + // class A { + // $} + + var nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; + return lineAtPosition === nextTokenStartLine; + } + + return false; + } + + function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFile): LineAndCharacter { + return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)); + } + + function findPrecedingListItem(commaToken: Node): { listItemIndex: number; list: Node } { + // CommaToken node is synthetic and thus will be stored in SyntaxList, however parent of the CommaToken points to the container of the SyntaxList skipping the list. + // In order to find the preceding list item we first need to locate SyntaxList itself and then search for the position of CommaToken + var syntaxList = forEach(commaToken.parent.getChildren(), c => { + // find syntax list that covers the span of CommaToken + if (c.kind == SyntaxKind.SyntaxList && c.pos <= commaToken.end && c.end >= commaToken.end) { + return c; + } + }); + Debug.assert(syntaxList); + + var children = syntaxList.getChildren(); + var commaIndex = indexOf(children, commaToken); + Debug.assert(commaIndex !== -1 && commaIndex !== 0); + + return { + listItemIndex: commaIndex - 1, + list: syntaxList + }; + } + + function positionBelongsToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean { + return candidate.end > position || !isCompletedNode(candidate, sourceFile); + } + + function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFile): boolean { + if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { + var elseKeyword = forEach(parent.getChildren(), c => c.kind === SyntaxKind.ElseKeyword && c); + Debug.assert(elseKeyword); + + var elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; + return elseKeywordStartLine === childStartLine; + } + } + + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + if (node.parent) { + switch (node.parent.kind) { + case SyntaxKind.TypeReference: + if ((node.parent).typeArguments) { + return getActualIndentationFromList((node.parent).typeArguments); + } + break; + case SyntaxKind.ObjectLiteral: + return getActualIndentationFromList((node.parent).properties); + case SyntaxKind.TypeLiteral: + return getActualIndentationFromList((node.parent).members); + case SyntaxKind.ArrayLiteral: + return getActualIndentationFromList((node.parent).elements); + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.Method: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + if ((node.parent).typeParameters && node.end < (node.parent).typeParameters.end) { + return getActualIndentationFromList((node.parent).typeParameters); + } + + return getActualIndentationFromList((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 getActualIndentationFromList((node.parent).arguments); + } + } + + return -1; + + function getActualIndentationFromList(list: Node[]): number { + var index = indexOf(list, node); + return index !== -1 ? deriveActualIndentationFromList(list, index, sourceFile, options) : -1; + } + } + + + function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): 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); + for (var i = index - 1; i >= 0; --i) { + if (list[i].kind === SyntaxKind.CommaToken) { + continue; + } + // skip list items that ends on the same line with the current list element + var prevEndLine = sourceFile.getLineAndCharacterFromPosition(list[i].end).line; + if (prevEndLine !== lineAndCharacter.line) { + return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options); + } + + lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); + } + return -1; + } + + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + var lineStart = sourceFile.getPositionFromLineAndCharacter(lineAndCharacter.line, 1); + var column = 0; + for (var i = 0; i < lineAndCharacter.character; ++i) { + var charCode = sourceFile.text.charCodeAt(lineStart + i); + if (!isWhiteSpace(charCode)) { + return column; + } + + if (charCode === CharacterCodes.tab) { + column += options.spacesPerTab; + } + else { + column++; + } + } + + return column; + } + + function findNextToken(previousToken: Node, parent: Node): Node { + return find(parent); + + function find(n: Node): Node { + if (isToken(n) && n.pos === previousToken.end) { + // this is token that starts at the end of previous token - return it + return n; + } + + var children = n.getChildren(); + for (var i = 0, len = children.length; i < len; ++i) { + var child = children[i]; + var shouldDiveInChildNode = + // previous token is enclosed somewhere in the child + (child.pos <= previousToken.pos && child.end > previousToken.end) || + // previous token ends exactly at the beginning of child + (child.pos === previousToken.end); + + if (shouldDiveInChildNode && nodeHasTokens(child)) { + return find(child); + } + } + + return undefined; + } + } + + function findPrecedingToken(position: number, sourceFile: SourceFile): Node { + return find(sourceFile); + + function findRightmostToken(n: Node): Node { + if (isToken(n)) { + return n; + } + + var children = n.getChildren(); + var candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length); + return candidate && findRightmostToken(candidate); + + } + + function find(n: Node): Node { + if (isToken(n)) { + return n; + } + + var children = n.getChildren(); + for (var i = 0, len = children.length; i < len; ++i) { + var child = children[i]; + if (nodeHasTokens(child)) { + if (position < child.end) { + if (child.getStart(sourceFile) >= position) { + // actual start of the node is past the position - previous token should be at the end of previous child + var candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ i); + return candidate && findRightmostToken(candidate) + } + else { + // candidate should be in this node + return find(child); + } + } + } + } + + Debug.assert(n.kind === SyntaxKind.SourceFile); + + // Here we know that none of child token nodes embrace the position, + // the only known case is when position is at the end of the file. + // Try to find the rightmost token in the file without filtering. + // Namely we are skipping the check: 'position < node.end' + if (children.length) { + var candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length); + return candidate && findRightmostToken(candidate); + } + } + + /// finds last node that is considered as candidate for search (isCandidate(node) === true) starting from 'exclusiveStartPosition' + function findRightmostChildNodeWithTokens(children: Node[], exclusiveStartPosition: number): Node { + for (var i = exclusiveStartPosition - 1; i >= 0; --i) { + if (nodeHasTokens(children[i])) { + return children[i]; + } + } + } + } + + /* + * Checks if node is something that can contain tokens (except EOF) - filters out EOF tokens, Missing\Omitted expressions, empty SyntaxLists and expression statements that wrap any of listed nodes. + */ + function nodeHasTokens(n: Node): boolean { + if (n.kind === SyntaxKind.ExpressionStatement) { + return nodeHasTokens((n).expression); + } + + if (n.kind === SyntaxKind.EndOfFileToken || n.kind === SyntaxKind.OmittedExpression || n.kind === SyntaxKind.Missing) { + return false; + } + + // SyntaxList is already realized so getChildCount should be fast and non-expensive + return n.kind !== SyntaxKind.SyntaxList || n.getChildCount() !== 0; + } + + function isToken(n: Node): boolean { + return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken; + } + + function nodeContentIsIndented(parent: Node, child: Node): boolean { + switch (parent.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: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.ModuleBlock: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.TypeLiteral: + case SyntaxKind.SwitchStatement: + case SyntaxKind.DefaultClause: + case SyntaxKind.CaseClause: + case SyntaxKind.ParenExpression: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.VariableStatement: + case SyntaxKind.VariableDeclaration: + return true; + default: + return false; + } + } + + /* + * Checks if node ends with 'expectedLastToken'. + * If child at position 'length - 1' is 'SemicolonToken' it is skipped and 'expectedLastToken' is compared with child at position 'length - 2'. + */ + function nodeEndsWith(n: Node, expectedLastToken: SyntaxKind, sourceFile: SourceFile): boolean { + var children = n.getChildren(sourceFile); + if (children.length) { + var last = children[children.length - 1]; + if (last.kind === expectedLastToken) { + return true; + } + else if (last.kind === SyntaxKind.SemicolonToken && children.length !== 1) { + return children[children.length - 2].kind === expectedLastToken; + } + } + return false; + } + + /* + * This function is always called when position of the cursor is located after the node + */ + function isCompletedNode(n: Node, sourceFile: SourceFile): boolean { + switch (n.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ObjectLiteral: + case SyntaxKind.Block: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.FunctionBlock: + case SyntaxKind.ModuleBlock: + case SyntaxKind.SwitchStatement: + return nodeEndsWith(n, SyntaxKind.CloseBraceToken, sourceFile); + case SyntaxKind.ParenExpression: + case SyntaxKind.CallSignature: + case SyntaxKind.CallExpression: + case SyntaxKind.ConstructSignature: + return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile); + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Method: + case SyntaxKind.ArrowFunction: + return !(n).body || isCompletedNode((n).body, sourceFile); + case SyntaxKind.ModuleDeclaration: + return (n).body && isCompletedNode((n).body, sourceFile); + case SyntaxKind.IfStatement: + if ((n).elseStatement) { + return isCompletedNode((n).elseStatement, sourceFile); + } + return isCompletedNode((n).thenStatement, sourceFile); + case SyntaxKind.ExpressionStatement: + return isCompletedNode((n).expression, sourceFile); + case SyntaxKind.ArrayLiteral: + return nodeEndsWith(n, SyntaxKind.CloseBracketToken, sourceFile); + case SyntaxKind.Missing: + return false; + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + // there is no such thing as terminator token for CaseClause\DefaultClause so for simplicitly always consider them non-completed + return false; + case SyntaxKind.WhileStatement: + return isCompletedNode((n).statement, sourceFile); + case SyntaxKind.DoStatement: + // rough approximation: if DoStatement has While keyword - then if node is completed is checking the presence of ')'; + var hasWhileKeyword = forEach(n.getChildren(), c => c.kind === SyntaxKind.WhileKeyword && c); + if(hasWhileKeyword) { + return nodeEndsWith(n, SyntaxKind.CloseParenToken, sourceFile); + } + return isCompletedNode((n).statement, sourceFile); + default: + return true; + } + } + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index 94f3cefdd5..7447e56df8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -11,6 +11,7 @@ /// /// /// +/// /// /// @@ -27,18 +28,18 @@ module ts { export interface Node { getSourceFile(): SourceFile; - getChildCount(): number; - getChildAt(index: number): Node; - getChildren(): Node[]; - getStart(): number; + getChildCount(sourceFile?: SourceFile): number; + getChildAt(index: number, sourceFile?: SourceFile): Node; + getChildren(sourceFile?: SourceFile): Node[]; + getStart(sourceFile?: SourceFile): number; getFullStart(): number; getEnd(): number; - getWidth(): number; + getWidth(sourceFile?: SourceFile): number; getFullWidth(): number; - getLeadingTriviaWidth(): number; - getFullText(): string; - getFirstToken(): Node; - getLastToken(): Node; + getLeadingTriviaWidth(sourceFile?: SourceFile): number; + getFullText(sourceFile?: SourceFile): string; + getFirstToken(sourceFile?: SourceFile): Node; + getLastToken(sourceFile?: SourceFile): Node; } export interface Symbol { @@ -100,8 +101,8 @@ module ts { return node; } - public getStart(): number { - return getTokenPosOfNode(this); + public getStart(sourceFile?: SourceFile): number { + return getTokenPosOfNode(this, sourceFile); } public getFullStart(): number { @@ -112,20 +113,20 @@ module ts { return this.end; } - public getWidth(): number { - return this.getEnd() - this.getStart(); + public getWidth(sourceFile?: SourceFile): number { + return this.getEnd() - this.getStart(sourceFile); } public getFullWidth(): number { return this.end - this.getFullStart(); } - public getLeadingTriviaWidth(): number { - return this.getStart() - this.pos; + public getLeadingTriviaWidth(sourceFile?: SourceFile): number { + return this.getStart(sourceFile) - this.pos; } - public getFullText(): string { - return this.getSourceFile().text.substring(this.pos, this.end); + public getFullText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); } private addSyntheticNodes(nodes: Node[], pos: number, end: number): number { @@ -157,9 +158,9 @@ module ts { return list; } - private createChildren() { + private createChildren(sourceFile?: SourceFile) { if (this.kind > SyntaxKind.Missing) { - scanner.setText(this.getSourceFile().text); + scanner.setText((sourceFile || this.getSourceFile()).text); var children: Node[] = []; var pos = this.pos; var processNode = (node: Node) => { @@ -185,36 +186,36 @@ module ts { this._children = children || emptyArray; } - public getChildCount(): number { - if (!this._children) this.createChildren(); + public getChildCount(sourceFile?: SourceFile): number { + if (!this._children) this.createChildren(sourceFile); return this._children.length; } - public getChildAt(index: number): Node { - if (!this._children) this.createChildren(); + public getChildAt(index: number, sourceFile?: SourceFile): Node { + if (!this._children) this.createChildren(sourceFile); return this._children[index]; } - public getChildren(): Node[] { - if (!this._children) this.createChildren(); + public getChildren(sourceFile?: SourceFile): Node[] { + if (!this._children) this.createChildren(sourceFile); return this._children; } - public getFirstToken(): Node { - var children = this.getChildren(); + public getFirstToken(sourceFile?: SourceFile): Node { + var children = this.getChildren(sourceFile); for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.kind < SyntaxKind.Missing) return child; - if (child.kind > SyntaxKind.Missing) return child.getFirstToken(); + if (child.kind > SyntaxKind.Missing) return child.getFirstToken(sourceFile); } } - public getLastToken(): Node { - var children = this.getChildren(); + public getLastToken(sourceFile?: SourceFile): Node { + var children = this.getChildren(sourceFile); for (var i = children.length - 1; i >= 0; i--) { var child = children[i]; if (child.kind < SyntaxKind.Missing) return child; - if (child.kind > SyntaxKind.Missing) return child.getLastToken(); + if (child.kind > SyntaxKind.Missing) return child.getLastToken(sourceFile); } } } @@ -3450,15 +3451,11 @@ module ts { function getIndentationAtPosition(filename: string, position: number, editorOptions: EditorOptions) { filename = TypeScript.switchToForwardSlashes(filename); - - var syntaxTree = getSyntaxTree(filename); - - var scriptSnapshot = syntaxTreeCache.getCurrentScriptSnapshot(filename); - var scriptText = TypeScript.SimpleText.fromScriptSnapshot(scriptSnapshot); - var textSnapshot = new TypeScript.Services.Formatting.TextSnapshot(scriptText); + + var sourceFile = getCurrentSourceFile(filename); var options = new TypeScript.FormattingOptions(!editorOptions.ConvertTabsToSpaces, editorOptions.TabSize, editorOptions.IndentSize, editorOptions.NewLineCharacter) - return TypeScript.Services.Formatting.SingleTokenIndenter.getIndentationAmount(position, syntaxTree.sourceUnit(), textSnapshot, options); + return formatting.SmartIndenter.getIndentation(position, sourceFile, options); } function getFormattingManager(filename: string, options: FormatCodeOptions) { diff --git a/tests/cases/fourslash/chainedFunctionFunctionArgIndent.ts b/tests/cases/fourslash/chainedFunctionFunctionArgIndent.ts index 8b9a54de52..ac5b74e83c 100644 --- a/tests/cases/fourslash/chainedFunctionFunctionArgIndent.ts +++ b/tests/cases/fourslash/chainedFunctionFunctionArgIndent.ts @@ -2,10 +2,10 @@ //// declare var $: any; //// $(".contentDiv").each(function (index, element) {/**/ //// // <-- ensure cursor is here after return on above -//// }); // Ensure indent is 0 after LBrace +//// }); goTo.marker(); edit.insert("\n"); verify.indentationIs(4); edit.insert("}"); -verify.indentationIs(0); +verify.indentationIs(4); // keep arguments indented diff --git a/tests/cases/fourslash/indentation.ts b/tests/cases/fourslash/indentation.ts index 3ff6dac45a..32319ae282 100644 --- a/tests/cases/fourslash/indentation.ts +++ b/tests/cases/fourslash/indentation.ts @@ -154,7 +154,7 @@ //// ////function argumentsListIndentation(bar, //// blah, -////{| "indent": 0 |} +//// {| "indent": 13 |} ////); //// //// diff --git a/tests/cases/fourslash/noSmartIndentInsideMultilineString.ts b/tests/cases/fourslash/noSmartIndentInsideMultilineString.ts index 9018a54b33..692dfe7075 100644 --- a/tests/cases/fourslash/noSmartIndentInsideMultilineString.ts +++ b/tests/cases/fourslash/noSmartIndentInsideMultilineString.ts @@ -8,5 +8,4 @@ goTo.marker("1"); edit.insert("\r\n"); -// Won't-fixed: Disable SmartIndent inside multiline string in TS. Should be 0 -verify.indentationIs(8); \ No newline at end of file +verify.indentationIs(0); \ No newline at end of file diff --git a/tests/cases/fourslash/smartIndentActualIndentation.ts b/tests/cases/fourslash/smartIndentActualIndentation.ts new file mode 100644 index 0000000000..b2874d3c1d --- /dev/null +++ b/tests/cases/fourslash/smartIndentActualIndentation.ts @@ -0,0 +1,17 @@ +/// + +//// class A { +//// /*1*/ +//// } + +////module M { +//// class C { +//// /*2*/ +//// } +////} + +goTo.marker("1"); +verify.indentationIs(12); + +goTo.marker("2"); +verify.indentationIs(16); diff --git a/tests/cases/fourslash/smartIndentAfterAlignedFunctionArgument.ts b/tests/cases/fourslash/smartIndentAfterAlignedFunctionArgument.ts index 8efd5a9c9b..d5aa2bc431 100644 --- a/tests/cases/fourslash/smartIndentAfterAlignedFunctionArgument.ts +++ b/tests/cases/fourslash/smartIndentAfterAlignedFunctionArgument.ts @@ -1,9 +1,10 @@ /// ////function foo(bar, -//// blah, +//// blah, baz, //// /**/ ////) { }; goTo.marker(); -verify.indentationIs(0); +// keep indentation of 'blah' +verify.indentationIs(13); diff --git a/tests/cases/fourslash/smartIndentCloseBrace.ts b/tests/cases/fourslash/smartIndentCloseBrace.ts new file mode 100644 index 0000000000..da2094d76c --- /dev/null +++ b/tests/cases/fourslash/smartIndentCloseBrace.ts @@ -0,0 +1,11 @@ +/// + +////class A { +//// {| "indentation": 0|} } +////class B { +//// var x = 1; +//// {| "indentation": 0|} } + +test.markers().forEach((marker) => { + verify.indentationAtPositionIs(marker.fileName, marker.position, marker.data.indentation); +}); diff --git a/tests/cases/fourslash/smartIndentDoStatement.ts b/tests/cases/fourslash/smartIndentDoStatement.ts new file mode 100644 index 0000000000..2dff10f927 --- /dev/null +++ b/tests/cases/fourslash/smartIndentDoStatement.ts @@ -0,0 +1,17 @@ +/// +//// do /*1*/ { +//// } while (true) +//// +//// do { /*2*/ +//// } /*3*/while (true)/*4*/ + +function verifyIndentationAfterNewLine(marker: string, indentation: number): void { + goTo.marker(marker); + edit.insert("\r\n"); + verify.indentationIs(indentation); +} + +verifyIndentationAfterNewLine("1", 0); +verifyIndentationAfterNewLine("2", 4); +verifyIndentationAfterNewLine("3", 0); +verifyIndentationAfterNewLine("4", 0); diff --git a/tests/cases/fourslash/smartIndentIfStatement.ts b/tests/cases/fourslash/smartIndentIfStatement.ts new file mode 100644 index 0000000000..fb377ec9c9 --- /dev/null +++ b/tests/cases/fourslash/smartIndentIfStatement.ts @@ -0,0 +1,20 @@ +/// + +//// if /*1*/(true) { } +//// +//// if (true) /*2*/ { /*3*/ +//// } /*4*/ +//// +//// if (1 === /*5*/ 2) { } + +function verifyIndentationAfterNewLine(marker: string, indentation: number): void { + goTo.marker(marker); + edit.insert("\r\n"); + verify.indentationIs(indentation); +} + +verifyIndentationAfterNewLine("1", 4); +verifyIndentationAfterNewLine("2", 0); +verifyIndentationAfterNewLine("3", 4); +verifyIndentationAfterNewLine("4", 0); +verifyIndentationAfterNewLine("5", 4); \ No newline at end of file diff --git a/tests/cases/fourslash/smartIndentInsideMultilineString.ts b/tests/cases/fourslash/smartIndentInsideMultilineString.ts index 8ad4430455..24a20844a9 100644 --- a/tests/cases/fourslash/smartIndentInsideMultilineString.ts +++ b/tests/cases/fourslash/smartIndentInsideMultilineString.ts @@ -21,15 +21,11 @@ //// } ////} -// Behavior below is not ideal, ideal is in comments goTo.marker("1"); -//verify.indentationIs(0); -verify.indentationIs(8); +verify.indentationIs(0); goTo.marker("2"); -//verify.indentationIs(0); -verify.indentationIs(4); +verify.indentationIs(0); goTo.marker("3"); -//verify.indentationIs(0); -verify.indentationIs(12); \ No newline at end of file +verify.indentationIs(0); \ No newline at end of file diff --git a/tests/cases/fourslash/smartIndentListItem.ts b/tests/cases/fourslash/smartIndentListItem.ts new file mode 100644 index 0000000000..f71db9c865 --- /dev/null +++ b/tests/cases/fourslash/smartIndentListItem.ts @@ -0,0 +1,14 @@ +/// +////[1, +//// 2 +//// + 3, 4, +//// /*1*/ +////[1, +//// 2 +//// + 3, 4 +//// /*2*/ + +goTo.marker("1"); +verify.indentationIs(4) +goTo.marker("2"); +verify.indentationIs(4) \ No newline at end of file diff --git a/tests/cases/fourslash/smartIndentOnFunctionParameters.ts b/tests/cases/fourslash/smartIndentOnFunctionParameters.ts index c357ab4805..012c4d93b4 100644 --- a/tests/cases/fourslash/smartIndentOnFunctionParameters.ts +++ b/tests/cases/fourslash/smartIndentOnFunctionParameters.ts @@ -22,7 +22,7 @@ goTo.marker("4"); verify.currentLineContentIs(" c"); goTo.marker("1"); edit.insert("\r\n"); -verify.indentationIs(0); +verify.indentationIs(4); goTo.marker("5"); verify.currentLineContentIs(" //comment"); goTo.marker("6"); diff --git a/tests/cases/fourslash/smartIndentStatementSwitch.ts b/tests/cases/fourslash/smartIndentStatementSwitch.ts index 902abe4e3e..e8d2a02948 100644 --- a/tests/cases/fourslash/smartIndentStatementSwitch.ts +++ b/tests/cases/fourslash/smartIndentStatementSwitch.ts @@ -11,7 +11,7 @@ //// case 1: //// {| "indentation": 12 |} //// break; -//// {| "indentation": 8 |} +//// {| "indentation": 12 |} // content of case clauses is always indented relatively to case clause //// } //// {| "indentation": 4 |} ////} diff --git a/tests/cases/fourslash/smartIndentTypeArgumentList.ts b/tests/cases/fourslash/smartIndentTypeArgumentList.ts new file mode 100644 index 0000000000..24136596ed --- /dev/null +++ b/tests/cases/fourslash/smartIndentTypeArgumentList.ts @@ -0,0 +1,8 @@ +/// + +////interface T1 extends T + +goTo.marker(); +verify.indentationIs(32); \ No newline at end of file diff --git a/tests/cases/fourslash/switchIndenting.ts b/tests/cases/fourslash/switchIndenting.ts index 3fee7ba2e4..8ea6f26408 100644 --- a/tests/cases/fourslash/switchIndenting.ts +++ b/tests/cases/fourslash/switchIndenting.ts @@ -8,6 +8,4 @@ goTo.marker(); edit.insert('case 1:\n'); -// ideally would be 8 -//verify.indentationIs(8); -verify.indentationIs(4); +verify.indentationIs(8); \ No newline at end of file