Skip to content

rewrite keymap system, introduce transient-like functionality#2100

Draft
mahmoodsh36 wants to merge 27 commits intolem-project:mainfrom
mahmoodsh36:transient
Draft

rewrite keymap system, introduce transient-like functionality#2100
mahmoodsh36 wants to merge 27 commits intolem-project:mainfrom
mahmoodsh36:transient

Conversation

@mahmoodsh36
Copy link
Contributor

this is very preliminary work. this is an opinionated rewrite of the keymap system that i think makes more sense and is more intuitive.
yet to be handled/written/re-written:

  • undefine-key
  • priorities (some stuff is broken)
  • proper transient-like functionality with proper grid displays and self-documenting keybindings
    the keymap system now allows for "dynamic" (non-static) keys or keymaps to be bound on-demand. unlike the old implementation which had nested pre-defined hashmaps, the new implementation uses a different data structure but tries to maintain backwards compatibility with the old implementation (atleast for now).
    i will be following this PR with more work and documentation/explanations. the goal was to rewrite the keymap system so that things like transient or which-key are a natural consequence of the data structure and are more intertwined with the core (unlike in emacs).

@mahmoodsh36
Copy link
Contributor Author

mahmoodsh36 commented Jan 18, 2026

Screen-2026-01-18_21 23 33 an initial preview of the popup thingy. the double instances of unnamed keymap thing is intentional since its displaying nested keymaps.

@mahmoodsh36
Copy link
Contributor Author

there are still things to be done such as:

  • implement "sticky" keys such as infixes
  • fix keymap priorities/precedence
  • implement undefine-key
  • some key sequences should be considered as a single prefix, such as keys "-g" that resemble commandline arguments and are composed of 2 keys not only one. this is currently not possible because a popup only displays single keys and doesnt recurse to display the longer key sequence which is the intended design, but maybe we should also allow for some sub-keymaps to be recursed into and longer key sequences to be displayed.
  • functionality isnt yet customizable beside the CLOS interface. more customization variables should be introduced that customize behavior.

@mahmoodsh36
Copy link
Contributor Author

i need to stop the habit of force pushing, lol

@mahmoodsh36
Copy link
Contributor Author

i introduced the following "suffix" values for now:

  • :cancel to drop the current key sequence entirely without invoking a command
  • :drop to avoid adding the current key to the key sequence, which makes the prefix act as an "infix" key
  • :back to avoid adding the current key and to pop the last recorded key which has the effect of "going back" to parent menu in the transient popup.

this behavior is customized by setting the value of the suffix to one of those (or to a symbol that is resolved later to a command that is executed). i dont think this belongs in the suffix slot, i think it would be more ideal for a suffix to always resolve to a command, but currently the function 'read-command' which is part of the event/key handler can only resolve a specific key sequence to a single command. currently the behavior of an infix (such as 'choice') is customized by overriding 'prefix-invoke' but this functionality i think belongs in the suffix itself. i have yet to think of a redesign to make this more intuitive.

i think the suffix itself shouldnt necessarily hold the special value that decides the behaviors cancel/drop/back, i think that belongs in a different defmethod (call it prefix-behavior) so that a prefix can both resolve to a command and have custom behavior.

before this, after typing / and trying to search, the following keys
would be interpreted as if they were entered in normal mode instead, and
searching functionality was broken. this is more of a bandaid than a
fix, i need to rewrite the whole 'undef-hook' thing which i think is
annoying and unintuitive
@mahmoodsh36
Copy link
Contributor Author

mahmoodsh36 commented Jan 30, 2026

there are still things to be done such as:

  • implement "sticky" keys such as infixes
  • fix keymap priorities/precedence
  • implement undefine-key
  • some key sequences should be considered as a single prefix, such as keys "-g" that resemble commandline arguments and are composed of 2 keys not only one. this is currently not possible because a popup only displays single keys and doesnt recurse to display the longer key sequence which is the intended design, but maybe we should also allow for some sub-keymaps to be recursed into and longer key sequences to be displayed.
  • functionality isnt yet customizable beside the CLOS interface. more customization variables should be introduced that customize behavior.

