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}