Skip to content

Commit 68bf5a5

Browse files
authored
Fix formatting the end of a selection (microsoft#46875)
* Fix formatting edits at end of range * Adjust test * Revert API baseline changes
1 parent 04f831d commit 68bf5a5

7 files changed

+89
-51
lines changed

src/services/formatting/formatting.ts

+50-41
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ namespace ts.formatting {
344344
}
345345

346346
export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] {
347-
const range = { pos: 0, end: sourceFileLike.text.length };
347+
const range = { pos: node.pos, end: node.end };
348348
return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(
349349
range,
350350
node,
@@ -438,6 +438,25 @@ namespace ts.formatting {
438438
}
439439
}
440440

441+
if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) {
442+
const token =
443+
formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() :
444+
formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token :
445+
undefined;
446+
447+
if (token) {
448+
processPair(
449+
token,
450+
sourceFile.getLineAndCharacterOfPosition(token.pos).line,
451+
enclosingNode,
452+
previousRange,
453+
previousRangeStartLine!,
454+
previousParent!,
455+
enclosingNode,
456+
/*dynamicIndentation*/ undefined);
457+
}
458+
}
459+
441460
return edits;
442461

443462
// local functions
@@ -653,29 +672,14 @@ namespace ts.formatting {
653672
});
654673

655674
// proceed any tokens in the node that are located after child nodes
656-
while (formattingScanner.isOnToken()) {
675+
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
657676
const tokenInfo = formattingScanner.readTokenInfo(node);
658-
if (tokenInfo.token.end > node.end) {
677+
if (tokenInfo.token.end > Math.min(node.end, originalRange.end)) {
659678
break;
660679
}
661680
consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node);
662681
}
663682