first 3 are done.

@mahmoodsh36
Copy link
Contributor Author

mahmoodsh36 commented Feb 4, 2026

things left to do:

  • better handling of keymap priority/precedence. currently it just maintains keymaps so that the more prioritized ones appear earlier during keymap tree traversal, which is also how it was done before this PR.
  • currently the buffer shows as a floating buffer that appears in the bottom right (unless it needs more width in which case it takes up the entire width of the window), we probably want the transient buffer to just show as a "natural" buffer that has some special behavior, like in emacs.
  • remove the keymap* type which is currently maintained for backwards-compatbility. i need to find a better way to handle the functionality that was originally handled through the function-table and undef-hook slots.
  • *special-keymap* is a hack and shouldnt be used. there are better ways to prioritize keymaps.
  • rewriting the command loop will make things make more sense, which i intend on doing in the future. the modifications i made to the event loop arent very smart, it had to be done for things like prefix sequences that have the :back or :drop behavior (the latter is somewhat similar to emacs' repeat-mode which allows for "sticky" keys).

some notes on the rewrite (including possible disadvantages):

  • the previous implementation of keymaps used nested hashmaps that formed a tree-like structure in which each node may have resolved to another keymaps or to a command symbol (in the leaf case). the new implementation introduces a richer structure of two types, prefix and keymap, a keymap can be thought of as a prefix too, but i made some distinction between the two in the code itself. a keymap may contain many prefixes (bindings) each possibly leading to other keymaps. a prefix holds a single key and a suffix, the suffix may be a command, a command symbol, a lambda, a prefix, ..., or a keymap. which is a bit similar to the previous implementation. i considered making it possible for a prefix to hold a sequence of keys instead of just a single key (but i decided not to), which made sense in the context of multi-key sequences like commandline-like args which would be useful to have in transient popups for things like legit (lem's alternative magit). but eventually i introduced the concept of "intermediate" prefixes for this, which are just prefixes that form a longer sequence that is kinda treated as a single prefix, this property is put into use by the transient module.
  • performance may be a concern since the original implementation had hashmaps which made the traversal as fast as O(log n) (well, more like O(1) because the key sequences are always gonna be constant in length) while the new one traverses the entire tree to find a key which is O(n). i dont think its really an issue unless lem is used in a script since the keymap traversal happens as a person types and it would be absurd if you could type keys faster than a computer could traverse a tree. but there is no reason that we couldnt make the new data structure traverse things in O(log n) time anyway.
  • keymaps can form trees in 2 possible ways. by nesting keymaps in the children slot or by assigning keymaps as suffixes of prefixeses. the former is for maintaining a parent-child relationship between keymaps that arent "suffixes" of their parents, while the latter is for "prefixed keymaps", which are keymaps that require a sequence of keys to get to from their ancestors. the root node is pre-defined in the variable *root-keymap* and all keymaps are descendants of this keymap. this keymap doesnt bind any keys itself and only maintains things like *global-keymap* as children.
  • the code in this PR for the transient extension is only vaguely inspired by emacs' transient since i never really used it much and only skimmed briefly through the code of transient.el for initial inspiration. the implementation i ended up with is the one that made the most sense to me at the time.
  • an extensive example of a transient UI is written in demo.lisp and it is bound to C-c t.

@mahmoodsh36
Copy link
Contributor Author

mahmoodsh36 commented Feb 4, 2026

Screen-2026-02-04_19 28 31

this is a screenshot of the popup shown by invoking the demo keymap defined in demo.lisp, it supports things like multi-key prefixes, nested menus, and different types of infixes/prefixes (implemented through CLOS by inheriting from prefix).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant