use std::time::{Duration, Instant};
use keymap_core::KeyInput;
use crate::{PendingSequence, SequenceKeymap, Step};
#[derive(Debug, Clone, Default)]
pub struct TimedPending {
inner: PendingSequence,
last_key_at: Option<Instant>,
}
impl TimedPending {
#[must_use]
pub fn new() -> Self {
TimedPending::default()
}
pub fn feed<'a, A>(
&mut self,
map: &'a SequenceKeymap<A>,
key: KeyInput,
now: Instant,
window: Duration,
) -> TimedStep<'a, A> {
let expired = if let Some(last) = self.last_key_at {
if now.duration_since(last) > window && !self.inner.is_empty() {
let keys = self.inner.flush();
self.last_key_at = None;
debug_assert!(!keys.is_empty(), "flushed buffer must be non-empty");
Some(keys)
} else {
None
}
} else {
None
};
let step = self.inner.feed(map, key);
match &step {
Step::Pending => {
self.last_key_at = Some(now);
}
Step::Fired(_) | Step::PassThrough(_) => {
self.last_key_at = None;
}
}
TimedStep { expired, step }
}
#[must_use]
pub fn deadline(&self, window: Duration) -> Option<Instant> {
self.last_key_at.map(|t| t + window)
}
pub fn flush(&mut self) -> Vec<KeyInput> {
self.last_key_at = None;
self.inner.flush()
}
#[must_use]
pub fn pending(&self) -> &[KeyInput] {
self.inner.pending()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[must_use]
#[derive(Debug)]
pub struct TimedStep<'a, A> {
pub expired: Option<Vec<KeyInput>>,
pub step: Step<'a, A>,
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use keymap_core::{Key, KeyInput, Modifiers};
use super::*;
use crate::{SequenceKeymap, Step};
fn plain(c: char) -> KeyInput {
KeyInput::new(Key::Char(c), Modifiers::NONE)
}
fn t(base: Instant, ms: u64) -> Instant {
base + Duration::from_millis(ms)
}
const WINDOW: Duration = Duration::from_millis(500);
fn jj_map() -> SequenceKeymap<&'static str> {
let j = plain('j');
let mut map = SequenceKeymap::new();
map.bind([j, j], "normal").unwrap();
map
}
#[test]
fn within_window_step_is_pending_no_expiry() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let r = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
assert!(r.expired.is_none(), "no expiry on first key");
assert!(matches!(r.step, Step::Pending));
assert_eq!(tp.pending(), &[plain('j')]);
}
#[test]
fn quick_second_key_fires_with_no_expiry() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
let r = tp.feed(&map, plain('j'), t(base, 100), WINDOW); assert!(r.expired.is_none());
assert!(matches!(r.step, Step::Fired(&"normal")));
assert!(tp.is_empty());
}
#[test]
fn slow_second_key_produces_expired_and_new_step() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
let r = tp.feed(&map, plain('j'), t(base, 600), WINDOW);
let expired = r.expired.expect("must have expired keys");
assert!(!expired.is_empty(), "Some(v) must be non-empty");
assert_eq!(expired, vec![plain('j')]);
assert!(matches!(r.step, Step::Pending));
assert_eq!(tp.pending(), &[plain('j')]);
}
#[test]
fn expired_vec_is_never_empty_some() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
let r = tp.feed(&map, plain('j'), t(base, 600), WINDOW);
if let Some(v) = r.expired {
assert!(!v.is_empty(), "Some(expired) must never be empty");
}
}
#[test]
fn deadline_after_prefix_equals_last_key_plus_window() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
assert_eq!(tp.deadline(WINDOW), Some(t(base, 0) + WINDOW));
}
#[test]
fn deadline_advances_on_each_pending_key() {
let mut map = SequenceKeymap::new();
map.bind([plain('a'), plain('b'), plain('c')], "abc")
.unwrap();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('a'), t(base, 0), WINDOW);
assert_eq!(tp.deadline(WINDOW), Some(t(base, 0) + WINDOW));
let _ = tp.feed(&map, plain('b'), t(base, 200), WINDOW);
assert_eq!(tp.deadline(WINDOW), Some(t(base, 200) + WINDOW));
}
#[test]
fn deadline_is_none_when_buffer_is_empty() {
let tp = TimedPending::new();
assert!(tp.deadline(WINDOW).is_none());
}
#[test]
fn deadline_is_none_after_fired() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
let _ = tp.feed(&map, plain('j'), t(base, 100), WINDOW); assert!(tp.deadline(WINDOW).is_none());
}
#[test]
fn deadline_is_none_after_passthrough() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
let _ = tp.feed(&map, plain('x'), t(base, 100), WINDOW); assert!(tp.deadline(WINDOW).is_none());
}
#[test]
fn some_expired_is_always_non_empty_across_a_stream() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let stream = [
(plain('j'), 0u64),
(plain('j'), 600), (plain('j'), 1200), (plain('j'), 1300), ];
for (key, ms) in stream {
let r = tp.feed(&map, key, t(base, ms), WINDOW);
if let Some(v) = r.expired {
assert!(
!v.is_empty(),
"Some(expired) must never be empty at ms={ms}"
);
}
}
}
#[test]
fn no_key_lost_or_duplicated_across_mixed_stream() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let stream = [
(plain('j'), 0u64),
(plain('j'), 100),
(plain('j'), 700),
(plain('j'), 1400),
];
let mut output: Vec<KeyInput> = Vec::new();
for (key, ms) in stream {
let r = tp.feed(&map, key, t(base, ms), WINDOW);
if let Some(expired) = r.expired {
output.extend(expired);
}
match r.step {
Step::Fired(_) => {
output.push(plain('j'));
output.push(plain('j'));
}
Step::PassThrough(keys) => output.extend(keys),
Step::Pending => {}
}
}
output.extend(tp.flush());
assert_eq!(output.len(), stream.len());
}
#[test]
fn flush_drains_pending_and_clears_clock() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let _ = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
assert!(!tp.is_empty());
assert!(tp.deadline(WINDOW).is_some());
let flushed = tp.flush();
assert_eq!(flushed, vec![plain('j')]);
assert!(tp.is_empty());
assert!(tp.deadline(WINDOW).is_none());
}
#[test]
fn flush_empty_is_no_op() {
let mut tp = TimedPending::new();
assert_eq!(tp.flush(), Vec::<KeyInput>::new());
assert!(tp.is_empty());
}
#[test]
fn gap_exactly_equal_to_window_does_not_expire() {
let map = jj_map();
let base = Instant::now();
let mut tp = TimedPending::new();
let r1 = tp.feed(&map, plain('j'), t(base, 0), WINDOW);
assert!(r1.expired.is_none(), "no expiry on first key");
assert!(matches!(r1.step, Step::Pending));
let r2 = tp.feed(&map, plain('j'), t(base, 500), WINDOW);
assert!(
r2.expired.is_none(),
"gap == WINDOW must not expire (boundary is strict >): expired={:?}",
r2.expired
);
assert!(
matches!(r2.step, Step::Fired(&"normal")),
"j j with gap==WINDOW should fire: {:?}",
r2.step
);
}
}