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(
309 Mode::Insert,
310 Key::Backspace,
311 Action::Backspace,
312 "erase one char",
313 );
314 m.bind(
315 Mode::Insert,
316 Key::Delete,
317 Action::DeleteForward,
318 "delete char at caret",
319 );
320 for (key, motion, label) in [
325 (Key::Left, Motion::Left, "caret left"),
326 (Key::Right, Motion::Right, "caret right"),
327 (Key::Up, Motion::Up, "caret up"),
328 (Key::Down, Motion::Down, "caret down"),
329 (Key::Home, Motion::LineStart, "caret to line start"),
330 (Key::End, Motion::LineEnd, "caret to line end"),
331 ] {
332 m.bind(Mode::Insert, key, Action::Move(motion), label);
333 }
334 m.bind(
335 Mode::Command,
336 Key::Esc,
337 Action::ChangeMode(Mode::Normal),
338 "abort",
339 );
340 m.bind(Mode::Command, Key::Enter, Action::SubmitCommand, "submit");
341 m.bind(
342 Mode::Command,
343 Key::Up,
344 Action::PromptHistory { back: true },
345 "older search",
346 );
347 m.bind(
348 Mode::Command,
349 Key::Down,
350 Action::PromptHistory { back: false },
351 "newer search",
352 );
353 m.bind(
354 Mode::Command,
355 Key::Backspace,
356 Action::Backspace,
357 "erase one char",
358 );
359 m.bind(
360 Mode::Command,
361 Key::Delete,
362 Action::DeleteForward,
363 "delete char at caret",
364 );
365 m.bind(
369 Mode::Command,
370 Key::Left,
371 Action::PromptCaret {
372 to: CaretMove::Left,
373 },
374 "caret left",
375 );
376 m.bind(
377 Mode::Command,
378 Key::Right,
379 Action::PromptCaret {
380 to: CaretMove::Right,
381 },
382 "caret right",
383 );
384 m.bind(
385 Mode::Command,
386 Key::Home,
387 Action::PromptCaret {
388 to: CaretMove::Start,
389 },
390 "caret to start",
391 );
392 m.bind(
393 Mode::Command,
394 Key::End,
395 Action::PromptCaret { to: CaretMove::End },
396 "caret to end",
397 );
398 m.bind(
399 Mode::Command,
400 Key::Ctrl('w'),
401 Action::PromptDeleteWord,
402 "delete word before caret",
403 );
404 m.bind(
407 Mode::Command,
408 Key::Ctrl('g'),
409 Action::SearchPreviewStep { forward: true },
410 "preview next match",
411 );
412 m.bind(
413 Mode::Command,
414 Key::Ctrl('t'),
415 Action::SearchPreviewStep { forward: false },
416 "preview previous match",
417 );
418 m.bind(
419 Mode::Command,
420 Key::Ctrl('u'),
421 Action::PromptClearToStart,
422 "clear to start",
423 );
424
425 nm(
430 &mut m,
431 Key::Char('/'),
432 Action::SearchOpen(SearchDirection::Forward),
433 "search forward",
434 );
435 nm(
436 &mut m,
437 Key::Char('?'),
438 Action::SearchOpen(SearchDirection::Backward),
439 "search backward",
440 );
441 nm(
448 &mut m,
449 Key::Char('n'),
450 Action::Move(Motion::SearchNext),
451 "next match",
452 );
453 nm(
454 &mut m,
455 Key::Char('N'),
456 Action::Move(Motion::SearchPrev),
457 "previous match",
458 );
459
460 m.bind_sequence(
463 Mode::Normal,
464 vec![Key::Char('g'), Key::Char('n')],
465 Action::TextObject(TextObject::NextMatch),
466 "next match (object)",
467 );
468 m.bind_sequence(
469 Mode::Normal,
470 vec![Key::Char('g'), Key::Char('N')],
471 Action::TextObject(TextObject::PrevMatch),
472 "previous match (object)",
473 );
474
475 nm(&mut m, Key::Ctrl('o'), Action::JumpBack, "jump back");
479 nm(&mut m, Key::Ctrl('i'), Action::JumpForward, "jump forward");
480 nm(
481 &mut m,
482 Key::Char('*'),
483 Action::SearchWord { reverse: false },
484 "search word forward",
485 );
486 nm(
487 &mut m,
488 Key::Char('#'),
489 Action::SearchWord { reverse: true },
490 "search word backward",
491 );
492 m.bind(
493 Mode::Visual,
494 Key::Esc,
495 Action::ChangeMode(Mode::Normal),
496 "to normal",
497 );
498 m.bind(
499 Mode::VisualLine,
500 Key::Esc,
501 Action::ChangeMode(Mode::Normal),
502 "to normal",
503 );
504 m
505 }
506
507 pub fn bind(&mut self, mode: Mode, key: Key, action: Action, desc: impl Into<String>) {
508 let binding = Binding::new(action, desc);
509 self.note_collisions(mode, std::slice::from_ref(&key), &binding);
510 self.bindings.insert((mode, key), binding);
511 }
512
513 fn note_collisions(&mut self, mode: Mode, keys: &[Key], binding: &Binding) {
519 let Some(first) = keys.first() else { return };
520 let spelled = format!("{keys:?}");
521
522 if let Some(hk) = to_hotkey(first) {
525 if let Some(why) = self.reserved.refuse(&hk) {
526 self.collisions.push(Collision::Reserved {
527 mode,
528 key: spelled.clone(),
529 description: binding.description.clone(),
530 why,
531 });
532 }
533 }
534
535 let existing = if keys.len() == 1 {
539 self.bindings
540 .get(&(mode, first.clone()))
541 .map(|b| &b.description)
542 } else {
543 self.sequences
544 .get(&(mode, keys.to_vec()))
545 .map(|b| &b.description)
546 };
547 if let Some(replaced) = existing {
548 self.collisions.push(Collision::Displaced {
549 mode,
550 key: spelled,
551 replaced: replaced.clone(),
552 with: binding.description.clone(),
553 });
554 }
555 }
556
557 #[must_use]
562 pub fn collisions(&self) -> &[Collision] {
563 &self.collisions
564 }
565
566 pub fn fatal_collisions(&self) -> impl Iterator<Item = &Collision> {
569 self.collisions.iter().filter(|c| c.is_fatal())
570 }
571
572 #[must_use]
573 pub fn lookup(&self, mode: Mode, key: &Key) -> Option<&Binding> {
574 self.bindings.get(&(mode, key.clone()))
575 }
576
577 #[must_use]
580 pub fn leader(&self) -> &Key {
581 &self.leader
582 }
583
584 pub fn set_leader(&mut self, key: Key) {
587 self.leader = key;
588 }
589
590 pub fn bind_sequence(
596 &mut self,
597 mode: Mode,
598 keys: Vec<Key>,
599 action: Action,
600 desc: impl Into<String>,
601 ) {
602 match keys.as_slice() {
603 [] => {}
604 [single] => self.bind(mode, single.clone(), action, desc),
605 _ => {
606 let binding = Binding::new(action, desc);
607 self.note_collisions(mode, &keys, &binding);
608 self.sequences.insert((mode, keys), binding);
609 }
610 }
611 }
612
613 #[must_use]
615 pub fn lookup_sequence(&self, mode: Mode, keys: &[Key]) -> Option<&Binding> {
616 self.sequences.get(&(mode, keys.to_vec()))
617 }
618
619 #[must_use]
626 pub fn is_sequence_prefix(&self, mode: Mode, prefix: &[Key]) -> bool {
627 self.sequences
628 .keys()
629 .any(|(m, seq)| *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix))
630 }
631
632 #[must_use]
648 pub fn sequences_extending(&self, mode: Mode, prefix: &[Key]) -> Vec<(&[Key], &Binding)> {
649 let mut v: Vec<(&[Key], &Binding)> = self
650 .sequences
651 .iter()
652 .filter(|((m, seq), _)| {
653 *m == mode && seq.len() > prefix.len() && seq.starts_with(prefix)
654 })
655 .map(|((_, seq), b)| (seq.as_slice(), b))
656 .collect();
657 v.sort_by(|a, b| format!("{:?}", a.0).cmp(&format!("{:?}", b.0)));
660 v
661 }
662
663 #[must_use]
665 pub fn sequence_len(&self) -> usize {
666 self.sequences.len()
667 }
668
669 #[must_use]
670 pub fn dispatch(&self, state: &ModalState, key: &Key) -> CountedAction {
671 let mode = state.mode();
672 if mode == Mode::Normal {
673 if let Key::Char(c) = key {
674 if c.is_ascii_digit() && *c != '0' {
675 return CountedAction::once(Action::Pending);
676 }
677 if *c == '0' && state.pending_count().is_some() {
678 return CountedAction::once(Action::Pending);
679 }
680 }
681 }
682 if mode == Mode::Insert {
683 if let Key::Char(c) = key {
684 return CountedAction::once(Action::InsertChar(*c));
685 }
686 if matches!(key, Key::Enter) {
687 return CountedAction::once(Action::InsertChar('\n'));
688 }
689 }
690 if mode == Mode::Command {
691 if let Key::Char(c) = key {
692 return CountedAction::once(Action::InsertChar(*c));
693 }
694 }
695 if let Some(b) = self.lookup(mode, key) {
696 return CountedAction::repeated(state.pending_count().unwrap_or(1), b.action.clone());
697 }
698 CountedAction::once(Action::Pending)
699 }
700
701 #[must_use]
702 pub fn len(&self) -> usize {
703 self.bindings.len()
704 }
705
706 #[must_use]
707 pub fn is_empty(&self) -> bool {
708 self.bindings.is_empty()
709 }
710
711 #[must_use]
713 pub fn entries_sorted(&self) -> Vec<(&Mode, &Key, &Binding)> {
714 let mut v: Vec<_> = self.bindings.iter().map(|((m, k), b)| (m, k, b)).collect();
715 v.sort_by(|a, b| {
716 (a.0.as_str(), format!("{:?}", a.1)).cmp(&(b.0.as_str(), format!("{:?}", b.1)))
717 });
718 v
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn default_vim_has_bindings() {
728 let k = Keymap::default_vim();
729 assert!(k.len() > 10);
730 assert!(k.lookup(Mode::Normal, &Key::Char('h')).is_some());
731 assert!(k.lookup(Mode::Insert, &Key::Esc).is_some());
732 assert!(k.lookup(Mode::Normal, &Key::Alt('f')).is_some());
733 }
734
735 #[test]
736 fn dispatch_normal_motion() {
737 let k = Keymap::default_vim();
738 let s = ModalState::new();
739 let a = k.dispatch(&s, &Key::Char('h'));
740 assert_eq!(a.count, 1);
741 assert_eq!(a.action, Action::Move(Motion::Left));
742 }
743
744 #[test]
745 fn dispatch_count_prefix_pends() {
746 let k = Keymap::default_vim();
747 let s = ModalState::new();
748 assert!(matches!(
749 k.dispatch(&s, &Key::Char('5')).action,
750 Action::Pending
751 ));
752 }
753
754 #[test]
755 fn dispatch_insert_char() {
756 let k = Keymap::default_vim();
757 let mut s = ModalState::new();
758 s.enter(Mode::Insert);
759 let a = k.dispatch(&s, &Key::Char('a'));
760 assert_eq!(a.action, Action::InsertChar('a'));
761 }
762
763 #[test]
764 fn lisp_structural_motions_bound() {
765 let k = Keymap::default_vim();
766 assert_eq!(
767 k.lookup(Mode::Normal, &Key::Alt('f')).unwrap().action,
768 Action::Move(Motion::ForwardSexp)
769 );
770 }
771
772 #[test]
773 fn default_leader_is_comma() {
774 assert_eq!(Keymap::new().leader(), &Key::Char(','));
775 }
776
777 #[test]
778 fn bind_sequence_stores_multikey_and_resolves() {
779 let mut k = Keymap::new();
780 let seq = vec![Key::Char(','), Key::Char('f'), Key::Char('f')];
781 k.bind_sequence(
782 Mode::Normal,
783 seq.clone(),
784 Action::Command {
785 name: "picker.files".into(),
786 args: vec![],
787 },
788 "find files",
789 );
790 let b = k.lookup_sequence(Mode::Normal, &seq).expect("seq bound");
792 assert!(matches!(&b.action, Action::Command { name, .. } if name == "picker.files"));
793 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(',')]));
796 assert!(k.is_sequence_prefix(Mode::Normal, &[Key::Char(','), Key::Char('f')]));
797 assert!(!k.is_sequence_prefix(Mode::Normal, &seq));
798 assert!(!k.is_sequence_prefix(Mode::Insert, &[Key::Char(',')]));
800 assert_eq!(k.sequence_len(), 1);
801 }
802
803 #[test]
804 fn bind_sequence_length_one_delegates_to_single() {
805 let mut k = Keymap::new();
806 k.bind_sequence(
807 Mode::Normal,
808 vec![Key::Char('x')],
809 Action::Undo,
810 "x is undo",
811 );
812 assert_eq!(k.sequence_len(), 0);
814 assert!(k.lookup(Mode::Normal, &Key::Char('x')).is_some());
815 }
816}
817
818#[must_use]
839pub fn to_hotkey(key: &Key) -> Option<awase::Hotkey> {
840 use awase::{Hotkey, Key as AK, Modifiers as M};
841 let named = |c: char| match c {
845 ' ' => Some(AK::Space),
846 c => AK::from_name(&c.to_ascii_lowercase().to_string()),
847 };
848 Some(match key {
849 Key::Char(c) => Hotkey::new(M::NONE, named(*c)?),
850 Key::Ctrl(c) => Hotkey::new(M::CTRL, named(*c)?),
851 Key::Alt(c) => Hotkey::new(M::ALT, named(*c)?),
852 Key::F(n) => Hotkey::new(M::NONE, AK::from_name(&format!("f{n}"))?),
853 Key::Chord(h) => *h,
855 Key::Esc => Hotkey::new(M::NONE, AK::Escape),
856 Key::Enter => Hotkey::new(M::NONE, AK::Return),
857 Key::Tab => Hotkey::new(M::NONE, AK::Tab),
858 Key::Backspace => Hotkey::new(M::NONE, AK::Backspace),
859 Key::Delete => Hotkey::new(M::NONE, AK::Delete),
860 Key::Left => Hotkey::new(M::NONE, AK::Left),
861 Key::Right => Hotkey::new(M::NONE, AK::Right),
862 Key::Up => Hotkey::new(M::NONE, AK::Up),
863 Key::Down => Hotkey::new(M::NONE, AK::Down),
864 Key::PageUp => Hotkey::new(M::NONE, AK::PageUp),
865 Key::PageDown => Hotkey::new(M::NONE, AK::PageDown),
866 Key::Home => Hotkey::new(M::NONE, AK::Home),
867 Key::End => Hotkey::new(M::NONE, AK::End),
868 })
869}
870
871#[cfg(test)]
872mod fleet_vocabulary {
873 use super::*;
874
875 #[test]
876 fn modifiers_survive_the_conversion() {
877 let h = to_hotkey(&Key::Ctrl('w')).expect("ctrl+w maps");
878 assert!(h.modifiers.contains(awase::Modifiers::CTRL));
879 assert_eq!(h.key, awase::Key::W);
880 }
881
882 #[test]
883 fn named_keys_map_to_their_fleet_spelling() {
884 assert_eq!(
888 to_hotkey(&Key::Esc).map(|h| h.key),
889 Some(awase::Key::Escape)
890 );
891 assert_eq!(
892 to_hotkey(&Key::Enter).map(|h| h.key),
893 Some(awase::Key::Return)
894 );
895 }
896
897 #[test]
898 fn every_variant_of_escribas_key_has_a_fleet_spelling() {
899 let all = [
903 Key::Char('a'),
904 Key::Ctrl('a'),
905 Key::Alt('a'),
906 Key::Esc,
907 Key::Enter,
908 Key::Tab,
909 Key::Backspace,
910 Key::Delete,
911 Key::Left,
912 Key::Right,
913 Key::Up,
914 Key::Down,
915 Key::PageUp,
916 Key::PageDown,
917 Key::Home,
918 Key::End,
919 ];
920 for k in all {
921 assert!(to_hotkey(&k).is_some(), "{k:?} has no fleet spelling");
922 }
923 }
924
925 #[test]
926 fn sequences_can_now_be_enumerated() {
927 let k = Keymap::default_vim();
931 let all = k.sequences_extending(Mode::Normal, &[]);
932 assert!(!all.is_empty(), "the default keymap binds sequences");
933 let g = k.sequences_extending(Mode::Normal, &[Key::Char('g')]);
934 assert!(
935 g.iter()
936 .all(|(seq, _)| seq.first() == Some(&Key::Char('g'))),
937 "a prefix query returns only its own continuations",
938 );
939 }
940}
941
942#[cfg(test)]
943mod collision_detection {
944 use super::*;
945
946 #[test]
947 fn a_reserved_chord_is_recorded_at_bind_time() {
948 let mut m = Keymap::new();
950 m.bind(Mode::Normal, Key::Alt('j'), Action::Undo, "focus down?");
951 let c = m.collisions();
952 assert_eq!(c.len(), 1, "{c:?}");
953 assert!(c[0].is_fatal(), "a chord the world owns can never fire");
954 assert!(
955 c[0].report().contains("window manager"),
956 "{}",
957 c[0].report()
958 );
959 }
960
961 #[test]
962 fn a_displaced_binding_is_recorded_but_not_fatal() {
963 let mut m = Keymap::new();
967 m.bind(Mode::Normal, Key::Char('x'), Action::Undo, "first");
968 m.bind(Mode::Normal, Key::Char('x'), Action::Redo, "second");
969 let c = m.collisions();
970 assert_eq!(c.len(), 1);
971 assert!(!c[0].is_fatal());
972 let r = c[0].report();
973 assert!(r.contains("first") && r.contains("second"), "{r}");
974 }
975
976 #[test]
977 #[allow(non_snake_case)]
979 fn a_sequence_whose_OPENER_is_reserved_is_caught() {
980 let mut m = Keymap::new();
983 m.bind_sequence(
984 Mode::Normal,
985 vec![Key::Alt('j'), Key::Char('x')],
986 Action::Undo,
987 "dead sequence",
988 );
989 assert!(m.fatal_collisions().count() == 1, "{:?}", m.collisions(),);
990 }
991
992 #[test]
993 fn an_ordinary_keymap_records_nothing() {
994 let mut m = Keymap::new();
997 m.bind(Mode::Normal, Key::Char('h'), Action::Undo, "left");
998 m.bind(Mode::Normal, Key::Ctrl('w'), Action::Redo, "window prefix");
999 assert!(m.collisions().is_empty(), "{:?}", m.collisions());
1000 }
1001
1002 #[test]
1003 fn the_shipped_default_keymap_is_clean() {
1004 let m = Keymap::default_vim();
1005 assert!(
1006 m.collisions().is_empty(),
1007 "escriba's own defaults must not collide:\n {}",
1008 m.collisions()
1009 .iter()
1010 .map(Collision::report)
1011 .collect::<Vec<_>>()
1012 .join("\n "),
1013 );
1014 }
1015}