mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-11 06:02:53 -05:00
Fix formatting the end of a selection (#46875)
* Fix formatting edits at end of range * Adjust test * Revert API baseline changes
This commit is contained in:
@@ -344,7 +344,7 @@ namespace ts.formatting {
|
||||
}
|
||||
|
||||
export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] {
|
||||
const range = { pos: 0, end: sourceFileLike.text.length };
|
||||
const range = { pos: node.pos, end: node.end };
|
||||
return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(
|
||||
range,
|
||||
node,
|
||||
@@ -438,6 +438,25 @@ namespace ts.formatting {
|
||||
}
|
||||
}
|
||||
|
||||
if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) {
|
||||
const token =
|
||||
formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() :
|
||||
formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token :
|
||||
undefined;
|
||||
|
||||
if (token) {
|
||||
processPair(
|
||||
token,
|
||||
sourceFile.getLineAndCharacterOfPosition(token.pos).line,
|
||||
enclosingNode,
|
||||
previousRange,
|
||||
previousRangeStartLine!,
|
||||
previousParent!,
|
||||
enclosingNode,
|
||||
/*dynamicIndentation*/ undefined);
|
||||
}
|
||||
}
|
||||
|
||||
return edits;
|
||||
|
||||
// local functions
|
||||
@@ -653,29 +672,14 @@ namespace ts.formatting {
|
||||
});
|
||||
|
||||
// proceed any tokens in the node that are located after child nodes
|
||||
while (formattingScanner.isOnToken()) {
|
||||
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
|
||||
const tokenInfo = formattingScanner.readTokenInfo(node);
|
||||
if (tokenInfo.token.end > node.end) {
|
||||
if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) {
|
||||
break;
|
||||
}
|
||||
consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node);
|
||||
}
|
||||
|
||||
if (!node.parent && 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,
|
||||
@@ -717,9 +721,12 @@ namespace ts.formatting {
|
||||
return inheritedIndentation;
|
||||
}
|
||||
|
||||
while (formattingScanner.isOnToken()) {
|
||||
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
|
||||
// proceed any parent tokens that are located prior to child.getStart()
|
||||
const tokenInfo = formattingScanner.readTokenInfo(node);
|
||||
if (tokenInfo.token.end > originalRange.end) {
|
||||
return inheritedIndentation;
|
||||
}
|
||||
if (tokenInfo.token.end > childStartPos) {
|
||||
if (tokenInfo.token.pos > childStartPos) {
|
||||
formattingScanner.skipToStartOf(child);
|
||||
@@ -731,7 +738,7 @@ namespace ts.formatting {
|
||||
consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node);
|
||||
}
|
||||
|
||||
if (!formattingScanner.isOnToken()) {
|
||||
if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) {
|
||||
return inheritedIndentation;
|
||||
}
|
||||
|
||||
@@ -773,7 +780,7 @@ namespace ts.formatting {
|
||||
|
||||
if (listStartToken !== SyntaxKind.Unknown) {
|
||||
// introduce a new indentation scope for lists (including list start and end tokens)
|
||||
while (formattingScanner.isOnToken()) {
|
||||
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
|
||||
const tokenInfo = formattingScanner.readTokenInfo(parent);
|
||||
if (tokenInfo.token.end > nodes.pos) {
|
||||
// stop when formatting scanner moves past the beginning of node list
|
||||
@@ -814,7 +821,7 @@ namespace ts.formatting {
|
||||
}
|
||||
|
||||
const listEndToken = getCloseTokenForOpenToken(listStartToken);
|
||||
if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) {
|
||||
if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
|
||||
let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent);
|
||||
if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) {
|
||||
const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line;
|
||||
@@ -969,7 +976,7 @@ namespace ts.formatting {
|
||||
previousStartLine: number,
|
||||
previousParent: Node,
|
||||
contextNode: Node,
|
||||
dynamicIndentation: DynamicIndentation): LineAction {
|
||||
dynamicIndentation: DynamicIndentation | undefined): LineAction {
|
||||
|
||||
formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode);
|
||||
|
||||
@@ -982,24 +989,26 @@ namespace ts.formatting {
|
||||
// win in a conflict with lower priority rules.
|
||||
forEachRight(rules, rule => {
|
||||
lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine);
|
||||
switch (lineAction) {
|
||||
case LineAction.LineRemoved:
|
||||
// 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 (currentParent.getStart(sourceFile) === currentItem.pos) {
|
||||
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
|
||||
}
|
||||
break;
|
||||
case LineAction.LineAdded:
|
||||
// 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 (currentParent.getStart(sourceFile) === currentItem.pos) {
|
||||
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.assert(lineAction === LineAction.None);
|
||||
if (dynamicIndentation) {
|
||||
switch (lineAction) {
|
||||
case LineAction.LineRemoved:
|
||||
// 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 (currentParent.getStart(sourceFile) === currentItem.pos) {
|
||||
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
|
||||
}
|
||||
break;
|
||||
case LineAction.LineAdded:
|
||||
// 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 (currentParent.getStart(sourceFile) === currentItem.pos) {
|
||||
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.assert(lineAction === LineAction.None);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ts.formatting {
|
||||
|
||||
export interface FormattingScanner {
|
||||
advance(): void;
|
||||
getStartPos(): number;
|
||||
isOnToken(): boolean;
|
||||
isOnEOF(): boolean;
|
||||
readTokenInfo(n: Node): TokenInfo;
|
||||
@@ -49,6 +50,7 @@ namespace ts.formatting {
|
||||
lastTrailingTriviaWasNewLine: () => wasNewLine,
|
||||
skipToEndOf,
|
||||
skipToStartOf,
|
||||
getStartPos: () => lastTokenInfo?.token.pos ?? scanner.getTokenPos(),
|
||||
});
|
||||
|
||||
lastTokenInfo = undefined;
|
||||
@@ -265,8 +267,7 @@ namespace ts.formatting {
|
||||
|
||||
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);
|
||||
return current !== SyntaxKind.EndOfFileToken && !isTrivia(current);
|
||||
}
|
||||
|
||||
function isOnEOF(): boolean {
|
||||
|
||||
@@ -2726,7 +2726,7 @@ namespace ts {
|
||||
return typeIsAccessible ? res : undefined;
|
||||
}
|
||||
|
||||
export function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
return kind === SyntaxKind.CallSignature
|
||||
|| kind === SyntaxKind.ConstructSignature
|
||||
|| kind === SyntaxKind.IndexSignature
|
||||
@@ -2734,7 +2734,7 @@ namespace ts {
|
||||
|| kind === SyntaxKind.MethodSignature;
|
||||
}
|
||||
|
||||
export function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
return kind === SyntaxKind.FunctionDeclaration
|
||||
|| kind === SyntaxKind.Constructor
|
||||
|| kind === SyntaxKind.MethodDeclaration
|
||||
@@ -2742,7 +2742,7 @@ namespace ts {
|
||||
|| kind === SyntaxKind.SetAccessor;
|
||||
}
|
||||
|
||||
export function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) {
|
||||
return kind === SyntaxKind.ModuleDeclaration;
|
||||
}
|
||||
|
||||
@@ -2831,13 +2831,29 @@ namespace ts {
|
||||
forEachChild(sourceFile, function visit(node): boolean | undefined {
|
||||
if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) {
|
||||
const lastToken = node.getLastToken(sourceFile);
|
||||
if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) {
|
||||
if (lastToken?.kind === SyntaxKind.SemicolonToken) {
|
||||
withSemicolon++;
|
||||
}
|
||||
else {
|
||||
withoutSemicolon++;
|
||||
}
|
||||
}
|
||||
else if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) {
|
||||
const lastToken = node.getLastToken(sourceFile);
|
||||
if (lastToken?.kind === SyntaxKind.SemicolonToken) {
|
||||
withSemicolon++;
|
||||
}
|
||||
else if (lastToken && lastToken.kind !== SyntaxKind.CommaToken) {
|
||||
const lastTokenLine = getLineAndCharacterOfPosition(sourceFile, lastToken.getStart(sourceFile)).line;
|
||||
const nextTokenLine = getLineAndCharacterOfPosition(sourceFile, getSpanOfTokenAtPosition(sourceFile, lastToken.end).start).line;
|
||||
// Avoid counting missing semicolon in single-line objects:
|
||||
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
|
||||
if (lastTokenLine !== nextTokenLine) {
|
||||
withoutSemicolon++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (withSemicolon + withoutSemicolon >= nStatementsToObserve) {
|
||||
return true;
|
||||
}
|
||||
@@ -2845,7 +2861,7 @@ namespace ts {
|
||||
return forEachChild(node, visit);
|
||||
});
|
||||
|
||||
// One statement missing a semicolon isn’t sufficient evidence to say the user
|
||||
// One statement missing a semicolon isn't sufficient evidence to say the user
|
||||
// doesn’t want semicolons, because they may not even be done writing that statement.
|
||||
if (withSemicolon === 0 && withoutSemicolon <= 1) {
|
||||
return true;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
////abstract class Ab {
|
||||
//// abstract met(n: string): void;
|
||||
//// met2(n: number): void {
|
||||
////
|
||||
//// return;
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
|
||||
12
tests/cases/fourslash/formatSelectionEditAtEndOfRange.ts
Normal file
12
tests/cases/fourslash/formatSelectionEditAtEndOfRange.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
|
||||
//// /*1*/var x = 1;/*2*/
|
||||
//// void 0;
|
||||
|
||||
format.setOption("semicolons", "remove");
|
||||
format.selection("1", "2");
|
||||
verify.currentFileContentIs(
|
||||
`var x = 1
|
||||
void 0;`
|
||||
);
|
||||
@@ -11,7 +11,7 @@ edit.applyRefactor({
|
||||
refactorName: "Extract type",
|
||||
actionName: "Extract to type alias",
|
||||
actionDescription: "Extract to type alias",
|
||||
newContent: `type /*RENAME*/NewType = string;
|
||||
newContent: `type /*RENAME*/NewType = string
|
||||
|
||||
interface A<T = NewType> {
|
||||
a: boolean
|
||||
|
||||
@@ -11,7 +11,7 @@ edit.applyRefactor({
|
||||
refactorName: "Extract type",
|
||||
actionName: "Extract to type alias",
|
||||
actionDescription: "Extract to type alias",
|
||||
newContent: `type /*RENAME*/NewType = boolean;
|
||||
newContent: `type /*RENAME*/NewType = boolean
|
||||
|
||||
interface A<T = string> {
|
||||
a: NewType
|
||||
|
||||
Reference in New Issue
Block a user