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}