radian-software / apheleia

🌷 Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.
MIT License
523 stars 74 forks source link

Apheleia sometimes overwrites changes in my JS buffers #226

Closed DinoChiesa closed 3 months ago

DinoChiesa commented 9 months ago

Short summary: when editing JS files, I often move around pretty quickly, edit or add some text, then save, then move again, add some other code, and save. All within a handful of seconds. The "save often" thing is a sort of muscle memory / mindless habit that happens when I'm "in the zone". I don't really think about it.

Now that I'm using Aphelia, If I wait a little while after the second save, often (maybe it depends on timing?) the second set of changes disappears from my buffer. This is in emacs 29.1 . If I'm not paying attention, I don't even realize it. I just keep going. Later, when I run tests, and see confounding results, I go examine the code and see, the code I had put in place earlier is now gone.

Demonstration: https://www.youtube.com/watch?v=sppRYlaqquU

The setup for my formatter for my JS mode looks like this:

(push '(my-prettier-js .
        ("/usr/local/bin/prettier"
         "--stdin-filepath" filepath "--parser=babel-flow"
         "--trailing-comma" "none"
         (apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
      apheleia-formatters)

(push '(js-mode . my-prettier-js) apheleia-mode-alist)

I think the way I can avoid this is to wait 5-6 seconds after every save, before making any further changes. That feels a little klunky to me. I could also just save ONCE, instead of lots of times. That's a personal habit that's ingrained, and hard to change.

The README says that Apheleia is designed to not apply the reformatting, if the buffer contents have changed while running the formatter. But I am not seeing that.

I use Apheleia for various languages (sh, go, java, ...) , and JS is the only case where I've noticed this. Obviously, the other languages use other formatters.

Can you make suggestions? Have I done something wrong with setting up the formatter? how can I diagnose or debug this? Or better, how can I solve it and eliminate this behavior?

I tested the prettier binary; when I run it outside of emacs, it seems to run quickly and correctly. Results get displayed in much less than a second. I haven't looked at how long it takes when run within emacs by apheleia in the after-save-hook. Not sure this is the problem, especially since Apheleia is supposed to not apply re-formatting if the file has changed. If that's so, it shouldn't matter how long the formatter takes to run, right?

I really like Apheleia, thanks for writing it, it's mostly really good. But it edges into chaotic evil territory when some of my changes get lost, some of the time!

Thanks

DinoChiesa commented 9 months ago

I looked into it further. It does seem to take 5-8 seconds between formatting and producing the diff. I don't know why that is the case. Even so, by design the thing I'm observing shouldn't happen anyway, right?

A possible explanation: race condition? apheleia-format-buffer is setting the buffer-local var apheleia--buffer-hash . This value gets overwritten by the after-save-hook invocation corresponding to the 2nd save. The "short circuit" check compares the current hash with the stored hash. Because the value has been overwritten, it looks like there have been no changes. And that's true: since the 2nd save, there have been no changes. But the re-formatting from the 1st save somehow gets applied. I am not sure why the reformatting from the 2nd save does not also get applied. Maybe there is a race condition and sometimes the re-formatting for the 2nd save finishes first?

If there were a way to "de-bounce" the re-formatting and only allow the results of the the LAST reformatting run to be started (not necessarily the last one to finish), that might solve the problem.

Another solution might be to use a lexically bound variable to store the "before state" buffer hash value, rather than a buffer-local variable (apheleia--buffer-hash). That would avoid the overwrite of the buffer-local var. There'd be two separate values, one for each run of apheleia-format-buffer. I am trying that now, so far it seems to work to avoid the problem.

It might partially mitigate the problem if I could make the re-format-and-then-diff happen more quickly than 8 seconds. Maybe there is something special in my environment causing it to be slow. If reformatting happened more quickly, It would also make the user experience better. if you have suggestions on diagnosing and solving that, please share. But in any case, speeding it up would not solve the problem completely.

raxod502 commented 9 months ago

Thanks for the detailed report. I agree with your diagnosis, I will see about implementing the best solution right now.

raxod502 commented 9 months ago

Merged a PR implementing your lexical variable idea. Let's take a look at the performance issue now.

raxod502 commented 9 months ago

Merged a PR adding detailed logging information with timestamps. Try it again with https://github.com/radian-software/apheleia/pull/235 and post the log, maybe we can determine what is so slow based on that output.

DinoChiesa commented 3 months ago

I had a separate problem that was causing the perf issues, which made the race condition much more likely. But I've solved that and you fixed the variable, and I think we're all good now. thank you!