Subject: Re: bug#72696: Track-changes errors out when file is overwritten using Node.js's fs.writeFile (at least on macOS)

From: Stefan Monnier <[email-masked]>
Date: Sun, 18 Aug 2024 12:10:37 -0400

> 1. In one shell, fire up GNU Emacs, open a file, and start something
> that uses track-changes; e.g., Eglot.  In my case it was a Python file
> called test.py with contents foo = 0.  Everything is happy so far.
> 2. In another shell, fire up a Node.js interpreter and execute const
> fs = require("node:fs/promises") followed by await
> fs.writeFile("test.py", "foo = 1").  This will overwrite the file.
> 3. Track-changes (and by extension Eglot) will error out.  Restarting
> Eglot makes everything go back to normal.
>
> The *Warnings* buffer now contains:
>
>     Warning (emacs): Missing/incorrect calls to
> ‘before/after-change-functions’!!
>     Details logged to `track-changes--error-log'

The above is only a "warning": it just logged the occurrence of something
undesired, but track-changes supposedly recovered from that.
IOW you describe two problems:

- Somehow/somewhere there is a "Missing/incorrect calls to
  ‘before/after-change-functions".
  [ But these problems are sufficiently common that track-changes has
    code devoted to detecting and handling those cases.  ]
  We want to try and fix the origin of this problem, but it's
  been with us "for ever", so it's not super extra urgent because there
  will still be other cases where it can show up.
  Eglot has had to deal with this problem since its inception, it just
  used to keep silent about it, contrary to track-changes (because
  I want to get rid of those problems).

- Eglot errors out.  This should definitely not happen and seems like
  a regression.  Restarting Eglot should definitely not be needed.

Could you clarify what you mean by Eglot erroring out and why
restarting it is necessary?

> And `track-changes--error-log' contains:
>
>     (("test.py" #1=(unexpected-after 1 8 0)
>       ((t track-changes--recover-from-error (#1#) nil)
>        (t track-changes--after (1 8 0) nil)
>        (t revert-buffer-insert-file-contents--default-function
>           ("/path/to/test.py"
>            nil)
>           nil)
>        (t revert-buffer--default (ignore-auto dont-ask) nil)
>        (t revert-buffer (ignore-auto dont-ask preserve-modes) nil)

Hmm... assuming the file really started with content "foo = 0" (and no
terminating newline), "track-changes--after (1 8 0)" looks OK: the
revert has changed the file size by 0 chars and the new content spans
chars from 1 to 8.  So my guess is that revert-buffer failed to call the
`before-changes-functions` or that it called it with a smaller region
(e.g. only the region that's actually modified (7..8), where "0" is
turned into "1").

Will have to dig deeper.

BTW, your recipe does not mention enabling auto-revert.  Have you
confirmed on your side that the problem shows up also in
a vanilla config?

        Stefan