use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
pub(crate) const DEFAULT_TYPE_AHEAD_TIMEOUT: Duration = Duration::from_millis(500);
#[derive(Default)]
pub(crate) struct TypeAheadState {
inner: RefCell<TypeAheadInner>,
}
#[derive(Default)]
struct TypeAheadInner {
last: Option<Instant>,
buffer: String,
}
impl TypeAheadState {
pub(crate) fn new() -> Rc<Self> {
Rc::new(Self::default())
}
pub(crate) fn search(
&self,
c: char,
current: usize,
count: usize,
timeout: Duration,
label: impl Fn(usize) -> Option<String>,
) -> Option<usize> {
if timeout.is_zero() || c.is_control() || count == 0 {
return None;
}
let now = Instant::now();
let buffer = {
let mut st = self.inner.borrow_mut();
let stale = st
.last
.map(|t| now.duration_since(t) > timeout)
.unwrap_or(true);
if stale {
st.buffer.clear();
}
st.last = Some(now);
st.buffer.push(c.to_ascii_lowercase());
st.buffer.clone()
};
let first = buffer.chars().next().unwrap();
let term: &str = if buffer.chars().all(|ch| ch == first) {
&buffer[..first.len_utf8()]
} else {
&buffer
};
for offset in 1..=count {
let i = (current + offset) % count;
if let Some(text) = label(i) {
if text.to_ascii_lowercase().starts_with(term) {
return Some(i);
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn labels(rows: &'static [&'static str]) -> impl Fn(usize) -> Option<String> {
move |i| rows.get(i).map(|s| s.to_string())
}
#[test]
fn repeating_the_same_letter_cycles_instead_of_growing_the_prefix() {
let rows: &'static [&'static str] = &["Banana", "Apple", "Blueberry", "Cherry", "Bilberry"];
let state = TypeAheadState::new();
let timeout = DEFAULT_TYPE_AHEAD_TIMEOUT;
let i1 = state
.search('b', 0, rows.len(), timeout, labels(rows))
.unwrap();
assert_eq!(i1, 2, "first 'b' from row 0 finds Blueberry");
let i2 = state
.search('b', i1, rows.len(), timeout, labels(rows))
.unwrap();
assert_eq!(i2, 4, "second 'b' cycles forward to Bilberry, not \"bb\"");
let i3 = state
.search('b', i2, rows.len(), timeout, labels(rows))
.unwrap();
assert_eq!(i3, 0, "third 'b' wraps back around to Banana");
}
#[test]
fn mixed_letters_still_grow_a_multi_char_prefix_search() {
let rows: &'static [&'static str] = &["Apple", "Cherry", "Cranberry", "Date"];
let state = TypeAheadState::new();
let timeout = DEFAULT_TYPE_AHEAD_TIMEOUT;
let i1 = state
.search('c', 0, rows.len(), timeout, labels(rows))
.unwrap();
assert_eq!(i1, 1, "'c' finds Cherry");
let i2 = state
.search('r', i1, rows.len(), timeout, labels(rows))
.unwrap();
assert_eq!(i2, 2, "'cr' narrows to Cranberry");
}
}