Skip to content

rewrite keymap system, introduce transient-like functionality#2100

Merged
vindarel merged 63 commits intolem-project:mainfrom
mahmoodsh36:transient
Apr 24, 2026
Merged

rewrite keymap system, introduce transient-like functionality#2100
vindarel merged 63 commits intolem-project:mainfrom
mahmoodsh36:transient

Conversation

@mahmoodsh36
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor Author

i need to stop the habit of force pushing, lol

@mahmoodsh36
Copy link
Copy Markdown
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
Copy Markdown
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.

Copy link
Copy Markdown
Contributor

@code-contractor-app code-contractor-app Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor validation failed ❌ — see the sticky comment for full results.

@@ -0,0 +1,77 @@
(in-package :lem/transient)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor: defpackage_rule

Contract: contract

AI check failed: "defpackage_rule"

Reason:
New files use IN-PACKAGE or other first forms instead of a top-level DEFPACKAGE or UIOP:DEFINE-PACKAGE as required.

(cond
((keymap-show-p keymap)
(show-transient keymap))
((loop for mode in active-modes
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor: loop_keywords_rule

Contract: contract

AI check failed: "loop_keywords_rule"

Reason:
Added code uses LOOP with unprefixed keywords (e.g. "for", "in", "do") instead of colon-prefixed keywords (e.g. ":for", ":in", ":do").

Comment thread src/keymap.lisp
:initarg :suffix
:documentation "the suffix defined for the prefix, could be another prefix or a keymap or a function that returns one.")
(active-p
:initarg :active-p
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor: docstring_rule

Contract: contract

AI check failed: "docstring_rule"

Reason:
The diff adds exported symbols (classes, generics, and commands) without the required docstrings/documentation. Examples: exported generics lack a :documentation option, exported classes lack class docstrings, and an exported define-command (demo-run) is defined without a docstring. These violate the rule requiring docstrings for exported functions, methods, classes and that generic functions include a :documentation option.

(define-key *copilot-completion-keymap* "Tab" 'copilot-accept-suggestion)
(define-key *copilot-completion-keymap* 'copilot-next-suggestion 'copilot-next-suggestion)
(define-key *copilot-completion-keymap* 'copilot-previous-suggestion 'copilot-previous-suggestion)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor: internal_symbol_rule

Contract: contract

AI check failed: "internal_symbol_rule"

Reason:
Added code contains direct references to internal (non-exported) symbols via package double-colon qualifiers (e.g. lem-core::other-keymaps), which violates the rule to use exported symbols from lem or lem-core and avoid internal symbol access.

@@ -0,0 +1,531 @@
(in-package :lem/transient)

(defvar *transient-popup-window*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Contractor: functional_style_rule

Contract: contract

AI check failed: "functional_style_rule"

Reason:
The diff adds multiple top-level dynamic state variables with defvar that appear to be used as shared state across functions, which violates the rule to avoid using defvar for state passed between functions (prefer explicit function arguments).

handling of undef-hook wasnt right which caused C-n/C-p not to work for
prompt completion.
undef-hook is a leftover from previous keymap code and i should think of
a way to rewrite its functionality that is more ideal.
@mahmoodsh36
Copy link
Copy Markdown
Contributor Author

rebased on main, tho there wasnt really a necessary reason to

@vindarel
Copy link
Copy Markdown
Collaborator

vindarel commented Apr 4, 2026

The PR being ready is great news.

IIRC you said the breaking changes are minimal, mostly name changes.

deprecation warnings instead hard renames/rewrites would be nice too.

that would be nice.

Also we kinda need a document that explains what are these hard renames.

Aaaaand

we kinda need a README for the transient system. I see there's a demo.lisp, but we need some english text with a presentation and simple examples (and screenshots). Can you do this?

I want to try everything,

and we'll ping cxxxr for review.

Comment thread extensions/transient/keymap.lisp Outdated
(when (or (typep suffix 'keymap)
(typep suffix 'prefix))
(f suffix)))))
(f (node)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid single-letters function names.

Comment thread extensions/transient/keymap.lisp Outdated
(when (or (typep suffix 'keymap)
(typep suffix 'prefix))
(f suffix)))))))
(f keymap)))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are no more labels so was it necessary??

Comment thread extensions/transient/keymap.lisp Outdated
(funcall (fdefinition (list 'setf property-method)) props object))))))))

(defun parse-transient (bindings)
"defines a transient menu. args yet to be documented."
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation (at least first parts) welcome.

@mahmoodsh36
Copy link
Copy Markdown
Contributor Author

notable renames

  • *keymaps* is no longer relevant. we use *root-keymap* which maintains the full structure at any point in time.
  • keymap's :parent arg became :base, so make-keymap and make-vi-keymap now take :base instead of :parent.
  • keymap's :name arg became :description, so make-keymap and make-vi-keymap now take :description instead of :name. and keymap-name is now keymap-description.
  • keymap-find-keybind was renamed to keymap-find (and modified).
  • other-keymaps now serves a similar job to what all-keymaps did before the changes (but very different). maybe i should reconsider the rename, but the issue is that it doesnt really collect 'all' keymaps.

defining keys remains the same, but the internal code in keymap.lisp and input.lisp was heavily modified. the diff shows many edited files but most just correspond to renames.

it would be easy to maintain backwards compatibility for things like the :name and :parent renames. the rest, not so much.

@mahmoodsh36 mahmoodsh36 force-pushed the transient branch 2 times, most recently from ee24264 to 6526f3c Compare April 18, 2026 12:55
@vindarel
Copy link
Copy Markdown
Collaborator

Thank you for the readme and the small updates.

This PR looks good to me.

Let's have a couple more testers?!

@vindarel vindarel merged commit 6d9bea0 into lem-project:main Apr 24, 2026
10 checks passed
@vindarel
Copy link
Copy Markdown
Collaborator

Merged 🥳 🚀

Thank you to the testers,

hats off to @mahmoodsh36 for this work.

@vindarel
Copy link
Copy Markdown
Collaborator

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.

2 participants