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