use std::ops::Range;
use std::rc::Rc;
use crate::document::ParsedDoc;
use crate::ui::line_render::visual_rows_of_str;
#[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>,
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>>,
}
#[derive(Debug, Clone, Default)]
pub struct EffectiveRowsCache {
key: Option<(u64, usize, Option<usize>)>,
base_total: usize,
patch: Option<Rc<Patch>>,
}
impl EffectiveRowsCache {
pub fn get(&self, key: (u64, usize, Option<usize>)) -> Option<(usize, Option<Rc<Patch>>)> {
(self.key == Some(key)).then(|| (self.base_total, self.patch.clone()))
}
pub fn store(
&mut self,
key: (u64, usize, Option<usize>),
base_total: usize,
patch: Option<Rc<Patch>>,
) {
self.key = Some(key);
self.base_total = base_total;
self.patch = patch;
}
}
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>,
raw_lines: &[&str],
) -> 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, 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).copied())
.unwrap_or(1)
}
pub fn raw_line_count(&self) -> usize {
self.patch.as_ref().map_or(0, |p| 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");
p.base_before + p.raw_prefix[raw_line.min(p.raw_wrap.len())]
}
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, 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>, raw_lines: &[&str]) -> 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 in raw_lines {
let rows = visual_rows_of_str(line, width).len().max(1);
raw_wrap.push(rows);
acc += rows;
raw_prefix.push(acc);
}
Self {
rendered,
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::ParsedDoc;
use crate::ui::line_render::visual_rows_for_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>, &[&str])>,
) -> Vec<RowHit> {
let mut out = Vec::new();
let n = parsed.lines.len();
let (start, end, raw): (usize, usize, &[&str]) = 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 (raw_line, text) in raw.iter().enumerate() {
let rows = visual_rows_of_str(text, width).len().max(1);
for sub in 0..rows {
out.push(RowHit::Raw { raw_line, sub });
}
}
}
if line >= start && line < end {
continue; }
let rows = visual_rows_for_line(&parsed.lines[line], width).max(1);
for sub in 0..rows {
out.push(RowHit::Rendered { line, sub });
}
}
out
}
fn check_against_brute_force(
parsed: &ParsedDoc,
width: usize,
reveal: Option<(Range<usize>, Vec<&str>)>,
) {
let er = match &reveal {
None => EffectiveRows::identity(parsed, width),
Some((r, raw)) => EffectiveRows::with_reveal(parsed, width, r.clone(), 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 = vec!["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 = vec!["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 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 = vec!["one", "two", "three"];
let width = 8;
let er = EffectiveRows::with_reveal(&parsed, width, block..block + 1, &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 });
}
}
}