diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index 531b768f6d8..1e40d6e1d0b 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -97,11 +97,23 @@ namespace ts.formatting { } export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { - return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon); + const outermostParent = findOutermostParentWithinListLevelFromPosition(position, SyntaxKind.SemicolonToken, sourceFile); + return formatOutermostParent(outermostParent, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon); + } + + export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { + const openingCurly = findPrecedingTokenOfKind(position, SyntaxKind.FirstPunctuation, sourceFile); + const block = openingCurly && openingCurly.parent; + if (!(block && isBlock(block))) { + return []; + } + const outermostParent = findOutermostParentWithinListLevel(block); + return formatOutermostParent(outermostParent, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnOpeningCurlyBrace); } export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { - return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace); + const outermostParent = findOutermostParentWithinListLevelFromPosition(position, SyntaxKind.CloseBraceToken, sourceFile); + return formatOutermostParent(outermostParent, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace); } export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] { @@ -121,44 +133,55 @@ namespace ts.formatting { return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection); } - function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { - const parent = findOutermostParent(position, expectedLastToken, sourceFile); + function formatOutermostParent(parent: Node | undefined, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { if (!parent) { return []; } + const span = { pos: getLineStartPositionForPosition(parent.getStart(sourceFile), sourceFile), end: parent.end }; + return formatSpan(span, sourceFile, options, rulesProvider, requestKind); } - function findOutermostParent(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node { + function findPrecedingTokenOfKind(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined { const precedingToken = findPrecedingToken(position, sourceFile); - // when it is claimed that trigger character was typed at given position - // we verify that there is a token with a matching kind whose end is equal to position (because the character was just typed). - // If this condition is not hold - then trigger character was typed in some other context, - // i.e.in comment and thus should not trigger autoformatting - if (!precedingToken || - precedingToken.kind !== expectedTokenKind || - position !== precedingToken.getEnd()) { - return undefined; - } + return precedingToken && precedingToken.kind === expectedTokenKind && position === precedingToken.getEnd() ? + precedingToken : + undefined; + } - // walk up and search for the parent node that ends at the same position with precedingToken. - // for cases like this - // - // let x = 1; - // while (true) { - // } - // after typing close curly in while statement we want to reformat just the while statement. - // However if we just walk upwards searching for the parent that has the same end value - - // we'll end up with the whole source file. isListElement allows to stop on the list element level - let current = precedingToken; + /** + * Validating `expectedLastToken` ensures the token was typed in the context we expect (eg: not a comment). + * @param expectedLastToken The last token constituting the desired parent node. + */ + function findOutermostParentWithinListLevelFromPosition(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile) { + const precedingToken = findPrecedingTokenOfKind(position, expectedLastToken, sourceFile); + return precedingToken && findOutermostParentWithinListLevel(precedingToken); + } + + /** + * Finds the outermost parent within the same list level as the token at position. + * + * Consider typing the following + * ``` + * let x = 1; + * while (true) { + * } + * ``` + * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding + * variable declaration. + */ + function findOutermostParentWithinListLevel(token: Node): Node { + // If we walk upwards searching for the parent that has the same end value, we'll end up with the whole source file. + // `isListElement` allows to stop on the list element level. + let current = token; while (current && current.parent && - current.parent.end === precedingToken.end && + current.parent.end === token.end && !isListElement(current.parent, current)) { current = current.parent; } diff --git a/src/services/formatting/formattingContext.ts b/src/services/formatting/formattingContext.ts index 043df72f434..fd79481173e 100644 --- a/src/services/formatting/formattingContext.ts +++ b/src/services/formatting/formattingContext.ts @@ -47,14 +47,6 @@ namespace ts.formatting { return this.contextNodeAllOnSameLine; } - public NextNodeAllOnSameLine(): boolean { - if (this.nextNodeAllOnSameLine === undefined) { - this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); - } - - return this.nextNodeAllOnSameLine; - } - public TokensAreOnSameLine(): boolean { if (this.tokensAreOnSameLine === undefined) { const startLine = this.sourceFile.getLineAndCharacterOfPosition(this.currentTokenSpan.pos).line; diff --git a/src/services/formatting/formattingRequestKind.ts b/src/services/formatting/formattingRequestKind.ts index d0397e4a8d9..6c671e1b888 100644 --- a/src/services/formatting/formattingRequestKind.ts +++ b/src/services/formatting/formattingRequestKind.ts @@ -7,6 +7,7 @@ namespace ts.formatting { FormatSelection, FormatOnEnter, FormatOnSemicolon, + FormatOnOpeningCurlyBrace, FormatOnClosingCurlyBrace } } \ No newline at end of file diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 2c1becb8dd1..7f5d362e796 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -671,7 +671,7 @@ namespace ts.formatting { // 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()); + return Rules.IsBeforeBlockContext(context) && !(context.NextNodeBlockIsOnOneLine()); } static IsMultilineBlockContext(context: FormattingContext): boolean { diff --git a/src/services/services.ts b/src/services/services.ts index b508285b182..bd8bfc81884 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1763,8 +1763,10 @@ namespace ts { function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const settings = toEditorSettings(options); - - if (key === "}") { + if (key === "{") { + return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings); + } + else if (key === "}") { return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings); } else if (key === ";") {