Kill Unsaved Emacs Buffers UX: Replacing Yes/No/Save with Meaningful Options

Upgrading to Emacs 29.1 worked like a charm.

There’ve been a couple of small neat additions next to all the big changes like native tree-sitter support – and there’s one in particular that you could say I almost hate.

Kill Anyway? Yes, No, Save and kill?!

When I compose email and decide to discard everything, I’m politely being asked whether I want to kill (aka ‘close’) the buffer and discard all changes. This applies to all other buffers that haven’t been saved, too, but I mostly run into this with email.

The confirmation step before killing a buffer is a sensible thing to do to prevent accidental data loss. Great idea.

It used to be a yes-or-no question, where “yes” continued the process (of killing the modified buffer) and “no” canceled it.

I’m not a fan of yes-or-no questions in Emacs, to be honest.

It’s a standard mechanism in Emacs, but it’s also used in many different ways. Sometimes, “yes” will discard changes, sometimes “yes” will save things, depending on context. The options are not meaningful, they require knowledge of the context, and so they are only a slight improvement over “OK” and “Cancel” – which is looked down upon for good reason in confirmation dialogs since Windows 95.

Emacs 29 adds a third option, a convenience option, to kill the buffer and save the contents. It’s being added to the yes-or-no convention, though, in an odd way.

Emacs 29.1 presents it like so (via kill-buffer--possibly-save, see source code):

Buffer MyAwesomeEmail.txt modified; kill anyway?

  • yes
  • no
  • save and then kill

It used to be just yes-or-no, and it became muscle memory over the past years. With the addition of the third option, I involuntarily pause for a heartbeat. Sometimes only after performing the muscle-memory-action, wondering if I did the right thing, because some meaning trickles into my consciousness only at a delay here.

It’s like this: The option to “save and then kill”, which I scan/read at a glance, changes the semantics of all the rest.

It’s basically: “save”, “yes”, “no”, that I’m noticing I am trying to parse automatically, where it used to be “kill? yes, no”.

But these three options don’t make sense!

Not that one action is “save”, what is “yes”? It’s discarding the contents. Even though it’s phrased affirmatively, it’s affirming “kill anyway?”, not the verb among the options, which is “save”. The verbs don’t gel well.

Also, how is “save” an appropriate answer to a yes-no question like “kill anyway?”?

This confuses the hell out of me. So I got rid of this.

Rationale for my replacement

This is how I render the question in my setup

Below, I’m sharing with you my drop-in replacement which I use to overwrite the confirmation function. It does a couple of things differently.

  • Since “kill anyway?”, the question that’s been used since forever in this confirmation, doesn’t work with anything except “yes” and “no”, I’m dropping that altogether. Let the options carry the weight.
  • I’m replacing “yes” and “no” with strong options. This is my main UX contribution: to offer options that tell what the effect is.

The confirmation will be presented like so:

Buffer MyAwesomeEmail.txt modified.

  • Save and kill buffer
  • Discard and kill buffer without saving
  • Cancel

“Cancel” replaces “no”, which would abort the process of “kill anyway?”, not the “save” part.

“Save and kill buffer” replaces “save and then kill”. I couldn’t find anything wrong with this new 3rd option on its own, except that it didn’t make sense in a yes-no question.

“Discard and kill buffer without saving” puts “discard” in the front. I settled on this because “kill buffer without saving” on its own, or in the front of a longer phrase, didn’t work as well for me when I was scanning the options. I basically read these as “Save”, “Discard”, “Cancel”, and that suffices to pick the correct one.

My choice of actions will probably be too long for the purists. With ido, for example, I’d rather see a much shorter “save”/”discard”/”cancel”. But I’m using vertico and select the options from a list.

Alternatives

Xah Lee improved this years ago with a custom xah-close-current-buffer function that asks the user: “Buffer %s modified; Do you want to save?”, where the affimation with “yes” is the safe one, not the dangerous one, and to abort the process, you just use C-g.

That approach is already able to handle all three cases: save and kill (“yes”), discard and kill (“no”), and cancel (hit C-g).

It may not sound like much, but this is actually a much better use of the yes-or-no prompt. It composes much better with the existing Emacs approach: C-g to abort the current command is always available, and that’s exactly what the “no” answer in the old yes-or-no question “kill anyway?” was doing.

Consequently, we can say that the “kill anyway?” question only effectively added the “yes” response to discard changes and duplicated C-g, while Xah Lee improved the phrase and added more powerful and useful options.

kill-buffer--possibly-save Replacement

I didn’t want to deviate from the Emacs 29 triple of answers even though “save an kill” and “discard and kill” are enough, and “cancel” is superfluous. Xah Lee’s approach is superior, though.

So here’s my replacement of kill-buffer--possibly-save in simple.el:

;; kill-with-intelligence.el --- Making buffer killing suck less. -*- lexical-binding: t -*-
;;
;;; Commentary:
;; Original function at the time of this writing is at:
;; https://github.com/emacs-mirror/emacs/blob/3907c884f03cf5f2a09696bda015b1060c7111ba/lisp/simple.el#L10980
;;
;;; Code:

(defun ct/kill-buffer--possibly-save--advice (original-function buffer &rest args)
  "Ask user in the minibuffer whether to save before killing.

Replaces `kill-buffer--possibly-save' as advice, so
ORIGINAL-FUNCTION is unused and never delegated to. Its first
parameter is the buffer, which is the `car' or ARGS."
  (let ((response
         (car
          (read-multiple-choice
           (format "Buffer %s modified."
                   (buffer-name))
           '((?s "Save and kill buffer" "save the buffer and then kill it")
             (?d "Discard and kill buffer without saving" "kill buffer without saving")
             (?c "Cancel" "Exit without doing anything"))
           nil nil (and (not use-short-answers)
                        (not (use-dialog-box-p)))))))
    (cond ((= response ?s)
           (with-current-buffer buffer (save-buffer))
           t)
          ((= response ?d)
           t)
          ((= response ?c)
           nil)
          )))

(advice-add 'kill-buffer--possibly-save :around #'ct/kill-buffer--possibly-save--advice)

;;;

(provide 'kill-with-intelligence)
;; kill-with-intelligence.el ends here

Update 2023-10-09: Updated code to address feedback by PhilHudson.