Re: On keybindings and the slow erosion of help's utility

Stefan Monnier <[email protected]> Tue, 28 Jul 2026 00:42:14 -0400
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
>> Also, what is the use-case for `(quote KEYMAP-VARIABLE)`?
>> Is it supposed to behave differently than returning
>> `(function KEYMAP)` where KEYMAP is the value of KEYMAP-VARIABLE?
>
> My idea was having something like
>
> (keymap-dynamic-bind ...
>   (:when #'org-at-table-p 'org-table-fedit-map))
>
> instead of 
>
> (keymap-dynamic-bind ...
>   (lambda () (when (org-at-table-p) org-table-fedit-map)))

We already allow the use of symbols as keymaps (where the actual keymap
is stored in the `symbol-function` cell of the symbol, rather than the
`symbol-value` cell):

    (keymapp 'ESC-prefix)  ==>  t

So, in short, you just accept either keymaps or commands.  Good.

> Let me explain with the example
>
> 	  (defun org-metaright--try-hook ()
> 	    "Run org-metaright-functions; continue if none handles it."
> 	    (interactive)
> 	    (unless (run-hook-with-args-until-success
> 		     'org-metaright-functions)
> 	      (setq dynamic-bind-status 'continue)))
>
> 	  (keymap-set org-mode-map "M-<right>"
> 	    (keymap-dynamic-bind org-metaright-filters
> 	      (:doc "Demote heading, list item, or move table column right.")
> 	      #'org-metaright--try-hook))
>
> In the above, org-metaright-functions can either do nothing (every
> abnormal hook returns nil), or perform an action and return non-nil.
> There is no separation between checking condition when to run a common
> and running the command itself. (Recall our earlier discussion about
> completion-at-point-functions)
>
> My idea how to handle such scenario is having a global variable where we
> can flag whether we should continue processing the keymap or not.

I thought "returns non-nil" is the way to say whether to continue
processing the keymap or not.

> The advantage of global variable is also that it can be set by
> underlying functions down the call chain.

I don't see when that would be convenient/needed.

> "Impure" here refers to #'fn in keymap-dynamic-bind (see the table) -
> custom function that will combine :when filter and running command. It
> is impure in a sense that it does not separate things.

Yeah, I think I understand this case (this is indeed something we need
for `indent-for-tab-command`, where we try to indent and only run
subsequent alternatives (such as completion) if indentation didn't
modify the buffer).

In theory we could arrange to split the indentation into "calculate
indentation" and "apply new indentation", but for historical
reasons the main API doesn't expose that intermediate step (I tried to
fix it with `prog-indent-calculate(-functions)` but there's a lot of
inertia).

> I proposed a very particular shape for those (to be implemented inside
> user-level `keymap-dynamic-bind'), but we do not have to hard-code them.

I see.  I'm trying to look at it from the `C-h k` side where we want the
API to be minimalist (the hard coding of sub-elements would be fine on
the side of helper macros/functions to define&set such "dynamic keymaps").

>> AFAIK "in the end" what we add to the hook is a function and nothing
>> more, so I think we should focus the discussion on that.
>> `:default` is a trivial function with an `always` condition, so we don't
>> need anything special for it.
>
> No, it is somewhat special.
> Consider the other proposed idea I had:
>
> (keymap-dynamic-bind org-metaright-filters ...)
> (add-hook 'org-metaright-filters #'org-metaright--at-heading)
>
> The add-hook should not add anything *after* :default. That will be no-op.

I think we can handle that with the DEPTH arg to `add-hook`.

>>> the macro generates:
>>>
>>>     (defun org-metaright--at-heading ()
>>>       "At a heading."                  ; first line of org-at-heading-p's docstring
>>>       (declare (side-effect-free t))   ; inherited from org-at-heading-p
>>>       (when (org-at-heading-p) #'org-do-demote))
>>>
>>>     (add-hook 'org-metaright-filters #'org-metaright--at-heading)
>>
>> Ah, so you use the function's docstring as the description of the
>> `:when` condition.  I guess it'd work, but it's a bit of an abuse (that
>> docstring does not describe what the function does).
>
> Yes, unless :doc is explicitly defined.

But `:doc` is in the argument to the macro, so the question is where
does it get stashed in the output of the macro, if not in the function's
docstring (where it's a bit of an abuse)?

>>> (keymap-set org-mode-map "M-<right>"
>>>   (keymap-dynamic-bind org-metaright-filters
>>> [...]
>>
>> I think I'd want to give a name to the metafunction (e.g. `org-metaright`)
>> Otherwise, how will users (and packages like Evil) bind it to some
>> other key (that's one of the problems with `menu-item`: if you do
>> `(keymap-set ... (keymap-lookup ...))` you rebind only the mapping
>> currently returned by the `:filter` rather than the whole `menu-item`).
>
> Why not simply
>
> (defvar org-metaright (keymap-dynamic-bind ...))?

Because, `org-metaright` will then not appear inside the keymap, so
`lookup-key` still won't return what you want.  IOW, the users need to
dig through the source code to see that they can refer to that object
using the variable `org-metaright` (which will signal `void-variable`
if they do so before the package is loaded).

OTOH, it's fine *if* `lookup-key` doesn't evaluate the conditions and
just returns the whole dynamic keymap.

> For the case of dynamic keymap, I think that the most sensible approach
> will be:
> 1. If C-h k M-<right> resolves to a keymap, continue reading FOO and
>    show the final documentation
> 2. If C-h k M-<right> resolves to a command, display the metacommand
>    documentation instead. The command itself will be marked there with ●
>    and will contain a link to command-specific documentation.
> 3. C-h k M-<right> ? will force displaying metacommand documentation,
>    even when it resolves to the keymap
> 4. If none of the conditions in C-h k M-<right> triggers, and no key
>    binding is resolved, display the metacommand documentation, with all
>    the conditions marked ○.

I'm wondering if there's a benefit to allowing dynamic keymaps to contain
a mix of keymaps and commands.  I get the impression that the only
meaningful cases will be either "all commands" or "all keymaps".
And I also get the impression that a lot of machinery you describe would
work well for "all commands" but wouldn't bring much benefit over the
current `menu-item` thingies for "all keymaps".

IOW my intuition tells me we had better treat those two as different
cases with different problems to solve.  E.g. for "all keymaps" we're
kind of forced to test the `:when` conditions during the keymap-lookup
(so they can't have side effects), whereas for the "all commands" we can
delay those tests to the moment we actually try to run the command.

>>> /Example 3: replacing =indent-for-tab-command=/
>>>
>>>     (defun indent-for-tab--try-indentation ()
>>>       "Indent; if nothing changed, continue."
>>>       (interactive)
>>>       (let ((prev (current-column)))
>>>         (indent-according-to-mode)
>>>         (when (eq prev (current-column))
>>>           (setq dynamic-bind-status 'continue))))
>>>
>>>     (keymap-set global-map "TAB"
>>>       (keymap-dynamic-bind indent-for-tab-filters
>>>         (:doc "Indent or complete, depending on context.")
>>>         (:name indent-for-tab--completion
>>>                :when #'completion-at-point-available-p
>>>                #'completion-at-point
>>>                :doc "Completion is available at point.")
>>>         #'indent-for-tab--try-indentation
>>>         (:default #'indent-relative))))
>>
>> Side note: I don't really know how to write a usable
>> `completion-at-point-available-p` here.  Also `indent-for-tab-command`s
>> integration of indentation and completion goes the other way around: we
>> use completion only after trying indentation.
>
> The key is `dynamic-bind-status'. It can be used to affect what should
> count as a command and what should be simply treated as keymap condition check.
> If you tell me more about what is needed (and point to sources), I can
> try to see how we can implement what you have in mind using the proposed API.

Don't know what you mean by "what you have in mind":
- "I don't really know how to write a usable `completion-at-point-available-p`"
  means literally that I don't know how we could write such a function
  such that it provides a good behavior.  I think I'd probably end up
  using something like just checking that `char-before` is not
  whitespace which is a far cry from testing "available-p".
- "integration of indentation and completion goes the other way around"
  just describes what `indent-for-tab-command` does:

    (let ((old-tick (buffer-chars-modified-tick))
          (old-point (point))
	  (old-indent (current-indentation)))

      ;; Indent the line.
      (or (not (eq (indent--funcall-widened indent-line-function) 'noindent))
          (indent--default-inside-comment)
          (when (or (<= (current-column) (current-indentation))
                    (not (eq tab-always-indent 'complete)))
            (indent--funcall-widened (default-value 'indent-line-function))))

      (cond
       ;; If the text was already indented right, try completion.
       ((and (eq tab-always-indent 'complete)
             (eql old-point (point))
             (eql old-tick (buffer-chars-modified-tick))
             (or (eq last-command this-command)
                 (let ((syn (syntax-class (syntax-after (point)))))
                   (pcase tab-first-completion
                     ('nil t)
                     ('eol (eolp))
                     ('word (not (eql 2 syn)))
                     ('word-or-paren (not (memq syn '(2 4 5))))
                     ('word-or-paren-or-punct (not (memq syn '(2 4 5 1))))))))
        (completion-at-point))

>>> The =:doc= function only calls functions whose =side-effect-free=
>>> property is non-nil — the same contract as =menu-item :filter=.
>> Hmm... wasn't aware of this use of `side-effect-free` for
>> menu-item filters.  Could you point me to the corresponding code/commit?
> This is simply a reference to the fact that menu-item :filter items are
> *supposed* to be side-effect-free.

[ Ah, I see, I thought you wrote "side-effect-free" to refer to that
  symbol property rather than to the general concept.  ]

>>> Nested dynamic bindings rebind =dynamic-bind-status= and
>>> =dynamic-bind-active-case= per dispatch.
>> I didn't understand this comment either.
> I hope that the above examples made things more clear.

Not really, but I don't think it's too important for now: I'll get there.


=== Stefan