use super::view::rows::{Row, WrappedLines, label, wrap_word_aware};
use super::{Block, BlockKind, Transcript};
use crate::interactive::tui::wrap::{cell_width, truncate_cells};
pub(crate) const PRE_CHAPTER: u32 = 0;
pub(crate) fn chapter_for(blocks: &[Block], kind: BlockKind) -> u32 {
let current = current_chapter(blocks);
if kind == BlockKind::User {
current + 1
} else {
current
}
}
pub(crate) fn current_chapter(blocks: &[Block]) -> u32 {
blocks.last().map(|b| b.chapter).unwrap_or(PRE_CHAPTER)
}
#[allow(dead_code)]
pub(crate) fn chapter_range(blocks: &[Block], chapter: u32) -> Option<(usize, usize)> {
let start = blocks.iter().position(|b| b.chapter == chapter)?;
let end = blocks
.iter()
.rposition(|b| b.chapter == chapter)
.map(|i| i + 1)
.unwrap_or(start);
Some((start, end))
}
pub(crate) fn chapter_request(blocks: &[Block], chapter: u32) -> Option<String> {
let block = blocks
.iter()
.find(|b| b.chapter == chapter && b.kind == BlockKind::User)?;
Some(request_line(&block.text))
}
pub(crate) fn foldable(blocks: &[Block], chapter: u32) -> bool {
chapter != PRE_CHAPTER
&& chapter != current_chapter(blocks)
&& chapter_request(blocks, chapter).is_some()
}
pub(crate) fn latest_foldable(blocks: &[Block]) -> Option<u32> {
let current = current_chapter(blocks);
(1..current)
.rev()
.find(|&chapter| foldable(blocks, chapter))
}
pub(crate) fn unfolded_row_count(block: &Block, width: usize) -> usize {
if block.text.is_empty() {
return 1;
}
let mut rows = if Row::label(block.kind).is_some() {
1
} else {
0
};
for raw in block.text.split('\n') {
rows += body_row_count(block, raw, width);
}
rows
}
pub(crate) fn folded_row(
blocks: &[Block],
chapter: u32,
start: usize,
end: usize,
width: usize,
) -> Option<Row> {
if chapter == PRE_CHAPTER || chapter == current_chapter(blocks) {
return None;
}
let run = &blocks[start..end];
let opener = run.iter().find(|b| b.kind == BlockKind::User)?;
let request = request_line(&opener.text);
let eff = width.max(1);
let shown: usize = run.iter().map(|b| unfolded_row_count(b, eff)).sum();
let marker = format!(" ▸ {} lines", shown.saturating_sub(1).max(1));
let lead = label(BlockKind::User).unwrap_or("YOU");
let prefix = format!("{lead} ");
let room = eff.saturating_sub(cell_width(&marker) + cell_width(&prefix));
let fit = room.saturating_sub(1).max(1);
let mut text = truncate_cells(&request, fit);
if cell_width(&request) > fit {
text.push('…');
}
Some(Row::body(BlockKind::User, prefix + &text + &marker))
}
fn body_row_count(block: &Block, raw: &str, width: usize) -> usize {
if raw.is_empty() || block.kind == BlockKind::Table {
return 1;
}
let mut probe: WrappedLines = Vec::new();
wrap_word_aware(raw, width.max(1), block.kind, &mut probe);
probe.len().max(1)
}
fn request_line(text: &str) -> String {
text.split('\n')
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
impl Transcript {
pub(crate) fn is_folded(&self, chapter: u32) -> bool {
self.folded.contains(&chapter)
}
pub(crate) fn toggle_chapter(&mut self, chapter: u32) -> bool {
if self.is_folded(chapter) {
self.folded.remove(&chapter);
self.invalidate_cache();
return true;
}
if !foldable(&self.blocks, chapter) {
return false;
}
self.folded.insert(chapter);
self.invalidate_cache();
true
}
pub(crate) fn toggle_latest_chapter(&mut self) -> bool {
let next = latest_foldable(&self.blocks);
next.is_some_and(|chapter| self.toggle_chapter(chapter))
}
pub(crate) fn auto_fold_finished_chapter(&mut self) -> bool {
if !self.is_following_tail() {
return false;
}
let chapter = current_chapter(&self.blocks);
if chapter == PRE_CHAPTER
|| self.is_folded(chapter)
|| chapter_request(&self.blocks, chapter).is_none()
{
return false;
}
self.folded.insert(chapter);
self.invalidate_cache();
true
}
}
#[cfg(test)]
#[path = "chapters_tests.rs"]
mod chapter_tests;
#[cfg(test)]
#[path = "autofold_tests.rs"]
mod autofold_tests;
#[cfg(test)]
#[path = "fold_cell_tests.rs"]
mod fold_cell_tests;