use std::sync::Arc;
use super::{Session, message::TranscriptLine};
#[derive(Default, Clone)]
pub struct CachedMessage {
pub revision: u64,
pub lines: Vec<TranscriptLine>,
}
pub struct TranscriptReflowCache {
pub width: u16,
pub total_rows: usize,
pub row_offsets: Vec<usize>, pub messages: Vec<CachedMessage>,
}
impl TranscriptReflowCache {
pub fn new(width: u16) -> Self {
Self {
width,
total_rows: 0,
row_offsets: Vec::new(),
messages: Vec::new(),
}
}
pub fn set_width(&mut self, new_width: u16) {
if self.width != new_width {
self.width = new_width;
self.invalidate_content();
}
}
pub fn invalidate_content(&mut self) {
for message in &mut self.messages {
message.lines.clear(); message.revision = 0; }
}
pub fn needs_reflow(&self, index: usize, current_revision: u64) -> bool {
if index >= self.messages.len() {
return true;
}
let cached = &self.messages[index];
cached.revision != current_revision
}
pub fn update_message(&mut self, index: usize, revision: u64, lines: Vec<TranscriptLine>) {
while self.messages.len() <= index {
self.messages.push(CachedMessage::default());
}
let message = &mut self.messages[index];
message.revision = revision;
message.lines = lines;
}
pub fn update_row_offsets_from(&mut self, start_index: usize) {
if start_index == 0 {
self.row_offsets.clear();
self.row_offsets.reserve(self.messages.len());
} else {
self.row_offsets.truncate(start_index);
}
let mut current_offset = if start_index > 0 && start_index <= self.row_offsets.len() {
self.row_offsets[start_index - 1] + self.messages[start_index - 1].lines.len()
} else if start_index > 0 && !self.row_offsets.is_empty() {
let last_idx = self.row_offsets.len() - 1;
self.row_offsets[last_idx] + self.messages[last_idx].lines.len()
} else {
0
};
let start = self.row_offsets.len();
for message in self.messages.iter().skip(start) {
self.row_offsets.push(current_offset);
current_offset += message.lines.len();
}
self.total_rows = current_offset;
}
pub fn total_rows(&self) -> usize {
self.total_rows
}
pub fn get_visible_range(&self, start_row: usize, max_rows: usize) -> Vec<TranscriptLine> {
if max_rows == 0 || start_row >= self.total_rows {
return Vec::new();
}
let mut result = Vec::with_capacity(max_rows.min(self.total_rows - start_row));
let current_row = start_row;
let remaining_rows = max_rows.min(self.total_rows - start_row);
let start_message_idx = match self.row_offsets.binary_search(¤t_row) {
Ok(idx) => idx,
Err(0) => 0,
Err(pos) => pos - 1,
};
for msg_idx in start_message_idx..self.messages.len() {
let msg_start_row = self.row_offsets[msg_idx];
let msg = &self.messages[msg_idx];
if msg_start_row >= current_row + remaining_rows {
break;
}
let skip_lines = current_row.saturating_sub(msg_start_row);
let target_count = remaining_rows - result.len();
result.extend(
msg.lines
.iter()
.skip(skip_lines)
.take(target_count)
.cloned(),
);
if result.len() >= remaining_rows {
break;
}
}
result
}
#[allow(dead_code)]
pub fn message_start_row(&self, index: usize) -> Option<usize> {
self.row_offsets.get(index).copied()
}
#[allow(dead_code)]
pub fn message_row_count(&self, index: usize) -> Option<usize> {
self.messages.get(index).map(|m| m.lines.len())
}
}
impl Session {
pub(super) fn ensure_reflow_cache(&mut self, width: u16) -> &mut TranscriptReflowCache {
let mut cache = self
.transcript_cache
.take()
.unwrap_or_else(|| TranscriptReflowCache::new(width));
let mut width_changed = false;
if cache.width != width {
cache.set_width(width);
width_changed = true;
}
while cache.messages.len() > self.lines.len() {
cache.messages.pop();
}
while cache.messages.len() < self.lines.len() {
cache.messages.push(CachedMessage::default());
}
let mut first_dirty = if width_changed {
0
} else {
self.first_dirty_line.unwrap_or(self.lines.len())
};
first_dirty = (first_dirty..self.lines.len())
.find(|&index| cache.needs_reflow(index, self.lines[index].revision))
.unwrap_or(self.lines.len());
if first_dirty == self.lines.len() {
cache.update_row_offsets_from(first_dirty);
self.first_dirty_line = None;
return self.transcript_cache.insert(cache);
}
for index in first_dirty..self.lines.len() {
let line = &self.lines[index];
if cache.needs_reflow(index, line.revision) {
let new_lines = self.reflow_message_lines(index, width);
cache.update_message(index, line.revision, new_lines);
}
}
cache.update_row_offsets_from(first_dirty);
self.first_dirty_line = None;
self.transcript_cache.insert(cache)
}
pub(crate) fn total_transcript_rows(&mut self, width: u16) -> usize {
if width == 0 {
return 0;
}
let cache = self.ensure_reflow_cache(width);
cache.total_rows()
}
pub(super) fn collect_transcript_window(
&mut self,
width: u16,
start_row: usize,
max_rows: usize,
) -> Vec<TranscriptLine> {
if max_rows == 0 {
return Vec::new();
}
let cache = self.ensure_reflow_cache(width);
cache.get_visible_range(start_row, max_rows)
}
pub(crate) fn collect_transcript_window_cached(
&mut self,
width: u16,
start_row: usize,
max_rows: usize,
) -> Arc<Vec<TranscriptLine>> {
if let Some((cached_offset, cached_width, cached_rows, cached_lines)) =
&self.visible_lines_cache
&& *cached_offset == start_row
&& *cached_width == width
&& *cached_rows == max_rows
{
return Arc::clone(cached_lines);
}
let visible_lines = self.collect_transcript_window(width, start_row, max_rows);
let arc_lines = Arc::new(visible_lines);
self.visible_lines_cache = Some((start_row, width, max_rows, Arc::clone(&arc_lines)));
arc_lines
}
}
impl Default for TranscriptReflowCache {
fn default() -> Self {
Self::new(80) }
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::text::Line;
use std::sync::Arc;
use crate::core_tui::types::{InlineMessageKind, InlineSegment, InlineTextStyle, InlineTheme};
fn line(text: impl Into<Line<'static>>) -> TranscriptLine {
TranscriptLine {
line: text.into(),
explicit_links: Vec::new(),
}
}
fn segment(text: &str) -> InlineSegment {
InlineSegment {
text: text.to_string(),
style: Arc::new(InlineTextStyle::default()),
}
}
#[test]
fn test_cache_initialization() {
let cache = TranscriptReflowCache::new(100);
assert_eq!(cache.width, 100);
assert_eq!(cache.total_rows(), 0);
assert!(cache.messages.is_empty());
}
#[test]
fn test_update_message() {
let mut cache = TranscriptReflowCache::new(80);
let test_line = line("Test line");
let lines = vec![test_line];
cache.update_message(0, 1, lines);
assert!(!cache.messages.is_empty());
assert_eq!(cache.messages[0].revision, 1);
assert_eq!(cache.messages[0].lines.len(), 1);
}
#[test]
fn test_row_offsets() {
let mut cache = TranscriptReflowCache::new(80);
cache.update_message(0, 1, vec![line(Line::default()), line(Line::default())]);
cache.update_message(1, 2, vec![line(Line::default())]);
cache.update_message(
2,
3,
vec![
line(Line::default()),
line(Line::default()),
line(Line::default()),
],
);
cache.update_row_offsets_from(0);
assert_eq!(cache.row_offsets, vec![0, 2, 3]); assert_eq!(cache.total_rows(), 6); }
#[test]
fn test_get_visible_range() {
let mut cache = TranscriptReflowCache::new(80);
cache.update_message(0, 1, vec![line("Line 1"), line("Line 2")]);
cache.update_message(1, 2, vec![line("Line 3")]);
cache.update_row_offsets_from(0);
let range = cache.get_visible_range(0, 2);
assert_eq!(range.len(), 2);
let range = cache.get_visible_range(1, 2);
assert_eq!(range.len(), 2);
}
#[test]
fn test_needs_reflow() {
let cache = TranscriptReflowCache::new(80);
assert!(cache.needs_reflow(0, 1));
let mut cache = TranscriptReflowCache::new(80);
cache.update_message(0, 1, vec![line(Line::default())]);
assert!(!cache.needs_reflow(0, 1));
assert!(cache.needs_reflow(0, 2));
}
#[test]
fn test_width_changes() {
let mut cache = TranscriptReflowCache::new(80);
assert_eq!(cache.width, 80);
cache.set_width(120);
assert_eq!(cache.width, 120);
}
#[test]
fn test_message_accessors() {
let mut cache = TranscriptReflowCache::new(80);
cache.update_message(0, 1, vec![line("Test"), line("Lines")]);
cache.update_row_offsets_from(0);
assert_eq!(cache.row_offsets.first().copied(), Some(0));
assert_eq!(cache.messages.first().map(|m| m.lines.len()), Some(2));
assert_eq!(cache.row_offsets.get(1).copied(), None); assert_eq!(cache.messages.get(1).map(|m| m.lines.len()), None); }
#[test]
fn test_empty_range() {
let cache = TranscriptReflowCache::new(80);
let range = cache.get_visible_range(0, 0);
assert!(range.is_empty());
}
#[test]
fn test_out_of_bounds_range() {
let cache = TranscriptReflowCache::new(80);
let range = cache.get_visible_range(100, 10); assert!(range.is_empty());
}
#[test]
fn test_incremental_row_offsets() {
let mut cache = TranscriptReflowCache::new(80);
cache.update_message(0, 1, vec![line("M1-L1"), line("M1-L2")]);
cache.update_message(1, 2, vec![line("M2-L1")]);
cache.update_message(2, 3, vec![line("M3-L1"), line("M3-L2")]);
cache.update_row_offsets_from(0);
assert_eq!(cache.row_offsets, vec![0, 2, 3]);
assert_eq!(cache.total_rows(), 5);
cache.update_message(1, 4, vec![line("M2-L1-New"), line("M2-L2-New")]);
cache.update_row_offsets_from(1);
assert_eq!(cache.row_offsets, vec![0, 2, 4]);
assert_eq!(cache.total_rows(), 6);
cache.update_message(3, 5, vec![line("M4-L1")]);
cache.update_row_offsets_from(3);
assert_eq!(cache.row_offsets, vec![0, 2, 4, 6]);
assert_eq!(cache.total_rows(), 7);
}
#[test]
fn visible_window_cache_respects_viewport_rows() {
let mut session = Session::new(InlineTheme::default(), None, 20);
for index in 0..6 {
session.push_line(
InlineMessageKind::Agent,
vec![segment(&format!("line {index}"))],
);
}
let first = session.collect_transcript_window_cached(80, 0, 2);
let cached = session.collect_transcript_window_cached(80, 0, 2);
let resized = session.collect_transcript_window_cached(80, 0, 3);
assert!(Arc::ptr_eq(&first, &cached));
assert!(!Arc::ptr_eq(&first, &resized));
assert_eq!(resized.len(), 3);
}
}