use crate::actions::{PageAction, PageMode};
use crate::key::{Key, KeyChord, NamedKey};
use crate::keymap::{Keymap, Lookup};
use std::time::Duration;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1000);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Step {
Pending,
Ambiguous { timeout_at: Duration },
Resolved(PageAction),
Reject,
EditModeActive,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditModeStep {
PassThrough(KeyChord),
Exited,
}
#[derive(Debug)]
pub struct Engine {
keymap: Keymap,
mode: PageMode,
return_mode: PageMode,
pending: Vec<KeyChord>,
pending_started: Option<Duration>,
count: u32,
register: Option<char>,
awaiting_register_char: bool,
timeout: Duration,
}
impl Engine {
pub fn new(keymap: Keymap) -> Self {
Self::with_timeout(keymap, DEFAULT_TIMEOUT)
}
pub fn with_timeout(keymap: Keymap, timeout: Duration) -> Self {
Self {
keymap,
mode: PageMode::Normal,
return_mode: PageMode::Normal,
pending: Vec::new(),
pending_started: None,
count: 0,
register: None,
awaiting_register_char: false,
timeout,
}
}
pub fn keymap(&self) -> &Keymap {
&self.keymap
}
pub fn keymap_mut(&mut self) -> &mut Keymap {
&mut self.keymap
}
pub fn set_keymap(&mut self, keymap: Keymap) {
self.keymap = keymap;
self.pending.clear();
self.pending_started = None;
self.count = 0;
self.register = None;
self.awaiting_register_char = false;
}
pub fn mode(&self) -> PageMode {
self.mode
}
pub fn set_mode(&mut self, mode: PageMode) {
self.mode = mode;
self.reset_pending();
}
pub fn pending(&self) -> &[KeyChord] {
&self.pending
}
pub fn register(&self) -> Option<char> {
self.register
}
pub fn count(&self) -> u32 {
self.count
}
pub fn count_buffer(&self) -> Option<u32> {
if self.count == 0 {
None
} else {
Some(self.count)
}
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn feed(&mut self, chord: KeyChord, now: Duration) -> Step {
if matches!(self.mode, PageMode::Insert) {
return Step::EditModeActive;
}
if self.awaiting_register_char {
self.awaiting_register_char = false;
if let Key::Char(c) = chord.key {
self.register = Some(c);
return Step::Pending;
}
self.register = None;
return Step::Reject;
}
if matches!(self.mode, PageMode::Normal | PageMode::Visual) && self.pending.is_empty() {
if chord.modifiers.is_empty() && chord.key == Key::Char('"') {
self.awaiting_register_char = true;
return Step::Pending;
}
if chord.modifiers.is_empty()
&& let Key::Char(c) = chord.key
&& c.is_ascii_digit()
{
let d = (c as u32) - ('0' as u32);
if self.count > 0 || d != 0 {
self.count = self.count.saturating_mul(10).saturating_add(d);
return Step::Pending;
}
}
}
self.pending.push(chord);
if self.pending_started.is_none() {
self.pending_started = Some(now);
}
match self.keymap.lookup(self.mode, &self.pending) {
Lookup::Match(action) => {
let action = action.clone();
let resolved = self.finalise_action(action);
Step::Resolved(resolved)
}
Lookup::Pending => {
if self
.keymap
.resolve_timeout(self.mode, &self.pending)
.is_some()
{
Step::Ambiguous {
timeout_at: self.pending_started.unwrap_or(now) + self.timeout,
}
} else {
Step::Pending
}
}
Lookup::NoMatch => {
self.reset_pending();
Step::Reject
}
}
}
pub fn tick(&mut self, now: Duration) -> Option<PageAction> {
let started = self.pending_started?;
if now < started + self.timeout {
return None;
}
let action = self
.keymap
.resolve_timeout(self.mode, &self.pending)
.cloned()?;
Some(self.finalise_action(action))
}
pub fn feed_edit_mode_key(&mut self, chord: KeyChord) -> EditModeStep {
if chord.modifiers.is_empty() && chord.key == Key::Named(NamedKey::Esc) {
self.mode = self.return_mode;
return EditModeStep::Exited;
}
EditModeStep::PassThrough(chord)
}
fn reset_pending(&mut self) {
self.pending.clear();
self.pending_started = None;
self.count = 0;
self.register = None;
self.awaiting_register_char = false;
}
fn finalise_action(&mut self, action: PageAction) -> PageAction {
let count = if self.count == 0 { 1 } else { self.count };
let action = apply_count(action, count);
self.apply_implicit_mode(&action);
self.reset_pending();
action
}
fn apply_implicit_mode(&mut self, action: &PageAction) {
let new_mode = match action {
PageAction::OpenOmnibar | PageAction::OpenCommandLine => Some(PageMode::Command),
PageAction::EnterHintMode | PageAction::EnterHintModeBackground => Some(PageMode::Hint),
PageAction::EnterInsertMode => {
self.return_mode = self.mode;
Some(PageMode::Insert)
}
PageAction::EnterMode(m) => Some(*m),
_ => None,
};
if let Some(m) = new_mode {
self.mode = m;
}
}
}
fn apply_count(action: PageAction, count: u32) -> PageAction {
match action {
PageAction::ScrollUp(_) => PageAction::ScrollUp(count),
PageAction::ScrollDown(_) => PageAction::ScrollDown(count),
PageAction::ScrollLeft(_) => PageAction::ScrollLeft(count),
PageAction::ScrollRight(_) => PageAction::ScrollRight(count),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key::{parse_key, parse_keys};
fn engine_with(bindings: &[(PageMode, &str, PageAction)]) -> Engine {
let mut km = Keymap::new();
km.set_leader('\\');
for (mode, keys, action) in bindings {
km.bind(*mode, keys, action.clone()).unwrap();
}
Engine::new(km)
}
fn t(ms: u64) -> Duration {
Duration::from_millis(ms)
}
#[test]
fn single_chord_resolves_immediately() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
let r = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::ScrollDown(1)));
}
#[test]
fn count_5j_scrolls_5() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
for c in parse_keys("5j").unwrap() {
let _ = e.feed(c, t(0));
}
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
let chords = parse_keys("5j").unwrap();
let r1 = e.feed(chords[0], t(0));
assert_eq!(r1, Step::Pending); let r2 = e.feed(chords[1], t(0));
assert_eq!(r2, Step::Resolved(PageAction::ScrollDown(5)));
}
#[test]
fn count_multidigit() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
for c in parse_keys("12").unwrap() {
let r = e.feed(c, t(0));
assert_eq!(r, Step::Pending);
}
let r = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::ScrollDown(12)));
}
#[test]
fn no_count_means_one() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
let r = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::ScrollDown(1)));
}
#[test]
fn zero_alone_is_a_binding_not_a_count() {
let mut e = engine_with(&[(PageMode::Normal, "0", PageAction::ScrollLeft(1))]);
let r = e.feed(parse_key("0").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::ScrollLeft(1)));
}
#[test]
fn zero_after_digit_continues_count() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
let r1 = e.feed(parse_key("1").unwrap(), t(0));
assert_eq!(r1, Step::Pending);
let r2 = e.feed(parse_key("0").unwrap(), t(0));
assert_eq!(r2, Step::Pending);
let r3 = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(r3, Step::Resolved(PageAction::ScrollDown(10)));
}
#[test]
fn ambiguity_resolves_via_tick() {
let mut e = engine_with(&[
(PageMode::Normal, "g", PageAction::HistoryBack),
(PageMode::Normal, "gg", PageAction::ScrollTop),
]);
let r = e.feed(parse_key("g").unwrap(), t(0));
assert!(matches!(r, Step::Ambiguous { .. }));
assert_eq!(e.tick(t(500)), None);
assert_eq!(e.tick(t(2000)), Some(PageAction::HistoryBack));
assert!(e.pending().is_empty());
}
#[test]
fn ambiguity_extends_to_longer_match() {
let mut e = engine_with(&[
(PageMode::Normal, "g", PageAction::HistoryBack),
(PageMode::Normal, "gg", PageAction::ScrollTop),
]);
let r1 = e.feed(parse_key("g").unwrap(), t(0));
assert!(matches!(r1, Step::Ambiguous { .. }));
let r2 = e.feed(parse_key("g").unwrap(), t(100));
assert_eq!(r2, Step::Resolved(PageAction::ScrollTop));
}
#[test]
fn pure_prefix_returns_pending_not_ambiguous() {
let mut e = engine_with(&[(PageMode::Normal, "<C-w>c", PageAction::TabClose)]);
let r = e.feed(parse_key("<C-w>").unwrap(), t(0));
assert_eq!(r, Step::Pending);
let r2 = e.feed(parse_key("c").unwrap(), t(50));
assert_eq!(r2, Step::Resolved(PageAction::TabClose));
}
#[test]
fn no_match_rejects_and_resets() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
let r = e.feed(parse_key("z").unwrap(), t(0));
assert_eq!(r, Step::Reject);
assert!(e.pending().is_empty());
let r2 = e.feed(parse_key("j").unwrap(), t(10));
assert_eq!(r2, Step::Resolved(PageAction::ScrollDown(1)));
}
#[test]
fn register_quote_a_then_y_captures_state() {
let mut e = engine_with(&[(PageMode::Normal, "y", PageAction::YankUrl)]);
let r1 = e.feed(parse_key("\"").unwrap(), t(0));
assert_eq!(r1, Step::Pending);
let r2 = e.feed(parse_key("a").unwrap(), t(0));
assert_eq!(r2, Step::Pending);
assert_eq!(e.register(), Some('a'));
let r3 = e.feed(parse_key("y").unwrap(), t(0));
assert_eq!(r3, Step::Resolved(PageAction::YankUrl));
assert_eq!(e.register(), None);
}
#[test]
fn omnibar_action_transitions_mode() {
let mut e = engine_with(&[(PageMode::Normal, "o", PageAction::OpenOmnibar)]);
assert_eq!(e.mode(), PageMode::Normal);
let r = e.feed(parse_key("o").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::OpenOmnibar));
assert_eq!(e.mode(), PageMode::Command);
}
#[test]
fn enter_hint_action_transitions_mode() {
let mut e = engine_with(&[(PageMode::Normal, "f", PageAction::EnterHintMode)]);
let r = e.feed(parse_key("f").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::EnterHintMode));
assert_eq!(e.mode(), PageMode::Hint);
}
#[test]
fn enter_mode_explicit_action() {
let mut e = engine_with(&[(
PageMode::Normal,
"v",
PageAction::EnterMode(PageMode::Visual),
)]);
let _ = e.feed(parse_key("v").unwrap(), t(0));
assert_eq!(e.mode(), PageMode::Visual);
}
#[test]
fn edit_mode_blocks_trie() {
let mut e = engine_with(&[
(PageMode::Normal, "i", PageAction::EnterInsertMode),
(PageMode::Normal, "j", PageAction::ScrollDown(1)),
]);
let r = e.feed(parse_key("i").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::EnterInsertMode));
assert_eq!(e.mode(), PageMode::Insert);
let r2 = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(r2, Step::EditModeActive);
}
#[test]
fn edit_mode_passthrough_then_esc_exits() {
let mut e = engine_with(&[(PageMode::Normal, "i", PageAction::EnterInsertMode)]);
let _ = e.feed(parse_key("i").unwrap(), t(0));
assert_eq!(e.mode(), PageMode::Insert);
let chord = parse_key("a").unwrap();
assert_eq!(
e.feed_edit_mode_key(chord),
EditModeStep::PassThrough(chord)
);
assert_eq!(e.mode(), PageMode::Insert);
let exit = e.feed_edit_mode_key(parse_key("<Esc>").unwrap());
assert_eq!(exit, EditModeStep::Exited);
assert_eq!(e.mode(), PageMode::Normal);
}
#[test]
fn count_does_not_apply_to_non_count_actions() {
let mut e = engine_with(&[(PageMode::Normal, "r", PageAction::Reload)]);
let _ = e.feed(parse_key("5").unwrap(), t(0));
let r = e.feed(parse_key("r").unwrap(), t(0));
assert_eq!(r, Step::Resolved(PageAction::Reload));
assert_eq!(e.count(), 0);
}
#[test]
fn tick_no_pending_returns_none() {
let mut e = engine_with(&[]);
assert_eq!(e.tick(t(5000)), None);
}
#[test]
fn count_buffer_none_until_digit() {
let mut e = engine_with(&[(PageMode::Normal, "j", PageAction::ScrollDown(1))]);
assert_eq!(e.count_buffer(), None);
let _ = e.feed(parse_key("1").unwrap(), t(0));
assert_eq!(e.count_buffer(), Some(1));
let _ = e.feed(parse_key("2").unwrap(), t(0));
assert_eq!(e.count_buffer(), Some(12));
let _ = e.feed(parse_key("j").unwrap(), t(0));
assert_eq!(e.count_buffer(), None);
}
}