1extern 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 sequences: HashMap<(Mode, Vec<Key>), Binding>,
54 leader: Key,
56}
57
58impl Default for Keymap {
59 fn default() -> Self {
60 Self {
61 bindings: HashMap::new(),
62 sequences: HashMap::new(),
63 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 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 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 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 #[must_use]
223 pub fn leader(&self) -> &Key {
224 &self.leader
225 }
226
227 pub fn set_leader(&mut self, key: Key) {
230 self.leader = key;
231 }
232
233 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 #[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 #[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 #[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 #[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 let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
403 assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
404 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 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 assert_eq!(k.sequence_len(), 0);
425 assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
426 }
427}