aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 19:53:28 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 19:53:28 -0500
commit47f222f66d7d481fb0d50661d64f49440455ff6d (patch)
treece208be918001b30ed67cf80292b47e5b22c46f0
parent36a453d2c1237b49f594b23433858a0146dbf31e (diff)
downloaddotemacs-47f222f66d7d481fb0d50661d64f49440455ff6d.tar.gz
dotemacs-47f222f66d7d481fb0d50661d64f49440455ff6d.zip
feat(keybindings): add cj/custom-keymap registration API
Phase 3 of the load-graph project. cj/register-prefix-map and cj/register-command bind a prefix map or command under the C-; prefix and register the which-key label once which-key loads. Feature modules will route their registration through these instead of mutating cj/custom-keymap directly, so keybindings.el stays the sole owner of the prefix and modules stop assuming the keymap already exists at load. Adds test-init-keymap-registration.el covering prefix-map and command resolution, the optional label, and invalid-key rejection. No modules are migrated yet; that follows in batches.
-rw-r--r--modules/keybindings.el26
-rw-r--r--tests/test-init-keymap-registration.el45
-rw-r--r--todo.org2
3 files changed, 72 insertions, 1 deletions
diff --git a/modules/keybindings.el b/modules/keybindings.el
index 6e8adeac..db480087 100644
--- a/modules/keybindings.el
+++ b/modules/keybindings.el
@@ -36,6 +36,32 @@
:doc "User custom prefix keymap base for nested keymaps.")
(keymap-global-set "C-;" cj/custom-keymap)
+;; ------------------------ Custom Keymap Registration -------------------------
+
+;; Feature modules register into the C-; prefix through these helpers rather
+;; than mutating `cj/custom-keymap' directly. This keeps keybindings.el the
+;; sole owner of the prefix and removes each module's hidden assumption that
+;; the keymap already exists. KEY is a `keymap-set'-style key relative to the
+;; C-; prefix (e.g. "c" binds C-; c). Modules must (require 'keybindings).
+
+(defun cj/register-prefix-map (key map &optional label)
+ "Bind prefix keymap MAP under KEY within `cj/custom-keymap'.
+When LABEL is non-nil, register it as the which-key description for the
+\"C-; KEY\" prefix once which-key loads."
+ (keymap-set cj/custom-keymap key map)
+ (when label
+ (with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements (concat "C-; " key) label))))
+
+(defun cj/register-command (key command &optional label)
+ "Bind COMMAND under KEY within `cj/custom-keymap'.
+When LABEL is non-nil, register it as the which-key description for the
+\"C-; KEY\" key once which-key loads."
+ (keymap-set cj/custom-keymap key command)
+ (when label
+ (with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements (concat "C-; " key) label))))
+
;; ------------------------------ Jump To Commands -----------------------------
(defun cj/jump-open-var (var)
diff --git a/tests/test-init-keymap-registration.el b/tests/test-init-keymap-registration.el
new file mode 100644
index 00000000..fcce1edb
--- /dev/null
+++ b/tests/test-init-keymap-registration.el
@@ -0,0 +1,45 @@
+;;; test-init-keymap-registration.el --- Keymap registration API contract -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Phase 3 of the load-graph project introduces a small registration API in
+;; keybindings.el so feature modules stop mutating `cj/custom-keymap' directly.
+;; These tests pin the contract: a registered prefix map or command resolves
+;; from `cj/custom-keymap', the which-key label is optional, and an invalid key
+;; signals. Each test rebinds `cj/custom-keymap' to a fresh keymap so the real
+;; global prefix is never polluted.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'keybindings)
+
+(ert-deftest test-init-keymap-registration-prefix-map-resolves ()
+ "Normal: a prefix map registered under KEY resolves from cj/custom-keymap."
+ (let ((cj/custom-keymap (make-sparse-keymap))
+ (sub (make-sparse-keymap)))
+ (cj/register-prefix-map "a" sub "alpha")
+ (should (eq sub (keymap-lookup cj/custom-keymap "a")))))
+
+(ert-deftest test-init-keymap-registration-command-resolves ()
+ "Normal: a command registered under KEY resolves from cj/custom-keymap."
+ (let ((cj/custom-keymap (make-sparse-keymap)))
+ (cj/register-command "b" #'ignore "beta")
+ (should (eq #'ignore (keymap-lookup cj/custom-keymap "b")))))
+
+(ert-deftest test-init-keymap-registration-label-optional ()
+ "Boundary: registration without a LABEL still binds the key."
+ (let ((cj/custom-keymap (make-sparse-keymap))
+ (sub (make-sparse-keymap)))
+ (cj/register-prefix-map "c" sub)
+ (should (eq sub (keymap-lookup cj/custom-keymap "c")))))
+
+(ert-deftest test-init-keymap-registration-invalid-key-signals ()
+ "Error: a non-string key is rejected by the underlying keymap-set."
+ (let ((cj/custom-keymap (make-sparse-keymap)))
+ (should-error (cj/register-prefix-map 42 (make-sparse-keymap)))))
+
+(provide 'test-init-keymap-registration)
+;;; test-init-keymap-registration.el ends here
diff --git a/todo.org b/todo.org
index a8b0e83c..9b6763aa 100644
--- a/todo.org
+++ b/todo.org
@@ -412,7 +412,7 @@ Do this incrementally. After each batch:
- Run =make test= or at least targeted tests.
- Check that keybindings still resolve and which-key labels still appear.
-**** TODO [#B] Centralize custom keymap registration :refactor:
+**** DOING [#B] Centralize custom keymap registration :refactor:
Many modules mutate =cj/custom-keymap= or global keys at top level. This is a
real architectural boundary because it forces load order and makes standalone