Skip to main content

escriba_runtime/
operator_pending.rs

1//! The operator-pending FSM — vim's `{count}{operator}{count}{motion}`
2//! key composition.
3//!
4//! After the `d`/`c`/`y` key ([`Action::Operator`]) the editor *waits* for a
5//! motion; the next motion key composes into an [`Action::ApplyOperator`] the
6//! runtime executes (the engine built in `apply_operator`). This is a pure
7//! `(State, Event) -> (State, effects)` machine, so it **stands on the fleet
8//! `zenmai` primitive** (the same Mealy-machine-with-effects abstraction
9//! `bolso-core` and `gaveta-client-core::Escort` use) rather than re-rolling a
10//! bespoke `Option<Operator>` + scattered `if let`s in the dispatch path.
11//!
12//! **Counts compose here, not in a naive repeat loop.** The `Event` and
13//! `Effect` are `(Action, count)`: the machine consumes the resolved action +
14//! its count off the keymap, and emits the action(s) the runtime should run +
15//! *how many times*. A bare motion (`5j`) passes through carrying its count; an
16//! operated motion multiplies the two counts (`3d2w` = delete `3 × 2 = 6`
17//! words), because vim's operator count and motion count multiply.
18
19use escriba_core::{Action, Operator};
20
21/// Operator-pending state. `Resting` = normal; `Awaiting { op, count }` = an
22/// operator key was pressed (with `count`, default 1) and the next motion will
23/// be operated over.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum OpState {
26    #[default]
27    Resting,
28    Awaiting {
29        op: Operator,
30        count: u32,
31    },
32}
33
34/// The zenmai machine. A ZST marker; the reducer is [`Self::step`]. The
35/// `(Action, u32)` event/effect pair is the action and its repetition count.
36pub struct OperatorPending;
37
38impl zenmai::Machine for OperatorPending {
39    type State = OpState;
40    /// The resolved action and its count (from the keymap's `CountedAction`).
41    type Event = (Action, u32);
42    /// The action the runtime should execute and how many times. Empty = the
43    /// key was consumed (an operator began, or a pending operator was cancelled).
44    type Effect = (Action, u32);
45
46    fn step(state: &OpState, event: (Action, u32)) -> (OpState, Vec<(Action, u32)>) {
47        let (action, count) = event;
48        match (state, action) {
49            // A stray `Pending` (mid-sequence key) never disturbs operator
50            // state and runs nothing — the runtime's own sequence buffer owns it.
51            (s, Action::Pending) => (*s, vec![]),
52
53            // Resting: an operator key arms the machine (capturing its count);
54            // everything else passes straight through carrying its own count.
55            (OpState::Resting, Action::Operator(op)) => {
56                (OpState::Awaiting { op, count }, vec![])
57            }
58            (OpState::Resting, other) => (OpState::Resting, vec![(other, count)]),
59
60            // Awaiting a motion: the motion composes into ApplyOperator, applied
61            // `op_count × motion_count` times (the two vim counts multiply).
62            (OpState::Awaiting { op, count: op_count }, Action::Move(motion)) => (
63                OpState::Resting,
64                vec![(
65                    Action::ApplyOperator { op: *op, motion },
66                    op_count.saturating_mul(count).max(1),
67                )],
68            ),
69
70            // Awaiting + anything-not-a-motion cancels the operator and drops
71            // the key (vim: `d` then a non-motion does nothing). Covers Esc
72            // (ChangeMode), operator-doubling `dd` (linewise — deferred), and
73            // any other key.
74            (OpState::Awaiting { .. }, _) => (OpState::Resting, vec![]),
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use escriba_core::{Mode, Motion};
83    use zenmai::Machine;
84
85    #[test]
86    fn operator_then_motion_composes_apply_operator() {
87        // `d` arms (count 1), `w` composes `dw` once.
88        let (s, fx) =
89            OperatorPending::step(&OpState::Resting, (Action::Operator(Operator::Delete), 1));
90        assert_eq!(s, OpState::Awaiting { op: Operator::Delete, count: 1 });
91        assert!(fx.is_empty(), "the operator key runs nothing, it waits");
92
93        let (s, fx) = OperatorPending::step(&s, (Action::Move(Motion::WordStartNext), 1));
94        assert_eq!(s, OpState::Resting);
95        assert_eq!(
96            fx,
97            vec![(Action::ApplyOperator { op: Operator::Delete, motion: Motion::WordStartNext }, 1)]
98        );
99    }
100
101    #[test]
102    fn operator_count_multiplies_the_motion() {
103        // `3dw` = delete 3 words: the operator's count (3) flows to the
104        // composed motion as the repetition count.
105        let (s, _) =
106            OperatorPending::step(&OpState::Resting, (Action::Operator(Operator::Delete), 3));
107        assert_eq!(s, OpState::Awaiting { op: Operator::Delete, count: 3 });
108        let (_, fx) = OperatorPending::step(&s, (Action::Move(Motion::WordStartNext), 1));
109        assert_eq!(
110            fx,
111            vec![(Action::ApplyOperator { op: Operator::Delete, motion: Motion::WordStartNext }, 3)]
112        );
113    }
114
115    #[test]
116    fn operator_and_motion_counts_multiply() {
117        // `3d2w` = delete 3×2 = 6 words.
118        let (s, _) =
119            OperatorPending::step(&OpState::Resting, (Action::Operator(Operator::Delete), 3));
120        let (_, fx) = OperatorPending::step(&s, (Action::Move(Motion::WordStartNext), 2));
121        assert_eq!(fx[0].1, 6, "operator count × motion count");
122    }
123
124    #[test]
125    fn bare_motion_passes_through_carrying_its_count() {
126        // `5j` is not an operated motion — it passes through with count 5.
127        let (s, fx) = OperatorPending::step(&OpState::Resting, (Action::Move(Motion::Down), 5));
128        assert_eq!(s, OpState::Resting);
129        assert_eq!(fx, vec![(Action::Move(Motion::Down), 5)]);
130    }
131
132    #[test]
133    fn esc_cancels_a_pending_operator_and_drops_the_key() {
134        let (s, fx) = OperatorPending::step(
135            &OpState::Awaiting { op: Operator::Change, count: 1 },
136            (Action::ChangeMode(Mode::Normal), 1),
137        );
138        assert_eq!(s, OpState::Resting, "Esc cancels the operator");
139        assert!(fx.is_empty(), "the cancel key is dropped");
140    }
141
142    #[test]
143    fn pending_never_disturbs_operator_state() {
144        // A stray Pending while awaiting keeps the operator armed (a multi-key
145        // motion like `gg` builds in the runtime's sequence buffer first).
146        let (s, fx) = OperatorPending::step(
147            &OpState::Awaiting { op: Operator::Yank, count: 1 },
148            (Action::Pending, 1),
149        );
150        assert_eq!(s, OpState::Awaiting { op: Operator::Yank, count: 1 });
151        assert!(fx.is_empty());
152    }
153
154    #[test]
155    fn inert_on_a_doubled_operator_dd_deferred() {
156        // `dd` (linewise) is deferred — a second operator cancels for now.
157        let (s, fx) = OperatorPending::step(
158            &OpState::Awaiting { op: Operator::Delete, count: 1 },
159            (Action::Operator(Operator::Delete), 1),
160        );
161        assert_eq!(s, OpState::Resting);
162        assert!(fx.is_empty());
163    }
164}