1extern crate self as escriba_keymap;
4
5use escriba_search::{CaretMove, Direction as SearchDirection};
6use std::collections::HashMap;
7
8use escriba_core::{Action, CountedAction, Mode, Motion, Operator, TextObject};
9use escriba_mode::ModalState;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Key {
14 Char(char),
15 Esc,
16 Enter,
17 Tab,
18 Backspace,
19 Delete,
20 Left,
21 Right,
22 Up,
23 Down,
24 PageUp,
25 PageDown,
26 Home,
27 End,
28 Ctrl(char),
29 Alt(char),
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Binding {
34 pub action: Action,
35 pub description: String,
36}
37
38impl Binding {
39 #[must_use]
40 pub fn new(action: Action, description: impl Into<String>) -> Self {
41 Self {
42 action,
43 description: description.into(),
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct Keymap {
50 bindings: HashMap<(Mode, Key), Binding>,
51 sequences: HashMap<(Mode, Vec<Key>), Binding>,
56 leader: Key,
58}
59
60impl Default for Keymap {
61 fn default() -> Self {
62 Self {
63 bindings: HashMap::new(),
64 sequences: HashMap::new(),
65 leader: Key::Char(','),
68 }
69 }
70}
71
72impl Keymap {
73 #[must_use]
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 #[must_use]
79 pub fn default_vim() -> Self {
80 let mut m = Self::new();
81 let nm = |m: &mut Keymap, k: Key, a: Action, d: &'static str| m.bind(Mode::Normal, k, a, d);
82 nm(
83 &mut m,
84 Key::Char('h'),
85 Action::Move(Motion::Left),
86 "move left",
87 );
88 nm(
89 &mut m,
90 Key::Char('l'),
91 Action::Move(Motion::Right),
92 "move right",
93 );
94 nm(
95 &mut m,
96 Key::Char('j'),
97 Action::Move(Motion::Down),
98 "move down",
99 );
100 nm(&mut m, Key::Char('k'), Action::Move(Motion::Up), "move up");
101 nm(
102 &mut m,
103 Key::Char('w'),
104 Action::Move(Motion::WordStartNext),
105 "word forward",
106 );
107 nm(
108 &mut m,
109 Key::Char('b'),
110 Action::Move(Motion::WordStartPrev),
111 "word back",
112 );
113 nm(
114 &mut m,
115 Key::Char('0'),
116 Action::Move(Motion::LineStart),
117 "line start",
118 );
119 nm(
120 &mut m,
121 Key::Char('$'),
122 Action::Move(Motion::LineEnd),
123 "line end",
124 );
125 nm(
126 &mut m,
127 Key::Char('G'),
128 Action::Move(Motion::DocEnd),
129 "doc end",
130 );
131 nm(
134 &mut m,
135 Key::Char('d'),
136 Action::Operator(Operator::Delete),
137 "delete (operator)",
138 );
139 nm(
140 &mut m,
141 Key::Char('c'),
142 Action::Operator(Operator::Change),
143 "change (operator)",
144 );
145 nm(
146 &mut m,
147 Key::Char('y'),
148 Action::Operator(Operator::Yank),
149 "yank (operator)",
150 );
151 nm(
153 &mut m,
154 Key::Alt('f'),
155 Action::Move(Motion::ForwardSexp),
156 "forward sexp",
157 );
158 nm(
159 &mut m,
160 Key::Alt('b'),
161 Action::Move(Motion::BackwardSexp),
162 "backward sexp",
163 );
164 nm(
165 &mut m,
166 Key::Alt('u'),
167 Action::Move(Motion::UpList),
168 "up list",
169 );
170 nm(
171 &mut m,
172 Key::Alt('d'),
173 Action::Move(Motion::DownList),
174 "down list",
175 );
176 nm(
178 &mut m,
179 Key::Char('i'),
180 Action::ChangeMode(Mode::Insert),
181 "insert",
182 );
183 nm(
184 &mut m,
185 Key::Char('v'),
186 Action::ChangeMode(Mode::Visual),
187 "visual",
188 );
189 nm(
190 &mut m,
191 Key::Char('V'),
192 Action::ChangeMode(Mode::VisualLine),
193 "visual line",
194 );
195 nm(
196 &mut m,
197 Key::Char(':'),
198 Action::ChangeMode(Mode::Command),
199 "command",
200 );
201 nm(&mut m, Key::Char('u'), Action::Undo, "undo");
202 nm(
203 &mut m,
204 Key::Char('.'),
205 Action::RepeatLastChange,
206 "repeat last change",
207 );
208 nm(&mut m, Key::Ctrl('r'), Action::Redo, "redo");
209 m.bind(
211 Mode::Insert,
212 Key::Esc,
213 Action::ChangeMode(Mode::Normal),
214 "to normal",
215 );
216 m.bind(
217 Mode::Command,
218 Key::Esc,
219 Action::ChangeMode(Mode::Normal),
220 "abort",
221 );
222 m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
223 m.bind(
224 Mode::Command,
225 Key::Up,
226 Action::PromptHistory { back: true },
227 "older search",
228 );
229 m.bind(
230 Mode::Command,
231 Key::Down,
232 Action::PromptHistory { back: false },
233 "newer search",
234 );
235 m.bind(
236 Mode::Command,
237 Key::Backspace,
238 Action::PromptBackspace,
239 "erase one char",
240 );
241 m.bind(
242 Mode::Command,
243 Key::Delete,
244 Action::PromptDelete,
245 "delete char at caret",
246 );
247 m.bind(
251 Mode::Command,
252 Key::Left,
253 Action::PromptCaret {
254 to: CaretMove::Left,
255 },
256 "caret left",
257 );
258 m.bind(
259 Mode::Command,
260 Key::Right,
261 Action::PromptCaret {
262 to: CaretMove::Right,
263 },
264 "caret right",
265 );
266 m.bind(
267 Mode::Command,
268 Key::Home,
269 Action::PromptCaret {
270 to: CaretMove::Start,
271 },
272 "caret to start",
273 );
274 m.bind(
275 Mode::Command,
276 Key::End,
277 Action::PromptCaret { to: CaretMove::End },
278 "caret to end",
279 );
280 m.bind(
281 Mode::Command,
282 Key::Ctrl('w'),
283 Action::PromptDeleteWord,
284 "delete word before caret",
285 );
286 m.bind(
289 Mode::Command,
290 Key::Ctrl('g'),
291 Action::SearchPreviewStep { forward: true },
292 "preview next match",
293 );
294 m.bind(
295 Mode::Command,
296 Key::Ctrl('t'),
297 Action::SearchPreviewStep { forward: false },
298 "preview previous match",
299 );
300 m.bind(
301 Mode::Command,
302 Key::Ctrl('u'),
303 Action::PromptClearToStart,
304 "clear to start",
305 );
306
307 nm(
312 &mut m,
313 Key::Char('/'),
314 Action::SearchOpen(SearchDirection::Forward),
315 "search forward",
316 );
317 nm(
318 &mut m,
319 Key::Char('?'),
320 Action::SearchOpen(SearchDirection::Backward),
321 "search backward",
322 );
323 nm(
330 &mut m,
331 Key::Char('n'),
332 Action::Move(Motion::SearchNext),
333 "next match",
334 );
335 nm(
336 &mut m,
337 Key::Char('N'),
338 Action::Move(Motion::SearchPrev),
339 "previous match",
340 );
341
342 m.bind_sequence(
345 Mode::Normal,
346 vec![Key::Char('g'), Key::Char('n')],
347 Action::TextObject(TextObject::NextMatch),
348 "next match (object)",
349 );
350 m.bind_sequence(
351 Mode::Normal,
352 vec![Key::Char('g'), Key::Char('N')],
353 Action::TextObject(TextObject::PrevMatch),
354 "previous match (object)",
355 );
356
357 nm(&mut m, Key::Ctrl('o'), Action::JumpBack, "jump back");
361 nm(&mut m, Key::Ctrl('i'), Action::JumpForward, "jump forward");
362 nm(
363 &mut m,
364 Key::Char('*'),
365 Action::SearchWord { reverse: false },
366 "search word forward",
367 );
368 nm(
369 &mut m,
370 Key::Char('#'),
371 Action::SearchWord { reverse: true },
372 "search word backward",
373 );
374 m.bind(
375 Mode::Visual,
376 Key::Esc,
377 Action::ChangeMode(Mode::Normal),
378 "to normal",
379 );
380 m.bind(
381 Mode::VisualLine,
382 Key::Esc,
383 Action::ChangeMode(Mode::Normal),
384 "to normal",
385 );
386 m
387 }
388
389 pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
390 self.bindings
391 .insert((mode, key), Binding::new(action, desc));
392 }
393
394 #[must_use]
395 pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
396 self.bindings.get(&(mode, key.clone()))
397 }
398
399 #[must_use]
402 pub fn leader(&self) -> &Key {
403 &self.leader
404 }
405
406 pub fn set_leader(&mut self, key: Key) {
409 self.leader = key;
410 }
411
412 pub fn bind_sequence(
418 &mut self,
419 mode: Mode,
420 keys: Vec<Key>,
421 action: Action,
422 desc: impl Into<String>,
423 ) {
424 match keys.as_slice() {
425 [] => {}
426 [single] => self.bind(mode, single.clone(), action, desc),
427 _ => {
428 self.sequences
429 .insert((mode, keys), Binding::new(action, desc));
430 }
431 }
432 }
433
434 #[must_use]
436 pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
437 self.sequences.get(&(mode, keys.to_vec()))
438 }
439
440 #[must_use]
447 pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
448 self.sequences
449 .keys()
450 .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
451 }
452
453 #[must_use]
455 pub fn sequence_len(&self) -> usize {
456 self.sequences.len()
457 }
458
459 #[must_use]
460 pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
461 let mode = state.mode();
462 if mode == Mode::Normal {
463 if let Key::Char(c) = key {
464 if c.is_ascii_digit() && *c != '0' {
465 return CountedAction::once(Action::Pending);
466 }
467 if *c == '0' && state.pending_count().is_some() {
468 return CountedAction::once(Action::Pending);
469 }
470 }
471 }
472 if mode == Mode::Insert {
473 if let Key::Char(c) = key {
474 return CountedAction::once(Action::InsertChar(*c));
475 }
476 if matches!(key, Key::Enter) {
477 return CountedAction::once(Action::InsertChar('\n'));
478 }
479 }
480 if mode == Mode::Command {
481 if let Key::Char(c) = key {
482 return CountedAction::once(Action::InsertChar(*c));
483 }
484 }
485 if let Some(b) = self.lookup(mode, key) {
486 return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
487 }
488 CountedAction::once(Action::Pending)
489 }
490
491 #[must_use]
492 pub fn len(&self) -> usize {
493 self.bindings.len()
494 }
495
496 #[must_use]
497 pub fn is_empty(&self) -> bool {
498 self.bindings.is_empty()
499 }
500
501 #[must_use]
503 pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
504 let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
505 v.sort_by(|a, b| {
506 (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
507 });
508 v
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn default_vim_has_bindings() {
518 let k = Keymap::default_vim();
519 assert!(k.len() > 10);
520 assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
521 assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
522 assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
523 }
524
525 #[test]
526 fn dispatch_normal_motion() {
527 let k = Keymap::default_vim();
528 let s = ModalState::new();
529 let a = k.dispatch(&s, &Key::Char('h'));
530 assert_eq!(a.count, 1);
531 assert_eq!(a.action, Action::Move(Motion::Left));
532 }
533
534 #[test]
535 fn dispatch_count_prefix_pends() {
536 let k = Keymap::default_vim();
537 let s = ModalState::new();
538 assert!(matches!(
539 k.dispatch(&s, &Key::Char('5')).action,
540 Action::Pending
541 ));
542 }
543
544 #[test]
545 fn dispatch_insert_char() {
546 let k = Keymap::default_vim();
547 let mut s = ModalState::new();
548 s.enter(Mode::Insert);
549 let a = k.dispatch(&s, &Key::Char('a'));
550 assert_eq!(a.action, Action::InsertChar('a'));
551 }
552
553 #[test]
554 fn lisp_structural_motions_bound() {
555 let k = Keymap::default_vim();
556 assert_eq!(
557 k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
558 Action::Move(Motion::ForwardSexp)
559 );
560 }
561
562 #[test]
563 fn default_leader_is_comma() {
564 assert_eq!(Keymap::new().leader(), &Key::Char(','));
565 }
566
567 #[test]
568 fn bind_sequence_stores_multikey_and_resolves() {
569 let mut k = Keymap::new();
570 let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
571 k.bind_sequence(
572 Mode::Normal,
573 seq.clone(),
574 Action::Command {
575 name: "picker.files".into(),
576 args: vec![],
577 },
578 "find files",
579 );
580 let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
582 assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
583 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
586 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
587 assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
588 assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
590 assert_eq!(k.sequence_len(), 1);
591 }
592
593 #[test]
594 fn bind_sequence_length_one_delegates_to_single() {
595 let mut k = Keymap::new();
596 k.bind_sequence(
597 Mode::Normal,
598 vec![Key::Char('x')],
599 Action::Undo,
600 "x is undo",
601 );
602 assert_eq!(k.sequence_len(), 0);
604 assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
605 }
606}