Skip to content

Conversation

ds300
Copy link
Contributor

@ds300 ds300 commented Apr 29, 2018

Howdy! 👋 😄

This PR is fixing a few things related to the cursorOffset option:

  1. Fix two bugs that were present before

    • If the cursor appeared before any ast nodes, it would error. (at least in TypeScript)
    • The cursor tracking was not accounting for comments preceding a node
  2. Improve tracking with a more nuanced approach (fixes --cursor-offset output is wrong sometimes #1981)

  3. Allow cursorOffset to be used with rangeStart and rangeEnd (fixes Allow cursorOffset to be used with rangeStart and rangeEnd #1869)

I'll explain how I did number 2, to help you read this PR.

How cursor offset was tracked before, in broad terms:

  1. find the 'cursor node', i.e. the smallest AST node which encompasses the cursor.
  2. calculate the relative cursor offset from the start of that node
  3. track that node until the final output text
  4. calculate the final cursorOffset as being the start position of the cursor node in the final text + the relative offset calculated in step 2.

This was producing bad results because if the text between the start of the AST node and the cursor would change (e.g. whitespace being inserted or deleted) then the offset calculated in step 2 is useless and should be ignored.

The approach I take in this PR is to use that relative offset as long as the text didn't change. Otherwise, I calculate a character diff between the input and output text for the cursor node, and then figure out where the best place for the cursor would be from that diff.

In order to do that, i had to "wrap" the printed version of the cursor node using two 'cursor placeholder's, one at either end. Previously there was only one inserted at the beginning.

Let me know if you have any more questions.

Thanks for the amazing work on Prettier. I can't live without it. 🙇

Copy link
Member

@j-f1 j-f1 left a comment

Choose a reason for hiding this comment

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

Awesome!



const z = 9
`)
Copy link
Member

Choose a reason for hiding this comment

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

What about

const y = 5
<|>



const z = 9

IMO that should either put it on the blank line between the two or right after the 5.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it puts it on the blank line between the two. I added this test case just now 👍

@ds300 ds300 force-pushed the fix/cursor-everything branch from b57f553 to d7f54bf Compare April 29, 2018 22:50
src/main/core.js Outdated
// diff old and new cursor node texts, with a special cursor
// symbol inserted to find out where it moves to

const CURSOR = Symbol("cursor");
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can move this to the file top level (together with UTF8BOM)

src/main/core.js Outdated
for (const entry of cursorNodeDiff) {
if (entry.removed) {
if (entry.value.indexOf(CURSOR) > -1) {
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe?

if (entry.removed && entry.value.indexOf(CURSOR) > -1) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not the same thing, because there's an else clause. Would need to change the else to else if (!entry.removed) in order for that to work, which IMO is a bit more confusing. But happy to change it if you prefer.

Copy link
Collaborator

@duailibe duailibe Apr 30, 2018

Choose a reason for hiding this comment

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

Oops sorry! No need to change

src/main/core.js Outdated
return text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd);
return {
formatted: formatted,
cursorOffset: cursorOffset
Copy link
Collaborator

Choose a reason for hiding this comment

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

return { formatted, cursorOffset }

=)

src/main/core.js Outdated
result = Object.assign({}, result, {
formatted: trimmed + opts.newLine,
cursorNodeStart
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can change the result object here

Copy link
Member

Choose a reason for hiding this comment

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

Removing result = and {}, should have that effect.

src/main/core.js Outdated
} else {
i += entry.count;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

let cursorOffset = newCursorNodeStart;

for (const entry of cursorNodeDiff) {
  /* code */
  cursorOffset += entry.count;
}

return { formatted: result.formatted, cursorOffset };

"original": "return <|> 15",
}
`;

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can improve these snapshots, see below

@duailibe
Copy link
Collaborator

I pushed a change to use our current infrastructure for tests, it's been working really well.. just create files in tests/cursor/, run tests and it'll save snapshots with only the code. Objects like { original: "", formated: "" } in snapshots have too much noise

@ds300
Copy link
Contributor Author

ds300 commented Apr 30, 2018

Cool stuff with the snapshots! I'll try to get around to those small changes later today.

@ds300
Copy link
Contributor Author

ds300 commented Apr 30, 2018

All done @duailibe !

@duailibe
Copy link
Collaborator

duailibe commented Apr 30, 2018

@ds300 Looks good!

I forgot to mention I only moved some of the tests to snapshots to show how to do it. What do you think making them all like that? Just put each test in a new file (don't worry about having too many) :)

@ds300
Copy link
Contributor Author

ds300 commented Apr 30, 2018

Haha, I wasn't sure whether you'd finish. I did the rest just now. Cheers!

@duailibe duailibe merged commit d77f5e9 into prettier:master May 1, 2018
@duailibe
Copy link
Collaborator

duailibe commented May 1, 2018

Thank you!!! 💯 👏 🎉

Copy link
Contributor

@vjeux vjeux left a comment

Choose a reason for hiding this comment

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

Thanks for fixing all those issues! I believed that cursor preservation was going to be critical and required to have a good format on save experience but it turned out I was wrong, people used it without it. But this is something that I think will provide a better user experience nonetheless!


thisWillBeFormatted(2, 3);

thisWontBeFormatted (2, 9<|>0 ,)
Copy link
Contributor

Choose a reason for hiding this comment

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

Those snapshot formats are really cool, super easy to understand what’s going on!

@ds300
Copy link
Contributor Author

ds300 commented May 2, 2018

I forgot to say that the reason I was working on this is that I was building a little toy text editor to see what it would feel like if prettier's formatting happened with a smooth animated transition. Turns out it feels great — https://code-in-motion.herokuapp.com/

@vjeux
Copy link
Contributor

vjeux commented May 2, 2018

This sounds awesome!

I’m currently in vacation with just my iphone and it doesn’t seem to be working. I’d love if you could make a small video :)

@ds300
Copy link
Contributor Author

ds300 commented May 2, 2018

sure, here's a small contrived example

codesbad

@vjeux
Copy link
Contributor

vjeux commented May 2, 2018

That looks slick! Do you do it “on save” or are you able to do it on arbitrary keystrokes that would lead to a code that actually parses?

@ds300
Copy link
Contributor Author

ds300 commented May 2, 2018

just on save so far. I tried to do it after every keystroke but it doesn't work because sometimes prettier deletes things you type like commas :) so to work automatically it would need some basic heuristics about when to format and when not to format.

But also triggering it manually gives this nice cathartic feeling, for me at least.

@lipis lipis added this to the 1.13 milestone May 9, 2018
@lock lock bot added the locked-due-to-inactivity Please open a new issue and fill out the template instead of commenting. label Aug 7, 2018
@lock lock bot locked as resolved and limited conversation to collaborators Aug 7, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
locked-due-to-inactivity Please open a new issue and fill out the template instead of commenting.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

--cursor-offset output is wrong sometimes Allow cursorOffset to be used with rangeStart and rangeEnd
5 participants