Skip to main content

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}