diff --git a/Jakefile b/Jakefile
index 79907e8a63a..97b1bc826ab 100644
--- a/Jakefile
+++ b/Jakefile
@@ -54,6 +54,7 @@ var servicesSources = [
}).concat([
"services.ts",
"shims.ts",
+ "formatting\\smartIndenter.ts"
].map(function (f) {
return path.join(servicesDirectory, f);
}));
@@ -318,7 +319,7 @@ function exec(cmd, completeHandler) {
complete();
})
try{
- ex.run();
+ ex.run();
} catch(e) {
console.log('Exception: ' + e)
}
@@ -385,7 +386,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/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts
new file mode 100644
index 00000000000..82bbaad81be
--- /dev/null
+++ b/src/services/formatting/smartIndenter.ts
@@ -0,0 +1,383 @@
+///
+
+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;
+ }
+
+ // try to find the node that will include 'position' starting from 'precedingToken'
+ // if such node is found - compute initial indentation for 'position' inside this node
+ var previous: Node;
+ var current = precedingToken;
+ var indentation: number;
+ while (current) {
+ if (!isToken(current) && isPositionBelongToNode(current, position, sourceFile)) {
+ indentation = getInitialIndentationInNode(position, current, previous, sourceFile, options);
+ break;
+ }
+
+ previous = current;
+ current = current.parent;
+ }
+
+ if (!current) {
+ return 0;
+ }
+
+ var currentStartLine: number = sourceFile.getLineAndCharacterFromPosition(current.getStart(sourceFile)).line;
+ var parent: Node = current.parent;
+ var parentStartLine: number;
+
+ // 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) {
+ parentStartLine = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)).line;
+ if (isNodeContentIndented(parent, current) && parentStartLine !== currentStartLine && !isChildOnTheSameLineWithElseInIfStatement(parent, current, sourceFile)) {
+ indentation += options.indentSpaces;
+ }
+
+ current = parent;
+ currentStartLine = parentStartLine;
+ parent = current.parent;
+ }
+
+ return indentation;
+ }
+
+ function getInitialIndentationInNode(position: number, parent: Node, previous: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
+ if (parent.kind === SyntaxKind.IfStatement) {
+ Debug.assert(previous);
+
+ // IfStatement will be parent when:
+ // - previous token is the immediate child of IfStatement - some token
+ // - Then\Else parts are completed and position is outside Then\Else statements
+ if ((parent).thenStatement === previous || (parent).elseStatement === previous) {
+ if (previous.getStart(sourceFile) > position || previous.end < position) {
+ return 0;
+ }
+ else {
+ return indentIfPositionOnDifferentLineWithNodeStart(position, previous, sourceFile, options);
+ }
+ }
+ }
+ else if (parent.kind === SyntaxKind.TryStatement) {
+ Debug.assert(previous);
+ // this is possible only if position is next to completed Try\Catch blocks - no indentation
+ if (previous.kind === SyntaxKind.TryBlock || previous.kind === SyntaxKind.CatchBlock) {
+ return 0;
+ }
+ }
+
+ return indentIfPositionOnDifferentLineWithNodeStart(position, parent, sourceFile, options);
+ }
+
+ function indentIfPositionOnDifferentLineWithNodeStart(position: number, node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
+ return isPositionOnTheSameLineWithNodeStart(position, node, sourceFile) ? 0 : options.indentSpaces;
+ }
+
+ function isPositionOnTheSameLineWithNodeStart(position: number, node: Node, sourceFile: SourceFile): boolean {
+ var lineAtPosition = sourceFile.getLineAndCharacterFromPosition(position).line;
+ var startLine = sourceFile.getLineAndCharacterFromPosition(node.getStart(sourceFile)).line;
+ return lineAtPosition === startLine;
+ }
+
+ function isPositionBelongToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean {
+ return candidate.end > position || !isCompletedNode(candidate, sourceFile);
+ }
+
+ function isPositionOnTheSameLineWithSomeBrace(token: Node, position: number, sourceFile: SourceFile): boolean {
+ switch (token.kind) {
+ case SyntaxKind.OpenBraceToken:
+ case SyntaxKind.OpenBracketToken:
+ case SyntaxKind.OpenParenToken:
+ case SyntaxKind.CloseBraceToken:
+ case SyntaxKind.CloseBracketToken:
+ case SyntaxKind.CloseParenToken:
+ return isPositionOnTheSameLineWithNodeStart(position, token, sourceFile);
+ default:
+ return false;
+ }
+ }
+
+ function isChildOnTheSameLineWithElseInIfStatement(parent: Node, child: Node, sourceFile: SourceFile): boolean {
+ if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) {
+ var elseKeyword = findTokenOfKind(parent, SyntaxKind.ElseKeyword);
+ Debug.assert(elseKeyword);
+
+ return isPositionOnTheSameLineWithNodeStart(child.getStart(sourceFile), elseKeyword, sourceFile);
+ }
+ }
+
+ function findTokenOfKind(parent: Node, kind: SyntaxKind) {
+ return forEach(parent.getChildren(), c => c.kind === kind && c);
+ }
+
+ // preserve indentation for list items
+ // - first list item is either on the same line with the parent: foo(a... . (in this case it is not indented) or on the different line (then it is indented with base level + delta)
+ // - subsequent list items inherit indentation for its sibling on the left when these siblings are also on the new line.
+ // 1. foo(a, b
+ // $ - indentation = base level + delta
+ // 2. foo (a,
+ // b, c, d,
+ // $ - same indentation with first child node on the previous line
+ // NOTE: indentation for list items spans from the beginning of the line to the first non-whitespace character
+ // /*test*/ x,
+ // $ <-- indentation for a new item will be here
+ function getCustomIndentationForListItem(leftSibling: Node, sourceFile: SourceFile): number {
+ if (leftSibling.parent) {
+ switch (leftSibling.parent.kind) {
+ case SyntaxKind.ObjectLiteral:
+ return getCustomIndentationFromList((leftSibling.parent).properties);
+ case SyntaxKind.TypeLiteral:
+ return getCustomIndentationFromList((leftSibling.parent).members);
+ case SyntaxKind.ArrayLiteral:
+ return getCustomIndentationFromList((leftSibling.parent).elements);
+ case SyntaxKind.FunctionDeclaration:
+ case SyntaxKind.FunctionExpression:
+ case SyntaxKind.ArrowFunction:
+ case SyntaxKind.Method:
+ case SyntaxKind.CallSignature:
+ case SyntaxKind.ConstructSignature:
+ if ((leftSibling.parent).typeParameters && leftSibling.end < (leftSibling.parent).typeParameters.end) {
+ return getCustomIndentationFromList((leftSibling.parent).typeParameters);
+ }
+ else {
+ return getCustomIndentationFromList((leftSibling.parent).parameters);
+ }
+ case SyntaxKind.NewExpression:
+ case SyntaxKind.CallExpression:
+ if ((leftSibling.parent).typeArguments && leftSibling.end < (leftSibling.parent).typeArguments.end) {
+ return getCustomIndentationFromList((leftSibling.parent).typeArguments);
+ }
+ else {
+ return getCustomIndentationFromList((leftSibling.parent).arguments);
+ }
+
+ break;
+ }
+ }
+
+ return -1;
+
+ function getCustomIndentationFromList(list: Node[]): number {
+ var index = indexOf(list, leftSibling);
+ if (index !== -1) {
+ var lineAndCol = sourceFile.getLineAndCharacterFromPosition(leftSibling.getStart(sourceFile));
+ for (var i = index - 1; i >= 0; --i) {
+ var prevLineAndCol = sourceFile.getLineAndCharacterFromPosition(list[i].getStart(sourceFile));
+ if (lineAndCol.line !== prevLineAndCol.line) {
+ // find the line start position
+ var lineStart = sourceFile.getPositionFromLineAndCharacter(lineAndCol.line, 1);
+ for (var i = 0; i <= lineAndCol.character; ++i) {
+ if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStart + i))) {
+ return i;
+ }
+ }
+ // code is unreachable because the rance that we check above includes at least one non-whitespace character at the very end
+ Debug.fail("Unreachable code")
+
+ }
+ lineAndCol = prevLineAndCol;
+ }
+ }
+ return -1;
+ }
+ }
+
+ function findPrecedingToken(position: number, sourceFile: SourceFile): Node {
+ return find(sourceFile, /*diveIntoLastChild*/ false);
+
+ function find(n: Node, diveIntoLastChild: boolean): Node {
+ if (isToken(n)) {
+ return n;
+ }
+
+ var children = n.getChildren();
+ if (diveIntoLastChild) {
+ var candidate = findLastChildNodeCandidate(children, children.length);
+ return candidate && find(candidate, diveIntoLastChild);
+ }
+
+ for (var i = 0, len = children.length; i < len; ++i) {
+ var child = children[i];
+ if (isCandidateNode(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 = findLastChildNodeCandidate(children, i);
+ return candidate && find(candidate, /*diveIntoLastChild*/ true)
+ }
+ else {
+ // candidate should be in this node
+ return find(child, diveIntoLastChild);
+ }
+ }
+ }
+ }
+
+ // here we know that none of child token nodes embrace the position
+ // try to find the closest token on the left
+ if (children.length) {
+ var candidate = findLastChildNodeCandidate(children, children.length);
+ return candidate && find(candidate, /*diveIntoLastChild*/ true);
+ }
+ }
+
+ // filters out EOF tokens, Missing\Omitted expressions, empty SyntaxLists and expression statements that wrap any of listed nodes.
+ function isCandidateNode(n: Node): boolean {
+ if (n.kind === SyntaxKind.ExpressionStatement) {
+ return isCandidateNode((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;
+ }
+
+ // finds last node that is considered as candidate for search (isCandidate(node) === true) starting from 'exclusiveStartPosition'
+ function findLastChildNodeCandidate(children: Node[], exclusiveStartPosition: number): Node {
+ for (var i = exclusiveStartPosition - 1; i >= 0; --i) {
+ if (isCandidateNode(children[i])) {
+ return children[i];
+ }
+ }
+ }
+ }
+
+ function isToken(n: Node): boolean {
+ return n.kind < SyntaxKind.Missing;
+ }
+
+ function isNodeContentIndented(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:
+ // 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.BinaryExpression:
+ case SyntaxKind.CallExpression:
+ case SyntaxKind.NewExpression:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ function isNodeEndWith(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;
+ }
+
+ 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 isNodeEndWith(n, SyntaxKind.CloseBraceToken, sourceFile);
+ case SyntaxKind.ParenExpression:
+ case SyntaxKind.CallSignature:
+ case SyntaxKind.CallExpression:
+ return isNodeEndWith(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 isNodeEndWith(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.VariableStatement:
+ // variable statement is considered completed if it either doesn'not have variable declarations or last variable declaration is completed
+ var variableDeclarations = (n).declarations;
+ return variableDeclarations.length === 0 || isCompletedNode(variableDeclarations[variableDeclarations.length - 1], sourceFile);
+ case SyntaxKind.VariableDeclaration:
+ // variable declaration is completed if it either doesn't have initializer or initializer is completed
+ return !(n).initializer || isCompletedNode((n).initializer, sourceFile);
+ case SyntaxKind.WhileStatement:
+ return isCompletedNode((n).statement, sourceFile);
+ case SyntaxKind.DoStatement:
+ 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 740bfb899b2..d7edb4349bb 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -11,6 +11,7 @@
///
///
///
+///
///
///
@@ -2932,15 +2933,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) {