mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-12 01:48:33 -05:00
initial revision of SmartIndenter (TODO: indentation in list items)
This commit is contained in:
5
Jakefile
5
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
|
||||
|
||||
383
src/services/formatting/smartIndenter.ts
Normal file
383
src/services/formatting/smartIndenter.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
///<reference path='..\services.ts' />
|
||||
|
||||
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 ((<IfStatement>parent).thenStatement === previous || (<IfStatement>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 && (<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((<ObjectLiteral>leftSibling.parent).properties);
|
||||
case SyntaxKind.TypeLiteral:
|
||||
return getCustomIndentationFromList((<TypeLiteralNode>leftSibling.parent).members);
|
||||
case SyntaxKind.ArrayLiteral:
|
||||
return getCustomIndentationFromList((<ArrayLiteral>leftSibling.parent).elements);
|
||||
case SyntaxKind.FunctionDeclaration:
|
||||
case SyntaxKind.FunctionExpression:
|
||||
case SyntaxKind.ArrowFunction:
|
||||
case SyntaxKind.Method:
|
||||
case SyntaxKind.CallSignature:
|
||||
case SyntaxKind.ConstructSignature:
|
||||
if ((<SignatureDeclaration>leftSibling.parent).typeParameters && leftSibling.end < (<SignatureDeclaration>leftSibling.parent).typeParameters.end) {
|
||||
return getCustomIndentationFromList((<SignatureDeclaration>leftSibling.parent).typeParameters);
|
||||
}
|
||||
else {
|
||||
return getCustomIndentationFromList((<SignatureDeclaration>leftSibling.parent).parameters);
|
||||
}
|
||||
case SyntaxKind.NewExpression:
|
||||
case SyntaxKind.CallExpression:
|
||||
if ((<CallExpression>leftSibling.parent).typeArguments && leftSibling.end < (<CallExpression>leftSibling.parent).typeArguments.end) {
|
||||
return getCustomIndentationFromList((<CallExpression>leftSibling.parent).typeArguments);
|
||||
}
|
||||
else {
|
||||
return getCustomIndentationFromList((<CallExpression>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((<ExpressionStatement>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 !(<FunctionDeclaration>n).body || isCompletedNode((<FunctionDeclaration>n).body, sourceFile);
|
||||
case SyntaxKind.ModuleDeclaration:
|
||||
return (<ModuleDeclaration>n).body && isCompletedNode((<ModuleDeclaration>n).body, sourceFile);
|
||||
case SyntaxKind.IfStatement:
|
||||
if ((<IfStatement>n).elseStatement) {
|
||||
return isCompletedNode((<IfStatement>n).elseStatement, sourceFile);
|
||||
}
|
||||
return isCompletedNode((<IfStatement>n).thenStatement, sourceFile);
|
||||
case SyntaxKind.ExpressionStatement:
|
||||
return isCompletedNode((<ExpressionStatement>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 = (<VariableStatement>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 !(<VariableDeclaration>n).initializer || isCompletedNode((<VariableDeclaration>n).initializer, sourceFile);
|
||||
case SyntaxKind.WhileStatement:
|
||||
return isCompletedNode((<WhileStatement>n).statement, sourceFile);
|
||||
case SyntaxKind.DoStatement:
|
||||
return isCompletedNode((<DoStatement>n).statement, sourceFile);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
/// <reference path='breakpoints.ts' />
|
||||
/// <reference path='indentation.ts' />
|
||||
/// <reference path='formatting\formatting.ts' />
|
||||
/// <reference path='formatting\smartIndenter.ts' />
|
||||
|
||||
/// <reference path='core\references.ts' />
|
||||
/// <reference path='resources\references.ts' />
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user