0

I've got a file open in Emacs, with unsaved changes.

I run a function in Emacs which first writes the current changes to a separate temporary file, then delegates to an interactive shell command which lets me pick and choose which of those changes from the temporary file to apply to the actual file.

So at the end of that function, it's possible that the file has all of the unsaved changes saved to it, or some, or none.

Is there some Emacs function I can run to force Emacs to check if the file now matches the buffer, and unset the modified flag if so?

4
  • Try using buffer-modified-p and set-buffer-modified-p. Commented Sep 12, 2023 at 11:14
  • @TristanRiehs buffer-modified-p is basically just a getter on a boolean - it doesn't actually check if the file has changed since the last time that flag was updated. But set-buffer-modified-p is promising - it triggers the "[file name] changed on disk; really edit the buffer?" prompt.
    – mtraceur
    Commented Sep 12, 2023 at 14:48
  • @TristanRiehs ..the next problem is that whatever check set-buffer-modfied-p is doing, it's not sharing. It knows if the file contents match buffer contents (because it prompts only if they don't match), but if the files are actually the same it goes and sets the flag anyway. (Next idea is that I can hook into that, by giving it a custom "prompt" function which just sets a boolean if it's called and returns whatever it wants to proceed.)
    – mtraceur
    Commented Sep 12, 2023 at 15:06
  • @TristanRiehs I managed to figure it out. Thanks for the lead on set-buffer-modified-p! I was very confident based on the documentation that it was just a dumb setter of the flag internal to Emacs, and it was thanks to your comment that I ended up testing it just to be absolutely sure.
    – mtraceur
    Commented Sep 12, 2023 at 22:46

1 Answer 1

0

Emacs has this capability built in, but it doesn't seem to be exposed in an API by itself, so we have to coax it out.

The built-in set-buffer-modified-p will sometimes check if file contents match buffer contents, as part of deciding whether to do that {{file name}} changed on disk; really edit buffer? prompt which you'll sometimes see in the minibuffer. But it only does that check if

  1. the call is setting the modified flag to true,
  2. the buffer's modified flag is currently cleared (Emacs believes the buffer is not modified), and
  3. the buffer's last-modified time is more recent than what Emacs currently remembers as the underlying file's last-modified time.

(None of this is documented or guaranteed as stable API as far as I know. I just figured this out empirically, by testing the behavior. And we'd have to look in the C implementation of set-buffer-modified-p in Emacs source code to rule out the possibility of some Emacs config that ends up changing/bypassing this behavior.)

So what we have to do is basically three tricks:

  1. clear the modified flag,

  2. set the cached "when was the underlying file last modified?" value far enough back in time (by calling set-visited-file-modtime - I ended up using '(0 0) for the time argument, which I presume will be earlier than any last-modified time this code will ever encounter), and

  3. run (set-buffer-modified-p t) such that our own code runs when the "really edit buffer?" prompt would happen - if and only if this happens, the buffer and file have different contents (by either redefining or advising ask-user-about-supersession-threat).

So putting that all together:

(defun buffer-differs-from-visited-file-p ()
    (with-visited-file-modtime '(0 0)
        (let ((differs nil))
            (with-advice 'ask-user-about-supersession-threat
                             :override (lambda (_) (setq differs t))
                (with-buffer-modified-p nil
                    (set-buffer-modified-p t)
                    differs)))))

; All the `unwind-protect` boilerplate wrapped up in macros:

(defmacro with-buffer-modified-p (flag &rest body)
    `(let ((--with-buffer-modified-p-- (buffer-modified-p)))
        (unwind-protect
            (progn
                (set-buffer-modified-p ,flag)
                ,@body)
            (set-buffer-modified-p --with-buffer-modified-p--))))

(defmacro with-visited-file-modtime (time-flag &rest body)
    `(let ((--with-visited-file-modtime-- (visited-file-modtime)))
        (unwind-protect
            (progn
                (set-visited-file-modtime ,time-flag)
                ,@body)
            (set-visited-file-modtime --with-visited-file-modtime--))))

(defmacro with-advice (symbol where function &rest body)
    `(unwind-protect
        (progn
            (advice-add ,symbol ,where ,function)
            ,@body)
        (advice-remove ,symbol ,function)))

Now we can simply call (buffer-differs-from-visited-file-p), and since it cleans up after itself, it's safe to use even if we just want to check for differences without changing the modified state.

Based on that, we can build a complete answer to this question:

  1. The core of the answer is just to run (set-buffer-modified-p (buffer-differs-from-visited-file-p)).

  2. We should also call (set-visited-file-modtime) with no arguments, to make sure Emacs re-reads the last-modified time of the file.

  3. For a more general solution:

    • In case buffer-modified-p is false but now the underlying file differs from the buffer, we need suppress the supersession threat prompt, similar to how we did above.

    • If the file being visited by the buffer does not exist, we should do something useful. I chose to immitate Emacs' own behavior: when you first run find-file, the file does not exist, the buffer has no characters in it, and the modified flag is nil. An ideal solution would probably let the caller choose between that, always marking the buffer as modified, or signaling an error (because the correct behavior in this case depends on external knowledge, such as whether or not the file is supposed to already/still exist when doing the check).

    • If buffer-file-name is nil (buffer is not visiting a file), we should do something useful. I chose to silently do nothing, since there's no modified state to refresh. An ideal solution would maybe let the caller choose between that and raising and error, and/or allow replacing the behaviour (like how revert-buffer can be customized by setting revert-buffer-function) (because the correct behavior in this case depends on why your code is checking modified state, and the user's UX preferences/expectations).

    • Might as well make the function interactive, in case you ever want to manually force a refresh of buffer modified state from M-x or bind it to a key.

Which brings us, finally, to:

(defun refresh-modified-state (&optional buffer)
    (interactive)
    (unless buffer
        (setq buffer (current-buffer)))
    (with-current-buffer buffer
        (when buffer-file-name
            (let ((differs (if (file-exists-p buffer-file-name)
                               (buffer-differs-from-visited-file-p)
                               (> (buffer-size) 0))))
                (with-advice 'ask-user-about-supersession-threat
                                 :override 'ignore
                    (set-buffer-modified-p differs)))
            (set-visited-file-modtime))))
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.