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