use std::collections::HashMap;
use std::collections::VecDeque;
use ratatui::text::Line;
const DEFAULT_CAPACITY: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CellId {
History(usize),
Active(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Key {
cell: CellId,
width: u16,
revision: u64,
}
#[derive(Debug)]
pub struct TranscriptCache {
capacity: usize,
entries: HashMap<Key, Vec<Line<'static>>>,
insertion_order: VecDeque<Key>,
}
impl Default for TranscriptCache {
fn default() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
}
impl TranscriptCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
entries: HashMap::with_capacity(capacity.max(1)),
insertion_order: VecDeque::with_capacity(capacity.max(1)),
}
}
#[must_use]
pub fn get(&self, cell: CellId, width: u16, revision: u64) -> Option<&[Line<'static>]> {
let key = Key {
cell,
width,
revision,
};
self.entries.get(&key).map(Vec::as_slice)
}
pub fn insert(&mut self, cell: CellId, width: u16, revision: u64, lines: Vec<Line<'static>>) {
let key = Key {
cell,
width,
revision,
};
if self.entries.insert(key, lines).is_some() {
return;
}
if self.entries.len() > self.capacity
&& let Some(oldest) = self.insertion_order.pop_front()
{
self.entries.remove(&oldest);
}
self.insertion_order.push_back(key);
}
#[allow(dead_code)] pub fn clear(&mut self) {
self.entries.clear();
self.insertion_order.clear();
}
#[cfg(test)]
pub fn len(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::text::Span;
fn line(s: &str) -> Line<'static> {
Line::from(Span::raw(s.to_string()))
}
#[test]
fn miss_returns_none() {
let cache = TranscriptCache::new();
assert!(cache.get(CellId::History(0), 80, 1).is_none());
}
#[test]
fn round_trip_returns_inserted_lines() {
let mut cache = TranscriptCache::new();
let lines = vec![line("hello"), line("world")];
cache.insert(CellId::History(0), 80, 1, lines.clone());
let got = cache
.get(CellId::History(0), 80, 1)
.expect("entry should be cached");
assert_eq!(got.len(), 2);
assert_eq!(got[0].spans[0].content, "hello");
}
#[test]
fn revision_bump_invalidates_cell() {
let mut cache = TranscriptCache::new();
cache.insert(CellId::History(0), 80, 1, vec![line("v1")]);
assert!(cache.get(CellId::History(0), 80, 1).is_some());
assert!(cache.get(CellId::History(0), 80, 2).is_none());
}
#[test]
fn width_change_invalidates_cell() {
let mut cache = TranscriptCache::new();
cache.insert(CellId::History(0), 80, 1, vec![line("v1")]);
assert!(cache.get(CellId::History(0), 80, 1).is_some());
assert!(cache.get(CellId::History(0), 100, 1).is_none());
}
#[test]
fn active_cells_are_distinct_from_history() {
let mut cache = TranscriptCache::new();
cache.insert(CellId::History(0), 80, 1, vec![line("history")]);
cache.insert(CellId::Active(0), 80, 1, vec![line("active")]);
assert_eq!(
cache.get(CellId::History(0), 80, 1).unwrap()[0].spans[0].content,
"history"
);
assert_eq!(
cache.get(CellId::Active(0), 80, 1).unwrap()[0].spans[0].content,
"active"
);
}
#[test]
fn reinsert_same_key_does_not_evict() {
let mut cache = TranscriptCache::with_capacity(2);
cache.insert(CellId::History(0), 80, 1, vec![line("a")]);
cache.insert(CellId::History(1), 80, 1, vec![line("b")]);
cache.insert(CellId::History(0), 80, 1, vec![line("a-prime")]);
assert!(cache.get(CellId::History(1), 80, 1).is_some());
}
#[test]
fn capacity_evicts_oldest_on_overflow() {
let mut cache = TranscriptCache::with_capacity(2);
cache.insert(CellId::History(0), 80, 1, vec![line("a")]);
cache.insert(CellId::History(1), 80, 1, vec![line("b")]);
cache.insert(CellId::History(2), 80, 1, vec![line("c")]);
assert!(cache.get(CellId::History(0), 80, 1).is_none());
assert!(cache.get(CellId::History(1), 80, 1).is_some());
assert!(cache.get(CellId::History(2), 80, 1).is_some());
assert_eq!(cache.len(), 2);
}
#[test]
fn clear_drops_everything() {
let mut cache = TranscriptCache::new();
cache.insert(CellId::History(0), 80, 1, vec![line("v1")]);
cache.clear();
assert!(cache.get(CellId::History(0), 80, 1).is_none());
assert_eq!(cache.len(), 0);
}
}