use std::hash::BuildHasher;
use hashbrown::HashTable;
use ratatui::text::Line;
use rustc_hash::FxBuildHasher;
use super::ast::Block;
use super::row_origin::RowOrigin;
#[derive(Debug, Default)]
pub(super) struct BlockMap {
table: HashTable<(u64, Block, CachedRows)>,
}
impl BlockMap {
pub fn with_capacity(capacity: usize) -> Self {
Self {
table: HashTable::with_capacity(capacity),
}
}
pub fn hash(block: &Block) -> u64 {
FxBuildHasher.hash_one(block)
}
pub fn take(&mut self, hash: u64, block: &Block) -> Option<(Block, CachedRows)> {
self.table
.find_entry(hash, |(_, key, _)| key == block)
.ok()
.map(|entry| {
let ((_, key, rows), _) = entry.remove();
(key, rows)
})
}
pub fn get(&self, hash: u64, block: &Block) -> Option<&CachedRows> {
self.table
.find(hash, |(_, key, _)| key == block)
.map(|(_, _, rows)| rows)
}
pub fn insert_new(&mut self, hash: u64, block: Block, rows: CachedRows) {
debug_assert!(self.get(hash, &block).is_none());
self.table
.insert_unique(hash, (hash, block, rows), |(hash, _, _)| *hash);
}
pub fn len(&self) -> usize {
self.table.len()
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.table.is_empty()
}
pub fn clear(&mut self) {
self.table.clear();
}
#[cfg(test)]
pub fn keys(&self) -> impl Iterator<Item = &Block> {
self.table.iter().map(|(_, key, _)| key)
}
}
#[derive(Debug, Clone)]
pub(super) struct CachedRows {
pub lines: Vec<Line<'static>>,
pub origins: Vec<RowOrigin>,
}
pub(super) fn is_cache_worthy(block: &Block) -> bool {
match block {
Block::Table { .. } | Block::CodeBlock { .. } => true,
Block::BlockQuote { blocks, .. } => blocks.iter().any(is_cache_worthy),
Block::List { items, .. } => items.iter().flat_map(|it| &it.blocks).any(is_cache_worthy),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct RenderSettings {
pub theme_addr: usize,
pub viewport_width: usize,
pub code_wrap: bool,
pub image_max_height: usize,
pub row_striping: bool,
pub big_h1: bool,
pub syntax_highlighting: bool,
pub reflow_paragraphs: bool,
pub highlight_generation: u64,
pub highlight_retry_epoch: u64,
}
#[derive(Debug, Default)]
pub struct RenderCache {
pub(super) settings: Option<RenderSettings>,
pub(super) entries: BlockMap,
pub(super) last_row_count: usize,
}
impl RenderCache {
pub(super) fn begin_build(&mut self, settings: RenderSettings) -> BlockMap {
if self.settings.as_ref() != Some(&settings) {
self.entries.clear();
self.settings = Some(settings);
}
let capacity = self.entries.len();
std::mem::replace(&mut self.entries, BlockMap::with_capacity(capacity))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn settings() -> RenderSettings {
RenderSettings {
theme_addr: 0,
viewport_width: 80,
code_wrap: false,
image_max_height: 20,
row_striping: false,
big_h1: false,
syntax_highlighting: true,
reflow_paragraphs: false,
highlight_generation: 0,
highlight_retry_epoch: 0,
}
}
#[test]
fn both_highlight_counters_clear_the_cache() {
for bump in [
|s: &mut RenderSettings| s.highlight_generation += 1,
|s: &mut RenderSettings| s.highlight_retry_epoch += 1,
] {
let mut cache = RenderCache::default();
cache.begin_build(settings());
let block = Block::HorizontalRule {
src: Default::default(),
};
cache.entries.insert_new(
BlockMap::hash(&block),
block,
CachedRows {
lines: vec![Line::from("x")],
origins: vec![RowOrigin::chrome(None)],
},
);
let prev = cache.begin_build(settings());
assert_eq!(prev.len(), 1, "an unchanged fingerprint must not clear");
cache.entries = prev;
let mut changed = settings();
bump(&mut changed);
let prev = cache.begin_build(changed);
assert!(prev.is_empty(), "a moved counter must clear the cache");
}
}
}