GNU bug report logs - #61176
post-command-hook is not run if minibuffer input is aborted

Previous Next

Package: emacs;

Reported by: Jonas Bernoulli <jonas <at> bernoul.li>

Date: Mon, 30 Jan 2023 15:08:01 UTC

Severity: normal

To reply to this bug, email your comments to 61176 AT debbugs.gnu.org.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to bug-gnu-emacs <at> gnu.org:
bug#61176; Package emacs. (Mon, 30 Jan 2023 15:08:02 GMT) Full text and rfc822 format available.

Acknowledgement sent to Jonas Bernoulli <jonas <at> bernoul.li>:
New bug report received and forwarded. Copy sent to bug-gnu-emacs <at> gnu.org. (Mon, 30 Jan 2023 15:08:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Jonas Bernoulli <jonas <at> bernoul.li>
To: bug-gnu-emacs <at> gnu.org
Subject: post-command-hook is not run if minibuffer input is aborted
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)




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#61176; Package emacs. (Wed, 01 Feb 2023 23:07:02 GMT) Full text and rfc822 format available.

Message #8 received at 61176 <at> debbugs.gnu.org (full text, mbox):

From: Stefan Monnier <monnier <at> iro.umontreal.ca>
To: Jonas Bernoulli <jonas <at> bernoul.li>
Cc: 61176 <at> debbugs.gnu.org
Subject: Re: bug#61176: post-command-hook is not run if minibuffer input is
 aborted
Date: Wed, 01 Feb 2023 18:06:24 -0500
> - 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.

Could you clarify what you mean here?
Let's say in the following scenario:

- The user hits a key like `M-x` which causes a minibuffer to be entered.
- the user hits C-g.
- Emacs exits the minibuffer and doesn't even call the command because
  the interactive args could not be gathered.

When do you expect `post-command-hook` to be run?

There's also the case where the command is called and it enters the
minibuffer (rather than doing it within the interactive spec).  Not sure
if it makes a significant difference.

Also it would help to know what you need `post-command-hook` for.

[ There are several "alternatives" to `post-command-hook` plus there
  are cases where code is executed not via a command, yet it can be
  viewed as a command execution as well (e.g. opening a file via
  `emacsclient`), so over the years ad-hoc calls to `post-command-hook`
  have been sprinkled outside of the "command-loop", which makes this
  whole business even more muddy.  ]


        Stefan





Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#61176; Package emacs. (Thu, 02 Feb 2023 14:37:02 GMT) Full text and rfc822 format available.

Message #11 received at 61176 <at> debbugs.gnu.org (full text, mbox):

From: Jonas Bernoulli <jonas <at> bernoul.li>
To: Stefan Monnier <monnier <at> iro.umontreal.ca>
Cc: 61176 <at> debbugs.gnu.org
Subject: Re: bug#61176: post-command-hook is not run if minibuffer input is
 aborted
Date: Thu, 02 Feb 2023 15:36:15 +0100
Stefan Monnier <monnier <at> iro.umontreal.ca> writes:

>> - 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.
>
> Could you clarify what you mean here?
> Let's say in the following scenario:
>
> - The user hits a key like `M-x` which causes a minibuffer to be entered.
> - the user hits C-g.
> - Emacs exits the minibuffer and doesn't even call the command because
>   the interactive args could not be gathered.
>
> When do you expect `post-command-hook` to be run?

Going back to the examples I provided, if the user does NOT abort, then
this happens:

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

And if the user DOES abort, I would like the behavior to be changed like
so:

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

> There's also the case where the command is called and it enters the
> minibuffer (rather than doing it within the interactive spec).  Not sure
> if it makes a significant difference.

The sequence of events is the same as in the above case.  I posted the
wrong log in the examples I provided.  I posted two instances of the user
not aborting instead providing the output for when they abort, which is:

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

So it seems that iff a command uses the minibuffer, then
post-command-hook is ALWAYS run for it right before the minibuffer is
entered, regardless of where the recursive edit is entered; and iff the
user aborts the minibuffer, then the post-command-hook is NEVER run
"after"/"post" the outer command.

> Also it would help to know what you need `post-command-hook` for.

This is relevant for the Transient package.  The following is a
simplified description of relevant parts of what it does.

Calling a transient prefix command installs a transient keymap and adds
functions to `pre-command-hook' and `post-command-hook'.

The pre-command function is responsible for determining whether a
subsequently called (suffix) command should exit the transient state.
If we are about to exit the transient, then this does also set some
global variables to nil, which are only relevant while the transient is
still active.  However, it does not and cannot unset all variables and
most importantly it does not remove the transient keymap and the pre-
and post-command functions.

This function may (or may not) set other global variables, so that the
command that is about to be called has access to the arguments set in
the transient command.  This is similar to how prefix arguments are
implemented.

However, if I remember correctly, in the case of prefix arguments, there
is some C code that takes care of unsetting the prefix argument, if the
next command is aborted.  Transient on the other hand has to do that in
Elisp, and it used the post-command-hook for that (among other things).

Then the command's interactive spec is processed and then the command is
called with the arguments thus determined.

Once that is done, then the post-command function is called.  It is
responsible for redisplaying the transient buffer that displays the
available suffix commands.  Or if the suffix command should exit the
transient, then it has to remove the transient map and the pre- and
post functions, and unset the variables that serve a similar role to
prefix-arg, as well as some internal variables.

The suffix command may use the minibuffer inside interactive and/or in
its body.  If that happens, then transient has to suspend the transient
keymap and pre- and post-command functions, while the minibuffer is in
use.

