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 F(u8),
34 Chord(awase::Hotkey),
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Binding {
50 pub action: Action,
51 pub description: String,
52}
53
54impl Binding {
55 #[must_use]
56 pub fn new(action: Action, description: impl Into<String>) -> Self {
57 Self {
58 action,
59 description: description.into(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum Collision {
72 Reserved {
77 mode: Mode,
78 key: String,
79 description: String,
80 why: String,
82 },
83 Displaced {
90 mode: Mode,
91 key: String,
92 replaced: String,
93 with: String,
94 },
95}
96
97impl Collision {
98 #[must_use]
100 pub fn report(&self) -> String {
101 match self {
102 Self::Reserved {
103 mode,
104 key,
105 description,
106 why,
107 } => format!("{mode:?} {key} ({description}) — {why}"),
108 Self::Displaced {
109 mode,
110 key,
111 replaced,
112 with,
113 } => format!("{mode:?} {key} — \"{replaced}\" was replaced by \"{with}\""),
114 }
115 }
116
117 #[must_use]
119 pub const fn is_fatal(&self) -> bool {
120 matches!(self, Self::Reserved { .. })
121 }
122}
123
124#[derive(Debug, Clone)]
125pub struct Keymap {
126 bindings: HashMap<(Mode, Key), Binding>,
127 sequences: HashMap<(Mode, Vec<Key>), Binding>,
132 leader: Key,
134 reserved: awase::Reserved,
138 collisions: Vec<Collision>,
140}
141
142impl Default for Keymap {
143 fn default() -> Self {
144 Self {
145 bindings: HashMap::new(),
146 sequences: HashMap::new(),
147 reserved: awase::Reserved::fleet_darwin(),
148 collisions: Vec::new(),
149 leader: Key::Char(','),
152 }
153 }
154}
155
156impl Keymap {
157 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 #[must_use]
163 pub fn default_vim() -> Self {
164 let mut m = Self::new();
165 let nm = |m: &mut Keymap, k: Key, a: Action, d: &'static str| m.bind(Mode::Normal, k, a, d);
166 nm(
167 &mut m,
168 Key::Char('h'),
169 Action::Move(Motion::Left),
170 "move left",
171 );
172 nm(
173 &mut m,
174 Key::Char('l'),
175 Action::Move(Motion::Right),
176 "move right",
177 );
178 nm(
179 &mut m,
180 Key::Char('j'),
181 Action::Move(Motion::Down),
182 "move down",
183 );
184 nm(&mut m, Key::Char('k'), Action::Move(Motion::Up), "move up");
185 nm(
186 &mut m,
187 Key::Char('w'),
188 Action::Move(Motion::WordStartNext),
189 "word forward",
190 );
191 nm(
192 &mut m,
193 Key::Char('b'),
194 Action::Move(Motion::WordStartPrev),
195 "word back",
196 );
197 nm(
198 &mut m,
199 Key::Char('0'),
200 Action::Move(Motion::LineStart),
201 "line start",
202 );
203 nm(
204 &mut m,
205 Key::Char('$'),
206 Action::Move(Motion::LineEnd),
207 "line end",
208 );
209 nm(
210 &mut m,
211 Key::Char('G'),
212 Action::Move(Motion::DocEnd),
213 "doc end",
214 );
215 nm(
218 &mut m,
219 Key::Char('d'),
220 Action::Operator(Operator::Delete),
221 "delete (operator)",
222 );
223 nm(
224 &mut m,
225 Key::Char('c'),
226 Action::Operator(Operator::Change),
227 "change (operator)",
228 );
229 nm(
230 &mut m,
231 Key::Char('y'),
232 Action::Operator(Operator::Yank),
233 "yank (operator)",
234 );
235 nm(
237 &mut m,
238 Key::Alt('f'),
239 Action::Move(Motion::ForwardSexp),
240 "forward sexp",
241 );
242 nm(
243 &mut m,
244 Key::Alt('b'),
245 Action::Move(Motion::BackwardSexp),
246 "backward sexp",
247 );
248 nm(
249 &mut m,
250 Key::Alt('u'),
251 Action::Move(Motion::UpList),
252 "up list",
253 );
254 nm(
255 &mut m,
256 Key::Alt('d'),
257 Action::Move(Motion::DownList),
258 "down list",
259 );
260 nm(
262 &mut m,
263 Key::Char('i'),
264 Action::ChangeMode(Mode::Insert),
265 "insert",
266 );
267 nm(
268 &mut m,
269 Key::Char('v'),
270 Action::ChangeMode(Mode::Visual),
271 "visual",
272 );
273 nm(
274 &mut m,
275 Key::Char('V'),
276 Action::ChangeMode(Mode::VisualLine),
277 "visual line",
278 );
279 nm(
280 &mut m,
281 Key::Char(':'),
282 Action::ChangeMode(Mode::Command),
283 "command",
284 );
285 nm(&mut m, Key::Char('u'), Action::Undo, "undo");
286 nm(
287 &mut m,
288 Key::Char('.'),
289 Action::RepeatLastChange,
290 "repeat last change",
291 );
292 nm(&mut m, Key::Ctrl('r'), Action::Redo, "redo");
293 m.bind(
295 Mode::Insert,
296 Key::Esc,
297 Action::ChangeMode(Mode::Normal),
298 "to normal",
299 );
300 m.bind(
301 Mode::Command,
302 Key::Esc,
303 Action::ChangeMode(Mode::Normal),
304 "abort",
305 );
306 m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
307 m.bind(
308 Mode::Command,
309 Key::Up,
310 Action::PromptHistory { back: true },
311 "older search",
312 );
313 m.bind(
314 Mode::Command,
315 Key::Down,
316 Action::PromptHistory { back: false },
317 "newer search",
318 );
319 m.bind(
320 Mode::Command,
321 Key::Backspace,
322 Action::PromptBackspace,
323 "erase one char",
324 );
325 m.bind(
326 Mode::Command,
327 Key::Delete,
328 Action::PromptDelete,
329 "delete char at caret",
330 );
331 m.bind(
335 Mode::Command,
336 Key::Left,
337 Action::PromptCaret {
338 to: CaretMove::Left,
339 },
340 "caret left",
341 );
342 m.bind(
343 Mode::Command,
344 Key::Right,
345 Action::PromptCaret {
346 to: CaretMove::Right,
347 },
348 "caret right",
349 );
350 m.bind(
351 Mode::Command,
352 Key::Home,
353 Action::PromptCaret {
354 to: CaretMove::Start,
355 },
356 "caret to start",
357 );
358 m.bind(
359 Mode::Command,
360 Key::End,
361 Action::PromptCaret { to: CaretMove::End },
362 "caret to end",
363 );
364 m.bind(
365 Mode::Command,
366 Key::Ctrl('w'),
367 Action::PromptDeleteWord,
368 "delete word before caret",
369 );
370 m.bind(
373 Mode::Command,
374 Key::Ctrl('g'),
375 Action::SearchPreviewStep { forward: true },
376 "preview next match",
377 );
378 m.bind(
379 Mode::Command,
380 Key::Ctrl('t'),
381 Action::SearchPreviewStep { forward: false },
382 "preview previous match",
383 );
384 m.bind(
385 Mode::Command,
386 Key::Ctrl('u'),
387 Action::PromptClearToStart,
388 "clear to start",
389 );
390
391 nm(
396 &mut m,
397 Key::Char('/'),
398 Action::SearchOpen(SearchDirection::Forward),
399 "search forward",
400 );
401 nm(
402 &mut m,
403 Key::Char('?'),
404 Action::SearchOpen(SearchDirection::Backward),
405 "search backward",
406 );
407 nm(
414 &mut m,
415 Key::Char('n'),
416 Action::Move(Motion::SearchNext),
417 "next match",
418 );
419 nm(
420 &mut m,
421 Key::Char('N'),
422 Action::Move(Motion::SearchPrev),
423 "previous match",
424 );
425
426 m.bind_sequence(
429 Mode::Normal,
430 vec![Key::Char('g'), Key::Char('n')],
431 Action::TextObject(TextObject::NextMatch),
432 "next match (object)",
433 );
434 m.bind_sequence(
435 Mode::Normal,
436 vec![Key::Char('g'), Key::Char('N')],
437 Action::TextObject(TextObject::PrevMatch),
438 "previous match (object)",
439 );
440
441 nm(&mut m, Key::Ctrl('o'), Action::JumpBack, "jump back");
445 nm(&mut m, Key::Ctrl('i'), Action::JumpForward, "jump forward");
446 nm(
447 &mut m,
448 Key::Char('*'),
449 Action::SearchWord { reverse: false },
450 "search word forward",
451 );
452 nm(
453 &mut m,
454 Key::Char('#'),
455 Action::SearchWord { reverse: true },
456 "search word backward",
457 );
458 m.bind(
459 Mode::Visual,
460 Key::Esc,
461 Action::ChangeMode(Mode::Normal),
462 "to normal",
463 );
464 m.bind(
465 Mode::VisualLine,
466 Key::Esc,
467 Action::ChangeMode(Mode::Normal),
468 "to normal",
469 );
470 m
471 }
472
473 pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
474 let binding = Binding::new(action, desc);
475 self.note_collisions(mode, std::slice::from_ref(&key), &binding);
476 self.bindings.insert((mode, key), binding);
477 }
478
479 fn note_collisions(&mut self, mode: Mode, keys: &[Key], binding: &Binding) {
485 let Some(first) = keys.first() else { return };
486 let spelled = format!("{keys:?}");
487
488 if let Some(hk) = to_hotkey(first) {
491 if let Some(why) = self.reserved.refuse(&hk) {
492 self.collisions.push(Collision::Reserved {
493 mode,
494 key: spelled.clone(),
495 description: binding.description.clone(),
496 why,
497 });
498 }
499 }
500
501 let existing = if keys.len() == 1 {
505 self.bindings
506 .get(&(mode, first.clone()))
507 .map(|b| &b.description)
508 } else {
509 self.sequences
510 .get(&(mode, keys.to_vec()))
511 .map(|b| &b.description)
512 };
513 if let Some(replaced) = existing {
514 self.collisions.push(Collision::Displaced {
515 mode,
516 key: spelled,
517 replaced: replaced.clone(),
518 with: binding.description.clone(),
519 });
520 }
521 }
522
523 #[must_use]
528 pub fn collisions(&self) -> &[Collision] {
529 &self.collisions
530 }
531
532 pub fn fatal_collisions(&self) -> impl Iterator<Item = &Collision> {
535 self.collisions.iter().filter(|c| c.is_fatal())
536 }
537
538 #[must_use]
539 pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
540 self.bindings.get(&(mode, key.clone()))
541 }
542
543 #[must_use]
546 pub fn leader(&self) -> &Key {
547 &self.leader
548 }
549
550 pub fn set_leader(&mut self, key: Key) {
553 self.leader = key;
554 }
555
556 pub fn bind_sequence(
562 &mut self,
563 mode: Mode,
564 keys: Vec<Key>,
565 action: Action,
566 desc: impl Into<String>,
567 ) {
568 match keys.as_slice() {
569 [] => {}
570 [single] => self.bind(mode, single.clone(), action, desc),
571 _ => {
572 let binding = Binding::new(action, desc);
573 self.note_collisions(mode, &keys, &binding);
574 self.sequences.insert((mode, keys), binding);
575 }
576 }
577 }
578
579 #[must_use]
581 pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
582 self.sequences.get(&(mode, keys.to_vec()))
583 }
584
585 #[must_use]
592 pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
593 self.sequences
594 .keys()
595 .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
596 }
597
598 #[must_use]
614 pub fn sequences_extending(&self, mode: Mode, prefix: &[Key]) -> Vec<(&[Key], &Binding)> {
615 let mut v: Vec<(&[Key], &Binding)> = self
616 .sequences
617 .iter()
618 .filter(|((m, seq), _)| {
619 *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix)
620 })
621 .map(|((_, seq), b)| (seq.as_slice(), b))
622 .collect();
623 v.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
626 v
627 }
628
629 #[must_use]
631 pub fn sequence_len(&self) -> usize {
632 self.sequences.len()
633 }
634
635 #[must_use]
636 pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
637 let mode = state.mode();
638 if mode == Mode::Normal {
639 if let Key::Char(c) = key {
640 if c.is_ascii_digit() && *c != '0' {
641 return CountedAction::once(Action::Pending);
642 }
643 if *c == '0' && state.pending_count().is_some() {
644 return CountedAction::once(Action::Pending);
645 }
646 }
647 }
648 if mode == Mode::Insert {
649 if let Key::Char(c) = key {
650 return CountedAction::once(Action::InsertChar(*c));
651 }
652 if matches!(key, Key::Enter) {
653 return CountedAction::once(Action::InsertChar('\n'));
654 }
655 }
656 if mode == Mode::Command {
657 if let Key::Char(c) = key {
658 return CountedAction::once(Action::InsertChar(*c));
659 }
660 }
661 if let Some(b) = self.lookup(mode, key) {
662 return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
663 }
664 CountedAction::once(Action::Pending)
665 }
666
667 #[must_use]
668 pub fn len(&self) -> usize {
669 self.bindings.len()
670 }
671
672 #[must_use]
673 pub fn is_empty(&self) -> bool {
674 self.bindings.is_empty()
675 }
676
677 #[must_use]
679 pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
680 let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
681 v.sort_by(|a, b| {
682 (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
683 });
684 v
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn default_vim_has_bindings() {
694 let k = Keymap::default_vim();
695 assert!(k.len() > 10);
696 assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
697 assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
698 assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
699 }
700
701 #[test]
702 fn dispatch_normal_motion() {
703 let k = Keymap::default_vim();
704 let s = ModalState::new();
705 let a = k.dispatch(&s, &Key::Char('h'));
706 assert_eq!(a.count, 1);
707 assert_eq!(a.action, Action::Move(Motion::Left));
708 }
709
710 #[test]
711 fn dispatch_count_prefix_pends() {
712 let k = Keymap::default_vim();
713 let s = ModalState::new();
714 assert!(matches!(
715 k.dispatch(&s, &Key::Char('5')).action,
716 Action::Pending
717 ));
718 }
719
720 #[test]
721 fn dispatch_insert_char() {
722 let k = Keymap::default_vim();
723 let mut s = ModalState::new();
724 s.enter(Mode::Insert);
725 let a = k.dispatch(&s, &Key::Char('a'));
726 assert_eq!(a.action, Action::InsertChar('a'));
727 }
728
729 #[test]
730 fn lisp_structural_motions_bound() {
731 let k = Keymap::default_vim();
732 assert_eq!(
733 k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
734 Action::Move(Motion::ForwardSexp)
735 );
736 }
737
738 #[test]
739 fn default_leader_is_comma() {
740 assert_eq!(Keymap::new().leader(), &Key::Char(','));
741 }
742
743 #[test]
744 fn bind_sequence_stores_multikey_and_resolves() {
745 let mut k = Keymap::new();
746 let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
747 k.bind_sequence(
748 Mode::Normal,
749 seq.clone(),
750 Action::Command {
751 name: "picker.files".into(),
752 args: vec![],
753 },
754 "find files",
755 );
756 let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
758 assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
759 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
762 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
763 assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
764 assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
766 assert_eq!(k.sequence_len(), 1);
767 }
768
769 #[test]
770 fn bind_sequence_length_one_delegates_to_single() {
771 let mut k = Keymap::new();
772 k.bind_sequence(
773 Mode::Normal,
774 vec![Key::Char('x')],
775 Action::Undo,
776 "x is undo",
777 );
778 assert_eq!(k.sequence_len(), 0);
780 assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
781 }
782}
783
784#[must_use]
805pub fn to_hotkey(key: &Key) -> Option<awase::Hotkey> {
806 use awase::{Hotkey, Key as AK, Modifiers as M};
807 let named = |c: char| match c {
811 ' ' => Some(AK::Space),
812 c => AK::from_name(&c.to_ascii_lowercase().to_string()),
813 };
814 Some(match key {
815 Key::Char(c) => Hotkey::new(M::NONE, named(*c)?),
816 Key::Ctrl(c) => Hotkey::new(M::CTRL, named(*c)?),
817 Key::Alt(c) => Hotkey::new(M::ALT, named(*c)?),
818 Key::F(n) => Hotkey::new(M::NONE, AK::from_name(&format!("f{n}"))?),
819 Key::Chord(h) => *h,
821 Key::Esc => Hotkey::new(M::NONE, AK::Escape),
822 Key::Enter => Hotkey::new(M::NONE, AK::Return),
823 Key::Tab => Hotkey::new(M::NONE, AK::Tab),
824 Key::Backspace => Hotkey::new(M::NONE, AK::Backspace),
825 Key::Delete => Hotkey::new(M::NONE, AK::Delete),
826 Key::Left => Hotkey::new(M::NONE, AK::Left),
827 Key::Right => Hotkey::new(M::NONE, AK::Right),
828 Key::Up => Hotkey::new(M::NONE, AK::Up),
829 Key::Down => Hotkey::new(M::NONE, AK::Down),
830 Key::PageUp => Hotkey::new(M::NONE, AK::PageUp),
831 Key::PageDown => Hotkey::new(M::NONE, AK::PageDown),
832 Key::Home => Hotkey::new(M::NONE, AK::Home),
833 Key::End => Hotkey::new(M::NONE, AK::End),
834 })
835}
836
837#[cfg(test)]
838mod fleet_vocabulary {
839 use super::*;
840
841 #[test]
842 fn modifiers_survive_the_conversion() {
843 let h = to_hotkey(&Key::Ctrl('w')).expect("ctrl+w maps");
844 assert!(h.modifiers.contains(awase::Modifiers::CTRL));
845 assert_eq!(h.key, awase::Key::W);
846 }
847
848 #[test]
849 fn named_keys_map_to_their_fleet_spelling() {
850 assert_eq!(
854 to_hotkey(&Key::Esc).map(|h| h.key),
855 Some(awase::Key::Escape)
856 );
857 assert_eq!(
858 to_hotkey(&Key::Enter).map(|h| h.key),
859 Some(awase::Key::Return)
860 );
861 }
862
863 #[test]
864 fn every_variant_of_escribas_key_has_a_fleet_spelling() {
865 let all = [
869 Key::Char('a'),
870 Key::Ctrl('a'),
871 Key::Alt('a'),
872 Key::Esc,
873 Key::Enter,
874 Key::Tab,
875 Key::Backspace,
876 Key::Delete,
877 Key::Left,
878 Key::Right,
879 Key::Up,
880 Key::Down,
881 Key::PageUp,
882 Key::PageDown,
883 Key::Home,
884 Key::End,
885 ];
886 for k in all {
887 assert!(to_hotkey(&k).is_some(), "{k:?} has no fleet spelling");
888 }
889 }
890
891 #[test]
892 fn sequences_can_now_be_enumerated() {
893 let k = Keymap::default_vim();
897 let all = k.sequences_extending(Mode::Normal, &[]);
898 assert!(!all.is_empty(), "the default keymap binds sequences");
899 let g = k.sequences_extending(Mode::Normal, &[Key::Char('g')]);
900 assert!(
901 g.iter()
902 .all(|(seq, _)| seq.first() == Some(&Key::Char('g'))),
903 "a prefix query returns only its own continuations",
904 );
905 }
906}
907
908#[cfg(test)]
909mod collision_detection {
910 use super::*;
911
912 #[test]
913 fn a_reserved_chord_is_recorded_at_bind_time() {
914 let mut m = Keymap::new();
916 m.bind(Mode::Normal, Key::Alt('j'), Action::Undo, "focus down?");
917 let c = m.collisions();
918 assert_eq!(c.len(), 1, "{c:?}");
919 assert!(c[0].is_fatal(), "a chord the world owns can never fire");
920 assert!(
921 c[0].report().contains("window manager"),
922 "{}",
923 c[0].report()
924 );
925 }
926
927 #[test]
928 fn a_displaced_binding_is_recorded_but_not_fatal() {
929 let mut m = Keymap::new();
933 m.bind(Mode::Normal, Key::Char('x'), Action::Undo, "first");
934 m.bind(Mode::Normal, Key::Char('x'), Action::Redo, "second");
935 let c = m.collisions();
936 assert_eq!(c.len(), 1);
937 assert!(!c[0].is_fatal());
938 let r = c[0].report();
939 assert!(r.contains("first") && r.contains("second"), "{r}");
940 }
941
942 #[test]
943 #[allow(non_snake_case)]
945 fn a_sequence_whose_OPENER_is_reserved_is_caught() {
946 let mut m = Keymap::new();
949 m.bind_sequence(
950 Mode::Normal,
951 vec![Key::Alt('j'), Key::Char('x')],
952 Action::Undo,
953 "dead sequence",
954 );
955 assert!(m.fatal_collisions().count() == 1, "{:?}", m.collisions(),);
956 }
957
958 #[test]
959 fn an_ordinary_keymap_records_nothing() {
960 let mut m = Keymap::new();
963 m.bind(Mode::Normal, Key::Char('h'), Action::Undo, "left");
964 m.bind(Mode::Normal, Key::Ctrl('w'), Action::Redo, "window prefix");
965 assert!(m.collisions().is_empty(), "{:?}", m.collisions());
966 }
967
968 #[test]
969 fn the_shipped_default_keymap_is_clean() {
970 let m = Keymap::default_vim();
971 assert!(
972 m.collisions().is_empty(),
973 "escriba's own defaults must not collide:\n {}",
974 m.collisions()
975 .iter()
976 .map(Collision::report)
977 .collect::<Vec<_>>()
978 .join("\n "),
979 );
980 }
981}