use std::rc::Rc;
use crate::picker_item::PickerItem;
#[derive(Default)]
pub(crate) struct MatchesCache {
items: Rc<[PickerItem]>,
query: String,
max_items: usize,
matches: Rc<[PickerItem]>,
}
impl MatchesCache {
pub fn get(
&mut self,
items: Rc<[PickerItem]>,
query: &str,
max_items: usize,
compute: impl FnOnce(&[PickerItem]) -> Vec<PickerItem>,
) -> Rc<[PickerItem]> {
if self.query == query && self.max_items == max_items && same_items(&self.items, &items) {
return Rc::clone(&self.matches);
}
let matches: Rc<[PickerItem]> = compute(&items).into();
*self = Self {
items,
query: query.to_owned(),
max_items,
matches: Rc::clone(&matches),
};
matches
}
}
pub(crate) fn empty_matches() -> Rc<[PickerItem]> {
thread_local! {
static EMPTY: Rc<[PickerItem]> = Rc::from(Vec::new());
}
EMPTY.with(Rc::clone)
}
fn same_items(a: &[PickerItem], b: &[PickerItem]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(x, y)| x.key() == y.key() && x.label() == y.label())
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
struct Counter(Rc<Cell<usize>>);
impl Counter {
fn new() -> (Self, Rc<Cell<usize>>) {
let calls = Rc::new(Cell::new(0));
(Self(calls.clone()), calls)
}
fn compute(&self, items: &[PickerItem]) -> Vec<PickerItem> {
self.0.set(self.0.get() + 1);
items.iter().take(2).cloned().collect()
}
}
fn items(labels: &[&str]) -> Rc<[PickerItem]> {
labels
.iter()
.map(|l| PickerItem::new(l.to_string(), l.to_string()))
.collect::<Vec<_>>()
.into()
}
#[test]
fn repeated_unchanged_frames_compute_once_and_share_one_list() {
let (counter, calls) = Counter::new();
let mut cache = MatchesCache::default();
let mut first = None;
for _ in 0..5 {
let matches = cache.get(items(&["Alpha", "Beta"]), "al", 8, |items| {
counter.compute(items)
});
first.get_or_insert_with(|| matches.clone());
assert!(
Rc::ptr_eq(first.as_ref().unwrap(), &matches),
"an unchanged frame must be served the cached list"
);
}
assert_eq!(calls.get(), 1, "five unchanged frames compute once");
}
#[test]
fn a_query_change_recomputes() {
let (counter, calls) = Counter::new();
let mut cache = MatchesCache::default();
cache.get(items(&["Alpha", "Beta"]), "al", 8, |i| counter.compute(i));
cache.get(items(&["Alpha", "Beta"]), "b", 8, |i| counter.compute(i));
assert_eq!(calls.get(), 2, "the new query must miss the cache");
}
#[test]
fn an_items_change_recomputes_and_an_equal_rebuild_hits() {
let (counter, calls) = Counter::new();
let mut cache = MatchesCache::default();
cache.get(items(&["Alpha", "Beta"]), "al", 8, |i| counter.compute(i));
cache.get(items(&["Alpha", "Beta"]), "al", 8, |i| counter.compute(i));
assert_eq!(calls.get(), 1, "an equal rebuild is the same identity");
cache.get(items(&["Alpha", "Gamma"]), "al", 8, |i| counter.compute(i));
assert_eq!(calls.get(), 2, "the edited label must miss the cache");
cache.get(items(&["Gamma", "Alpha"]), "al", 8, |i| counter.compute(i));
assert_eq!(calls.get(), 3, "the reorder must miss the cache");
}
#[test]
fn a_cap_change_recomputes() {
let (counter, calls) = Counter::new();
let mut cache = MatchesCache::default();
cache.get(items(&["Alpha", "Beta"]), "al", 8, |i| counter.compute(i));
cache.get(items(&["Alpha", "Beta"]), "al", 4, |i| counter.compute(i));
assert_eq!(calls.get(), 2, "a changed cap must miss the cache");
}
}