Skip to main content

escriba_keymap/
lib.rs

1//! `escriba-keymap` — mode-aware keybinding dispatch.
2
3extern crate self as escriba_keymap;
4
5use std::collections::HashMap;
6
7use escriba_core::{Action, CountedAction, Mode, Motion};
8use escriba_mode::ModalState;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Key {
13    Char(char),
14    Esc,
15    Enter,
16    Tab,
17    Backspace,
18    Left,
19    Right,
20    Up,
21    Down,
22    PageUp,
23    PageDown,
24    Home,
25    End,
26    Ctrl(char),
27    Alt(char),
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Binding {
32    pub action: Action,
33    pub description: String,
34}
35
36impl Binding {
37    #[must_use]
38    pub fn new(action: Action, description: impl Into<String>) -> Self {
39        Self {
40            action,
41            description: description.into(),
42        }
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct Keymap {
48    bindings: HashMap<(Mode, Key), Binding>,
49    /// Multi-key sequence bindings (`<leader>ff`, `gg`, `<C-w>h`).
50    /// Keyed by the full key sequence; resolved by the runtime's
51    /// pending-stroke loop ([`lookup_sequence`](Keymap::lookup_sequence)
52    /// + [`is_sequence_prefix`](Keymap::is_sequence_prefix)).
53    sequences: HashMap<(Mode, Vec<Key>), Binding>,
54    /// The prefix `<leader>` resolves to at sequence-apply time.
55    leader: Key,
56}
57
58impl Default for Keymap {
59    fn default() -> Self {
60        Self {
61            bindings: HashMap::new(),
62            sequences: HashMap::new(),
63            // blnvim's leader is comma; escriba ships blnvim-parity
64            // defaults, so the prefix users press matches muscle memory.
65            leader: Key::Char(','),
66        }
67    }
68}
69
70impl Keymap {
71    #[must_use]
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    #[must_use]
77    pub fn default_vim() -> Self {
78        let mut m = Self::new();
79        let nm = |m: &mut Keymap, k: Key, a: Action, d: &'static str| m.bind(Mode::Normal, k, a, d);
80        nm(
81            &mut m,
82            Key::Char('h'),
83            Action::Move(Motion::Left),
84            "move left",
85        );
86        nm(
87            &mut m,
88            Key::Char('l'),
89            Action::Move(Motion::Right),
90            "move right",
91        );
92        nm(
93            &mut m,
94            Key::Char('j'),
95            Action::Move(Motion::Down),
96            "move down",
97        );
98        nm(&mut m, Key::Char('k'), Action::Move(Motion::Up), "move up");
99        nm(
100            &mut m,
101            Key::Char('w'),
102            Action::Move(Motion::WordStartNext),
103            "word forward",
104        );
105        nm(
106            &mut m,
107            Key::Char('b'),
108            Action::Move(Motion::WordStartPrev),
109            "word back",
110        );
111        nm(
112            &mut m,
113            Key::Char('0'),
114            Action::Move(Motion::LineStart),
115            "line start",
116        );
117        nm(
118            &mut m,
119            Key::Char('$'),
120            Action::Move(Motion::LineEnd),
121            "line end",
122        );
123        nm(
124            &mut m,
125            Key::Char('G'),
126            Action::Move(Motion::DocEnd),
127            "doc end",
128        );
129        // Structural Lisp motions — Alt-prefixed like emacs paredit.
130        nm(
131            &mut m,
132            Key::Alt('f'),
133            Action::Move(Motion::ForwardSexp),
134            "forward sexp",
135        );
136        nm(
137            &mut m,
138            Key::Alt('b'),
139            Action::Move(Motion::BackwardSexp),
140            "backward sexp",
141        );
142        nm(
143            &mut m,
144            Key::Alt('u'),
145            Action::Move(Motion::UpList),
146            "up list",
147        );
148        nm(
149            &mut m,
150            Key::Alt('d'),
151            Action::Move(Motion::DownList),
152            "down list",
153        );
154        // Mode changes.
155        nm(
156            &mut m,
157            Key::Char('i'),
158            Action::ChangeMode(Mode::Insert),
159            "insert",
160        );
161        nm(
162            &mut m,
163            Key::Char('v'),
164            Action::ChangeMode(Mode::Visual),
165            "visual",
166        );
167        nm(
168            &mut m,
169            Key::Char('V'),
170            Action::ChangeMode(Mode::VisualLine),
171            "visual line",
172        );
173        nm(
174            &mut m,
175            Key::Char(':'),
176            Action::ChangeMode(Mode::Command),
177            "command",
178        );
179        nm(&mut m, Key::Char('u'), Action::Undo, "undo");
180        nm(&mut m, Key::Ctrl('r'), Action::Redo, "redo");
181        // Insert → Normal on Esc.
182        m.bind(
183            Mode::Insert,
184            Key::Esc,
185            Action::ChangeMode(Mode::Normal),
186            "to normal",
187        );
188        m.bind(
189            Mode::Command,
190            Key::Esc,
191            Action::ChangeMode(Mode::Normal),
192            "abort",
193        );
194        m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
195        m.bind(
196            Mode::Visual,
197            Key::Esc,
198            Action::ChangeMode(Mode::Normal),
199            "to normal",
200        );
201        m.bind(
202            Mode::VisualLine,
203            Key::Esc,
204            Action::ChangeMode(Mode::Normal),
205            "to normal",
206        );
207        m
208    }
209
210    pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
211        self.bindings
212            .insert((mode, key), Binding::new(action, desc));
213    }
214
215    #[must_use]
216    pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
217        self.bindings.get(&(mode, key.clone()))
218    }
219
220    /// The leader key — what `<leader>` resolves to when a sequence
221    /// binding is applied. Defaults to `,` (blnvim parity).
222    #[must_use]
223    pub fn leader(&self) -> &Key {
224        &self.leader
225    }
226
227    /// Override the leader key. Applied before sequence bindings so
228    /// `<leader>`-prefixed specs resolve against the chosen prefix.
229    pub fn set_leader(&mut self, key: Key) {
230        self.leader = key;
231    }
232
233    /// Bind a multi-key SEQUENCE — `<leader>ff` →
234    /// `[Char(','), Char('f'), Char('f')]`, `gg` →
235    /// `[Char('g'), Char('g')]`. A length-1 sequence delegates to
236    /// [`bind`](Keymap::bind) so callers never special-case it; an
237    /// empty sequence is a no-op.
238    pub fn bind_sequence(
239        &mut self,
240        mode: Mode,
241        keys: Vec<Key>,
242        action: Action,
243        desc: impl Into<String>,
244    ) {
245        match keys.as_slice() {
246            [] => {}
247            [single] => self.bind(mode, single.clone(), action, desc),
248            _ => {
249                self.sequences
250                    .insert((mode, keys), Binding::new(action, desc));
251            }
252        }
253    }
254
255    /// Exact-match lookup for a full key sequence.
256    #[must_use]
257    pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
258        self.sequences.get(&(mode, keys.to_vec()))
259    }
260
261    /// Does any bound sequence in `mode` STRICTLY extend `prefix`
262    /// (i.e. `prefix` is a proper prefix of a longer bound sequence)?
263    /// Drives the runtime's pending-stroke state: a partial sequence
264    /// that is still a live prefix is held pending rather than
265    /// dispatched. Linear scan — fine at fleet sequence counts; a
266    /// trie is a later optimization if profiling ever asks for it.
267    #[must_use]
268    pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
269        self.sequences
270            .keys()
271            .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
272    }
273
274    /// Count of bound multi-key sequences — for `--keymap` / doctor.
275    #[must_use]
276    pub fn sequence_len(&self) -> usize {
277        self.sequences.len()
278    }
279
280    #[must_use]
281    pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
282        let mode = state.mode();
283        if mode == Mode::Normal {
284            if let Key::Char(c) = key {
285                if c.is_ascii_digit() && *c != '0' {
286                    return CountedAction::once(Action::Pending);
287                }
288                if *c == '0' && state.pending_count().is_some() {
289                    return CountedAction::once(Action::Pending);
290                }
291            }
292        }
293        if mode == Mode::Insert {
294            if let Key::Char(c) = key {
295                return CountedAction::once(Action::InsertChar(*c));
296            }
297            if matches!(key, Key::Enter) {
298                return CountedAction::once(Action::InsertChar('\n'));
299            }
300        }
301        if mode == Mode::Command {
302            if let Key::Char(c) = key {
303                return CountedAction::once(Action::InsertChar(*c));
304            }
305        }
306        if let Some(b) = self.lookup(mode, key) {
307            return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
308        }
309        CountedAction::once(Action::Pending)
310    }
311
312    #[must_use]
313    pub fn len(&self) -> usize {
314        self.bindings.len()
315    }
316
317    #[must_use]
318    pub fn is_empty(&self) -> bool {
319        self.bindings.is_empty()
320    }
321
322    /// Sorted view over every binding — for `escriba --keymap` and palettes.
323    #[must_use]
324    pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
325        let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
326        v.sort_by(|a, b| {
327            (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
328        });
329        v
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn default_vim_has_bindings() {
339        let k = Keymap::default_vim();
340        assert!(k.len() > 10);
341        assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
342        assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
343        assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
344    }
345
346    #[test]
347    fn dispatch_normal_motion() {
348        let k = Keymap::default_vim();
349        let s = ModalState::new();
350        let a = k.dispatch(&s, &Key::Char('h'));
351        assert_eq!(a.count, 1);
352        assert_eq!(a.action, Action::Move(Motion::Left));
353    }
354
355    #[test]
356    fn dispatch_count_prefix_pends() {
357        let k = Keymap::default_vim();
358        let s = ModalState::new();
359        assert!(matches!(
360            k.dispatch(&s, &Key::Char('5')).action,
361            Action::Pending
362        ));
363    }
364
365    #[test]
366    fn dispatch_insert_char() {
367        let k = Keymap::default_vim();
368        let mut s = ModalState::new();
369        s.enter(Mode::Insert);
370        let a = k.dispatch(&s, &Key::Char('a'));
371        assert_eq!(a.action, Action::InsertChar('a'));
372    }
373
374    #[test]
375    fn lisp_structural_motions_bound() {
376        let k = Keymap::default_vim();
377        assert_eq!(
378            k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
379            Action::Move(Motion::ForwardSexp)
380        );
381    }
382
383    #[test]
384    fn default_leader_is_comma() {
385        assert_eq!(Keymap::new().leader(), &Key::Char(','));
386    }
387
388    #[test]
389    fn bind_sequence_stores_multikey_and_resolves() {
390        let mut k = Keymap::new();
391        let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
392        k.bind_sequence(
393            Mode::Normal,
394            seq.clone(),
395            Action::Command {
396                name: "picker.files".into(),
397                args: vec![],
398            },
399            "find files",
400        );
401        // Exact match resolves.
402        let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
403        assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
404        // Proper prefixes are live; the full sequence is NOT a prefix
405        // of itself.
406        assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
407        assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
408        assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
409        // Wrong mode → not a prefix.
410        assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
411        assert_eq!(k.sequence_len(), 1);
412    }
413
414    #[test]
415    fn bind_sequence_length_one_delegates_to_single() {
416        let mut k = Keymap::new();
417        k.bind_sequence(
418            Mode::Normal,
419            vec![Key::Char('x')],
420            Action::Undo,
421            "x is undo",
422        );
423        // Lands in the single-key table, not the sequence table.
424        assert_eq!(k.sequence_len(), 0);
425        assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
426    }
427}