From b7355e30afe6440487dfb4e19c6cbd0758f71846 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 28 Jul 2022 12:11:22 -0700 Subject: [PATCH] Fix trailing formatting edit when range ends mid-token (#50082) --- src/services/formatting/formatting.ts | 25 ++++++++++++++++++- .../fourslash/formatTemplateStringOnPaste.ts | 6 +++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/cases/fourslash/formatTemplateStringOnPaste.ts diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index 3f81d4d374d..3b8534b8ef3 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -403,6 +403,7 @@ namespace ts.formatting { // formatting context is used by rules provider const formattingContext = new FormattingContext(sourceFile, requestKind, options); + let previousRangeTriviaEnd: number; let previousRange: TextRangeWithKind; let previousParent: Node; let previousRangeStartLine: number; @@ -439,12 +440,32 @@ namespace ts.formatting { } if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) { + // Formatting edits happen by looking at pairs of contiguous tokens (see `processPair`), + // typically inserting or deleting whitespace between them. The recursive `processNode` + // logic above bails out as soon as it encounters a token that is beyond the end of the + // range we're supposed to format (or if we reach the end of the file). But this potentially + // leaves out an edit that would occur *inside* the requested range but cannot be discovered + // without looking at one token *beyond* the end of the range: consider the line `x = { }` + // with a selection from the beginning of the line to the space inside the curly braces, + // inclusive. We would expect a format-selection would delete the space (if rules apply), + // but in order to do that, we need to process the pair ["{", "}"], but we stopped processing + // just before getting there. This block handles this trailing edit. const tokenInfo = formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() : formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token : undefined; - if (tokenInfo) { + if (tokenInfo && tokenInfo.pos === previousRangeTriviaEnd!) { + // We need to check that tokenInfo and previousRange are contiguous: the `originalRange` + // may have ended in the middle of a token, which means we will have stopped formatting + // on that token, leaving `previousRange` pointing to the token before it, but already + // having moved the formatting scanner (where we just got `tokenInfo`) to the next token. + // If this happens, our supposed pair [previousRange, tokenInfo] actually straddles the + // token that intersects the end of the range we're supposed to format, so the pair will + // produce bogus edits if we try to `processPair`. Recall that the point of this logic is + // to perform a trailing edit at the end of the selection range: but there can be no valid + // edit in the middle of a token where the range ended, so if we have a non-contiguous + // pair here, we're already done and we can ignore it. const parent = findPrecedingToken(tokenInfo.end, sourceFile, enclosingNode)?.parent || previousParent!; processPair( tokenInfo, @@ -888,6 +909,7 @@ namespace ts.formatting { } if (currentTokenInfo.trailingTrivia) { + previousRangeTriviaEnd = last(currentTokenInfo.trailingTrivia).end; processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); } @@ -976,6 +998,7 @@ namespace ts.formatting { } previousRange = range; + previousRangeTriviaEnd = range.end; previousParent = parent; previousRangeStartLine = rangeStart.line; diff --git a/tests/cases/fourslash/formatTemplateStringOnPaste.ts b/tests/cases/fourslash/formatTemplateStringOnPaste.ts new file mode 100644 index 00000000000..f4681e090e0 --- /dev/null +++ b/tests/cases/fourslash/formatTemplateStringOnPaste.ts @@ -0,0 +1,6 @@ +/// + +//// const x = `${0}/*0*/abc/*1*/`; + +format.selection("0", "1"); +verify.currentFileContentIs("const x = `${0}abc`;");