Skip to content

Fix formatting the end of a selection #46875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 29, 2021

Conversation

andrewbranch
Copy link
Member

I thought I filed a bug for this in the past, but I can't find it. Formatting works by advancing the scanner token by token and deciding if any rules apply for a pair of consecutive tokens. When you format a specific range, we used to stop the scanner as soon as it advanced outside that range. But that means we usually failed to apply the last edit inside the selection, because we sometimes need to compare the last token within the selection with the first token after the selection to come up with that edit. This should unblock #46832.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Nov 19, 2021
break;
}
consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node);
}

if (!node.parent && formattingScanner.isOnEOF()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moved up a level in the call stack, outside the recursive processNode function. It was checking !node.parent as a way to execute only at the top of that recursive chain, but it’s simpler to just move it out of the recursion altogether. Also, in #46832, Gabriela is calling into this with synthetic nodes that don't have parent pointers set. We can and maybe should set them, but this was a bit of a hack and it's much cleaner and safer to move it.

Comment on lines +441 to +445
if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) {
const token =
formattingScanner.isOnEOF() ? formattingScanner.readEOFTokenRange() :
formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(enclosingNode).token :
undefined;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s where that block of code moved to, with some minor changes. We previously handled edits at the end of a file in this block by getting a token representation of the EOF token and passing it to processPair. But for the same to work with formatting a selection, we’ll probably be dealing with a regular token instead of an EOF.

@@ -717,9 +721,12 @@ namespace ts.formatting {
return inheritedIndentation;
}

while (formattingScanner.isOnToken()) {
while (formattingScanner.isOnToken() && formattingScanner.getStartPos() < originalRange.end) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isOnToken() used to return false if the scanner moved past the end of the original range. Since we need to let it move past that range to apply the last edit, I removed that condition and inlined it where it was called previously.

previousRangeStartLine!,
previousParent!,
enclosingNode,
/*dynamicIndentation*/ undefined);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly have no idea what the right value for this is and if it ever matters, but it didn’t matter for any existing tests, so I made it optional and skipped line/indentation edits in the implementation if it’s not passed.

Copy link
Member

@gabritto gabritto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't believe this is happening ✨
also, I don't know enough about the formatter to confidently review this PR, but I didn't see anything immediately wrong and the tests look ok 🤷‍♀️

@@ -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 };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't believe the fix here was so simple 🤦‍♀️ thanks!

@@ -265,8 +267,7 @@ namespace ts.formatting {

function isOnToken(): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for my own understanding, what is isOnToken supposed to do and also why were we checking if startPos < endPos before and are not checking it now?
I guess a pre-requisite question for that: what are startPos and endPos and the several poss supposed to be?

Copy link
Member

@gabritto gabritto Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you answered the first part of my question already in another comment, didn't see the comment when I was reviewing

@@ -438,6 +438,25 @@ namespace ts.formatting {
}
}

if (previousRange! && formattingScanner.getStartPos() >= originalRange.end) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need the bang in previousRange! here?

Copy link
Member Author

@andrewbranch andrewbranch Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript is complaining that I’m using it before initialization, but it gets initialized in child functions. It’s complaining because the type annotation doesn’t include undefined, so it wants to prevent me from using it where it could be undefined. I think it actually could be undefined since I don’t think the child functions are guaranteed to run. Probably I should just annotate it with | undefined, but then I need to add bangs in multiple other places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for clarifying

withSemicolon++;
}
else {
withoutSemicolon++;
}
}
else if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is tangentially related; it just allows us to infer a preference for semicolonless style from property and method signatures, as long as they don’t end in a comma or are in a single-line object type kinda thing where it’s very common to omit punctuation altogether, even among semicolon users.

@andrewbranch andrewbranch merged commit 68bf5a5 into microsoft:main Nov 29, 2021
@andrewbranch andrewbranch deleted the bug/formatSelection branch November 29, 2021 17:27
mprobst pushed a commit to mprobst/TypeScript that referenced this pull request Jan 10, 2022
* Fix formatting edits at end of range

* Adjust test

* Revert API baseline changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants