Subject: bug#61176: post-command-hook is not run if minibuffer input is aborted

From: Jonas Bernoulli <[email-masked]>
Date: Mon, 30 Jan 2023 16:07:30 +0100

> -- Variable: post-command-hook
>     This normal hook is run by the editor command loop after it
>     executes each command (including commands terminated prematurely by
>     quitting or by errors).  At that time, ‘this-command’ refers to the
>     command that just ran, and ‘last-command’ refers to the command
>     before that.
>
>     This hook is also run when Emacs first enters the command loop (at
>     which point ‘this-command’ and ‘last-command’ are both ‘nil’).

- post-command-hook is run even if a command is "terminated prematurely
  by quitting or by errors".  This is very useful when it is crucial
  that some cleanup is run even when something goes wrong.

- When a command uses the minibuffer, then post-command-hook is
  additionally run when the minibuffer is setup, with this-command
  being the command that uses the minibuffer.  This happens before
  minibuffer-setup-hook is run.

  This is surprising, because undocumented, but easy to detect, because
  at this time this-command-keys-vector returns an empty vector.  Never-
  the-less this should be documented (instead of being "fixed"; I depend
  on this behavior, and so might others).

- However, when the command reads from the minibuffer and the user
  aborts that, then post-command-hook is NOT run a second time AFTER
  the command.

  This is extremely inconvenient.  IMO, the fact that this hook is
  documented to run even if the command "terminated prematurely by
  quitting or by errors", implies that the hook is run even if the
  quitting is done intentionally by the user.

  This could be fixed simply by running post-command-hook in this case
  as well.  If that is considered a dangerous change in behavior, then
  maybe a very similar hook --say unwind-command-hook-- could be added.

Cheers,
Jonas

Oh -- here's some code that can be used to observe this behavior:

(keymap-global-set "<f1>" '-command)
(keymap-global-set "<f2>" '-prepare)
(keymap-global-set "<f3>" '-cleanup)

(defun -prepare ()
  (interactive)
  (add-hook 'post-command-hook '-post)
  (add-hook 'minibuffer-setup-hook '-setup)
  (add-hook 'minibuffer-exit-hook '-exit))

(defun -cleanup ()
  (interactive)
  (remove-hook 'post-command-hook '-post)
  (remove-hook 'minibuffer-setup-hook '-setup)
  (remove-hook 'minibuffer-exit-hook '-exit))

(defun -post ()
  (message ";; -post    (%-10s %s)" (this-command-keys-vector) this-command))

(defun -setup ()
  (message ";; -setup   (%-10s %s)" (this-command-keys-vector) this-command))

(defun -exit ()
  (message ";; -exit    (%-10s %s)" (this-command-keys-vector) this-command))

(defun -command ()
  (interactive)
  (message ";; -command"))

;; -command
;; -post    ([f1]       -command)

(defun -command ()
  (interactive)
  (message ";; -command")
  (error "error in command"))

;; -command
;; -command: error in command
;; -post    ([]         -command)

(defun -command ()
  (interactive (error "error in interactive"))
  (message ";; -command"))

;; call-interactively: error in interactive
;; -post    ([]         -command)

(defun -command (arg)
  (interactive (list (read-string ": ")))
  (message ";; -command"))

;; -setup   ([f1]       -command)
;; -post    ([]         -command)
;; -post    ([97]       self-insert-command)
;; -exit    ([return]   exit-minibuffer)
;; -command
;; -post    ([f1]       -command)

;; -setup   ([f1]       -command)
;; -post    ([]         -command)
;; -exit    ([7]        abort-minibuffers)
;; Quit
;; -post    ([]         abort-minibuffers)

(defun -command ()
  (interactive)
  (message ";; -command")
  (read-string "-command: "))

;; -setup   ([f1]       -command)
;; -post    ([]         -command)
;; -post    ([97]       self-insert-command)
;; -exit    ([return]   exit-minibuffer)
;; -post    ([f1]       exit-minibuffer)

;; -setup   ([f1]       -command)
;; -post    ([]         -command)
;; -exit    ([return]   exit-minibuffer)
;; -post    ([f1]       exit-minibuffer)

(defun -command (arg)
  (interactive
   (list (read-from-minibuffer
	  ": " nil (let ((map (make-sparse-keymap)))
		     (set-keymap-parent map minibuffer-local-map)
		     (define-key map "a"
		       (lambda () (interactive)
			 (error "error in minibuffer")))
		     map))))
  (message ";; -command"))

;; -setup   ([f1]       -command)
;; -post    ([]         -command)
;; (lambda nil (interactive) (error error in minibuffer)): error in minibuffer
;; -post    ([]         (lambda nil (interactive) (error error in minibuffer)))
;; -exit    ([return]   exit-minibuffer)
;; -command
;; -post    ([f1]       -command)