lattice_keymap/registry.rs
1//! `KeymapRegistry` -- the public, layered keymap engine the
2//! input dispatcher consults. Audit slice 8.c of the M3
3//! refactor; see `docs/dev/architecture/keymap-architecture.md` for the design.
4//!
5//! ## Five-layer model (DESIGN.md §5.2.3)
6//!
7//! Bindings live in five layers, in priority order
8//! (`Builtin < MajorMode < MinorMode(_) < User < Buffer`):
9//!
10//! 1. **Builtin** -- the default vim keymap, registered at
11//! startup from the existing `KeymapEntry` catalog.
12//! 2. **MajorMode** -- per-major-mode (rust, markdown, ...)
13//! additions / overrides.
14//! 3. **MinorMode** -- pushed/popped layers
15//! (active-snippet, completion-popup, picker, chord-capture).
16//! One layer per [`ModeId`]: re-pushing the same mode replaces
17//! that layer's bindings rather than stacking a sibling.
18//! 4. **User** -- compiled `init.rs` bindings.
19//! 5. **Buffer** -- per-buffer ad-hoc bindings (`:nmap <buffer>`).
20//!
21//! ## Wait-free reads, mailbox-style writes (in spirit)
22//!
23//! Reads (`lookup`) walk one merged trie per `BindingMode` --
24//! the layers are physically merged into the read structure on
25//! every write. Read cost is one `ArcSwap::load` + the trie
26//! walk (audit slice 8.b: ~17ns single-chord, ~43ns three-chord).
27//!
28//! Writes (`bind` / `unbind` / `push_layer` / `pop_layer`)
29//! take a brief mutex on the layer stack, mutate the affected
30//! per-mode tries, rebuild the merged structure for every mode
31//! that changed, and `ArcSwap::store` it. The mutex covers
32//! pure in-memory work (no I/O); typical write completes in
33//! sub-millisecond per the slice 8.b merge bench (~444 ns per
34//! layer-merge × 6 modes = ~3 µs worst case).
35//!
36//! Writes are infrequent (startup catalog enumeration; minor-
37//! mode push/pop on UI events; user `:bind` / `:unmap`); the
38//! brief lock has no correctness exposure to the keystroke
39//! path because reads never touch it.
40//!
41//! ## Gating: always-on vs. mode layers
42//!
43//! `Builtin`, `User` and `Buffer` are always on. `MajorMode(id)` and
44//! `MinorMode(id)` layers fire only when the caller names `id` in the
45//! `active_modes` slice passed to [`KeymapHandle::lookup_with_context`]
46//! (active major first, then minors in activation order). A gated layer
47//! overlays the always-on merge, so an active mode's chord wins even over
48//! a `User` rebind of the same chord.
49//!
50//! # Examples
51//!
52//! ```
53//! use lattice_grammar::{CommandId, CommandInvocation, SourceLocation};
54//! use lattice_keymap::{BindingMode, KeymapHandle, KeymapLayer, LookupResult, ModeId};
55//! use lattice_keymap::{KeymapCapability, KeymapError};
56//! use lattice_protocol::KeyChord;
57//!
58//! let keymap = KeymapHandle::new();
59//! let cap = KeymapCapability::Full;
60//! let src = || SourceLocation::synthetic("doc");
61//! let (builtin_w, diff_w) = (CommandId::new(1), CommandId::new(2));
62//!
63//! keymap.try_bind_chord_string(cap, KeymapLayer::Builtin, BindingMode::Normal, "w",
64//! CommandInvocation::of(builtin_w), src()).unwrap();
65//! let diff = ModeId::new("diff-mode");
66//! keymap.try_bind_chord_string(cap, KeymapLayer::MinorMode(diff), BindingMode::Normal, "w",
67//! CommandInvocation::of(diff_w), src()).unwrap();
68//!
69//! let fired = |active: &[ModeId]| match keymap.lookup_with_context(
70//! BindingMode::Normal, &[KeyChord::char('w')], active,
71//! ) {
72//! LookupResult::Bound { command, .. } => Some(command.command.command),
73//! _ => None,
74//! };
75//! assert_eq!(fired(&[]), Some(builtin_w)); // diff-mode not active here
76//! assert_eq!(fired(&[diff]), Some(diff_w)); // active mode shadows the builtin
77//!
78//! // Capabilities scope writes: user config cannot touch the builtin layer.
79//! let denied = keymap.try_bind_chord_string(KeymapCapability::User, KeymapLayer::Builtin,
80//! BindingMode::Normal, "x", CommandInvocation::of(builtin_w), src());
81//! assert!(matches!(denied, Err(KeymapError::CapabilityDenied { .. })));
82//! ```
83
84use std::collections::HashMap;
85use std::sync::{Arc, Mutex};
86
87use arc_swap::ArcSwap;
88use lattice_grammar::{CommandId, CommandInvocation, SourceLocation};
89use lattice_protocol::chord::{ChordParseError, KeyChord, parse_chord_sequence};
90
91use crate::resolution::{Continuation, KeymapResolution, LayerHit};
92use crate::{
93 BindingMode, BoundCommand, ChordPattern, KeymapLayer, KeymapTrie, LookupResult, ModeId,
94};
95
96/// Privilege bundle a writer presents when calling
97/// capability-gated bind APIs (slice 8.h). Mirrors the WIT
98/// `keymap-write` capability variants in DESIGN.md §5.5: the
99/// host hands one of these to every caller of the registry --
100/// built-in startup, the user's compiled `init.rs`, each loaded
101/// plugin -- and the registry enforces the layer scope before
102/// committing any write.
103///
104/// Today the enforcement runs purely in-process (no WASM host
105/// has landed yet). When the plugin host is built, the WIT
106/// `bind` / `unbind` / `push-layer` / `pop-layer` host functions
107/// translate the caller's manifest-declared capability into one
108/// of these variants and call through `try_*`.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum KeymapCapability {
111 /// Unrestricted: write to any layer. Reserved for the host's
112 /// startup pass that registers the built-in catalog.
113 Full,
114 /// Write only to [`KeymapLayer::User`]. The compiled
115 /// `init.rs` runs with this capability at boot. Mirror of
116 /// the WIT "user" capability; denies writes to `Builtin`,
117 /// `MajorMode`, `MinorMode(_)`, and `Buffer`.
118 User,
119 /// Write to any [`KeymapLayer::MinorMode`] or
120 /// [`KeymapLayer::Buffer`] layer. Plugins receive this when
121 /// their manifest declares `keymap-write:minor-mode` --
122 /// permits transient overlays (custom modes, popup
123 /// overrides) but denies writes to `Builtin` / `MajorMode` /
124 /// `User`.
125 MinorMode,
126 /// Write only to a single specified [`KeymapLayer::MinorMode`]
127 /// identified by its [`ModeId`]. Mirror of the WIT
128 /// `keymap-write:plugin-layer` variant: each subsystem
129 /// (plugin, user init.rs extending an existing mode) gets
130 /// a capability scoped to one specific mode's keymap layer
131 /// — e.g. `OwnedLayer { mode_id: ModeId::new("diff-mode") }`
132 /// authorises writes only to the `diff-mode` layer.
133 ///
134 /// K.1.b (2026-05-30): re-keyed from opaque `LayerId` to
135 /// `ModeId` so the capability names the mode it targets
136 /// directly. Matches emacs's `(:map foo-mode-map ...)`
137 /// shape: the binding is scoped to the mode, lives + dies
138 /// with the mode's activation lifecycle.
139 OwnedLayer {
140 /// The one mode whose layer (`MinorMode(mode_id)` or
141 /// `MajorMode(mode_id)`) this capability may write.
142 mode_id: ModeId,
143 },
144}
145
146/// Errors returned by the capability-gated bind APIs.
147/// Slice 8.h.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum KeymapError {
150 /// The supplied capability doesn't authorise writes to the
151 /// requested layer. Surfaced to the host so it can echo
152 /// `:bind` / `:unmap` errors and so plugin manifests that
153 /// claim the wrong scope fail loudly at first registration.
154 CapabilityDenied {
155 /// The capability the caller presented.
156 capability: KeymapCapability,
157 /// The layer it tried to write (for `try_push_layer`, the layer
158 /// the push would have created).
159 layer: KeymapLayer,
160 },
161 /// The supplied chord string couldn't be parsed.
162 InvalidChord(ChordParseError),
163}
164
165impl std::fmt::Display for KeymapError {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 KeymapError::CapabilityDenied { capability, layer } => write!(
169 f,
170 "keymap capability {capability:?} cannot write to {layer:?}",
171 ),
172 KeymapError::InvalidChord(e) => write!(f, "invalid chord: {e:?}"),
173 }
174 }
175}
176
177impl std::error::Error for KeymapError {}
178
179/// OM.2b: what `<leader>` expands to unless the host sets otherwise.
180///
181/// `<Space>` rather than vim's historical `\\`. Vim's default is an artifact of
182/// `\\` being one of the few unbound keys in 1991; the modern vim world
183/// overwhelmingly maps leader to space, and nvim-orgmode's documented bindings
184/// assume it. Convention beats historical accuracy on a surface this
185/// muscle-memory-bound (the standing "UX follows convention" rule).
186pub const DEFAULT_LEADER: &str = "<Space>";
187
188/// Expand every `<leader>` / `<Leader>` token in `chord_str` to `leader`.
189///
190/// Textual, before parsing, and everywhere in the sequence rather than only at
191/// the front — vim expands `<Leader>` wherever it appears, and a chord like
192/// `g<leader>x` is legal there.
193///
194/// A `leader` value that does not itself parse is not this function's problem:
195/// the expanded string goes through `parse_chord_sequence` like any other, so a
196/// malformed `keymap.leader` surfaces as an ordinary `InvalidChord` on each
197/// binding that used it — skipped and logged, never a panic.
198///
199/// Only the exact spellings `<leader>` and `<Leader>` are recognised (not
200/// `<LEADER>`). Most callers want [`KeymapHandle::expand_leader`], which
201/// supplies the registry's configured leader.
202///
203/// # Examples
204///
205/// ```
206/// use lattice_keymap::{DEFAULT_LEADER, expand_leader};
207///
208/// assert_eq!(expand_leader("<leader>ff", DEFAULT_LEADER), "<Space>ff");
209/// assert_eq!(expand_leader("g<Leader>x", ","), "g,x");
210/// assert_eq!(expand_leader("gd", ","), "gd");
211/// ```
212pub fn expand_leader(chord_str: &str, leader: &str) -> String {
213 if !chord_str.contains("<leader>") && !chord_str.contains("<Leader>") {
214 // The overwhelmingly common case: no allocation, no scan cost beyond
215 // the two `contains`.
216 return chord_str.to_string();
217 }
218 chord_str
219 .replace("<leader>", leader)
220 .replace("<Leader>", leader)
221}
222
223/// Returns `true` when `capability` authorises writes to
224/// `layer`. The check is the only place layer scope is
225/// enforced -- every capability-gated API funnels through here.
226fn capability_allows(capability: KeymapCapability, layer: KeymapLayer) -> bool {
227 match (capability, layer) {
228 (KeymapCapability::Full, _) => true,
229 (KeymapCapability::User, KeymapLayer::User) => true,
230 (KeymapCapability::MinorMode, KeymapLayer::MinorMode(_) | KeymapLayer::Buffer) => true,
231 // OM.2: `OwnedLayer` authorises the named mode's OWN layer, whichever
232 // kind the mode is. A plugin-declared MAJOR (`org-mode`) writes to
233 // `MajorMode(org-mode)` under exactly the same gate a plugin minor
234 // writes to `MinorMode(...)` — the capability names a mode, and a mode
235 // has one layer. Restricting it to minors would have meant handing a
236 // plugin major a broader capability to do a narrower thing.
237 (
238 KeymapCapability::OwnedLayer { mode_id: cap_mode },
239 KeymapLayer::MinorMode(layer_mode) | KeymapLayer::MajorMode(layer_mode),
240 ) => cap_mode == layer_mode,
241 _ => false,
242 }
243}
244
245/// Stable id for a registered layer. Issued by
246/// [`KeymapHandle::push_layer`] (minor-mode overlays, per-buffer
247/// bindings); the caller passes it to [`KeymapHandle::pop_layer`] to
248/// remove the layer. Layers created implicitly by a `bind` also get one
249/// internally, but it is never handed out — remove those with
250/// [`KeymapHandle::remove_layer`].
251///
252/// Ids are allocated from a per-registry counter starting at 1 and are
253/// never reused within that registry.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
255pub struct LayerId(u32);
256
257impl LayerId {
258 /// The underlying counter value — for logging and diagnostics only; it
259 /// carries no priority or ordering meaning between layers.
260 pub fn raw(self) -> u32 {
261 self.0
262 }
263}
264
265/// One layer in the registry's stack. Per-layer bindings are
266/// keyed by `BindingMode` so the merge can route a `Normal` ->
267/// `Normal` (and not bleed `Visual` bindings into Normal lookup).
268#[derive(Debug, Clone)]
269struct RegistryLayer {
270 /// Where this layer sits in priority order. The vec is
271 /// sorted ascending by this; merges run lowest -> highest.
272 layer: KeymapLayer,
273 /// Stable id (only meaningful for `MinorMode` and `Buffer`
274 /// layers that the caller may want to pop later).
275 id: LayerId,
276 /// Human-readable label for `:describe-key` provenance.
277 /// "builtin", "major:rust", "minor:completion-popup", ...
278 label: String,
279 /// Per-mode tries.
280 modes: HashMap<BindingMode, KeymapTrie>,
281}
282
283/// Wait-free read cell. The current merged-across-layers
284/// per-mode trie set. Rebuilt by writers; read by every
285/// keystroke.
286#[derive(Debug, Default, Clone)]
287struct MergedKeymap {
288 by_mode: HashMap<BindingMode, KeymapTrie>,
289}
290
291/// Internal registry state, behind the registry's mutex.
292/// Holds the per-layer trie tables; the merged cell is
293/// rebuilt from this on every write.
294struct RegistryInner {
295 layers: Vec<RegistryLayer>,
296 next_layer_id: u32,
297}
298
299impl RegistryInner {
300 fn new() -> Self {
301 Self {
302 layers: Vec::new(),
303 next_layer_id: 1,
304 }
305 }
306
307 /// Mutate (insert if absent) the per-layer trie for
308 /// `(layer, mode)`. Returns a mutable reference. Maintains
309 /// the layer-vec sort.
310 fn layer_mut(&mut self, layer: KeymapLayer, label_for_new: &str) -> &mut RegistryLayer {
311 if let Some(pos) = self.layers.iter().position(|l| l.layer == layer) {
312 return &mut self.layers[pos];
313 }
314 let id = LayerId(self.next_layer_id);
315 self.next_layer_id += 1;
316 let new = RegistryLayer {
317 layer,
318 id,
319 label: label_for_new.to_string(),
320 modes: HashMap::new(),
321 };
322 // Insert sorted ascending by KeymapLayer.
323 let pos = self
324 .layers
325 .iter()
326 .position(|l| l.layer > layer)
327 .unwrap_or(self.layers.len());
328 self.layers.insert(pos, new);
329 &mut self.layers[pos]
330 }
331
332 /// Merge **only** the always-on layers (Builtin + User +
333 /// Buffer). Both `MajorMode` and `MinorMode` layers are
334 /// excluded — they're folded in per-keystroke based on the
335 /// active buffer's mode set (see
336 /// [`KeymapHandle::lookup_with_context`]).
337 ///
338 /// Pre-K.1.c the merge included every layer regardless of
339 /// activation. K.1.c excluded `MinorMode`. A major mode was
340 /// still folded in unconditionally, which was harmless only
341 /// while every major returned an empty `keymap()`. The first
342 /// major with real bindings (`ai-conversation`'s `i` →
343 /// focus-prompt) then fired its chords in EVERY buffer —
344 /// pressing `i` on the read-only dashboard jumped the cursor to
345 /// EOF and entered Insert. A major-mode keymap must be gated by
346 /// the active major exactly as a minor-mode keymap is gated by
347 /// active minors.
348 fn build_always_on_merged(&self) -> MergedKeymap {
349 let mut merged = MergedKeymap::default();
350 for layer in &self.layers {
351 if matches!(
352 layer.layer,
353 KeymapLayer::MinorMode(_) | KeymapLayer::MajorMode(_)
354 ) {
355 continue;
356 }
357 for (mode, trie) in &layer.modes {
358 let target = merged.by_mode.entry(*mode).or_default();
359 target.merge_over(trie);
360 }
361 }
362 merged
363 }
364
365 /// Snapshot the per-`ModeId` gated tries for the read-side
366 /// cache — both `MajorMode` and `MinorMode` layers, keyed by
367 /// their `ModeId`. Each value is the full per-`BindingMode`
368 /// trie set for that mode's keymap layer. The keystroke path
369 /// consults this map for each entry of the active-mode slice
370 /// (the active major first, then minors in activation order —
371 /// last-wins) when composing the merged trie. A `ModeId`
372 /// identifies exactly one registered mode, which has exactly
373 /// one kind, so major and minor entries never collide.
374 fn build_gated_mode_tries(&self) -> HashMap<ModeId, Arc<HashMap<BindingMode, KeymapTrie>>> {
375 let mut out = HashMap::new();
376 for layer in &self.layers {
377 let mode_id = match layer.layer {
378 KeymapLayer::MajorMode(id) | KeymapLayer::MinorMode(id) => id,
379 _ => continue,
380 };
381 out.insert(mode_id, Arc::new(layer.modes.clone()));
382 }
383 out
384 }
385}
386
387/// Keymap registry. Cheap to clone (`Arc`-backed); every
388/// caller (App, plugins, the future WIT host) holds a
389/// [`KeymapHandle`] that wraps the same underlying registry.
390///
391/// Construction: [`KeymapRegistry::new`] returns an empty
392/// registry; the App's startup pass enumerates the
393/// `KeymapEntry` catalog and calls `bind` for each entry into
394/// `KeymapLayer::Builtin`.
395pub struct KeymapRegistry {
396 inner: Mutex<RegistryInner>,
397 /// OM.2b: the chord `<leader>` expands to when a binding is
398 /// REGISTERED by string. Default [`DEFAULT_LEADER`]; the host sets
399 /// it from the `keymap.leader` option at boot.
400 ///
401 /// Expansion happens at bind time, never at lookup time — the
402 /// keystroke path must not learn a new concept for this, and a
403 /// binding that has landed is an ordinary chord sequence with no
404 /// memory of how it was spelled. The consequence, stated rather
405 /// than hidden: changing `keymap.leader` after boot does not move
406 /// bindings that already landed (`emacs-keys-prefix` had the same
407 /// shape before it was made live).
408 leader: ArcSwap<String>,
409 /// K.1.c (2026-05-30): cached merge of the **always-on**
410 /// layers only (`Builtin + MajorMode + User + Buffer`).
411 /// Wait-free read; rebuilt by writers. Pre-K.1.c this
412 /// cached every layer; minor-mode layers are now folded in
413 /// per-keystroke (see [`merged_minor_modes`](Self::merged_minor_modes)).
414 merged: Arc<ArcSwap<MergedKeymap>>,
415 /// K.1.c (2026-05-30): per-`ModeId` minor-mode trie cache.
416 /// Wait-free read; consulted per-keystroke for each mode
417 /// in `active_modes[active_buffer]` (reverse activation
418 /// order, last-wins) by
419 /// [`KeymapHandle::lookup_with_context`]. Rebuilt by
420 /// writers alongside `merged`.
421 gated_mode_tries: Arc<ArcSwap<HashMap<ModeId, Arc<HashMap<BindingMode, KeymapTrie>>>>>,
422 /// (C′) Set by a write that only touched its own layer's trie,
423 /// cleared by [`Self::ensure_derived_fresh`] on the next read.
424 /// See that method for the measurement that motivated it.
425 derived_dirty: std::sync::atomic::AtomicBool,
426 /// VM.4: the command registry the motion mirror asks "is this a motion?".
427 ///
428 /// `None` until the host sets it, and harmless while `None`: nothing is
429 /// mirrored, which is what a bare `KeymapHandle::new()` in a unit test
430 /// wants. Setting it RESCANS every existing layer, so it doesn't matter
431 /// when boot sets it relative to the first bind. The guarantee this exists
432 /// for must not depend on one line staying above another.
433 ///
434 /// Held as the live `ArcSwap` handle rather than a snapshot, so a motion a
435 /// plugin registers at runtime is visible to the next bind of its keymap.
436 commands: arc_swap::ArcSwapOption<lattice_grammar::CommandRegistryHandle>,
437 /// MARG.2 (2026-06-03): reverse cache for the keybinding
438 /// annotator surface. Indexes Normal-mode bindings by
439 /// [`CommandId`] so `:` line command completion can show
440 /// `<C-w>v` next to `:split-pane-vertical` (see
441 /// `docs/dev/architecture/marginalia.md` §6). Rebuilt
442 /// alongside `merged` at every bind / unbind / push /
443 /// pop. Wait-free read.
444 ///
445 /// Coverage limits (v1):
446 /// - **Normal mode only.** The `:` line completion picker
447 /// surfaces "what keybinding fires this command" — the
448 /// useful answer is Normal-mode chord (where operators /
449 /// motions / window commands live). Insert-mode
450 /// bindings (`<C-x><C-o>` etc.) get their own annotator
451 /// slice if/when needed.
452 /// - **Literal-only paths.** Bindings whose chord path
453 /// contains `ChordPattern::CharLiteral` (e.g.
454 /// `f<char>`, `m<char>`) are skipped: the marginalia
455 /// column wants a clean chord-only sequence, not a
456 /// placeholder. Such commands still show in
457 /// `:describe-command`; the annotator just doesn't
458 /// prepend the chord.
459 /// - **First-binding-wins.** When multiple chords bind
460 /// the same command, the first one encountered during
461 /// the trie walk is stored. Alternates remain
462 /// reachable via `:describe-command`. MRU-influenced
463 /// "pick the chord the user actually uses" is a
464 /// post-v1 follow-up flagged in marginalia.md §8.
465 /// - **Layer provenance.** Each entry now carries the
466 /// [`KeymapLayer`] alongside the chord so the completion
467 /// margin can show which mode provides the binding and
468 /// filter by active modes (MARG.3).
469 pub(crate) reverse_cache: Arc<ArcSwap<HashMap<CommandId, Vec<(KeyChord, KeymapLayer)>>>>,
470}
471
472impl KeymapRegistry {
473 /// An empty registry (no layers, leader [`DEFAULT_LEADER`], no command
474 /// registry — so no motion mirroring until
475 /// [`KeymapHandle::set_command_registry`]). Callers normally use
476 /// [`KeymapHandle::new`], which wraps exactly this.
477 pub fn new() -> Arc<Self> {
478 Arc::new(Self {
479 inner: Mutex::new(RegistryInner::new()),
480 derived_dirty: std::sync::atomic::AtomicBool::new(false),
481 commands: arc_swap::ArcSwapOption::empty(),
482 leader: ArcSwap::from_pointee(DEFAULT_LEADER.to_string()),
483 merged: Arc::new(ArcSwap::from_pointee(MergedKeymap::default())),
484 gated_mode_tries: Arc::new(ArcSwap::from_pointee(HashMap::new())),
485 reverse_cache: Arc::new(ArcSwap::from_pointee(HashMap::new())),
486 })
487 }
488
489 /// MARG.2 (2026-06-03): rebuild + store the Normal-mode
490 /// reverse cache. Called by every write site after the
491 /// `merged` ArcSwap has been stored. Cheap: walks the
492 /// Normal-mode trie once (O(N) over bound chords).
493 ///
494 /// Also walks the gated mode tries (`MinorMode` / `MajorMode`
495 /// layers) that the always-on merged trie excludes — most
496 /// notably emacs-keys-mode chords, which are registered at
497 /// `KeymapLayer::MinorMode(emacs-keys-mode)` and would
498 /// otherwise be invisible to the completion margin's
499 /// `KeybindingAnnotator`.
500 /// (C′) Rebuild the derived state IF a write marked it stale.
501 ///
502 /// `merged`, `gated_mode_tries` and `reverse_cache` are all pure
503 /// functions of the layer set. Rebuilding them on every `bind` made
504 /// a burst of N bindings O(N²) — three full rebuilds per binding —
505 /// which measured at **734.8 ms for `register_normal_bindings`
506 /// alone** and ~1.1 s of a 1.4 s `Editor::boot`, paid at every real
507 /// editor start, not just in tests.
508 ///
509 /// So writes now only touch their own layer's trie and set the
510 /// flag; the rebuild happens once, here, on the next read. Boot's
511 /// ~1000 bindings become ~1000 cheap inserts and ONE rebuild.
512 ///
513 /// `push_layer` / `pop_layer` still rebuild eagerly, so activating
514 /// a mode never leaves a keystroke to pay for it — the residual
515 /// exposure is a keystroke immediately after a direct `bind()`,
516 /// which is `:map` and plugin binds only.
517 fn ensure_derived_fresh(&self) {
518 // The flag is cleared AFTER the stores, never before.
519 //
520 // Clearing first is a race, and a real one: a second thread
521 // reads `false`, takes the fast path, and loads an `ArcSwap`
522 // the first thread has not published yet. It surfaced as three
523 // `input::tests` chord tests that passed alone and failed in
524 // the full parallel run — exactly the shape of this bug.
525 if !self
526 .derived_dirty
527 .load(std::sync::atomic::Ordering::Acquire)
528 {
529 return;
530 }
531 let inner = self.inner.lock().expect("registry mutex");
532 // Re-check under the lock: another rebuilder may have finished
533 // while we waited, in which case its `Release` store already
534 // published everything we would rebuild.
535 if !self
536 .derived_dirty
537 .load(std::sync::atomic::Ordering::Acquire)
538 {
539 return;
540 }
541 let merged = inner.build_always_on_merged();
542 let minors = inner.build_gated_mode_tries();
543 self.merged.store(Arc::new(merged));
544 self.gated_mode_tries.store(Arc::new(minors));
545 self.rebuild_reverse_cache();
546 // Release: a reader observing `false` through the Acquire load
547 // above is guaranteed to see these stores.
548 self.derived_dirty
549 .store(false, std::sync::atomic::Ordering::Release);
550 drop(inner);
551 }
552
553 fn rebuild_reverse_cache(&self) {
554 let merged = self.merged.load();
555 let mut cache = build_reverse_cache_from_merged(&merged);
556 // K.1.c (2026-05-30): also walk gated mode tries so
557 // MinorMode/MajorMode bindings (e.g. emacs-keys-mode's
558 // <C-x><C-f> → ex:files) appear in the command
559 // completion margin's keybinding annotation column.
560 // MARG.3 (2026-07-15): preserves bound.layer for mode
561 // provenance.
562 let gated = self.gated_mode_tries.load();
563 for per_mode in gated.values() {
564 if let Some(trie) = per_mode.get(&BindingMode::Normal) {
565 trie.walk_bindings(|path, bound| {
566 let mut chords: Vec<KeyChord> = Vec::with_capacity(path.len());
567 for seg in path {
568 match seg {
569 ChordPattern::Literal(c) => chords.push(*c),
570 ChordPattern::CharLiteral => return,
571 }
572 }
573 if chords.is_empty() {
574 return;
575 }
576 let layer = bound.layer;
577 // First-binding-wins: the always-on pass was
578 // processed first, so a command bound at User
579 // or Buffer level retains its higher-priority
580 // chord in the display.
581 cache
582 .entry(bound.command.command)
583 .or_insert_with(|| chords.into_iter().map(|c| (c, layer)).collect());
584 });
585 }
586 }
587 self.reverse_cache.store(Arc::new(cache));
588 }
589}
590
591impl Default for KeymapRegistry {
592 fn default() -> Self {
593 // Allow `KeymapRegistry::default()` for tests without
594 // forcing the Arc wrap. Consumers should still go
595 // through `KeymapHandle`.
596 Self {
597 inner: Mutex::new(RegistryInner::new()),
598 leader: ArcSwap::from_pointee(DEFAULT_LEADER.to_string()),
599 derived_dirty: std::sync::atomic::AtomicBool::new(false),
600 commands: arc_swap::ArcSwapOption::empty(),
601 merged: Arc::new(ArcSwap::from_pointee(MergedKeymap::default())),
602 gated_mode_tries: Arc::new(ArcSwap::from_pointee(HashMap::new())),
603 reverse_cache: Arc::new(ArcSwap::from_pointee(HashMap::new())),
604 }
605 }
606}
607
608/// MARG.2 (2026-06-03): walk the merged Normal-mode trie and
609/// produce a `CommandId → Vec<(KeyChord, KeymapLayer)>` map for
610/// the keybinding annotator. Skips bindings whose chord path
611/// contains `ChordPattern::CharLiteral` (wildcard) since the
612/// marginalia column wants a clean chord-only sequence.
613/// First-binding-wins on collisions. The [`KeymapLayer`] from
614/// each [`BoundCommand`](crate::BoundCommand) is preserved for
615/// MARG.3 mode-aware filtering and provenance display.
616fn build_reverse_cache_from_merged(
617 merged: &MergedKeymap,
618) -> HashMap<CommandId, Vec<(KeyChord, KeymapLayer)>> {
619 let mut out: HashMap<CommandId, Vec<(KeyChord, KeymapLayer)>> = HashMap::new();
620 let Some(trie) = merged.by_mode.get(&BindingMode::Normal) else {
621 return out;
622 };
623 trie.walk_bindings(|path, bound| {
624 let mut chords: Vec<KeyChord> = Vec::with_capacity(path.len());
625 for seg in path {
626 match seg {
627 ChordPattern::Literal(c) => chords.push(*c),
628 // Skip the whole binding when any segment is
629 // a wildcard. Returning from the closure
630 // skips THIS binding; the walker continues
631 // with the next one.
632 ChordPattern::CharLiteral => return,
633 }
634 }
635 if chords.is_empty() {
636 return;
637 }
638 let layer = bound.layer;
639 out.entry(bound.command.command)
640 .or_insert_with(|| chords.into_iter().map(|c| (c, layer)).collect());
641 });
642 out
643}
644
645/// Is a binding in `layer` active on a buffer whose gated modes are
646/// `active_modes`? Shared by [`KeymapHandle::resolve_trace`] and
647/// [`KeymapHandle::continuations`] so the two never drift.
648///
649/// - Builtin / User / Buffer are always-on (they live in the always-on
650/// merged trie `lookup_with_context` starts from).
651/// - K.1.c fix (210da76c): a `MajorMode` layer is NOT always-on —
652/// `build_always_on_merged` excludes it, and `lookup_with_context` folds it
653/// in only when the buffer's active-mode slice names it. So a major-mode
654/// binding is active iff it is THIS buffer's active major. Gate it exactly
655/// like a minor (callers pass `ActiveModes::keymap_gated_ids()` — active
656/// major first, then active minors). Hard-coding `MajorMode → true` made
657/// `:describe-key` report every major's chords as firing in every buffer
658/// (`i` → ai-conv-focus-prompt shown globally) — the introspection half of
659/// the same bug 210da76c fixed on the dispatch side.
660///
661/// **Invariant:** this must agree with `lookup_with_context` for the same
662/// `(layer, active_modes)` pair, or introspection contradicts dispatch.
663fn layer_is_active(layer: KeymapLayer, active_modes: &[ModeId]) -> bool {
664 match layer {
665 KeymapLayer::Builtin | KeymapLayer::User | KeymapLayer::Buffer => true,
666 KeymapLayer::MajorMode(id) | KeymapLayer::MinorMode(id) => active_modes.contains(&id),
667 }
668}
669
670/// Render a trie path for display: literals through `Display for KeyChord`
671/// (which escapes `<` and a bare space), wildcard descents as `{char}` —
672/// the spelling `:describe-key` and `:keymap` already use for `f{char}` /
673/// `'{mark}` style bindings.
674fn render_chord_path(path: &[ChordPattern]) -> String {
675 let mut out = String::new();
676 for seg in path {
677 match seg {
678 ChordPattern::Literal(c) => out.push_str(&c.to_string()),
679 ChordPattern::CharLiteral => out.push_str("{char}"),
680 }
681 }
682 out
683}
684
685/// Position of `mode` in [`BindingMode::all`] — the declaration order the
686/// help output groups by, so continuation listings read Normal-first rather
687/// than in `HashMap` order.
688fn mode_order(mode: BindingMode) -> usize {
689 BindingMode::all()
690 .iter()
691 .position(|&m| m == mode)
692 .unwrap_or(usize::MAX)
693}
694
695/// Does `chord` overtype a Select-mode selection when nothing binds it?
696///
697/// The ONE definition of the rule. Select's overtype fallback calls it, and so
698/// does the motion mirror when deciding what may be bound in Select. Two copies
699/// of "what counts as typing" would drift apart, and a keymap that disagrees
700/// with its dispatcher is exactly the bug VM.4 fixed.
701///
702/// Vim's rule (visual.txt, Select mode): "Printable characters, <NL> and <CR>
703/// cause the selection to be deleted, and Vim enters Insert mode."
704///
705/// - A bare `Char` overtypes: any character, space and non-ASCII included.
706/// - `<CR>` is `Special(Enter)`.
707/// - `<NL>` has no key of its own; a terminal sends it as Ctrl-J.
708///
709/// Ctrl, Alt or Super otherwise make it a chord, not typing. Shift doesn't
710/// count: Select strips it before the lookup, and a shifted letter arrives as
711/// its uppercase `Char` anyway.
712///
713/// # Examples
714///
715/// ```
716/// use lattice_keymap::overtypes_in_select;
717/// use lattice_protocol::KeyChord;
718/// use lattice_protocol::chord::SpecialKey;
719///
720/// assert!(overtypes_in_select(&KeyChord::char('x')));
721/// assert!(overtypes_in_select(&KeyChord::special(SpecialKey::Enter)));
722/// assert!(overtypes_in_select(&KeyChord::ctrl('j'))); // <NL>
723/// assert!(!overtypes_in_select(&KeyChord::ctrl('d')));
724/// assert!(!overtypes_in_select(&KeyChord::special(SpecialKey::Down)));
725/// ```
726pub fn overtypes_in_select(chord: &KeyChord) -> bool {
727 use lattice_protocol::chord::{KeyKind, SpecialKey};
728 let (ctrl, alt, super_) = (chord.mods.ctrl(), chord.mods.alt(), chord.mods.super_());
729 match chord.key {
730 KeyKind::Char(_) | KeyKind::Special(SpecialKey::Enter) if !ctrl && !alt && !super_ => true,
731 KeyKind::Char('j') => ctrl && !alt && !super_,
732 _ => false,
733 }
734}
735
736/// May a binding at `path` live in Select without stealing typed text?
737///
738/// In Select a key that overtypes replaces the selection (select-mode.md §1,
739/// §4), but the dispatcher consults the trie first, and a bound key, or the
740/// prefix of a binding, takes the keystroke instead. Only the FIRST chord
741/// matters, because that's the one looked up on a fresh keystroke. A `{char}`
742/// wildcard in first position matches every printable, so it's never safe.
743fn select_safe(path: &[ChordPattern]) -> bool {
744 match path.first() {
745 None | Some(ChordPattern::CharLiteral) => false,
746 Some(ChordPattern::Literal(chord)) => !overtypes_in_select(chord),
747 }
748}
749
750// ── VM.4: a motion is live in Visual (and, when it can't be typed, Select) ──
751//
752// THE INVARIANT: in a layer's Visual or Select trie, a binding shares the
753// Normal row's `Arc` at the same path IF AND ONLY IF it is the motion mirror.
754//
755// Identity is how the mirror recognises its own rows, so every write that
756// would otherwise share an `Arc` gives the explicit copy its own allocation:
757// an explicit Visual/Select write reusing the Normal row, a Normal write whose
758// `Arc` an explicit Visual/Select row already holds, `bind_modes` naming Normal
759// with a mirror mode, and a caller-built trie handed to `push_layer`.
760
761/// The binding-modes a motion is live in beyond Normal.
762///
763/// Operator-pending is deliberately NOT here. A motion's operator rows are
764/// `<op-prefix><chord>` expansions over the operator vocabulary (`Builtins`),
765/// which lives downstream of this crate, so the host's `expand_grammar_rows`
766/// applies it. Visual and Select need only the command's KIND, which this crate
767/// can ask for, so they are a property of the keymap.
768const MOTION_MIRROR_MODES: [BindingMode; 2] = [BindingMode::Visual, BindingMode::Select];
769
770fn is_motion(commands: &lattice_grammar::CommandRegistry, bound: &BoundCommand) -> bool {
771 commands
772 .lookup(bound.command.command)
773 .is_some_and(|spec| matches!(spec.kind, lattice_grammar::CommandKind::Motion))
774}
775
776/// Reconcile the Visual and Select mirrors of ONE Normal path after its Normal
777/// binding changed from `old` to `new`.
778///
779/// Three rules, and each is something a naive "insert if absent" gets wrong:
780///
781/// 1. **A mirror follows its source.** A slot holding `old` BY IDENTITY is our
782/// mirror, so it's replaced by `new` when `new` is a motion and removed
783/// otherwise. Bind-if-absent alone would see the slot occupied and leave a
784/// stale mirror pointing at a motion the Normal row no longer binds.
785/// 2. **A deliberate binding is never touched.** A slot holding anything else
786/// was written on purpose (Visual's `x` / `s` aliases, a mode's own Visual
787/// override) and survives.
788/// 3. **An empty slot is filled** when `new` is a motion, and, for Select,
789/// when the path's first chord wouldn't overtype.
790fn reconcile_motion_mirrors(
791 layer: &mut RegistryLayer,
792 commands: &lattice_grammar::CommandRegistry,
793 path: &[ChordPattern],
794 old: Option<&Arc<BoundCommand>>,
795 new: Option<&Arc<BoundCommand>>,
796) {
797 let new_motion = new.filter(|b| is_motion(commands, b));
798 for mode in MOTION_MIRROR_MODES {
799 // Select only takes a motion whose first chord wouldn't overtype:
800 // `w`, `f{char}`, `%` and `[[` stay Visual-only, while arrows,
801 // Home/End, PageUp/PageDown and `<C-d>` / `<C-u>` extend in Select too.
802 let mirrored = new_motion.filter(|_| mode != BindingMode::Select || select_safe(path));
803 let (occupied, ours) = match layer.modes.get(&mode).and_then(|t| t.get(path)) {
804 Some(held) => (true, old.is_some_and(|o| Arc::ptr_eq(held, o))),
805 None => (false, false),
806 };
807 match (occupied, ours, mirrored) {
808 (true, true, Some(n)) | (false, _, Some(n)) => {
809 layer
810 .modes
811 .entry(mode)
812 .or_default()
813 .insert(path, Arc::clone(n));
814 }
815 (true, true, None) => {
816 if let Some(trie) = layer.modes.get_mut(&mode) {
817 trie.remove(path);
818 }
819 }
820 (true, false, _) | (false, _, None) => {}
821 }
822 }
823}
824
825/// Mirror every motion in a layer's Normal trie. For `push_layer`, which
826/// installs caller-built tries without passing through `bind`, and for
827/// `set_command_registry`'s rescan.
828fn mirror_layer_motions(layer: &mut RegistryLayer, commands: &lattice_grammar::CommandRegistry) {
829 let Some(normal) = layer.modes.get(&BindingMode::Normal) else {
830 return;
831 };
832 let mut motions: Vec<(Vec<ChordPattern>, Arc<BoundCommand>)> = Vec::new();
833 normal.walk_bindings(|path, bound| {
834 if is_motion(commands, bound) {
835 motions.push((path.to_vec(), Arc::clone(bound)));
836 }
837 });
838 for (path, bound) in &motions {
839 reconcile_motion_mirrors(layer, commands, path, None, Some(bound));
840 }
841}
842
843/// An explicit Visual / Select write gets its own allocation when it would
844/// otherwise share the Normal row's `Arc` (see THE INVARIANT above).
845fn distinct_from_normal(
846 layer: &RegistryLayer,
847 path: &[ChordPattern],
848 bound: &Arc<BoundCommand>,
849) -> Arc<BoundCommand> {
850 let shares = layer
851 .modes
852 .get(&BindingMode::Normal)
853 .and_then(|t| t.get(path))
854 .is_some_and(|normal| Arc::ptr_eq(normal, bound));
855 if shares {
856 Arc::new(BoundCommand::clone(bound))
857 } else {
858 Arc::clone(bound)
859 }
860}
861
862/// A Normal write of `bound` at `path`: any Visual / Select row already holding
863/// that same `Arc` got there by an explicit earlier write, not by the mirror,
864/// so it gets its own allocation before the mirror looks at identity.
865fn unshare_explicit_at(
866 layer: &mut RegistryLayer,
867 path: &[ChordPattern],
868 bound: &Arc<BoundCommand>,
869) {
870 for mode in MOTION_MIRROR_MODES {
871 let Some(trie) = layer.modes.get_mut(&mode) else {
872 continue;
873 };
874 if trie.get(path).is_some_and(|held| Arc::ptr_eq(held, bound)) {
875 trie.insert(path, Arc::new(BoundCommand::clone(bound)));
876 }
877 }
878}
879
880/// `push_layer`: a caller-built trie set contains no registry mirrors, because
881/// the mirror only ever runs inside this registry. So any Visual / Select row
882/// sharing its Normal row's `Arc` is an explicit multi-mode declaration, and is
883/// unshared before the mirror runs.
884fn unshare_explicit_mirror_modes(layer: &mut RegistryLayer) {
885 let Some(normal) = layer.modes.get(&BindingMode::Normal) else {
886 return;
887 };
888 let mut shared: Vec<(BindingMode, Vec<ChordPattern>, Arc<BoundCommand>)> = Vec::new();
889 for mode in MOTION_MIRROR_MODES {
890 let Some(trie) = layer.modes.get(&mode) else {
891 continue;
892 };
893 trie.walk_bindings(|path, bound| {
894 if normal.get(path).is_some_and(|n| Arc::ptr_eq(n, bound)) {
895 shared.push((mode, path.to_vec(), Arc::new(BoundCommand::clone(bound))));
896 }
897 });
898 }
899 for (mode, path, fresh) in shared {
900 layer.modes.entry(mode).or_default().insert(&path, fresh);
901 }
902}
903
904/// Editor-facing handle to the keymap registry.
905///
906/// **Reads are wait-free.** [`Self::lookup`] does one
907/// `ArcSwap::load` + one trie walk. The keystroke path holds
908/// the returned `Arc<MergedKeymap>` only for the duration of
909/// the lookup; concurrent writes (mode push/pop, user `:bind`,
910/// plugin registration) cannot stall it.
911///
912/// **Writes are mutex-routed.** [`Self::bind`] / [`Self::unbind`]
913/// / [`Self::push_layer`] / [`Self::pop_layer`] take a brief
914/// mutex on the layer stack, mutate the affected per-mode
915/// tries, rebuild the merged cell, and `ArcSwap::store` it.
916/// Writes are infrequent (startup; mode transitions; user
917/// commands), so the lock is uncontended in practice.
918#[derive(Clone)]
919pub struct KeymapHandle {
920 pub(crate) registry: Arc<KeymapRegistry>,
921}
922
923impl std::fmt::Debug for KeymapHandle {
924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925 f.debug_struct("KeymapHandle").finish_non_exhaustive()
926 }
927}
928
929impl KeymapHandle {
930 /// A handle on a fresh, empty [`KeymapRegistry`]. Clone the handle to
931 /// share the registry; a second `new()` is an unrelated registry.
932 pub fn new() -> Self {
933 Self {
934 registry: KeymapRegistry::new(),
935 }
936 }
937
938 /// Reverse-lookup entries for `id`: the chord sequence that fires it,
939 /// each chord paired with the layer providing the binding. Empty when
940 /// nothing binds `id`. Freshens the derived state first.
941 ///
942 /// Coverage is deliberately narrow (it feeds the `:` completion
943 /// margin): **Normal mode only**, **literal-only paths** (a binding
944 /// through a `{char}` wildcard is skipped), and **first binding wins**
945 /// when several chords bind the same command. Both the always-on
946 /// layers and every gated mode layer are indexed.
947 ///
948 /// (C′) Prefer this over [`Self::reverse_cache_arc`]: a raw handle
949 /// on the `ArcSwap` bypasses the registry's lazy derived-state
950 /// refresh and can read a cache that a pending `bind` has invalidated.
951 /// Cold path (the command palette and the completion margin's
952 /// keybinding column), so paying a rebuild here is free where paying
953 /// it per write was not.
954 pub fn reverse_entries(&self, id: CommandId) -> Vec<(KeyChord, KeymapLayer)> {
955 self.registry.ensure_derived_fresh();
956 self.registry
957 .reverse_cache
958 .load()
959 .get(&id)
960 .cloned()
961 .unwrap_or_default()
962 }
963
964 /// The raw reverse-cache cell behind [`Self::reverse_entries`].
965 ///
966 /// Exists for `lattice-host`, which builds a `KeymapReverseLookupHandle`
967 /// from it: `lattice-keymap` cannot depend on `lattice-completion`
968 /// (circular dependency), so that type lives downstream and takes the
969 /// cache through this accessor. Reads through it skip the lazy
970 /// derived-state refresh, so they can lag a `bind` until the next
971 /// lookup-style call freshens it.
972 pub fn reverse_cache_arc(
973 &self,
974 ) -> Arc<ArcSwap<HashMap<CommandId, Vec<(KeyChord, KeymapLayer)>>>> {
975 Arc::clone(&self.registry.reverse_cache)
976 }
977
978 /// Look up the typed binding for `chords` in `mode`.
979 /// Wait-free.
980 ///
981 /// K.1.c (2026-05-30): preserves pre-K.1.c semantics by
982 /// treating **every registered mode layer — major and minor — as
983 /// active** (overlaid in `ModeId`-alphabetical order, matching K.1.b's
984 /// sorted-layers-vec merge order). Legacy callers (the
985 /// translate dispatcher's completion-popup / snippet
986 /// keystroke path) continue to work unchanged — their
987 /// mode lifecycle already gates at push/pop, so
988 /// "everything always active" matches the push/pop
989 /// surface.
990 ///
991 /// For per-buffer-gated lookup (the emacs-style
992 /// composability story — `do` in diff-mode only fires
993 /// when the active buffer has diff-mode active) use
994 /// [`Self::lookup_with_context`] with the buffer's
995 /// `active_modes`. D.5 wires diff-mode through that
996 /// path; other modes migrate as their consumers care.
997 pub fn lookup(&self, mode: BindingMode, chords: &[KeyChord]) -> LookupResult {
998 self.registry.ensure_derived_fresh();
999 let minors = self.registry.gated_mode_tries.load();
1000 let mut sorted: Vec<ModeId> = minors.keys().copied().collect();
1001 sorted.sort();
1002 self.lookup_with_context(mode, chords, &sorted)
1003 }
1004
1005 /// K.1.c (2026-05-30): mode-aware lookup.
1006 ///
1007 /// Composes a fresh per-keystroke merged trie:
1008 /// 1. Start with the cached always-on merge
1009 /// (`Builtin + User + Buffer`).
1010 /// 2. For each `mode_id` in `active_modes` (in slice order —
1011 /// callers pass the active major first, then minors in
1012 /// activation order) overlay that mode's `MajorMode` /
1013 /// `MinorMode` layer on top — **later entries win**, so a
1014 /// minor beats the major and the last-activated minor wins.
1015 /// 3. Note: the always-on cache already has `User /
1016 /// Buffer` overlaid above `Builtin`. The
1017 /// minor-mode overlay applied in step 2 therefore
1018 /// sits *above* User/Buffer at lookup time — which
1019 /// differs from the pre-K.1.c "MinorMode < User <
1020 /// Buffer" priority. Rationale: mode-scoped chords
1021 /// (`do` in `diff-mode`, `M-d` in
1022 /// `corfu-popupinfo-map`) intentionally claim their
1023 /// chord while the mode is active, even over the
1024 /// user's global rebinds; users wanting to override
1025 /// a specific mode's binding use the
1026 /// `OwnedLayer { mode_id }` capability to bind
1027 /// inside that mode's layer (where same-layer
1028 /// last-write-wins applies). This matches emacs's
1029 /// minor-mode-precedence-over-global semantics.
1030 ///
1031 /// Wait-free reads: two `ArcSwap::load` calls
1032 /// (`merged`, `gated_mode_tries`) + per-`active_modes`
1033 /// merge work. Typical `active_modes.len()` is 0-3 so
1034 /// the overhead is small; with an empty slice no composite is
1035 /// built at all. A `ModeId` with no registered layer is skipped.
1036 ///
1037 /// # Examples
1038 ///
1039 /// ```
1040 /// use lattice_grammar::{CommandId, CommandInvocation, SourceLocation};
1041 /// use lattice_keymap::{BindingMode, ChordPattern, KeymapHandle, KeymapLayer, LookupResult, ModeId};
1042 /// use lattice_protocol::KeyChord;
1043 ///
1044 /// let keymap = KeymapHandle::new();
1045 /// let path = [ChordPattern::Literal(KeyChord::char('g')), ChordPattern::Literal(KeyChord::char('d'))];
1046 /// keymap.bind(KeymapLayer::MajorMode(ModeId::new("rust-mode")), BindingMode::Normal, &path,
1047 /// CommandInvocation::of(CommandId::new(9)), SourceLocation::synthetic("doc"));
1048 ///
1049 /// let rust = [ModeId::new("rust-mode")];
1050 /// let g = KeyChord::char('g');
1051 /// assert!(matches!(keymap.lookup_with_context(BindingMode::Normal, &[g], &rust), LookupResult::Partial));
1052 /// assert!(matches!(
1053 /// keymap.lookup_with_context(BindingMode::Normal, &[g, KeyChord::char('d')], &rust),
1054 /// LookupResult::Bound { .. },
1055 /// ));
1056 /// // Not this buffer's major: the layer is invisible.
1057 /// assert!(matches!(keymap.lookup_with_context(BindingMode::Normal, &[g], &[]), LookupResult::Unbound));
1058 /// ```
1059 pub fn lookup_with_context(
1060 &self,
1061 mode: BindingMode,
1062 chords: &[KeyChord],
1063 active_modes: &[ModeId],
1064 ) -> LookupResult {
1065 self.registry.ensure_derived_fresh();
1066 let always_on = self.registry.merged.load();
1067 // Fast path: no gated modes active → use the cached
1068 // always-on trie directly, no per-tick allocation.
1069 if active_modes.is_empty() {
1070 return match always_on.by_mode.get(&mode) {
1071 Some(trie) => trie.lookup(chords),
1072 None => LookupResult::Unbound,
1073 };
1074 }
1075 // Per-tick fold: start from always-on (Builtin + User +
1076 // Buffer), then overlay each gated mode in `active_modes`
1077 // order — the caller supplies the active major first, then
1078 // minors in activation order, so a minor overlays (wins
1079 // over) the major and later minors win over earlier ones.
1080 let gated = self.registry.gated_mode_tries.load();
1081 let mut composite = KeymapTrie::new();
1082 if let Some(base) = always_on.by_mode.get(&mode) {
1083 composite.merge_over(base);
1084 }
1085 for mode_id in active_modes {
1086 if let Some(per_mode) = gated.get(mode_id)
1087 && let Some(trie) = per_mode.get(&mode)
1088 {
1089 composite.merge_over(trie);
1090 }
1091 }
1092 composite.lookup(chords)
1093 }
1094
1095 /// WK.1: the immediate continuations of `chords` in the **same
1096 /// composite** [`Self::lookup_with_context`] resolves against.
1097 ///
1098 /// This is which-key's source of truth, and design §2 makes it the
1099 /// feature's one correctness property: the popup is derived from the
1100 /// trie the dispatcher walks, never from the static catalog. Both
1101 /// halves of this function mirror `lookup_with_context` exactly —
1102 /// the same always-on fast path, the same overlay order — and differ
1103 /// only in the terminal step, where that one resolves a binding and
1104 /// this one reports a node's children. Any other construction
1105 /// reintroduces the `:describe-bindings` bug (a mode that shadows a
1106 /// builtin chord not reflected, so the view advertises a binding
1107 /// that will not fire) in a more visible place.
1108 ///
1109 /// **Not** [`Self::continuations`], which is `:describe-key`'s query:
1110 /// activation-agnostic, all-layers, one row per registration. That
1111 /// one answers "does this chord exist anywhere"; this one answers
1112 /// "what can I press next, here". Same trie, opposite contexts.
1113 ///
1114 /// `None` when the prefix leaves the trie. Telemetry path — runs on
1115 /// the actor thread after the idle delay, never per keystroke.
1116 pub fn continuations_with_context(
1117 &self,
1118 mode: BindingMode,
1119 chords: &[KeyChord],
1120 active_modes: &[ModeId],
1121 ) -> Option<crate::trie::NodeView> {
1122 self.registry.ensure_derived_fresh();
1123 let always_on = self.registry.merged.load();
1124 if active_modes.is_empty() {
1125 return always_on
1126 .by_mode
1127 .get(&mode)
1128 .and_then(|trie| trie.node_view(chords));
1129 }
1130 let gated = self.registry.gated_mode_tries.load();
1131 let mut composite = KeymapTrie::new();
1132 if let Some(base) = always_on.by_mode.get(&mode) {
1133 composite.merge_over(base);
1134 }
1135 for mode_id in active_modes {
1136 if let Some(per_mode) = gated.get(mode_id)
1137 && let Some(trie) = per_mode.get(&mode)
1138 {
1139 composite.merge_over(trie);
1140 }
1141 }
1142 composite.node_view(chords)
1143 }
1144
1145 /// Register a binding at `(layer, mode, path)`. Replaces
1146 /// any prior binding at the exact same triple within the
1147 /// same layer (last-bind-wins per layer); higher-priority
1148 /// layers shadow lower-priority ones automatically via the
1149 /// merged-trie rebuild.
1150 pub fn bind(
1151 &self,
1152 layer: KeymapLayer,
1153 mode: BindingMode,
1154 path: &[ChordPattern],
1155 command: CommandInvocation,
1156 source: SourceLocation,
1157 ) {
1158 let bound = Arc::new(BoundCommand::from_invocation(command, source, layer));
1159 self.bind_bound(layer, mode, path, bound);
1160 }
1161
1162 /// Register one binding across SEVERAL modes in a single call.
1163 /// Equivalent to calling [`Self::bind`] once per mode, but inserts
1164 /// into every mode's trie under one lock and rebuilds the merged
1165 /// trie + reverse cache ONCE (not per mode). The same
1166 /// `Arc<BoundCommand>` is shared across the modes' tries, except that a
1167 /// Visual or Select entry named alongside Normal gets its own copy: it's
1168 /// an explicit declaration, and the motion mirror tells its own rows apart
1169 /// by identity (VM.4).
1170 ///
1171 /// This is the imperative multi-mode primitive `init.rs` / plugins /
1172 /// host helpers use directly (the declarative peer is
1173 /// [`crate::Keymap::bind_chord_modes`] and the `keymap_entry!`
1174 /// `mode: [..]` form). `modes` must be non-empty; an empty slice is
1175 /// a no-op.
1176 pub fn bind_modes(
1177 &self,
1178 layer: KeymapLayer,
1179 modes: &[BindingMode],
1180 path: &[ChordPattern],
1181 command: CommandInvocation,
1182 source: SourceLocation,
1183 ) {
1184 if modes.is_empty() {
1185 return;
1186 }
1187 let bound = Arc::new(BoundCommand::from_invocation(command, source, layer));
1188 let label = default_label(layer);
1189 let commands = self.registry.commands.load_full();
1190 let binds_normal = modes.contains(&BindingMode::Normal);
1191 let (merged, minors) = {
1192 let mut inner = self.registry.inner.lock().expect("registry mutex");
1193 let layer_ref = inner.layer_mut(layer, &label);
1194 let old_normal = layer_ref
1195 .modes
1196 .get(&BindingMode::Normal)
1197 .and_then(|t| t.get(path))
1198 .cloned()
1199 .filter(|_| binds_normal);
1200 for &mode in modes {
1201 // VM.4: one `Arc` across the named modes, EXCEPT a mirror mode
1202 // named alongside Normal. That's an explicit declaration, so it
1203 // gets its own allocation and identity keeps meaning "mirror".
1204 let entry = if binds_normal && MOTION_MIRROR_MODES.contains(&mode) {
1205 Arc::new(BoundCommand::clone(&bound))
1206 } else {
1207 Arc::clone(&bound)
1208 };
1209 layer_ref.modes.entry(mode).or_default().insert(path, entry);
1210 }
1211 if binds_normal && let Some(commands) = commands.as_deref() {
1212 reconcile_motion_mirrors(
1213 layer_ref,
1214 &commands.load(),
1215 path,
1216 old_normal.as_ref(),
1217 Some(&bound),
1218 );
1219 }
1220 (
1221 inner.build_always_on_merged(),
1222 inner.build_gated_mode_tries(),
1223 )
1224 };
1225 self.registry.merged.store(Arc::new(merged));
1226 self.registry.gated_mode_tries.store(Arc::new(minors));
1227 self.registry.rebuild_reverse_cache();
1228 }
1229
1230 /// Lower-level binder: register a pre-built
1231 /// `Arc<BoundCommand>` directly, e.g. one built with
1232 /// [`BoundCommand::with_fall_through`] or shared across several
1233 /// paths. Used by the per-mode registration helpers
1234 /// (`lattice_ui_tui::keymap_replace::register_replace_bindings` and
1235 /// siblings). [`Self::bind`] is this plus `BoundCommand::from_invocation`.
1236 ///
1237 /// Same semantics as [`Self::bind`]: last write wins at the exact
1238 /// `(layer, mode, path)`; with a command registry set, a Normal-mode
1239 /// motion is mirrored into Visual (and Select when its first chord
1240 /// cannot be typed). The binding's own `layer` field is not
1241 /// consulted — `layer` decides where it goes.
1242 pub fn bind_bound(
1243 &self,
1244 layer: KeymapLayer,
1245 mode: BindingMode,
1246 path: &[ChordPattern],
1247 bound: Arc<BoundCommand>,
1248 ) {
1249 let label = default_label(layer);
1250 // VM.4: loaded before the lock. `ArcSwap` reads are wait-free, so the
1251 // mutex still covers nothing but trie writes.
1252 let commands = self.registry.commands.load_full();
1253 {
1254 let mut inner = self.registry.inner.lock().expect("registry mutex");
1255 let layer_ref = inner.layer_mut(layer, &label);
1256 if MOTION_MIRROR_MODES.contains(&mode) {
1257 // An explicit Visual / Select write must not pass for the mirror
1258 // by reusing the Normal row's allocation.
1259 let own = distinct_from_normal(layer_ref, path, &bound);
1260 layer_ref.modes.entry(mode).or_default().insert(path, own);
1261 } else if mode == BindingMode::Normal {
1262 let old = layer_ref
1263 .modes
1264 .get(&mode)
1265 .and_then(|t| t.get(path))
1266 .cloned();
1267 layer_ref
1268 .modes
1269 .entry(mode)
1270 .or_default()
1271 .insert(path, Arc::clone(&bound));
1272 // Re-binding the identical `Arc` is the one case where a Visual /
1273 // Select row holding it genuinely IS the mirror.
1274 if !old.as_ref().is_some_and(|o| Arc::ptr_eq(o, &bound)) {
1275 unshare_explicit_at(layer_ref, path, &bound);
1276 }
1277 if let Some(commands) = commands.as_deref() {
1278 reconcile_motion_mirrors(
1279 layer_ref,
1280 &commands.load(),
1281 path,
1282 old.as_ref(),
1283 Some(&bound),
1284 );
1285 }
1286 } else {
1287 layer_ref.modes.entry(mode).or_default().insert(path, bound);
1288 }
1289 }
1290 // (C′) Touch only this layer's trie and mark the derived state
1291 // stale; `ensure_derived_fresh` rebuilds once on the next read.
1292 // Rebuilding all three here made a burst of N bindings O(N²) —
1293 // 734.8 ms in `register_normal_bindings` alone. The reverse
1294 // cache is still rebuilt wholesale (never incrementally), so
1295 // its `or_insert_with` first-in-walk-order semantics are
1296 // bit-identical to before; only WHEN it happens changed.
1297 self.registry
1298 .derived_dirty
1299 .store(true, std::sync::atomic::Ordering::Release);
1300 }
1301
1302 /// Remove the binding at `(layer, mode, path)`. No-op if
1303 /// nothing was registered there. Returns the dropped
1304 /// binding so callers can echo provenance ("unbound `dd`
1305 /// from user, init.rs:42").
1306 pub fn unbind(
1307 &self,
1308 layer: KeymapLayer,
1309 mode: BindingMode,
1310 path: &[ChordPattern],
1311 ) -> Option<Arc<BoundCommand>> {
1312 let commands = self.registry.commands.load_full();
1313 let (dropped, merged, minors) = {
1314 let mut inner = self.registry.inner.lock().expect("registry mutex");
1315 let pos = inner.layers.iter().position(|l| l.layer == layer)?;
1316 let layer_ref = &mut inner.layers[pos];
1317 let dropped = layer_ref.modes.get_mut(&mode)?.remove(path);
1318 // VM.4: the mirror follows its source out. `new = None` removes only
1319 // the Visual / Select rows that are this binding BY IDENTITY; a
1320 // deliberate binding at the same path stays.
1321 if mode == BindingMode::Normal
1322 && let Some(gone) = dropped.as_ref()
1323 && let Some(commands) = commands.as_deref()
1324 {
1325 reconcile_motion_mirrors(layer_ref, &commands.load(), path, Some(gone), None);
1326 }
1327 (
1328 dropped,
1329 inner.build_always_on_merged(),
1330 inner.build_gated_mode_tries(),
1331 )
1332 };
1333 self.registry.merged.store(Arc::new(merged));
1334 self.registry.gated_mode_tries.store(Arc::new(minors));
1335 // MARG.2: keep the reverse-cache in lockstep with the
1336 // merged trie. Every site that stores `merged` /
1337 // `gated_mode_tries` must also rebuild the reverse
1338 // cache or the keybinding annotator will surface
1339 // stale chord text.
1340 self.registry.rebuild_reverse_cache();
1341 dropped
1342 }
1343
1344 /// Install a minor-mode or buffer layer.
1345 ///
1346 /// K.1.b (2026-05-30): for `PushLayerKind::MinorMode(mode_id)`,
1347 /// the layer's identity is the `mode_id` — pushing for the
1348 /// same mode_id is **idempotent on the layer**: the
1349 /// existing layer's bindings are replaced, no sibling layer
1350 /// is minted. The same holds for `MajorMode(mode_id)` and for
1351 /// `Buffer`: the registry finds an existing layer by its
1352 /// [`KeymapLayer`] key, so a second `Buffer` push replaces the
1353 /// first's bindings and returns the same `LayerId`.
1354 ///
1355 /// A replacing push also replaces the layer's label, and discards any
1356 /// bindings previously added to that layer with [`Self::bind`].
1357 ///
1358 /// `bindings` is the layer's full per-mode binding set --
1359 /// computed by the caller (e.g. completion-popup wires its
1360 /// overrides at activation time). The registry copies the
1361 /// tries in; the caller's `KeymapTrie` instances are no
1362 /// longer needed after the call returns.
1363 ///
1364 /// Returns the `LayerId` of the installed layer (whether
1365 /// freshly minted or pre-existing for the same `mode_id`).
1366 /// For `MinorMode`, prefer popping via
1367 /// [`Self::pop_minor_mode_layer`] (by `mode_id`) over
1368 /// [`Self::pop_layer`] (by `LayerId`); both work but the
1369 /// former is what matches the install signature.
1370 pub fn push_layer(
1371 &self,
1372 kind: PushLayerKind,
1373 label: impl Into<String>,
1374 bindings: HashMap<BindingMode, KeymapTrie>,
1375 ) -> LayerId {
1376 let label = label.into();
1377 let commands = self.registry.commands.load_full();
1378 let (id, merged, minors) = {
1379 let mut inner = self.registry.inner.lock().expect("registry mutex");
1380 let layer = match kind {
1381 PushLayerKind::MajorMode(mode_id) => KeymapLayer::MajorMode(mode_id),
1382 PushLayerKind::MinorMode(mode_id) => KeymapLayer::MinorMode(mode_id),
1383 PushLayerKind::Buffer => KeymapLayer::Buffer,
1384 };
1385 // K.1.b: idempotent-on-identity for MinorMode —
1386 // re-pushing the same mode_id replaces bindings on
1387 // the existing layer rather than minting a new one.
1388 // Buffer always mints fresh (Buffer layer is a
1389 // singleton in practice but we don't enforce that
1390 // here).
1391 let (id, pos) = if let Some(pos) = inner.layers.iter().position(|l| l.layer == layer) {
1392 let existing_id = inner.layers[pos].id;
1393 inner.layers[pos].label = label;
1394 inner.layers[pos].modes = bindings;
1395 (existing_id, pos)
1396 } else {
1397 let id = LayerId(inner.next_layer_id);
1398 inner.next_layer_id += 1;
1399 let new = RegistryLayer {
1400 layer,
1401 id,
1402 label,
1403 modes: bindings,
1404 };
1405 let pos = inner
1406 .layers
1407 .iter()
1408 .position(|l| l.layer > layer)
1409 .unwrap_or(inner.layers.len());
1410 inner.layers.insert(pos, new);
1411 (id, pos)
1412 };
1413 // VM.4: `push_layer` installs caller-built tries without passing
1414 // through `bind`. That's why a mirror hung only on `bind` would miss
1415 // every mode layer, and why a RE-push used to drop a plugin's Visual
1416 // motions until something re-ran a host pass.
1417 let layer_ref = &mut inner.layers[pos];
1418 unshare_explicit_mirror_modes(layer_ref);
1419 if let Some(commands) = commands.as_deref() {
1420 mirror_layer_motions(layer_ref, &commands.load());
1421 }
1422 (
1423 id,
1424 inner.build_always_on_merged(),
1425 inner.build_gated_mode_tries(),
1426 )
1427 };
1428 self.registry.merged.store(Arc::new(merged));
1429 self.registry.gated_mode_tries.store(Arc::new(minors));
1430 // MARG.2: keep the reverse-cache in lockstep with the
1431 // merged trie. Every site that stores `merged` /
1432 // `gated_mode_tries` must also rebuild the reverse
1433 // cache or the keybinding annotator will surface
1434 // stale chord text.
1435 self.registry.rebuild_reverse_cache();
1436 id
1437 }
1438
1439 /// Pop the layer issued by an earlier `push_layer`.
1440 /// No-op if the id is unknown (caller may double-pop on
1441 /// the way out of an error path; defensive).
1442 pub fn pop_layer(&self, id: LayerId) {
1443 let (merged, minors) = {
1444 let mut inner = self.registry.inner.lock().expect("registry mutex");
1445 let pos = inner.layers.iter().position(|l| l.id == id);
1446 if let Some(pos) = pos {
1447 inner.layers.remove(pos);
1448 }
1449 (
1450 inner.build_always_on_merged(),
1451 inner.build_gated_mode_tries(),
1452 )
1453 };
1454 self.registry.merged.store(Arc::new(merged));
1455 self.registry.gated_mode_tries.store(Arc::new(minors));
1456 // MARG.2: keep the reverse-cache in lockstep with the
1457 // merged trie. Every site that stores `merged` /
1458 // `gated_mode_tries` must also rebuild the reverse
1459 // cache or the keybinding annotator will surface
1460 // stale chord text.
1461 self.registry.rebuild_reverse_cache();
1462 }
1463
1464 /// Remove an entire layer by its [`KeymapLayer`] identity, dropping every
1465 /// binding it holds across all binding-modes, then rebuild the merged /
1466 /// gated / reverse caches. The teardown seam for a plugin mode's keymap
1467 /// (PH7.12b): `lattice-plugin-host`'s `bind_mode_keymap` binds a plugin mode's chords into
1468 /// `KeymapLayer::MinorMode(mode_id)` via [`Self::try_bind_chord_string`] —
1469 /// an *implicitly-created* layer, so the host never holds a [`LayerId`] to
1470 /// [`pop_layer`](Self::pop_layer) with. This removes it by the layer key
1471 /// the host *does* know (the mode's own `MinorMode(mode_id)`). No-op if no
1472 /// such layer exists (idempotent second unload / a mode that bound nothing).
1473 /// Mirrors `pop_layer`'s rebuild exactly — every site that stores `merged` /
1474 /// `gated_mode_tries` must also rebuild the reverse cache.
1475 pub fn remove_layer(&self, layer: KeymapLayer) {
1476 let (merged, minors) = {
1477 let mut inner = self.registry.inner.lock().expect("registry mutex");
1478 if let Some(pos) = inner.layers.iter().position(|l| l.layer == layer) {
1479 inner.layers.remove(pos);
1480 }
1481 (
1482 inner.build_always_on_merged(),
1483 inner.build_gated_mode_tries(),
1484 )
1485 };
1486 self.registry.merged.store(Arc::new(merged));
1487 self.registry.gated_mode_tries.store(Arc::new(minors));
1488 self.registry.rebuild_reverse_cache();
1489 }
1490
1491 /// VM.4: give the keymap the command registry it asks "is this a motion?".
1492 ///
1493 /// From here on every write mirrors motions into Visual, and into Select
1494 /// when their first chord can't be typed (keymap-architecture.md §15).
1495 /// Rescans every existing layer, so calling this after bindings have landed
1496 /// is exactly as good as calling it first. Boot calls it the moment the
1497 /// handle exists.
1498 pub fn set_command_registry(&self, commands: lattice_grammar::CommandRegistryHandle) {
1499 let snapshot = commands.load_full();
1500 self.registry.commands.store(Some(Arc::new(commands)));
1501 {
1502 let mut inner = self.registry.inner.lock().expect("registry mutex");
1503 for layer in inner.layers.iter_mut() {
1504 mirror_layer_motions(layer, &snapshot);
1505 }
1506 }
1507 self.registry
1508 .derived_dirty
1509 .store(true, std::sync::atomic::Ordering::Release);
1510 }
1511
1512 /// VM.4: every registered layer, lowest priority first.
1513 ///
1514 /// Exists so an invariant can be asserted across ALL layers rather than
1515 /// the one a test happens to name. The "a motion is live in Visual" drift
1516 /// test used to check `Builtin` only, so it could never have noticed a
1517 /// re-pushed mode layer losing its mirror.
1518 pub fn layers(&self) -> Vec<KeymapLayer> {
1519 let inner = self
1520 .registry
1521 .inner
1522 .lock()
1523 .unwrap_or_else(|e| e.into_inner());
1524 inner.layers.iter().map(|l| l.layer).collect()
1525 }
1526
1527 /// Total binding count across all layers. Telemetry +
1528 /// tests; not on the hot path.
1529 pub fn binding_count(&self) -> usize {
1530 let inner = self.registry.inner.lock().expect("registry mutex");
1531 inner
1532 .layers
1533 .iter()
1534 .flat_map(|l| l.modes.values())
1535 .map(|t| t.binding_count())
1536 .sum()
1537 }
1538
1539 /// K.1.d (2026-05-30): enumerate every binding registered
1540 /// for `chords` in `mode` across all layers, returning the
1541 /// layer + binding pair for each. Telemetry path (drives
1542 /// `:describe-key`'s mode-aware section); not on the
1543 /// keystroke hot path. Order: layer-priority ascending
1544 /// (Builtin first, then MajorMode, then MinorMode layers
1545 /// in ModeId-alphabetical order, then User, then Buffer).
1546 /// Callers cross-reference the layers' MinorMode entries
1547 /// against the active buffer's `ActiveModes` to mark
1548 /// which would actually fire right now.
1549 pub fn enumerate_chord_bindings(
1550 &self,
1551 mode: BindingMode,
1552 chords: &[KeyChord],
1553 ) -> Vec<(KeymapLayer, Arc<BoundCommand>)> {
1554 let inner = self.registry.inner.lock().expect("registry mutex");
1555 let mut hits = Vec::new();
1556 for layer in &inner.layers {
1557 if let Some(trie) = layer.modes.get(&mode)
1558 && let LookupResult::Bound { command, .. } = trie.lookup(chords)
1559 {
1560 hits.push((layer.layer, command));
1561 }
1562 }
1563 hits
1564 }
1565
1566 /// Full per-layer trace for `chords` in `mode`.
1567 ///
1568 /// Returns a [`KeymapResolution`] whose `hits` list every registered
1569 /// layer that has a terminal binding at the given chord path, in
1570 /// priority order ascending (Builtin first, Buffer last). The `active`
1571 /// flag on each hit is set by crossing the layer against `active_modes`:
1572 /// - `Builtin`, `User`, `Buffer` are always active.
1573 /// - `MajorMode(id)` and `MinorMode(id)` are active iff `id` is
1574 /// contained in `active_modes`.
1575 ///
1576 /// Telemetry path; not on the keystroke hot path.
1577 pub fn resolve_trace(
1578 &self,
1579 mode: BindingMode,
1580 chords: &[KeyChord],
1581 active_modes: &[ModeId],
1582 ) -> KeymapResolution {
1583 let pairs = self.enumerate_chord_bindings(mode, chords);
1584 let hits = pairs
1585 .into_iter()
1586 .map(|(layer, command)| LayerHit {
1587 layer,
1588 command,
1589 active: layer_is_active(layer, active_modes),
1590 })
1591 .collect();
1592 KeymapResolution { mode, hits }
1593 }
1594
1595 /// Run [`Self::resolve_trace`] for every `BindingMode` variant.
1596 ///
1597 /// Returns only the modes that have at least one registered binding
1598 /// for `chords` (i.e. non-empty `hits`). Callers that want to display
1599 /// `:describe-key` with all modes iterate the returned vec; modes with
1600 /// no bindings are omitted to keep output compact.
1601 ///
1602 /// Telemetry path; not on the keystroke hot path.
1603 pub fn resolve_trace_all_modes(
1604 &self,
1605 chords: &[KeyChord],
1606 active_modes: &[ModeId],
1607 ) -> Vec<KeymapResolution> {
1608 BindingMode::all()
1609 .iter()
1610 .map(|&mode| self.resolve_trace(mode, chords, active_modes))
1611 .filter(|r| !r.hits.is_empty())
1612 .collect()
1613 }
1614
1615 /// Is `chords` still an INCOMPLETE prefix of some REGISTERED binding —
1616 /// in any binding mode, in any layer, active here or not?
1617 ///
1618 /// This is the question that lets `:describe-key`'s chord capture end a
1619 /// sequence without reserving a terminator key. A chord argument is a
1620 /// sequence (`gg`, `<C-w>v`, `<leader>fz`), so capture cannot submit on the
1621 /// first keystroke; but the trie already distinguishes "waiting for more"
1622 /// from "this is the answer" on every keystroke of ordinary dispatch, and
1623 /// that is exactly the distinction capture needs. Asking here is what frees
1624 /// `<CR>` / `<Esc>` / `<BS>` to be describable keys rather than controls.
1625 ///
1626 /// **Any LAYER, not the active ones (DK.4).** This deliberately does not
1627 /// take an `active_modes` slice, and the omission is the fix for a
1628 /// user-reported truncation: capture runs while the `*command-line*`
1629 /// buffer is focused, so an activation-gated query resolves against the
1630 /// MINIBUFFER's modes — which are never the org / magit / plugin modes
1631 /// whose chords the user is asking about. `<C-c><C-x><C-b>` submitted
1632 /// after two chords and described `<C-c><C-x>`, because org-mode's
1633 /// `MajorMode` layer was invisible to the question. No multi-chord
1634 /// binding owned by a major mode or a plugin could be captured at all.
1635 ///
1636 /// The rule that replaces it: `:describe-key` answers for EVERY key, not
1637 /// only the keys active where you stand — so capture keeps reading while
1638 /// any registered binding anywhere could extend the sequence, and the
1639 /// rendered answer marks each layer `[active]` / `[inactive]` for the
1640 /// buffer the prompt was opened from. Sequence SHAPE is a property of the
1641 /// keymap; what FIRES is a property of the buffer. Only the second one is
1642 /// contextual.
1643 ///
1644 /// **Any mode, not the current one**, for the same reason: an
1645 /// Insert-mode-only prefix must not submit early mid-sequence.
1646 ///
1647 /// `Unbound` deliberately terminates. "This key does nothing" is a first-
1648 /// class answer — it is the one a user asking why `<M-k>` did nothing
1649 /// needs — and treating it as "keep waiting" would hang capture on exactly
1650 /// the query that motivated it. A chord that is BOUND at this depth also
1651 /// terminates, matching dispatch: the trie stops at the first binding, so
1652 /// anything grown beneath it can never fire (the continuations are still
1653 /// listed in the answer, flagged as unreachable).
1654 ///
1655 /// Telemetry path; not on the keystroke hot path.
1656 pub fn any_layer_expects_more(&self, chords: &[KeyChord]) -> bool {
1657 let inner = self.registry.inner.lock().expect("registry mutex");
1658 inner.layers.iter().any(|layer| {
1659 layer
1660 .modes
1661 .values()
1662 .any(|trie| matches!(trie.lookup(chords), LookupResult::Partial))
1663 })
1664 }
1665
1666 /// DK.4: every binding registered strictly BELOW `chords`, across all
1667 /// layers and all binding modes, annotated with whether its layer is
1668 /// active on the buffer described by `active_modes`.
1669 ///
1670 /// The prefix half of "`:describe-key` answers for every key". A prefix
1671 /// has no binding of its own, so the layer trace is empty and the honest
1672 /// answer is its subtree: what can follow, what each continuation runs,
1673 /// and which of them can fire here.
1674 ///
1675 /// Results are sorted by binding mode (declaration order), then by
1676 /// rendered chord suffix, so the output is stable across runs.
1677 /// Continuations whose path crosses a `CharLiteral` wildcard (`f{char}`,
1678 /// `'{mark}`) are included with the wildcard rendered as `{char}`.
1679 ///
1680 /// Telemetry path; not on the keystroke hot path.
1681 pub fn continuations(&self, chords: &[KeyChord], active_modes: &[ModeId]) -> Vec<Continuation> {
1682 let inner = self.registry.inner.lock().expect("registry mutex");
1683 let mut out: Vec<Continuation> = Vec::new();
1684 for layer in &inner.layers {
1685 for (&mode, trie) in &layer.modes {
1686 trie.walk_continuations(chords, |suffix, bound| {
1687 out.push(Continuation {
1688 mode,
1689 suffix: render_chord_path(suffix),
1690 layer: layer.layer,
1691 command: Arc::clone(bound),
1692 active: layer_is_active(layer.layer, active_modes),
1693 });
1694 });
1695 }
1696 }
1697 drop(inner);
1698 out.sort_by(|a, b| {
1699 mode_order(a.mode)
1700 .cmp(&mode_order(b.mode))
1701 .then_with(|| a.suffix.cmp(&b.suffix))
1702 .then_with(|| a.layer.cmp(&b.layer))
1703 });
1704 out
1705 }
1706
1707 /// Human-readable label for a `KeymapLayer`, derived from the layer's
1708 /// registered label string (set at `push_layer` / `bind` time). Falls
1709 /// back to `default_label` when the layer hasn't been explicitly named.
1710 /// Used by `:describe-key` output.
1711 pub fn layer_label_string(&self, layer: KeymapLayer) -> String {
1712 let inner = self.registry.inner.lock().expect("registry mutex");
1713 inner
1714 .layers
1715 .iter()
1716 .find(|l| l.layer == layer)
1717 .map(|l| l.label.clone())
1718 .unwrap_or_else(|| default_label(layer))
1719 }
1720
1721 /// Human-readable label for the layer carrying `id`, if any.
1722 /// Drives `:describe-key`'s provenance row ("user, init.rs:42";
1723 /// "minor-mode:completion-popup"). Telemetry path; not on the
1724 /// hot path.
1725 pub fn layer_label(&self, id: LayerId) -> Option<String> {
1726 let inner = self.registry.inner.lock().expect("registry mutex");
1727 inner
1728 .layers
1729 .iter()
1730 .find(|l| l.id == id)
1731 .map(|l| l.label.clone())
1732 }
1733
1734 // ---- Slice 8.h: capability-gated WIT-shaped API.
1735
1736 /// Capability-gated [`Self::bind`]. The host hands every
1737 /// caller a [`KeymapCapability`] derived from its manifest;
1738 /// this entry point checks the capability before committing
1739 /// the write so plugins / `init.rs` can't escape their
1740 /// declared scope.
1741 ///
1742 /// Scope: `Full` → any layer; `User` → `User` only; `MinorMode` →
1743 /// any `MinorMode(_)` or `Buffer`; `OwnedLayer { mode_id }` → that
1744 /// mode's own `MinorMode(mode_id)` or `MajorMode(mode_id)`.
1745 ///
1746 /// # Errors
1747 ///
1748 /// [`KeymapError::CapabilityDenied`] when the capability does not
1749 /// cover `layer`; nothing is written.
1750 pub fn try_bind(
1751 &self,
1752 capability: KeymapCapability,
1753 layer: KeymapLayer,
1754 mode: BindingMode,
1755 path: &[ChordPattern],
1756 command: CommandInvocation,
1757 source: SourceLocation,
1758 ) -> Result<(), KeymapError> {
1759 if !capability_allows(capability, layer) {
1760 return Err(KeymapError::CapabilityDenied { capability, layer });
1761 }
1762 self.bind(layer, mode, path, command, source);
1763 Ok(())
1764 }
1765
1766 /// Capability-gated convenience that parses `chord_str`
1767 /// (`"<leader>w"`, `"gd"`, `"<C-w>j"`) into a
1768 /// `Vec<ChordPattern::Literal>` before delegating to
1769 /// [`Self::try_bind`]. The host's WIT `bind` host-fn calls
1770 /// this; user `init.rs` calls a thin wrapper around it.
1771 ///
1772 /// `chord_str` must round-trip through
1773 /// [`lattice_protocol::chord::parse_chord_sequence`]; wildcards
1774 /// (`<CharLiteral>`) aren't expressible from chord strings
1775 /// today and require [`Self::try_bind`] with a hand-built
1776 /// `&[ChordPattern]`. `<leader>` / `<Leader>` is expanded first
1777 /// against [`Self::leader`].
1778 ///
1779 /// # Errors
1780 ///
1781 /// [`KeymapError::InvalidChord`] when the (leader-expanded) string does
1782 /// not parse; otherwise as [`Self::try_bind`]. The chord is parsed
1783 /// before the capability is checked.
1784 ///
1785 /// # Examples
1786 ///
1787 /// ```
1788 /// use lattice_grammar::{CommandId, CommandInvocation, SourceLocation};
1789 /// use lattice_keymap::{BindingMode, KeymapCapability, KeymapError, KeymapHandle, KeymapLayer, LookupResult};
1790 /// use lattice_protocol::KeyChord;
1791 ///
1792 /// let keymap = KeymapHandle::new();
1793 /// keymap.set_leader(",");
1794 /// let cmd = CommandInvocation::of(CommandId::new(3));
1795 /// keymap.try_bind_chord_string(KeymapCapability::User, KeymapLayer::User, BindingMode::Normal,
1796 /// "<leader>w", cmd.clone(), SourceLocation::synthetic("init.rs")).unwrap();
1797 ///
1798 /// let hit = keymap.lookup(BindingMode::Normal, &[KeyChord::char(','), KeyChord::char('w')]);
1799 /// assert!(matches!(hit, LookupResult::Bound { command, .. } if command.command == cmd));
1800 ///
1801 /// let bad = keymap.try_bind_chord_string(KeymapCapability::User, KeymapLayer::User,
1802 /// BindingMode::Normal, "<Nope>", cmd, SourceLocation::synthetic("init.rs"));
1803 /// assert!(matches!(bad, Err(KeymapError::InvalidChord(_))));
1804 /// ```
1805 pub fn try_bind_chord_string(
1806 &self,
1807 capability: KeymapCapability,
1808 layer: KeymapLayer,
1809 mode: BindingMode,
1810 chord_str: &str,
1811 command: CommandInvocation,
1812 source: SourceLocation,
1813 ) -> Result<(), KeymapError> {
1814 // OM.2b: `<leader>` is expanded here, at the single choke point every
1815 // string-bound binding funnels through — plugin modes, plugin
1816 // `register-binding`, and the user's init.rs all arrive by this route.
1817 let chord_str = &self.expand_leader(chord_str);
1818 let chords = parse_chord_sequence(chord_str).map_err(KeymapError::InvalidChord)?;
1819 let path: Vec<ChordPattern> = chords.into_iter().map(ChordPattern::Literal).collect();
1820 self.try_bind(capability, layer, mode, &path, command, source)
1821 }
1822
1823 /// OM.4b: every terminal binding in ONE layer's `mode` trie, as
1824 /// `(chord path, bound command)` pairs.
1825 ///
1826 /// The existing walks are either global (`merged`, every layer folded
1827 /// together) or per-`CommandId` (the reverse cache). Neither answers "what
1828 /// did *this* layer bind", which is what a caller expanding a mode's
1829 /// contributions needs: the chord comes from the layer, the command's kind
1830 /// comes from the `CommandRegistry`, and only the pairing identifies e.g. a
1831 /// text object that needs operator-pending rows generating.
1832 ///
1833 /// Snapshots into a `Vec` rather than lending an iterator, because the
1834 /// caller's next move is to *write* bindings into the same registry and it
1835 /// must not be holding the lock while doing so. `O(bindings in one layer)`,
1836 /// off any hot path — this runs at plugin load, not per keystroke.
1837 pub fn layer_bindings(
1838 &self,
1839 layer: KeymapLayer,
1840 mode: BindingMode,
1841 ) -> Vec<(Vec<ChordPattern>, Arc<BoundCommand>)> {
1842 let inner = self
1843 .registry
1844 .inner
1845 .lock()
1846 .unwrap_or_else(|e| e.into_inner());
1847 let Some(entry) = inner.layers.iter().find(|l| l.layer == layer) else {
1848 return Vec::new();
1849 };
1850 let Some(trie) = entry.modes.get(&mode) else {
1851 return Vec::new();
1852 };
1853 let mut out = Vec::new();
1854 trie.walk_bindings(|path, bound| out.push((path.to_vec(), Arc::clone(bound))));
1855 out
1856 }
1857
1858 /// OM.2b: the chord `<leader>` expands to at bind time.
1859 pub fn leader(&self) -> Arc<String> {
1860 self.registry.leader.load_full()
1861 }
1862
1863 /// Set what `<leader>` expands to. The host calls this at boot from the
1864 /// `keymap.leader` option, BEFORE any subsystem or plugin registers its
1865 /// bindings — expansion is bind-time, so a leader set afterwards does not
1866 /// move bindings that already landed.
1867 pub fn set_leader(&self, leader: &str) {
1868 self.registry.leader.store(Arc::new(leader.to_string()));
1869 }
1870
1871 /// Expand `<leader>` in `chord_str` against the current leader.
1872 /// Exposed so a caller that must parse a binding string itself
1873 /// (`:describe-key`) resolves it the same way binding did.
1874 pub fn expand_leader(&self, chord_str: &str) -> String {
1875 expand_leader(chord_str, &self.registry.leader.load())
1876 }
1877
1878 /// Capability-gated [`Self::unbind`] from a vim-notation chord
1879 /// **string** — the symmetric counterpart to
1880 /// [`try_bind_chord_string`](Self::try_bind_chord_string), so a
1881 /// caller that bound by string (a plugin's `register-binding`,
1882 /// PL8.D) can reverse it by the same string on unload without
1883 /// re-parsing to `ChordPattern`s itself. An unparseable chord is
1884 /// [`KeymapError::InvalidChord`]; a capability denial is
1885 /// [`KeymapError::CapabilityDenied`]; `Ok(None)` means the path
1886 /// wasn't bound (idempotent re-unbind).
1887 pub fn try_unbind_chord_string(
1888 &self,
1889 capability: KeymapCapability,
1890 layer: KeymapLayer,
1891 mode: BindingMode,
1892 chord_str: &str,
1893 ) -> Result<Option<Arc<BoundCommand>>, KeymapError> {
1894 // Expand on the way out too, or a binding registered as `<leader>x`
1895 // could never be reversed by the string that created it — the
1896 // symmetry this method exists for.
1897 let chord_str = &self.expand_leader(chord_str);
1898 let chords = parse_chord_sequence(chord_str).map_err(KeymapError::InvalidChord)?;
1899 let path: Vec<ChordPattern> = chords.into_iter().map(ChordPattern::Literal).collect();
1900 self.try_unbind(capability, layer, mode, &path)
1901 }
1902
1903 /// Capability-gated [`Self::unbind`]. Returns the dropped
1904 /// binding (or `None` when the path wasn't bound) so the
1905 /// host can echo "unbound `dd` (was: delete-line)".
1906 pub fn try_unbind(
1907 &self,
1908 capability: KeymapCapability,
1909 layer: KeymapLayer,
1910 mode: BindingMode,
1911 path: &[ChordPattern],
1912 ) -> Result<Option<Arc<BoundCommand>>, KeymapError> {
1913 if !capability_allows(capability, layer) {
1914 return Err(KeymapError::CapabilityDenied { capability, layer });
1915 }
1916 Ok(self.unbind(layer, mode, path))
1917 }
1918
1919 /// Capability-gated [`Self::push_layer`]. Permitted for every
1920 /// capability except `User`, whatever `kind` names: `MinorMode` and
1921 /// `OwnedLayer { mode_id }` are NOT checked against the pushed
1922 /// layer's kind or mode id here, unlike [`Self::try_bind`]. The `User`
1923 /// capability can't push runtime layers -- user config writes live
1924 /// in the static `User` layer registered at boot.
1925 ///
1926 /// # Errors
1927 ///
1928 /// [`KeymapError::CapabilityDenied`] for `User`, naming the layer the
1929 /// push would have created.
1930 pub fn try_push_layer(
1931 &self,
1932 capability: KeymapCapability,
1933 kind: PushLayerKind,
1934 label: impl Into<String>,
1935 bindings: HashMap<BindingMode, KeymapTrie>,
1936 ) -> Result<LayerId, KeymapError> {
1937 // `User` capability cannot push minor-mode layers --
1938 // it's the only one denied here. Every other capability
1939 // either has full reach (`Full`) or is scoped to
1940 // minor-mode-style layers anyway.
1941 if matches!(capability, KeymapCapability::User) {
1942 // Synthesise a `KeymapLayer` tag for the error so
1943 // the message is consistent with `try_bind`'s
1944 // denials. The actual layer hasn't been installed;
1945 // the synthesised tag reflects the layer *kind* the
1946 // caller tried to write to.
1947 let placeholder = match kind {
1948 PushLayerKind::MajorMode(mode_id) => KeymapLayer::MajorMode(mode_id),
1949 PushLayerKind::MinorMode(mode_id) => KeymapLayer::MinorMode(mode_id),
1950 PushLayerKind::Buffer => KeymapLayer::Buffer,
1951 };
1952 return Err(KeymapError::CapabilityDenied {
1953 capability,
1954 layer: placeholder,
1955 });
1956 }
1957 Ok(self.push_layer(kind, label, bindings))
1958 }
1959
1960 /// K.1.b (2026-05-30): pop a minor-mode layer by its
1961 /// `ModeId`. The natural complement to
1962 /// `push_layer(PushLayerKind::MinorMode(mode_id), …)` —
1963 /// callers don't have to thread a separate `LayerId`
1964 /// through teardown when the mode id is what they already
1965 /// know. No-op if no layer for `mode_id` is currently
1966 /// installed (defensive against double-pop on error paths).
1967 /// Returns `true` iff a layer was removed.
1968 pub fn pop_minor_mode_layer(&self, mode_id: ModeId) -> bool {
1969 let (removed, merged, minors) = {
1970 let mut inner = self.registry.inner.lock().expect("registry mutex");
1971 let pos = inner
1972 .layers
1973 .iter()
1974 .position(|l| l.layer == KeymapLayer::MinorMode(mode_id));
1975 let removed = if let Some(pos) = pos {
1976 inner.layers.remove(pos);
1977 true
1978 } else {
1979 false
1980 };
1981 (
1982 removed,
1983 inner.build_always_on_merged(),
1984 inner.build_gated_mode_tries(),
1985 )
1986 };
1987 self.registry.merged.store(Arc::new(merged));
1988 self.registry.gated_mode_tries.store(Arc::new(minors));
1989 // MARG.2: keep the reverse-cache in lockstep with the
1990 // merged trie. Every site that stores `merged` /
1991 // `gated_mode_tries` must also rebuild the reverse
1992 // cache or the keybinding annotator will surface
1993 // stale chord text.
1994 self.registry.rebuild_reverse_cache();
1995 removed
1996 }
1997}
1998
1999impl Default for KeymapHandle {
2000 fn default() -> Self {
2001 Self::new()
2002 }
2003}
2004
2005/// What kind of runtime-pushed layer to install.
2006///
2007/// K.1.b (2026-05-30): `MinorMode` now carries a typed
2008/// [`ModeId`] — the layer's identity = the mode's identity.
2009/// Pushing for the same `mode_id` is idempotent on the layer
2010/// (replaces bindings; no sibling layer minted). `Buffer`
2011/// stays opaque (a future K.1.x slice will type it on
2012/// `lattice_core::BufferId` for symmetry).
2013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2014pub enum PushLayerKind {
2015 /// Install / replace the [`KeymapLayer::MajorMode`] layer for this mode.
2016 MajorMode(ModeId),
2017 /// Install / replace the [`KeymapLayer::MinorMode`] layer for this mode.
2018 MinorMode(ModeId),
2019 /// Install / replace the single [`KeymapLayer::Buffer`] layer. There is
2020 /// one `Buffer` layer per registry, not one per buffer: a second push
2021 /// replaces the first's bindings and returns the same [`LayerId`].
2022 Buffer,
2023}
2024
2025fn default_label(layer: KeymapLayer) -> String {
2026 match layer {
2027 // K.2.4.A.2: user-facing friendly labels, not Debug slugs.
2028 KeymapLayer::Builtin => "Built-in".into(),
2029 KeymapLayer::MajorMode(mode_id) => format!("Major mode: {mode_id}"),
2030 // K.1.b: label derives from ModeId so `:describe-key`
2031 // provenance reads `Minor mode: diff-mode` without drift.
2032 KeymapLayer::MinorMode(mode_id) => format!("Minor mode: {mode_id}"),
2033 KeymapLayer::User => "User config".into(),
2034 KeymapLayer::Buffer => "Buffer".into(),
2035 }
2036}
2037
2038#[cfg(test)]
2039mod tests {
2040 #![allow(clippy::unwrap_used, clippy::panic)]
2041 use super::*;
2042 use lattice_protocol::chord::SpecialKey;
2043 use lattice_protocol::ids::CommandId;
2044
2045 fn invocation(n: u64) -> CommandInvocation {
2046 CommandInvocation::of(CommandId::new(n))
2047 }
2048
2049 fn src(label: &'static str) -> SourceLocation {
2050 let _ = label;
2051 SourceLocation::synthetic("test")
2052 }
2053
2054 fn lit(c: char) -> ChordPattern {
2055 ChordPattern::Literal(KeyChord::char(c))
2056 }
2057
2058 fn pressed(c: char) -> KeyChord {
2059 KeyChord::char(c)
2060 }
2061
2062 fn ctrl(c: char) -> KeyChord {
2063 KeyChord::ctrl(c)
2064 }
2065
2066 #[test]
2067 fn lookup_returns_bound_after_bind() {
2068 let h = KeymapHandle::new();
2069 h.bind(
2070 KeymapLayer::Builtin,
2071 BindingMode::Normal,
2072 &[lit('d'), lit('d')],
2073 invocation(1),
2074 src("dd"),
2075 );
2076 let r = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('d')]);
2077 match r {
2078 LookupResult::Bound { command, .. } => {
2079 assert_eq!(command.command.command, CommandId::new(1));
2080 }
2081 other => panic!("expected Bound, got {other:?}"),
2082 }
2083 }
2084
2085 #[test]
2086 fn bind_modes_registers_in_every_named_mode() {
2087 let h = KeymapHandle::new();
2088 h.bind_modes(
2089 KeymapLayer::Builtin,
2090 &[BindingMode::Normal, BindingMode::Visual],
2091 &[lit('z'), lit('n')],
2092 invocation(7),
2093 src("zn"),
2094 );
2095 for mode in [BindingMode::Normal, BindingMode::Visual] {
2096 match h.lookup(mode, &[pressed('z'), pressed('n')]) {
2097 LookupResult::Bound { command, .. } => {
2098 assert_eq!(command.command.command, CommandId::new(7));
2099 }
2100 other => panic!("expected Bound in {mode:?}, got {other:?}"),
2101 }
2102 }
2103 // A mode that was NOT named stays unbound.
2104 assert!(matches!(
2105 h.lookup(BindingMode::Insert, &[pressed('z'), pressed('n')]),
2106 LookupResult::Unbound
2107 ));
2108 }
2109
2110 #[test]
2111 fn bind_modes_empty_slice_is_a_noop() {
2112 let h = KeymapHandle::new();
2113 h.bind_modes(
2114 KeymapLayer::Builtin,
2115 &[],
2116 &[lit('x')],
2117 invocation(1),
2118 src("x"),
2119 );
2120 assert!(matches!(
2121 h.lookup(BindingMode::Normal, &[pressed('x')]),
2122 LookupResult::Unbound
2123 ));
2124 }
2125
2126 #[test]
2127 fn higher_layer_shadows_lower() {
2128 let h = KeymapHandle::new();
2129 h.bind(
2130 KeymapLayer::Builtin,
2131 BindingMode::Normal,
2132 &[lit('d'), lit('d')],
2133 invocation(100),
2134 src("builtin.dd"),
2135 );
2136 h.bind(
2137 KeymapLayer::User,
2138 BindingMode::Normal,
2139 &[lit('d'), lit('d')],
2140 invocation(200),
2141 src("user.dd"),
2142 );
2143 let r = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('d')]);
2144 match r {
2145 LookupResult::Bound { command, .. } => {
2146 assert_eq!(
2147 command.command.command,
2148 CommandId::new(200),
2149 "user layer must win over builtin"
2150 );
2151 assert_eq!(command.layer, KeymapLayer::User);
2152 }
2153 other => panic!("expected Bound, got {other:?}"),
2154 }
2155 }
2156
2157 #[test]
2158 fn unbinding_user_layer_uncovers_builtin() {
2159 let h = KeymapHandle::new();
2160 h.bind(
2161 KeymapLayer::Builtin,
2162 BindingMode::Normal,
2163 &[lit('d'), lit('d')],
2164 invocation(100),
2165 src("builtin.dd"),
2166 );
2167 h.bind(
2168 KeymapLayer::User,
2169 BindingMode::Normal,
2170 &[lit('d'), lit('d')],
2171 invocation(200),
2172 src("user.dd"),
2173 );
2174 // Unbind user.dd -> builtin.dd should resurface.
2175 let dropped = h
2176 .unbind(
2177 KeymapLayer::User,
2178 BindingMode::Normal,
2179 &[lit('d'), lit('d')],
2180 )
2181 .expect("user binding existed");
2182 assert_eq!(dropped.command.command, CommandId::new(200));
2183 let r = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('d')]);
2184 match r {
2185 LookupResult::Bound { command, .. } => {
2186 assert_eq!(command.command.command, CommandId::new(100));
2187 assert_eq!(command.layer, KeymapLayer::Builtin);
2188 }
2189 other => panic!("expected Bound, got {other:?}"),
2190 }
2191 }
2192
2193 #[test]
2194 fn modes_are_independent() {
2195 let h = KeymapHandle::new();
2196 h.bind(
2197 KeymapLayer::Builtin,
2198 BindingMode::Normal,
2199 &[lit('j')],
2200 invocation(1),
2201 src("normal.j"),
2202 );
2203 h.bind(
2204 KeymapLayer::Builtin,
2205 BindingMode::Visual,
2206 &[lit('j')],
2207 invocation(2),
2208 src("visual.j"),
2209 );
2210 // Same chord, different mode -> distinct bindings.
2211 let normal = h.lookup(BindingMode::Normal, &[pressed('j')]);
2212 let visual = h.lookup(BindingMode::Visual, &[pressed('j')]);
2213 match (normal, visual) {
2214 (LookupResult::Bound { command: nb, .. }, LookupResult::Bound { command: vb, .. }) => {
2215 assert_eq!(nb.command.command, CommandId::new(1));
2216 assert_eq!(vb.command.command, CommandId::new(2));
2217 }
2218 other => panic!("expected two Bound results, got {other:?}"),
2219 }
2220 }
2221
2222 #[test]
2223 fn unrelated_mode_lookup_is_unbound() {
2224 let h = KeymapHandle::new();
2225 h.bind(
2226 KeymapLayer::Builtin,
2227 BindingMode::Normal,
2228 &[lit('j')],
2229 invocation(1),
2230 src("normal.j"),
2231 );
2232 let r = h.lookup(BindingMode::Insert, &[pressed('j')]);
2233 assert!(matches!(r, LookupResult::Unbound), "got {r:?}");
2234 }
2235
2236 #[test]
2237 fn push_minor_mode_shadows_builtins_then_pop_restores() {
2238 let h = KeymapHandle::new();
2239 h.bind(
2240 KeymapLayer::Builtin,
2241 BindingMode::Insert,
2242 &[ChordPattern::Literal(KeyChord::special(SpecialKey::Tab))],
2243 invocation(1),
2244 src("builtin.tab"),
2245 );
2246
2247 // Active-snippet minor mode wants <Tab> for placeholder
2248 // navigation. Push a minor-mode layer with its own
2249 // <Tab> binding.
2250 let snippet_mode = ModeId::new("snippet");
2251 let mut minor_modes = HashMap::new();
2252 let mut t = KeymapTrie::new();
2253 let bound = Arc::new(BoundCommand::from_invocation(
2254 invocation(99),
2255 src("snippet.tab"),
2256 KeymapLayer::MinorMode(snippet_mode),
2257 ));
2258 t.insert(
2259 &[ChordPattern::Literal(KeyChord::special(SpecialKey::Tab))],
2260 bound,
2261 );
2262 minor_modes.insert(BindingMode::Insert, t);
2263 let id = h.push_layer(
2264 PushLayerKind::MinorMode(snippet_mode),
2265 "snippet",
2266 minor_modes,
2267 );
2268
2269 // <Tab> -> snippet.tab.
2270 let r = h.lookup(BindingMode::Insert, &[KeyChord::special(SpecialKey::Tab)]);
2271 match r {
2272 LookupResult::Bound { command, .. } => {
2273 assert_eq!(command.command.command, CommandId::new(99));
2274 }
2275 other => panic!("expected Bound (snippet), got {other:?}"),
2276 }
2277
2278 // Pop the layer -> builtin.tab resurfaces.
2279 h.pop_layer(id);
2280 let r = h.lookup(BindingMode::Insert, &[KeyChord::special(SpecialKey::Tab)]);
2281 match r {
2282 LookupResult::Bound { command, .. } => {
2283 assert_eq!(command.command.command, CommandId::new(1));
2284 }
2285 other => panic!("expected Bound (builtin), got {other:?}"),
2286 }
2287 }
2288
2289 #[test]
2290 fn pop_unknown_id_is_noop() {
2291 let h = KeymapHandle::new();
2292 h.pop_layer(LayerId(9999));
2293 // No panic, no state change.
2294 assert_eq!(h.binding_count(), 0);
2295 }
2296
2297 #[test]
2298 fn binding_count_tallies_across_layers() {
2299 let h = KeymapHandle::new();
2300 h.bind(
2301 KeymapLayer::Builtin,
2302 BindingMode::Normal,
2303 &[lit('j')],
2304 invocation(1),
2305 src("j"),
2306 );
2307 h.bind(
2308 KeymapLayer::Builtin,
2309 BindingMode::Normal,
2310 &[lit('k')],
2311 invocation(2),
2312 src("k"),
2313 );
2314 h.bind(
2315 KeymapLayer::User,
2316 BindingMode::Normal,
2317 &[lit('j')],
2318 invocation(3),
2319 src("user.j"),
2320 );
2321 // Count is per-binding-per-layer (3 entries: 2 builtin
2322 // + 1 user), not the merged-deduped count.
2323 assert_eq!(h.binding_count(), 3);
2324 }
2325
2326 #[test]
2327 fn lookup_partial_then_complete() {
2328 let h = KeymapHandle::new();
2329 h.bind(
2330 KeymapLayer::Builtin,
2331 BindingMode::Normal,
2332 &[lit('g'), lit('d')],
2333 invocation(1),
2334 src("gd"),
2335 );
2336 let r = h.lookup(BindingMode::Normal, &[pressed('g')]);
2337 assert!(matches!(r, LookupResult::Partial), "got {r:?}");
2338 let r = h.lookup(BindingMode::Normal, &[pressed('g'), pressed('d')]);
2339 assert!(matches!(r, LookupResult::Bound { .. }), "got {r:?}");
2340 }
2341
2342 #[test]
2343 fn lookup_against_empty_registry_is_unbound() {
2344 let h = KeymapHandle::new();
2345 let r = h.lookup(BindingMode::Normal, &[pressed('j')]);
2346 assert!(matches!(r, LookupResult::Unbound), "got {r:?}");
2347 }
2348
2349 // ---- DK.4: sequence SHAPE is activation-agnostic ----------------
2350 //
2351 // `:describe-key` answers for every key, not only the keys active where
2352 // the user stands. These pin the two halves of that: capture keeps
2353 // reading while any REGISTERED layer can extend the sequence, and a
2354 // prefix reports its subtree with each continuation flagged
2355 // active/inactive for the buffer described.
2356
2357 /// A three-chord binding owned by a MAJOR mode (org's
2358 /// `<C-c><C-x><C-b>`), asked about with NO modes active — which is
2359 /// exactly the context chord capture runs in, because the focused
2360 /// buffer while the prompt is open is `*command-line*`.
2361 ///
2362 /// The activation-gated question answers "nothing follows `<C-c><C-x>`"
2363 /// and capture submits two chords early. The shape question answers
2364 /// "org can still extend this", which is the truth about the keymap.
2365 #[test]
2366 fn expects_more_sees_layers_that_are_not_active() {
2367 let h = KeymapHandle::new();
2368 let org = ModeId::new("org-mode");
2369 h.bind(
2370 KeymapLayer::MajorMode(org),
2371 BindingMode::Normal,
2372 &[
2373 ChordPattern::Literal(ctrl('c')),
2374 ChordPattern::Literal(ctrl('x')),
2375 ChordPattern::Literal(ctrl('b')),
2376 ],
2377 invocation(1),
2378 src("org"),
2379 );
2380 let prefix = [ctrl('c'), ctrl('x')];
2381 // The old activation-gated query, with the minibuffer's (empty)
2382 // mode set — this is the truncation the user reported.
2383 assert!(
2384 !matches!(
2385 h.lookup_with_context(BindingMode::Normal, &prefix, &[]),
2386 LookupResult::Partial
2387 ),
2388 "precondition: an inactive major is invisible to a gated lookup"
2389 );
2390 assert!(
2391 h.any_layer_expects_more(&prefix),
2392 "capture must keep reading: org's layer can still extend <C-c><C-x>"
2393 );
2394 assert!(
2395 h.any_layer_expects_more(&[ctrl('c')]),
2396 "…and at every shorter depth of the same sequence"
2397 );
2398 assert!(
2399 !h.any_layer_expects_more(&[ctrl('c'), ctrl('x'), ctrl('b')]),
2400 "the complete sequence terminates — nothing is grown beneath it"
2401 );
2402 assert!(
2403 !h.any_layer_expects_more(&[ctrl('q')]),
2404 "an unregistered chord terminates immediately ('it does nothing' \
2405 is the answer that motivated capture)"
2406 );
2407 }
2408
2409 /// A chord that is BOUND at this depth terminates capture even though
2410 /// longer chords exist beneath it — matching dispatch, which stops at
2411 /// the first binding and never consults children. The continuations are
2412 /// still reported (flagged unreachable by the caller), because a chord
2413 /// silently killing the family below it is precisely what a user needs
2414 /// `:describe-key` to tell them.
2415 #[test]
2416 fn a_bound_prefix_terminates_capture_but_keeps_its_subtree_visible() {
2417 let h = KeymapHandle::new();
2418 let mode = ModeId::new("some-mode");
2419 h.bind(
2420 KeymapLayer::MinorMode(mode),
2421 BindingMode::Normal,
2422 &[lit('g'), lit('D')],
2423 invocation(1),
2424 src("bound-prefix"),
2425 );
2426 h.bind(
2427 KeymapLayer::MinorMode(mode),
2428 BindingMode::Normal,
2429 &[lit('g'), lit('D'), lit('d')],
2430 invocation(2),
2431 src("unreachable"),
2432 );
2433 assert!(
2434 !h.any_layer_expects_more(&[pressed('g'), pressed('D')]),
2435 "bound at this depth ⇒ capture submits, as dispatch would fire"
2436 );
2437 let cont = h.continuations(&[pressed('g'), pressed('D')], &[]);
2438 assert_eq!(cont.len(), 1, "the shadowed continuation is still listed");
2439 assert_eq!(cont[0].suffix, "d");
2440 }
2441
2442 /// The prefix answer itself: suffixes, layers, and an `active` flag that
2443 /// tracks the DESCRIBED buffer's modes rather than the query.
2444 #[test]
2445 fn continuations_list_every_layer_and_flag_the_active_ones() {
2446 let h = KeymapHandle::new();
2447 let org = ModeId::new("org-mode");
2448 let other = ModeId::new("other-mode");
2449 h.bind(
2450 KeymapLayer::MajorMode(org),
2451 BindingMode::Normal,
2452 &[
2453 ChordPattern::Literal(ctrl('c')),
2454 ChordPattern::Literal(ctrl('x')),
2455 ChordPattern::Literal(ctrl('b')),
2456 ],
2457 invocation(1),
2458 src("org"),
2459 );
2460 h.bind(
2461 KeymapLayer::MinorMode(other),
2462 BindingMode::Insert,
2463 &[
2464 ChordPattern::Literal(ctrl('c')),
2465 ChordPattern::Literal(ctrl('x')),
2466 lit('p'),
2467 ],
2468 invocation(2),
2469 src("other"),
2470 );
2471 let prefix = [ctrl('c'), ctrl('x')];
2472
2473 let none_active = h.continuations(&prefix, &[]);
2474 assert_eq!(
2475 none_active
2476 .iter()
2477 .map(|c| c.suffix.as_str())
2478 .collect::<Vec<_>>(),
2479 vec!["<C-b>", "p"],
2480 "both continuations are reported even with no mode active — \
2481 describe-key answers for every key"
2482 );
2483 assert!(
2484 none_active.iter().all(|c| !c.active),
2485 "…flagged inactive, so 'exists' is distinguishable from 'fires here'"
2486 );
2487 // Normal-mode entry sorts before the Insert-mode one.
2488 assert_eq!(none_active[0].mode, BindingMode::Normal);
2489 assert_eq!(none_active[1].mode, BindingMode::Insert);
2490
2491 let in_org = h.continuations(&prefix, &[org]);
2492 assert!(
2493 in_org[0].active && !in_org[1].active,
2494 "in an org buffer, org's continuation fires and the other does not"
2495 );
2496 }
2497
2498 /// A wildcard descent (`f{char}`, `'{mark}`) renders as `{char}` rather
2499 /// than being dropped from the listing.
2500 #[test]
2501 fn continuations_render_wildcard_descents() {
2502 let h = KeymapHandle::new();
2503 h.bind(
2504 KeymapLayer::Builtin,
2505 BindingMode::Normal,
2506 &[lit('g'), lit('\''), ChordPattern::CharLiteral],
2507 invocation(1),
2508 src("mark"),
2509 );
2510 let cont = h.continuations(&[pressed('g')], &[]);
2511 assert_eq!(cont.len(), 1);
2512 assert_eq!(cont[0].suffix, "'{char}");
2513 }
2514
2515 #[test]
2516 fn layer_label_round_trips_for_runtime_pushed_layers() {
2517 let h = KeymapHandle::new();
2518 let snippet_mode = ModeId::new("snippet");
2519 let mut bindings = HashMap::new();
2520 let mut t = KeymapTrie::new();
2521 let bound = Arc::new(BoundCommand::from_invocation(
2522 invocation(1),
2523 src("snippet"),
2524 KeymapLayer::MinorMode(snippet_mode),
2525 ));
2526 t.insert(&[lit('q')], bound);
2527 bindings.insert(BindingMode::Normal, t);
2528 let id = h.push_layer(PushLayerKind::MinorMode(snippet_mode), "snippet", bindings);
2529 assert_eq!(h.layer_label(id).as_deref(), Some("snippet"));
2530 // Unknown id -> None.
2531 assert!(h.layer_label(LayerId(9999)).is_none());
2532 }
2533
2534 // ---- Slice 8.h: capability gating ----
2535
2536 /// `Full` -- the host's startup capability -- writes to
2537 /// every layer. The built-in catalog enumeration relies on
2538 /// this.
2539 #[test]
2540 fn full_capability_writes_to_every_layer() {
2541 let h = KeymapHandle::new();
2542 for layer in [
2543 KeymapLayer::Builtin,
2544 KeymapLayer::MajorMode(ModeId::new("test-major")),
2545 KeymapLayer::MinorMode(ModeId::new("test-minor-7")),
2546 KeymapLayer::User,
2547 KeymapLayer::Buffer,
2548 ] {
2549 let r = h.try_bind(
2550 KeymapCapability::Full,
2551 layer,
2552 BindingMode::Normal,
2553 &[lit('j')],
2554 invocation(1),
2555 src("startup"),
2556 );
2557 assert!(r.is_ok(), "Full denied {layer:?}");
2558 // Clean up so the next iteration doesn't conflict.
2559 let _ = h.unbind(layer, BindingMode::Normal, &[lit('j')]);
2560 }
2561 }
2562
2563 /// `User` capability writes only to `KeymapLayer::User`.
2564 /// Mirrors the WIT spec: the compiled `init.rs` runs with
2565 /// this capability and can rebind `dd` etc., but can't
2566 /// touch the built-in catalog.
2567 #[test]
2568 fn user_capability_accepts_user_layer() {
2569 let h = KeymapHandle::new();
2570 let r = h.try_bind(
2571 KeymapCapability::User,
2572 KeymapLayer::User,
2573 BindingMode::Normal,
2574 &[lit('d'), lit('d')],
2575 invocation(42),
2576 src("init.rs:1"),
2577 );
2578 assert!(r.is_ok());
2579 }
2580
2581 #[test]
2582 fn user_capability_denies_builtin_layer() {
2583 let h = KeymapHandle::new();
2584 let r = h.try_bind(
2585 KeymapCapability::User,
2586 KeymapLayer::Builtin,
2587 BindingMode::Normal,
2588 &[lit('j')],
2589 invocation(1),
2590 src("init.rs"),
2591 );
2592 match r {
2593 Err(KeymapError::CapabilityDenied {
2594 capability: KeymapCapability::User,
2595 layer: KeymapLayer::Builtin,
2596 }) => {}
2597 other => panic!("expected CapabilityDenied, got {other:?}"),
2598 }
2599 }
2600
2601 #[test]
2602 fn user_capability_denies_minor_mode_and_buffer_layers() {
2603 let h = KeymapHandle::new();
2604 for layer in [
2605 KeymapLayer::MinorMode(ModeId::new("test-minor")),
2606 KeymapLayer::Buffer,
2607 ] {
2608 let r = h.try_bind(
2609 KeymapCapability::User,
2610 layer,
2611 BindingMode::Normal,
2612 &[lit('j')],
2613 invocation(1),
2614 src("init.rs"),
2615 );
2616 assert!(
2617 matches!(r, Err(KeymapError::CapabilityDenied { .. })),
2618 "User must deny {layer:?}"
2619 );
2620 }
2621 }
2622
2623 #[test]
2624 fn minor_mode_capability_accepts_minor_mode_and_buffer() {
2625 let h = KeymapHandle::new();
2626 for layer in [
2627 KeymapLayer::MinorMode(ModeId::new("test-minor-3")),
2628 KeymapLayer::Buffer,
2629 ] {
2630 let r = h.try_bind(
2631 KeymapCapability::MinorMode,
2632 layer,
2633 BindingMode::Normal,
2634 &[lit('j')],
2635 invocation(1),
2636 src("plugin"),
2637 );
2638 assert!(r.is_ok(), "MinorMode denied {layer:?}");
2639 let _ = h.unbind(layer, BindingMode::Normal, &[lit('j')]);
2640 }
2641 }
2642
2643 #[test]
2644 fn minor_mode_capability_denies_builtin_and_user() {
2645 let h = KeymapHandle::new();
2646 for layer in [
2647 KeymapLayer::Builtin,
2648 KeymapLayer::MajorMode(ModeId::new("major-mode")),
2649 KeymapLayer::User,
2650 ] {
2651 let r = h.try_bind(
2652 KeymapCapability::MinorMode,
2653 layer,
2654 BindingMode::Normal,
2655 &[lit('j')],
2656 invocation(1),
2657 src("plugin"),
2658 );
2659 assert!(
2660 matches!(r, Err(KeymapError::CapabilityDenied { .. })),
2661 "MinorMode must deny {layer:?}"
2662 );
2663 }
2664 }
2665
2666 #[test]
2667 fn owned_layer_capability_accepts_only_its_own_id() {
2668 let h = KeymapHandle::new();
2669 // Push two minor-mode layers; only the first's mode is
2670 // authorised by the OwnedLayer capability we mint.
2671 let mode_a = ModeId::new("plugin-a");
2672 let mode_b = ModeId::new("plugin-b");
2673 let _id_a = h.push_layer(PushLayerKind::MinorMode(mode_a), "plugin-a", HashMap::new());
2674 let _id_b = h.push_layer(PushLayerKind::MinorMode(mode_b), "plugin-b", HashMap::new());
2675 let cap = KeymapCapability::OwnedLayer { mode_id: mode_a };
2676
2677 // Plugin-a writes to its own MinorMode(mode_a) -- ok.
2678 let r = h.try_bind(
2679 cap,
2680 KeymapLayer::MinorMode(mode_a),
2681 BindingMode::Normal,
2682 &[lit('j')],
2683 invocation(1),
2684 src("plugin-a"),
2685 );
2686 assert!(r.is_ok());
2687
2688 // Plugin-a tries to write to plugin-b's layer -- denied.
2689 let r = h.try_bind(
2690 cap,
2691 KeymapLayer::MinorMode(mode_b),
2692 BindingMode::Normal,
2693 &[lit('k')],
2694 invocation(2),
2695 src("plugin-a"),
2696 );
2697 assert!(matches!(r, Err(KeymapError::CapabilityDenied { .. })));
2698
2699 // Plugin-a tries to write to Builtin -- denied.
2700 let r = h.try_bind(
2701 cap,
2702 KeymapLayer::Builtin,
2703 BindingMode::Normal,
2704 &[lit('k')],
2705 invocation(2),
2706 src("plugin-a"),
2707 );
2708 assert!(matches!(r, Err(KeymapError::CapabilityDenied { .. })));
2709 }
2710
2711 #[test]
2712 fn remove_layer_drops_a_minor_modes_bindings_leaving_others() {
2713 let h = KeymapHandle::new();
2714 let mode_a = ModeId::new("plugin-a-mode");
2715 let mode_b = ModeId::new("plugin-b-mode");
2716 // Two plugin minor-mode layers, each with one chord (the shape
2717 // `bind_mode_keymap` produces — an implicitly-created MinorMode layer,
2718 // no LayerId handed back to the host).
2719 h.try_bind(
2720 KeymapCapability::OwnedLayer { mode_id: mode_a },
2721 KeymapLayer::MinorMode(mode_a),
2722 BindingMode::Normal,
2723 &[lit('j')],
2724 invocation(1),
2725 src("plugin-a"),
2726 )
2727 .unwrap();
2728 h.try_bind(
2729 KeymapCapability::OwnedLayer { mode_id: mode_b },
2730 KeymapLayer::MinorMode(mode_b),
2731 BindingMode::Normal,
2732 &[lit('k')],
2733 invocation(2),
2734 src("plugin-b"),
2735 )
2736 .unwrap();
2737 assert_eq!(h.binding_count(), 2);
2738
2739 // Remove plugin-a's layer by its MinorMode key (the host has no LayerId).
2740 h.remove_layer(KeymapLayer::MinorMode(mode_a));
2741 assert_eq!(h.binding_count(), 1);
2742 // plugin-a's chord is gone from every layer; plugin-b's survives.
2743 assert!(
2744 h.enumerate_chord_bindings(BindingMode::Normal, &[KeyChord::char('j')])
2745 .is_empty()
2746 );
2747 let b_hits = h.enumerate_chord_bindings(BindingMode::Normal, &[KeyChord::char('k')]);
2748 assert_eq!(b_hits.len(), 1);
2749 assert_eq!(b_hits[0].0, KeymapLayer::MinorMode(mode_b));
2750
2751 // Idempotent: removing an already-gone layer is a no-op.
2752 h.remove_layer(KeymapLayer::MinorMode(mode_a));
2753 assert_eq!(h.binding_count(), 1);
2754 }
2755
2756 #[test]
2757 fn user_capability_cannot_push_layer() {
2758 let h = KeymapHandle::new();
2759 let r = h.try_push_layer(
2760 KeymapCapability::User,
2761 PushLayerKind::MinorMode(ModeId::new("should-fail-mode")),
2762 "should-fail",
2763 HashMap::new(),
2764 );
2765 assert!(matches!(r, Err(KeymapError::CapabilityDenied { .. })));
2766 }
2767
2768 #[test]
2769 fn minor_mode_capability_can_push_layer() {
2770 let h = KeymapHandle::new();
2771 let r = h.try_push_layer(
2772 KeymapCapability::MinorMode,
2773 PushLayerKind::MinorMode(ModeId::new("plugin-overlay")),
2774 "plugin-overlay",
2775 HashMap::new(),
2776 );
2777 assert!(r.is_ok());
2778 }
2779
2780 // ── OM.2b: `<leader>` expansion ────────────────────────────────
2781
2782 #[test]
2783 fn expand_leader_substitutes_both_spellings_anywhere() {
2784 assert_eq!(expand_leader("<leader>oh", "<Space>"), "<Space>oh");
2785 assert_eq!(expand_leader("<Leader>oh", "<Space>"), "<Space>oh");
2786 // Vim expands the token wherever it appears, not only at the front.
2787 assert_eq!(expand_leader("g<leader>x", ","), "g,x");
2788 // Two occurrences both expand.
2789 assert_eq!(expand_leader("<leader><leader>", ","), ",,");
2790 // Untouched when absent — the overwhelmingly common case.
2791 assert_eq!(expand_leader("<C-w>j", "<Space>"), "<C-w>j");
2792 assert_eq!(expand_leader("dd", "<Space>"), "dd");
2793 }
2794
2795 #[test]
2796 fn a_leader_binding_resolves_under_the_expanded_chord() {
2797 let h = KeymapHandle::new();
2798 h.try_bind_chord_string(
2799 KeymapCapability::User,
2800 KeymapLayer::User,
2801 BindingMode::Normal,
2802 "<leader>oh",
2803 invocation(7),
2804 src("org"),
2805 )
2806 .expect("binds");
2807
2808 // It landed as `<Space>oh` — an ordinary chord sequence with no memory
2809 // of having been written with a leader.
2810 let seq = parse_chord_sequence("<Space>oh").expect("parses");
2811 assert!(matches!(
2812 h.lookup(BindingMode::Normal, &seq),
2813 LookupResult::Bound { .. }
2814 ));
2815 }
2816
2817 #[test]
2818 fn set_leader_changes_what_later_bindings_expand_to() {
2819 let h = KeymapHandle::new();
2820 assert_eq!(*h.leader(), DEFAULT_LEADER);
2821 h.set_leader(",");
2822 h.try_bind_chord_string(
2823 KeymapCapability::User,
2824 KeymapLayer::User,
2825 BindingMode::Normal,
2826 "<leader>x",
2827 invocation(9),
2828 src("user"),
2829 )
2830 .expect("binds");
2831 let seq = parse_chord_sequence(",x").expect("parses");
2832 assert!(matches!(
2833 h.lookup(BindingMode::Normal, &seq),
2834 LookupResult::Bound { .. }
2835 ));
2836 }
2837
2838 #[test]
2839 fn a_leader_binding_can_be_unbound_by_the_string_that_created_it() {
2840 // The symmetry `try_unbind_chord_string` exists for. Without
2841 // expanding on the way out, a plugin could bind `<leader>x` and never
2842 // reverse it on unload.
2843 let h = KeymapHandle::new();
2844 h.try_bind_chord_string(
2845 KeymapCapability::User,
2846 KeymapLayer::User,
2847 BindingMode::Normal,
2848 "<leader>x",
2849 invocation(11),
2850 src("user"),
2851 )
2852 .expect("binds");
2853 let removed = h
2854 .try_unbind_chord_string(
2855 KeymapCapability::User,
2856 KeymapLayer::User,
2857 BindingMode::Normal,
2858 "<leader>x",
2859 )
2860 .expect("no capability or parse error");
2861 assert!(removed.is_some(), "the leader binding was reversed");
2862 }
2863
2864 #[test]
2865 fn a_malformed_leader_degrades_to_an_invalid_chord_not_a_panic() {
2866 let h = KeymapHandle::new();
2867 h.set_leader("<not-a-key>");
2868 let err = h.try_bind_chord_string(
2869 KeymapCapability::User,
2870 KeymapLayer::User,
2871 BindingMode::Normal,
2872 "<leader>x",
2873 invocation(13),
2874 src("user"),
2875 );
2876 assert!(
2877 matches!(err, Err(KeymapError::InvalidChord(_))),
2878 "a bad leader surfaces per-binding, skipped and logged by the caller"
2879 );
2880 }
2881
2882 #[test]
2883 fn try_bind_chord_string_parses_and_binds() {
2884 let h = KeymapHandle::new();
2885 let r = h.try_bind_chord_string(
2886 KeymapCapability::User,
2887 KeymapLayer::User,
2888 BindingMode::Normal,
2889 "<C-w>j",
2890 invocation(7),
2891 src("init.rs"),
2892 );
2893 assert!(r.is_ok());
2894
2895 // The path must round-trip through the trie -- press
2896 // <C-w> then j and the user-layer binding fires.
2897 let lookup = h.lookup(
2898 BindingMode::Normal,
2899 &[KeyChord::ctrl('w'), KeyChord::char('j')],
2900 );
2901 match lookup {
2902 LookupResult::Bound { command, .. } => {
2903 assert_eq!(command.command.command, CommandId::new(7));
2904 assert_eq!(command.layer, KeymapLayer::User);
2905 }
2906 other => panic!("expected Bound, got {other:?}"),
2907 }
2908 }
2909
2910 #[test]
2911 fn try_bind_chord_string_rejects_invalid_chord() {
2912 let h = KeymapHandle::new();
2913 // `<lt>` is parseable; `<` alone is not (unterminated angle).
2914 let r = h.try_bind_chord_string(
2915 KeymapCapability::User,
2916 KeymapLayer::User,
2917 BindingMode::Normal,
2918 "<not-closed",
2919 invocation(1),
2920 src("init.rs"),
2921 );
2922 assert!(matches!(r, Err(KeymapError::InvalidChord(_))));
2923 }
2924
2925 /// Architecture-doc test: "user remaps `dd` and the
2926 /// rebinding survives a restart". Persistence isn't a
2927 /// registry concern (init.rs reruns at boot), so we
2928 /// simulate the surviving-restart shape: the user's
2929 /// `[d, d]` binding at `KeymapLayer::User` overrides the
2930 /// built-in's same-path binding, and the override stays
2931 /// authoritative across an arbitrary number of intervening
2932 /// reads / merges.
2933 #[test]
2934 fn user_remaps_dd_and_overrides_builtin() {
2935 let h = KeymapHandle::new();
2936 // Built-in catalog -- `[d, d]` -> command 100.
2937 h.try_bind(
2938 KeymapCapability::Full,
2939 KeymapLayer::Builtin,
2940 BindingMode::Normal,
2941 &[lit('d'), lit('d')],
2942 invocation(100),
2943 src("builtin.dd"),
2944 )
2945 .unwrap();
2946 // User `init.rs` -- rebind `[d, d]` -> command 200.
2947 h.try_bind(
2948 KeymapCapability::User,
2949 KeymapLayer::User,
2950 BindingMode::Normal,
2951 &[lit('d'), lit('d')],
2952 invocation(200),
2953 src("init.rs:42"),
2954 )
2955 .unwrap();
2956
2957 let r = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('d')]);
2958 match r {
2959 LookupResult::Bound { command, .. } => {
2960 assert_eq!(
2961 command.command.command,
2962 CommandId::new(200),
2963 "user override must win",
2964 );
2965 assert_eq!(command.layer, KeymapLayer::User);
2966 }
2967 other => panic!("expected Bound (user.dd), got {other:?}"),
2968 }
2969
2970 // Drive a few synthetic reads to assure the override
2971 // survives the merged-trie rebuild even when other
2972 // unrelated writes happen.
2973 for c in ['j', 'k', 'l'] {
2974 h.try_bind(
2975 KeymapCapability::Full,
2976 KeymapLayer::Builtin,
2977 BindingMode::Normal,
2978 &[lit(c)],
2979 invocation(c as u64),
2980 src("builtin"),
2981 )
2982 .unwrap();
2983 }
2984 let r = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('d')]);
2985 match r {
2986 LookupResult::Bound { command, .. } => {
2987 assert_eq!(command.command.command, CommandId::new(200));
2988 }
2989 other => panic!("expected Bound (user.dd) after extra binds, got {other:?}"),
2990 }
2991 }
2992
2993 /// Architecture-doc test: "two plugins try to bind the
2994 /// same chord". Each plugin pushes its own MinorMode layer;
2995 /// the registry merges in priority order (LayerId
2996 /// ascending). The plugin pushed last wins, but the older
2997 /// plugin's binding stays in its layer so a future pop_layer
2998 /// of the winner restores it.
2999 #[test]
3000 fn conflicting_plugins_resolve_via_layer_priority() {
3001 let h = KeymapHandle::new();
3002 let mode_a = ModeId::new("plugin-a");
3003 let mode_b = ModeId::new("plugin-b");
3004
3005 // Plugin A pushes its layer + binds `<leader>x`.
3006 let _id_a = h.push_layer(PushLayerKind::MinorMode(mode_a), "plugin-a", HashMap::new());
3007 h.try_bind(
3008 KeymapCapability::OwnedLayer { mode_id: mode_a },
3009 KeymapLayer::MinorMode(mode_a),
3010 BindingMode::Normal,
3011 &[lit('x')],
3012 invocation(1),
3013 src("plugin-a"),
3014 )
3015 .unwrap();
3016
3017 // Plugin B pushes after A. K.1.b: the registry sorts
3018 // MinorMode layers by ModeId (alphabetical via the
3019 // interned string), so "plugin-b" > "plugin-a"; B's
3020 // layer ends up higher in the merge and wins on
3021 // overlapping chords. K.1.c will replace this
3022 // ModeId-alphabetic ordering with per-buffer active-
3023 // mode reverse-activation order.
3024 let id_b = h.push_layer(PushLayerKind::MinorMode(mode_b), "plugin-b", HashMap::new());
3025 h.try_bind(
3026 KeymapCapability::OwnedLayer { mode_id: mode_b },
3027 KeymapLayer::MinorMode(mode_b),
3028 BindingMode::Normal,
3029 &[lit('x')],
3030 invocation(2),
3031 src("plugin-b"),
3032 )
3033 .unwrap();
3034
3035 // Plugin B's binding wins (higher ModeId in alpha order).
3036 let r = h.lookup(BindingMode::Normal, &[pressed('x')]);
3037 match r {
3038 LookupResult::Bound { command, .. } => {
3039 assert_eq!(command.command.command, CommandId::new(2));
3040 }
3041 other => panic!("expected Bound (plugin-b.x), got {other:?}"),
3042 }
3043
3044 // Pop plugin B; plugin A's binding resurfaces.
3045 h.pop_layer(id_b);
3046 let r = h.lookup(BindingMode::Normal, &[pressed('x')]);
3047 match r {
3048 LookupResult::Bound { command, .. } => {
3049 assert_eq!(
3050 command.command.command,
3051 CommandId::new(1),
3052 "plugin-a's binding should reappear after b pops",
3053 );
3054 }
3055 other => panic!("expected Bound (plugin-a.x), got {other:?}"),
3056 }
3057 }
3058
3059 /// Architecture-doc test: "plugin binds chord that fires
3060 /// plugin command". Without the WASM host, we simulate the
3061 /// host-side bind path: a plugin with a dedicated
3062 /// `OwnedLayer` capability binds a chord to a typed
3063 /// `CommandInvocation` carrying the plugin's command id.
3064 /// Lookup of the chord returns the plugin command.
3065 #[test]
3066 fn plugin_binds_chord_that_fires_plugin_command() {
3067 let h = KeymapHandle::new();
3068 let mode_id = ModeId::new("plugin-foo");
3069 let _id = h.push_layer(
3070 PushLayerKind::MinorMode(mode_id),
3071 "plugin-foo",
3072 HashMap::new(),
3073 );
3074 let cap = KeymapCapability::OwnedLayer { mode_id };
3075 let plugin_cmd = invocation(0xFEED);
3076
3077 // Bind `<C-x>fo` (multi-chord prefix, since `<leader>`
3078 // isn't yet parseable by `parse_chord_sequence`).
3079 h.try_bind_chord_string(
3080 cap,
3081 KeymapLayer::MinorMode(mode_id),
3082 BindingMode::Normal,
3083 "<C-x>fo",
3084 plugin_cmd.clone(),
3085 src("plugin-foo:wit"),
3086 )
3087 .unwrap();
3088
3089 let path = vec![
3090 KeyChord::ctrl('x'),
3091 KeyChord::char('f'),
3092 KeyChord::char('o'),
3093 ];
3094 let r = h.lookup(BindingMode::Normal, &path);
3095 match r {
3096 LookupResult::Bound { command, .. } => {
3097 assert_eq!(command.command.command, plugin_cmd.command);
3098 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
3099 }
3100 other => panic!("expected Bound (plugin command), got {other:?}"),
3101 }
3102 }
3103
3104 // ---- K.1.c: per-buffer active-mode filter ----
3105
3106 /// K.1.c: `lookup_with_context` with empty `active_modes`
3107 /// skips every minor-mode layer's bindings. Legacy
3108 /// `lookup` (which iterates *all* registered minor modes)
3109 /// continues to see them — that's the back-compat path.
3110 /// Together: the new context-aware API lets callers opt
3111 /// into per-buffer gating without disrupting any existing
3112 /// dispatch path.
3113 #[test]
3114 fn lookup_with_context_empty_active_modes_skips_minor_modes() {
3115 let h = KeymapHandle::new();
3116 let diff_mode = ModeId::new("diff-mode");
3117 // Push diff-mode with `do` → command 42.
3118 let mut bindings = HashMap::new();
3119 let mut trie = KeymapTrie::new();
3120 let bound = Arc::new(BoundCommand::from_invocation(
3121 invocation(42),
3122 src("diff-mode.do"),
3123 KeymapLayer::MinorMode(diff_mode),
3124 ));
3125 trie.insert(&[lit('d'), lit('o')], bound);
3126 bindings.insert(BindingMode::Normal, trie);
3127 h.push_layer(PushLayerKind::MinorMode(diff_mode), "diff-mode", bindings);
3128
3129 // Legacy lookup sees the binding (all modes active).
3130 let legacy = h.lookup(BindingMode::Normal, &[pressed('d'), pressed('o')]);
3131 assert!(
3132 matches!(legacy, LookupResult::Bound { .. }),
3133 "legacy lookup must see diff-mode.do"
3134 );
3135
3136 // Context-aware lookup with empty active_modes does NOT
3137 // fire the diff-mode binding — diff-mode isn't active.
3138 let ctx_empty =
3139 h.lookup_with_context(BindingMode::Normal, &[pressed('d'), pressed('o')], &[]);
3140 assert!(
3141 matches!(ctx_empty, LookupResult::Unbound),
3142 "lookup_with_context(&[]) must NOT see diff-mode.do (mode not active)"
3143 );
3144
3145 // Context-aware with diff-mode listed → fires.
3146 let ctx_active = h.lookup_with_context(
3147 BindingMode::Normal,
3148 &[pressed('d'), pressed('o')],
3149 &[diff_mode],
3150 );
3151 match ctx_active {
3152 LookupResult::Bound { command, .. } => {
3153 assert_eq!(command.command.command, CommandId::new(42));
3154 }
3155 other => panic!("expected Bound (diff-mode.do active), got {other:?}"),
3156 }
3157 }
3158
3159 /// K.1.c: chord reuse across modes is the headline
3160 /// composability story. Two modes both bind `do` to
3161 /// different commands; the lookup result depends on which
3162 /// mode is in `active_modes` for that buffer. Per-buffer
3163 /// activation drives semantics — exactly the emacs
3164 /// `(:map foo-mode-map …)` shape.
3165 #[test]
3166 fn lookup_with_context_chord_reuse_across_modes() {
3167 let h = KeymapHandle::new();
3168 let diff_mode = ModeId::new("diff-mode");
3169 let overlay_mode = ModeId::new("my-overlay-mode");
3170
3171 // diff-mode binds `do` → command 100 (diff-get).
3172 let mut diff_bindings = HashMap::new();
3173 let mut diff_trie = KeymapTrie::new();
3174 diff_trie.insert(
3175 &[lit('d'), lit('o')],
3176 Arc::new(BoundCommand::from_invocation(
3177 invocation(100),
3178 src("diff-mode.do"),
3179 KeymapLayer::MinorMode(diff_mode),
3180 )),
3181 );
3182 diff_bindings.insert(BindingMode::Normal, diff_trie);
3183 h.push_layer(
3184 PushLayerKind::MinorMode(diff_mode),
3185 "diff-mode",
3186 diff_bindings,
3187 );
3188
3189 // overlay-mode binds the same `do` → command 200.
3190 let mut overlay_bindings = HashMap::new();
3191 let mut overlay_trie = KeymapTrie::new();
3192 overlay_trie.insert(
3193 &[lit('d'), lit('o')],
3194 Arc::new(BoundCommand::from_invocation(
3195 invocation(200),
3196 src("overlay.do"),
3197 KeymapLayer::MinorMode(overlay_mode),
3198 )),
3199 );
3200 overlay_bindings.insert(BindingMode::Normal, overlay_trie);
3201 h.push_layer(
3202 PushLayerKind::MinorMode(overlay_mode),
3203 "overlay",
3204 overlay_bindings,
3205 );
3206
3207 // Buffer A: only diff-mode active → diff-mode.do wins.
3208 match h.lookup_with_context(
3209 BindingMode::Normal,
3210 &[pressed('d'), pressed('o')],
3211 &[diff_mode],
3212 ) {
3213 LookupResult::Bound { command, .. } => {
3214 assert_eq!(command.command.command, CommandId::new(100));
3215 }
3216 other => panic!("expected diff-mode.do, got {other:?}"),
3217 }
3218
3219 // Buffer B: only overlay-mode active → overlay.do wins.
3220 match h.lookup_with_context(
3221 BindingMode::Normal,
3222 &[pressed('d'), pressed('o')],
3223 &[overlay_mode],
3224 ) {
3225 LookupResult::Bound { command, .. } => {
3226 assert_eq!(command.command.command, CommandId::new(200));
3227 }
3228 other => panic!("expected overlay.do, got {other:?}"),
3229 }
3230
3231 // Buffer C: neither active → no binding.
3232 let neither =
3233 h.lookup_with_context(BindingMode::Normal, &[pressed('d'), pressed('o')], &[]);
3234 assert!(matches!(neither, LookupResult::Unbound));
3235 }
3236
3237 /// A `MajorMode` layer must be gated by the active major,
3238 /// exactly like a `MinorMode` layer is gated by active minors.
3239 /// Regression: major-mode keymaps were folded into the
3240 /// always-on merge, so the first major with a real keymap
3241 /// (`ai-conversation`'s `i` → focus-prompt) fired its chords in
3242 /// EVERY buffer — pressing `i` on the read-only dashboard jumped
3243 /// the cursor to EOF and entered Insert.
3244 #[test]
3245 fn lookup_with_context_gates_major_mode_by_active_major() {
3246 let h = KeymapHandle::new();
3247 let convo = ModeId::new("ai-conversation-mode");
3248 // ai-conversation binds `i` → command 156 (focus-prompt),
3249 // registered as a MAJOR-mode layer.
3250 let mut bindings = HashMap::new();
3251 let mut trie = KeymapTrie::new();
3252 trie.insert(
3253 &[lit('i')],
3254 Arc::new(BoundCommand::from_invocation(
3255 invocation(156),
3256 src("ai-conversation.focus-prompt"),
3257 KeymapLayer::MajorMode(convo),
3258 )),
3259 );
3260 bindings.insert(BindingMode::Normal, trie);
3261 h.push_layer(
3262 PushLayerKind::MajorMode(convo),
3263 "ai-conversation-mode",
3264 bindings,
3265 );
3266
3267 // A buffer whose active major is NOT ai-conversation (e.g. the
3268 // dashboard) must NOT resolve `i` to focus-prompt.
3269 let other_major = ModeId::new("dashboard-mode");
3270 let on_dashboard =
3271 h.lookup_with_context(BindingMode::Normal, &[pressed('i')], &[other_major]);
3272 assert!(
3273 matches!(on_dashboard, LookupResult::Unbound),
3274 "major-mode `i` must NOT fire when a different major is active, got {on_dashboard:?}"
3275 );
3276
3277 // Empty active modes (no major resolved yet) — also must not fire.
3278 let no_modes = h.lookup_with_context(BindingMode::Normal, &[pressed('i')], &[]);
3279 assert!(
3280 matches!(no_modes, LookupResult::Unbound),
3281 "major-mode `i` must NOT fire with no active major, got {no_modes:?}"
3282 );
3283
3284 // The ai-conversation buffer (its major active) DOES resolve it.
3285 let on_convo = h.lookup_with_context(BindingMode::Normal, &[pressed('i')], &[convo]);
3286 match on_convo {
3287 LookupResult::Bound { command, .. } => {
3288 assert_eq!(command.command.command, CommandId::new(156));
3289 }
3290 other => panic!("expected focus-prompt bound on ai-conversation, got {other:?}"),
3291 }
3292 }
3293
3294 /// Introspection regression (the `:describe-key` half of 210da76c):
3295 /// `resolve_trace` must mark a `MajorMode` hit `active` iff its id is the
3296 /// buffer's active major — NOT unconditionally. The old code hard-coded
3297 /// `MajorMode(_) => true` (a stale "majors are always-on" assumption), so
3298 /// `:describe-key i` reported `ai-conversation`'s `i` → focus-prompt as
3299 /// firing in EVERY buffer.
3300 #[test]
3301 fn resolve_trace_gates_major_mode_hit_by_active_major() {
3302 let h = KeymapHandle::new();
3303 let convo = ModeId::new("ai-conversation-mode");
3304 // Builtin `i` (always-on) + a MajorMode(ai-conversation) `i`.
3305 h.bind(
3306 KeymapLayer::Builtin,
3307 BindingMode::Normal,
3308 &[lit('i')],
3309 invocation(1),
3310 src("builtin.insert"),
3311 );
3312 let mut bindings = HashMap::new();
3313 let mut trie = KeymapTrie::new();
3314 trie.insert(
3315 &[lit('i')],
3316 Arc::new(BoundCommand::from_invocation(
3317 invocation(156),
3318 src("ai-conversation.focus-prompt"),
3319 KeymapLayer::MajorMode(convo),
3320 )),
3321 );
3322 bindings.insert(BindingMode::Normal, trie);
3323 h.push_layer(
3324 PushLayerKind::MajorMode(convo),
3325 "ai-conversation-mode",
3326 bindings,
3327 );
3328
3329 let major_hit = |active_modes: &[ModeId]| -> bool {
3330 let res = h.resolve_trace(BindingMode::Normal, &[pressed('i')], active_modes);
3331 res.hits
3332 .iter()
3333 .find(|hit| matches!(hit.layer, KeymapLayer::MajorMode(id) if id == convo))
3334 .map(|hit| hit.active)
3335 .expect("the MajorMode(ai-conversation) hit is enumerated")
3336 };
3337
3338 // A different active major (e.g. the dashboard) → NOT active.
3339 assert!(
3340 !major_hit(&[ModeId::new("dashboard-mode")]),
3341 "a non-active major's binding must not be marked active in introspection",
3342 );
3343 // No active major → NOT active.
3344 assert!(
3345 !major_hit(&[]),
3346 "no active major → the major hit is inactive"
3347 );
3348 // The ai-conversation buffer (its major active) → active.
3349 assert!(
3350 major_hit(&[convo]),
3351 "the active major's binding IS active on its own buffer",
3352 );
3353
3354 // The Builtin hit is always active regardless of the mode slice.
3355 let res = h.resolve_trace(BindingMode::Normal, &[pressed('i')], &[]);
3356 let builtin_active = res
3357 .hits
3358 .iter()
3359 .find(|hit| matches!(hit.layer, KeymapLayer::Builtin))
3360 .map(|hit| hit.active)
3361 .expect("the Builtin hit is enumerated");
3362 assert!(builtin_active, "Builtin is always-on");
3363 }
3364
3365 /// K.1.c: "last-activated wins" for overlapping minor-mode
3366 /// bindings — the per-buffer activation order in
3367 /// `active_modes` is iterated in order, with later
3368 /// entries overlaying earlier ones (matching emacs's
3369 /// `minor-mode-map-alist` re-promotion semantics).
3370 /// Reordering `active_modes` flips the winner without
3371 /// re-registering anything in the keymap registry.
3372 #[test]
3373 fn lookup_with_context_last_activated_wins() {
3374 let h = KeymapHandle::new();
3375 let mode_a = ModeId::new("mode-a");
3376 let mode_b = ModeId::new("mode-b");
3377
3378 let bind_in = |mode_id: ModeId, cmd: u64| {
3379 let mut bindings = HashMap::new();
3380 let mut trie = KeymapTrie::new();
3381 trie.insert(
3382 &[lit('x')],
3383 Arc::new(BoundCommand::from_invocation(
3384 invocation(cmd),
3385 src("test"),
3386 KeymapLayer::MinorMode(mode_id),
3387 )),
3388 );
3389 bindings.insert(BindingMode::Normal, trie);
3390 h.push_layer(PushLayerKind::MinorMode(mode_id), "test", bindings);
3391 };
3392 bind_in(mode_a, 1);
3393 bind_in(mode_b, 2);
3394
3395 // [a, b] order: b activated last → b wins.
3396 match h.lookup_with_context(BindingMode::Normal, &[pressed('x')], &[mode_a, mode_b]) {
3397 LookupResult::Bound { command, .. } => {
3398 assert_eq!(
3399 command.command.command,
3400 CommandId::new(2),
3401 "last-activated (b) must win",
3402 );
3403 }
3404 other => panic!("expected Bound, got {other:?}"),
3405 }
3406
3407 // [b, a] order: a activated last → a wins.
3408 match h.lookup_with_context(BindingMode::Normal, &[pressed('x')], &[mode_b, mode_a]) {
3409 LookupResult::Bound { command, .. } => {
3410 assert_eq!(
3411 command.command.command,
3412 CommandId::new(1),
3413 "reordering active_modes flips the winner",
3414 );
3415 }
3416 other => panic!("expected Bound, got {other:?}"),
3417 }
3418 }
3419
3420 // ── VM.4: a motion is live in Visual (and Select) by construction ──────
3421
3422 fn stub_motion() -> lattice_grammar::MotionSpec {
3423 lattice_grammar::MotionSpec {
3424 curswant: lattice_grammar::CurswantEffect::default(),
3425 jump: false,
3426 exclusive: true,
3427 apply: Arc::new(|ctx| {
3428 Ok(lattice_grammar::registry::MotionResult {
3429 curswant: None,
3430 target: ctx.from,
3431 linewise: false,
3432 exclusive: None,
3433 notice: None,
3434 })
3435 }),
3436 args_schema: vec![],
3437 }
3438 }
3439
3440 /// A registry holding one motion and one action, as the live handle the
3441 /// keymap expects. Two kinds, because every mirror rule is a statement
3442 /// about the difference between them.
3443 fn motion_and_action() -> (lattice_grammar::CommandRegistryHandle, CommandId, CommandId) {
3444 let mut r = lattice_grammar::CommandRegistry::new();
3445 let motion = r.register_motion("motion:vm4-test", "mirror test motion", stub_motion());
3446 let action = r.register_action(
3447 "action:vm4-test",
3448 "mirror test action",
3449 lattice_grammar::ActionSpec {
3450 apply: Arc::new(|_| Ok(lattice_grammar::Effect::None)),
3451 args_schema: vec![],
3452 },
3453 );
3454 (Arc::new(ArcSwap::from_pointee(r)), motion.0, action)
3455 }
3456
3457 fn special(k: SpecialKey) -> ChordPattern {
3458 ChordPattern::Literal(KeyChord::special(k))
3459 }
3460
3461 fn slot(
3462 h: &KeymapHandle,
3463 layer: KeymapLayer,
3464 mode: BindingMode,
3465 path: &[ChordPattern],
3466 ) -> Option<Arc<BoundCommand>> {
3467 h.layer_bindings(layer, mode)
3468 .into_iter()
3469 .find(|(p, _)| p.as_slice() == path)
3470 .map(|(_, b)| b)
3471 }
3472
3473 fn command_at(
3474 h: &KeymapHandle,
3475 layer: KeymapLayer,
3476 mode: BindingMode,
3477 path: &[ChordPattern],
3478 ) -> Option<CommandId> {
3479 slot(h, layer, mode, path).map(|b| b.command.command)
3480 }
3481
3482 fn bind_normal(h: &KeymapHandle, path: &[ChordPattern], id: CommandId) {
3483 h.bind(
3484 KeymapLayer::Builtin,
3485 BindingMode::Normal,
3486 path,
3487 CommandInvocation::of(id),
3488 src("t"),
3489 );
3490 }
3491
3492 /// `]]` is printable. It extends the selection in Visual, but in Select
3493 /// it must stay unbound so `]` overtypes.
3494 #[test]
3495 fn a_printable_motion_is_live_in_visual_but_not_select() {
3496 let (commands, motion, _) = motion_and_action();
3497 let h = KeymapHandle::new();
3498 h.set_command_registry(commands);
3499 let path = [lit(']'), lit(']')];
3500 bind_normal(&h, &path, motion);
3501
3502 assert_eq!(
3503 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3504 Some(motion),
3505 "a motion is live in Visual without anyone binding it there"
3506 );
3507 assert_eq!(
3508 command_at(&h, KeymapLayer::Builtin, BindingMode::Select, &path),
3509 None,
3510 "a printable motion in Select would take the key meant to overtype"
3511 );
3512 }
3513
3514 #[test]
3515 fn a_non_printable_motion_is_live_in_visual_and_select() {
3516 let (commands, motion, _) = motion_and_action();
3517 let h = KeymapHandle::new();
3518 h.set_command_registry(commands);
3519 for path in [
3520 vec![ChordPattern::Literal(ctrl('d'))],
3521 vec![special(SpecialKey::PageDown)],
3522 ] {
3523 bind_normal(&h, &path, motion);
3524 for mode in [BindingMode::Visual, BindingMode::Select] {
3525 assert_eq!(
3526 command_at(&h, KeymapLayer::Builtin, mode, &path),
3527 Some(motion),
3528 "{path:?} can't be typed, so it extends in {mode:?}"
3529 );
3530 }
3531 }
3532 }
3533
3534 #[test]
3535 fn an_action_bound_in_normal_is_not_mirrored() {
3536 let (commands, _, action) = motion_and_action();
3537 let h = KeymapHandle::new();
3538 h.set_command_registry(commands);
3539 let path = [special(SpecialKey::PageDown)];
3540 bind_normal(&h, &path, action);
3541
3542 for mode in [BindingMode::Visual, BindingMode::Select] {
3543 assert_eq!(command_at(&h, KeymapLayer::Builtin, mode, &path), None);
3544 }
3545 }
3546
3547 /// The hole VM.4 exists to close. `push_layer` REPLACES a mode layer's
3548 /// tries wholesale, so under the host-pass design a re-pushed mode layer
3549 /// lost its Visual motions until something re-ran the pass.
3550 #[test]
3551 fn a_re_pushed_mode_layer_keeps_its_visual_motion() {
3552 let (commands, motion, _) = motion_and_action();
3553 let h = KeymapHandle::new();
3554 h.set_command_registry(commands);
3555 let mode_id = ModeId::new("vm4-repush");
3556 let layer = KeymapLayer::MinorMode(mode_id);
3557 let path = [lit(']'), lit(']')];
3558
3559 let tries = || {
3560 let mut normal = KeymapTrie::new();
3561 normal.insert(
3562 &path,
3563 Arc::new(BoundCommand::from_invocation(
3564 CommandInvocation::of(motion),
3565 src("t"),
3566 layer,
3567 )),
3568 );
3569 HashMap::from([(BindingMode::Normal, normal)])
3570 };
3571 h.push_layer(PushLayerKind::MinorMode(mode_id), "vm4", tries());
3572 h.push_layer(PushLayerKind::MinorMode(mode_id), "vm4", tries());
3573
3574 assert_eq!(
3575 command_at(&h, layer, BindingMode::Visual, &path),
3576 Some(motion),
3577 "a re-push must not strip the mirror"
3578 );
3579 }
3580
3581 /// A caller-built trie that names Visual explicitly, sharing one `Arc`
3582 /// with Normal (the natural way to build one), keeps that Visual row when
3583 /// Normal is unbound. Select, which it didn't name, was the mirror and
3584 /// follows Normal out.
3585 #[test]
3586 fn a_pushed_layer_that_names_visual_keeps_it_after_normal_goes() {
3587 let (commands, motion, _) = motion_and_action();
3588 let h = KeymapHandle::new();
3589 h.set_command_registry(commands);
3590 let mode_id = ModeId::new("vm4-explicit-push");
3591 let layer = KeymapLayer::MinorMode(mode_id);
3592 let path = [special(SpecialKey::PageDown)];
3593 let shared = Arc::new(BoundCommand::from_invocation(
3594 CommandInvocation::of(motion),
3595 src("t"),
3596 layer,
3597 ));
3598 let mut normal = KeymapTrie::new();
3599 normal.insert(&path, Arc::clone(&shared));
3600 let mut visual = KeymapTrie::new();
3601 visual.insert(&path, shared);
3602 h.push_layer(
3603 PushLayerKind::MinorMode(mode_id),
3604 "vm4",
3605 HashMap::from([(BindingMode::Normal, normal), (BindingMode::Visual, visual)]),
3606 );
3607 assert_eq!(
3608 command_at(&h, layer, BindingMode::Select, &path),
3609 Some(motion),
3610 "test premise: Select got the mirror"
3611 );
3612
3613 h.unbind(layer, BindingMode::Normal, &path);
3614 assert_eq!(
3615 command_at(&h, layer, BindingMode::Visual, &path),
3616 Some(motion),
3617 "Visual was declared by the caller, so it isn't the mirror's to remove"
3618 );
3619 assert_eq!(command_at(&h, layer, BindingMode::Select, &path), None);
3620 }
3621
3622 #[test]
3623 fn setting_the_registry_after_binding_mirrors_what_already_exists() {
3624 let (commands, motion, _) = motion_and_action();
3625 let h = KeymapHandle::new();
3626 let path = [lit(']'), lit(']')];
3627 bind_normal(&h, &path, motion);
3628 assert_eq!(
3629 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3630 None,
3631 "test premise: nothing to ask 'is this a motion?', so nothing mirrored"
3632 );
3633
3634 h.set_command_registry(commands);
3635 assert_eq!(
3636 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3637 Some(motion),
3638 "the order boot sets the registry in must not matter"
3639 );
3640 assert!(
3641 matches!(
3642 h.lookup(BindingMode::Visual, &[pressed(']'), pressed(']')]),
3643 LookupResult::Bound { .. }
3644 ),
3645 "the rescan must reach the merged trie a keystroke reads"
3646 );
3647 }
3648
3649 #[test]
3650 fn a_deliberate_visual_binding_survives_whichever_order_it_lands_in() {
3651 let (commands, motion, action) = motion_and_action();
3652 let path = [lit(']'), lit(']')];
3653 let bind_visual = |h: &KeymapHandle| {
3654 h.bind(
3655 KeymapLayer::Builtin,
3656 BindingMode::Visual,
3657 &path,
3658 CommandInvocation::of(action),
3659 src("t"),
3660 );
3661 };
3662
3663 // Visual first, then the Normal motion.
3664 let h = KeymapHandle::new();
3665 h.set_command_registry(commands.clone());
3666 bind_visual(&h);
3667 bind_normal(&h, &path, motion);
3668 assert_eq!(
3669 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3670 Some(action)
3671 );
3672
3673 // The Normal motion first, then Visual.
3674 let h = KeymapHandle::new();
3675 h.set_command_registry(commands);
3676 bind_normal(&h, &path, motion);
3677 bind_visual(&h);
3678 assert_eq!(
3679 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3680 Some(action)
3681 );
3682 }
3683
3684 /// Bind-if-absent alone gets this wrong: the slot is occupied, so the old
3685 /// mirror would stay, pointing at a motion the Normal row no longer binds.
3686 #[test]
3687 fn rebinding_a_normal_path_moves_its_mirror_with_it() {
3688 let mut r = lattice_grammar::CommandRegistry::new();
3689 let first = r
3690 .register_motion("motion:vm4-first", "first", stub_motion())
3691 .0;
3692 let second = r
3693 .register_motion("motion:vm4-second", "second", stub_motion())
3694 .0;
3695 let h = KeymapHandle::new();
3696 h.set_command_registry(Arc::new(ArcSwap::from_pointee(r)));
3697 let path = [special(SpecialKey::PageDown)];
3698 bind_normal(&h, &path, first);
3699 bind_normal(&h, &path, second);
3700
3701 for mode in [BindingMode::Visual, BindingMode::Select] {
3702 assert_eq!(
3703 command_at(&h, KeymapLayer::Builtin, mode, &path),
3704 Some(second),
3705 "the {mode:?} mirror must follow the Normal row it mirrors"
3706 );
3707 }
3708 }
3709
3710 #[test]
3711 fn rebinding_a_normal_motion_to_an_action_removes_the_mirror() {
3712 let (commands, motion, action) = motion_and_action();
3713 let h = KeymapHandle::new();
3714 h.set_command_registry(commands);
3715 let path = [special(SpecialKey::PageDown)];
3716 bind_normal(&h, &path, motion);
3717 bind_normal(&h, &path, action);
3718
3719 for mode in [BindingMode::Visual, BindingMode::Select] {
3720 assert_eq!(
3721 command_at(&h, KeymapLayer::Builtin, mode, &path),
3722 None,
3723 "an action is not live in {mode:?} just because a motion used to be"
3724 );
3725 }
3726 }
3727
3728 #[test]
3729 fn unbinding_normal_removes_the_mirror_but_not_a_deliberate_binding() {
3730 let (commands, motion, action) = motion_and_action();
3731 let h = KeymapHandle::new();
3732 h.set_command_registry(commands);
3733 let mirrored = [lit(']'), lit(']')];
3734 let deliberate = [lit('['), lit('[')];
3735 bind_normal(&h, &mirrored, motion);
3736 h.bind(
3737 KeymapLayer::Builtin,
3738 BindingMode::Visual,
3739 &deliberate,
3740 CommandInvocation::of(action),
3741 src("t"),
3742 );
3743 bind_normal(&h, &deliberate, motion);
3744
3745 h.unbind(KeymapLayer::Builtin, BindingMode::Normal, &mirrored);
3746 h.unbind(KeymapLayer::Builtin, BindingMode::Normal, &deliberate);
3747
3748 assert_eq!(
3749 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &mirrored),
3750 None
3751 );
3752 assert_eq!(
3753 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &deliberate),
3754 Some(action),
3755 "unbinding Normal must not take a binding someone wrote in Visual"
3756 );
3757 }
3758
3759 /// `bind_modes` shares one `Arc` across the modes it names. A Visual copy
3760 /// named alongside Normal is an explicit declaration, and must survive an
3761 /// unbind of Normal that removes a mirror.
3762 #[test]
3763 fn bind_modes_normal_and_visual_declares_visual_explicitly() {
3764 let (commands, motion, _) = motion_and_action();
3765 let h = KeymapHandle::new();
3766 h.set_command_registry(commands);
3767 let path = [special(SpecialKey::PageDown)];
3768 h.bind_modes(
3769 KeymapLayer::Builtin,
3770 &[BindingMode::Normal, BindingMode::Visual],
3771 &path,
3772 CommandInvocation::of(motion),
3773 src("t"),
3774 );
3775 assert_eq!(
3776 command_at(&h, KeymapLayer::Builtin, BindingMode::Select, &path),
3777 Some(motion),
3778 "test premise: Select, which wasn't named, got the mirror"
3779 );
3780
3781 h.unbind(KeymapLayer::Builtin, BindingMode::Normal, &path);
3782 assert_eq!(
3783 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3784 Some(motion),
3785 "Visual was named explicitly, so it isn't the mirror's to remove"
3786 );
3787 assert_eq!(
3788 command_at(&h, KeymapLayer::Builtin, BindingMode::Select, &path),
3789 None,
3790 "Select was the mirror, and followed Normal out"
3791 );
3792 }
3793
3794 #[test]
3795 fn no_registry_means_no_mirror_and_no_panic() {
3796 let (_, motion, _) = motion_and_action();
3797 let h = KeymapHandle::new();
3798 let path = [special(SpecialKey::PageDown)];
3799 bind_normal(&h, &path, motion);
3800 h.unbind(KeymapLayer::Builtin, BindingMode::Normal, &path);
3801 for mode in [BindingMode::Visual, BindingMode::Select] {
3802 assert_eq!(command_at(&h, KeymapLayer::Builtin, mode, &path), None);
3803 }
3804 }
3805
3806 /// `f{char}`: the exact-pattern slot is `[f, {char}]`, which `lookup`
3807 /// can't address. It's mirrored into Visual exactly and resolves from a
3808 /// real keystroke there. In Select `f` stays unbound, so it overtypes.
3809 #[test]
3810 fn a_wildcard_motion_path_is_mirrored_exactly() {
3811 let (commands, motion, _) = motion_and_action();
3812 let h = KeymapHandle::new();
3813 h.set_command_registry(commands);
3814 let path = [lit('f'), ChordPattern::CharLiteral];
3815 bind_normal(&h, &path, motion);
3816
3817 assert_eq!(
3818 command_at(&h, KeymapLayer::Builtin, BindingMode::Visual, &path),
3819 Some(motion)
3820 );
3821 assert!(matches!(
3822 h.lookup(BindingMode::Visual, &[pressed('f'), pressed('x')]),
3823 LookupResult::Bound { .. }
3824 ));
3825 assert_eq!(
3826 command_at(&h, KeymapLayer::Builtin, BindingMode::Select, &path),
3827 None
3828 );
3829 assert!(matches!(
3830 h.lookup(BindingMode::Select, &[pressed('f')]),
3831 LookupResult::Unbound
3832 ));
3833 }
3834
3835 /// The single definition of "would overtype", pinned case by case. Select's
3836 /// fallback and the mirror both call it, so this is what they agree on.
3837 #[test]
3838 fn overtypes_in_select_matches_the_dispatcher_rule() {
3839 use lattice_protocol::chord::{KeyKind, KeyMods};
3840 let with = |key: KeyKind, mods: KeyMods| KeyChord::new(key, mods);
3841
3842 for c in ['a', 'A', ' ', 'é', '%', '['] {
3843 assert!(overtypes_in_select(&pressed(c)), "{c:?} is typed text");
3844 }
3845 assert!(
3846 overtypes_in_select(&with(KeyKind::Char('a'), KeyMods::SHIFT)),
3847 "Shift doesn't make a chord"
3848 );
3849 // Vim: "Printable characters, <NL> and <CR> cause the selection to be
3850 // deleted". <NL> arrives as Ctrl-J.
3851 let enter = KeyKind::Special(SpecialKey::Enter);
3852 assert!(overtypes_in_select(&with(enter, KeyMods::NONE)), "<CR>");
3853 assert!(overtypes_in_select(&with(enter, KeyMods::SHIFT)), "<S-CR>");
3854 assert!(overtypes_in_select(&ctrl('j')), "<NL> is <C-j>");
3855
3856 for (label, chord) in [
3857 ("<C-a>", ctrl('a')),
3858 ("<M-a>", with(KeyKind::Char('a'), KeyMods::ALT)),
3859 ("<D-a>", with(KeyKind::Char('a'), KeyMods::SUPER)),
3860 ("<C-CR>", with(enter, KeyMods::CTRL)),
3861 ("<M-CR>", with(enter, KeyMods::ALT)),
3862 (
3863 "<M-C-j>",
3864 with(KeyKind::Char('j'), KeyMods::CTRL | KeyMods::ALT),
3865 ),
3866 ("<Left>", KeyChord::special(SpecialKey::Left)),
3867 ("<PageDown>", KeyChord::special(SpecialKey::PageDown)),
3868 ("<Esc>", KeyChord::special(SpecialKey::Esc)),
3869 ("<Tab>", KeyChord::special(SpecialKey::Tab)),
3870 ] {
3871 assert!(
3872 !overtypes_in_select(&chord),
3873 "{label} is a chord, not typing"
3874 );
3875 }
3876 }
3877}