There are two kinds of suffix commands that may (or may not) use the
minibuffer:

(1) Commands that are specifically designed to be used to set the value
of some argument in the transient command.  These commands are fully
under our control and are designed to handle the suspension and resuming
of the transient map and the pre- and post-command hooks, using
minibuffer-setup-hook, minibuffer-exit-hook, and unwind-protect.

(2) Arbitrary commands, which may have been written with Transient in
mind, but which more likely do nothing to account for the needs of
Transient, i.e., any command that exists in Emacs.

When a (2) command is invoked and that uses the minibuffer, then our
post-command function detects that because it is called with
this-command being the suffix command and this-command-keys-vector being
an empty vector.  The transient map and pre- and post-command hooks are
then suspended like when a (1) command uses the minibuffer, but it is
not possible to use unwind-protect to ensure that these things are
reinstated (or the transient is fully exited), even if the user
aborts while the minibuffer is active.

If post-command-hook were run ("post" command) regardless of whether
the user aborts the minibuffer, then such aborts would not have to be
handled specifically.

Since that is not the case, the pre-mature invocation of the
post-command function, has to delay the resume-or-exit work until some
other event occurs.  Currently that is being done by adding a new
minibuffer-exit function and *another* post-command function.  The first
is designed perform the resume/exit behavior if the minibuffer is
aborted, and the second function is designed to perform the resume/exit
behavior if the first function did not end up doing that, i.e., if the
minibuffer was not aborted.

This is fragile.  Heuristics have to be used to determine whether the
minibuffer is exited normally or if it was aborted.  (There is only
minibuffer-exit-hook, and as far as I can tell, there is no reliable way
to determine whether that was called because the minibuffer was exited
normally or was aborted.)  This approach works more or less, but a few
times I already though I had finally tweaked it enough to handle all
edge-cases, only to later learn that was not so.  Currently there is one
case where it doesn't work as intended.  And if a third-party completion
framework were used that exits the minibuffer in some highly unexpected
way then it would also not work (but currently no such framework does
that, I believe)).

> [ There are several "alternatives" to `post-command-hook` plus there
>   are cases where code is executed not via a command, yet it can be
>   viewed as a command execution as well (e.g. opening a file via
>   `emacsclient`), so over the years ad-hoc calls to `post-command-hook`
>   have been sprinkled outside of the "command-loop", which makes this
>   whole business even more muddy.  ]

What alternatives are you thinking about?

Piuu, that got a bit long.  I left out details, but I hope it became
clear why I need the post-command-hook to always be run *post* command
(and that that is a legitimate need).

> - The user hits a key like `M-x` which causes a minibuffer to be entered.
> - the user hits C-g.
> - Emacs exits the minibuffer and doesn't even call the command because
>   the interactive args could not be gathered.

That could be used as an argument as to why post-command-hook should
not be run when the interactive minibuffer is aborted: if we never go
"inside" the command, there also is no point in going "post" command.

However, it does not appear that this is actually the reason why
post-command-hook is not being run "post" command, not from a design
perspective at least.  If the user aborts the minibuffer usage of the
following command, which uses the minibuffer in its body, then the
post-command-hook also isn't being run "post" command, even though we
clearly made it "into" the command:

>> (defun -command ()
>>   (interactive)
>>   (message ";; -command")
>>   (read-string "-command: "))
>>
>> ;; -command
>> ;; -setup   ([f1]       -command)
>> ;; -post    ([]         -command)
>> ;; -exit    ([7]        abort-minibuffers)
>> ;; Quit
>> ;; -post    ([]         abort-minibuffers)

Thanks for looking into this!
Jonas




Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#61176; Package emacs. (Sun, 05 Feb 2023 14:48:02 GMT) Full text and rfc822 format available.

Message #14 received at 61176 <at> debbugs.gnu.org (full text, mbox):

From: Stefan Monnier <monnier <at> iro.umontreal.ca>
To: Jonas Bernoulli <jonas <at> bernoul.li>
Cc: 61176 <at> debbugs.gnu.org
Subject: Re: bug#61176: post-command-hook is not run if minibuffer input is
 aborted
Date: Sun, 05 Feb 2023 09:47:42 -0500
> And if the user DOES abort, I would like the behavior to be changed like
> so:
>
>>> ;; -setup   ([f1]       -command)
>>> ;; -post    ([]         -command)
>>> ;; -exit    ([7]        abort-minibuffers)
>>> ;; Quit
>>> ;; -post    ([]         abort-minibuffers)
> *NEW* -post    ([f1]       -command)

Hmm... fully agreed and I wonder why it's not run.
[ The rest of the discussion is largely independent from this.  ]

> The suffix command may use the minibuffer inside interactive and/or in
> its body.  If that happens, then transient has to suspend the transient
> keymap and pre- and post-command functions, while the minibuffer is in
> use.

FWIW, I have played with similar issues in the context of prefix
commands and `set-transient-map` (yet, can't find the corresponding
code, sorry) and I remember using `minibuffer-depth` to detect the use
of a minibuffer (i.e. record the original `minibuffer-depth` and/or
`recursion-depth` compare it to the current depth in
`pre-command-hook`).
I see `transient.el` also uses `minibuffer-depth`, so I guess I'm not
telling you anything you didn't already know.

I also remember considering adding hooks to `read-from-minibuffer` and
`recursive-edit` so we can more proactively and reliably let-bind
variables around them (e.g. we should let-bind `this-command` around
them).


        Stefan





This bug report was last modified 1 year and 88 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.