Skip to main content

lattice_keymap/
binding_mode.rs

1//! `BindingMode` — the vim-modal state a chord resolves in.
2//!
3//! K.3 (2026-06-07): moved from `lattice-mode` into `lattice-keymap`
4//! so the keymap trie, `KeymapLayer`, and `resolve_trace` can reference
5//! the mode enum without a dep cycle back to `lattice-mode`.
6//!
7//! `lattice-mode::BindingMode` and `lattice-host::keymap::BindingMode`
8//! are retained as re-export shims.
9
10/// Where a binding takes effect. Multi-key sequences (e.g. `gg`)
11/// resolve atomically; intermediate single-key prefixes (`g`, `z`) get
12/// their own descriptor entries so `:describe-key g` explains the
13/// pending substate.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum BindingMode {
16    /// Vim Normal mode — the default command state.
17    Normal,
18    /// Insert mode: unbound printable chords insert themselves as text.
19    Insert,
20    /// Charwise / Linewise / Blockwise visual share the same chord
21    /// table; differences are in the operator dispatch (Range::Selection
22    /// resolution).
23    Visual,
24    /// Vim Select mode (SN.3d). Its own chord table — distinct from
25    /// [`Self::Visual`] because a bare printable key dispatches
26    /// differently (Visual: command lookup; Select: replace-and-insert).
27    /// Motions/extensions are duplicated from the Visual table and kept
28    /// honest by a parity test. See `docs/dev/architecture/select-mode.md`.
29    Select,
30    /// Vim Replace mode (`R`): typed chars overwrite rather than insert.
31    Replace,
32    /// `:` minibuffer.
33    Command,
34    /// `/` `?` minibuffer.
35    Search,
36    /// `Effect::OpenPrompt`'s generic one-line prompt — the third
37    /// buffer-backed readline surface, peer of [`Self::Command`] and
38    /// [`Self::Search`].
39    ///
40    /// Its own table for the same reason they have theirs: a minibuffer is
41    /// not an Insert buffer. Resolving these surfaces against the Insert
42    /// table let every globally-active minor's Insert bindings leak onto
43    /// them — auto-pair's `<BS>` shadowed the builtin backspace on the `:`
44    /// line, so backspace did nothing there in either renderer.
45    Prompt,
46    /// After `d` / `y` / `c` / `>` / `<` / `gU` / `gu` / `g~` -- waiting
47    /// for a motion or text-object target.
48    OperatorPending,
49    /// After `g` -- waiting for the second key.
50    AfterG,
51    /// After `z` -- waiting for the second key.
52    AfterZ,
53    /// After `m` -- waiting for the mark name.
54    AfterMark,
55    /// After `'` (jump to mark line) -- waiting for mark name.
56    AfterJumpMarkLine,
57    /// After `` ` `` (jump to mark exact) -- waiting for mark name.
58    AfterJumpMarkExact,
59    /// After `"` -- waiting for register name.
60    AfterRegister,
61    /// After `q` (when not already recording) -- waiting for register
62    /// name to record into.
63    AfterMacroStart,
64    /// After `@` -- waiting for register name to play (or `@` for last).
65    AfterMacroPlay,
66    /// After `f` / `F` / `t` / `T` -- waiting for the target char.
67    AfterFindChar,
68    /// After `i<x>` / `a<x>` in operator-pending -- waiting for the
69    /// text-object key.
70    AfterTextObject,
71    /// While the §5.11 help overlay is active.
72    Help,
73    /// After `<C-w>` -- waiting for the window-management
74    /// resolution key.
75    AfterCtrlW,
76    /// After `<C-x>` in Insert mode -- waiting for the
77    /// expansion-prefix resolution key (`<C-x><C-o>` ->
78    /// completion trigger; future siblings: `<C-x><C-s>`
79    /// snippet expand, `<C-x><C-f>` filename completion).
80    AfterCtrlX,
81    /// **Insert-mode completion popup minor mode** (Phase
82    /// 4.2.g.1). Active only while
83    /// `App.insert_completion.is_some()`. Bindings inside this
84    /// layer override Insert-mode + Normal-mode meanings for
85    /// the popup's lifetime; closing the popup deactivates the
86    /// layer.
87    CompletionPopup,
88    /// **Active-snippet minor mode** (Phase 4.2.g.4). Active
89    /// only while `App.active_snippet.is_some()`. Bindings
90    /// inside this layer override Insert-mode meanings for the
91    /// snippet's lifetime: `<Tab>` jumps to the next
92    /// placeholder (instead of inserting a literal tab),
93    /// `<S-Tab>` to the previous, `<Esc>` exits the snippet
94    /// and Insert mode. Closing the snippet (reaching `$0`,
95    /// pressing `<Esc>`, or `:snippet-leave`) deactivates the
96    /// layer.
97    Snippet,
98}
99
100impl BindingMode {
101    /// Human-readable label used in `:describe-key` output and diagnostics.
102    /// Also the `Display` form.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use lattice_keymap::BindingMode;
108    ///
109    /// assert_eq!(BindingMode::OperatorPending.label(), "Operator-Pending");
110    /// assert_eq!(BindingMode::AfterG.to_string(), "After-g");
111    /// ```
112    pub fn label(self) -> &'static str {
113        match self {
114            BindingMode::Normal => "Normal",
115            BindingMode::Insert => "Insert",
116            BindingMode::Visual => "Visual",
117            BindingMode::Select => "Select",
118            BindingMode::Replace => "Replace",
119            BindingMode::Command => "Command",
120            BindingMode::Search => "Search",
121            BindingMode::Prompt => "Prompt",
122            BindingMode::OperatorPending => "Operator-Pending",
123            BindingMode::AfterG => "After-g",
124            BindingMode::AfterZ => "After-z",
125            BindingMode::AfterMark => "After-m",
126            BindingMode::AfterJumpMarkLine => "After-'",
127            BindingMode::AfterJumpMarkExact => "After-`",
128            BindingMode::AfterRegister => "After-\"",
129            BindingMode::AfterMacroStart => "After-q (record)",
130            BindingMode::AfterMacroPlay => "After-@",
131            BindingMode::AfterFindChar => "After-f/F/t/T",
132            BindingMode::AfterTextObject => "After-i/a (text-object)",
133            BindingMode::Help => "Help-overlay",
134            BindingMode::AfterCtrlW => "After-<C-w> (window-management)",
135            BindingMode::AfterCtrlX => "After-<C-x> (Insert expansion-prefix)",
136            BindingMode::CompletionPopup => "Completion popup (minor mode)",
137            BindingMode::Snippet => "Active-snippet (minor mode)",
138        }
139    }
140
141    /// The binding modes `:describe-key` iterates, in declaration order.
142    /// Used by [`KeymapHandle::resolve_trace_all_modes`](crate::KeymapHandle::resolve_trace_all_modes)
143    /// to walk every mode.
144    ///
145    /// Note: the list currently omits [`Self::Prompt`] (23 of the 24
146    /// variants), so a chord bound only in the Prompt table is not
147    /// reported by the all-modes trace.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use lattice_keymap::BindingMode;
153    ///
154    /// assert_eq!(BindingMode::all()[0], BindingMode::Normal);
155    /// assert!(BindingMode::all().contains(&BindingMode::Snippet));
156    /// ```
157    pub fn all() -> &'static [BindingMode] {
158        use BindingMode::*;
159        &[
160            Normal,
161            Insert,
162            Visual,
163            Select,
164            Replace,
165            Command,
166            Search,
167            OperatorPending,
168            AfterG,
169            AfterZ,
170            AfterMark,
171            AfterJumpMarkLine,
172            AfterJumpMarkExact,
173            AfterRegister,
174            AfterMacroStart,
175            AfterMacroPlay,
176            AfterFindChar,
177            AfterTextObject,
178            Help,
179            AfterCtrlW,
180            AfterCtrlX,
181            CompletionPopup,
182            Snippet,
183        ]
184    }
185}
186
187impl std::fmt::Display for BindingMode {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.write_str(self.label())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn label_covers_all_variants() {
199        // Exhaustive check: every variant must return a non-empty label.
200        let modes = [
201            BindingMode::Normal,
202            BindingMode::Insert,
203            BindingMode::Visual,
204            BindingMode::Select,
205            BindingMode::Replace,
206            BindingMode::Command,
207            BindingMode::Search,
208            BindingMode::OperatorPending,
209            BindingMode::AfterG,
210            BindingMode::AfterZ,
211            BindingMode::AfterMark,
212            BindingMode::AfterJumpMarkLine,
213            BindingMode::AfterJumpMarkExact,
214            BindingMode::AfterRegister,
215            BindingMode::AfterMacroStart,
216            BindingMode::AfterMacroPlay,
217            BindingMode::AfterFindChar,
218            BindingMode::AfterTextObject,
219            BindingMode::Help,
220            BindingMode::AfterCtrlW,
221            BindingMode::AfterCtrlX,
222            BindingMode::CompletionPopup,
223            BindingMode::Snippet,
224        ];
225        for m in modes {
226            assert!(!m.label().is_empty(), "empty label for {m:?}");
227        }
228    }
229
230    #[test]
231    fn display_equals_label() {
232        assert_eq!(format!("{}", BindingMode::Normal), "Normal");
233        assert_eq!(format!("{}", BindingMode::Insert), "Insert");
234        assert_eq!(
235            format!("{}", BindingMode::OperatorPending),
236            "Operator-Pending"
237        );
238    }
239
240    #[test]
241    fn all_covers_every_variant() {
242        // Must match the count in `label_covers_all_variants`.
243        assert_eq!(BindingMode::all().len(), 23);
244        // Every variant must appear exactly once (no duplicates).
245        let mut seen = std::collections::HashSet::new();
246        for m in BindingMode::all() {
247            assert!(seen.insert(*m), "duplicate: {m:?}");
248        }
249    }
250}