diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 71afec908ab..59d76e1a388 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -329,6 +329,21 @@ namespace ts { return undefined; } + /** + * Like `forEach`, but iterates in reverse order. + */ + export function forEachRight(array: ReadonlyArray | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { + if (array) { + for (let i = array.length - 1; i >= 0; i--) { + const result = callback(array[i], i); + if (result) { + return result; + } + } + } + return undefined; + } + /** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */ export function firstDefined(array: ReadonlyArray | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { if (array === undefined) { diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index f158aff7e5c..a23801a3153 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -651,6 +651,21 @@ namespace ts.formatting { consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); } + if (formattingScanner.isOnEOF()) { + const token = formattingScanner.readEOFTokenRange(); + if (token.end <= node.end && previousRange) { + processPair( + token, + sourceFile.getLineAndCharacterOfPosition(token.pos).line, + node, + previousRange, + previousRangeStartLine, + previousParent, + contextNode, + nodeDynamicIndentation); + } + } + function processChildNode( child: Node, inheritedIndentation: number, @@ -950,7 +965,10 @@ namespace ts.formatting { let trimTrailingWhitespaces = false; let lineAction = LineAction.None; if (rules) { - for (const rule of rules) { + // Apply rules in reverse order so that higher when higher priority rules (which are first in the array) + // create text changes at the same position as lower priority rules, the higher priority text changes + // are evaluated last. + forEachRight(rules, rule => { lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); switch (lineAction) { case LineAction.LineRemoved: @@ -974,10 +992,10 @@ namespace ts.formatting { // 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 trimTrailingWhitespaces = !(rule.action & RuleAction.DeleteTrivia) && rule.flags !== RuleFlags.CanDeleteNewLines; - } + }); } else { - trimTrailingWhitespaces = true; + trimTrailingWhitespaces = currentItem.kind !== SyntaxKind.EndOfFileToken; } if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { diff --git a/src/services/formatting/formattingScanner.ts b/src/services/formatting/formattingScanner.ts index a54f353e347..66aa4397a0f 100644 --- a/src/services/formatting/formattingScanner.ts +++ b/src/services/formatting/formattingScanner.ts @@ -6,7 +6,9 @@ namespace ts.formatting { export interface FormattingScanner { advance(): void; isOnToken(): boolean; + isOnEOF(): boolean; readTokenInfo(n: Node): TokenInfo; + readEOFTokenRange(): TextRangeWithKind; getCurrentLeadingTrivia(): TextRangeWithKind[] | undefined; lastTrailingTriviaWasNewLine(): boolean; skipToEndOf(node: Node): void; @@ -38,7 +40,9 @@ namespace ts.formatting { const res = cb({ advance, readTokenInfo, + readEOFTokenRange, isOnToken, + isOnEOF, getCurrentLeadingTrivia: () => leadingTrivia, lastTrailingTriviaWasNewLine: () => wasNewLine, skipToEndOf, @@ -249,12 +253,22 @@ namespace ts.formatting { return token; } + function readEOFTokenRange(): TextRangeWithKind { + Debug.assert(isOnEOF()); + return createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), SyntaxKind.EndOfFileToken); + } + function isOnToken(): boolean { const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos(); return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current); } + function isOnEOF(): boolean { + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + return current === SyntaxKind.EndOfFileToken; + } + // when containing node in the tree is token // but its kind differs from the kind that was returned by the scanner, // then kind needs to be fixed. This might happen in cases diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 8af57d93190..cc49a129e81 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -9,7 +9,9 @@ namespace ts.formatting { export function getAllRules(): RuleSpec[] { const allTokens: SyntaxKind[] = []; for (let token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { - allTokens.push(token); + if (token !== SyntaxKind.EndOfFileToken) { + allTokens.push(token); + } } function anyTokenExcept(...tokens: SyntaxKind[]): TokenRange { return { tokens: allTokens.filter(t => !tokens.some(t2 => t2 === t)), isSpecific: false }; @@ -17,6 +19,7 @@ namespace ts.formatting { const anyToken: TokenRange = { tokens: allTokens, isSpecific: false }; const anyTokenIncludingMultilineComments = tokenRangeFrom([...allTokens, SyntaxKind.MultiLineCommentTrivia]); + const anyTokenIncludingEOF = tokenRangeFrom([...allTokens, SyntaxKind.EndOfFileToken]); const keywords = tokenRangeFromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); const binaryOperators = tokenRangeFromRange(SyntaxKind.FirstBinaryOperator, SyntaxKind.LastBinaryOperator); const binaryKeywordOperators = [SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword, SyntaxKind.OfKeyword, SyntaxKind.AsKeyword, SyntaxKind.IsKeyword]; @@ -310,8 +313,8 @@ namespace ts.formatting { rule("SpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.Space), rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.DeleteTrivia), - rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyToken, [isOptionDisabled("insertTrailingSemicolon"), isSemicolonDeletionContext], RuleAction.DeleteToken), - rule("OptionalSemicolon", anyToken, anyToken, [isOptionEnabled("insertTrailingSemicolon"), isSemicolonInsertionContext], RuleAction.TrailingSemicolon), + rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [isOptionDisabled("insertTrailingSemicolon"), isSemicolonDeletionContext], RuleAction.DeleteToken), + rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [isOptionEnabled("insertTrailingSemicolon"), isSemicolonInsertionContext], RuleAction.TrailingSemicolon), ]; function isSemicolonDeletionContext(context: FormattingContext): boolean { diff --git a/src/services/types.ts b/src/services/types.ts index fa2cdc1c821..722539af150 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -758,7 +758,7 @@ namespace ts { insertSpaceBeforeFunctionParenthesis: false, placeOpenBraceOnNewLineForFunctions: false, placeOpenBraceOnNewLineForControlBlocks: false, - insertTrailingSemicolon: true, + // insertTrailingSemicolon: true, }; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index cf93f126f9b..f14b1818746 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2054,13 +2054,14 @@ namespace ts { return false; } - const sourceFile = node.getSourceFile(); - const nextToken = findNextToken(node, sourceFile, sourceFile); - if (!nextToken) { - return false; + // See comment in parser’s `parseDoStatement` + if (node.kind === SyntaxKind.DoStatement) { + return true; } - if (nextToken.kind === SyntaxKind.CloseBraceToken) { + const sourceFile = node.getSourceFile(); + const nextToken = findNextToken(node, sourceFile, sourceFile); + if (!nextToken || nextToken.kind === SyntaxKind.CloseBraceToken) { return true; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 67d6a98e11e..3e2573d5f7a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3098,6 +3098,7 @@ declare namespace ts { export interface UserPreferences { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; + readonly semicolonPreference?: "auto" | "semicolons" | "no semicolons"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; @@ -5305,6 +5306,7 @@ declare namespace ts { readonly placeOpenBraceOnNewLineForControlBlocks?: boolean; readonly insertSpaceBeforeTypeAnnotation?: boolean; readonly indentMultiLineObjectLiteralBeginningOnBlankLine?: boolean; + readonly insertTrailingSemicolon?: boolean; } function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings; interface DefinitionInfo extends DocumentSpan { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 279357eddf7..0de3e8e9cb4 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3098,6 +3098,7 @@ declare namespace ts { export interface UserPreferences { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; + readonly semicolonPreference?: "auto" | "semicolons" | "no semicolons"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; @@ -5305,6 +5306,7 @@ declare namespace ts { readonly placeOpenBraceOnNewLineForControlBlocks?: boolean; readonly insertSpaceBeforeTypeAnnotation?: boolean; readonly indentMultiLineObjectLiteralBeginningOnBlankLine?: boolean; + readonly insertTrailingSemicolon?: boolean; } function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings; interface DefinitionInfo extends DocumentSpan {