Skip to main content

keymap_seq/
progress.rs

1//! Caller-owned progress through a [`SequenceKeymap`].
2
3use keymap_core::KeyInput;
4
5use crate::{Match, SequenceKeymap};
6
7/// A caller-owned pending buffer for resolving multi-key sequences, with the
8/// clear/retain bookkeeping folded in.
9///
10/// [`SequenceKeymap::lookup`] answers "exact / prefix / miss" for the keys so
11/// far, but leaves the *caller* to push each key, clear the buffer on a hit or a
12/// miss, and keep it on a prefix — the mechanical, easy-to-get-wrong half of
13/// `keymap-seq`'s "the caller owns the buffer" contract (see the crate docs and
14/// the `leader_sequence` example). `PendingSequence` owns exactly that buffer and
15/// that bookkeeping, and nothing else: like the rest of `keymap-rs` it holds **no
16/// clock**.
17///
18/// The idle window a `jj`-style binding needs — "abandon a half-typed prefix if
19/// the next key is too slow" — stays the caller's policy. The library cannot see
20/// time, so it cannot decide a prefix was abandoned; the caller runs its own
21/// timer and calls [`flush`](Self::flush) when that timer fires. Making the idle
22/// flush an explicit method (rather than a buffer the caller forgets to drain) is
23/// the point: it is the one step the old example could only describe in a comment.
24///
25/// It is generic over no lifetime — the [`SequenceKeymap`] is passed to
26/// [`feed`](Self::feed) per call rather than borrowed for the buffer's lifetime —
27/// so a `PendingSequence` can live as a field beside the map it reads. Holding a
28/// borrow would make that a self-referential struct, which is exactly the layout
29/// a modal-UI caller wants (`struct App { map: SequenceKeymap<A>, pending:
30/// PendingSequence }`).
31///
32/// ```
33/// use keymap_core::{Key, KeyInput, Modifiers};
34/// use keymap_seq::{PendingSequence, SequenceKeymap, Step};
35///
36/// #[derive(Debug, PartialEq)]
37/// enum Action {
38///     Save,
39/// }
40///
41/// let cx = KeyInput::new(Key::Char('x'), Modifiers::CTRL);
42/// let cs = KeyInput::new(Key::Char('s'), Modifiers::CTRL);
43/// let mut map = SequenceKeymap::new();
44/// map.bind([cx, cs], Action::Save).unwrap();
45///
46/// let mut pending = PendingSequence::new();
47/// assert!(matches!(pending.feed(&map, cx), Step::Pending));
48/// assert!(matches!(pending.feed(&map, cs), Step::Fired(&Action::Save)));
49/// // Fired cleared the buffer, so the next key starts a fresh sequence.
50/// assert!(pending.is_empty());
51/// ```
52#[derive(Debug, Clone, Default)]
53pub struct PendingSequence {
54    pending: Vec<KeyInput>,
55}
56
57impl PendingSequence {
58    /// Creates an empty pending buffer.
59    #[must_use]
60    pub fn new() -> Self {
61        PendingSequence::default()
62    }
63
64    /// Pushes `key` onto the buffer, resolves the buffer against `map`, and
65    /// returns what the caller should do — applying the clear/retain bookkeeping
66    /// itself.
67    ///
68    /// This is the [`SequenceKeymap::lookup`] trichotomy turned into an action:
69    /// - [`Match::Exact`] → [`Step::Fired`], and the buffer is **cleared** (the
70    ///   sequence completed; the next `feed` starts fresh).
71    /// - [`Match::Prefix`] → [`Step::Pending`], and the buffer is **kept** (more
72    ///   keys may follow; (re)start your idle timer).
73    /// - [`Match::NoMatch`] → [`Step::PassThrough`] carrying the **whole** buffer
74    ///   (every key buffered so far, in order), and the buffer is **cleared**.
75    ///
76    /// The whole buffer — not just `key` — is returned on a miss because the
77    /// earlier keys were held on the promise of a longer binding that did not
78    /// materialise, so they are now the caller's to handle (replay, pass to the
79    /// PTY, …). A key that would have started its own sequence but arrived
80    /// mid-prefix is passed through with the rest, not re-fed: "is this key a
81    /// fresh start or the tail of an abandoned prefix?" is lookahead policy, which
82    /// stays the caller's (the same reason the library has no clock).
83    pub fn feed<'a, A>(&mut self, map: &'a SequenceKeymap<A>, key: KeyInput) -> Step<'a, A> {
84        self.pending.push(key);
85        match map.lookup(&self.pending) {
86            Match::Exact(action) => {
87                self.pending.clear();
88                Step::Fired(action)
89            }
90            Match::Prefix => Step::Pending,
91            Match::NoMatch => Step::PassThrough(std::mem::take(&mut self.pending)),
92        }
93    }
94
95    /// Drains the pending buffer, returning the keys held in it (empty if none),
96    /// and leaves the buffer empty.
97    ///
98    /// This is the **idle flush**: call it when your timer says a pending prefix
99    /// was abandoned (no further key arrived in time). The returned keys are
100    /// literals the caller now owns — the dangling `j` of a half-typed `jj` that
101    /// must still reach the application as a plain `j`. It takes no time argument
102    /// and consults no window: the decision that the prefix is stale is the
103    /// caller's; `flush` only performs the drain the caller decided on. Flushing
104    /// an empty buffer is a no-op that returns an empty `Vec`.
105    pub fn flush(&mut self) -> Vec<KeyInput> {
106        std::mem::take(&mut self.pending)
107    }
108
109    /// The keys buffered so far (a proper prefix of some binding), in press order.
110    #[must_use]
111    pub fn pending(&self) -> &[KeyInput] {
112        &self.pending
113    }
114
115    /// Returns `true` if no keys are buffered.
116    #[must_use]
117    pub fn is_empty(&self) -> bool {
118        self.pending.is_empty()
119    }
120}
121
122/// What a [`PendingSequence::feed`] resolved to — the caller's next action.
123///
124/// `Step` is the *action-oriented* dual of [`Match`]: where `Match` states a fact
125/// about the table (exact / prefix / miss), `Step` states what the caller does
126/// (fire it / keep waiting / take these keys back). The trichotomy is the same
127/// closed set — `feed` is a thin translation of the [`SequenceKeymap::lookup`]
128/// result into buffer operations, and a pure trie lookup has no fourth outcome —
129/// so, like `Match`, this enum is **not** `#[non_exhaustive]`: match all three
130/// arms and let the compiler check coverage. (`flush`'s drain is a `Vec`, not a
131/// `Step`, so it adds no variant.)
132#[derive(Debug, PartialEq, Eq)]
133#[must_use]
134pub enum Step<'a, A> {
135    /// The buffer completed a binding, resolving to this action; the buffer has
136    /// been cleared.
137    Fired(&'a A),
138    /// The buffer is a proper prefix of a longer binding; it has been kept. Keep
139    /// reading (and, for a timed binding, (re)start your idle timer).
140    Pending,
141    /// The buffer is not on any binding path; it has been cleared and its keys —
142    /// every key buffered so far, in order — are handed back for the caller to
143    /// handle.
144    PassThrough(Vec<KeyInput>),
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use keymap_core::{Key, Modifiers};
151
152    #[derive(Debug, Clone, PartialEq)]
153    enum Action {
154        Save,
155        Quit,
156        Top,
157        Solo,
158    }
159
160    fn ctrl(c: char) -> KeyInput {
161        KeyInput::new(Key::Char(c), Modifiers::CTRL)
162    }
163
164    fn plain(c: char) -> KeyInput {
165        KeyInput::new(Key::Char(c), Modifiers::NONE)
166    }
167
168    /// `ctrl+x ctrl+s` -> Save, `ctrl+x ctrl+c` -> Quit, `g g` -> Top, `q` -> Solo.
169    fn sample() -> SequenceKeymap<Action> {
170        let mut map = SequenceKeymap::new();
171        map.bind([ctrl('x'), ctrl('s')], Action::Save).unwrap();
172        map.bind([ctrl('x'), ctrl('c')], Action::Quit).unwrap();
173        map.bind([plain('g'), plain('g')], Action::Top).unwrap();
174        map.bind([plain('q')], Action::Solo).unwrap();
175        map
176    }
177
178    #[test]
179    fn prefix_then_exact_fires_and_clears() {
180        let map = sample();
181        let mut p = PendingSequence::new();
182        assert_eq!(p.feed(&map, ctrl('x')), Step::Pending);
183        assert_eq!(p.pending(), &[ctrl('x')]);
184        assert_eq!(p.feed(&map, ctrl('s')), Step::Fired(&Action::Save));
185        // Fired cleared the buffer.
186        assert!(p.is_empty());
187    }
188
189    #[test]
190    fn clear_after_exact_lets_the_next_sequence_start_fresh() {
191        // The "clear 忘れ" case, asserted through the *next* transition: a third
192        // `g` after a completed `g g` must be a fresh Prefix, not a phantom Exact.
193        let map = sample();
194        let mut p = PendingSequence::new();
195        assert_eq!(p.feed(&map, plain('g')), Step::Pending);
196        assert_eq!(p.feed(&map, plain('g')), Step::Fired(&Action::Top));
197        assert_eq!(p.feed(&map, plain('g')), Step::Pending);
198        assert_eq!(p.pending(), &[plain('g')]);
199    }
200
201    #[test]
202    fn passthrough_returns_the_whole_buffer_not_just_the_last_key() {
203        // An abandoned prefix (`ctrl+x` then an unrelated key) hands back both
204        // keys, in order — dropping the held `ctrl+x` would silently eat input.
205        let map = sample();
206        let mut p = PendingSequence::new();
207        assert_eq!(p.feed(&map, ctrl('x')), Step::Pending);
208        assert_eq!(
209            p.feed(&map, plain('z')),
210            Step::PassThrough(vec![ctrl('x'), plain('z')])
211        );
212        assert!(p.is_empty());
213    }
214
215    #[test]
216    fn length_one_sequence_fires_on_the_first_key() {
217        let map = sample();
218        let mut p = PendingSequence::new();
219        assert_eq!(p.feed(&map, plain('q')), Step::Fired(&Action::Solo));
220        assert!(p.is_empty());
221    }
222
223    #[test]
224    fn empty_map_passes_every_key_through() {
225        let map: SequenceKeymap<Action> = SequenceKeymap::new();
226        let mut p = PendingSequence::new();
227        // No binding can exist, so a key is never a Prefix to wait on.
228        assert_eq!(
229            p.feed(&map, plain('a')),
230            Step::PassThrough(vec![plain('a')])
231        );
232        assert!(p.is_empty());
233    }
234
235    #[test]
236    fn abandoned_prefix_does_not_contaminate_a_fresh_sequence() {
237        // The example's own stream: an abandoned `ctrl+x z`, then a clean `g g`.
238        let map = sample();
239        let mut p = PendingSequence::new();
240        assert_eq!(p.feed(&map, ctrl('x')), Step::Pending);
241        assert_eq!(
242            p.feed(&map, plain('z')),
243            Step::PassThrough(vec![ctrl('x'), plain('z')])
244        );
245        assert_eq!(p.feed(&map, plain('g')), Step::Pending);
246        assert_eq!(p.feed(&map, plain('g')), Step::Fired(&Action::Top));
247        assert!(p.is_empty());
248    }
249
250    #[test]
251    fn a_key_that_could_start_a_sequence_is_passed_through_mid_prefix() {
252        // `a b` and `c` bound; `a` then `c` makes `[a, c]` a miss — `c` is handed
253        // back *with* `a`, not re-fed as a fresh start (lookahead is the caller's).
254        let mut map = SequenceKeymap::new();
255        map.bind([plain('a'), plain('b')], Action::Save).unwrap();
256        map.bind([plain('c')], Action::Solo).unwrap();
257        let mut p = PendingSequence::new();
258        assert_eq!(p.feed(&map, plain('a')), Step::Pending);
259        assert_eq!(
260            p.feed(&map, plain('c')),
261            Step::PassThrough(vec![plain('a'), plain('c')])
262        );
263    }
264
265    #[test]
266    fn branching_prefix_reaches_both_leaves() {
267        let map = sample();
268        let mut p = PendingSequence::new();
269        assert_eq!(p.feed(&map, ctrl('x')), Step::Pending);
270        assert_eq!(p.feed(&map, ctrl('c')), Step::Fired(&Action::Quit));
271        assert_eq!(p.feed(&map, ctrl('x')), Step::Pending);
272        assert_eq!(p.feed(&map, ctrl('s')), Step::Fired(&Action::Save));
273    }
274
275    #[test]
276    fn deep_prefix_is_pending_at_every_step_then_fires() {
277        let mut map = SequenceKeymap::new();
278        map.bind([plain('a'), plain('b'), plain('c')], Action::Top)
279            .unwrap();
280        let mut p = PendingSequence::new();
281        assert_eq!(p.feed(&map, plain('a')), Step::Pending);
282        assert_eq!(p.feed(&map, plain('b')), Step::Pending);
283        assert_eq!(p.feed(&map, plain('c')), Step::Fired(&Action::Top));
284    }
285
286    // --- flush: the idle-flush litmus and its abnormal cases. ---
287
288    #[test]
289    fn flush_drains_a_pending_prefix_as_literals_exactly_once() {
290        // The litmus the old example could only narrate: a prefix held, then the
291        // caller's idle timer flushes the dangling key as a literal, once, and the
292        // buffer ends empty.
293        let mut map = SequenceKeymap::new();
294        map.bind([plain('j'), plain('j')], Action::Top).unwrap();
295        let mut p = PendingSequence::new();
296        assert_eq!(p.feed(&map, plain('j')), Step::Pending);
297        assert_eq!(p.pending(), &[plain('j')]);
298        assert_eq!(p.flush(), vec![plain('j')]);
299        assert!(p.is_empty());
300        // Idempotent: a second flush has nothing left to drain.
301        assert_eq!(p.flush(), Vec::<KeyInput>::new());
302    }
303
304    #[test]
305    fn flush_on_empty_buffer_is_a_no_op() {
306        let mut p = PendingSequence::new();
307        assert_eq!(p.flush(), Vec::<KeyInput>::new());
308        assert!(p.is_empty());
309    }
310
311    #[test]
312    fn flush_after_fired_or_passthrough_is_empty() {
313        // Fired and PassThrough both already cleared the buffer, so flush must not
314        // replay their keys (which would double-emit them).
315        let map = sample();
316        let mut p = PendingSequence::new();
317        assert_eq!(p.feed(&map, plain('q')), Step::Fired(&Action::Solo)); // cleared
318        assert_eq!(p.flush(), Vec::<KeyInput>::new());
319        assert_eq!(
320            p.feed(&map, plain('z')),
321            Step::PassThrough(vec![plain('z')])
322        ); // cleared
323        assert_eq!(p.flush(), Vec::<KeyInput>::new());
324    }
325
326    #[test]
327    fn no_key_is_dropped_or_duplicated_across_a_stream() {
328        // Conservation law (Theresa): the keys returned by PassThrough + flush,
329        // plus the keys consumed by each Fired sequence, equal the input stream in
330        // order, with nothing lost or repeated. Catches clear-bugs and the
331        // mid-prefix pass-through edge in one assertion.
332        let map = sample();
333        let stream = [
334            ctrl('x'),
335            ctrl('s'), // completes Save: consumes [ctrl+x, ctrl+s]
336            ctrl('x'),
337            plain('z'), // miss: passes through [ctrl+x, z]
338            plain('g'), // dangling prefix at end: flushed
339        ];
340        let mut p = PendingSequence::new();
341        let mut seen: Vec<KeyInput> = Vec::new();
342        let mut buf: Vec<KeyInput> = Vec::new();
343        for key in stream {
344            buf.push(key);
345            match p.feed(&map, key) {
346                Step::Fired(_) => {
347                    seen.append(&mut buf);
348                }
349                Step::Pending => {}
350                Step::PassThrough(keys) => {
351                    // The buffered keys must equal what we tracked independently.
352                    assert_eq!(keys, buf);
353                    seen.append(&mut buf);
354                }
355            }
356        }
357        // Whatever is still pending is what flush will hand back.
358        let mut flushed = p.flush();
359        seen.append(&mut flushed);
360        buf.clear();
361        assert_eq!(seen, stream);
362    }
363}