1use crate::actions::{PageAction, PageMode};
45use crate::key::{Key, KeyChord, NamedKey};
46use crate::keymap::{Keymap, Lookup};
47use std::time::Duration;
48
49pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1000);
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Step {
55 Pending,
58 Ambiguous { timeout_at: Duration },
62 Resolved(PageAction),
65 Reject,
68 EditModeActive,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum EditModeStep {
77 PassThrough(KeyChord),
81 Exited,
83}
84
85#[derive(Debug)]
87pub struct Engine {
88 keymap: Keymap,
89 mode: PageMode,
90 return_mode: PageMode,
93 pending: Vec<KeyChord>,
94 pending_started: Option<Duration>,
97 count: u32,
100 register: Option<char>,
103 awaiting_register_char: bool,
105 timeout: Duration,
106}
107
108impl Engine {
109 pub fn new(keymap: Keymap) -> Self {
110 Self::with_timeout(keymap, DEFAULT_TIMEOUT)
111 }
112
113 pub fn with_timeout(keymap: Keymap, timeout: Duration) -> Self {
114 Self {
115 keymap,
116 mode: PageMode::Normal,
117 return_mode: PageMode::Normal,
118 pending: Vec::new(),
119 pending_started: None,
120 count: 0,
121 register: None,
122 awaiting_register_char: false,
123 timeout,
124 }
125 }
126
127 pub fn keymap(&self) -> &Keymap {
128 &self.keymap
129 }
130
131 pub fn keymap_mut(&mut self) -> &mut Keymap {
132 &mut self.keymap
133 }
134
135 pub fn set_keymap(&mut self, keymap: Keymap) {
139 self.keymap = keymap;
140 self.pending.clear();
141 self.pending_started = None;
142 self.count = 0;
143 self.register = None;
144 self.awaiting_register_char = false;
145 }
146
147 pub fn mode(&self) -> PageMode {
148 self.mode
149 }
150
151 pub fn set_mode(&mut self, mode: PageMode) {
153 self.mode = mode;
154 self.reset_pending();
155 }
156
157 pub fn pending(&self) -> &[KeyChord] {
159 &self.pending
160 }
161
162 pub fn register(&self) -> Option<char> {
164 self.register
165 }
166
167 pub fn count(&self) -> u32 {
169 self.count
170 }
171
172 pub fn count_buffer(&self) -> Option<u32> {
182 if self.count == 0 {
183 None
184 } else {
185 Some(self.count)
186 }
187 }
188
189 pub fn timeout(&self) -> Duration {
191 self.timeout
192 }
193
194 pub fn feed(&mut self, chord: KeyChord, now: Duration) -> Step {
198 if matches!(self.mode, PageMode::Insert) {
199 return Step::EditModeActive;
200 }
201
202 if self.awaiting_register_char {
204 self.awaiting_register_char = false;
205 if let Key::Char(c) = chord.key {
206 self.register = Some(c);
207 return Step::Pending;
208 }
209 self.register = None;
211 return Step::Reject;
212 }
213
214 if matches!(self.mode, PageMode::Normal | PageMode::Visual) && self.pending.is_empty() {
217 if chord.modifiers.is_empty() && chord.key == Key::Char('"') {
219 self.awaiting_register_char = true;
220 return Step::Pending;
221 }
222 if chord.modifiers.is_empty()
228 && let Key::Char(c) = chord.key
229 && c.is_ascii_digit()
230 {
231 let d = (c as u32) - ('0' as u32);
232 if self.count > 0 || d != 0 {
233 self.count = self.count.saturating_mul(10).saturating_add(d);
234 return Step::Pending;
235 }
236 }
237 }
238
239 self.pending.push(chord);
241 if self.pending_started.is_none() {
242 self.pending_started = Some(now);
243 }
244 match self.keymap.lookup(self.mode, &self.pending) {
245 Lookup::Match(action) => {
246 let action = action.clone();
247 let resolved = self.finalise_action(action);
248 Step::Resolved(resolved)
249 }
250 Lookup::Pending => {
251 if self
255 .keymap
256 .resolve_timeout(self.mode, &self.pending)
257 .is_some()
258 {
259 Step::Ambiguous {
260 timeout_at: self.pending_started.unwrap_or(now) + self.timeout,
261 }
262 } else {
263 Step::Pending
264 }
265 }
266 Lookup::NoMatch => {
267 self.reset_pending();
268 Step::Reject
269 }
270 }
271 }
272
273 pub fn tick(&mut self, now: Duration) -> Option<PageAction> {
277 let started = self.pending_started?;
278 if now < started + self.timeout {
279 return None;
280 }
281 let action = self
282 .keymap
283 .resolve_timeout(self.mode, &self.pending)
284 .cloned()?;
285 Some(self.finalise_action(action))
286 }
287
288 pub fn feed_edit_mode_key(&mut self, chord: KeyChord) -> EditModeStep {
294 if chord.modifiers.is_empty() && chord.key == Key::Named(NamedKey::Esc) {
296 self.mode = self.return_mode;
297 return EditModeStep::Exited;
298 }
299 EditModeStep::PassThrough(chord)
300 }
301
302 fn reset_pending(&mut self) {
304 self.pending.clear();
305 self.pending_started = None;
306 self.count = 0;
307 self.register = None;
308 self.awaiting_register_char = false;
309 }
310
311 fn finalise_action(&mut self, action: PageAction) -> PageAction {
314 let count = if self.count == 0 { 1 } else { self.count };
315 let action = apply_count(action, count);
316 self.apply_implicit_mode(&action);
317 self.reset_pending();
318 action
319 }
320
321 fn apply_implicit_mode(&mut self, action: &PageAction) {
322 let new_mode = match action {
323 PageAction::OpenOmnibar | PageAction::OpenCommandLine => Some(PageMode::Command),
324 PageAction::EnterHintMode | PageAction::EnterHintModeBackground => Some(PageMode::Hint),
325 PageAction::EnterInsertMode => {
326 self.return_mode = self.mode;
327 Some(PageMode::Insert)
328 }
329 PageAction::EnterMode(m) => Some(*m),
330 _ => None,
331 };
332 if let Some(m) = new_mode {
333 self.mode = m;
334 }
335 }
336}
337
338fn apply_count(action: PageAction, count: u32) -> PageAction {
341 match action {
342 PageAction::ScrollUp(_) => PageAction::ScrollUp(count),
343 PageAction::ScrollDown(_) => PageAction::ScrollDown(count),
344 PageAction::ScrollLeft(_) => PageAction::ScrollLeft(count),
345 PageAction::ScrollRight(_) => PageAction::ScrollRight(count),
346 other => other,
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::key::{parse_key, parse_keys};
354
355 fn engine_with(bindings: &[(PageMode, &str, PageAction)]) -> Engine {
356 let mut km = Keymap::new();
357 km.set_leader('\\');
358 for (mode, keys, action) in bindings {
359 km.bind(*mode, keys, action.clone()).unwrap();
360 }
361 Engine::new(km)
362 }
363
364 fn t(ms: u64) -> Duration {
365 Duration::from_millis(ms)
366 }
367
368 #[test]
369 fn single_chord_resolves_immediately() {
370 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
371 let r = e.feed(parse_key("j").unwrap(), t(0));
372 assert_eq!(r, Step::Resolved(PageAction::ScrollDown(1)));
373 }
374
375 #[test]
376 fn count_5j_scrolls_5() {
377 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
378 for c in parse_keys("5j").unwrap() {
379 let _ = e.feed(c, t(0));
380 }
381 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
383 let chords = parse_keys("5j").unwrap();
384 let r1 = e.feed(chords[0], t(0));
385 assert_eq!(r1, Step::Pending); let r2 = e.feed(chords[1], t(0));
387 assert_eq!(r2, Step::Resolved(PageAction::ScrollDown(5)));
388 }
389
390 #[test]
391 fn count_multidigit() {
392 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
393 for c in parse_keys("12").unwrap() {
394 let r = e.feed(c, t(0));
395 assert_eq!(r, Step::Pending);
396 }
397 let r = e.feed(parse_key("j").unwrap(), t(0));
398 assert_eq!(r, Step::Resolved(PageAction::ScrollDown(12)));
399 }
400
401 #[test]
402 fn no_count_means_one() {
403 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
404 let r = e.feed(parse_key("j").unwrap(), t(0));
405 assert_eq!(r, Step::Resolved(PageAction::ScrollDown(1)));
406 }
407
408 #[test]
409 fn zero_alone_is_a_binding_not_a_count() {
410 let mut e = engine_with(&[(PageMode::Normal, "0", PageAction::ScrollLeft(1))]);
412 let r = e.feed(parse_key("0").unwrap(), t(0));
413 assert_eq!(r, Step::Resolved(PageAction::ScrollLeft(1)));
414 }
415
416 #[test]
417 fn zero_after_digit_continues_count() {
418 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
419 let r1 = e.feed(parse_key("1").unwrap(), t(0));
420 assert_eq!(r1, Step::Pending);
421 let r2 = e.feed(parse_key("0").unwrap(), t(0));
422 assert_eq!(r2, Step::Pending);
423 let r3 = e.feed(parse_key("j").unwrap(), t(0));
424 assert_eq!(r3, Step::Resolved(PageAction::ScrollDown(10)));
425 }
426
427 #[test]
428 fn ambiguity_resolves_via_tick() {
429 let mut e = engine_with(&[
430 (PageMode::Normal, "g", PageAction::HistoryBack),
431 (PageMode::Normal, "gg", PageAction::ScrollTop),
432 ]);
433 let r = e.feed(parse_key("g").unwrap(), t(0));
434 assert!(matches!(r, Step::Ambiguous { .. }));
435 assert_eq!(e.tick(t(500)), None);
437 assert_eq!(e.tick(t(2000)), Some(PageAction::HistoryBack));
439 assert!(e.pending().is_empty());
441 }
442
443 #[test]
444 fn ambiguity_extends_to_longer_match() {
445 let mut e = engine_with(&[
446 (PageMode::Normal, "g", PageAction::HistoryBack),
447 (PageMode::Normal, "gg", PageAction::ScrollTop),
448 ]);
449 let r1 = e.feed(parse_key("g").unwrap(), t(0));
450 assert!(matches!(r1, Step::Ambiguous { .. }));
451 let r2 = e.feed(parse_key("g").unwrap(), t(100));
454 assert_eq!(r2, Step::Resolved(PageAction::ScrollTop));
455 }
456
457 #[test]
458 fn pure_prefix_returns_pending_not_ambiguous() {
459 let mut e = engine_with(&[(PageMode::Normal, "<C-w>c", PageAction::TabClose)]);
462 let r = e.feed(parse_key("<C-w>").unwrap(), t(0));
463 assert_eq!(r, Step::Pending);
464 let r2 = e.feed(parse_key("c").unwrap(), t(50));
465 assert_eq!(r2, Step::Resolved(PageAction::TabClose));
466 }
467
468 #[test]
469 fn no_match_rejects_and_resets() {
470 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
471 let r = e.feed(parse_key("z").unwrap(), t(0));
472 assert_eq!(r, Step::Reject);
473 assert!(e.pending().is_empty());
474 let r2 = e.feed(parse_key("j").unwrap(), t(10));
476 assert_eq!(r2, Step::Resolved(PageAction::ScrollDown(1)));
477 }
478
479 #[test]
480 fn register_quote_a_then_y_captures_state() {
481 let mut e = engine_with(&[(PageMode::Normal, "y", PageAction::YankUrl)]);
486 let r1 = e.feed(parse_key("\"").unwrap(), t(0));
487 assert_eq!(r1, Step::Pending);
488 let r2 = e.feed(parse_key("a").unwrap(), t(0));
489 assert_eq!(r2, Step::Pending);
490 assert_eq!(e.register(), Some('a'));
492 let r3 = e.feed(parse_key("y").unwrap(), t(0));
493 assert_eq!(r3, Step::Resolved(PageAction::YankUrl));
494 assert_eq!(e.register(), None);
496 }
497
498 #[test]
499 fn omnibar_action_transitions_mode() {
500 let mut e = engine_with(&[(PageMode::Normal, "o", PageAction::OpenOmnibar)]);
501 assert_eq!(e.mode(), PageMode::Normal);
502 let r = e.feed(parse_key("o").unwrap(), t(0));
503 assert_eq!(r, Step::Resolved(PageAction::OpenOmnibar));
504 assert_eq!(e.mode(), PageMode::Command);
505 }
506
507 #[test]
508 fn enter_hint_action_transitions_mode() {
509 let mut e = engine_with(&[(PageMode::Normal, "f", PageAction::EnterHintMode)]);
510 let r = e.feed(parse_key("f").unwrap(), t(0));
511 assert_eq!(r, Step::Resolved(PageAction::EnterHintMode));
512 assert_eq!(e.mode(), PageMode::Hint);
513 }
514
515 #[test]
516 fn enter_mode_explicit_action() {
517 let mut e = engine_with(&[(
518 PageMode::Normal,
519 "v",
520 PageAction::EnterMode(PageMode::Visual),
521 )]);
522 let _ = e.feed(parse_key("v").unwrap(), t(0));
523 assert_eq!(e.mode(), PageMode::Visual);
524 }
525
526 #[test]
527 fn edit_mode_blocks_trie() {
528 let mut e = engine_with(&[
529 (PageMode::Normal, "i", PageAction::EnterInsertMode),
530 (PageMode::Normal, "j", PageAction::ScrollDown(1)),
531 ]);
532 let r = e.feed(parse_key("i").unwrap(), t(0));
533 assert_eq!(r, Step::Resolved(PageAction::EnterInsertMode));
534 assert_eq!(e.mode(), PageMode::Insert);
535 let r2 = e.feed(parse_key("j").unwrap(), t(0));
537 assert_eq!(r2, Step::EditModeActive);
538 }
539
540 #[test]
541 fn edit_mode_passthrough_then_esc_exits() {
542 let mut e = engine_with(&[(PageMode::Normal, "i", PageAction::EnterInsertMode)]);
543 let _ = e.feed(parse_key("i").unwrap(), t(0));
544 assert_eq!(e.mode(), PageMode::Insert);
545 let chord = parse_key("a").unwrap();
546 assert_eq!(
547 e.feed_edit_mode_key(chord),
548 EditModeStep::PassThrough(chord)
549 );
550 assert_eq!(e.mode(), PageMode::Insert);
551 let exit = e.feed_edit_mode_key(parse_key("<Esc>").unwrap());
552 assert_eq!(exit, EditModeStep::Exited);
553 assert_eq!(e.mode(), PageMode::Normal);
554 }
555
556 #[test]
557 fn count_does_not_apply_to_non_count_actions() {
558 let mut e = engine_with(&[(PageMode::Normal, "r", PageAction::Reload)]);
559 let _ = e.feed(parse_key("5").unwrap(), t(0));
560 let r = e.feed(parse_key("r").unwrap(), t(0));
561 assert_eq!(r, Step::Resolved(PageAction::Reload));
563 assert_eq!(e.count(), 0);
564 }
565
566 #[test]
567 fn tick_no_pending_returns_none() {
568 let mut e = engine_with(&[]);
569 assert_eq!(e.tick(t(5000)), None);
570 }
571
572 #[test]
573 fn count_buffer_none_until_digit() {
574 let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
575 assert_eq!(e.count_buffer(), None);
576 let _ = e.feed(parse_key("1").unwrap(), t(0));
577 assert_eq!(e.count_buffer(), Some(1));
578 let _ = e.feed(parse_key("2").unwrap(), t(0));
579 assert_eq!(e.count_buffer(), Some(12));
580 let _ = e.feed(parse_key("j").unwrap(), t(0));
581 assert_eq!(e.count_buffer(), None);
583 }
584}