use bmux_tui::prelude::Line;
use std::collections::BTreeSet;
use std::time::Instant;
use super::indexed_transcript_layout::IndexedTranscriptLayout;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptLayoutSignature(String);
impl TranscriptLayoutSignature {
#[must_use]
pub const fn new(value: String) -> Self {
Self(value)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TranscriptLayoutRows {
Rendered(Vec<Line>),
Anchored {
rows: Vec<Line>,
anchors: Vec<bcode_plugin_sdk::tui_visual::TuiVisualAnchor>,
},
Selected {
rows: Vec<Line>,
anchors: Vec<bcode_plugin_sdk::tui_visual::TuiVisualAnchor>,
selection: std::collections::BTreeMap<usize, bcode_plugin_sdk::tui::PluginTuiSelectionRow>,
},
Markdown {
rows: Vec<Line>,
anchors: Vec<bcode_plugin_sdk::tui_visual::TuiVisualAnchor>,
projection: std::sync::Arc<bcode_markdown_render::MarkdownRenderResult>,
body_start: usize,
},
BlankSpan(usize),
}
impl Default for TranscriptLayoutRows {
fn default() -> Self {
Self::Rendered(Vec::new())
}
}
impl TranscriptLayoutRows {
#[must_use]
pub const fn len(&self) -> usize {
match self {
Self::Rendered(rows)
| Self::Anchored { rows, .. }
| Self::Selected { rows, .. }
| Self::Markdown { rows, .. } => rows.len(),
Self::BlankSpan(len) => *len,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl From<Vec<Line>> for TranscriptLayoutRows {
fn from(rows: Vec<Line>) -> Self {
Self::Rendered(rows)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TranscriptLayoutFingerprint(String);
impl TranscriptLayoutFingerprint {
#[must_use]
pub const fn new(value: String) -> Self {
Self(value)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriptLayoutInvalidation {
CacheHit,
Incremental,
Width,
Explicit,
}
impl TranscriptLayoutInvalidation {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::CacheHit => "cache_hit",
Self::Incremental => "incremental",
Self::Width => "width",
Self::Explicit => "explicit",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TranscriptLayoutSyncStats {
pub invalidation: TranscriptLayoutInvalidation,
pub entries_scanned: usize,
pub signatures_changed: usize,
pub entries_rebuilt: usize,
pub rows_regenerated: usize,
pub duration_micros: u64,
}
impl TranscriptLayoutSyncStats {
#[must_use]
pub const fn cache_hit(duration_micros: u64) -> Self {
Self {
invalidation: TranscriptLayoutInvalidation::CacheHit,
entries_scanned: 0,
signatures_changed: 0,
entries_rebuilt: 0,
rows_regenerated: 0,
duration_micros,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TranscriptLayoutCache {
width: Option<u16>,
fingerprint: Option<TranscriptLayoutFingerprint>,
structural_fingerprint: Option<TranscriptLayoutFingerprint>,
entries: IndexedTranscriptLayout,
sync_stats: Vec<TranscriptLayoutSyncStats>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VisibleTranscriptLine {
pub row_index: usize,
pub entry_index: usize,
pub row_in_entry: usize,
pub source: VisibleTranscriptSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VisibleTranscriptSource {
HistoryBanner,
Transcript,
Pending,
}
impl TranscriptLayoutCache {
#[must_use]
pub fn changed_content_below(&self, bottom: usize) -> bool {
self.entries.changed_content_below(bottom)
}
pub fn suppress_visual_content_changes(&mut self, invocations: &BTreeSet<String>) {
self.entries.suppress_visual_content_changes(invocations);
}
pub fn clear_content_changes(&mut self) {
self.entries.clear_content_changes();
}
pub fn source_position(&self, index: usize, row: usize) -> Option<usize> {
self.entries.source_position(index, row)
}
pub fn source_row(&self, index: usize, position: usize) -> Option<usize> {
self.entries.source_row(index, position)
}
pub fn selection_row(
&self,
index: usize,
row: usize,
) -> Option<&bcode_plugin_sdk::tui::PluginTuiSelectionRow> {
self.entries.selection_row(index, row)
}
pub fn content_anchor(&self, index: usize, row: usize) -> Option<(&str, usize)> {
self.entries.content_anchor(index, row)
}
pub fn resolve_content_anchor(&self, index: usize, key: &str, offset: usize) -> Option<usize> {
self.entries.resolve_content_anchor(index, key, offset)
}
pub fn content_anchor_row(&self, index: usize, key: &str) -> Option<usize> {
self.entries.content_anchor_row(index, key)
}
#[must_use]
pub fn is_current(&self, fingerprint: &TranscriptLayoutFingerprint) -> bool {
self.fingerprint.as_ref() == Some(fingerprint)
}
#[must_use]
pub fn structure_is_current(&self, fingerprint: &TranscriptLayoutFingerprint) -> bool {
self.structural_fingerprint.as_ref() == Some(fingerprint)
}
pub fn sync_visuals<S, R>(
&mut self,
fingerprint: TranscriptLayoutFingerprint,
invocation_ids: &BTreeSet<String>,
signature: S,
rows: R,
) -> TranscriptLayoutSyncStats
where
S: Fn(usize) -> TranscriptLayoutSignature,
R: FnMut(usize) -> TranscriptLayoutRows,
{
let started = Instant::now();
let (entries_scanned, signatures_changed, rows_regenerated) =
self.entries.sync_visuals(invocation_ids, signature, rows);
self.fingerprint = Some(fingerprint);
let stats = TranscriptLayoutSyncStats {
invalidation: TranscriptLayoutInvalidation::Incremental,
entries_scanned,
signatures_changed,
entries_rebuilt: signatures_changed,
rows_regenerated,
duration_micros: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
};
self.sync_stats.push(stats);
stats
}
pub fn sync_transcript_entries<S, R>(
&mut self,
fingerprint: TranscriptLayoutFingerprint,
indexes: &BTreeSet<usize>,
signature: S,
rows: R,
) -> TranscriptLayoutSyncStats
where
S: Fn(usize) -> TranscriptLayoutSignature,
R: FnMut(usize) -> TranscriptLayoutRows,
{
let started = Instant::now();
let (entries_scanned, signatures_changed, rows_regenerated) = self
.entries
.sync_transcript_entries(indexes, signature, rows);
self.fingerprint = Some(fingerprint);
let stats = TranscriptLayoutSyncStats {
invalidation: TranscriptLayoutInvalidation::Incremental,
entries_scanned,
signatures_changed,
entries_rebuilt: signatures_changed,
rows_regenerated,
duration_micros: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
};
self.sync_stats.push(stats);
stats
}
pub fn record_cache_hit(&mut self, duration_micros: u64) {
self.sync_stats
.push(TranscriptLayoutSyncStats::cache_hit(duration_micros));
}
pub fn drain_sync_stats(&mut self) -> Vec<TranscriptLayoutSyncStats> {
std::mem::take(&mut self.sync_stats)
}
pub fn sync<TS, TR, TI, PS, PR, HS, HR, R>(
&mut self,
spec: TranscriptLayoutSpec<TS, TR, TI, PS, PR, HS, HR, R>,
) -> TranscriptLayoutSyncStats
where
TS: Fn(usize) -> TranscriptLayoutSignature,
TR: Fn(usize) -> TranscriptLayoutRows,
TI: Fn(usize) -> Option<String>,
PS: Fn(usize) -> TranscriptLayoutSignature,
PR: Fn(usize) -> TranscriptLayoutRows,
HS: FnOnce() -> Option<TranscriptLayoutSignature>,
HR: FnOnce() -> TranscriptLayoutRows,
R: FnOnce() -> bool,
{
let started = Instant::now();
let width_changed = self.width != Some(spec.width);
let explicit_reset = (spec.reset)();
let invalidation = if width_changed {
TranscriptLayoutInvalidation::Width
} else if explicit_reset {
TranscriptLayoutInvalidation::Explicit
} else {
TranscriptLayoutInvalidation::Incremental
};
if width_changed || explicit_reset {
self.width = Some(spec.width);
self.fingerprint = None;
self.structural_fingerprint = None;
self.entries.clear();
}
let history_signature = (spec.history_banner_signature)();
let entries_scanned = spec
.transcript_len
.saturating_add(spec.pending_len)
.saturating_add(usize::from(history_signature.is_some()));
let (history_changed, history_rows) = self
.entries
.sync_history(history_signature, spec.history_banner_rows);
let (transcript_changed, transcript_rows) = self.entries.sync_transcript(
spec.transcript_len,
spec.transcript_signature,
spec.transcript_rows,
spec.transcript_invocation_id,
);
let (pending_changed, pending_rows) =
self.entries
.sync_pending(spec.pending_len, spec.pending_signature, spec.pending_rows);
let signatures_changed = history_changed
.saturating_add(transcript_changed)
.saturating_add(pending_changed);
let rows_regenerated = history_rows
.saturating_add(transcript_rows)
.saturating_add(pending_rows);
if width_changed || explicit_reset {
self.entries.clear_content_changes();
}
self.fingerprint = Some(spec.fingerprint);
self.structural_fingerprint = Some(spec.structural_fingerprint);
let stats = TranscriptLayoutSyncStats {
invalidation,
entries_scanned,
signatures_changed,
entries_rebuilt: signatures_changed,
rows_regenerated,
duration_micros: u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX),
};
self.sync_stats.push(stats);
stats
}
#[must_use]
pub fn total_rows(&self) -> usize {
self.entries.total_rows()
}
#[must_use]
pub fn visible_lines_from_top(
&self,
start: usize,
viewport_height: u16,
) -> Vec<VisibleTranscriptLine> {
self.entries.visible_lines_from_top(start, viewport_height)
}
#[must_use]
pub fn line(&self, visible: VisibleTranscriptLine) -> Option<&Line> {
self.entries.line(visible)
}
#[must_use]
pub fn line_at_row(&self, row: usize) -> Option<VisibleTranscriptLine> {
self.entries.line_at_row(row)
}
#[must_use]
pub fn first_entry_start_at_or_after_row(&self, row: usize) -> Option<usize> {
self.entries.first_entry_start_at_or_after_row(row)
}
#[must_use]
pub fn entry_start_row(
&self,
source: VisibleTranscriptSource,
entry_index: usize,
) -> Option<usize> {
self.entries.entry_start_row(source, entry_index)
}
pub fn entry_row_count(
&self,
source: VisibleTranscriptSource,
entry_index: usize,
) -> Option<usize> {
self.entries.entry_row_count(source, entry_index)
}
#[must_use]
pub fn visible_transcript_entry_indexes(&self, top_row: usize, height: u16) -> BTreeSet<usize> {
self.entries
.visible_lines_from_top(top_row, height)
.into_iter()
.filter_map(|line| {
(line.source == VisibleTranscriptSource::Transcript).then_some(line.entry_index)
})
.collect()
}
#[cfg(test)]
#[must_use]
pub fn transcript_entry_row_ptr(&self, entry_index: usize) -> Option<*const Line> {
let start = self.entry_start_row(VisibleTranscriptSource::Transcript, entry_index)?;
let line = self.line_at_row(start)?;
self.line(line).map(std::ptr::from_ref)
}
}
pub struct TranscriptLayoutSpec<TS, TR, TI, PS, PR, HS, HR, R> {
pub width: u16,
pub fingerprint: TranscriptLayoutFingerprint,
pub structural_fingerprint: TranscriptLayoutFingerprint,
pub transcript_len: usize,
pub pending_len: usize,
pub transcript_signature: TS,
pub transcript_rows: TR,
pub transcript_invocation_id: TI,
pub pending_signature: PS,
pub pending_rows: PR,
pub history_banner_signature: HS,
pub history_banner_rows: HR,
pub reset: R,
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::type_complexity)]
fn spec(
fingerprint: &str,
transcript_len: usize,
) -> TranscriptLayoutSpec<
impl Fn(usize) -> TranscriptLayoutSignature,
impl Fn(usize) -> TranscriptLayoutRows,
impl Fn(usize) -> Option<String>,
impl Fn(usize) -> TranscriptLayoutSignature,
impl Fn(usize) -> TranscriptLayoutRows,
impl FnOnce() -> Option<TranscriptLayoutSignature>,
impl FnOnce() -> TranscriptLayoutRows,
impl FnOnce() -> bool,
> {
TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new(fingerprint.to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new(format!(
"structural-{fingerprint}"
)),
transcript_len,
pending_len: 0,
transcript_signature: |index| TranscriptLayoutSignature::new(format!("item-{index}")),
transcript_rows: |index| vec![Line::from(format!("row-{index}"))].into(),
transcript_invocation_id: |_| None,
pending_signature: |index| TranscriptLayoutSignature::new(format!("pending-{index}")),
pending_rows: |_| Vec::new().into(),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
}
}
#[test]
fn width_change_rebuilds_cached_rows_at_new_height() {
let mut cache = TranscriptLayoutCache::default();
let rows_for_width = |width: u16| {
let row_count = if width >= 40 { 1 } else { 3 };
move |_| {
(0..row_count)
.map(|row| Line::from(format!("row-{row}")))
.collect::<Vec<_>>()
.into()
}
};
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("wide".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("wide-structure".to_owned()),
transcript_len: 1,
pending_len: 0,
transcript_signature: |_| TranscriptLayoutSignature::new("item-wide".to_owned()),
transcript_rows: rows_for_width(80),
transcript_invocation_id: |_| None,
pending_signature: |_| unreachable!("no pending"),
pending_rows: |_| unreachable!("no pending"),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(cache.total_rows(), 1);
let stats = cache.sync(TranscriptLayoutSpec {
width: 20,
fingerprint: TranscriptLayoutFingerprint::new("narrow".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("narrow-structure".to_owned()),
transcript_len: 1,
pending_len: 0,
transcript_signature: |_| TranscriptLayoutSignature::new("item-narrow".to_owned()),
transcript_rows: rows_for_width(20),
transcript_invocation_id: |_| None,
pending_signature: |_| unreachable!("no pending"),
pending_rows: |_| unreachable!("no pending"),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(stats.invalidation, TranscriptLayoutInvalidation::Width);
assert_eq!(stats.rows_regenerated, 3);
assert_eq!(cache.total_rows(), 3);
}
#[test]
fn sync_stats_report_scans_and_rebuilds() {
let mut cache = TranscriptLayoutCache::default();
let initial = cache.sync(spec("one", 10));
assert_eq!(initial.invalidation, TranscriptLayoutInvalidation::Width);
assert_eq!(initial.entries_scanned, 10);
assert_eq!(initial.signatures_changed, 10);
assert_eq!(initial.entries_rebuilt, 10);
assert_eq!(initial.rows_regenerated, 10);
let unchanged = cache.sync(spec("two", 10));
assert_eq!(
unchanged.invalidation,
TranscriptLayoutInvalidation::Incremental
);
assert_eq!(unchanged.entries_scanned, 10);
assert_eq!(unchanged.signatures_changed, 0);
assert_eq!(unchanged.entries_rebuilt, 0);
assert_eq!(unchanged.rows_regenerated, 0);
}
#[test]
fn targeted_visual_sync_updates_row_index_without_scanning_siblings() {
let mut cache = TranscriptLayoutCache::default();
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("initial".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("structure".to_owned()),
transcript_len: 3,
pending_len: 0,
transcript_signature: |index| TranscriptLayoutSignature::new(format!("item-{index}")),
transcript_rows: |_| vec![Line::from("row")].into(),
transcript_invocation_id: |index| Some(format!("call-{index}")),
pending_signature: |index| TranscriptLayoutSignature::new(format!("pending-{index}")),
pending_rows: |_| Vec::new().into(),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
let stats = cache.sync_visuals(
TranscriptLayoutFingerprint::new("updated".to_owned()),
&BTreeSet::from(["call-1".to_owned()]),
|index| TranscriptLayoutSignature::new(format!("updated-{index}")),
|_| vec![Line::from("one"), Line::from("two"), Line::from("three")].into(),
);
assert_eq!(stats.entries_scanned, 1);
assert_eq!(stats.entries_rebuilt, 1);
assert_eq!(stats.rows_regenerated, 3);
assert_eq!(cache.total_rows(), 5);
assert_eq!(
cache.entry_start_row(VisibleTranscriptSource::Transcript, 2),
Some(4)
);
let visible = cache.visible_lines_from_top(3, 2);
assert_eq!(visible.len(), 2);
assert_eq!(visible[0].entry_index, 1);
assert_eq!(visible[0].row_in_entry, 2);
assert_eq!(visible[1].entry_index, 2);
assert_eq!(visible[1].row_in_entry, 0);
}
#[test]
fn item_only_update_preserves_row_cache_for_unaffected_entries() {
let mut cache = TranscriptLayoutCache::default();
let rebuilds = std::cell::Cell::new(0_usize);
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("initial".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("structure".to_owned()),
transcript_len: 4,
pending_len: 0,
transcript_signature: |index| TranscriptLayoutSignature::new(format!("item-{index}")),
transcript_rows: |_| vec![Line::from("row")].into(),
transcript_invocation_id: |_| None,
pending_signature: |_| unreachable!("no pending entries"),
pending_rows: |_| unreachable!("no pending entries"),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(cache.total_rows(), 4);
let stats = cache.sync_transcript_entries(
TranscriptLayoutFingerprint::new("item-updated".to_owned()),
&BTreeSet::from([2_usize]),
|index| {
if index == 2 {
TranscriptLayoutSignature::new("item-2-changed".to_owned())
} else {
TranscriptLayoutSignature::new(format!("item-{index}"))
}
},
|index| {
rebuilds.set(rebuilds.get().saturating_add(1));
assert_eq!(index, 2, "only the damaged entry may regenerate rows");
vec![Line::from("changed one"), Line::from("changed two")].into()
},
);
assert_eq!(stats.entries_scanned, 1);
assert_eq!(stats.entries_rebuilt, 1);
assert_eq!(stats.rows_regenerated, 2);
assert_eq!(rebuilds.get(), 1);
assert_eq!(cache.total_rows(), 5);
assert_eq!(
cache.entry_start_row(VisibleTranscriptSource::Transcript, 1),
Some(1),
"the entry before the damaged one keeps its cached position"
);
assert_eq!(
cache.entry_start_row(VisibleTranscriptSource::Transcript, 3),
Some(4),
"later entries shift by the row delta without being rebuilt"
);
}
#[test]
fn removing_pending_entry_removes_its_cached_rows() {
let mut cache = TranscriptLayoutCache::default();
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("with-pending".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("structure-with".to_owned()),
transcript_len: 1,
pending_len: 1,
transcript_signature: |_| TranscriptLayoutSignature::new("item".to_owned()),
transcript_rows: |_| vec![Line::from("committed")].into(),
transcript_invocation_id: |_| None,
pending_signature: |_| TranscriptLayoutSignature::new("pending".to_owned()),
pending_rows: |_| vec![Line::from("pending-one"), Line::from("pending-two")].into(),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(cache.total_rows(), 3);
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("without-pending".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new(
"structure-without".to_owned(),
),
transcript_len: 1,
pending_len: 0,
transcript_signature: |_| TranscriptLayoutSignature::new("item".to_owned()),
transcript_rows: |_| vec![Line::from("committed")].into(),
transcript_invocation_id: |_| None,
pending_signature: |_| unreachable!("no pending entries"),
pending_rows: |_| unreachable!("no pending entries"),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
assert_eq!(cache.total_rows(), 1);
assert_eq!(
cache
.visible_lines_from_top(0, 10)
.into_iter()
.filter_map(|visible| cache.line(visible))
.map(|line| line
.spans
.iter()
.map(|span| span.content.as_str())
.collect::<String>())
.collect::<Vec<_>>(),
["committed"]
);
}
#[test]
fn first_entry_navigation_crosses_indexed_sections() {
let mut cache = TranscriptLayoutCache::default();
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("initial".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new("structure".to_owned()),
transcript_len: 1,
pending_len: 1,
transcript_signature: |_| TranscriptLayoutSignature::new("item".to_owned()),
transcript_rows: |_| vec![Line::from("a"), Line::from("b")].into(),
transcript_invocation_id: |_| None,
pending_signature: |_| TranscriptLayoutSignature::new("pending".to_owned()),
pending_rows: |_| vec![Line::from("pending")].into(),
history_banner_signature: || Some(TranscriptLayoutSignature::new("history".to_owned())),
history_banner_rows: || vec![Line::from("history")].into(),
reset: || false,
});
assert_eq!(cache.first_entry_start_at_or_after_row(0), Some(0));
assert_eq!(cache.first_entry_start_at_or_after_row(2), Some(3));
assert_eq!(cache.first_entry_start_at_or_after_row(3), Some(3));
}
#[test]
fn transcript_viewport_reaches_before_through_and_after_logical_interaction_span() {
let mut cache = TranscriptLayoutCache::default();
cache.sync(TranscriptLayoutSpec {
width: 80,
fingerprint: TranscriptLayoutFingerprint::new("interaction-span".to_owned()),
structural_fingerprint: TranscriptLayoutFingerprint::new(
"interaction-span-structure".to_owned(),
),
transcript_len: 3,
pending_len: 0,
transcript_signature: |index| TranscriptLayoutSignature::new(format!("item-{index}")),
transcript_rows: |index| match index {
0 => vec![Line::from("before")].into(),
1 => TranscriptLayoutRows::BlankSpan(20),
2 => vec![Line::from("after")].into(),
_ => unreachable!("three transcript entries"),
},
transcript_invocation_id: |_| None,
pending_signature: |_| unreachable!("no pending entries"),
pending_rows: |_| unreachable!("no pending entries"),
history_banner_signature: || None,
history_banner_rows: || Vec::new().into(),
reset: || false,
});
let top = cache.visible_lines_from_top(0, 4);
assert_eq!((top[0].entry_index, top[0].row_in_entry), (0, 0));
assert_eq!((top[1].entry_index, top[1].row_in_entry), (1, 0));
let middle = cache.visible_lines_from_top(8, 4);
assert!(
middle
.iter()
.all(|line| line.entry_index == 1 && (7..11).contains(&line.row_in_entry))
);
let bottom = cache.visible_lines_from_top(19, 4);
assert_eq!(
bottom
.iter()
.map(|line| (line.entry_index, line.row_in_entry))
.collect::<Vec<_>>(),
[(1, 18), (1, 19), (2, 0)]
);
assert!(cache.visible_lines_from_top(22, 4).is_empty());
}
#[test]
fn cache_hits_are_recorded_without_scanning() {
let mut cache = TranscriptLayoutCache::default();
cache.record_cache_hit(7);
assert_eq!(
cache.drain_sync_stats(),
vec![TranscriptLayoutSyncStats::cache_hit(7)]
);
assert!(cache.drain_sync_stats().is_empty());
}
}