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, PartialEq, Eq)]
55pub enum Collision {
56 Reserved {
61 mode: Mode,
62 key: String,
63 description: String,
64 why: String,
66 },
67 Displaced {
74 mode: Mode,
75 key: String,
76 replaced: String,
77 with: String,
78 },
79}
80
81impl Collision {
82 #[must_use]
84 pub fn report(&self) -> String {
85 match self {
86 Self::Reserved {
87 mode,
88 key,
89 description,
90 why,
91 } => format!("{mode:?} {key} ({description}) — {why}"),
92 Self::Displaced {
93 mode,
94 key,
95 replaced,
96 with,
97 } => format!("{mode:?} {key} — \"{replaced}\" was replaced by \"{with}\""),
98 }
99 }
100
101 #[must_use]
103 pub const fn is_fatal(&self) -> bool {
104 matches!(self, Self::Reserved { .. })
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct Keymap {
110 bindings: HashMap<(Mode, Key), Binding>,
111 sequences: HashMap<(Mode, Vec<Key>), Binding>,
116 leader: Key,
118 reserved: awase::Reserved,
122 collisions: Vec<Collision>,
124}
125
126impl Default for Keymap {
127 fn default() -> Self {
128 Self {
129 bindings: HashMap::new(),
130 sequences: HashMap::new(),
131 reserved: awase::Reserved::fleet_darwin(),
132 collisions: Vec::new(),
133 leader: Key::Char(','),
136 }
137 }
138}
139
140impl Keymap {
141 #[must_use]
142 pub fn new() -> Self {
143 Self::default()
144 }
145
146 #[must_use]
147 pub fn default_vim() -> Self {
148 let mut m = Self::new();
149 let nm = |m: &mut Keymap, k: Key, a: Action, d: &'static str| m.bind(Mode::Normal, k, a, d);
150 nm(
151 &mut m,
152 Key::Char('h'),
153 Action::Move(Motion::Left),
154 "move left",
155 );
156 nm(
157 &mut m,
158 Key::Char('l'),
159 Action::Move(Motion::Right),
160 "move right",
161 );
162 nm(
163 &mut m,
164 Key::Char('j'),
165 Action::Move(Motion::Down),
166 "move down",
167 );
168 nm(&mut m, Key::Char('k'), Action::Move(Motion::Up), "move up");
169 nm(
170 &mut m,
171 Key::Char('w'),
172 Action::Move(Motion::WordStartNext),
173 "word forward",
174 );
175 nm(
176 &mut m,
177 Key::Char('b'),
178 Action::Move(Motion::WordStartPrev),
179 "word back",
180 );
181 nm(
182 &mut m,
183 Key::Char('0'),
184 Action::Move(Motion::LineStart),
185 "line start",
186 );
187 nm(
188 &mut m,
189 Key::Char('$'),
190 Action::Move(Motion::LineEnd),
191 "line end",
192 );
193 nm(
194 &mut m,
195 Key::Char('G'),
196 Action::Move(Motion::DocEnd),
197 "doc end",
198 );
199 nm(
202 &mut m,
203 Key::Char('d'),
204 Action::Operator(Operator::Delete),
205 "delete (operator)",
206 );
207 nm(
208 &mut m,
209 Key::Char('c'),
210 Action::Operator(Operator::Change),
211 "change (operator)",
212 );
213 nm(
214 &mut m,
215 Key::Char('y'),
216 Action::Operator(Operator::Yank),
217 "yank (operator)",
218 );
219 nm(
221 &mut m,
222 Key::Alt('f'),
223 Action::Move(Motion::ForwardSexp),
224 "forward sexp",
225 );
226 nm(
227 &mut m,
228 Key::Alt('b'),
229 Action::Move(Motion::BackwardSexp),
230 "backward sexp",
231 );
232 nm(
233 &mut m,
234 Key::Alt('u'),
235 Action::Move(Motion::UpList),
236 "up list",
237 );
238 nm(
239 &mut m,
240 Key::Alt('d'),
241 Action::Move(Motion::DownList),
242 "down list",
243 );
244 nm(
246 &mut m,
247 Key::Char('i'),
248 Action::ChangeMode(Mode::Insert),
249 "insert",
250 );
251 nm(
252 &mut m,
253 Key::Char('v'),
254 Action::ChangeMode(Mode::Visual),
255 "visual",
256 );
257 nm(
258 &mut m,
259 Key::Char('V'),
260 Action::ChangeMode(Mode::VisualLine),
261 "visual line",
262 );
263 nm(
264 &mut m,
265 Key::Char(':'),
266 Action::ChangeMode(Mode::Command),
267 "command",
268 );
269 nm(&mut m, Key::Char('u'), Action::Undo, "undo");
270 nm(
271 &mut m,
272 Key::Char('.'),
273 Action::RepeatLastChange,
274 "repeat last change",
275 );
276 nm(&mut m, Key::Ctrl('r'), Action::Redo, "redo");
277 m.bind(
279 Mode::Insert,
280 Key::Esc,
281 Action::ChangeMode(Mode::Normal),
282 "to normal",
283 );
284 m.bind(
285 Mode::Command,
286 Key::Esc,
287 Action::ChangeMode(Mode::Normal),
288 "abort",
289 );
290 m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
291 m.bind(
292 Mode::Command,
293 Key::Up,
294 Action::PromptHistory { back: true },
295 "older search",
296 );
297 m.bind(
298 Mode::Command,
299 Key::Down,
300 Action::PromptHistory { back: false },
301 "newer search",
302 );
303 m.bind(
304 Mode::Command,
305 Key::Backspace,
306 Action::PromptBackspace,
307 "erase one char",
308 );
309 m.bind(
310 Mode::Command,
311 Key::Delete,
312 Action::PromptDelete,
313 "delete char at caret",
314 );
315 m.bind(
319 Mode::Command,
320 Key::Left,
321 Action::PromptCaret {
322 to: CaretMove::Left,
323 },
324 "caret left",
325 );
326 m.bind(
327 Mode::Command,
328 Key::Right,
329 Action::PromptCaret {
330 to: CaretMove::Right,
331 },
332 "caret right",
333 );
334 m.bind(
335 Mode::Command,
336 Key::Home,
337 Action::PromptCaret {
338 to: CaretMove::Start,
339 },
340 "caret to start",
341 );
342 m.bind(
343 Mode::Command,
344 Key::End,
345 Action::PromptCaret { to: CaretMove::End },
346 "caret to end",
347 );
348 m.bind(
349 Mode::Command,
350 Key::Ctrl('w'),
351 Action::PromptDeleteWord,
352 "delete word before caret",
353 );
354 m.bind(
357 Mode::Command,
358 Key::Ctrl('g'),
359 Action::SearchPreviewStep { forward: true },
360 "preview next match",
361 );
362 m.bind(
363 Mode::Command,
364 Key::Ctrl('t'),
365 Action::SearchPreviewStep { forward: false },
366 "preview previous match",
367 );
368 m.bind(
369 Mode::Command,
370 Key::Ctrl('u'),
371 Action::PromptClearToStart,
372 "clear to start",
373 );
374
375 nm(
380 &mut m,
381 Key::Char('/'),
382 Action::SearchOpen(SearchDirection::Forward),
383 "search forward",
384 );
385 nm(
386 &mut m,
387 Key::Char('?'),
388 Action::SearchOpen(SearchDirection::Backward),
389 "search backward",
390 );
391 nm(
398 &mut m,
399 Key::Char('n'),
400 Action::Move(Motion::SearchNext),
401 "next match",
402 );
403 nm(
404 &mut m,
405 Key::Char('N'),
406 Action::Move(Motion::SearchPrev),
407 "previous match",
408 );
409
410 m.bind_sequence(
413 Mode::Normal,
414 vec![Key::Char('g'), Key::Char('n')],
415 Action::TextObject(TextObject::NextMatch),
416 "next match (object)",
417 );
418 m.bind_sequence(
419 Mode::Normal,
420 vec![Key::Char('g'), Key::Char('N')],
421 Action::TextObject(TextObject::PrevMatch),
422 "previous match (object)",
423 );
424
425 nm(&mut m, Key::Ctrl('o'), Action::JumpBack, "jump back");
429 nm(&mut m, Key::Ctrl('i'), Action::JumpForward, "jump forward");
430 nm(
431 &mut m,
432 Key::Char('*'),
433 Action::SearchWord { reverse: false },
434 "search word forward",
435 );
436 nm(
437 &mut m,
438 Key::Char('#'),
439 Action::SearchWord { reverse: true },
440 "search word backward",
441 );
442 m.bind(
443 Mode::Visual,
444 Key::Esc,
445 Action::ChangeMode(Mode::Normal),
446 "to normal",
447 );
448 m.bind(
449 Mode::VisualLine,
450 Key::Esc,
451 Action::ChangeMode(Mode::Normal),
452 "to normal",
453 );
454 m
455 }
456
457 pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
458 let binding = Binding::new(action, desc);
459 self.note_collisions(mode, std::slice::from_ref(&key), &binding);
460 self.bindings.insert((mode, key), binding);
461 }
462
463 fn note_collisions(&mut self, mode: Mode, keys: &[Key], binding: &Binding) {
469 let Some(first) = keys.first() else { return };
470 let spelled = format!("{keys:?}");
471
472 if let Some(hk) = to_hotkey(first) {
475 if let Some(why) = self.reserved.refuse(&hk) {
476 self.collisions.push(Collision::Reserved {
477 mode,
478 key: spelled.clone(),
479 description: binding.description.clone(),
480 why,
481 });
482 }
483 }
484
485 let existing = if keys.len() == 1 {
489 self.bindings
490 .get(&(mode, first.clone()))
491 .map(|b| &b.description)
492 } else {
493 self.sequences
494 .get(&(mode, keys.to_vec()))
495 .map(|b| &b.description)
496 };
497 if let Some(replaced) = existing {
498 self.collisions.push(Collision::Displaced {
499 mode,
500 key: spelled,
501 replaced: replaced.clone(),
502 with: binding.description.clone(),
503 });
504 }
505 }
506
507 #[must_use]
512 pub fn collisions(&self) -> &[Collision] {
513 &self.collisions
514 }
515
516 pub fn fatal_collisions(&self) -> impl Iterator<Item = &Collision> {
519 self.collisions.iter().filter(|c| c.is_fatal())
520 }
521
522 #[must_use]
523 pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
524 self.bindings.get(&(mode, key.clone()))
525 }
526
527 #[must_use]
530 pub fn leader(&self) -> &Key {
531 &self.leader
532 }
533
534 pub fn set_leader(&mut self, key: Key) {
537 self.leader = key;
538 }
539
540 pub fn bind_sequence(
546 &mut self,
547 mode: Mode,
548 keys: Vec<Key>,
549 action: Action,
550 desc: impl Into<String>,
551 ) {
552 match keys.as_slice() {
553 [] => {}
554 [single] => self.bind(mode, single.clone(), action, desc),
555 _ => {
556 let binding = Binding::new(action, desc);
557 self.note_collisions(mode, &keys, &binding);
558 self.sequences.insert((mode, keys), binding);
559 }
560 }
561 }
562
563 #[must_use]
565 pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
566 self.sequences.get(&(mode, keys.to_vec()))
567 }
568
569 #[must_use]
576 pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
577 self.sequences
578 .keys()
579 .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
580 }
581
582 #[must_use]
598 pub fn sequences_extending(&self, mode: Mode, prefix: &[Key]) -> Vec<(&[Key], &Binding)> {
599 let mut v: Vec<(&[Key], &Binding)> = self
600 .sequences
601 .iter()
602 .filter(|((m, seq), _)| {
603 *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix)
604 })
605 .map(|((_, seq), b)| (seq.as_slice(), b))
606 .collect();
607 v.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
610 v
611 }
612
613 #[must_use]
615 pub fn sequence_len(&self) -> usize {
616 self.sequences.len()
617 }
618
619 #[must_use]
620 pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
621 let mode = state.mode();
622 if mode == Mode::Normal {
623 if let Key::Char(c) = key {
624 if c.is_ascii_digit() && *c != '0' {
625 return CountedAction::once(Action::Pending);
626 }
627 if *c == '0' && state.pending_count().is_some() {
628 return CountedAction::once(Action::Pending);
629 }
630 }
631 }
632 if mode == Mode::Insert {
633 if let Key::Char(c) = key {
634 return CountedAction::once(Action::InsertChar(*c));
635 }
636 if matches!(key, Key::Enter) {
637 return CountedAction::once(Action::InsertChar('\n'));
638 }
639 }
640 if mode == Mode::Command {
641 if let Key::Char(c) = key {
642 return CountedAction::once(Action::InsertChar(*c));
643 }
644 }
645 if let Some(b) = self.lookup(mode, key) {
646 return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
647 }
648 CountedAction::once(Action::Pending)
649 }
650
651 #[must_use]
652 pub fn len(&self) -> usize {
653 self.bindings.len()
654 }
655
656 #[must_use]
657 pub fn is_empty(&self) -> bool {
658 self.bindings.is_empty()
659 }
660
661 #[must_use]
663 pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
664 let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
665 v.sort_by(|a, b| {
666 (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
667 });
668 v
669 }
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 #[test]
677 fn default_vim_has_bindings() {
678 let k = Keymap::default_vim();
679 assert!(k.len() > 10);
680 assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
681 assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
682 assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
683 }
684
685 #[test]
686 fn dispatch_normal_motion() {
687 let k = Keymap::default_vim();
688 let s = ModalState::new();
689 let a = k.dispatch(&s, &Key::Char('h'));
690 assert_eq!(a.count, 1);
691 assert_eq!(a.action, Action::Move(Motion::Left));
692 }
693
694 #[test]
695 fn dispatch_count_prefix_pends() {
696 let k = Keymap::default_vim();
697 let s = ModalState::new();
698 assert!(matches!(
699 k.dispatch(&s, &Key::Char('5')).action,
700 Action::Pending
701 ));
702 }
703
704 #[test]
705 fn dispatch_insert_char() {
706 let k = Keymap::default_vim();
707 let mut s = ModalState::new();
708 s.enter(Mode::Insert);
709 let a = k.dispatch(&s, &Key::Char('a'));
710 assert_eq!(a.action, Action::InsertChar('a'));
711 }
712
713 #[test]
714 fn lisp_structural_motions_bound() {
715 let k = Keymap::default_vim();
716 assert_eq!(
717 k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
718 Action::Move(Motion::ForwardSexp)
719 );
720 }
721
722 #[test]
723 fn default_leader_is_comma() {
724 assert_eq!(Keymap::new().leader(), &Key::Char(','));
725 }
726
727 #[test]
728 fn bind_sequence_stores_multikey_and_resolves() {
729 let mut k = Keymap::new();
730 let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
731 k.bind_sequence(
732 Mode::Normal,
733 seq.clone(),
734 Action::Command {
735 name: "picker.files".into(),
736 args: vec![],
737 },
738 "find files",
739 );
740 let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
742 assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
743 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
746 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
747 assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
748 assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
750 assert_eq!(k.sequence_len(), 1);
751 }
752
753 #[test]
754 fn bind_sequence_length_one_delegates_to_single() {
755 let mut k = Keymap::new();
756 k.bind_sequence(
757 Mode::Normal,
758 vec![Key::Char('x')],
759 Action::Undo,
760 "x is undo",
761 );
762 assert_eq!(k.sequence_len(), 0);
764 assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
765 }
766}
767
768#[must_use]
789pub fn to_hotkey(key: &Key) -> Option<awase::Hotkey> {
790 use awase::{Hotkey, Key as AK, Modifiers as M};
791 let named = |c: char| match c {
795 ' ' => Some(AK::Space),
796 c => AK::from_name(&c.to_ascii_lowercase().to_string()),
797 };
798 Some(match key {
799 Key::Char(c) => Hotkey::new(M::NONE, named(*c)?),
800 Key::Ctrl(c) => Hotkey::new(M::CTRL, named(*c)?),
801 Key::Alt(c) => Hotkey::new(M::ALT, named(*c)?),
802 Key::Esc => Hotkey::new(M::NONE, AK::Escape),
803 Key::Enter => Hotkey::new(M::NONE, AK::Return),
804 Key::Tab => Hotkey::new(M::NONE, AK::Tab),
805 Key::Backspace => Hotkey::new(M::NONE, AK::Backspace),
806 Key::Delete => Hotkey::new(M::NONE, AK::Delete),
807 Key::Left => Hotkey::new(M::NONE, AK::Left),
808 Key::Right => Hotkey::new(M::NONE, AK::Right),
809 Key::Up => Hotkey::new(M::NONE, AK::Up),
810 Key::Down => Hotkey::new(M::NONE, AK::Down),
811 Key::PageUp => Hotkey::new(M::NONE, AK::PageUp),
812 Key::PageDown => Hotkey::new(M::NONE, AK::PageDown),
813 Key::Home => Hotkey::new(M::NONE, AK::Home),
814 Key::End => Hotkey::new(M::NONE, AK::End),
815 })
816}
817
818#[cfg(test)]
819mod fleet_vocabulary {
820 use super::*;
821
822 #[test]
823 fn modifiers_survive_the_conversion() {
824 let h = to_hotkey(&Key::Ctrl('w')).expect("ctrl+w maps");
825 assert!(h.modifiers.contains(awase::Modifiers::CTRL));
826 assert_eq!(h.key, awase::Key::W);
827 }
828
829 #[test]
830 fn named_keys_map_to_their_fleet_spelling() {
831 assert_eq!(
835 to_hotkey(&Key::Esc).map(|h| h.key),
836 Some(awase::Key::Escape)
837 );
838 assert_eq!(
839 to_hotkey(&Key::Enter).map(|h| h.key),
840 Some(awase::Key::Return)
841 );
842 }
843
844 #[test]
845 fn every_variant_of_escribas_key_has_a_fleet_spelling() {
846 let all = [
850 Key::Char('a'),
851 Key::Ctrl('a'),
852 Key::Alt('a'),
853 Key::Esc,
854 Key::Enter,
855 Key::Tab,
856 Key::Backspace,
857 Key::Delete,
858 Key::Left,
859 Key::Right,
860 Key::Up,
861 Key::Down,
862 Key::PageUp,
863 Key::PageDown,
864 Key::Home,
865 Key::End,
866 ];
867 for k in all {
868 assert!(to_hotkey(&k).is_some(), "{k:?} has no fleet spelling");
869 }
870 }
871
872 #[test]
873 fn sequences_can_now_be_enumerated() {
874 let k = Keymap::default_vim();
878 let all = k.sequences_extending(Mode::Normal, &[]);
879 assert!(!all.is_empty(), "the default keymap binds sequences");
880 let g = k.sequences_extending(Mode::Normal, &[Key::Char('g')]);
881 assert!(
882 g.iter()
883 .all(|(seq, _)| seq.first() == Some(&Key::Char('g'))),
884 "a prefix query returns only its own continuations",
885 );
886 }
887}
888
889#[cfg(test)]
890mod collision_detection {
891 use super::*;
892
893 #[test]
894 fn a_reserved_chord_is_recorded_at_bind_time() {
895 let mut m = Keymap::new();
897 m.bind(Mode::Normal, Key::Alt('j'), Action::Undo, "focus down?");
898 let c = m.collisions();
899 assert_eq!(c.len(), 1, "{c:?}");
900 assert!(c[0].is_fatal(), "a chord the world owns can never fire");
901 assert!(
902 c[0].report().contains("window manager"),
903 "{}",
904 c[0].report()
905 );
906 }
907
908 #[test]
909 fn a_displaced_binding_is_recorded_but_not_fatal() {
910 let mut m = Keymap::new();
914 m.bind(Mode::Normal, Key::Char('x'), Action::Undo, "first");
915 m.bind(Mode::Normal, Key::Char('x'), Action::Redo, "second");
916 let c = m.collisions();
917 assert_eq!(c.len(), 1);
918 assert!(!c[0].is_fatal());
919 let r = c[0].report();
920 assert!(r.contains("first") && r.contains("second"), "{r}");
921 }
922
923 #[test]
924 #[allow(non_snake_case)]
926 fn a_sequence_whose_OPENER_is_reserved_is_caught() {
927 let mut m = Keymap::new();
930 m.bind_sequence(
931 Mode::Normal,
932 vec![Key::Alt('j'), Key::Char('x')],
933 Action::Undo,
934 "dead sequence",
935 );
936 assert!(m.fatal_collisions().count() == 1, "{:?}", m.collisions(),);
937 }
938
939 #[test]
940 fn an_ordinary_keymap_records_nothing() {
941 let mut m = Keymap::new();
944 m.bind(Mode::Normal, Key::Char('h'), Action::Undo, "left");
945 m.bind(Mode::Normal, Key::Ctrl('w'), Action::Redo, "window prefix");
946 assert!(m.collisions().is_empty(), "{:?}", m.collisions());
947 }
948
949 #[test]
950 fn the_shipped_default_keymap_is_clean() {
951 let m = Keymap::default_vim();
952 assert!(
953 m.collisions().is_empty(),
954 "escriba's own defaults must not collide:\n {}",
955 m.collisions()
956 .iter()
957 .map(Collision::report)
958 .collect::<Vec<_>>()
959 .join("\n "),
960 );
961 }
962}