From ddbe031a8b446d8573246d9ac63ef26509bb64b6 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 12 Sep 2014 00:18:29 -0700 Subject: [PATCH] use actual indentation if possible --- src/services/formatting/smartIndenter.ts | 207 ++++++++++++------ .../fourslash/smartIndentActualIndentation.ts | 17 ++ 2 files changed, 161 insertions(+), 63 deletions(-) create mode 100644 tests/cases/fourslash/smartIndentActualIndentation.ts diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 6e04833922b..19240d537b1 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -2,6 +2,12 @@ module ts.formatting { export module SmartIndenter { + + interface LineAndCharacter { + line: number; + character: number; + } + export function getIndentation(position: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { if (position > sourceFile.text.length) { return 0; // past EOF @@ -22,24 +28,10 @@ module ts.formatting { 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 precedingListItem = findPrecedingListItem(precedingToken); - var precedingListItemStartLineAndChar = sourceFile.getLineAndCharacterFromPosition(precedingListItem.getStart(sourceFile)); - var listStartLine = getStartLineForNode(precedingListItem.parent, sourceFile); - - if (precedingListItemStartLineAndChar.line !== listStartLine) { - return findFirstNonWhitespaceCharacterInLine(precedingListItemStartLineAndChar.line, precedingListItemStartLineAndChar.character, sourceFile); - // previous list item starts on the different line with list, find first non-whitespace character in this line and use its position as indentation - var lineStartPosition = sourceFile.getPositionFromLineAndCharacter(precedingListItemStartLineAndChar.line, 1); - for (var i = 0; i < precedingListItemStartLineAndChar.character; ++i) { - if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStartPosition + i))) { - return i; - } - } - - // seems that this is the first non-whitespace character on the line - return it - return precedingListItemStartLineAndChar.character; + var actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation; } } @@ -47,31 +39,27 @@ module ts.formatting { // if such node is found - compute initial indentation for 'position' inside this node var previous: Node; var current = precedingToken; - var currentStartLine: number; + var currentStart: LineAndCharacter; var indentation: number; while (current) { if (isPositionBelongToNode(current, position, sourceFile)) { - currentStartLine = getStartLineForNode(current, sourceFile); + currentStart = getStartLineAndCharacterForNode(current, sourceFile); if (discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(precedingToken, current, lineAtPosition, sourceFile)) { indentation = 0; } else { - indentation = isNodeContentIndented(current, previous) && lineAtPosition !== currentStartLine ? options.indentSpaces : 0; + indentation = isNodeContentIndented(current, previous) && lineAtPosition !== currentStart.line ? options.indentSpaces : 0; } break; } - var customIndentation = getCustomIndentationForListItem(current, sourceFile); - if (customIndentation !== -1) { - return customIndentation; - } // check if current node is a list item - if yes, take indentation from it - var customIndentation = getCustomIndentationForListItem(current, sourceFile); - if (customIndentation !== -1) { - return customIndentation; + var actualIndentation = getActualIndentationForListItem(current, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation; } previous = current; @@ -85,37 +73,121 @@ module ts.formatting { var parent: Node = current.parent; - var parentStartLine: number; + 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 customIndentation = getCustomIndentationForListItem(current, sourceFile); - if (customIndentation !== -1) { - return customIndentation + indentation; + var actualIndentation = getActualIndentationForListItem(current, sourceFile, options); + if (actualIndentation !== -1) { + return actualIndentation + indentation; + } + + parentStart = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)); + var parentAndChildShareLine = + parentStart.line === currentStart.line || + isChildStartsOnTheSameLineWithElseInIfStatement(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 + indentation; } - parentStartLine = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)).line; // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line - var increaseIndentation = - isNodeContentIndented(parent, current) && - parentStartLine !== currentStartLine && - !isChildStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStartLine, sourceFile); + var increaseIndentation = isNodeContentIndented(parent, current) && !parentAndChildShareLine; if (increaseIndentation) { indentation += options.indentSpaces; } current = parent; - currentStartLine = parentStartLine; + currentStart = parentStart; parent = current.parent; } return indentation; } + function isDeclaration(n: Node): boolean { + switch(n.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.Method: + case SyntaxKind.Property: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.VariableDeclaration: + return true; + default: + return false; + } + } + + 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.LabelledStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ThrowKeyword: + case SyntaxKind.TryStatement: + case SyntaxKind.VariableStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + return true; + default: + return false; + } + } + + 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 precedingListItem = findPrecedingListItem(commaToken); + var precedingListItemStartLineAndChar = sourceFile.getLineAndCharacterFromPosition(precedingListItem.getStart(sourceFile)); + var listStart = getStartLineAndCharacterForNode(precedingListItem.parent, sourceFile); + + if (precedingListItemStartLineAndChar.line !== listStart.line) { + return findColumnForFirstNonWhitespaceCharacterInLine(precedingListItemStartLineAndChar, sourceFile, options); + // previous list item starts on the different line with list, find first non-whitespace character in this line and use its position as indentation + var lineStartPosition = sourceFile.getPositionFromLineAndCharacter(precedingListItemStartLineAndChar.line, 1); + for (var i = 0; i < precedingListItemStartLineAndChar.character; ++i) { + if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStartPosition + i))) { + return i; + } + } + + // seems that this is the first non-whitespace character on the line - return it + return precedingListItemStartLineAndChar.character; + } + + return -1; + } + + function getActualIndentationForNode(current: Node, parent: Node, currentLineAndChar: LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + var useActualIndentation = + (isDeclaration(current) || isStatement(current)) && + (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine); + + if (!useActualIndentation) { + return -1; + } + + return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); + } + function discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): boolean { var nextToken = findNextToken(precedingToken, current); if (!nextToken) { @@ -136,15 +208,15 @@ module ts.formatting { // class A { // $} - var nextTokenStartLine = getStartLineForNode(nextToken, sourceFile); + var nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; return lineAtPosition === nextTokenStartLine; } return false; } - function getStartLineForNode(n: Node, sourceFile: SourceFile): number { - return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)).line; + function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFile): LineAndCharacter { + return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)); } function findPrecedingListItem(commaToken: Node): Node { @@ -174,20 +246,20 @@ module ts.formatting { var elseKeyword = forEach(parent.getChildren(), c => c.kind === SyntaxKind.ElseKeyword && c); Debug.assert(elseKeyword); - var elseKeywordStartLine = getStartLineForNode(elseKeyword, sourceFile); + var elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; return elseKeywordStartLine === childStartLine; } } - function getCustomIndentationForListItem(node: Node, sourceFile: SourceFile): number { + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { if (node.parent) { switch (node.parent.kind) { case SyntaxKind.ObjectLiteral: - return getCustomIndentationFromList((node.parent).properties); + return getActualIndentationFromList((node.parent).properties); case SyntaxKind.TypeLiteral: - return getCustomIndentationFromList((node.parent).members); + return getActualIndentationFromList((node.parent).members); case SyntaxKind.ArrayLiteral: - return getCustomIndentationFromList((node.parent).elements); + return getActualIndentationFromList((node.parent).elements); case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: @@ -195,18 +267,18 @@ module ts.formatting { case SyntaxKind.CallSignature: case SyntaxKind.ConstructSignature: if ((node.parent).typeParameters && node.end < (node.parent).typeParameters.end) { - return getCustomIndentationFromList((node.parent).typeParameters); + return getActualIndentationFromList((node.parent).typeParameters); } else { - return getCustomIndentationFromList((node.parent).parameters); + return getActualIndentationFromList((node.parent).parameters); } case SyntaxKind.NewExpression: case SyntaxKind.CallExpression: if ((node.parent).typeArguments && node.end < (node.parent).typeArguments.end) { - return getCustomIndentationFromList((node.parent).typeArguments); + return getActualIndentationFromList((node.parent).typeArguments); } else { - return getCustomIndentationFromList((node.parent).arguments); + return getActualIndentationFromList((node.parent).arguments); } break; @@ -215,31 +287,40 @@ module ts.formatting { return -1; - function getCustomIndentationFromList(list: Node[]): number { + function getActualIndentationFromList(list: Node[]): number { var index = indexOf(list, node); if (index !== -1) { - var lineAndCol = sourceFile.getLineAndCharacterFromPosition(node.getStart(sourceFile)); + var lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile);; for (var i = index - 1; i >= 0; --i) { - var prevLineAndCol = sourceFile.getLineAndCharacterFromPosition(list[i].getStart(sourceFile)); - if (lineAndCol.line !== prevLineAndCol.line) { - return findFirstNonWhitespaceCharacterInLine(lineAndCol.line, lineAndCol.character, sourceFile); + var prevLineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); + if (lineAndCharacter.line !== prevLineAndCharacter.line) { + return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options); } - lineAndCol = prevLineAndCol; + lineAndCharacter = prevLineAndCharacter; } } return -1; } } - function findFirstNonWhitespaceCharacterInLine(line: number, maxCharacter: number, sourceFile: SourceFile): number { - var lineStart = sourceFile.getPositionFromLineAndCharacter(line, 1); - for (var i = 0; i < maxCharacter; ++i) { - if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStart + i))) { - return i; + 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 maxCharacter; + return column; } function findNextToken(previousToken: Node, parent: Node): Node { @@ -257,10 +338,10 @@ module ts.formatting { var shouldDiveInChildNode = // previous token is enclosed somewhere in the child (child.pos <= previousToken.pos && child.end > previousToken.end) || - // previous token end exactly at the beginning of child + // previous token ends exactly at the beginning of child (child.pos === previousToken.end); - if (shouldDiveInChildNode && isCandidateNode(child)) { + if (shouldDiveInChildNode && isCandidateNode(child)) { return find(child); } } diff --git a/tests/cases/fourslash/smartIndentActualIndentation.ts b/tests/cases/fourslash/smartIndentActualIndentation.ts new file mode 100644 index 00000000000..b2874d3c1d8 --- /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);