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

Stefan Monnier <[email protected]>
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Thanks, sorry for the delay, and here are some questions/comments:

> - nil → not applicable; try the next function
> - =#'command= → run this command interactively
> - =#'keymap= → return this keymap (Emacs enters it for further lookup)
> - ='keymap-variable= → a symbol whose value is returned as a keymap

Not completely sure what you mean by the above notation.
Do you mean that the function should return values of the form
`(function CMD)` and `(quote VAR)`?
If so, why use those notations that make it look like an ELisp
expression instead of using something like `(:command CMD)`
and `(:keymap KM)`?

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?

> Two special variables govern impure functions:
>
>     dynamic-bind-status       ;; nil = handled; 'continue = try next case
>     dynamic-bind-active-case   ;; current case's description (read-only)

I don't understand what you mean by that (nor what you mean by "impure
functions").

> /C-level binding head: =dynamic-bind=/
>
> Recognized by =access_keymap= (analogous to =menu-item=):
>
>     (dynamic-bind
>       :filter   FILTER-FN      ; runs hook: hook → command/keymap/nil
>       :doc      DOC-FN         ; (dynamic-binding) → string for C-h k
>       :enumerate ENUM-FN       ; (dynamic-binding) → list for where-is
>       :default  COMMAND)       ; fallback when hook returns nil

- Where is the HOOK?  Is it hidden within FILTER-FN?
- Can `:doc` be replaced by the docstring of FILTER-FN?
- Not sure we need/want enumerate.  Can't we use FILTER-FN for it?

> /Lisp macro: =keymap-dynamic-bind=/
>
>     (keymap-dynamic-bind VARIABLE
>       (:doc STRING)
>       CASE1
>       CASE2
>       ...
>       CASE-N)

We can drop `:doc` and just recognize a string as being the docstring, no?

> Case forms:

> | Macro form                                   | In the hook variable... |
> |----------------------------------------------+-----------------------------------------------------------|
> | =#'fn=                                         | Added as-is |
> | =(:name NAME :when #'condition TARGET)=        | Generates named function NAME; added to hook              |
> | =(:name NAME :when #'condition TARGET :doc X)= | Same, with explicit docstring                             |
> | =(:when #'condition TARGET)=                   | Stores anonymous lambda in hook (always impure for =C-h k=) |
> | =(:when #'condition TARGET :doc X)=            | Anonymous, with explicit docstring                        |
> | =(:default TARGET)=                            | Anonymous default; fires unconditionally at the end       |
> | =(:name NAME :default TARGET)=                 | Named default function |
> | =(:default TARGET :doc X)=                     | Anonymous default with docstring                          |

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.  `:doc` can be the function's docstring,
so again nothing special is needed.
But how is `:when` encoded into the function such that it can be
used for the *Help*?

>     (:name org-metaright--at-heading :when #'org-at-heading-p #'org-do-demote)
>
> 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).

> (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`).

> =C-h k M-<right>= reports:
>
>     M-<right> runs a dynamic binding.
>
>     Demote heading, list item, or move table column right.
>
>     Cases, tried in order:
>       ? Run org-metaright-functions; continue if none handles it.
>       ○ Is the cursor at a table?          → org-table-move-column
>       ○ True if point is at a drawer.       → org-indent-drawer
>       ○ True if point is at a block.        → org-indent-block
>       ● At a heading.                       → org-do-demote
>       ○ Is the cursor at a plain list item? → org-indent-item
>       ○ (default)                           → forward-word
>
>     '?' marks cases never called during documentation lookup
>     (not declared side-effect-free).
>     '●' marks the case that would match at point.

I like this.  So we'd try to dynamically generate a docstring that plays
the role of what we currently write by hand in metafunctions (like
`indent-for-tab-command`), with the added twist that we can try to
pinpoint which alternative is currently "active".

BTW, if the hook returns a keymap rather than a command what do we put
into the docstring?  In that same case, won't the users sometimes want
to do `C-h k M-<right> FOO` so as to see the binding they get for FOO in
that keymap (especially if that keymap pops a menu) instead of (or in
addition to) seeing the docstring of the metafunction?

> Users can override individual named cases:
>
>     (defun my-heading-action ()
>       "My custom heading handler."
>       (declare (side-effect-free t))
>       (when (org-at-heading-p) #'my-custom-command))
>
>     (remove-hook 'org-metaright-filters #'org-metaright--at-heading)
>     (add-hook 'org-metaright-filters #'my-heading-action)

Hopefully the `remove-hook` is not needed (especially since it's an
internal function, as evidenced by the "--" in its name).

> /Example 2: anonymous =:when= (impure)/

I'm not fond of (ab)using `side-effect-free`, to be honest.
There are several different notions of "pure", so I wouldn't be
surprised if such an abuse could bite us in some cases.

> /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.

> In =access_keymap= (src/keymap.c):
>
> 1. Define symbols =Qdynamic_bind=, =QCfilter=, =QCdoc=, =QCenumerate=, =QCdefault=.
>
> 2. After the existing =menu-item= check, add a check for =dynamic-bind=:
>    - Extract =:filter= from the plist.
>    - Call it with the binding and =:default=.
>    - If it returns non-nil, use that as the resolved binding.
>    - Otherwise use =:default= or nil (fallthrough to parent keymaps).

IOW it behaves just like it does for `menu-item` and arguably we could
just use `menu-item` instead here (modulo the limitations of `menu-item`
for the other parts of the functionality we want).

> Integration with =where-is=
> ============================
>
> The =:enumerate= function calls each =side-effect-free= function in the
> hook and collects the returned command symbols.  For keymap targets, the
> keymap is traced recursively via =where-is-internal=.  For impure
> functions, the result is not statically known — a documented limitation.

IIRC currently `where-is-internal` does not run any ELisp code (except
the one used initially to get the `current-active-maps`).  E.g. it does
not run `:filter`s for `menu-item`s.

IIRC this was the case many years ago for technical reasons, but
I can't remember exactly why.  Maybe this is not a problem any more (I
have the vague impression the problem was linked with GCPRO issues and
may have disappeared when we switched to a conservative stack scanning).

It would be nice to get `where-is` to be able to find both the binding of
`org-metaright` *and* the binding of `org-table-move-column` within it.

> 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?

> Nested dynamic bindings rebind =dynamic-bind-status= and
> =dynamic-bind-active-case= per dispatch.

I didn't understand this comment either.


=== Stefan
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.