664-
if (!node.parent && formattingScanner.isOnEOF()) {
665-
const token = formattingScanner.readEOFTokenRange();
666-
if (token.end <= node.end && previousRange) {
667-
processPair(
668-
token,
669-
sourceFile.getLineAndCharacterOfPosition(token.pos).line,
670-
node,
671-
previousRange,
672-
previousRangeStartLine,
673-
previousParent,
674-
contextNode,
675-
nodeDynamicIndentation);
676-
}
677-
}
678-
679683
function processChildNode(
680684
child: Node,
681685
inheritedIndentation: number,
@@ -717,9 +721,12 @@ namespace ts.formatting {
717721
return inheritedIndentation;
718722
}
719723

720-
while (formattingScanner.isOnToken()) {
724+
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
721725
// proceed any parent tokens that are located prior to child.getStart()
722726
const tokenInfo = formattingScanner.readTokenInfo(node);
727+
if (tokenInfo.token.end > originalRange.end) {
728+
return inheritedIndentation;
729+
}
723730
if (tokenInfo.token.end > childStartPos) {
724731
if (tokenInfo.token.pos > childStartPos) {
725732
formattingScanner.skipToStartOf(child);
@@ -731,7 +738,7 @@ namespace ts.formatting {
731738
consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node);
732739
}
733740

734-
if (!formattingScanner.isOnToken()) {
741+
if (!formattingScanner.isOnToken() || formattingScanner.getStartPos() >= originalRange.end) {
735742
return inheritedIndentation;
736743
}
737744

@@ -773,7 +780,7 @@ namespace ts.formatting {
773780

774781
if (listStartToken !== SyntaxKind.Unknown) {
775782
// introduce a new indentation scope for lists (including list start and end tokens)
776-
while (formattingScanner.isOnToken()) {
783+
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
777784
const tokenInfo = formattingScanner.readTokenInfo(parent);
778785
if (tokenInfo.token.end > nodes.pos) {
779786
// stop when formatting scanner moves past the beginning of node list
@@ -814,7 +821,7 @@ namespace ts.formatting {
814821
}
815822

816823
const listEndToken = getCloseTokenForOpenToken(listStartToken);
817-
if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) {
824+
if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
818825
let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent);
819826
if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) {
820827
const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line;
@@ -969,7 +976,7 @@ namespace ts.formatting {
969976
previousStartLine: number,
970977
previousParent: Node,
971978
contextNode: Node,
972-
dynamicIndentation: DynamicIndentation): LineAction {
979+
dynamicIndentation: DynamicIndentation | undefined): LineAction {
973980

974981
formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode);
975982

@@ -982,24 +989,26 @@ namespace ts.formatting {
982989
// win in a conflict with lower priority rules.
983990
forEachRight(rules, rule => {
984991
lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine);
985-
switch (lineAction) {
986-
case LineAction.LineRemoved:
987-
// Handle the case where the next line is moved to be the end of this line.
988-
// In this case we don't indent the next line in the next pass.
989-
if (currentParent.getStart(sourceFile) === currentItem.pos) {
990-
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
991-
}
992-
break;
993-
case LineAction.LineAdded:
994-
// Handle the case where token2 is moved to the new line.
995-
// In this case we indent token2 in the next pass but we set
996-
// sameLineIndent flag to notify the indenter that the indentation is within the line.
997-
if (currentParent.getStart(sourceFile) === currentItem.pos) {
998-
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
999-
}
1000-
break;
1001-
default:
1002-
Debug.assert(lineAction === LineAction.None);
992+
if (dynamicIndentation) {
993+
switch (lineAction) {
994+
case LineAction.LineRemoved:
995+
// Handle the case where the next line is moved to be the end of this line.
996+
// In this case we don't indent the next line in the next pass.
997+
if (currentParent.getStart(sourceFile) === currentItem.pos) {
998+
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode);
999+
}
1000+
break;
1001+
case LineAction.LineAdded:
1002+
// Handle the case where token2 is moved to the new line.
1003+
// In this case we indent token2 in the next pass but we set
1004+
// sameLineIndent flag to notify the indenter that the indentation is within the line.
1005+
if (currentParent.getStart(sourceFile) === currentItem.pos) {
1006+
dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode);
1007+
}
1008+
break;
1009+
default:
1010+
Debug.assert(lineAction === LineAction.None);
1011+
}
10031012
}
10041013

10051014
// 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

src/services/formatting/formattingScanner.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace ts.formatting {
55

66
export interface FormattingScanner {
77
advance(): void;
8+
getStartPos(): number;
89
isOnToken(): boolean;
910
isOnEOF(): boolean;
1011
readTokenInfo(n: Node): TokenInfo;
@@ -49,6 +50,7 @@ namespace ts.formatting {
4950
lastTrailingTriviaWasNewLine: () => wasNewLine,
5051
skipToEndOf,
5152
skipToStartOf,
53+
getStartPos: () => lastTokenInfo?.token.pos ?? scanner.getTokenPos(),
5254
});
5355

5456
lastTokenInfo = undefined;
@@ -265,8 +267,7 @@ namespace ts.formatting {
265267

266268
function isOnToken(): boolean {
267269
const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken();
268-
const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos();
269-
return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current);
270+
return current !== SyntaxKind.EndOfFileToken && !isTrivia(current);
270271
}
271272

272273
function isOnEOF(): boolean {

src/services/utilities.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -2726,23 +2726,23 @@ namespace ts {
27262726
return typeIsAccessible ? res : undefined;
27272727
}
27282728

2729-
export function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) {
2729+
function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) {
27302730
return kind === SyntaxKind.CallSignature
27312731
|| kind === SyntaxKind.ConstructSignature
27322732
|| kind === SyntaxKind.IndexSignature
27332733
|| kind === SyntaxKind.PropertySignature
27342734
|| kind === SyntaxKind.MethodSignature;
27352735
}
27362736

2737-
export function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) {
2737+
function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) {
27382738
return kind === SyntaxKind.FunctionDeclaration
27392739
|| kind === SyntaxKind.Constructor
27402740
|| kind === SyntaxKind.MethodDeclaration
27412741
|| kind === SyntaxKind.GetAccessor
27422742
|| kind === SyntaxKind.SetAccessor;
27432743
}
27442744

2745-
export function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) {
2745+
function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) {
27462746
return kind === SyntaxKind.ModuleDeclaration;
27472747
}
27482748

@@ -2831,21 +2831,37 @@ namespace ts {
28312831
forEachChild(sourceFile, function visit(node): boolean | undefined {
28322832
if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) {
28332833
const lastToken = node.getLastToken(sourceFile);
2834-
if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) {
2834+
if (lastToken?.kind === SyntaxKind.SemicolonToken) {
28352835
withSemicolon++;
28362836
}
28372837
else {
28382838
withoutSemicolon++;
28392839
}
28402840
}
2841+
else if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) {
2842+
const lastToken = node.getLastToken(sourceFile);
2843+
if (lastToken?.kind === SyntaxKind.SemicolonToken) {
2844+
withSemicolon++;
2845+
}
2846+
else if (lastToken && lastToken.kind !== SyntaxKind.CommaToken) {
2847+
const lastTokenLine = getLineAndCharacterOfPosition(sourceFile, lastToken.getStart(sourceFile)).line;
2848+
const nextTokenLine = getLineAndCharacterOfPosition(sourceFile, getSpanOfTokenAtPosition(sourceFile, lastToken.end).start).line;
2849+
// Avoid counting missing semicolon in single-line objects:
2850+
// `function f(p: { x: string /*no semicolon here is insignificant*/ }) {`
2851+
if (lastTokenLine !== nextTokenLine) {
2852+
withoutSemicolon++;
2853+
}
2854+
}
2855+
}
2856+
28412857
if (withSemicolon + withoutSemicolon >= nStatementsToObserve) {
28422858
return true;
28432859
}
28442860

28452861
return forEachChild(node, visit);
28462862
});
28472863

2848-
// One statement missing a semicolon isnt sufficient evidence to say the user
2864+
// One statement missing a semicolon isn't sufficient evidence to say the user
28492865
// doesn’t want semicolons, because they may not even be done writing that statement.
28502866
if (withSemicolon === 0 && withoutSemicolon <= 1) {
28512867
return true;

tests/cases/fourslash/completionsOverridingMethod5.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
////abstract class Ab {
77
//// abstract met(n: string): void;
88
//// met2(n: number): void {
9-
////
9+
//// return;
1010
//// }
1111
////}
1212
////
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
4+
//// /*1*/var x = 1;/*2*/
5+
//// void 0;
6+
7+
format.setOption("semicolons", "remove");
8+
format.selection("1", "2");
9+
verify.currentFileContentIs(
10+
`var x = 1
11+
void 0;`
12+
);

tests/cases/fourslash/refactorExtractType12.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ edit.applyRefactor({
1111
refactorName: "Extract type",
1212
actionName: "Extract to type alias",
1313
actionDescription: "Extract to type alias",
14-
newContent: `type /*RENAME*/NewType = string;
14+
newContent: `type /*RENAME*/NewType = string
1515
1616
interface A<T = NewType> {
1717
a: boolean

tests/cases/fourslash/refactorExtractType13.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ edit.applyRefactor({
1111
refactorName: "Extract type",
1212
actionName: "Extract to type alias",
1313
actionDescription: "Extract to type alias",
14-
newContent: `type /*RENAME*/NewType = boolean;
14+
newContent: `type /*RENAME*/NewType = boolean
1515
1616
interface A<T = string> {
1717
a: NewType

0 commit comments

Comments
 (0)