use std::ops::Range;
use std::rc::Rc;
use crate::document::wrap::{revealed_row_count, Indent};
use crate::document::ParsedDoc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowHit {
Rendered { line: usize, sub: usize },
Raw { raw_line: usize, sub: usize },
}
#[derive(Debug, Clone)]
pub struct Patch {
rendered: Range<usize>,
first_line: usize,
base_before: usize,
base_block_rows: usize,
raw_wrap: Vec<usize>,
raw_prefix: Vec<usize>,
}
pub struct EffectiveRows<'a> {
parsed: &'a ParsedDoc,
width: usize,
base_total: usize,
patch: Option<Rc<Patch>>,
}
pub type EffectiveRowsKey = (u64, usize, Option<(usize, u32, u32)>);
#[derive(Debug, Clone, Default)]
pub struct EffectiveRowsCache {
key: Option<EffectiveRowsKey>,
base_total: usize,
patch: Option<Rc<Patch>>,
}
impl EffectiveRowsCache {
pub fn get(&self, key: EffectiveRowsKey) -> Option<(usize, Option<Rc<Patch>>)> {
(self.key == Some(key)).then(|| (self.base_total, self.patch.clone()))
}
pub fn store(&mut self, key: EffectiveRowsKey, base_total: usize, patch: Option<Rc<Patch>>) {
self.key = Some(key);
self.base_total = base_total;
self.patch = patch;
}
}
pub type RevealedLine<'s> = (&'s str, Indent);
impl<'a> EffectiveRows<'a> {
pub fn identity(parsed: &'a ParsedDoc, width: usize) -> Self {
let width = width.max(1);
Self {
parsed,
width,
base_total: parsed.total_visual_rows(width),
patch: None,
}
}
pub fn with_reveal(
parsed: &'a ParsedDoc,
width: usize,
rendered: Range<usize>,
first_line: usize,
raw_lines: &[RevealedLine],
) -> Self {
let width = width.max(1);
Self {
parsed,
width,
base_total: parsed.total_visual_rows(width),
patch: Some(Rc::new(Patch::build(
parsed, width, rendered, first_line, raw_lines,
))),
}
}
pub fn from_cached(
parsed: &'a ParsedDoc,
width: usize,
base_total: usize,
patch: Option<Rc<Patch>>,
) -> Self {
Self {
parsed,
width: width.max(1),
base_total,
patch,
}
}
pub fn has_reveal(&self) -> bool {
self.patch.is_some()
}
pub fn cache_parts(&self) -> (usize, Option<Rc<Patch>>) {
(self.base_total, self.patch.clone())
}
pub fn block_rendered(&self) -> Option<Range<usize>> {
self.patch.as_ref().map(|p| p.rendered.clone())
}
pub fn raw_wrap_at(&self, raw_line: usize) -> usize {
self.patch
.as_ref()
.and_then(|p| p.raw_wrap.get(raw_line.checked_sub(p.first_line)?).copied())
.unwrap_or(1)
}
pub fn raw_lines(&self) -> Range<usize> {
self.patch
.as_ref()
.map_or(0..0, |p| p.first_line..p.first_line + p.raw_wrap.len())
}
pub fn total_visual_rows(&self) -> usize {
match &self.patch {
None => self.base_total,
Some(p) => self.base_total - p.base_block_rows + p.raw_rows_total(),
}
}
pub fn raw_line_visual_row(&self, raw_line: usize) -> usize {
let p = self
.patch
.as_ref()
.expect("raw_line_visual_row on identity view");
let idx = raw_line.saturating_sub(p.first_line).min(p.raw_wrap.len());
p.base_before + p.raw_prefix[idx]
}
pub fn line_at_visual_row(&self, v: usize) -> RowHit {
let Some(p) = &self.patch else {
let (line, sub) = self.parsed.line_at_visual_row(v, self.width);
return RowHit::Rendered { line, sub };
};
if v < p.base_before {
let (line, sub) = self.parsed.line_at_visual_row(v, self.width);
return RowHit::Rendered { line, sub };
}
let raw_total = p.raw_rows_total();
if v < p.base_before + raw_total {
let local = v - p.base_before;
let raw_line = p
.raw_prefix
.partition_point(|&s| s <= local)
.saturating_sub(1);
let sub = local - p.raw_prefix[raw_line];
return RowHit::Raw {
raw_line: p.first_line + raw_line,
sub,
};
}
let base_coord = v + p.base_block_rows - raw_total;
let (line, sub) = self.parsed.line_at_visual_row(base_coord, self.width);
RowHit::Rendered { line, sub }
}
}
impl Patch {
fn build(
parsed: &ParsedDoc,
width: usize,
rendered: Range<usize>,
first_line: usize,
raw_lines: &[RevealedLine],
) -> Self {
let base_before = parsed.visual_rows_before(rendered.start, width);
let base_end = parsed.visual_rows_before(rendered.end, width);
let base_block_rows = base_end.saturating_sub(base_before);
let mut raw_wrap = Vec::with_capacity(raw_lines.len());
let mut raw_prefix = Vec::with_capacity(raw_lines.len() + 1);
raw_prefix.push(0usize);
let mut acc = 0usize;
for &(line, indent) in raw_lines {
let rows = revealed_row_count(line, indent, width);
raw_wrap.push(rows);
acc += rows;
raw_prefix.push(acc);
}
Self {
rendered,
first_line,
base_before,
base_block_rows,
raw_wrap,
raw_prefix,
}
}
fn raw_rows_total(&self) -> usize {
*self.raw_prefix.last().unwrap_or(&0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Theme;
use crate::document::wrap::visual_rows_for_line;
use crate::document::ParsedDoc;
use ratatui::text::Line;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn reflowed(src: &str) -> ParsedDoc {
ParsedDoc::build_with_overrides(
src,
theme(),
false,
24,
None,
None,
false,
80,
false,
false,
true,
true, None,
)
}
fn expand(
parsed: &ParsedDoc,
width: usize,
reveal: Option<(Range<usize>, &[RevealedLine])>,
) -> Vec<RowHit> {
expand_from(parsed, width, reveal, 0)
}
fn expand_from(
parsed: &ParsedDoc,
width: usize,
reveal: Option<(Range<usize>, &[RevealedLine])>,
first_line: usize,
) -> Vec<RowHit> {
let mut out = Vec::new();
let n = parsed.lines.len();
let (start, end, raw): (usize, usize, &[RevealedLine]) = match &reveal {
Some((r, raw)) => (r.start, r.end, raw),
None => (n, n, &[]),
};
for line in 0..n {
if line == start && !raw.is_empty() {
for (i, &(text, indent)) in raw.iter().enumerate() {
let rows = visual_rows_for_line(&Line::raw(text), indent, width).max(1);
for sub in 0..rows {
out.push(RowHit::Raw {
raw_line: first_line + i,
sub,
});
}
}
}
if line >= start && line < end {
continue; }
let rows =
visual_rows_for_line(&parsed.lines[line], parsed.row_indent(line), width).max(1);
for sub in 0..rows {
out.push(RowHit::Rendered { line, sub });
}
}
out
}
fn flat<'s>(lines: &[&'s str]) -> Vec<RevealedLine<'s>> {
lines.iter().map(|&l| (l, Indent::NONE)).collect()
}
fn check_against_brute_force(
parsed: &ParsedDoc,
width: usize,
reveal: Option<(Range<usize>, Vec<RevealedLine>)>,
) {
let er = match &reveal {
None => EffectiveRows::identity(parsed, width),
Some((r, raw)) => EffectiveRows::with_reveal(parsed, width, r.clone(), 0, raw),
};
let expected = expand(
parsed,
width,
reveal.as_ref().map(|(r, raw)| (r.clone(), raw.as_slice())),
);
assert_eq!(
er.total_visual_rows(),
expected.len(),
"total_visual_rows disagrees with brute force at width {width}"
);
for (v, want) in expected.iter().enumerate() {
assert_eq!(
er.line_at_visual_row(v),
*want,
"line_at_visual_row({v}) disagrees at width {width}"
);
}
}
#[test]
fn identity_matches_base_at_several_widths() {
let parsed = reflowed("# Title\n\nalpha beta gamma delta epsilon\n\nlast\n");
for width in [80, 20, 12, 8] {
check_against_brute_force(&parsed, width, None);
}
}
#[test]
fn reveal_taller_than_rendered_expands() {
let parsed = reflowed("intro\n\none\ntwo\nthree\n\nafter\n");
let block = parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains("one two three")
})
.expect("reflowed paragraph must render as one line");
let raw = flat(&["one", "two", "three"]);
for width in [80, 20, 6] {
check_against_brute_force(&parsed, width, Some((block..block + 1, raw.clone())));
}
}
#[test]
fn reveal_with_wrapping_raw_lines() {
let parsed = reflowed("head\n\nalpha bravo\ncharlie delta echo\n\ntail\n");
let block = parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains("alpha bravo charlie")
})
.expect("reflowed paragraph must render as one line");
let raw = flat(&["alpha bravo", "charlie delta echo"]);
for width in [80, 10, 6] {
check_against_brute_force(&parsed, width, Some((block..block + 1, raw.clone())));
}
}
#[test]
fn a_nested_reveal_splices_one_row_inside_its_block() {
let parsed = reflowed("- one\n- alpha bravo charlie\n delta echo foxtrot\n- four\n");
let row = parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains("alpha bravo charlie delta")
})
.expect("the item's paragraph must render as one flow row");
let raw = [
("- alpha bravo charlie", Indent::hanging(2)),
(" delta echo foxtrot", Indent::hanging(2)),
];
for width in [80, 12, 9] {
let er = EffectiveRows::with_reveal(&parsed, width, row..row + 1, 1, &raw);
let expected = expand_from(&parsed, width, Some((row..row + 1, &raw)), 1);
assert_eq!(er.total_visual_rows(), expected.len(), "width {width}");
for (v, want) in expected.iter().enumerate() {
assert_eq!(er.line_at_visual_row(v), *want, "row {v} at width {width}");
}
assert_eq!(er.raw_lines(), 1..3);
for line in er.raw_lines() {
let vr = er.raw_line_visual_row(line);
assert_eq!(
expected[vr],
RowHit::Raw {
raw_line: line,
sub: 0
}
);
}
}
}
#[test]
fn raw_line_visual_row_matches_expansion() {
let parsed = reflowed("intro\n\none\ntwo\nthree\n\nafter\n");
let block = parsed
.lines
.iter()
.position(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
.contains("one two three")
})
.unwrap();
let raw = flat(&["one", "two", "three"]);
let width = 8;
let er = EffectiveRows::with_reveal(&parsed, width, block..block + 1, 0, &raw);
let expected = expand(&parsed, width, Some((block..block + 1, &raw)));
for raw_line in 0..raw.len() {
let vr = er.raw_line_visual_row(raw_line);
assert_eq!(expected[vr], RowHit::Raw { raw_line, sub: 0 });
}
}
}