Skip to main content

escriba_mode/
lib.rs

1//! `escriba-mode` — modal state machine, typed as a sum so illegal mode
2//! field combinations are *unrepresentable*.
3//!
4//! Phase 1: vim-ish Normal/Insert/Visual/VisualLine/Command. The earlier
5//! design was a product type — `{ mode, pending_count, pending_operator,
6//! minibuffer }` — where nonsense like `mode: Insert` carrying a
7//! `pending_operator: Some(Delete)` was *constructible* and only kept sane
8//! by a runtime guard inside `enter()`. Per the org-level ★★
9//! UNREPRESENTABILITY rule, the fix is structural: model the per-mode state
10//! as a SUM so each mode carries ONLY the data that is valid in that mode.
11//!
12//! - `Normal` carries the pending count (`5dd`) + pending operator (the `d`
13//!   in `dw`) — these exist NOWHERE else.
14//! - `Insert` / `VisualLine` carry nothing.
15//! - `Visual` carries nothing in phase 1 (a future `anchor: Position` lands
16//!   here, in the one variant where a visual anchor is meaningful).
17//! - `Command` carries ONLY the accumulating minibuffer line.
18//!
19//! There is no way to construct a `Command` without dropping the pending
20//! operator, or an `Insert` that still holds a count — the type system
21//! refuses it. The only way to change mode is the typed transition methods
22//! (`enter_*` / `enter` / `escape`), so the per-mode invariant is enforced
23//! by construction at every transition, not re-checked by a guard.
24//!
25//! Typestate destination: a future revision can promote this to a
26//! phantom-typestate `Modal<P>` where illegal *transitions* (not just
27//! illegal field combos) are `E0599` compile errors. That is a larger
28//! ripple across the keymap-dispatch and runtime borrow sites; this sum
29//! type is the pragmatic first tier — illegal field combinations are
30//! already truly-unrepresentable, and the transition surface is sealed
31//! behind methods so the typestate promotion is a later, localized change.
32
33extern crate self as escriba_mode;
34
35use escriba_core::{Mode, Operator};
36use serde::{Deserialize, Serialize};
37
38/// Pending operator-pending state — only meaningful in [`ModalState::Normal`].
39///
40/// Holds the count prefix being accumulated (`5` in `5dd`) and the pending
41/// operator (`d` in `dw`). Both live HERE and only here: no insert-mode or
42/// command-mode value can carry them, because the only place the type
43/// system admits them is the `Normal` variant.
44#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
45pub struct PendingOp {
46    /// Accumulating count prefix (`None` ⇒ no count typed yet, effective 1).
47    pub count: Option<u32>,
48    /// Pending operator awaiting a motion (the `d` in `dw`).
49    pub operator: Option<Operator>,
50}
51
52/// The modal state machine — a SUM over the five editor modes, each
53/// carrying ONLY the data valid in that mode.
54///
55/// `#[serde(tag = "mode")]` keeps the wire shape readable (`{"mode":
56/// "Normal", "pending": {…}}`) and is the parse boundary: a deserialized
57/// value can only be one of the legal shapes.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
59#[serde(tag = "mode")]
60pub enum ModalState {
61    Normal { pending: PendingOp },
62    Insert,
63    Visual,
64    VisualLine,
65    Command { minibuffer: String },
66}
67
68impl Default for ModalState {
69    fn default() -> Self {
70        Self::Normal {
71            pending: PendingOp::default(),
72        }
73    }
74}
75
76impl ModalState {
77    #[must_use]
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// The [`Mode`] discriminant of the current state — the projection the
83    /// renderers + keymap dispatch read.
84    #[must_use]
85    pub const fn mode(&self) -> Mode {
86        match self {
87            Self::Normal { .. } => Mode::Normal,
88            Self::Insert => Mode::Insert,
89            Self::Visual => Mode::Visual,
90            Self::VisualLine => Mode::VisualLine,
91            Self::Command { .. } => Mode::Command,
92        }
93    }
94
95    // ── Typed transitions — the ONLY way to change mode ──────────────────
96    //
97    // Each transition drops the data that is invalid in the destination
98    // mode by *construction*: entering `Insert` builds the `Insert` variant
99    // which has no field to hold a pending operator, so the operator is
100    // gone — not "cleared by a guard", but structurally absent.
101
102    /// Enter [`Mode::Normal`] with no pending count/operator.
103    pub fn enter_normal(&mut self) {
104        *self = Self::Normal {
105            pending: PendingOp::default(),
106        };
107    }
108
109    /// Enter [`Mode::Insert`]. Any pending count/operator/minibuffer is
110    /// dropped by construction.
111    pub fn enter_insert(&mut self) {
112        *self = Self::Insert;
113    }
114
115    /// Enter [`Mode::Visual`].
116    pub fn enter_visual(&mut self) {
117        *self = Self::Visual;
118    }
119
120    /// Enter [`Mode::VisualLine`].
121    pub fn enter_visual_line(&mut self) {
122        *self = Self::VisualLine;
123    }
124
125    /// Enter [`Mode::Command`] with an empty minibuffer.
126    pub fn enter_command(&mut self) {
127        *self = Self::Command {
128            minibuffer: String::new(),
129        };
130    }
131
132    /// Leave any mode back to a clean [`Mode::Normal`] — the `<Esc>`
133    /// transition.
134    pub fn escape(&mut self) {
135        self.enter_normal();
136    }
137
138    /// Dispatch to the matching typed transition for a target [`Mode`].
139    ///
140    /// Kept for callers that hold a runtime `Mode` value (e.g. the keymap's
141    /// `Action::ChangeMode(Mode)`); it is sugar over the `enter_*` methods
142    /// and preserves the invariant the same way.
143    pub fn enter(&mut self, mode: Mode) {
144        match mode {
145            Mode::Normal => self.enter_normal(),
146            Mode::Insert => self.enter_insert(),
147            Mode::Visual => self.enter_visual(),
148            Mode::VisualLine => self.enter_visual_line(),
149            Mode::Command => self.enter_command(),
150        }
151    }
152
153    // ── Pending-op surface — no-ops outside Normal ───────────────────────
154    //
155    // These mutate the `Normal` variant's pending state. Called in any
156    // other mode they are silent no-ops: there is no field to mutate, so
157    // the count/operator concept simply does not exist there — which is the
158    // whole point of the sum type.
159
160    /// Set the pending operator. No-op unless in [`Mode::Normal`].
161    pub fn set_operator(&mut self, op: Operator) {
162        if let Self::Normal { pending } = self {
163            pending.operator = Some(op);
164        }
165    }
166
167    /// Append a digit to the pending count. No-op unless in [`Mode::Normal`].
168    pub fn append_count(&mut self, digit: u32) {
169        if let Self::Normal { pending } = self {
170            let n = pending.count.unwrap_or(0);
171            pending.count = Some(n.saturating_mul(10).saturating_add(digit));
172        }
173    }
174
175    /// The current pending count (`None` ⇒ none accumulated). Always `None`
176    /// outside [`Mode::Normal`].
177    #[must_use]
178    pub const fn pending_count(&self) -> Option<u32> {
179        match self {
180            Self::Normal { pending } => pending.count,
181            _ => None,
182        }
183    }
184
185    /// The current pending operator. Always `None` outside [`Mode::Normal`].
186    #[must_use]
187    pub const fn pending_operator(&self) -> Option<Operator> {
188        match self {
189            Self::Normal { pending } => pending.operator,
190            _ => None,
191        }
192    }
193
194    /// Take the pending count, leaving it cleared; defaults to 1 (and at
195    /// least 1). No-op-returning-1 outside [`Mode::Normal`].
196    #[must_use]
197    pub fn consume_count(&mut self) -> u32 {
198        match self {
199            Self::Normal { pending } => pending.count.take().unwrap_or(1).max(1),
200            _ => 1,
201        }
202    }
203
204    /// Clear any pending count without consuming its value.
205    pub fn clear_count(&mut self) {
206        if let Self::Normal { pending } = self {
207            pending.count = None;
208        }
209    }
210
211    /// Take the pending operator, leaving it cleared.
212    #[must_use]
213    pub fn consume_operator(&mut self) -> Option<Operator> {
214        match self {
215            Self::Normal { pending } => pending.operator.take(),
216            _ => None,
217        }
218    }
219
220    // ── Command minibuffer surface — no-ops outside Command ──────────────
221
222    /// Read the command-mode minibuffer (empty string outside
223    /// [`Mode::Command`]).
224    #[must_use]
225    pub fn minibuffer(&self) -> &str {
226        match self {
227            Self::Command { minibuffer } => minibuffer,
228            _ => "",
229        }
230    }
231
232    /// Push a char onto the minibuffer. No-op unless in [`Mode::Command`].
233    pub fn push_minibuffer(&mut self, ch: char) {
234        if let Self::Command { minibuffer } = self {
235            minibuffer.push(ch);
236        }
237    }
238
239    /// Pop a char off the minibuffer. `None` unless in [`Mode::Command`].
240    pub fn pop_minibuffer(&mut self) -> Option<char> {
241        match self {
242            Self::Command { minibuffer } => minibuffer.pop(),
243            _ => None,
244        }
245    }
246
247    /// Append a raw fragment to the minibuffer (used by the command
248    /// registry's `__quit__` sentinel handshake). No-op outside
249    /// [`Mode::Command`].
250    pub fn push_minibuffer_str(&mut self, s: &str) {
251        if let Self::Command { minibuffer } = self {
252            minibuffer.push_str(s);
253        }
254    }
255
256    /// Clear the minibuffer in place. No-op outside [`Mode::Command`].
257    pub fn clear_minibuffer(&mut self) {
258        if let Self::Command { minibuffer } = self {
259            minibuffer.clear();
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn default_is_clean_normal() {
270        let s = ModalState::new();
271        assert_eq!(s.mode(), Mode::Normal);
272        assert_eq!(s.pending_count(), None);
273        assert_eq!(s.pending_operator(), None);
274    }
275
276    // ── Legal transitions work ───────────────────────────────────────────
277
278    #[test]
279    fn enter_transitions_set_mode() {
280        let mut s = ModalState::new();
281        s.enter_insert();
282        assert_eq!(s.mode(), Mode::Insert);
283        s.enter_visual();
284        assert_eq!(s.mode(), Mode::Visual);
285        s.enter_visual_line();
286        assert_eq!(s.mode(), Mode::VisualLine);
287        s.enter_command();
288        assert_eq!(s.mode(), Mode::Command);
289        s.escape();
290        assert_eq!(s.mode(), Mode::Normal);
291    }
292
293    #[test]
294    fn enter_by_mode_value_dispatches() {
295        for m in [
296            Mode::Normal,
297            Mode::Insert,
298            Mode::Visual,
299            Mode::VisualLine,
300            Mode::Command,
301        ] {
302            let mut s = ModalState::new();
303            s.enter(m);
304            assert_eq!(s.mode(), m);
305        }
306    }
307
308    // ── Illegal field combinations are UNREPRESENTABLE ───────────────────
309
310    #[test]
311    fn leaving_normal_structurally_drops_pending() {
312        // Set a count + operator in Normal, then enter Insert. The pending
313        // data is GONE — not because a guard cleared it, but because the
314        // `Insert` variant has no field that could hold it. There is no
315        // expressible `ModalState::Insert { pending_operator: … }`.
316        let mut s = ModalState::new();
317        s.set_operator(Operator::Delete);
318        s.append_count(5);
319        assert_eq!(s.pending_operator(), Some(Operator::Delete));
320        assert_eq!(s.pending_count(), Some(5));
321        s.enter_insert();
322        // The compiler refuses `Insert` carrying these; the accessors prove
323        // there is no value to read.
324        assert_eq!(s.pending_operator(), None);
325        assert_eq!(s.pending_count(), None);
326    }
327
328    #[test]
329    fn pending_ops_are_noops_outside_normal() {
330        // Attempting to set an operator / count while NOT in Normal cannot
331        // corrupt the state — the variants have nowhere to store them.
332        let mut s = ModalState::new();
333        s.enter_insert();
334        s.set_operator(Operator::Yank);
335        s.append_count(9);
336        assert_eq!(s.pending_operator(), None);
337        assert_eq!(s.pending_count(), None);
338        assert_eq!(s.consume_count(), 1, "no count exists outside Normal");
339        assert_eq!(s.consume_operator(), None);
340    }
341
342    #[test]
343    fn minibuffer_is_noop_outside_command() {
344        // An Insert-mode value cannot accumulate a command line — there is
345        // no minibuffer field on the `Insert` variant.
346        let mut s = ModalState::new();
347        s.enter_insert();
348        s.push_minibuffer('w');
349        assert_eq!(s.minibuffer(), "", "insert mode has no minibuffer");
350        assert_eq!(s.pop_minibuffer(), None);
351    }
352
353    #[test]
354    fn entering_command_clears_prior_minibuffer() {
355        let mut s = ModalState::new();
356        s.enter_command();
357        s.push_minibuffer('q');
358        assert_eq!(s.minibuffer(), "q");
359        // Re-entering command starts a fresh, empty line.
360        s.enter_command();
361        assert_eq!(s.minibuffer(), "");
362    }
363
364    // ── Behavioral round-trips (parity with the old product type) ────────
365
366    #[test]
367    fn normal_resets_pending_state() {
368        let mut s = ModalState::new();
369        s.enter_insert();
370        s.set_operator(Operator::Delete);
371        s.append_count(5);
372        s.enter_normal();
373        assert!(s.pending_count().is_none());
374        assert!(s.pending_operator().is_none());
375    }
376
377    #[test]
378    fn count_accumulates() {
379        let mut s = ModalState::new();
380        s.append_count(5);
381        s.append_count(3);
382        assert_eq!(s.consume_count(), 53);
383        assert_eq!(s.consume_count(), 1); // default 1 when consumed again
384    }
385
386    #[test]
387    fn operator_round_trip() {
388        let mut s = ModalState::new();
389        s.set_operator(Operator::Yank);
390        assert_eq!(s.consume_operator(), Some(Operator::Yank));
391        assert_eq!(s.consume_operator(), None);
392    }
393
394    #[test]
395    fn minibuffer_append_pop() {
396        let mut s = ModalState::new();
397        s.enter_command();
398        s.push_minibuffer('w');
399        assert_eq!(s.minibuffer(), "w");
400        assert_eq!(s.pop_minibuffer(), Some('w'));
401    }
402
403    #[test]
404    fn minibuffer_str_and_clear() {
405        let mut s = ModalState::new();
406        s.enter_command();
407        s.push_minibuffer_str("__quit__");
408        assert!(s.minibuffer().contains("__quit__"));
409        s.clear_minibuffer();
410        assert_eq!(s.minibuffer(), "");
411    }
412
413    /// The wire shape round-trips, and a deserialized value is one of the
414    /// legal variants only (the parse boundary rejects illegal shapes).
415    #[test]
416    fn serde_round_trip_per_variant() {
417        for s in [
418            ModalState::new(),
419            {
420                let mut n = ModalState::new();
421                n.append_count(12);
422                n.set_operator(Operator::Delete);
423                n
424            },
425            ModalState::Insert,
426            ModalState::Visual,
427            ModalState::VisualLine,
428            {
429                let mut c = ModalState::new();
430                c.enter_command();
431                c.push_minibuffer('x');
432                c
433            },
434        ] {
435            let json = serde_json::to_string(&s).unwrap();
436            let back: ModalState = serde_json::from_str(&json).unwrap();
437            assert_eq!(s, back);
438        }
439    }
440}