diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index c038cf39868..e8e80b9898b 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -132,7 +132,7 @@ module ts { function writeLiteral(s: string) { if (s && s.length) { write(s); - var lineStartsOfS = getLineStarts(s); + var lineStartsOfS = computeLineStarts(s); if (lineStartsOfS.length > 1) { lineCount = lineCount + lineStartsOfS.length - 1; linePos = output.length - s.length + lineStartsOfS[lineStartsOfS.length - 1]; diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index b7582b9ed06..cd08aaa0914 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -782,18 +782,16 @@ module ts { }; })(); - function getLineAndCharacterlFromSourcePosition(position: number) { - if (!lineStarts) { - lineStarts = getLineStarts(sourceText); - } - return getLineAndCharacterOfPosition(lineStarts, position); + function getLineStarts(): number[] { + return lineStarts || (lineStarts = computeLineStarts(sourceText)); + } + + function getLineAndCharacterFromSourcePosition(position: number) { + return getLineAndCharacterOfPosition(getLineStarts(), position); } function getPositionFromSourceLineAndCharacter(line: number, character: number): number { - if (!lineStarts) { - lineStarts = getLineStarts(sourceText); - } - return getPositionFromLineAndCharacter(lineStarts, line, character); + return getPositionFromLineAndCharacter(getLineStarts(), line, character); } function error(message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): void { @@ -3888,8 +3886,9 @@ module ts { file = createRootNode(SyntaxKind.SourceFile, 0, sourceText.length, rootNodeFlags); file.filename = normalizePath(filename); file.text = sourceText; - file.getLineAndCharacterFromPosition = getLineAndCharacterlFromSourcePosition; + file.getLineAndCharacterFromPosition = getLineAndCharacterFromSourcePosition; file.getPositionFromLineAndCharacter = getPositionFromSourceLineAndCharacter; + file.getLineStarts = getLineStarts; file.syntacticErrors = []; file.semanticErrors = []; var referenceComments = processReferenceComments(); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 81d16b5f487..1aa74a35a71 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -244,7 +244,7 @@ module ts { return tokenStrings[t]; } - export function getLineStarts(text: string): number[] { + export function computeLineStarts(text: string): number[] { var result: number[] = new Array(); var pos = 0; var lineStart = 0; @@ -292,7 +292,7 @@ module ts { } export function positionToLineAndCharacter(text: string, pos: number) { - var lineStarts = getLineStarts(text); + var lineStarts = computeLineStarts(text); return getLineAndCharacterOfPosition(lineStarts, pos); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b2da87c4ec0..cab45aadbe6 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -547,6 +547,7 @@ module ts { text: string; getLineAndCharacterFromPosition(position: number): { line: number; character: number }; getPositionFromLineAndCharacter(line: number, character: number): number; + getLineStarts(): number[]; amdDependencies: string[]; referencedFiles: FileReference[]; syntacticErrors: Diagnostic[]; diff --git a/src/harness/harness.ts b/src/harness/harness.ts index e35fe83f9b2..8bfaeb5800f 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -882,7 +882,7 @@ module Harness { // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so // we have to string-based splitting instead and try to figure out the delimiting chars - var lineStarts = ts.getLineStarts(inputFile.content); + var lineStarts = ts.computeLineStarts(inputFile.content); var lines = inputFile.content.split('\n'); lines.forEach((line, lineIndex) => { if (line.length > 0 && line.charAt(line.length - 1) === '\r') { diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts index f7a6bdbf3e5..f7d6bcfeaa1 100644 --- a/src/harness/sourceMapRecorder.ts +++ b/src/harness/sourceMapRecorder.ts @@ -223,7 +223,7 @@ module Harness.SourceMapRecoder { sourceMapNames = sourceMapData.sourceMapNames; jsFile = currentJsFile; - jsLineMap = ts.getLineStarts(jsFile.code); + jsLineMap = ts.computeLineStarts(jsFile.code); spansOnSingleLine = []; prevWrittenSourcePos = 0; @@ -294,7 +294,7 @@ module Harness.SourceMapRecoder { sourceMapRecoder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]); sourceMapRecoder.WriteLine("-------------------------------------------------------------------"); - tsLineMap = ts.getLineStarts(newSourceFileCode); + tsLineMap = ts.computeLineStarts(newSourceFileCode); tsCode = newSourceFileCode; prevWrittenSourcePos = 0; } @@ -390,7 +390,7 @@ module Harness.SourceMapRecoder { } } - var tsCodeLineMap = ts.getLineStarts(sourceText); + var tsCodeLineMap = ts.computeLineStarts(sourceText); for (var i = 0; i < tsCodeLineMap.length; i++) { writeSourceMapIndent(prevEmittedCol, i == 0 ? markerIds[index] : " >"); sourceMapRecoder.Write(getTextOfLine(i, tsCodeLineMap, sourceText)); diff --git a/src/services/formatting/format.ts b/src/services/formatting/format.ts new file mode 100644 index 00000000000..72c55332a3c --- /dev/null +++ b/src/services/formatting/format.ts @@ -0,0 +1,535 @@ +/// +/// +/// + +module ts.formatting { + + export interface TextRangeWithKind extends TextRange { + kind: SyntaxKind; + } + + export interface TokenInfo extends TextRange { + leadingTrivia: TextRangeWithKind[]; + token: TextRangeWithKind; + trailingTrivia: TextRangeWithKind[]; + pos: number; + end: number; + } + + var formattingScanner = createScanner(ScriptTarget.ES5, /*skipTrivia*/ false); + + export function formatOnEnter(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[]{ + var line = getNonAdjustedLineAndCharacterFromPosition(position, sourceFile).line; + // get the span for the previous\current line + var span = { + // get start position for the previous line + pos: getStartPositionOfLine(line - 1, sourceFile), + // get end position for the current line (end value is exclusive so add 1 to the result) + end: getEndLinePosition(line, sourceFile) + 1 + } + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnEnter, formattingScanner); + } + + export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[]{ + return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon); + } + + export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[] { + return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace); + } + + export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[]{ + var span = { + pos: 0, + end: sourceFile.text.length + }; + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatDocument, formattingScanner); + } + + export function formatSelection(start: number, end: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeOptions): TextChange[]{ + // format from the beginning of the line + var span = { + pos: getStartLinePositionForPosition(start, sourceFile), + end: end + }; + return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection, formattingScanner); + } + + function getEndLinePosition(line: number, sourceFile: SourceFile): number { + var lineStarts = sourceFile.getLineStarts(); + if (line === lineStarts.length - 1) { + // last line - return EOF - + return sourceFile.text.length - lineStarts[line]; + } + else { + // current line start + var start = lineStarts[line]; + // take the start position of the next line -1 = it should be some line break + var pos = lineStarts[line + 1] - 1; + Debug.assert(isLineBreak(sourceFile.text.charCodeAt(pos))); + // walk backwards skipping line breaks, stop the the beginning of current line. + // i.e: + // + // $ <- end of line for this position should match the start position + while (start <= pos && isLineBreak(sourceFile.text.charCodeAt(pos))) { + pos--; + } + return pos; + } + } + + function getStartPositionOfLine(line: number, sourceFile: SourceFile): number { + return sourceFile.getLineStarts()[line]; + } + + function getStartLinePositionForPosition(position: number, sourceFile: SourceFile): number { + var lineStarts = sourceFile.getLineStarts(); + var line = getNonAdjustedLineAndCharacterFromPosition(position, sourceFile).line; + return lineStarts[line]; + } + + function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeOptions, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[]{ + var parent = findOutermostParent(position, expectedLastToken, sourceFile); + if (!parent) { + return []; + } + var span = { + pos: getStartLinePositionForPosition(parent.pos, sourceFile), + end: parent.end + }; + return formatSpan(span, sourceFile, options, rulesProvider, requestKind, formattingScanner); + } + + function findOutermostParent(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node { + var precedingToken = findPrecedingToken(position, sourceFile); + if (!precedingToken || precedingToken.kind !== expectedTokenKind) { + return undefined; + } + + // walk up and search for the parent node that ends at the same position with precedingToken + var current = precedingToken; + while (current && + current.parent && + current.parent.end === precedingToken.end && + !isListElement(current.parent, current) ) { + current = current.parent; + } + + return current; + } + + function rangeContainsRange(initial: TextRange, candidate: TextRange): boolean { + return startEndContainsRange(initial.pos, initial.end, candidate); + } + + function startEndContainsRange(start: number, end: number, candidate: TextRange): boolean { + return start <= candidate.pos && end >= candidate.end; + } + + function rangeOverlapsWithRange(r1: TextRange, r2: TextRange): boolean { + var start = Math.max(r1.pos, r2.pos); + var end = Math.min(r1.end, r2.end); + return start < end; + } + + function isListElement(parent: Node, node: Node): boolean { + switch (parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + return rangeContainsRange((parent).members, node); + case SyntaxKind.ModuleDeclaration: + var body = (parent).body; + return body && body.kind === SyntaxKind.Block && rangeContainsRange((body).statements, node); + case SyntaxKind.SourceFile: + case SyntaxKind.Block: + case SyntaxKind.TryBlock: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + case SyntaxKind.ModuleBlock: + return rangeContainsRange((parent).statements, node) + } + } + + function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { + return find(sourceFile); + + function find(n: Node): Node { + var candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); + return (candidate && find(candidate)) || n; + } + } + + function getIndentationForNode(n: Node, sourceFile: SourceFile, options: FormatCodeOptions): number { + var start = sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)); + return SmartIndenter.getIndentationForNode(n, start, /*indentationDelta*/ 0, sourceFile, options); + } + + function getNonAdjustedLineAndCharacterFromPosition(position: number, sourceFile: SourceFile): LineAndCharacter { + var lineAndChar = sourceFile.getLineAndCharacterFromPosition(position); + return { line: lineAndChar.line - 1, character: lineAndChar.character - 1 }; + } + + function formatSpan(originalRange: TextRange, + sourceFile: SourceFile, + options: FormatCodeOptions, + rulesProvider: RulesProvider, + requestKind: FormattingRequestKind, + scanner: Scanner): TextChange[] { + + // formatting context to be used by rules provider to get rules + var formattingContext = new FormattingContext(sourceFile, requestKind); + + var enclosingNode = findEnclosingNode(originalRange, sourceFile); + var initialIndentation = getIndentationForNode(enclosingNode, sourceFile, options); + + scanner.setText(sourceFile.text); + scanner.setTextPos(enclosingNode.pos); + + var previousRange: TextRangeWithKind; + var previousParent: Node; + var previousRangeStartLine: number; + + var lastTriviaWasNewLine = false; + var edits: TextChange[] = []; + + // advance the scaner + scanner.scan(); + var currentTokenInfo = fetchNextTokenInfo(); + + if (currentTokenInfo.token) { + var startLine = getNonAdjustedLineAndCharacterFromPosition(enclosingNode.getStart(sourceFile), sourceFile).line; + processNode(enclosingNode, enclosingNode, startLine, initialIndentation); + } + return edits; + + function processNode(node: Node, contextNode: Node, nodeStartLine: number, indentation: number) { + // TODO: skip nodes that has skipped or missing tokens + if (!rangeOverlapsWithRange(originalRange, node)) { + return; + } + + if (!rangeContainsRange(node, currentTokenInfo)) { + // node and its descendents don't contain current token from the scanner - skip it + return; + } + + var childContextNode = contextNode; + forEachChild( + node, + child => processChildNode(child, /*containingList*/ undefined, /*listElementIndex*/ -1), + nodes => { + for (var i = 0, len = nodes.length; i < len; ++i) { + processChildNode(nodes[i], /*containingList*/ nodes, /*listElementIndex*/ i) + } + } + ); + + while (currentTokenInfo.token && node.end >= currentTokenInfo.token.end) { + currentTokenInfo = consumeCurrentToken(node, childContextNode, indentation); + } + + /// Local functions + + function processChildNode(child: Node, containingList: Node[], listElementIndex: number): void { + var start = child.getStart(sourceFile); + + while (currentTokenInfo.token && start >= currentTokenInfo.token.end) { + // we've walked past the current token + // ask parent to handle it + currentTokenInfo = consumeCurrentToken(node, childContextNode, indentation); + childContextNode = node; + } + + if (!currentTokenInfo.token) { + return; + } + + // ensure that current token is inside child node + Debug.assert(currentTokenInfo.token.end <= child.end); + if (isToken(child) && currentTokenInfo.token.end === child.end) { + // tokens belong to parent nodes + currentTokenInfo = consumeCurrentToken(node, childContextNode, indentation); + childContextNode = node; + } + else { + var childStartLine = getNonAdjustedLineAndCharacterFromPosition(start, sourceFile).line; + + var childIndentation = indentation; + if (listElementIndex === -1) { + // child is not list element + + } + else { + // child is a list element + } + // determine child indentation + // if child + // TODO: share this code with SmartIndenter + var increaseIndentation = + childStartLine !== nodeStartLine && + !SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(node, child, childStartLine, sourceFile) && + SmartIndenter.nodeContentIsIndented(node, child); + + processNode(child, childContextNode, childStartLine, increaseIndentation ? indentation + options.IndentSize : indentation); + childContextNode = node; + } + } + } + + function fetchNextTokenInfo(): TokenInfo { + if (currentTokenInfo) { + lastTriviaWasNewLine = + currentTokenInfo.trailingTrivia && + currentTokenInfo.trailingTrivia[currentTokenInfo.trailingTrivia.length - 1].kind === SyntaxKind.NewLineTrivia; + } + + var leadingTrivia: TextRangeWithKind[]; + var trailingTrivia: TextRangeWithKind[]; + var tokenRange: TextRangeWithKind; + + var startPos = scanner.getStartPos(); + var initialStartPos = startPos; + + while (startPos < originalRange.end) { + var t = scanner.getToken(); + + if (tokenRange && !isTrivia(t)) { + // have already seen the token and item under cursor is not a trivia + break; + } + + // advance the cursor + scanner.scan(); + + var item = { pos: startPos, end: scanner.getStartPos(), kind: t }; + startPos = item.end; + + if (isTrivia(t)) { + if (tokenRange) { + + if (!trailingTrivia) { + trailingTrivia = []; + } + + trailingTrivia.push(item); + + if (t === SyntaxKind.NewLineTrivia) { + // trailing trivia is cut at the new line + break; + } + } + else { + if (!leadingTrivia) { + leadingTrivia = []; + } + + leadingTrivia.push(item); + } + } + else { + tokenRange = item; + } + } + + return { + leadingTrivia: leadingTrivia, + token: tokenRange, + trailingTrivia: trailingTrivia, + pos: initialStartPos, + end: scanner.getStartPos() + }; + } + + function consumeCurrentToken(parent: Node, contextNode: Node, indentation: number): TokenInfo { + Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); + + if (currentTokenInfo.leadingTrivia) { + processTrivia(currentTokenInfo.leadingTrivia, parent, contextNode, indentation); + } + + processRange(currentTokenInfo.token, parent, contextNode, indentation); + + if (currentTokenInfo.trailingTrivia) { + processTrivia(currentTokenInfo.trailingTrivia, parent, contextNode, indentation); + } + + return fetchNextTokenInfo(); + } + + function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, currentIndentation: number): void { + for (var i = 0, len = trivia.length; i < len; ++i) { + var triviaItem = trivia[i]; + if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { + processRange(triviaItem, parent, contextNode, currentIndentation); + } + } + } + + function processRange(range: TextRangeWithKind, parent: Node, contextNode: Node, indentation: number) { + var rangeStart = getNonAdjustedLineAndCharacterFromPosition(range.pos, sourceFile); + if (rangeContainsRange(originalRange, range)) { + var indentToken = false; + if (!previousRange) { + var originalStart = getNonAdjustedLineAndCharacterFromPosition(originalRange.pos, sourceFile); + // TODO: implement + if (isTrivia(range.kind)) { + trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); + } + else { + trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); + } + } + else { + processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode) + indentToken = rangeStart.line !== previousRangeStartLine; + } + + if (lastTriviaWasNewLine && indentToken) { + // TODO: handle indentation in multiline comments + if (!isTrivia(range.kind)) { + var currentIndentation = rangeStart.character; + if (indentation !== currentIndentation) { + var indentationString = getIndentationString(indentation, options); + var startLinePosition = getStartPositionOfLine(rangeStart.line, sourceFile); + recordReplace(startLinePosition, currentIndentation, indentationString); + } + } + } + } + + previousRange = range; + previousParent = parent; + previousRangeStartLine = rangeStart.line; + } + + function processPair(currentItem: TextRangeWithKind, + currentStartLine: number, + currentParent: Node, + previousItem: TextRangeWithKind, + previousStartLine: number, + previousParent: Node, + contextNode: Node): void { + + // TODO: compute common parent + formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); + var rule = rulesProvider.getRulesMap().GetRule(formattingContext); + + var trimTrailingWhitespaces: boolean; + if (rule) { + applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); + + if (rule.Operation.Action & (RuleAction.Space | RuleAction.Delete) && currentStartLine !== previousStartLine) { + // Old code: + // Handle the case where the next line is moved to be the end of this line. + // In this case we don't indent the next line in the next pass. + // this.forceSkipIndentingNextToken(t2.start()); + lastTriviaWasNewLine = false; + } + else if (rule.Operation.Action & RuleAction.NewLine && currentStartLine === previousStartLine) { + // Old code: + // Handle the case where token2 is moved to the new line. + // In this case we indent token2 in the next pass but we set + // sameLineIndent flag to notify the indenter that the indentation is within the line. + // this.forceIndentNextToken(t2.start()); + lastTriviaWasNewLine = true; + } + + // TODO: check if this is still needed + trimTrailingWhitespaces = + (rule.Operation.Action & (RuleAction.NewLine | RuleAction.Space)) && + rule.Flag !== RuleFlags.CanDeleteNewLines; + } + else { + trimTrailingWhitespaces = true; + } + + if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { + // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line + trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); + } + } + + function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { + for (var line = line1; line < line2; ++line) { + var lineStartPosition = getStartPositionOfLine(line, sourceFile); + var lineEndPosition = getEndLinePosition(line, sourceFile); + + // if (token && (token.kind == SyntaxKind.MultiLineCommentTrivia || token.kind == SyntaxKind.SingleLineCommentTrivia) && token.start() <= line.endPosition() && token.end() >= line.endPosition()) + + if (range && isComment(range.kind)&& false) { + continue; + } + + var pos = lineEndPosition; + while (pos >= lineStartPosition && isWhiteSpace(sourceFile.text.charCodeAt(pos))) { + pos--; + } + if (pos !== lineEndPosition) { + Debug.assert(pos === lineStartPosition || !isWhiteSpace(sourceFile.text.charCodeAt(pos))); + recordDelete(pos + 1, lineEndPosition - pos); + } + } + } + + function newTextChange(start: number, len: number, newText: string): TextChange { + return { span: new TypeScript.TextSpan(start, len), newText: newText } + } + + function recordDelete(start: number, len: number) { + if (len) { + edits.push(newTextChange(start, len, "")); + } + } + + function recordReplace(start: number, len: number, newText: string) { + if (len || newText) { + edits.push(newTextChange(start, len, newText)); + } + } + + function applyRuleEdits(rule: Rule, + previousRange: TextRangeWithKind, + previousStartLine: number, + currentRange: TextRangeWithKind, + currentStartLine: number): void { + + var between: TextRange; + switch (rule.Operation.Action) { + case RuleAction.Ignore: + // no action required + return; + case RuleAction.Delete: + if (previousRange.end !== currentRange.pos) { + // delete characters starting from t1.end up to t2.pos exclusive + recordDelete(previousRange.end, currentRange.pos - previousRange.end); + } + break; + case RuleAction.NewLine: + // exit early if we on different lines and rule cannot change number of newlines + // if line1 and line2 are on subsequent lines then no edits are required - ok to exit + // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines + if (rule.Flag !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return; + } + + // edit should not be applied only if we have one line feed between elements + var lineDelta = currentStartLine - previousStartLine; + if (lineDelta !== 1) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.NewLineCharacter); + } + break; + case RuleAction.Space: + // exit early if we on different lines and rule cannot change number of newlines + if (rule.Flag !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return; + } + + var posDelta = currentRange.pos - previousRange.end; + if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); + } + break; + } + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/formatter.ts b/src/services/formatting/new/formatter.ts new file mode 100644 index 00000000000..e4a1229abef --- /dev/null +++ b/src/services/formatting/new/formatter.ts @@ -0,0 +1,320 @@ +// +// 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 Formatter extends MultipleTokenIndenter { + private previousTokenSpan: TokenSpan = null; + private previousTokenParent: IndentationNodeContext = null; + + // TODO: implement it with skipped tokens in Fidelity + private scriptHasErrors: boolean = false; + + private rulesProvider: RulesProvider; + private formattingRequestKind: FormattingRequestKind; + private formattingContext: FormattingContext; + + constructor(textSpan: TextSpan, + sourceUnit: SourceUnitSyntax, + indentFirstToken: boolean, + options: FormattingOptions, + snapshot: ITextSnapshot, + rulesProvider: RulesProvider, + formattingRequestKind: FormattingRequestKind) { + + super(textSpan, sourceUnit, snapshot, indentFirstToken, options); + + this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); + + this.rulesProvider = rulesProvider; + this.formattingRequestKind = formattingRequestKind; + this.formattingContext = new FormattingContext(this.snapshot(), this.formattingRequestKind); + } + + public static getEdits(textSpan: TextSpan, + sourceUnit: SourceUnitSyntax, + options: FormattingOptions, + indentFirstToken: boolean, + snapshot: ITextSnapshot, + rulesProvider: RulesProvider, + formattingRequestKind: FormattingRequestKind): TextEditInfo[] { + var walker = new Formatter(textSpan, sourceUnit, indentFirstToken, options, snapshot, rulesProvider, formattingRequestKind); + visitNodeOrToken(walker, sourceUnit); + return walker.edits(); + } + + public visitTokenInSpan(token: ISyntaxToken): void { + if (token.fullWidth() !== 0) { + var tokenSpan = new TextSpan(this.position() + token.leadingTriviaWidth(), width(token)); + if (this.textSpan().containsTextSpan(tokenSpan)) { + this.processToken(token); + } + } + + // Call the base class to process the token and indent it if needed + super.visitTokenInSpan(token); + } + + private processToken(token: ISyntaxToken): void { + var position = this.position(); + + // Extract any leading comments + if (token.leadingTriviaWidth() !== 0) { + this.processTrivia(token.leadingTrivia(), position); + position += token.leadingTriviaWidth(); + } + + // Push the token + var currentTokenSpan = new TokenSpan(token.kind(), position, width(token)); + if (!this.parent().hasSkippedOrMissingTokenChild()) { + if (this.previousTokenSpan) { + // Note that formatPair calls TrimWhitespaceInLineRange in between the 2 tokens + this.formatPair(this.previousTokenSpan, this.previousTokenParent, currentTokenSpan, this.parent()); + } + else { + // We still want to trim whitespace even if it is the first trivia of the first token. Trim from the beginning of the span to the trivia + this.trimWhitespaceInLineRange(this.getLineNumber(this.textSpan()), this.getLineNumber(currentTokenSpan)); + } + } + this.previousTokenSpan = currentTokenSpan; + if (this.previousTokenParent) { + // Make sure to clear the previous parent before assigning a new value to it + this.indentationNodeContextPool().releaseNode(this.previousTokenParent, /* recursive */true); + } + this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); + position += width(token); + + // Extract any trailing comments + if (token.trailingTriviaWidth() !== 0) { + this.processTrivia(token.trailingTrivia(), position); + } + } + + private processTrivia(triviaList: ISyntaxTriviaList, fullStart: number) { + var position = fullStart; + + for (var i = 0, n = triviaList.count(); i < n ; i++) { + var trivia = triviaList.syntaxTriviaAt(i); + // For a comment, format it like it is a token. For skipped text, eat it up as a token, but skip the formatting + if (trivia.isComment() || trivia.isSkippedToken()) { + var currentTokenSpan = new TokenSpan(trivia.kind(), position, trivia.fullWidth()); + if (this.textSpan().containsTextSpan(currentTokenSpan)) { + if (trivia.isComment() && this.previousTokenSpan) { + // Note that formatPair calls TrimWhitespaceInLineRange in between the 2 tokens + this.formatPair(this.previousTokenSpan, this.previousTokenParent, currentTokenSpan, this.parent()); + } + else { + // We still want to trim whitespace even if it is the first trivia of the first token. Trim from the beginning of the span to the trivia + var startLine = this.getLineNumber(this.previousTokenSpan || this.textSpan()); + this.trimWhitespaceInLineRange(startLine, this.getLineNumber(currentTokenSpan)); + } + this.previousTokenSpan = currentTokenSpan; + if (this.previousTokenParent) { + // Make sure to clear the previous parent before assigning a new value to it + this.indentationNodeContextPool().releaseNode(this.previousTokenParent, /* recursive */true); + } + this.previousTokenParent = this.parent().clone(this.indentationNodeContextPool()); + } + } + + position += trivia.fullWidth(); + } + } + + private findCommonParents(parent1: IndentationNodeContext, parent2: IndentationNodeContext): IndentationNodeContext { + // TODO: disable debug assert message + + var shallowParent: IndentationNodeContext; + var shallowParentDepth: number; + var deepParent: IndentationNodeContext; + var deepParentDepth: number; + + if (parent1.depth() < parent2.depth()) { + shallowParent = parent1; + shallowParentDepth = parent1.depth(); + deepParent = parent2; + deepParentDepth = parent2.depth(); + } + else { + shallowParent = parent2; + shallowParentDepth = parent2.depth(); + deepParent = parent1; + deepParentDepth = parent1.depth(); + } + + Debug.assert(shallowParentDepth >= 0, "Expected shallowParentDepth >= 0"); + Debug.assert(deepParentDepth >= 0, "Expected deepParentDepth >= 0"); + Debug.assert(deepParentDepth >= shallowParentDepth, "Expected deepParentDepth >= shallowParentDepth"); + + while (deepParentDepth > shallowParentDepth) { + deepParent = deepParent.parent(); + deepParentDepth--; + } + + Debug.assert(deepParentDepth === shallowParentDepth, "Expected deepParentDepth === shallowParentDepth"); + + while (deepParent.node() && shallowParent.node()) { + if (deepParent.node() === shallowParent.node()) { + return deepParent; + } + deepParent = deepParent.parent(); + shallowParent = shallowParent.parent(); + } + + // The root should be the first element in the parent chain, we can not be here unless something wrong + // happened along the way + throw Errors.invalidOperation(); + } + + private formatPair(t1: TokenSpan, t1Parent: IndentationNodeContext, t2: TokenSpan, t2Parent: IndentationNodeContext): void { + var token1Line = this.getLineNumber(t1); + var token2Line = this.getLineNumber(t2); + + // Find common parent + var commonParent= this.findCommonParents(t1Parent, t2Parent); + + // Update the context + this.formattingContext.updateContext(t1, t1Parent, t2, t2Parent, commonParent); + + // Find rules matching the current context + var rule = this.rulesProvider.getRulesMap().GetRule(this.formattingContext); + + if (rule != null) { + // Record edits from the rule + this.RecordRuleEdits(rule, t1, t2); + + // Handle the case where the next line is moved to be the end of this line. + // In this case we don't indent the next line in the next pass. + if ((rule.Operation.Action == RuleAction.Space || rule.Operation.Action == RuleAction.Delete) && + token1Line != token2Line) { + this.forceSkipIndentingNextToken(t2.start()); + } + + // Handle the case where token2 is moved to the new line. + // In this case we indent token2 in the next pass but we set + // sameLineIndent flag to notify the indenter that the indentation is within the line. + if (rule.Operation.Action == RuleAction.NewLine && token1Line == token2Line) { + this.forceIndentNextToken(t2.start()); + } + } + + // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line + if (token1Line != token2Line && (!rule || (rule.Operation.Action != RuleAction.Delete && rule.Flag != RuleFlags.CanDeleteNewLines))) { + this.trimWhitespaceInLineRange(token1Line, token2Line, t1); + } + } + + private getLineNumber(span: TextSpan): number { + return this.snapshot().getLineNumberFromPosition(span.start()); + } + + private trimWhitespaceInLineRange(startLine: number, endLine: number, token?: TokenSpan): void { + for (var lineNumber = startLine; lineNumber < endLine; ++lineNumber) { + var line = this.snapshot().getLineFromLineNumber(lineNumber); + + this.trimWhitespace(line, token); + } + } + + private trimWhitespace(line: ITextSnapshotLine, token?: TokenSpan): void { + // Don't remove the trailing spaces inside comments (this includes line comments and block comments) + if (token && (token.kind == SyntaxKind.MultiLineCommentTrivia || token.kind == SyntaxKind.SingleLineCommentTrivia) && token.start() <= line.endPosition() && token.end() >= line.endPosition()) + return; + + var text = line.getText(); + var index = 0; + + for (index = text.length - 1; index >= 0; --index) { + if (!CharacterInfo.isWhitespace(text.charCodeAt(index))) { + break; + } + } + + ++index; + + if (index < text.length) { + this.recordEdit(line.startPosition() + index, line.length() - index, ""); + } + } + + private RecordRuleEdits(rule: Rule, t1: TokenSpan, t2: TokenSpan): void { + if (rule.Operation.Action == RuleAction.Ignore) { + return; + } + + var betweenSpan: TextSpan; + + switch (rule.Operation.Action) { + case RuleAction.Delete: + { + betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); + + if (betweenSpan.length() > 0) { + this.recordEdit(betweenSpan.start(), betweenSpan.length(), ""); + return; + } + } + break; + + case RuleAction.NewLine: + { + if (!(rule.Flag == RuleFlags.CanDeleteNewLines || this.getLineNumber(t1) == this.getLineNumber(t2))) { + return; + } + + betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); + + var doEdit = false; + var betweenText = this.snapshot().getText(betweenSpan); + + var lineFeedLoc = betweenText.indexOf(this.options.newLineCharacter); + if (lineFeedLoc < 0) { + // no linefeeds, do the edit + doEdit = true; + } + else { + // We only require one line feed. If there is another one, do the edit + lineFeedLoc = betweenText.indexOf(this.options.newLineCharacter, lineFeedLoc + 1); + if (lineFeedLoc >= 0) { + doEdit = true; + } + } + + if (doEdit) { + this.recordEdit(betweenSpan.start(), betweenSpan.length(), this.options.newLineCharacter); + return; + } + } + break; + + case RuleAction.Space: + { + if (!(rule.Flag == RuleFlags.CanDeleteNewLines || this.getLineNumber(t1) == this.getLineNumber(t2))) { + return; + } + + betweenSpan = new TextSpan(t1.end(), t2.start() - t1.end()); + + if (betweenSpan.length() > 1 || this.snapshot().getText(betweenSpan) != " ") { + this.recordEdit(betweenSpan.start(), betweenSpan.length(), " "); + return; + } + } + break; + } + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/formatting.ts b/src/services/formatting/new/formatting.ts new file mode 100644 index 00000000000..a2a15c374e4 --- /dev/null +++ b/src/services/formatting/new/formatting.ts @@ -0,0 +1,39 @@ +// +// 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. +// + +/// +/// +/// +/// +/// +// /// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +// /// +// /// +// /// \ No newline at end of file diff --git a/src/services/formatting/new/formattingContext.ts b/src/services/formatting/new/formattingContext.ts new file mode 100644 index 00000000000..ffac516690b --- /dev/null +++ b/src/services/formatting/new/formattingContext.ts @@ -0,0 +1,130 @@ +// +// 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 ts.formatting { + export class FormattingContext { + public currentTokenSpan: TextRangeWithKind = null; + public nextTokenSpan: TextRangeWithKind = null; + public contextNode: Node = null; + public currentTokenParent: Node = null; + public nextTokenParent: Node = null; + + private contextNodeAllOnSameLine: boolean = null; + private nextNodeAllOnSameLine: boolean = null; + private tokensAreOnSameLine: boolean = null; + private contextNodeBlockIsOnOneLine: boolean = null; + private nextNodeBlockIsOnOneLine: boolean = null; + + constructor(private sourceFile: SourceFile, public formattingRequestKind: FormattingRequestKind) { + } + + public updateContext(currentRange: TextRangeWithKind, currentTokenParent: Node, nextRange: TextRangeWithKind, nextTokenParent: Node, commonParent: Node) { + Debug.assert(currentRange != null, "currentTokenSpan is null"); + Debug.assert(currentTokenParent != null, "currentTokenParent is null"); + Debug.assert(nextRange != null, "nextTokenSpan is null"); + Debug.assert(nextTokenParent != null, "nextTokenParent is null"); + Debug.assert(commonParent != null, "commonParent is null"); + + this.currentTokenSpan = currentRange; + this.currentTokenParent = currentTokenParent; + this.nextTokenSpan = nextRange; + this.nextTokenParent = nextTokenParent; + this.contextNode = commonParent; + + this.contextNodeAllOnSameLine = null; + this.nextNodeAllOnSameLine = null; + this.tokensAreOnSameLine = null; + this.contextNodeBlockIsOnOneLine = null; + this.nextNodeBlockIsOnOneLine = null; + } + + public ContextNodeAllOnSameLine(): boolean { + if (this.contextNodeAllOnSameLine === null) { + this.contextNodeAllOnSameLine = this.NodeIsOnOneLine(this.contextNode); + } + + return this.contextNodeAllOnSameLine; + } + + public NextNodeAllOnSameLine(): boolean { + if (this.nextNodeAllOnSameLine === null) { + this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); + } + + return this.nextNodeAllOnSameLine; + } + + public TokensAreOnSameLine(): boolean { + if (this.tokensAreOnSameLine === null) { + + //var startLine = this.snapshot.getLineNumberFromPosition(this.currentTokenSpan.token.pos); + //var endLine = this.snapshot.getLineNumberFromPosition(this.nextTokenSpan.token.pos); + + var startLine = this.sourceFile.getLineAndCharacterFromPosition(this.currentTokenSpan.pos).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(this.nextTokenSpan.pos).line; + this.tokensAreOnSameLine = (startLine == endLine); + } + + return this.tokensAreOnSameLine; + } + + public ContextNodeBlockIsOnOneLine() { + if (this.contextNodeBlockIsOnOneLine === null) { + this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); + } + + return this.contextNodeBlockIsOnOneLine; + } + + public NextNodeBlockIsOnOneLine() { + if (this.nextNodeBlockIsOnOneLine === null) { + this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); + } + + return this.nextNodeBlockIsOnOneLine; + } + + public NodeIsOnOneLine(node: Node): boolean { + return; + + var startLine = this.sourceFile.getLineAndCharacterFromPosition(node.getStart(this.sourceFile)).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(node.getEnd()).line; + //var startLine = this.snapshot.getLineNumberFromPosition(node.start()); + //var endLine = this.snapshot.getLineNumberFromPosition(node.end()); + + return startLine == endLine; + } + + // Now we know we have a block (or a fake block represented by some other kind of node with an open and close brace as children). + // IMPORTANT!!! This relies on the invariant that IsBlockContext must return true ONLY for nodes with open and close braces as immediate children + public BlockIsOnOneLine(node: Node): boolean { + var openBrace = findChildOfKind(node, SyntaxKind.OpenBraceToken, this.sourceFile); + var closeBrace = findChildOfKind(node, SyntaxKind.CloseBraceToken, this.sourceFile); + if (openBrace && closeBrace) { + var startLine = this.sourceFile.getLineAndCharacterFromPosition(openBrace.getEnd()).line; + var endLine = this.sourceFile.getLineAndCharacterFromPosition(closeBrace.getStart(this.sourceFile)).line; + return startLine === endLine; + } + + //var block = node.node(); + + //// Now check if they are on the same line + //return this.snapshot.getLineNumberFromPosition(end(block.openBraceToken)) === + // this.snapshot.getLineNumberFromPosition(start(block.closeBraceToken)); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/formattingManager.ts b/src/services/formatting/new/formattingManager.ts new file mode 100644 index 00000000000..359f19c84ed --- /dev/null +++ b/src/services/formatting/new/formattingManager.ts @@ -0,0 +1,123 @@ +// +// 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 FormattingManager { + private options: FormattingOptions; + + constructor(private syntaxTree: SyntaxTree, + private snapshot: ITextSnapshot, + private rulesProvider: RulesProvider, + editorOptions: ts.EditorOptions) { + // + // TODO: convert to use FormattingOptions instead of EditorOptions + this.options = new FormattingOptions(!editorOptions.ConvertTabsToSpaces, editorOptions.TabSize, editorOptions.IndentSize, editorOptions.NewLineCharacter) + } + + public formatSelection(minChar: number, limChar: number): ts.TextChange[] { + var span = TextSpan.fromBounds(minChar, limChar); + return this.formatSpan(span, FormattingRequestKind.FormatSelection); + } + + public formatDocument(): ts.TextChange[] { + var span = TextSpan.fromBounds(0, this.snapshot.getLength()); + return this.formatSpan(span, FormattingRequestKind.FormatDocument); + } + + public formatOnSemicolon(caretPosition: number): ts.TextChange[] { + var sourceUnit = this.syntaxTree.sourceUnit(); + var semicolonPositionedToken = findToken(sourceUnit, caretPosition - 1); + + if (semicolonPositionedToken.kind() === SyntaxKind.SemicolonToken) { + // Find the outer most parent that this semicolon terminates + var current: ISyntaxElement = semicolonPositionedToken; + while (current.parent !== null && + end(current.parent) === end(semicolonPositionedToken) && + current.parent.kind() !== SyntaxKind.List) { + current = current.parent; + } + + // Compute the span + var span = new TextSpan(fullStart(current), fullWidth(current)); + + // Format the span + return this.formatSpan(span, FormattingRequestKind.FormatOnSemicolon); + } + + return []; + } + + public formatOnClosingCurlyBrace(caretPosition: number): ts.TextChange[] { + var sourceUnit = this.syntaxTree.sourceUnit(); + var closeBracePositionedToken = findToken(sourceUnit, caretPosition - 1); + + if (closeBracePositionedToken.kind() === SyntaxKind.CloseBraceToken) { + // Find the outer most parent that this closing brace terminates + var current: ISyntaxElement = closeBracePositionedToken; + while (current.parent !== null && + end(current.parent) === end(closeBracePositionedToken) && + current.parent.kind() !== SyntaxKind.List) { + current = current.parent; + } + + // Compute the span + var span = new TextSpan(fullStart(current), fullWidth(current)); + + // Format the span + return this.formatSpan(span, FormattingRequestKind.FormatOnClosingCurlyBrace); + } + + return []; + } + + public formatOnEnter(caretPosition: number): ts.TextChange[] { + var lineNumber = this.snapshot.getLineNumberFromPosition(caretPosition); + + if (lineNumber > 0) { + // Format both lines + var prevLine = this.snapshot.getLineFromLineNumber(lineNumber - 1); + var currentLine = this.snapshot.getLineFromLineNumber(lineNumber); + var span = TextSpan.fromBounds(prevLine.startPosition(), currentLine.endPosition()); + + // Format the span + return this.formatSpan(span, FormattingRequestKind.FormatOnEnter); + + } + + return []; + } + + private formatSpan(span: TextSpan, formattingRequestKind: FormattingRequestKind): ts.TextChange[] { + // Always format from the beginning of the line + var startLine = this.snapshot.getLineFromPosition(span.start()); + span = TextSpan.fromBounds(startLine.startPosition(), span.end()); + + var result: ts.TextChange[] = []; + + var formattingEdits = Formatter.getEdits(span, this.syntaxTree.sourceUnit(), this.options, true, this.snapshot, this.rulesProvider, formattingRequestKind); + + // + // TODO: Change the ILanguageService interface to return TextEditInfo (with start, and length) instead of TextEdit (with minChar and limChar) + formattingEdits.forEach((item) => { + var edit = new ts.TextChange(new TextSpan(item.position, item.length), item.replaceWith); + result.push(edit); + }); + + return result; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/formattingRequestKind.ts b/src/services/formatting/new/formattingRequestKind.ts new file mode 100644 index 00000000000..1e7df200554 --- /dev/null +++ b/src/services/formatting/new/formattingRequestKind.ts @@ -0,0 +1,26 @@ +// +// 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 ts.formatting { + export enum FormattingRequestKind { + FormatDocument, + FormatSelection, + FormatOnEnter, + FormatOnSemicolon, + FormatOnClosingCurlyBrace + } +} \ No newline at end of file diff --git a/src/services/formatting/new/indentationNodeContext.ts b/src/services/formatting/new/indentationNodeContext.ts new file mode 100644 index 00000000000..838031227be --- /dev/null +++ b/src/services/formatting/new/indentationNodeContext.ts @@ -0,0 +1,103 @@ +// +// 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 ts.formatting { + export class IndentationNodeContext { + private _node: Node; + private _parent: IndentationNodeContext; + private _fullStart: number; + private _indentationAmount: number; + private _childIndentationAmountDelta: number; + private _depth: number; + private _hasSkippedOrMissingTokenChild: boolean; + + constructor(parent: IndentationNodeContext, node: Node, fullStart: number, indentationAmount: number, childIndentationAmountDelta: number) { + this.update(parent, node, fullStart, indentationAmount, childIndentationAmountDelta); + } + + public parent(): IndentationNodeContext { + return this._parent; + } + + public node(): Node { + return this._node; + } + + public fullStart(): number { + return this._fullStart; + } + + public fullWidth(): number { + return this._node.getFullWidth(); + } + + public start(): number { + return this._node.getStart(); + } + + public end(): number { + return this._node.getEnd(); + } + + public indentationAmount(): number { + return this._indentationAmount; + } + + public childIndentationAmountDelta(): number { + return this._childIndentationAmountDelta; + } + + public depth(): number { + return this._depth; + } + + public kind(): SyntaxKind { + return this._node.kind; + } + + public hasSkippedOrMissingTokenChild(): boolean { + if (this._hasSkippedOrMissingTokenChild === null) { + // this._hasSkippedOrMissingTokenChild = Syntax.nodeHasSkippedOrMissingTokens(this._node); + } + return this._hasSkippedOrMissingTokenChild; + } + + public clone(pool: IndentationNodeContextPool): IndentationNodeContext { + var parent: IndentationNodeContext = null; + if (this._parent) { + parent = this._parent.clone(pool); + } + return pool.getNode(parent, this._node, this._fullStart, this._indentationAmount, this._childIndentationAmountDelta); + } + + public update(parent: IndentationNodeContext, node: Node, fullStart: number, indentationAmount: number, childIndentationAmountDelta: number) { + this._parent = parent; + this._node = node; + this._fullStart = fullStart; + this._indentationAmount = indentationAmount; + this._childIndentationAmountDelta = childIndentationAmountDelta; + this._hasSkippedOrMissingTokenChild = null; + + if (parent) { + this._depth = parent.depth() + 1; + } + else { + this._depth = 0; + } + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/indentationNodeContextPool.ts b/src/services/formatting/new/indentationNodeContextPool.ts new file mode 100644 index 00000000000..b4bf2f09980 --- /dev/null +++ b/src/services/formatting/new/indentationNodeContextPool.ts @@ -0,0 +1,43 @@ +// +// 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 ts.formatting { + export class IndentationNodeContextPool { + private nodes: IndentationNodeContext[] = []; + + public getNode(parent: IndentationNodeContext, node: Node, fullStart: number, indentationLevel: number, childIndentationLevelDelta: number): IndentationNodeContext { + if (this.nodes.length > 0) { + var cachedNode = this.nodes.pop(); + cachedNode.update(parent, node, fullStart, indentationLevel, childIndentationLevelDelta); + return cachedNode; + } + + return new IndentationNodeContext(parent, node, fullStart, indentationLevel, childIndentationLevelDelta); + } + + public releaseNode(node: IndentationNodeContext, recursive: boolean = false): void { + this.nodes.push(node); + + if (recursive) { + var parent = node.parent(); + if (parent) { + this.releaseNode(parent, recursive); + } + } + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/indentationTrackingWalker.ts b/src/services/formatting/new/indentationTrackingWalker.ts new file mode 100644 index 00000000000..b07b477623e --- /dev/null +++ b/src/services/formatting/new/indentationTrackingWalker.ts @@ -0,0 +1,349 @@ +// +// 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 IndentationTrackingWalker extends SyntaxWalker { + private _position: number = 0; + private _parent: IndentationNodeContext = null; + private _textSpan: TextSpan; + private _snapshot: ITextSnapshot; + private _lastTriviaWasNewLine: boolean; + private _indentationNodeContextPool: IndentationNodeContextPool; + private _text: ISimpleText; + + constructor(textSpan: TextSpan, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, indentFirstToken: boolean, public options: FormattingOptions) { + super(); + + // Create a pool object to manage context nodes while walking the tree + this._indentationNodeContextPool = new IndentationNodeContextPool(); + + this._textSpan = textSpan; + this._text = sourceUnit.syntaxTree.text; + this._snapshot = snapshot; + this._parent = this._indentationNodeContextPool.getNode(null, sourceUnit, 0, 0, 0); + + // Is the first token in the span at the start of a new line. + this._lastTriviaWasNewLine = indentFirstToken; + } + + public position(): number { + return this._position; + } + + public parent(): IndentationNodeContext { + return this._parent; + } + + public textSpan(): TextSpan { + return this._textSpan; + } + + public snapshot(): ITextSnapshot { + return this._snapshot; + } + + public indentationNodeContextPool(): IndentationNodeContextPool { + return this._indentationNodeContextPool; + } + + public forceIndentNextToken(tokenStart: number): void { + this._lastTriviaWasNewLine = true; + this.forceRecomputeIndentationOfParent(tokenStart, true); + } + + public forceSkipIndentingNextToken(tokenStart: number): void { + this._lastTriviaWasNewLine = false; + this.forceRecomputeIndentationOfParent(tokenStart, false); + } + + public indentToken(token: ISyntaxToken, indentationAmount: number, commentIndentationAmount: number): void { + throw Errors.abstract(); + } + + public visitTokenInSpan(token: ISyntaxToken): void { + if (this._lastTriviaWasNewLine) { + // Compute the indentation level at the current token + var indentationAmount = this.getTokenIndentationAmount(token); + var commentIndentationAmount = this.getCommentIndentationAmount(token); + + // Process the token + this.indentToken(token, indentationAmount, commentIndentationAmount); + } + } + + public visitToken(token: ISyntaxToken): void { + var tokenSpan = new TextSpan(this._position, token.fullWidth()); + + if (tokenSpan.intersectsWithTextSpan(this._textSpan)) { + this.visitTokenInSpan(token); + + // Only track new lines on tokens within the range. Make sure to check that the last trivia is a newline, and not just one of the trivia + var trivia = token.trailingTrivia(); + this._lastTriviaWasNewLine = trivia.hasNewLine() && trivia.syntaxTriviaAt(trivia.count() - 1).kind() == SyntaxKind.NewLineTrivia; + } + + // Update the position + this._position += token.fullWidth(); + } + + public visitNode(node: ISyntaxNode): void { + var nodeSpan = new TextSpan(this._position, fullWidth(node)); + + if (nodeSpan.intersectsWithTextSpan(this._textSpan)) { + // Update indentation level + var indentation = this.getNodeIndentation(node); + + // Update the parent + var currentParent = this._parent; + this._parent = this._indentationNodeContextPool.getNode(currentParent, node, this._position, indentation.indentationAmount, indentation.indentationAmountDelta); + + // Visit node + visitNodeOrToken(this, node); + + // Reset state + this._indentationNodeContextPool.releaseNode(this._parent); + this._parent = currentParent; + } + else { + // We're skipping the node, so update our position accordingly. + this._position += fullWidth(node); + } + } + + private getTokenIndentationAmount(token: ISyntaxToken): number { + // If this is the first token of a node, it should follow the node indentation and not the child indentation; + // (e.g.class in a class declaration or module in module declariotion). + // Open and close braces should follow the indentation of thier parent as well(e.g. + // class { + // } + // Also in a do-while statement, the while should be indented like the parent. + if (firstToken(this._parent.node()) === token || + token.kind() === SyntaxKind.OpenBraceToken || token.kind() === SyntaxKind.CloseBraceToken || + token.kind() === SyntaxKind.OpenBracketToken || token.kind() === SyntaxKind.CloseBracketToken || + (token.kind() === SyntaxKind.WhileKeyword && this._parent.node().kind() == SyntaxKind.DoStatement)) { + return this._parent.indentationAmount(); + } + + return (this._parent.indentationAmount() + this._parent.childIndentationAmountDelta()); + } + + private getCommentIndentationAmount(token: ISyntaxToken): number { + // If this is token terminating an indentation scope, leading comments should be indented to follow the children + // indentation level and not the node + + if (token.kind() === SyntaxKind.CloseBraceToken || token.kind() === SyntaxKind.CloseBracketToken) { + return (this._parent.indentationAmount() + this._parent.childIndentationAmountDelta()); + } + return this._parent.indentationAmount(); + } + + private getNodeIndentation(node: ISyntaxNode, newLineInsertedByFormatting?: boolean): { indentationAmount: number; indentationAmountDelta: number; } { + var parent = this._parent; + + // We need to get the parent's indentation, which could be one of 2 things. If first token of the parent is in the span, use the parent's computed indentation. + // If the parent was outside the span, use the actual indentation of the parent. + var parentIndentationAmount: number; + if (this._textSpan.containsPosition(parent.start())) { + parentIndentationAmount = parent.indentationAmount(); + } + else { + if (parent.kind() === SyntaxKind.Block && !this.shouldIndentBlockInParent(this._parent.parent())) { + // Blocks preserve the indentation of their containing node (unless they're a + // standalone block in a list). i.e. if you have: + // + // function foo( + // a: number) { + // + // Then we expect the indentation of the block to be tied to the function, not to + // the line that the block is defined on. If we were to do the latter, then the + // indentation would be here: + // + // function foo( + // a: number) { + // | + // + // Instead of: + // + // function foo( + // a: number) { + // | + parent = this._parent.parent(); + } + + var line = this._snapshot.getLineFromPosition(parent.start()).getText(); + var firstNonWhiteSpacePosition = Indentation.firstNonWhitespacePosition(line); + parentIndentationAmount = Indentation.columnForPositionInString(line, firstNonWhiteSpacePosition, this.options); + } + var parentIndentationAmountDelta = parent.childIndentationAmountDelta(); + + // The indentation level of the node + var indentationAmount: number; + + // The delta it adds to its children. + var indentationAmountDelta: number; + var parentNode = parent.node(); + + switch (node.kind()) { + default: + // General case + // This node should follow the child indentation set by its parent + // This node does not introduce any new indentation scope, indent any decendants of this node (tokens or child nodes) + // using the same indentation level + indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); + indentationAmountDelta = 0; + break; + + // Statements introducing {} + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ObjectType: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MemberFunctionDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.IndexMemberDeclaration: + case SyntaxKind.CatchClause: + // Statements introducing [] + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ArrayType: + case SyntaxKind.ElementAccessExpression: + case SyntaxKind.IndexSignature: + // Other statements + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.CaseSwitchClause: + case SyntaxKind.DefaultSwitchClause: + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.SimpleArrowFunctionExpression: + case SyntaxKind.ParenthesizedArrowFunctionExpression: + case SyntaxKind.VariableDeclaration: + case SyntaxKind.ExportAssignment: + + // Expressions which have argument lists or parameter lists + case SyntaxKind.InvocationExpression: + case SyntaxKind.ObjectCreationExpression: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + + // These nodes should follow the child indentation set by its parent; + // they introduce a new indenation scope; children should be indented at one level deeper + indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); + indentationAmountDelta = this.options.indentSpaces; + break; + + case SyntaxKind.IfStatement: + if (parent.kind() === SyntaxKind.ElseClause && + !SyntaxUtilities.isLastTokenOnLine((parentNode).elseKeyword, this._text)) { + // This is an else if statement with the if on the same line as the else, do not indent the if statmement. + // Note: Children indentation has already been set by the parent if statement, so no need to increment + indentationAmount = parentIndentationAmount; + } + else { + // Otherwise introduce a new indenation scope; children should be indented at one level deeper + indentationAmount = (parentIndentationAmount + parentIndentationAmountDelta); + } + indentationAmountDelta = this.options.indentSpaces; + break; + + case SyntaxKind.ElseClause: + // Else should always follow its parent if statement indentation. + // Note: Children indentation has already been set by the parent if statement, so no need to increment + indentationAmount = parentIndentationAmount; + indentationAmountDelta = this.options.indentSpaces; + break; + + + case SyntaxKind.Block: + // Check if the block is a member in a list of statements (if the parent is a source unit, module, or block, or switch clause) + if (this.shouldIndentBlockInParent(parent)) { + indentationAmount = parentIndentationAmount + parentIndentationAmountDelta; + } + else { + indentationAmount = parentIndentationAmount; + } + + indentationAmountDelta = this.options.indentSpaces; + break; + } + + // If the parent happens to start on the same line as this node, then override the current node indenation with that + // of the parent. This avoid having to add an extra level of indentation for the children. e.g.: + // return { + // a:1 + // }; + // instead of: + // return { + // a:1 + // }; + // We also need to pass the delta (if it is nonzero) to the children, so that subsequent lines get indented. Essentially, if any node starting on the given line + // has a nonzero delta , the resulting delta should be inherited from this node. This is to indent cases like the following: + // return a + // || b; + // Lastly, it is possible the node indentation needs to be recomputed because the formatter inserted a newline before its first token. + // If this is the case, we know the node no longer starts on the same line as its parent (or at least we shouldn't treat it as such). + if (parentNode) { + if (!newLineInsertedByFormatting /*This could be false or undefined here*/) { + var parentStartLine = this._snapshot.getLineNumberFromPosition(parent.start()); + var currentNodeStartLine = this._snapshot.getLineNumberFromPosition(this._position + leadingTriviaWidth(node)); + if (parentStartLine === currentNodeStartLine || newLineInsertedByFormatting === false /*meaning a new line was removed and we are force recomputing*/) { + indentationAmount = parentIndentationAmount; + indentationAmountDelta = Math.min(this.options.indentSpaces, parentIndentationAmountDelta + indentationAmountDelta); + } + } + } + + return { + indentationAmount: indentationAmount, + indentationAmountDelta: indentationAmountDelta + }; + } + + private shouldIndentBlockInParent(parent: IndentationNodeContext): boolean { + switch (parent.kind()) { + case SyntaxKind.SourceUnit: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.Block: + case SyntaxKind.CaseSwitchClause: + case SyntaxKind.DefaultSwitchClause: + return true; + + default: + return false; + } + } + + private forceRecomputeIndentationOfParent(tokenStart: number, newLineAdded: boolean /*as opposed to removed*/): void { + var parent = this._parent; + if (parent.fullStart() === tokenStart) { + // Temporarily pop the parent before recomputing + this._parent = parent.parent(); + var indentation = this.getNodeIndentation(parent.node(), /* newLineInsertedByFormatting */ newLineAdded); + parent.update(parent.parent(), parent.node(), parent.fullStart(), indentation.indentationAmount, indentation.indentationAmountDelta); + this._parent = parent; + } + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/multipleTokenIndenter.ts b/src/services/formatting/new/multipleTokenIndenter.ts new file mode 100644 index 00000000000..23181571427 --- /dev/null +++ b/src/services/formatting/new/multipleTokenIndenter.ts @@ -0,0 +1,206 @@ +// +// 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 MultipleTokenIndenter extends IndentationTrackingWalker { + private _edits: TextEditInfo[] = []; + + constructor(textSpan: TextSpan, sourceUnit: SourceUnitSyntax, snapshot: ITextSnapshot, indentFirstToken: boolean, options: FormattingOptions) { + super(textSpan, sourceUnit, snapshot, indentFirstToken, options); + } + + public indentToken(token: ISyntaxToken, indentationAmount: number, commentIndentationAmount: number): void { + // Ignore generated tokens + if (token.fullWidth() === 0) { + return; + } + + // If we have any skipped tokens as children, do not process this node for indentation or formatting + if (this.parent().hasSkippedOrMissingTokenChild()) { + return; + } + + // Be strict, and only consider nodes that fall inside the span. This avoids indenting a multiline string + // on enter at the end of, as the whole token was not included in the span + var tokenSpan = new TextSpan(this.position() + token.leadingTriviaWidth(), width(token)); + if (!this.textSpan().containsTextSpan(tokenSpan)) { + return; + } + + // Compute an indentation string for this token + var indentationString = Indentation.indentationString(indentationAmount, this.options); + + var commentIndentationString = Indentation.indentationString(commentIndentationAmount, this.options); + + // Record any needed indentation edits + this.recordIndentationEditsForToken(token, indentationString, commentIndentationString); + } + + public edits(): TextEditInfo[]{ + return this._edits; + } + + public recordEdit(position: number, length: number, replaceWith: string): void { + this._edits.push(new TextEditInfo(position, length, replaceWith)); + } + + private recordIndentationEditsForToken(token: ISyntaxToken, indentationString: string, commentIndentationString: string) { + var position = this.position(); + var indentNextTokenOrTrivia = true; + var leadingWhiteSpace = ""; // We need to track the whitespace before a multiline comment + + // Process any leading trivia if any + var triviaList = token.leadingTrivia(); + if (triviaList) { + for (var i = 0, length = triviaList.count(); i < length; i++, position += trivia.fullWidth()) { + var trivia = triviaList.syntaxTriviaAt(i); + // Skip this trivia if it is not in the span + if (!this.textSpan().containsTextSpan(new TextSpan(position, trivia.fullWidth()))) { + continue; + } + + switch (trivia.kind()) { + case SyntaxKind.MultiLineCommentTrivia: + // We will only indent the first line of the multiline comment if we were planning to indent the next trivia. However, + // subsequent lines will always be indented + this.recordIndentationEditsForMultiLineComment(trivia, position, commentIndentationString, leadingWhiteSpace, !indentNextTokenOrTrivia /* already indented first line */); + indentNextTokenOrTrivia = false; + leadingWhiteSpace = ""; + break; + + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.SkippedTokenTrivia: + if (indentNextTokenOrTrivia) { + this.recordIndentationEditsForSingleLineOrSkippedText(trivia, position, commentIndentationString); + indentNextTokenOrTrivia = false; + } + break; + + case SyntaxKind.WhitespaceTrivia: + // If the next trivia is a comment, use the comment indentation level instead of the regular indentation level + // If the next trivia is a newline, this whole line is just whitespace, so don't do anything (trimming will take care of it) + var nextTrivia = length > i + 1 && triviaList.syntaxTriviaAt(i + 1); + var whiteSpaceIndentationString = nextTrivia && nextTrivia.isComment() ? commentIndentationString : indentationString; + if (indentNextTokenOrTrivia) { + if (!(nextTrivia && nextTrivia.isNewLine())) { + this.recordIndentationEditsForWhitespace(trivia, position, whiteSpaceIndentationString); + } + indentNextTokenOrTrivia = false; + } + leadingWhiteSpace += trivia.fullText(); + break; + + case SyntaxKind.NewLineTrivia: + // We hit a newline processing the trivia. We need to add the indentation to the + // next line as well. Note: don't bother indenting the newline itself. This will + // just insert ugly whitespace that most users probably will not want. + indentNextTokenOrTrivia = true; + leadingWhiteSpace = ""; + break; + + default: + throw Errors.invalidOperation(); + } + } + + } + + if (token.kind() !== SyntaxKind.EndOfFileToken && indentNextTokenOrTrivia) { + // If the last trivia item was a new line, or no trivia items were encounterd record the + // indentation edit at the token position + if (indentationString.length > 0) { + this.recordEdit(position, 0, indentationString); + } + } + } + + private recordIndentationEditsForSingleLineOrSkippedText(trivia: ISyntaxTrivia, fullStart: number, indentationString: string): void { + // Record the edit + if (indentationString.length > 0) { + this.recordEdit(fullStart, 0, indentationString); + } + } + + private recordIndentationEditsForWhitespace(trivia: ISyntaxTrivia, fullStart: number, indentationString: string): void { + var text = trivia.fullText(); + + // Check if the current indentation matches the desired indentation or not + if (indentationString === text) { + return; + } + + // Record the edit + this.recordEdit(fullStart, text.length, indentationString); + } + + private recordIndentationEditsForMultiLineComment(trivia: ISyntaxTrivia, fullStart: number, indentationString: string, leadingWhiteSpace: string, firstLineAlreadyIndented: boolean): void { + // If the multiline comment spans multiple lines, we need to add the right indent amount to + // each successive line segment as well. + var position = fullStart; + var segments = Syntax.splitMultiLineCommentTriviaIntoMultipleLines(trivia); + + if (segments.length <= 1) { + if (!firstLineAlreadyIndented) { + // Process the one-line multiline comment just like a single line comment + this.recordIndentationEditsForSingleLineOrSkippedText(trivia, fullStart, indentationString); + } + return; + } + + // Find number of columns in first segment + var whiteSpaceColumnsInFirstSegment = Indentation.columnForPositionInString(leadingWhiteSpace, leadingWhiteSpace.length, this.options); + + var indentationColumns = Indentation.columnForPositionInString(indentationString, indentationString.length, this.options); + var startIndex = 0; + if (firstLineAlreadyIndented) { + startIndex = 1; + position += segments[0].length; + } + for (var i = startIndex; i < segments.length; i++) { + var segment = segments[i]; + this.recordIndentationEditsForSegment(segment, position, indentationColumns, whiteSpaceColumnsInFirstSegment); + position += segment.length; + } + } + + private recordIndentationEditsForSegment(segment: string, fullStart: number, indentationColumns: number, whiteSpaceColumnsInFirstSegment: number): void { + // Indent subsequent lines using a column delta of the actual indentation relative to the first line + var firstNonWhitespacePosition = Indentation.firstNonWhitespacePosition(segment); + var leadingWhiteSpaceColumns = Indentation.columnForPositionInString(segment, firstNonWhitespacePosition, this.options); + var deltaFromFirstSegment = leadingWhiteSpaceColumns - whiteSpaceColumnsInFirstSegment; + var finalColumns = indentationColumns + deltaFromFirstSegment; + if (finalColumns < 0) { + finalColumns = 0; + } + var indentationString = Indentation.indentationString(finalColumns, this.options); + + if (firstNonWhitespacePosition < segment.length && + CharacterInfo.isLineTerminator(segment.charCodeAt(firstNonWhitespacePosition))) { + // If this segment was just a newline, then don't bother indenting it. That will just + // leave the user with an ugly indent in their output that they probably do not want. + return; + } + + if (indentationString === segment.substring(0, firstNonWhitespacePosition)) { + return; + } + + // Record the edit + this.recordEdit(fullStart, firstNonWhitespacePosition, indentationString); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/rule.ts b/src/services/formatting/new/rule.ts new file mode 100644 index 00000000000..f2550d40761 --- /dev/null +++ b/src/services/formatting/new/rule.ts @@ -0,0 +1,32 @@ +// +// 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 ts.formatting { + export class Rule { + constructor( + public Descriptor: RuleDescriptor, + public Operation: RuleOperation, + public Flag: RuleFlags = RuleFlags.None) { + } + + public toString() { + return "[desc=" + this.Descriptor + "," + + "operation=" + this.Operation + "," + + "flag=" + this.Flag + "]"; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/ruleAction.ts b/src/services/formatting/new/ruleAction.ts new file mode 100644 index 00000000000..aa80943ec3d --- /dev/null +++ b/src/services/formatting/new/ruleAction.ts @@ -0,0 +1,25 @@ +// +// 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 ts.formatting { + export enum RuleAction { + Ignore = 0x00000001, + Space = 0x00000002, + NewLine = 0x00000004, + Delete = 0x00000008 + } +} \ No newline at end of file diff --git a/src/services/formatting/new/ruleDescriptor.ts b/src/services/formatting/new/ruleDescriptor.ts new file mode 100644 index 00000000000..e93f035b5be --- /dev/null +++ b/src/services/formatting/new/ruleDescriptor.ts @@ -0,0 +1,46 @@ +// +// 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 ts.formatting { + export class RuleDescriptor { + constructor(public LeftTokenRange: Shared.TokenRange, public RightTokenRange: Shared.TokenRange) { + } + + public toString(): string { + return "[leftRange=" + this.LeftTokenRange + "," + + "rightRange=" + this.RightTokenRange + "]"; + } + + static create1(left: SyntaxKind, right: SyntaxKind): RuleDescriptor { + return RuleDescriptor.create4(Shared.TokenRange.FromToken(left), Shared.TokenRange.FromToken(right)); + } + + static create2(left: Shared.TokenRange, right: SyntaxKind): RuleDescriptor { + return RuleDescriptor.create4(left, Shared.TokenRange.FromToken(right)); + } + + static create3(left: SyntaxKind, right: Shared.TokenRange): RuleDescriptor + //: this(TokenRange.FromToken(left), right) + { + return RuleDescriptor.create4(Shared.TokenRange.FromToken(left), right); + } + + static create4(left: Shared.TokenRange, right: Shared.TokenRange): RuleDescriptor { + return new RuleDescriptor(left, right); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/ruleFlag.ts b/src/services/formatting/new/ruleFlag.ts new file mode 100644 index 00000000000..3b61bc78754 --- /dev/null +++ b/src/services/formatting/new/ruleFlag.ts @@ -0,0 +1,23 @@ +// +// 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 ts.formatting { + export enum RuleFlags { + None, + CanDeleteNewLines + } +} \ No newline at end of file diff --git a/src/services/formatting/new/ruleOperation.ts b/src/services/formatting/new/ruleOperation.ts new file mode 100644 index 00000000000..e0e75b6a711 --- /dev/null +++ b/src/services/formatting/new/ruleOperation.ts @@ -0,0 +1,44 @@ +// +// 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 ts.formatting { + export class RuleOperation { + public Context: RuleOperationContext; + public Action: RuleAction; + + constructor() { + this.Context = null; + this.Action = null; + } + + public toString(): string { + return "[context=" + this.Context + "," + + "action=" + this.Action + "]"; + } + + static create1(action: RuleAction) { + return RuleOperation.create2(RuleOperationContext.Any, action) + } + + static create2(context: RuleOperationContext, action: RuleAction) { + var result = new RuleOperation(); + result.Context = context; + result.Action = action; + return result; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/ruleOperationContext.ts b/src/services/formatting/new/ruleOperationContext.ts new file mode 100644 index 00000000000..2c2e8e8b5a4 --- /dev/null +++ b/src/services/formatting/new/ruleOperationContext.ts @@ -0,0 +1,47 @@ +// +// 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 ts.formatting { + + export class RuleOperationContext { + private customContextChecks: { (context: FormattingContext): boolean; }[]; + + constructor(...funcs: { (context: FormattingContext): boolean; }[]) { + this.customContextChecks = funcs; + } + + static Any: RuleOperationContext = new RuleOperationContext(); + + + public IsAny(): boolean { + return this == RuleOperationContext.Any; + } + + public InContext(context: FormattingContext): boolean { + if (this.IsAny()) { + return true; + } + + for (var i = 0, len = this.customContextChecks.length; i < len; i++) { + if (!this.customContextChecks[i](context)) { + return false; + } + } + return true; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/rules.ts b/src/services/formatting/new/rules.ts new file mode 100644 index 00000000000..aef884331b7 --- /dev/null +++ b/src/services/formatting/new/rules.ts @@ -0,0 +1,687 @@ +// +// 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 ts.formatting { + export class Rules { + public getRuleName(rule: Rule) { + var o: ts.Map = this; + for (var name in o) { + if (o[name] === rule) { + return name; + } + } + throw new Error(TypeScript.getDiagnosticMessage(TypeScript.DiagnosticCode.Unknown_rule, null)); + } + + [name: string]: any; + + public IgnoreBeforeComment: Rule; + public IgnoreAfterLineComment: Rule; + + // Space after keyword but not before ; or : or ? + public NoSpaceBeforeSemicolon: Rule; + public NoSpaceBeforeColon: Rule; + public NoSpaceBeforeQMark: Rule; + public SpaceAfterColon: Rule; + public SpaceAfterQMark: Rule; + public SpaceAfterSemicolon: Rule; + + // Space/new line after }. + public SpaceAfterCloseBrace: Rule; + + // Special case for (}, else) and (}, while) since else & while tokens are not part of the tree which makes SpaceAfterCloseBrace rule not applied + // Also should not apply to }) + public SpaceBetweenCloseBraceAndElse: Rule; + public SpaceBetweenCloseBraceAndWhile: Rule; + public NoSpaceAfterCloseBrace: Rule; + + // No space for indexer and dot + public NoSpaceBeforeDot: Rule; + public NoSpaceAfterDot: Rule; + public NoSpaceBeforeOpenBracket: Rule; + public NoSpaceAfterOpenBracket: Rule; + public NoSpaceBeforeCloseBracket: Rule; + public NoSpaceAfterCloseBracket: Rule; + + // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. + public SpaceAfterOpenBrace: Rule; + public SpaceBeforeCloseBrace: Rule; + public NoSpaceBetweenEmptyBraceBrackets: Rule; + + // Insert new line after { and before } in multi-line contexts. + public NewLineAfterOpenBraceInBlockContext: Rule; + + // For functions and control block place } on a new line [multi-line rule] + public NewLineBeforeCloseBraceInBlockContext: Rule; + + // Special handling of unary operators. + // Prefix operators generally shouldn't have a space between + // them and their target unary expression. + public NoSpaceAfterUnaryPrefixOperator: Rule; + public NoSpaceAfterUnaryPreincrementOperator: Rule; + public NoSpaceAfterUnaryPredecrementOperator: Rule; + public NoSpaceBeforeUnaryPostincrementOperator: Rule; + public NoSpaceBeforeUnaryPostdecrementOperator: Rule; + + // More unary operator special-casing. + // DevDiv 181814: Be careful when removing leading whitespace + // around unary operators. Examples: + // 1 - -2 --X--> 1--2 + // a + ++b --X--> a+++b + public SpaceAfterPostincrementWhenFollowedByAdd: Rule; + public SpaceAfterAddWhenFollowedByUnaryPlus: Rule; + public SpaceAfterAddWhenFollowedByPreincrement: Rule; + public SpaceAfterPostdecrementWhenFollowedBySubtract: Rule; + public SpaceAfterSubtractWhenFollowedByUnaryMinus: Rule; + public SpaceAfterSubtractWhenFollowedByPredecrement: Rule; + + public NoSpaceBeforeComma: Rule; + + public SpaceAfterCertainKeywords: Rule; + public NoSpaceBeforeOpenParenInFuncCall: Rule; + public SpaceAfterFunctionInFuncDecl: Rule; + public NoSpaceBeforeOpenParenInFuncDecl: Rule; + public SpaceAfterVoidOperator: Rule; + + public NoSpaceBetweenReturnAndSemicolon: Rule; + + // Add a space between statements. All keywords except (do,else,case) has open/close parens after them. + // So, we have a rule to add a space for [),Any], [do,Any], [else,Any], and [case,Any] + public SpaceBetweenStatements: Rule; + + // This low-pri rule takes care of "try {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. + public SpaceAfterTryFinally: Rule; + + // For get/set members, we check for (identifier,identifier) since get/set don't have tokens and they are represented as just an identifier token. + // Though, we do extra check on the context to make sure we are dealing with get/set node. Example: + // get x() {} + // set x(val) {} + public SpaceAfterGetSetInMember: Rule; + + // Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options. + public SpaceBeforeBinaryKeywordOperator: Rule; + public SpaceAfterBinaryKeywordOperator: Rule; + + // TypeScript-specific rules + + // Treat constructor as an identifier in a function declaration, and remove spaces between constructor and following left parentheses + public NoSpaceAfterConstructor: Rule; + + // Use of module as a function call. e.g.: import m2 = module("m2"); + public NoSpaceAfterModuleImport: Rule; + + // Add a space around certain TypeScript keywords + public SpaceAfterCertainTypeScriptKeywords: Rule; + public SpaceBeforeCertainTypeScriptKeywords: Rule; + + // Treat string literals in module names as identifiers, and add a space between the literal and the opening Brace braces, e.g.: module "m2" { + public SpaceAfterModuleName: Rule; + + // Lambda expressions + public SpaceAfterArrow: Rule; + + // Optional parameters and var args + public NoSpaceAfterEllipsis: Rule; + public NoSpaceAfterOptionalParameters: Rule; + + // generics + public NoSpaceBeforeOpenAngularBracket: Rule; + public NoSpaceBetweenCloseParenAndAngularBracket: Rule; + public NoSpaceAfterOpenAngularBracket: Rule; + public NoSpaceBeforeCloseAngularBracket: Rule; + public NoSpaceAfterCloseAngularBracket: Rule; + + // Remove spaces in empty interface literals. e.g.: x: {} + public NoSpaceBetweenEmptyInterfaceBraceBrackets: Rule; + + // These rules are higher in priority than user-configurable rules. + public HighPriorityCommonRules: Rule[]; + + // These rules are lower in priority than user-configurable rules. + public LowPriorityCommonRules: Rule[]; + + /// + /// Rules controlled by user options + /// + + // Insert space after comma delimiter + public SpaceAfterComma: Rule; + public NoSpaceAfterComma: Rule; + + // Insert space before and after binary operators + public SpaceBeforeBinaryOperator: Rule; + public SpaceAfterBinaryOperator: Rule; + public NoSpaceBeforeBinaryOperator: Rule; + public NoSpaceAfterBinaryOperator: Rule; + + // Insert space after keywords in control flow statements + public SpaceAfterKeywordInControl: Rule; + public NoSpaceAfterKeywordInControl: Rule; + + // Open Brace braces after function + //TypeScript: Function can have return types, which can be made of tons of different token kinds + public FunctionOpenBraceLeftTokenRange: Shared.TokenRange; + public SpaceBeforeOpenBraceInFunction: Rule; + public NewLineBeforeOpenBraceInFunction: Rule; + + // Open Brace braces after TypeScript module/class/interface + public TypeScriptOpenBraceLeftTokenRange: Shared.TokenRange; + public SpaceBeforeOpenBraceInTypeScriptDeclWithBlock: Rule; + public NewLineBeforeOpenBraceInTypeScriptDeclWithBlock: Rule; + + // Open Brace braces after control block + public ControlOpenBraceLeftTokenRange: Shared.TokenRange; + public SpaceBeforeOpenBraceInControl: Rule; + public NewLineBeforeOpenBraceInControl: Rule; + + // Insert space after semicolon in for statement + public SpaceAfterSemicolonInFor: Rule; + public NoSpaceAfterSemicolonInFor: Rule; + + // Insert space after opening and before closing nonempty parenthesis + public SpaceAfterOpenParen: Rule; + public SpaceBeforeCloseParen: Rule; + public NoSpaceBetweenParens: Rule; + public NoSpaceAfterOpenParen: Rule; + public NoSpaceBeforeCloseParen: Rule; + + // Insert space after function keyword for anonymous functions + public SpaceAfterAnonymousFunctionKeyword: Rule; + public NoSpaceAfterAnonymousFunctionKeyword: Rule; + + constructor() { + /// + /// Common Rules + /// + + // Leave comments alone + this.IgnoreBeforeComment = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.Comments), RuleOperation.create1(RuleAction.Ignore)); + this.IgnoreAfterLineComment = new Rule(RuleDescriptor.create3(SyntaxKind.SingleLineCommentTrivia, Shared.TokenRange.Any), RuleOperation.create1(RuleAction.Ignore)); + + // Space after keyword but not before ; or : or ? + this.NoSpaceBeforeSemicolon = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.SemicolonToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeColon = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.ColonToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Delete)); + this.NoSpaceBeforeQMark = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.QuestionToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Delete)); + this.SpaceAfterColon = new Rule(RuleDescriptor.create3(SyntaxKind.ColonToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Space)); + this.SpaceAfterQMark = new Rule(RuleDescriptor.create3(SyntaxKind.QuestionToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Space)); + this.SpaceAfterSemicolon = new Rule(RuleDescriptor.create3(SyntaxKind.SemicolonToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + + // Space after }. + this.SpaceAfterCloseBrace = new Rule(RuleDescriptor.create3(SyntaxKind.CloseBraceToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsAfterCodeBlockContext), RuleAction.Space)); + + // Special case for (}, else) and (}, while) since else & while tokens are not part of the tree which makes SpaceAfterCloseBrace rule not applied + this.SpaceBetweenCloseBraceAndElse = new Rule(RuleDescriptor.create1(SyntaxKind.CloseBraceToken, SyntaxKind.ElseKeyword), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.SpaceBetweenCloseBraceAndWhile = new Rule(RuleDescriptor.create1(SyntaxKind.CloseBraceToken, SyntaxKind.WhileKeyword), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.NoSpaceAfterCloseBrace = new Rule(RuleDescriptor.create3(SyntaxKind.CloseBraceToken, Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken, SyntaxKind.SemicolonToken])), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // No space for indexer and dot + this.NoSpaceBeforeDot = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.DotToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterDot = new Rule(RuleDescriptor.create3(SyntaxKind.DotToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeOpenBracket = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.OpenBracketToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterOpenBracket = new Rule(RuleDescriptor.create3(SyntaxKind.OpenBracketToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeCloseBracket = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.CloseBracketToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterCloseBracket = new Rule(RuleDescriptor.create3(SyntaxKind.CloseBracketToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Place a space before open brace in a function declaration + this.FunctionOpenBraceLeftTokenRange = Shared.TokenRange.AnyIncludingMultilineComments; + this.SpaceBeforeOpenBraceInFunction = new Rule(RuleDescriptor.create2(this.FunctionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines); + + // Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc) + this.TypeScriptOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia]); + this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsTypeScriptDeclWithBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines); + + // Place a space before open brace in a control flow construct + this.ControlOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.DoKeyword, SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword, SyntaxKind.ElseKeyword]); + this.SpaceBeforeOpenBraceInControl = new Rule(RuleDescriptor.create2(this.ControlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines); + + // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. + this.SpaceAfterOpenBrace = new Rule(RuleDescriptor.create3(SyntaxKind.OpenBraceToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSingleLineBlockContext), RuleAction.Space)); + this.SpaceBeforeCloseBrace = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.CloseBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSingleLineBlockContext), RuleAction.Space)); + this.NoSpaceBetweenEmptyBraceBrackets = new Rule(RuleDescriptor.create1(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsObjectContext), RuleAction.Delete)); + + // Insert new line after { and before } in multi-line contexts. + this.NewLineAfterOpenBraceInBlockContext = new Rule(RuleDescriptor.create3(SyntaxKind.OpenBraceToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsMultilineBlockContext), RuleAction.NewLine)); + + // For functions and control block place } on a new line [multi-line rule] + this.NewLineBeforeCloseBraceInBlockContext = new Rule(RuleDescriptor.create2(Shared.TokenRange.AnyIncludingMultilineComments, SyntaxKind.CloseBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsMultilineBlockContext), RuleAction.NewLine)); + + // Special handling of unary operators. + // Prefix operators generally shouldn't have a space between + // them and their target unary expression. + this.NoSpaceAfterUnaryPrefixOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.UnaryPrefixOperators, Shared.TokenRange.UnaryPrefixExpressions), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Delete)); + this.NoSpaceAfterUnaryPreincrementOperator = new Rule(RuleDescriptor.create3(SyntaxKind.PlusPlusToken, Shared.TokenRange.UnaryPreincrementExpressions), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterUnaryPredecrementOperator = new Rule(RuleDescriptor.create3(SyntaxKind.MinusMinusToken, Shared.TokenRange.UnaryPredecrementExpressions), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeUnaryPostincrementOperator = new Rule(RuleDescriptor.create2(Shared.TokenRange.UnaryPostincrementExpressions, SyntaxKind.PlusPlusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeUnaryPostdecrementOperator = new Rule(RuleDescriptor.create2(Shared.TokenRange.UnaryPostdecrementExpressions, SyntaxKind.MinusMinusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // More unary operator special-casing. + // DevDiv 181814: Be careful when removing leading whitespace + // around unary operators. Examples: + // 1 - -2 --X--> 1--2 + // a + ++b --X--> a+++b + this.SpaceAfterPostincrementWhenFollowedByAdd = new Rule(RuleDescriptor.create1(SyntaxKind.PlusPlusToken, SyntaxKind.PlusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterAddWhenFollowedByUnaryPlus = new Rule(RuleDescriptor.create1(SyntaxKind.PlusToken, SyntaxKind.PlusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterAddWhenFollowedByPreincrement = new Rule(RuleDescriptor.create1(SyntaxKind.PlusToken, SyntaxKind.PlusPlusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterPostdecrementWhenFollowedBySubtract = new Rule(RuleDescriptor.create1(SyntaxKind.MinusMinusToken, SyntaxKind.MinusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterSubtractWhenFollowedByUnaryMinus = new Rule(RuleDescriptor.create1(SyntaxKind.MinusToken, SyntaxKind.MinusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterSubtractWhenFollowedByPredecrement = new Rule(RuleDescriptor.create1(SyntaxKind.MinusToken, SyntaxKind.MinusMinusToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + + this.NoSpaceBeforeComma = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.CommaToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + this.SpaceAfterCertainKeywords = new Rule(RuleDescriptor.create4(Shared.TokenRange.FromTokens([SyntaxKind.VarKeyword, SyntaxKind.ThrowKeyword, SyntaxKind.NewKeyword, SyntaxKind.DeleteKeyword, SyntaxKind.ReturnKeyword, SyntaxKind.TypeOfKeyword]), Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.NoSpaceBeforeOpenParenInFuncCall = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsFunctionCallOrNewContext), RuleAction.Delete)); + this.SpaceAfterFunctionInFuncDecl = new Rule(RuleDescriptor.create3(SyntaxKind.FunctionKeyword, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Space)); + this.NoSpaceBeforeOpenParenInFuncDecl = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsFunctionDeclContext), RuleAction.Delete)); + this.SpaceAfterVoidOperator = new Rule(RuleDescriptor.create3(SyntaxKind.VoidKeyword, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsVoidOpContext), RuleAction.Space)); + + this.NoSpaceBetweenReturnAndSemicolon = new Rule(RuleDescriptor.create1(SyntaxKind.ReturnKeyword, SyntaxKind.SemicolonToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Add a space between statements. All keywords except (do,else,case) has open/close parens after them. + // So, we have a rule to add a space for [),Any], [do,Any], [else,Any], and [case,Any] + this.SpaceBetweenStatements = new Rule(RuleDescriptor.create4(Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.DoKeyword, SyntaxKind.ElseKeyword, SyntaxKind.CaseKeyword]), Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotForContext), RuleAction.Space)); + + // This low-pri rule takes care of "try {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. + this.SpaceAfterTryFinally = new Rule(RuleDescriptor.create2(Shared.TokenRange.FromTokens([SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword]), SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + + // get x() {} + // set x(val) {} + this.SpaceAfterGetSetInMember = new Rule(RuleDescriptor.create2(Shared.TokenRange.FromTokens([SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]), SyntaxKind.Identifier), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Space)); + + // Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options. + this.SpaceBeforeBinaryKeywordOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.BinaryKeywordOperators), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterBinaryKeywordOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.BinaryKeywordOperators, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + + // TypeScript-specific higher priority rules + + // Treat constructor as an identifier in a function declaration, and remove spaces between constructor and following left parentheses + this.NoSpaceAfterConstructor = new Rule(RuleDescriptor.create1(SyntaxKind.ConstructorKeyword, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Use of module as a function call. e.g.: import m2 = module("m2"); + this.NoSpaceAfterModuleImport = new Rule(RuleDescriptor.create2(Shared.TokenRange.FromTokens([SyntaxKind.ModuleKeyword, SyntaxKind.RequireKeyword]), SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Add a space around certain TypeScript keywords + this.SpaceAfterCertainTypeScriptKeywords = new Rule(RuleDescriptor.create4(Shared.TokenRange.FromTokens([SyntaxKind.ClassKeyword, SyntaxKind.DeclareKeyword, SyntaxKind.EnumKeyword, SyntaxKind.ExportKeyword, SyntaxKind.ExtendsKeyword, SyntaxKind.GetKeyword, SyntaxKind.ImplementsKeyword, SyntaxKind.ImportKeyword, SyntaxKind.InterfaceKeyword, SyntaxKind.ModuleKeyword, SyntaxKind.PrivateKeyword, SyntaxKind.PublicKeyword, SyntaxKind.SetKeyword, SyntaxKind.StaticKeyword]), Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.SpaceBeforeCertainTypeScriptKeywords = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.FromTokens([SyntaxKind.ExtendsKeyword, SyntaxKind.ImplementsKeyword])), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + + // Treat string literals in module names as identifiers, and add a space between the literal and the opening Brace braces, e.g.: module "m2" { + this.SpaceAfterModuleName = new Rule(RuleDescriptor.create1(SyntaxKind.StringLiteral, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsModuleDeclContext), RuleAction.Space)); + + // Lambda expressions + this.SpaceAfterArrow = new Rule(RuleDescriptor.create3(SyntaxKind.EqualsGreaterThanToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + + // Optional parameters and var args + this.NoSpaceAfterEllipsis = new Rule(RuleDescriptor.create1(SyntaxKind.DotDotDotToken, SyntaxKind.Identifier), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterOptionalParameters = new Rule(RuleDescriptor.create3(SyntaxKind.QuestionToken, Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.CommaToken])), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsNotBinaryOpContext), RuleAction.Delete)); + + // generics + this.NoSpaceBeforeOpenAngularBracket = new Rule(RuleDescriptor.create2(Shared.TokenRange.TypeNames, SyntaxKind.LessThanToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsTypeArgumentOrParameterContext), RuleAction.Delete)); + this.NoSpaceBetweenCloseParenAndAngularBracket = new Rule(RuleDescriptor.create1(SyntaxKind.CloseParenToken, SyntaxKind.LessThanToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsTypeArgumentOrParameterContext), RuleAction.Delete)); + this.NoSpaceAfterOpenAngularBracket = new Rule(RuleDescriptor.create3(SyntaxKind.LessThanToken, Shared.TokenRange.TypeNames), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsTypeArgumentOrParameterContext), RuleAction.Delete)); + this.NoSpaceBeforeCloseAngularBracket = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.GreaterThanToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsTypeArgumentOrParameterContext), RuleAction.Delete)); + this.NoSpaceAfterCloseAngularBracket = new Rule(RuleDescriptor.create3(SyntaxKind.GreaterThanToken, Shared.TokenRange.FromTokens([SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.GreaterThanToken, SyntaxKind.CommaToken])), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsTypeArgumentOrParameterContext), RuleAction.Delete)); + + // Remove spaces in empty interface literals. e.g.: x: {} + this.NoSpaceBetweenEmptyInterfaceBraceBrackets = new Rule(RuleDescriptor.create1(SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsObjectTypeContext), RuleAction.Delete)); + + // These rules are higher in priority than user-configurable rules. + this.HighPriorityCommonRules = + [ + this.IgnoreBeforeComment, this.IgnoreAfterLineComment, + this.NoSpaceBeforeColon, this.SpaceAfterColon, this.NoSpaceBeforeQMark, this.SpaceAfterQMark, + this.NoSpaceBeforeDot, this.NoSpaceAfterDot, + this.NoSpaceAfterUnaryPrefixOperator, + this.NoSpaceAfterUnaryPreincrementOperator, this.NoSpaceAfterUnaryPredecrementOperator, + this.NoSpaceBeforeUnaryPostincrementOperator, this.NoSpaceBeforeUnaryPostdecrementOperator, + this.SpaceAfterPostincrementWhenFollowedByAdd, + this.SpaceAfterAddWhenFollowedByUnaryPlus, this.SpaceAfterAddWhenFollowedByPreincrement, + this.SpaceAfterPostdecrementWhenFollowedBySubtract, + this.SpaceAfterSubtractWhenFollowedByUnaryMinus, this.SpaceAfterSubtractWhenFollowedByPredecrement, + this.NoSpaceAfterCloseBrace, + this.SpaceAfterOpenBrace, this.SpaceBeforeCloseBrace, this.NewLineBeforeCloseBraceInBlockContext, + this.SpaceAfterCloseBrace, this.SpaceBetweenCloseBraceAndElse, this.SpaceBetweenCloseBraceAndWhile, this.NoSpaceBetweenEmptyBraceBrackets, + this.SpaceAfterFunctionInFuncDecl, this.NewLineAfterOpenBraceInBlockContext, this.SpaceAfterGetSetInMember, + this.NoSpaceBetweenReturnAndSemicolon, + this.SpaceAfterCertainKeywords, + this.NoSpaceBeforeOpenParenInFuncCall, + this.SpaceBeforeBinaryKeywordOperator, this.SpaceAfterBinaryKeywordOperator, + this.SpaceAfterVoidOperator, + + // TypeScript-specific rules + this.NoSpaceAfterConstructor, this.NoSpaceAfterModuleImport, + this.SpaceAfterCertainTypeScriptKeywords, this.SpaceBeforeCertainTypeScriptKeywords, + this.SpaceAfterModuleName, + this.SpaceAfterArrow, + this.NoSpaceAfterEllipsis, + this.NoSpaceAfterOptionalParameters, + this.NoSpaceBetweenEmptyInterfaceBraceBrackets, + this.NoSpaceBeforeOpenAngularBracket, + this.NoSpaceBetweenCloseParenAndAngularBracket, + this.NoSpaceAfterOpenAngularBracket, + this.NoSpaceBeforeCloseAngularBracket, + this.NoSpaceAfterCloseAngularBracket + ]; + + // These rules are lower in priority than user-configurable rules. + this.LowPriorityCommonRules = + [ + this.NoSpaceBeforeSemicolon, + this.SpaceBeforeOpenBraceInControl, this.SpaceBeforeOpenBraceInFunction, this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock, + this.NoSpaceBeforeComma, + this.NoSpaceBeforeOpenBracket, this.NoSpaceAfterOpenBracket, + this.NoSpaceBeforeCloseBracket, this.NoSpaceAfterCloseBracket, + this.SpaceAfterSemicolon, + this.NoSpaceBeforeOpenParenInFuncDecl, + this.SpaceBetweenStatements, this.SpaceAfterTryFinally + ]; + + /// + /// Rules controlled by user options + /// + + // Insert space after comma delimiter + this.SpaceAfterComma = new Rule(RuleDescriptor.create3(SyntaxKind.CommaToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.NoSpaceAfterComma = new Rule(RuleDescriptor.create3(SyntaxKind.CommaToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Insert space before and after binary operators + this.SpaceBeforeBinaryOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.BinaryOperators), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.SpaceAfterBinaryOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.BinaryOperators, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Space)); + this.NoSpaceBeforeBinaryOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.Any, Shared.TokenRange.BinaryOperators), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Delete)); + this.NoSpaceAfterBinaryOperator = new Rule(RuleDescriptor.create4(Shared.TokenRange.BinaryOperators, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsBinaryOpContext), RuleAction.Delete)); + + // Insert space after keywords in control flow statements + this.SpaceAfterKeywordInControl = new Rule(RuleDescriptor.create2(Shared.TokenRange.Keywords, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext), RuleAction.Space)); + this.NoSpaceAfterKeywordInControl = new Rule(RuleDescriptor.create2(Shared.TokenRange.Keywords, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext), RuleAction.Delete)); + + // Open Brace braces after function + //TypeScript: Function can have return types, which can be made of tons of different token kinds + this.NewLineBeforeOpenBraceInFunction = new Rule(RuleDescriptor.create2(this.FunctionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext, Rules.IsBeforeMultilineBlockContext), RuleAction.NewLine), RuleFlags.CanDeleteNewLines); + + // Open Brace braces after TypeScript module/class/interface + this.NewLineBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsTypeScriptDeclWithBlockContext, Rules.IsBeforeMultilineBlockContext), RuleAction.NewLine), RuleFlags.CanDeleteNewLines); + + // Open Brace braces after control block + this.NewLineBeforeOpenBraceInControl = new Rule(RuleDescriptor.create2(this.ControlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext, Rules.IsBeforeMultilineBlockContext), RuleAction.NewLine), RuleFlags.CanDeleteNewLines); + + // Insert space after semicolon in for statement + this.SpaceAfterSemicolonInFor = new Rule(RuleDescriptor.create3(SyntaxKind.SemicolonToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsForContext), RuleAction.Space)); + this.NoSpaceAfterSemicolonInFor = new Rule(RuleDescriptor.create3(SyntaxKind.SemicolonToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext, Rules.IsForContext), RuleAction.Delete)); + + // Insert space after opening and before closing nonempty parenthesis + this.SpaceAfterOpenParen = new Rule(RuleDescriptor.create3(SyntaxKind.OpenParenToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.SpaceBeforeCloseParen = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.CloseParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Space)); + this.NoSpaceBetweenParens = new Rule(RuleDescriptor.create1(SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceAfterOpenParen = new Rule(RuleDescriptor.create3(SyntaxKind.OpenParenToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + this.NoSpaceBeforeCloseParen = new Rule(RuleDescriptor.create2(Shared.TokenRange.Any, SyntaxKind.CloseParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsSameLineTokenContext), RuleAction.Delete)); + + // Insert space after function keyword for anonymous functions + this.SpaceAfterAnonymousFunctionKeyword = new Rule(RuleDescriptor.create1(SyntaxKind.FunctionKeyword, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Space)); + this.NoSpaceAfterAnonymousFunctionKeyword = new Rule(RuleDescriptor.create1(SyntaxKind.FunctionKeyword, SyntaxKind.OpenParenToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext), RuleAction.Delete)); + } + + /// + /// Contexts + /// + + static IsForContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ForStatement; + } + + static IsNotForContext(context: FormattingContext): boolean { + return !Rules.IsForContext(context); + } + + static IsBinaryOpContext(context: FormattingContext): boolean { + + switch (context.contextNode.kind) { + case SyntaxKind.BinaryExpression: + return true; + //// binary expressions + //case SyntaxKind.AssignmentExpression: + //case SyntaxKind.AddAssignmentExpression: + //case SyntaxKind.SubtractAssignmentExpression: + //case SyntaxKind.MultiplyAssignmentExpression: + //case SyntaxKind.DivideAssignmentExpression: + //case SyntaxKind.ModuloAssignmentExpression: + //case SyntaxKind.AndAssignmentExpression: + //case SyntaxKind.ExclusiveOrAssignmentExpression: + //case SyntaxKind.OrAssignmentExpression: + //case SyntaxKind.LeftShiftAssignmentExpression: + //case SyntaxKind.SignedRightShiftAssignmentExpression: + //case SyntaxKind.UnsignedRightShiftAssignmentExpression: + //case SyntaxKind.ConditionalExpression: + //case SyntaxKind.LogicalOrExpression: + //case SyntaxKind.LogicalAndExpression: + //case SyntaxKind.BitwiseOrExpression: + //case SyntaxKind.BitwiseExclusiveOrExpression: + //case SyntaxKind.BitwiseAndExpression: + //case SyntaxKind.EqualsWithTypeConversionExpression: + //case SyntaxKind.NotEqualsWithTypeConversionExpression: + //case SyntaxKind.EqualsExpression: + //case SyntaxKind.NotEqualsExpression: + //case SyntaxKind.LessThanExpression: + //case SyntaxKind.GreaterThanExpression: + //case SyntaxKind.LessThanOrEqualExpression: + //case SyntaxKind.GreaterThanOrEqualExpression: + //case SyntaxKind.InstanceOfExpression: + //case SyntaxKind.InExpression: + //case SyntaxKind.LeftShiftExpression: + //case SyntaxKind.SignedRightShiftExpression: + //case SyntaxKind.UnsignedRightShiftExpression: + //case SyntaxKind.MultiplyExpression: + //case SyntaxKind.DivideExpression: + //case SyntaxKind.ModuloExpression: + //case SyntaxKind.AddExpression: + //case SyntaxKind.SubtractExpression: + // return true; + + // equal in import a = module('a'); + case SyntaxKind.ImportDeclaration: + // equal in var a = 0; + case SyntaxKind.VariableDeclaration: + // TODO: + //case SyntaxKind.EqualsValueClause: + return context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; + // "in" keyword in for (var x in []) { } + case SyntaxKind.ForInStatement: + return context.currentTokenSpan.kind === SyntaxKind.InKeyword || context.nextTokenSpan.kind === SyntaxKind.InKeyword; + } + return false; + } + + static IsNotBinaryOpContext(context: FormattingContext): boolean { + return !Rules.IsBinaryOpContext(context); + } + + static IsSameLineTokenOrBeforeMultilineBlockContext(context: FormattingContext): boolean { + //// This check is mainly used inside SpaceBeforeOpenBraceInControl and SpaceBeforeOpenBraceInFunction. + //// + //// Ex: + //// if (1) { .... + //// * ) and { are on the same line so apply the rule. Here we don't care whether it's same or multi block context + //// + //// Ex: + //// if (1) + //// { ... } + //// * ) and { are on differnet lines. We only need to format if the block is multiline context. So in this case we don't format. + //// + //// Ex: + //// if (1) + //// { ... + //// } + //// * ) and { are on differnet lines. We only need to format if the block is multiline context. So in this case we format. + + return context.TokensAreOnSameLine() || Rules.IsBeforeMultilineBlockContext(context); + } + + // This check is done before an open brace in a control construct, a function, or a typescript block declaration + static IsBeforeMultilineBlockContext(context: FormattingContext): boolean { + return Rules.IsBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine()); + } + + static IsMultilineBlockContext(context: FormattingContext): boolean { + return Rules.IsBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); + } + + static IsSingleLineBlockContext(context: FormattingContext): boolean { + return Rules.IsBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); + } + + static IsBlockContext(context: FormattingContext): boolean { + return Rules.NodeIsBlockContext(context.contextNode); + } + + static IsBeforeBlockContext(context: FormattingContext): boolean { + return Rules.NodeIsBlockContext(context.nextTokenParent); + } + + // IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children + static NodeIsBlockContext(node: Node): boolean { + if (Rules.NodeIsTypeScriptDeclWithBlockContext(node)) { + // This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc). + return true; + } + + switch (node.kind) { + case SyntaxKind.Block: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ObjectLiteral: + return true; + } + + return false; + } + + static IsFunctionDeclContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.Method: + //case SyntaxKind.MemberFunctionDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + ///case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Constructor: + case SyntaxKind.ArrowFunction: + //case SyntaxKind.ConstructorDeclaration: + //case SyntaxKind.SimpleArrowFunctionExpression: + //case SyntaxKind.ParenthesizedArrowFunctionExpression: + case SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one + return true; + } + + return false; + } + + static IsTypeScriptDeclWithBlockContext(context: FormattingContext): boolean { + return Rules.NodeIsTypeScriptDeclWithBlockContext(context.contextNode); + } + + static NodeIsTypeScriptDeclWithBlockContext(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeLiteral: + case SyntaxKind.ModuleDeclaration: + return true; + } + + return false; + } + + static IsAfterCodeBlockContext(context: FormattingContext): boolean { + switch (context.currentTokenParent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.Block: + case SyntaxKind.SwitchStatement: + return true; + } + return false; + } + + static IsControlDeclContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WithStatement: + // TODO + // case SyntaxKind.ElseClause: + case SyntaxKind.CatchBlock: + case SyntaxKind.FinallyBlock: + return true; + + default: + return false; + } + } + + static IsObjectContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ObjectLiteral; + } + + static IsFunctionCallContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.CallExpression; + } + + static IsNewContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.NewExpression; + } + + static IsFunctionCallOrNewContext(context: FormattingContext): boolean { + return Rules.IsFunctionCallContext(context) || Rules.IsNewContext(context); + } + + static IsSameLineTokenContext(context: FormattingContext): boolean { + return context.TokensAreOnSameLine(); + } + + static IsNotFormatOnEnter(context: FormattingContext): boolean { + return context.formattingRequestKind != FormattingRequestKind.FormatOnEnter; + } + + static IsModuleDeclContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ModuleDeclaration; + } + + static IsObjectTypeContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.TypeLiteral;// && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; + } + + static IsTypeArgumentOrParameter(tokenKind: SyntaxKind, parentKind: SyntaxKind): boolean { + return; + //return ((tokenKind === SyntaxKind.LessThanToken || tokenKind === SyntaxKind.GreaterThanToken) && + // (parentKind === SyntaxKind.TypeParameterList || parentKind === SyntaxKind.TypeArgumentList)); + } + + static IsTypeArgumentOrParameterContext(context: FormattingContext): boolean { + return Rules.IsTypeArgumentOrParameter(context.currentTokenSpan.kind, context.currentTokenParent.kind) || + Rules.IsTypeArgumentOrParameter(context.nextTokenSpan.kind, context.nextTokenParent.kind); + } + + static IsVoidOpContext(context: FormattingContext): boolean { + return; + //return context.currentTokenSpan.token.kind === SyntaxKind.VoidKeyword && context.currentTokenParent.kind() === SyntaxKind.VoidExpression; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/rulesMap.ts b/src/services/formatting/new/rulesMap.ts new file mode 100644 index 00000000000..6aa472d8d1d --- /dev/null +++ b/src/services/formatting/new/rulesMap.ts @@ -0,0 +1,189 @@ +// +// 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 ts.formatting { + export class RulesMap { + public map: RulesBucket[]; + public mapRowLength: number; + + constructor() { + this.map = []; + this.mapRowLength = 0; + } + + static create(rules: Rule[]): RulesMap { + var result = new RulesMap(); + result.Initialize(rules); + return result; + } + + public Initialize(rules: Rule[]) { + this.mapRowLength = SyntaxKind.LastToken + 1; + this.map = new Array(this.mapRowLength * this.mapRowLength);//new Array(this.mapRowLength * this.mapRowLength); + + // This array is used only during construction of the rulesbucket in the map + var rulesBucketConstructionStateList: RulesBucketConstructionState[] = new Array(this.map.length);//new Array(this.map.length); + + this.FillRules(rules, rulesBucketConstructionStateList); + return this.map; + } + + public FillRules(rules: Rule[], rulesBucketConstructionStateList: RulesBucketConstructionState[]): void { + rules.forEach((rule) => { + this.FillRule(rule, rulesBucketConstructionStateList); + }); + } + + private GetRuleBucketIndex(row: number, column: number): number { + var rulesBucketIndex = (row * this.mapRowLength) + column; + //Debug.Assert(rulesBucketIndex < this.map.Length, "Trying to access an index outside the array."); + return rulesBucketIndex; + } + + private FillRule(rule: Rule, rulesBucketConstructionStateList: RulesBucketConstructionState[]): void { + var specificRule = rule.Descriptor.LeftTokenRange != Shared.TokenRange.Any && + rule.Descriptor.RightTokenRange != Shared.TokenRange.Any; + + rule.Descriptor.LeftTokenRange.GetTokens().forEach((left) => { + rule.Descriptor.RightTokenRange.GetTokens().forEach((right) => { + var rulesBucketIndex = this.GetRuleBucketIndex(left, right); + + var rulesBucket = this.map[rulesBucketIndex]; + if (rulesBucket == undefined) { + rulesBucket = this.map[rulesBucketIndex] = new RulesBucket(); + } + + rulesBucket.AddRule(rule, specificRule, rulesBucketConstructionStateList, rulesBucketIndex); + }) + }) + } + + public GetRule(context: FormattingContext): Rule { + var bucketIndex = this.GetRuleBucketIndex(context.currentTokenSpan.kind, context.nextTokenSpan.kind); + var bucket = this.map[bucketIndex]; + if (bucket != null) { + for (var i = 0, len = bucket.Rules().length; i < len; i++) { + var rule = bucket.Rules()[i]; + if (rule.Operation.Context.InContext(context)) + return rule; + } + } + return null; + } + } + + var MaskBitSize = 5; + var Mask = 0x1f; + + export enum RulesPosition { + IgnoreRulesSpecific = 0, + IgnoreRulesAny = MaskBitSize * 1, + ContextRulesSpecific = MaskBitSize * 2, + ContextRulesAny = MaskBitSize * 3, + NoContextRulesSpecific = MaskBitSize * 4, + NoContextRulesAny = MaskBitSize * 5 + } + + export class RulesBucketConstructionState { + private rulesInsertionIndexBitmap: number; + + constructor() { + //// The Rules list contains all the inserted rules into a rulebucket in the following order: + //// 1- Ignore rules with specific token combination + //// 2- Ignore rules with any token combination + //// 3- Context rules with specific token combination + //// 4- Context rules with any token combination + //// 5- Non-context rules with specific token combination + //// 6- Non-context rules with any token combination + //// + //// The member rulesInsertionIndexBitmap is used to describe the number of rules + //// in each sub-bucket (above) hence can be used to know the index of where to insert + //// the next rule. It's a bitmap which contains 6 different sections each is given 5 bits. + //// + //// Example: + //// In order to insert a rule to the end of sub-bucket (3), we get the index by adding + //// the values in the bitmap segments 3rd, 2nd, and 1st. + this.rulesInsertionIndexBitmap = 0; + } + + public GetInsertionIndex(maskPosition: RulesPosition): number { + var index = 0; + + var pos = 0; + var indexBitmap = this.rulesInsertionIndexBitmap; + + while (pos <= maskPosition) { + index += (indexBitmap & Mask); + indexBitmap >>= MaskBitSize; + pos += MaskBitSize; + } + + return index; + } + + public IncreaseInsertionIndex(maskPosition: RulesPosition): void { + var value = (this.rulesInsertionIndexBitmap >> maskPosition) & Mask; + value++; + Debug.assert((value & Mask) == value, "Adding more rules into the sub-bucket than allowed. Maximum allowed is 32 rules."); + + var temp = this.rulesInsertionIndexBitmap & ~(Mask << maskPosition); + temp |= value << maskPosition; + + this.rulesInsertionIndexBitmap = temp; + } + } + + export class RulesBucket { + private rules: Rule[]; + + constructor() { + this.rules = []; + } + + public Rules(): Rule[] { + return this.rules; + } + + public AddRule(rule: Rule, specificTokens: boolean, constructionState: RulesBucketConstructionState[], rulesBucketIndex: number): void { + var position: RulesPosition; + + if (rule.Operation.Action == RuleAction.Ignore) { + position = specificTokens ? + RulesPosition.IgnoreRulesSpecific : + RulesPosition.IgnoreRulesAny; + } + else if (!rule.Operation.Context.IsAny()) { + position = specificTokens ? + RulesPosition.ContextRulesSpecific : + RulesPosition.ContextRulesAny; + } + else { + position = specificTokens ? + RulesPosition.NoContextRulesSpecific : + RulesPosition.NoContextRulesAny; + } + + var state = constructionState[rulesBucketIndex]; + if (state === undefined) { + state = constructionState[rulesBucketIndex] = new RulesBucketConstructionState(); + } + var index = state.GetInsertionIndex(position); + this.rules.splice(index, 0, rule); + state.IncreaseInsertionIndex(position); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/rulesProvider.ts b/src/services/formatting/new/rulesProvider.ts new file mode 100644 index 00000000000..f7374a58142 --- /dev/null +++ b/src/services/formatting/new/rulesProvider.ts @@ -0,0 +1,117 @@ +// +// 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 ts.formatting { + export class RulesProvider { + private globalRules: Rules; + private options: ts.FormatCodeOptions; + private activeRules: Rule[]; + private rulesMap: RulesMap; + + constructor(private logger: TypeScript.Logger) { + this.globalRules = new Rules(); + } + + public getRuleName(rule: Rule): string { + return this.globalRules.getRuleName(rule); + } + + public getRuleByName(name: string): Rule { + return this.globalRules[name]; + } + + public getRulesMap() { + return this.rulesMap; + } + + public ensureUpToDate(options: ts.FormatCodeOptions) { + if (this.options == null || !ts.compareDataObjects(this.options, options)) { + var activeRules = this.createActiveRules(options); + var rulesMap = RulesMap.create(activeRules); + + this.activeRules = activeRules; + this.rulesMap = rulesMap; + this.options = ts.clone(options); + } + } + + private createActiveRules(options: ts.FormatCodeOptions): Rule[] { + var rules = this.globalRules.HighPriorityCommonRules.slice(0); + + if (options.InsertSpaceAfterCommaDelimiter) { + rules.push(this.globalRules.SpaceAfterComma); + } + else { + rules.push(this.globalRules.NoSpaceAfterComma); + } + + if (options.InsertSpaceAfterFunctionKeywordForAnonymousFunctions) { + rules.push(this.globalRules.SpaceAfterAnonymousFunctionKeyword); + } + else { + rules.push(this.globalRules.NoSpaceAfterAnonymousFunctionKeyword); + } + + if (options.InsertSpaceAfterKeywordsInControlFlowStatements) { + rules.push(this.globalRules.SpaceAfterKeywordInControl); + } + else { + rules.push(this.globalRules.NoSpaceAfterKeywordInControl); + } + + if (options.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis) { + rules.push(this.globalRules.SpaceAfterOpenParen); + rules.push(this.globalRules.SpaceBeforeCloseParen); + rules.push(this.globalRules.NoSpaceBetweenParens); + } + else { + rules.push(this.globalRules.NoSpaceAfterOpenParen); + rules.push(this.globalRules.NoSpaceBeforeCloseParen); + rules.push(this.globalRules.NoSpaceBetweenParens); + } + + if (options.InsertSpaceAfterSemicolonInForStatements) { + rules.push(this.globalRules.SpaceAfterSemicolonInFor); + } + else { + rules.push(this.globalRules.NoSpaceAfterSemicolonInFor); + } + + if (options.InsertSpaceBeforeAndAfterBinaryOperators) { + rules.push(this.globalRules.SpaceBeforeBinaryOperator); + rules.push(this.globalRules.SpaceAfterBinaryOperator); + } + else { + rules.push(this.globalRules.NoSpaceBeforeBinaryOperator); + rules.push(this.globalRules.NoSpaceAfterBinaryOperator); + } + + if (options.PlaceOpenBraceOnNewLineForControlBlocks) { + rules.push(this.globalRules.NewLineBeforeOpenBraceInControl); + } + + if (options.PlaceOpenBraceOnNewLineForFunctions) { + rules.push(this.globalRules.NewLineBeforeOpenBraceInFunction); + rules.push(this.globalRules.NewLineBeforeOpenBraceInTypeScriptDeclWithBlock); + } + + rules = rules.concat(this.globalRules.LowPriorityCommonRules); + + return rules; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/smartIndenter.ts b/src/services/formatting/new/smartIndenter.ts new file mode 100644 index 00000000000..df57929dff3 --- /dev/null +++ b/src/services/formatting/new/smartIndenter.ts @@ -0,0 +1,401 @@ +/// +/// + +module ts.formatting { + export module SmartIndenter { + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorOptions): number { + if (position > sourceFile.text.length) { + return 0; // past EOF + } + + var precedingToken = ServicesSyntaxUtilities.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.IndentSize : 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; + } + + return getIndentationForNode(current, currentStart, indentationDelta, sourceFile, options); + } + + export function getIndentationForNode(current: Node, currentStart: LineAndCharacter, indentationDelta: number, sourceFile: SourceFile, options: EditorOptions): number { + 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.IndentSize; + } + + current = parent; + currentStart = parentStart; + parent = current.parent; + } + + return indentationDelta; + } + + /* + * Function returns -1 if indentation cannot be determined + */ + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorOptions): number { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + var commaItemInfo = ServicesSyntaxUtilities.findListItemInfo(commaToken); + Debug.assert(commaItemInfo.listItemIndex > 0); + // The item we're interested in is right before the comma + return deriveActualIndentationFromList(commaItemInfo.list.getChildren(), commaItemInfo.listItemIndex - 1, 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: EditorOptions): 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 = ServicesSyntaxUtilities.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 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: EditorOptions): 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: EditorOptions): 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: EditorOptions): 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.TabSize; + } + else { + column++; + } + } + + return column; + } + + 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; + } + } + } +} + diff --git a/src/services/formatting/new/smartIndenter.ts.orig b/src/services/formatting/new/smartIndenter.ts.orig new file mode 100644 index 00000000000..422c7d0c408 --- /dev/null +++ b/src/services/formatting/new/smartIndenter.ts.orig @@ -0,0 +1,476 @@ +/// + +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 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; + } + } + + // 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 currentStartLine: number; + var indentation: number; + + while (current) { + if (isPositionBelongToNode(current, position, sourceFile)) { +<<<<<<< HEAD + +======= +>>>>>>> added support for smart indentation in the middle of list items, updated test baselines + currentStartLine = getStartLineForNode(current, sourceFile); + + if (discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(precedingToken, current, lineAtPosition, sourceFile)) { + indentation = 0; + } + else { + indentation = isNodeContentIndented(current, previous) && lineAtPosition !== currentStartLine ? 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; + } + + 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 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) { +<<<<<<< HEAD +======= + + // check if current node is a list item - if yes, take indentation from it +>>>>>>> added support for smart indentation in the middle of list items, updated test baselines + var customIndentation = getCustomIndentationForListItem(current, sourceFile); + if (customIndentation !== -1) { + return customIndentation + 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); + + if (increaseIndentation) { + indentation += options.indentSpaces; + } + + current = parent; + currentStartLine = parentStartLine; + parent = current.parent; + } + + return indentation; + } + + function discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(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 = getStartLineForNode(nextToken, sourceFile); + return lineAtPosition === nextTokenStartLine; + } + + return false; + } + + function getStartLineForNode(n: Node, sourceFile: SourceFile): number { + return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)).line; + } + + function findPrecedingListItem(commaToken: Node): 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 children[commaIndex - 1]; + } + + function isPositionBelongToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean { + return candidate.end > position || !isCompletedNode(candidate, sourceFile); + } + + function isChildStartsOnTheSameLineWithElseInIfStatement(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 = getStartLineForNode(elseKeyword, sourceFile); + return elseKeywordStartLine === childStartLine; + } + } + + function getCustomIndentationForListItem(node: Node, sourceFile: SourceFile): number { + if (node.parent) { + switch (node.parent.kind) { + case SyntaxKind.ObjectLiteral: + return getCustomIndentationFromList((node.parent).properties); + case SyntaxKind.TypeLiteral: + return getCustomIndentationFromList((node.parent).members); + case SyntaxKind.ArrayLiteral: + return getCustomIndentationFromList((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 getCustomIndentationFromList((node.parent).typeParameters); + } + else { + return getCustomIndentationFromList((node.parent).parameters); + } + case SyntaxKind.NewExpression: + case SyntaxKind.CallExpression: + if ((node.parent).typeArguments && node.end < (node.parent).typeArguments.end) { + return getCustomIndentationFromList((node.parent).typeArguments); + } + else { + return getCustomIndentationFromList((node.parent).arguments); + } + + break; + } + } + + return -1; + + function getCustomIndentationFromList(list: Node[]): number { + var index = indexOf(list, node); + if (index !== -1) { + var lineAndCol = sourceFile.getLineAndCharacterFromPosition(node.getStart(sourceFile)); + for (var i = index - 1; i >= 0; --i) { + var prevLineAndCol = sourceFile.getLineAndCharacterFromPosition(list[i].getStart(sourceFile)); + if (lineAndCol.line !== prevLineAndCol.line) { +<<<<<<< HEAD + // 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 range that we check above includes at least one non-whitespace character at the very end + Debug.fail("Unreachable code") + +======= + return findFirstNonWhitespaceCharacterInLine(lineAndCol.line, lineAndCol.character, sourceFile); +>>>>>>> added support for smart indentation in the middle of list items, updated test baselines + } + lineAndCol = prevLineAndCol; + } + } + 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; + } + } + + return maxCharacter; + } + + 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 end exactly at the beginning of child + (child.pos === previousToken.end); + + if (shouldDiveInChildNode && isCandidateNode(child)) { + return find(child); + } + } + } + } + + 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, /*exclusiveStartPosition*/ 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, /*exclusiveStartPosition*/ 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, /*exclusiveStartPosition*/ children.length); + return candidate && find(candidate, /*diveIntoLastChild*/ true); + } + } + + /// 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]; + } + } + } + } + + /// 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 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; + } + + 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: + 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 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/formatting/new/snapshotPoint.ts b/src/services/formatting/new/snapshotPoint.ts new file mode 100644 index 00000000000..781c9c69d31 --- /dev/null +++ b/src/services/formatting/new/snapshotPoint.ts @@ -0,0 +1,30 @@ +// +// 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 ts.formatting { + + export class SnapshotPoint { + constructor(public snapshot: ITextSnapshot, public position: number) { + } + public getContainingLine(): ITextSnapshotLine { + return this.snapshot.getLineFromPosition(this.position); + } + public add(offset: number): SnapshotPoint { + return new SnapshotPoint(this.snapshot, this.position + offset); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/textEditInfo.ts b/src/services/formatting/new/textEditInfo.ts new file mode 100644 index 00000000000..18ee085dc19 --- /dev/null +++ b/src/services/formatting/new/textEditInfo.ts @@ -0,0 +1,28 @@ +// +// 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 ts.formatting { + export class TextEditInfo { + + constructor(public position: number, public length: number, public replaceWith: string) { + } + + public toString() { + return "[ position: " + this.position + ", length: " + this.length + ", replaceWith: '" + this.replaceWith + "' ]"; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/textSnapshot.ts b/src/services/formatting/new/textSnapshot.ts new file mode 100644 index 00000000000..096159f12ab --- /dev/null +++ b/src/services/formatting/new/textSnapshot.ts @@ -0,0 +1,89 @@ +// +// 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 ts.formatting { + export interface ITextSnapshot { + getLength(): number; + getText(span: TypeScript.TextSpan): string; + getLineNumberFromPosition(position: number): number; + getLineFromPosition(position: number): ITextSnapshotLine; + getLineFromLineNumber(lineNumber: number): ITextSnapshotLine; + } + + export class TextSnapshot implements ITextSnapshot { + private lines: TextSnapshotLine[]; + + constructor(private snapshot: /*ISimpleText*/ any) { + this.lines = []; + } + + public getLength(): number { + return this.snapshot.length(); + } + + public getText(span: TypeScript.TextSpan): string { + return this.snapshot.substr(span.start(), span.length()); + } + + public getLineNumberFromPosition(position: number): number { + return this.snapshot.lineMap().getLineNumberFromPosition(position); + } + + public getLineFromPosition(position: number): ITextSnapshotLine { + var lineNumber = this.getLineNumberFromPosition(position); + return this.getLineFromLineNumber(lineNumber); + } + + public getLineFromLineNumber(lineNumber: number): ITextSnapshotLine { + var line = this.lines[lineNumber]; + if (line === undefined) { + line = this.getLineFromLineNumberWorker(lineNumber); + this.lines[lineNumber] = line; + } + return line; + } + + private getLineFromLineNumberWorker(lineNumber: number): ITextSnapshotLine { + var lineMap = this.snapshot.lineMap().lineStarts(); + var lineMapIndex = lineNumber; //Note: lineMap is 0-based + if (lineMapIndex < 0 || lineMapIndex >= lineMap.length) + throw new Error(TypeScript.getDiagnosticMessage(TypeScript.DiagnosticCode.Invalid_line_number_0, [lineMapIndex])); + var start = lineMap[lineMapIndex]; + + var end: number; + var endIncludingLineBreak: number; + var lineBreak = ""; + if (lineMapIndex == lineMap.length) { + end = endIncludingLineBreak = this.snapshot.length(); + } + else { + endIncludingLineBreak = (lineMapIndex >= lineMap.length - 1 ? this.snapshot.length() : lineMap[lineMapIndex + 1]); + for (var p = endIncludingLineBreak - 1; p >= start; p--) { + var c = this.snapshot.substr(p, 1); + //TODO: Other ones? + if (c != "\r" && c != "\n") { + break; + } + } + end = p + 1; + lineBreak = this.snapshot.substr(end, endIncludingLineBreak - end); + } + var result = new TextSnapshotLine(this, lineNumber, start, end, lineBreak); + return result; + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/textSnapshotLine.ts b/src/services/formatting/new/textSnapshotLine.ts new file mode 100644 index 00000000000..f0843b82e4e --- /dev/null +++ b/src/services/formatting/new/textSnapshotLine.ts @@ -0,0 +1,80 @@ +// +// 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 ts.formatting { + export interface ITextSnapshotLine { + snapshot(): ITextSnapshot; + + start(): SnapshotPoint; + startPosition(): number; + + end(): SnapshotPoint; + endPosition(): number; + + endIncludingLineBreak(): SnapshotPoint; + endIncludingLineBreakPosition(): number; + + length(): number; + lineNumber(): number; + getText(): string; + } + + export class TextSnapshotLine implements ITextSnapshotLine { + constructor(private _snapshot: ITextSnapshot, private _lineNumber: number, private _start: number, private _end: number, private _lineBreak: string) { + } + + public snapshot() { + return this._snapshot; + } + + public start() { + return new SnapshotPoint(this._snapshot, this._start); + } + + public startPosition() { + return this._start; + } + + public end() { + return new SnapshotPoint(this._snapshot, this._end); + } + + public endPosition() { + return this._end; + } + + public endIncludingLineBreak() { + return new SnapshotPoint(this._snapshot, this._end + this._lineBreak.length); + } + + public endIncludingLineBreakPosition() { + return this._end + this._lineBreak.length; + } + + public length() { + return this._end - this._start; + } + + public lineNumber() { + return this._lineNumber; + } + + public getText(): string { + return this._snapshot.getText(TypeScript.TextSpan.fromBounds(this._start, this._end)); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/tokenRange.ts b/src/services/formatting/new/tokenRange.ts new file mode 100644 index 00000000000..5022384eeb5 --- /dev/null +++ b/src/services/formatting/new/tokenRange.ts @@ -0,0 +1,152 @@ +// +// 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 ts.formatting { + export module Shared { + export interface ITokenAccess { + GetTokens(): SyntaxKind[]; + Contains(token: SyntaxKind): boolean; + } + + export class TokenRangeAccess implements ITokenAccess { + private tokens: SyntaxKind[]; + + constructor(from: SyntaxKind, to: SyntaxKind, except: SyntaxKind[]) { + this.tokens = []; + for (var token = from; token <= to; token++) { + if (except.indexOf(token) < 0) { + this.tokens.push(token); + } + } + } + + public GetTokens(): SyntaxKind[] { + return this.tokens; + } + + public Contains(token: SyntaxKind): boolean { + return this.tokens.indexOf(token) >= 0; + } + + + public toString(): string { + return "[tokenRangeStart=" + SyntaxKind[this.tokens[0]] + "," + + "tokenRangeEnd=" + SyntaxKind[this.tokens[this.tokens.length - 1]] + "]"; + } + } + + export class TokenValuesAccess implements ITokenAccess { + private tokens: SyntaxKind[]; + + constructor(tks: SyntaxKind[]) { + this.tokens = tks && tks.length ? tks : []; + } + + public GetTokens(): SyntaxKind[] { + return this.tokens; + } + + public Contains(token: SyntaxKind): boolean { + return this.tokens.indexOf(token) >= 0; + } + } + + export class TokenSingleValueAccess implements ITokenAccess { + constructor(public token: SyntaxKind) { + } + + public GetTokens(): SyntaxKind[] { + return [this.token]; + } + + public Contains(tokenValue: SyntaxKind): boolean { + return tokenValue == this.token; + } + + public toString(): string { + return "[singleTokenKind=" + SyntaxKind[this.token] + "]"; + } + } + + export class TokenAllAccess implements ITokenAccess { + public GetTokens(): SyntaxKind[] { + var result: SyntaxKind[] = []; + for (var token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { + result.push(token); + } + return result; + } + + public Contains(tokenValue: SyntaxKind): boolean { + return true; + } + + public toString(): string { + return "[allTokens]"; + } + } + + export class TokenRange { + constructor(public tokenAccess: ITokenAccess) { + } + + static FromToken(token: SyntaxKind): TokenRange { + return new TokenRange(new TokenSingleValueAccess(token)); + } + + static FromTokens(tokens: SyntaxKind[]): TokenRange { + return new TokenRange(new TokenValuesAccess(tokens)); + } + + static FromRange(f: SyntaxKind, to: SyntaxKind, except: SyntaxKind[] = []): TokenRange { + return new TokenRange(new TokenRangeAccess(f, to, except)); + } + + static AllTokens(): TokenRange { + return new TokenRange(new TokenAllAccess()); + } + + public GetTokens(): SyntaxKind[] { + return this.tokenAccess.GetTokens(); + } + + public Contains(token: SyntaxKind): boolean { + return this.tokenAccess.Contains(token); + } + + public toString(): string { + return this.tokenAccess.toString(); + } + + static Any: TokenRange = TokenRange.AllTokens(); + static AnyIncludingMultilineComments = TokenRange.FromTokens(TokenRange.Any.GetTokens().concat([SyntaxKind.MultiLineCommentTrivia])); + static Keywords = TokenRange.FromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); + static Operators = TokenRange.FromRange(SyntaxKind.SemicolonToken, SyntaxKind.SlashEqualsToken); + static BinaryOperators = TokenRange.FromRange(SyntaxKind.LessThanToken, SyntaxKind.SlashEqualsToken); + static BinaryKeywordOperators = TokenRange.FromTokens([SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword]); + static ReservedKeywords = TokenRange.FromRange(SyntaxKind.FirstFutureReservedWord, SyntaxKind.LastFutureReservedWord); + static UnaryPrefixOperators = TokenRange.FromTokens([SyntaxKind.PlusPlusToken, SyntaxKind.MinusMinusToken, SyntaxKind.TildeToken, SyntaxKind.ExclamationToken]); + static UnaryPrefixExpressions = TokenRange.FromTokens([SyntaxKind.NumericLiteral, SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.OpenBraceToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPreincrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPostincrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); + static UnaryPredecrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]); + static UnaryPostdecrementExpressions = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]); + static Comments = TokenRange.FromTokens([SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia]); + static TypeNames = TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.NumberKeyword, SyntaxKind.StringKeyword, SyntaxKind.BooleanKeyword, SyntaxKind.VoidKeyword, SyntaxKind.AnyKeyword]); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/new/tokenSpan.ts b/src/services/formatting/new/tokenSpan.ts new file mode 100644 index 00000000000..7194b0ce4fa --- /dev/null +++ b/src/services/formatting/new/tokenSpan.ts @@ -0,0 +1,25 @@ +// +// 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 ts.formatting { + export class TokenSpan extends TypeScript.TextSpan { + constructor(public kind: SyntaxKind, start: number, length: number) { + super(start, length); + } + } +} \ No newline at end of file diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 79b238c893d..d7dc3f422fe 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -2,7 +2,7 @@ module ts.formatting { export module SmartIndenter { - export function getIndentation(position: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorOptions): number { if (position > sourceFile.text.length) { return 0; // past EOF } @@ -44,7 +44,7 @@ module ts.formatting { indentationDelta = 0; } else { - indentationDelta = lineAtPosition !== currentStart.line ? options.indentSpaces : 0; + indentationDelta = lineAtPosition !== currentStart.line ? options.IndentSize : 0; } break; @@ -91,7 +91,44 @@ module ts.formatting { // 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; + indentationDelta += options.IndentSize; + } + + current = parent; + currentStart = parentStart; + parent = current.parent; + } + + return indentationDelta; + } + + export function getIndentationForNode(current: Node, currentStart: LineAndCharacter, indentationDelta: number, sourceFile: SourceFile, options: EditorOptions): number { + 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.IndentSize; } current = parent; @@ -105,7 +142,7 @@ module ts.formatting { /* * Function returns -1 if indentation cannot be determined */ - function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorOptions): number { // previous token is comma that separates items in list - find the previous item and try to derive indentation from it var commaItemInfo = findListItemInfo(commaToken); Debug.assert(commaItemInfo.listItemIndex > 0); @@ -121,7 +158,7 @@ module ts.formatting { currentLineAndChar: LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: SourceFile, - options: TypeScript.FormattingOptions): number { + options: EditorOptions): 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 @@ -172,7 +209,7 @@ module ts.formatting { return candidate.end > position || !isCompletedNode(candidate, sourceFile); } - function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFile): boolean { + export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFile): boolean { if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { var elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile); Debug.assert(elseKeyword); @@ -182,7 +219,7 @@ module ts.formatting { } } - function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorOptions): number { if (node.parent) { switch (node.parent.kind) { case SyntaxKind.TypeReference: @@ -226,7 +263,7 @@ module ts.formatting { } - function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function deriveActualIndentationFromList(list: Node[], index: number, sourceFile: SourceFile, options: EditorOptions): number { Debug.assert(index >= 0 && index < list.length); var node = list[index]; @@ -248,7 +285,7 @@ module ts.formatting { return -1; } - function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number { + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorOptions): number { var lineStart = sourceFile.getPositionFromLineAndCharacter(lineAndCharacter.line, 1); var column = 0; for (var i = 0; i < lineAndCharacter.character; ++i) { @@ -258,7 +295,7 @@ module ts.formatting { } if (charCode === CharacterCodes.tab) { - column += options.spacesPerTab; + column += options.TabSize; } else { column++; @@ -268,7 +305,7 @@ module ts.formatting { return column; } - function nodeContentIsIndented(parent: Node, child: Node): boolean { + export function nodeContentIsIndented(parent: Node, child: Node): boolean { switch (parent.kind) { case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: diff --git a/src/services/formatting/stringUtilities.ts b/src/services/formatting/stringUtilities.ts new file mode 100644 index 00000000000..5f5d0b0fd4a --- /dev/null +++ b/src/services/formatting/stringUtilities.ts @@ -0,0 +1,52 @@ +module ts.formatting { + + var internedTabsIndentation: string[]; + var internedSpacesIndentation: string[]; + + export function getIndentationString(indentation: number, options: FormatCodeOptions): string { + if (!options.ConvertTabsToSpaces) { + var tabs = Math.floor(indentation / options.TabSize); + var spaces = indentation - tabs * options.TabSize; + + var tabString: string; + if (!internedTabsIndentation) { + internedTabsIndentation = []; + } + + if (internedTabsIndentation[tabs] === undefined) { + internedTabsIndentation[tabs] = tabString = repeat('\t', tabs); + } + else { + tabString = internedTabsIndentation[tabs]; + } + + return spaces ? tabString + repeat(" ", spaces) : tabString; + } + else { + var spacesString: string; + var index = indentation / options.IndentSize; + if (!internedSpacesIndentation) { + internedSpacesIndentation = []; + } + + if (internedSpacesIndentation[index] === undefined) { + internedSpacesIndentation[index] = spacesString = repeat(" ", indentation);; + } + else { + spacesString = internedSpacesIndentation[index]; + } + + var remainder = indentation % options.IndentSize; + return remainder ? spacesString + repeat(" ", remainder) : spacesString; + } + + function repeat(value: string, count: number): string { + var s = ""; + for (var i = 0; i < count; ++i) { + s += value; + } + + return s; + } + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index 618026408fe..ab62b5fecae 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -13,6 +13,7 @@ /// /// /// +/// /// /// @@ -640,6 +641,7 @@ module ts { public text: string; public getLineAndCharacterFromPosition(position: number): { line: number; character: number } { return null; } public getPositionFromLineAndCharacter(line: number, character: number): number { return -1; } + public getLineStarts(): number[] { return undefined; } public amdDependencies: string[]; public referencedFiles: FileReference[]; public syntacticErrors: Diagnostic[]; @@ -975,6 +977,15 @@ module ts { ConvertTabsToSpaces: boolean; } + export function copyEditorOptions(o: EditorOptions): EditorOptions { + return { + IndentSize: o.IndentSize, + TabSize: o.TabSize, + NewLineCharacter: o.NewLineCharacter, + ConvertTabsToSpaces: o.ConvertTabsToSpaces + }; + } + export interface FormatCodeOptions extends EditorOptions { InsertSpaceAfterCommaDelimiter: boolean; InsertSpaceAfterSemicolonInForStatements: boolean; @@ -986,6 +997,23 @@ module ts { PlaceOpenBraceOnNewLineForControlBlocks: boolean; } + export function copyFormatCodeOptions(o: FormatCodeOptions): FormatCodeOptions { + return { + IndentSize: o.IndentSize, + TabSize: o.TabSize, + NewLineCharacter: o.NewLineCharacter, + ConvertTabsToSpaces: o.ConvertTabsToSpaces, + InsertSpaceAfterCommaDelimiter: o.InsertSpaceAfterCommaDelimiter, + InsertSpaceAfterSemicolonInForStatements: o.InsertSpaceAfterSemicolonInForStatements, + InsertSpaceBeforeAndAfterBinaryOperators: o.InsertSpaceBeforeAndAfterBinaryOperators, + InsertSpaceAfterKeywordsInControlFlowStatements: o.InsertSpaceAfterKeywordsInControlFlowStatements, + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: o.InsertSpaceAfterFunctionKeywordForAnonymousFunctions, + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: o.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis, + PlaceOpenBraceOnNewLineForFunctions: o.PlaceOpenBraceOnNewLineForFunctions, + PlaceOpenBraceOnNewLineForControlBlocks: o.PlaceOpenBraceOnNewLineForControlBlocks + }; + } + export interface DefinitionInfo { fileName: string; textSpan: TypeScript.TextSpan; @@ -1996,6 +2024,7 @@ module ts { export function createLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry): LanguageService { var syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); var formattingRulesProvider: TypeScript.Services.Formatting.RulesProvider; + var ruleProvider: ts.formatting.RulesProvider; var hostCache: HostCache; // A cache of all the information about the files on the host side. var program: Program; @@ -2026,6 +2055,16 @@ module ts { return fullTypeCheckChecker_doNotAccessDirectly || (fullTypeCheckChecker_doNotAccessDirectly = program.getTypeChecker(/*fullTypeCheck*/ true)); } + function getRuleProvider(options: FormatCodeOptions) { + // Ensure rules are initialized and up to date wrt to formatting options + if (!ruleProvider) { + ruleProvider = new ts.formatting.RulesProvider(host); + } + + ruleProvider.ensureUpToDate(options); + return ruleProvider; + } + function createCompilerHost(): CompilerHost { return { getSourceFile: (filename, languageVersion) => { @@ -4968,7 +5007,7 @@ module ts { var sourceFile = getCurrentSourceFile(filename); var options = new TypeScript.FormattingOptions(!editorOptions.ConvertTabsToSpaces, editorOptions.TabSize, editorOptions.IndentSize, editorOptions.NewLineCharacter) - return formatting.SmartIndenter.getIndentation(position, sourceFile, options); + return formatting.SmartIndenter.getIndentation(position, sourceFile, copyEditorOptions(editorOptions)); } function getFormattingManager(filename: string, options: FormatCodeOptions) { @@ -4994,6 +5033,10 @@ module ts { function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[] { fileName = TypeScript.switchToForwardSlashes(fileName); + var options = copyFormatCodeOptions(options); + var sourceFile = getCurrentSourceFile(fileName); + var edits = formatting.formatSelection(start, end, sourceFile, getRuleProvider(options), options); + return edits; var manager = getFormattingManager(fileName, options); return manager.formatSelection(start, end); @@ -5002,6 +5045,11 @@ module ts { function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { fileName = TypeScript.switchToForwardSlashes(fileName); + var sourceFile = getCurrentSourceFile(fileName); + var options = copyFormatCodeOptions(options) + var edits = formatting.formatDocument(sourceFile, getRuleProvider(options), options); + return edits; + var manager = getFormattingManager(fileName, options); return manager.formatDocument(); } @@ -5011,13 +5059,25 @@ module ts { var manager = getFormattingManager(fileName, options); + var sourceFile = getCurrentSourceFile(fileName); + var options = copyFormatCodeOptions(options); + if (key === "}") { + var edits = formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(options), options); + return edits; + return manager.formatOnClosingCurlyBrace(position); } else if (key === ";") { + var edits = formatting.formatOnSemicolon(position, sourceFile, getRuleProvider(options), options); + return edits; + return manager.formatOnSemicolon(position); } else if (key === "\n") { + var edits = formatting.formatOnEnter(position, sourceFile, getRuleProvider(options), options); + return edits; + return manager.formatOnEnter(position); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 4ceb20fdcee..6965685884c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -233,6 +233,10 @@ module ts { return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken; } + export function isComment(kind: SyntaxKind): boolean { + return kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia; + } + function isKeyword(n: Node): boolean { return n.kind >= SyntaxKind.FirstKeyword && n.kind <= SyntaxKind.LastKeyword; }