Skip to main content

keymap_seq/
map.rs

1//! The sequence table: a prefix-free trie of [`KeyInput`] sequences.
2
3use std::collections::HashMap;
4
5use keymap_core::KeyInput;
6
7/// A table mapping *sequences* of normalized [`KeyInput`]s to a caller-defined
8/// action `A`.
9///
10/// This is the sequence-level analogue of `keymap_core::Keymap`: state-free, with
11/// the action type `A` brought by the caller. It is **prefix-free** by
12/// construction — see [`SequenceKeymap::bind`] and the crate-level docs — so
13/// [`lookup`](SequenceKeymap::lookup) never needs a timeout to disambiguate.
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct SequenceKeymap<A> {
17    root: Node<A>,
18}
19
20/// One trie node. The prefix-free invariant is "`action.is_some()` implies
21/// `children.is_empty()`": a node that resolves to an action is always a leaf,
22/// and a node with children never resolves on its own.
23#[derive(Debug, Clone)]
24struct Node<A> {
25    action: Option<A>,
26    children: HashMap<KeyInput, Node<A>>,
27}
28
29impl<A> Node<A> {
30    fn new() -> Self {
31        Node {
32            action: None,
33            children: HashMap::new(),
34        }
35    }
36
37    fn is_terminal(&self) -> bool {
38        self.action.is_some()
39    }
40}
41
42/// Appends keys to `seq` while descending from `node` to the first terminal it
43/// can reach. `node` is assumed non-terminal; the prefix-free invariant
44/// guarantees a non-terminal node has at least one child, so a leaf is always
45/// reached. When the node fans out, an arbitrary branch is taken — the caller
46/// only needs *one* colliding example.
47fn extend_to_terminal<A>(node: &Node<A>, seq: &mut Vec<KeyInput>) {
48    let mut cur = node;
49    while !cur.is_terminal() {
50        let Some((key, child)) = cur.children.iter().next() else {
51            break;
52        };
53        seq.push(*key);
54        cur = child;
55    }
56}
57
58/// Depth-first collects every leaf `(path, action)` under `node`, threading a
59/// shared `path` stack (pushed on descent, popped on the way back up) so each
60/// recorded path is the exact chord sequence from the root to that leaf. The
61/// prefix-free invariant means a node with an action has no children, so no
62/// internal node is ever recorded.
63fn collect_bindings<'a, A>(
64    node: &'a Node<A>,
65    path: &mut Vec<KeyInput>,
66    out: &mut Vec<(Vec<KeyInput>, &'a A)>,
67) {
68    if let Some(action) = &node.action {
69        out.push((path.clone(), action));
70    }
71    for (key, child) in &node.children {
72        path.push(*key);
73        collect_bindings(child, path, out);
74        path.pop();
75    }
76}
77
78impl<A> Default for SequenceKeymap<A> {
79    fn default() -> Self {
80        SequenceKeymap { root: Node::new() }
81    }
82}
83
84impl<A> SequenceKeymap<A> {
85    /// Creates an empty sequence map.
86    #[must_use]
87    pub fn new() -> Self {
88        SequenceKeymap::default()
89    }
90
91    /// Returns `true` if no sequences are bound.
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.root.children.is_empty()
95    }
96
97    /// Binds a key `sequence` to `action`.
98    ///
99    /// Re-binding the *exact* same sequence overwrites and returns the previous
100    /// action, mirroring `keymap_core::Keymap::bind`.
101    ///
102    /// # Errors
103    ///
104    /// - [`SeqBindError::Empty`] if `sequence` yields no keys.
105    /// - [`SeqBindError::PrefixShadow`] if the new sequence would be a proper
106    ///   prefix of an existing binding, or an existing binding would be a proper
107    ///   prefix of it. Either case is unresolvable without a timeout (see the
108    ///   crate docs), so it is rejected rather than silently shadowed; the map is
109    ///   left unchanged.
110    pub fn bind(
111        &mut self,
112        sequence: impl IntoIterator<Item = KeyInput>,
113        action: A,
114    ) -> Result<Option<A>, SeqBindError> {
115        let keys: Vec<KeyInput> = sequence.into_iter().collect();
116        if keys.is_empty() {
117            return Err(SeqBindError::Empty);
118        }
119
120        // Descend, creating nodes as needed. Passing *through* an existing
121        // terminal means a shorter binding (`keys[..depth]`) is already a prefix
122        // of `keys`.
123        let mut node = &mut self.root;
124        for (depth, key) in keys.iter().enumerate() {
125            if node.is_terminal() {
126                return Err(SeqBindError::PrefixShadow {
127                    sequence: keys.clone(),
128                    conflict: keys[..depth].to_vec(),
129                });
130            }
131            node = node.children.entry(*key).or_insert_with(Node::new);
132        }
133
134        // `node` is the terminal for `keys`. If it already has children, `keys`
135        // is a proper prefix of one or more existing (longer) bindings; surface
136        // one of them by descending to any terminal beneath this node.
137        if !node.children.is_empty() {
138            let mut conflict = keys.clone();
139            extend_to_terminal(node, &mut conflict);
140            return Err(SeqBindError::PrefixShadow {
141                sequence: keys,
142                conflict,
143            });
144        }
145
146        Ok(node.action.replace(action))
147    }
148
149    /// Resolves the keys pressed so far against the table.
150    ///
151    /// `keys` is the caller's pending buffer. The result is one of:
152    /// - [`Match::Exact`] — `keys` is a complete binding; the caller should fire
153    ///   it and clear the buffer.
154    /// - [`Match::Prefix`] — `keys` is a proper prefix of one or more bindings;
155    ///   the caller should keep buffering (and, for timed bindings, run its
156    ///   timer).
157    /// - [`Match::NoMatch`] — `keys` is not on any binding path; the caller
158    ///   should clear the buffer and handle the keys itself (e.g. pass through).
159    pub fn lookup(&self, keys: &[KeyInput]) -> Match<'_, A> {
160        let mut node = &self.root;
161        for key in keys {
162            match node.children.get(key) {
163                Some(child) => node = child,
164                None => return Match::NoMatch,
165            }
166        }
167        match &node.action {
168            Some(action) => Match::Exact(action),
169            None if node.children.is_empty() => Match::NoMatch,
170            None => Match::Prefix,
171        }
172    }
173
174    /// Enumerates every bound sequence as a `(path, action)` pair — the
175    /// sequence-level dual of `keymap_core::Keymap::iter`.
176    ///
177    /// Each yielded `Vec<KeyInput>` is a complete bound sequence (a leaf path);
178    /// by the prefix-free invariant it is never a partial prefix. Unlike
179    /// `Keymap::iter`, which can borrow its stored key, a trie path is
180    /// reconstructed during traversal, so each item owns its `Vec` and borrows
181    /// only the action.
182    ///
183    /// Order is unspecified (the trie's children live in a `HashMap`); collect
184    /// and sort caller-side for stable output. This is the data source for
185    /// listing, search, and serialization on top of `keymap-seq`.
186    pub fn bindings(&self) -> impl Iterator<Item = (Vec<KeyInput>, &A)> {
187        let mut out = Vec::new();
188        let mut path = Vec::new();
189        collect_bindings(&self.root, &mut path, &mut out);
190        out.into_iter()
191    }
192
193    /// Enumerates the keys that may immediately follow `prefix`, for rendering a
194    /// which-key-style menu of what can be pressed next.
195    ///
196    /// Each item is a [`KeyInput`] that extends `prefix` by one, paired with what
197    /// pressing it does: complete a binding ([`Continuation::Action`], carrying
198    /// the action) or open a deeper prefix ([`Continuation::Prefix`]).
199    ///
200    /// The iterator is empty when `prefix` does not name an internal node —
201    /// whether it is off the tree, already completes a binding (a leaf has no
202    /// children), or the map is empty. `continuations` deliberately does not
203    /// re-encode that distinction; call [`lookup`](Self::lookup) to tell those
204    /// apart.
205    ///
206    /// Order is unspecified (the children live in a `HashMap`); collect and sort
207    /// caller-side for a stable menu.
208    ///
209    /// ```
210    /// use keymap_core::{Key, KeyInput, Modifiers};
211    /// use keymap_seq::{Continuation, SequenceKeymap};
212    ///
213    /// #[derive(Debug, PartialEq)]
214    /// enum Action { Save, Quit }
215    ///
216    /// let k = |c| KeyInput::new(Key::Char(c), Modifiers::NONE);
217    /// let mut map = SequenceKeymap::new();
218    /// map.bind([k('a'), k('b')], Action::Save).unwrap();
219    /// map.bind([k('c')], Action::Quit).unwrap();
220    ///
221    /// // From the root: `a` opens a deeper prefix, `c` completes a binding.
222    /// let mut next: Vec<_> = map.continuations(&[]).collect();
223    /// next.sort_by_key(|(key, _)| key.to_string());
224    /// assert_eq!(
225    ///     next,
226    ///     vec![
227    ///         (k('a'), Continuation::Prefix),
228    ///         (k('c'), Continuation::Action(&Action::Quit)),
229    ///     ],
230    /// );
231    ///
232    /// // A completed binding has no continuations.
233    /// assert_eq!(map.continuations(&[k('c')]).count(), 0);
234    /// ```
235    pub fn continuations(
236        &self,
237        prefix: &[KeyInput],
238    ) -> impl Iterator<Item = (KeyInput, Continuation<'_, A>)> {
239        let mut node = &self.root;
240        for key in prefix {
241            match node.children.get(key) {
242                Some(child) => node = child,
243                None => return None.into_iter().flatten(),
244            }
245        }
246        // A child is, by the prefix-free invariant, terminal (an action, no
247        // children) xor an internal prefix — so the classification is total.
248        Some(node.children.iter().map(|(key, child)| {
249            let continuation = child
250                .action
251                .as_ref()
252                .map_or(Continuation::Prefix, Continuation::Action);
253            (*key, continuation)
254        }))
255        .into_iter()
256        .flatten()
257    }
258}
259
260/// The outcome of resolving a pending key buffer with
261/// [`SequenceKeymap::lookup`].
262///
263/// `Resolution` is deliberately *not* the name here: that word is reserved by
264/// `keymap-core` for raw-byte passthrough. This is purely the table-view answer
265/// — whether the keys are an exact hit, a prefix, or a miss — leaving "should I
266/// wait?" to the caller.
267///
268/// The three variants are exhaustive on purpose: a pure trie lookup has no other
269/// outcome, and timeout (`jj`-style) handling lives in the caller, not in this
270/// result. So this enum is *not* `#[non_exhaustive]` — callers can and should
271/// match all three arms with the compiler checking coverage.
272#[derive(Debug, PartialEq, Eq)]
273#[must_use]
274pub enum Match<'a, A> {
275    /// The keys are a complete binding, resolving to this action.
276    Exact(&'a A),
277    /// The keys are a proper prefix of at least one longer binding.
278    Prefix,
279    /// The keys are not on any binding path.
280    NoMatch,
281}
282
283/// What pressing one more key after a prefix does, as reported by
284/// [`SequenceKeymap::continuations`].
285///
286/// The two variants are exhaustive on purpose, mirroring [`Match`]: by the
287/// prefix-free invariant a node is terminal *xor* internal, so a child is either
288/// a completed binding or a deeper prefix — there is no third kind of node. This
289/// enum is therefore *not* `#[non_exhaustive]`; a third variant could only arise
290/// by abandoning the trie's defining invariant.
291#[derive(Debug, PartialEq, Eq)]
292#[must_use]
293pub enum Continuation<'a, A> {
294    /// Pressing this key completes a binding, resolving to this action.
295    Action(&'a A),
296    /// Pressing this key opens a deeper prefix; more keys must follow.
297    Prefix,
298}
299
300/// Why a [`SequenceKeymap::bind`] call was rejected.
301#[derive(Debug, Clone, PartialEq, Eq)]
302#[non_exhaustive]
303pub enum SeqBindError {
304    /// The sequence contained no keys.
305    Empty,
306    /// The sequence collides with an existing binding on a shared prefix: one is
307    /// a proper prefix of the other, which cannot be resolved without a timeout.
308    PrefixShadow {
309        /// The sequence whose binding was rejected.
310        sequence: Vec<KeyInput>,
311        /// An existing binding it collides with — one of `sequence`/`conflict`
312        /// is a proper prefix of the other. When several existing bindings share
313        /// the prefix, this names one of them.
314        conflict: Vec<KeyInput>,
315    },
316}
317
318impl core::fmt::Display for SeqBindError {
319    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
320        match self {
321            SeqBindError::Empty => f.write_str("empty key sequence"),
322            SeqBindError::PrefixShadow { .. } => {
323                f.write_str("key sequence shares a prefix with an existing binding")
324            }
325        }
326    }
327}
328
329impl std::error::Error for SeqBindError {}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use keymap_core::{Key, Modifiers};
335
336    #[derive(Debug, Clone, PartialEq)]
337    enum Action {
338        Save,
339        Quit,
340        Top,
341        Solo,
342    }
343
344    fn ctrl(c: char) -> KeyInput {
345        KeyInput::new(Key::Char(c), Modifiers::CTRL)
346    }
347
348    fn plain(c: char) -> KeyInput {
349        KeyInput::new(Key::Char(c), Modifiers::NONE)
350    }
351
352    /// `ctrl+x ctrl+s` -> Save, `ctrl+x ctrl+c` -> Quit, `g g` -> Top,
353    /// `q` (length-1 sequence) -> Solo.
354    fn sample() -> SequenceKeymap<Action> {
355        let mut map = SequenceKeymap::new();
356        map.bind([ctrl('x'), ctrl('s')], Action::Save).unwrap();
357        map.bind([ctrl('x'), ctrl('c')], Action::Quit).unwrap();
358        map.bind([plain('g'), plain('g')], Action::Top).unwrap();
359        map.bind([plain('q')], Action::Solo).unwrap();
360        map
361    }
362
363    #[test]
364    fn single_key_sequence_resolves() {
365        let map = sample();
366        assert_eq!(map.lookup(&[plain('q')]), Match::Exact(&Action::Solo));
367    }
368
369    #[test]
370    fn prefix_then_leaf() {
371        let map = sample();
372        // One key in: still a prefix.
373        assert_eq!(map.lookup(&[ctrl('x')]), Match::Prefix);
374        // Full sequence: exact.
375        assert_eq!(
376            map.lookup(&[ctrl('x'), ctrl('s')]),
377            Match::Exact(&Action::Save)
378        );
379    }
380
381    #[test]
382    fn branching_prefix_reaches_both_leaves() {
383        let map = sample();
384        assert_eq!(
385            map.lookup(&[ctrl('x'), ctrl('c')]),
386            Match::Exact(&Action::Quit)
387        );
388        assert_eq!(
389            map.lookup(&[ctrl('x'), ctrl('s')]),
390            Match::Exact(&Action::Save)
391        );
392    }
393
394    #[test]
395    fn miss_on_first_key() {
396        let map = sample();
397        assert_eq!(map.lookup(&[plain('z')]), Match::NoMatch);
398    }
399
400    #[test]
401    fn miss_off_a_prefix_path() {
402        let map = sample();
403        // `ctrl+x` is a valid prefix, but `ctrl+x z` leaves the tree.
404        assert_eq!(map.lookup(&[ctrl('x'), plain('z')]), Match::NoMatch);
405    }
406
407    #[test]
408    fn empty_buffer_on_nonempty_map_is_prefix() {
409        let map = sample();
410        // Nothing typed yet, but bindings exist: still possible.
411        assert_eq!(map.lookup(&[]), Match::Prefix);
412    }
413
414    #[test]
415    fn empty_buffer_on_empty_map_is_no_match() {
416        let map: SequenceKeymap<Action> = SequenceKeymap::new();
417        assert!(map.is_empty());
418        assert_eq!(map.lookup(&[]), Match::NoMatch);
419        assert_eq!(map.lookup(&[ctrl('x')]), Match::NoMatch);
420    }
421
422    #[test]
423    fn rebinding_exact_sequence_overwrites_and_returns_previous() {
424        let mut map = SequenceKeymap::new();
425        assert_eq!(map.bind([ctrl('x'), ctrl('s')], Action::Save), Ok(None));
426        assert_eq!(
427            map.bind([ctrl('x'), ctrl('s')], Action::Quit),
428            Ok(Some(Action::Save))
429        );
430        assert_eq!(
431            map.lookup(&[ctrl('x'), ctrl('s')]),
432            Match::Exact(&Action::Quit)
433        );
434    }
435
436    #[test]
437    fn empty_sequence_is_rejected() {
438        let mut map: SequenceKeymap<Action> = SequenceKeymap::new();
439        assert_eq!(map.bind([], Action::Save), Err(SeqBindError::Empty));
440    }
441
442    #[test]
443    fn existing_short_binding_shadows_new_longer_one() {
444        let mut map = SequenceKeymap::new();
445        map.bind([plain('g')], Action::Solo).unwrap();
446        // `g` is already terminal; `g g` would pass through it.
447        assert_eq!(
448            map.bind([plain('g'), plain('g')], Action::Top),
449            Err(SeqBindError::PrefixShadow {
450                sequence: vec![plain('g'), plain('g')],
451                conflict: vec![plain('g')],
452            })
453        );
454        // The map is unchanged: `g` still resolves, `g g` was never added.
455        assert_eq!(map.lookup(&[plain('g')]), Match::Exact(&Action::Solo));
456    }
457
458    #[test]
459    fn new_short_binding_shadows_existing_longer_one() {
460        let mut map = SequenceKeymap::new();
461        map.bind([plain('g'), plain('g')], Action::Top).unwrap();
462        // `g` would be a prefix of the existing `g g`.
463        assert_eq!(
464            map.bind([plain('g')], Action::Solo),
465            Err(SeqBindError::PrefixShadow {
466                sequence: vec![plain('g')],
467                conflict: vec![plain('g'), plain('g')],
468            })
469        );
470        // Unchanged: `g g` still resolves, `g` is only a prefix.
471        assert_eq!(map.lookup(&[plain('g')]), Match::Prefix);
472        assert_eq!(
473            map.lookup(&[plain('g'), plain('g')]),
474            Match::Exact(&Action::Top)
475        );
476    }
477
478    #[test]
479    fn deep_sequence_is_prefix_at_every_step_then_exact() {
480        // A length-3 sequence: every proper prefix resolves to Prefix, the full
481        // sequence to Exact, and a wrong final key to NoMatch.
482        let mut map = SequenceKeymap::new();
483        map.bind([plain('a'), plain('b'), plain('c')], Action::Top)
484            .unwrap();
485        assert_eq!(map.lookup(&[plain('a')]), Match::Prefix);
486        assert_eq!(map.lookup(&[plain('a'), plain('b')]), Match::Prefix);
487        assert_eq!(
488            map.lookup(&[plain('a'), plain('b'), plain('c')]),
489            Match::Exact(&Action::Top)
490        );
491        assert_eq!(
492            map.lookup(&[plain('a'), plain('b'), plain('z')]),
493            Match::NoMatch
494        );
495    }
496
497    #[test]
498    fn independent_single_and_sequence_coexist() {
499        // `q` (single) and `ctrl+x ctrl+s` (sequence) share no prefix, so both
500        // bind cleanly — only a *shared* prefix is forbidden.
501        let map = sample();
502        assert_eq!(map.lookup(&[plain('q')]), Match::Exact(&Action::Solo));
503        assert_eq!(map.lookup(&[ctrl('x')]), Match::Prefix);
504    }
505
506    /// Collects `continuations` into a vec sorted by the canonical key string, so
507    /// the `HashMap`'s unspecified order does not make assertions flaky.
508    fn sorted_continuations<'a>(
509        map: &'a SequenceKeymap<Action>,
510        prefix: &[KeyInput],
511    ) -> Vec<(KeyInput, Continuation<'a, Action>)> {
512        let mut out: Vec<_> = map.continuations(prefix).collect();
513        out.sort_by_key(|(key, _)| key.to_string());
514        out
515    }
516
517    #[test]
518    fn continuations_at_root_list_the_leader_keys() {
519        // sample(): `ctrl+x …` (prefix), `g g` (prefix via `g`), `q` (action).
520        assert_eq!(
521            sorted_continuations(&sample(), &[]),
522            vec![
523                (ctrl('x'), Continuation::Prefix),
524                (plain('g'), Continuation::Prefix),
525                (plain('q'), Continuation::Action(&Action::Solo)),
526            ]
527        );
528    }
529
530    #[test]
531    fn continuations_after_a_prefix_classify_each_child() {
532        // After `ctrl+x`, both `ctrl+s` (Save) and `ctrl+c` (Quit) complete.
533        assert_eq!(
534            sorted_continuations(&sample(), &[ctrl('x')]),
535            vec![
536                (ctrl('c'), Continuation::Action(&Action::Quit)),
537                (ctrl('s'), Continuation::Action(&Action::Save)),
538            ]
539        );
540    }
541
542    #[test]
543    fn continuations_mix_action_and_prefix() {
544        // `a b` (action) and `a c d` (deeper) share the prefix `a`: after `a`,
545        // `b` completes while `c` opens a further prefix.
546        let mut map = SequenceKeymap::new();
547        map.bind([plain('a'), plain('b')], Action::Save).unwrap();
548        map.bind([plain('a'), plain('c'), plain('d')], Action::Quit)
549            .unwrap();
550        assert_eq!(
551            sorted_continuations(&map, &[plain('a')]),
552            vec![
553                (plain('b'), Continuation::Action(&Action::Save)),
554                (plain('c'), Continuation::Prefix),
555            ]
556        );
557    }
558
559    #[test]
560    fn a_completed_binding_has_no_continuations() {
561        // `q` is a leaf; nothing follows it.
562        assert_eq!(sample().continuations(&[plain('q')]).count(), 0);
563        assert_eq!(sample().continuations(&[ctrl('x'), ctrl('s')]).count(), 0);
564    }
565
566    #[test]
567    fn off_tree_prefix_has_no_continuations() {
568        let map = sample();
569        // A first key bound nowhere, and a valid prefix then a wrong key.
570        assert_eq!(map.continuations(&[plain('z')]).count(), 0);
571        assert_eq!(map.continuations(&[ctrl('x'), plain('z')]).count(), 0);
572    }
573
574    #[test]
575    fn empty_map_has_no_continuations() {
576        let map: SequenceKeymap<Action> = SequenceKeymap::new();
577        assert_eq!(map.continuations(&[]).count(), 0);
578        assert_eq!(map.continuations(&[plain('a')]).count(), 0);
579    }
580
581    #[test]
582    fn continuations_descend_to_deeper_internal_nodes() {
583        // `a b` (action) and `a c d e` (deep) share the prefix `a`; this drives
584        // the descent past depth 1 on a *hit* (the other tests stop at depth 1).
585        let mut map = SequenceKeymap::new();
586        map.bind([plain('a'), plain('b')], Action::Save).unwrap();
587        map.bind(
588            [plain('a'), plain('c'), plain('d'), plain('e')],
589            Action::Quit,
590        )
591        .unwrap();
592        // Depth 2: through `a c` to an internal node whose child `d` is a deeper
593        // prefix (a `Prefix` continuation returned away from the root).
594        assert_eq!(
595            sorted_continuations(&map, &[plain('a'), plain('c')]),
596            vec![(plain('d'), Continuation::Prefix)]
597        );
598        // Depth 3: one key short of the leaf, `e` completes the binding.
599        assert_eq!(
600            sorted_continuations(&map, &[plain('a'), plain('c'), plain('d')]),
601            vec![(plain('e'), Continuation::Action(&Action::Quit))]
602        );
603    }
604
605    /// Collects `bindings` into a vec sorted by the joined canonical path, so the
606    /// `HashMap`'s unspecified order does not make assertions flaky.
607    fn sorted_bindings(map: &SequenceKeymap<Action>) -> Vec<(Vec<KeyInput>, &Action)> {
608        let mut out: Vec<_> = map.bindings().collect();
609        out.sort_by_key(|(path, _)| {
610            path.iter()
611                .map(ToString::to_string)
612                .collect::<Vec<_>>()
613                .join(" ")
614        });
615        out
616    }
617
618    #[test]
619    fn bindings_enumerate_every_leaf_path() {
620        // sample(): two branches under `ctrl+x`, the `g g` leaf, and the `q` leaf.
621        assert_eq!(
622            sorted_bindings(&sample()),
623            vec![
624                (vec![ctrl('x'), ctrl('c')], &Action::Quit),
625                (vec![ctrl('x'), ctrl('s')], &Action::Save),
626                (vec![plain('g'), plain('g')], &Action::Top),
627                (vec![plain('q')], &Action::Solo),
628            ]
629        );
630    }
631
632    #[test]
633    fn bindings_of_empty_map_is_empty() {
634        let map: SequenceKeymap<Action> = SequenceKeymap::new();
635        assert_eq!(map.bindings().count(), 0);
636    }
637
638    #[test]
639    fn bindings_never_yield_internal_prefixes() {
640        // Every yielded path must itself resolve to Exact (a leaf) — never a
641        // partial prefix. This is the prefix-free invariant, viewed through
642        // enumeration.
643        let map = sample();
644        for (path, action) in map.bindings() {
645            assert_eq!(map.lookup(&path), Match::Exact(action));
646        }
647    }
648
649    #[test]
650    fn continuations_nonempty_iff_lookup_is_prefix() {
651        // Cross-method invariant: a buffer has continuations exactly when it is a
652        // proper prefix; an exact hit or a miss has none. Catches a desync between
653        // the two descents without re-deriving the contents (which would be
654        // tautological).
655        let map = sample();
656        let cases: &[&[KeyInput]] = &[
657            &[],
658            &[plain('q')],
659            &[ctrl('x')],
660            &[ctrl('x'), ctrl('s')],
661            &[plain('z')],
662            &[ctrl('x'), plain('z')],
663        ];
664        for keys in cases {
665            let has_next = map.continuations(keys).count() > 0;
666            match map.lookup(keys) {
667                Match::Prefix => assert!(has_next, "Prefix must have continuations: {keys:?}"),
668                Match::Exact(_) | Match::NoMatch => {
669                    assert!(!has_next, "non-Prefix must have no continuations: {keys:?}");
670                }
671            }
672        }
673    }
674}