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, Operator};
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        // Operators — `d`/`c`/`y` arm the operator-pending FSM; the next
130        // motion composes (e.g. `dw`, `c$`, `y0`).
131        nm(
132            &mut m,
133            Key::Char('d'),
134            Action::Operator(Operator::Delete),
135            "delete (operator)",
136        );
137        nm(
138            &mut m,
139            Key::Char('c'),
140            Action::Operator(Operator::Change),
141            "change (operator)",
142        );
143        nm(
144            &mut m,
145            Key::Char('y'),
146            Action::Operator(Operator::Yank),
147            "yank (operator)",
148        );
149        // Structural Lisp motions — Alt-prefixed like emacs paredit.
150        nm(
151            &mut m,
152            Key::Alt('f'),
153            Action::Move(Motion::ForwardSexp),
154            "forward sexp",
155        );
156        nm(
157            &mut m,
158            Key::Alt('b'),
159            Action::Move(Motion::BackwardSexp),
160            "backward sexp",
161        );
162        nm(
163            &mut m,
164            Key::Alt('u'),
165            Action::Move(Motion::UpList),
166            "up list",
167        );
168        nm(
169            &mut m,
170            Key::Alt('d'),
171            Action::Move(Motion::DownList),
172            "down list",
173        );
174        // Mode changes.
175        nm(
176            &mut m,
177            Key::Char('i'),
178            Action::ChangeMode(Mode::Insert),
179            "insert",
180        );
181        nm(
182            &mut m,
183            Key::Char('v'),
184            Action::ChangeMode(Mode::Visual),
185            "visual",
186        );
187        nm(
188            &mut m,
189            Key::Char('V'),
190            Action::ChangeMode(Mode::VisualLine),
191            "visual line",
192        );
193        nm(
194            &mut m,
195            Key::Char(':'),
196            Action::ChangeMode(Mode::Command),
197            "command",
198        );
199        nm(&mut m, Key::Char('u'), Action::Undo, "undo");
200        nm(&mut m, Key::Ctrl('r'), Action::Redo, "redo");
201        // Insert → Normal on Esc.
202        m.bind(
203            Mode::Insert,
204            Key::Esc,
205            Action::ChangeMode(Mode::Normal),
206            "to normal",
207        );
208        m.bind(
209            Mode::Command,
210            Key::Esc,
211            Action::ChangeMode(Mode::Normal),
212            "abort",
213        );
214        m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
215        m.bind(
216            Mode::Visual,
217            Key::Esc,
218            Action::ChangeMode(Mode::Normal),
219            "to normal",
220        );
221        m.bind(
222            Mode::VisualLine,
223            Key::Esc,
224            Action::ChangeMode(Mode::Normal),
225            "to normal",
226        );
227        m
228    }
229
230    pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
231        self.bindings
232            .insert((mode, key), Binding::new(action, desc));
233    }
234
235    #[must_use]
236    pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
237        self.bindings.get(&(mode, key.clone()))
238    }
239
240    /// The leader key — what `<leader>` resolves to when a sequence
241    /// binding is applied. Defaults to `,` (blnvim parity).
242    #[must_use]
243    pub fn leader(&self) -> &Key {
244        &self.leader
245    }
246
247    /// Override the leader key. Applied before sequence bindings so
248    /// `<leader>`-prefixed specs resolve against the chosen prefix.
249    pub fn set_leader(&mut self, key: Key) {
250        self.leader = key;
251    }
252
253    /// Bind a multi-key SEQUENCE — `<leader>ff` →
254    /// `[Char(','), Char('f'), Char('f')]`, `gg` →
255    /// `[Char('g'), Char('g')]`. A length-1 sequence delegates to
256    /// [`bind`](Keymap::bind) so callers never special-case it; an
257    /// empty sequence is a no-op.
258    pub fn bind_sequence(
259        &mut self,
260        mode: Mode,
261        keys: Vec<Key>,
262        action: Action,
263        desc: impl Into<String>,
264    ) {
265        match keys.as_slice() {
266            [] => {}
267            [single] => self.bind(mode, single.clone(), action, desc),
268            _ => {
269                self.sequences
270                    .insert((mode, keys), Binding::new(action, desc));
271            }
272        }
273    }
274
275    /// Exact-match lookup for a full key sequence.
276    #[must_use]
277    pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
278        self.sequences.get(&(mode, keys.to_vec()))
279    }
280
281    /// Does any bound sequence in `mode` STRICTLY extend `prefix`
282    /// (i.e. `prefix` is a proper prefix of a longer bound sequence)?
283    /// Drives the runtime's pending-stroke state: a partial sequence
284    /// that is still a live prefix is held pending rather than
285    /// dispatched. Linear scan — fine at fleet sequence counts; a
286    /// trie is a later optimization if profiling ever asks for it.
287    #[must_use]
288    pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
289        self.sequences
290            .keys()
291            .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
292    }
293
294    /// Count of bound multi-key sequences — for `--keymap` / doctor.
295    #[must_use]
296    pub fn sequence_len(&self) -> usize {
297        self.sequences.len()
298    }
299
300    #[must_use]
301    pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
302        let mode = state.mode();
303        if mode == Mode::Normal {
304            if let Key::Char(c) = key {
305                if c.is_ascii_digit() && *c != '0' {
306                    return CountedAction::once(Action::Pending);
307                }
308                if *c == '0' && state.pending_count().is_some() {
309                    return CountedAction::once(Action::Pending);
310                }
311            }
312        }
313        if mode == Mode::Insert {
314            if let Key::Char(c) = key {
315                return CountedAction::once(Action::InsertChar(*c));
316            }
317            if matches!(key, Key::Enter) {
318                return CountedAction::once(Action::InsertChar('\n'));
319            }
320        }
321        if mode == Mode::Command {
322            if let Key::Char(c) = key {
323                return CountedAction::once(Action::InsertChar(*c));
324            }
325        }
326        if let Some(b) = self.lookup(mode, key) {
327            return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
328        }
329        CountedAction::once(Action::Pending)
330    }
331
332    #[must_use]
333    pub fn len(&self) -> usize {
334        self.bindings.len()
335    }
336
337    #[must_use]
338    pub fn is_empty(&self) -> bool {
339        self.bindings.is_empty()
340    }
341
342    /// Sorted view over every binding — for `escriba --keymap` and palettes.
343    #[must_use]
344    pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
345        let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
346        v.sort_by(|a, b| {
347            (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
348        });
349        v
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn default_vim_has_bindings() {
359        let k = Keymap::default_vim();
360        assert!(k.len() > 10);
361        assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
362        assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
363        assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
364    }
365
366    #[test]
367    fn dispatch_normal_motion() {
368        let k = Keymap::default_vim();
369        let s = ModalState::new();
370        let a = k.dispatch(&s, &Key::Char('h'));
371        assert_eq!(a.count, 1);
372        assert_eq!(a.action, Action::Move(Motion::Left));
373    }
374
375    #[test]
376    fn dispatch_count_prefix_pends() {
377        let k = Keymap::default_vim();
378        let s = ModalState::new();
379        assert!(matches!(
380            k.dispatch(&s, &Key::Char('5')).action,
381            Action::Pending
382        ));
383    }
384
385    #[test]
386    fn dispatch_insert_char() {
387        let k = Keymap::default_vim();
388        let mut s = ModalState::new();
389        s.enter(Mode::Insert);
390        let a = k.dispatch(&s, &Key::Char('a'));
391        assert_eq!(a.action, Action::InsertChar('a'));
392    }
393
394    #[test]
395    fn lisp_structural_motions_bound() {
396        let k = Keymap::default_vim();
397        assert_eq!(
398            k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
399            Action::Move(Motion::ForwardSexp)
400        );
401    }
402
403    #[test]
404    fn default_leader_is_comma() {
405        assert_eq!(Keymap::new().leader(), &Key::Char(','));
406    }
407
408    #[test]
409    fn bind_sequence_stores_multikey_and_resolves() {
410        let mut k = Keymap::new();
411        let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
412        k.bind_sequence(
413            Mode::Normal,
414            seq.clone(),
415            Action::Command {
416                name: "picker.files".into(),
417                args: vec![],
418            },
419            "find files",
420        );
421        // Exact match resolves.
422        let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
423        assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
424        // Proper prefixes are live; the full sequence is NOT a prefix
425        // of itself.
426        assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
427        assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
428        assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
429        // Wrong mode → not a prefix.
430        assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
431        assert_eq!(k.sequence_len(), 1);
432    }
433
434    #[test]
435    fn bind_sequence_length_one_delegates_to_single() {
436        let mut k = Keymap::new();
437        k.bind_sequence(
438            Mode::Normal,
439            vec![Key::Char('x')],
440            Action::Undo,
441            "x is undo",
442        );
443        // Lands in the single-key table, not the sequence table.
444        assert_eq!(k.sequence_len(), 0);
445        assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
446    }
447}