Re: On keybindings and the slow erosion of help's utility
Ihor Radchenko <[email protected]> Sat, 01 Aug 2026 17:18:27 +0000
| Newsgroups | gmane.emacs.devel |
|---|---|
| Message-ID | <87zez592ut.fsf@localhost> |
Stefan Monnier <[email protected]> writes: >> 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. Consider that (run-hooks-with-args-until-success ...) returns non-nil and we thus executed the matching command by side effect. But in the API I described, each condition can only return nil, command, or keymap. If our (org-metaright--try-hook) already executed the command, it cannot return nil - that would make us search further, which we do not want. It cannot return a command or keymap either - we do not want to execute those. Maybe, we can treat non-keymap, non-command return value as "stop here, we are done". That's an option, yes. >> 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"). That's what I meant by (dynamic-bind :filter FILTER-FN ; runs hook: hook =E2=86=92 command/keymap/nil :doc DOC-FN ; (dynamic-binding) =E2=86=92 string for C-h k :enumerate ENUM-FN ; (dynamic-binding) =E2=86=92 list for where-= is :default COMMAND) ; fallback when hook returns nil :doc is a custom function that will take care about rendering documentation. The `keymap-dynamic-bind' will define a particular standard shape of :doc function, but it can be anything. >>> 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-o= p. > > I think we can handle that with the DEPTH arg to `add-hook`. That can work. I just feel that some users will shot themselves by passing 99 depth sooner rather than later. But I do not have hard feeling about this particular design detail. >>>> 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)? What I have in mind is (keymap-dynamic-bind org-metaright-filters (:doc "Overall keymap docstring") (#'org-metaright--at-heading :doc "Condition-specific docstring")) >>>> (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-metarigh= t`) >>> 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). What about (keymap-set org-mode-map "M-<right>" 'org-metaright) ; <- symbol > OTOH, it's fine *if* `lookup-key` doesn't evaluate the conditions and > just returns the whole dynamic keymap. Sure. >> 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 = =E2=97=8F >> 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 =E2=97=8B. > > 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. I think https://elpa.nongnu.org/nongnu/casual.html as a whole is a counterexample to your intuition. It defines whole keymaps depending on cursor location. For example, see https://elpa.nongnu.org/nongnu/doc/casual.html#Dired-Usage Another example is context menus :) >>>> /Example 3: replacing =3Dindent-for-tab-command=3D/ >>> ... >>> 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-availab= le-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: > ... > <indent-for-tab-command source omitted> One important thing in this code that we did not discuss yet is prefix argument. It will need to be passed to the conditions. Otherwise, let me try: (defun indent--should-run-completion () (and (eq tab-always-indent 'complete) (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)))))= )))) (defun indent--for-tab-1 (arg) "Maybe run indent-for-tab-command. If it did not run or did nothing, return nil." (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 (<=3D (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 (eql old-point (point)) (eql old-tick (buffer-chars-modified-tick)) (indent--should-run-completion)) nil) ;; <<------ nil here ;; If a prefix argument was given, rigidly indent the following ;; sexp to match the change in the current line's indentation. (arg (let ((end-marker (save-excursion (forward-line 0) (forward-sexp) (point-marker))) (indentation-change (- (current-indentation) old-indent))) (save-excursion (forward-line 1) (when (and (not (zerop indentation-change)) (< (point) end-marker)) (indent-rigidly (point) end-marker indentation-change) t ;; <--- non-nil ))))))) (keymap-set global-map "TAB" (keymap-dynamic-bind indent-for-tab-filters (:doc "Indent or complete, depending on context.") (:when #'use-region-p #'indent-region) (:when (or ;; indent-to-left-margin is only meant for indenting, ;; so we force it to always insert a tab here. (eq indent-line-function 'indent-to-left-margin) (and (not tab-always-indent) (or (> (current-column) (current-indentation)) (eq this-command last-command)))) #'insert-tab) #'indent--for-tab-1 (:when #'indent--should-run-completion #'completion-at-point)))) --=20 Ihor Radchenko // yantar92, Org mode maintainer, Learn more about Org mode at <https://orgmode.org/>. Support Org development at <https://liberapay.com/org-mode>, or support my work at <https://liberapay.com/yantar92>