Merge pull request #671 from Microsoft/new_smart_indentation
LS: smart indentation
This commit is contained in:
commit
fdc9b9d285
4
Jakefile
4
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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -36,5 +36,4 @@
|
|||
///<reference path='indentationNodeContextPool.ts' />
|
||||
///<reference path='indentationTrackingWalker.ts' />
|
||||
///<reference path='multipleTokenIndenter.ts' />
|
||||
///<reference path='singleTokenIndenter.ts' />
|
||||
///<reference path='formatter.ts' />
|
|
@ -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.
|
||||
//
|
||||
|
||||
///<reference path='formatting.ts' />
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
522
src/services/formatting/smartIndenter.ts
Normal file
522
src/services/formatting/smartIndenter.ts
Normal file
|
@ -0,0 +1,522 @@
|
|||
///<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;
|
||||
}
|
||||
|
||||
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 && (<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 ((<TypeReferenceNode>node.parent).typeArguments) {
|
||||
return getActualIndentationFromList((<TypeReferenceNode>node.parent).typeArguments);
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.ObjectLiteral:
|
||||
return getActualIndentationFromList((<ObjectLiteral>node.parent).properties);
|
||||
case SyntaxKind.TypeLiteral:
|
||||
return getActualIndentationFromList((<TypeLiteralNode>node.parent).members);
|
||||
case SyntaxKind.ArrayLiteral:
|
||||
return getActualIndentationFromList((<ArrayLiteral>node.parent).elements);
|
||||
case SyntaxKind.FunctionDeclaration:
|
||||
case SyntaxKind.FunctionExpression:
|
||||
case SyntaxKind.ArrowFunction:
|
||||
case SyntaxKind.Method:
|
||||
case SyntaxKind.CallSignature:
|
||||
case SyntaxKind.ConstructSignature:
|
||||
if ((<SignatureDeclaration>node.parent).typeParameters && node.end < (<SignatureDeclaration>node.parent).typeParameters.end) {
|
||||
return getActualIndentationFromList((<SignatureDeclaration>node.parent).typeParameters);
|
||||
}
|
||||
|
||||
return getActualIndentationFromList((<SignatureDeclaration>node.parent).parameters);
|
||||
case SyntaxKind.NewExpression:
|
||||
case SyntaxKind.CallExpression:
|
||||
if ((<CallExpression>node.parent).typeArguments && node.end < (<CallExpression>node.parent).typeArguments.end) {
|
||||
return getActualIndentationFromList((<CallExpression>node.parent).typeArguments);
|
||||
}
|
||||
|
||||
return getActualIndentationFromList((<CallExpression>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((<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;
|
||||
}
|
||||
|
||||
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 !(<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 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((<WhileStatement>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((<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' />
|
||||
|
@ -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 <SourceFile>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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
////
|
||||
////function argumentsListIndentation(bar,
|
||||
//// blah,
|
||||
////{| "indent": 0 |}
|
||||
//// {| "indent": 13 |}
|
||||
////);
|
||||
////
|
||||
////
|
||||
|
|
|
@ -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);
|
||||
verify.indentationIs(0);
|
17
tests/cases/fourslash/smartIndentActualIndentation.ts
Normal file
17
tests/cases/fourslash/smartIndentActualIndentation.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
//// class A {
|
||||
//// /*1*/
|
||||
//// }
|
||||
|
||||
////module M {
|
||||
//// class C {
|
||||
//// /*2*/
|
||||
//// }
|
||||
////}
|
||||
|
||||
goTo.marker("1");
|
||||
verify.indentationIs(12);
|
||||
|
||||
goTo.marker("2");
|
||||
verify.indentationIs(16);
|
|
@ -1,9 +1,10 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
////function foo(bar,
|
||||
//// blah,
|
||||
//// blah, baz,
|
||||
//// /**/
|
||||
////) { };
|
||||
|
||||
goTo.marker();
|
||||
verify.indentationIs(0);
|
||||
// keep indentation of 'blah'
|
||||
verify.indentationIs(13);
|
||||
|
|
11
tests/cases/fourslash/smartIndentCloseBrace.ts
Normal file
11
tests/cases/fourslash/smartIndentCloseBrace.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
////class A {
|
||||
//// {| "indentation": 0|} }
|
||||
////class B {
|
||||
//// var x = 1;
|
||||
//// {| "indentation": 0|} }
|
||||
|
||||
test.markers().forEach((marker) => {
|
||||
verify.indentationAtPositionIs(marker.fileName, marker.position, marker.data.indentation);
|
||||
});
|
17
tests/cases/fourslash/smartIndentDoStatement.ts
Normal file
17
tests/cases/fourslash/smartIndentDoStatement.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
//// 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);
|
20
tests/cases/fourslash/smartIndentIfStatement.ts
Normal file
20
tests/cases/fourslash/smartIndentIfStatement.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
//// 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);
|
|
@ -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);
|
||||
verify.indentationIs(0);
|
14
tests/cases/fourslash/smartIndentListItem.ts
Normal file
14
tests/cases/fourslash/smartIndentListItem.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
////[1,
|
||||
//// 2
|
||||
//// + 3, 4,
|
||||
//// /*1*/
|
||||
////[1,
|
||||
//// 2
|
||||
//// + 3, 4
|
||||
//// /*2*/
|
||||
|
||||
goTo.marker("1");
|
||||
verify.indentationIs(4)
|
||||
goTo.marker("2");
|
||||
verify.indentationIs(4)
|
|
@ -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");
|
||||
|
|
|
@ -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 |}
|
||||
////}
|
||||
|
|
8
tests/cases/fourslash/smartIndentTypeArgumentList.ts
Normal file
8
tests/cases/fourslash/smartIndentTypeArgumentList.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/// <reference path='fourslash.ts'/>
|
||||
|
||||
////interface T1 extends T<number,
|
||||
//// string
|
||||
//// /**/>
|
||||
|
||||
goTo.marker();
|
||||
verify.indentationIs(32);
|
|
@ -8,6 +8,4 @@
|
|||
goTo.marker();
|
||||
edit.insert('case 1:\n');
|
||||
|
||||
// ideally would be 8
|
||||
//verify.indentationIs(8);
|
||||
verify.indentationIs(4);
|
||||
verify.indentationIs(8);
|
Loading…
Reference in a new issue