fmtview 0.2.1

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use std::collections::{HashMap, VecDeque, hash_map::Entry};

use anyhow::Result;
use ratatui::text::Line;

use crate::line_index::ViewFile;

use super::super::{
    RENDER_CACHE_MAX_LINES, WRAP_RENDER_CHUNK_ROWS, WRAP_RENDER_CHUNKS_PER_LINE,
    highlight::HighlightCheckpointIndex,
};
use super::{
    line::render_logical_line_window_with_status_indexed, types::RenderRequest,
    wrap::WrapCheckpointIndex,
};

#[derive(Debug, Default)]
pub(in crate::viewer) struct LineWindowCache {
    pub(in crate::viewer) start: usize,
    pub(in crate::viewer) lines: Vec<String>,
}

pub(in crate::viewer) struct LineWindow<'a> {
    pub(in crate::viewer) lines: &'a [String],
}

impl LineWindowCache {
    pub(in crate::viewer) fn read(
        &mut self,
        file: &dyn ViewFile,
        top: usize,
        height: usize,
        margin: usize,
    ) -> Result<LineWindow<'_>> {
        if height == 0 || (file.line_count_exact() && top >= file.line_count()) {
            return Ok(LineWindow { lines: &[] });
        }

        let cached_end = self.start.saturating_add(self.lines.len());
        let requested_end = if file.line_count_exact() {
            top.saturating_add(height).min(file.line_count())
        } else {
            top.saturating_add(height)
        };
        if top >= self.start && requested_end <= cached_end {
            let start = top - self.start;
            let end = requested_end - self.start;
            return Ok(LineWindow {
                lines: &self.lines[start..end],
            });
        }

        let fetch_start = top.saturating_sub(margin);
        let fetch_count = if file.line_count_exact() {
            height
                .saturating_add(margin.saturating_mul(2))
                .min(file.line_count().saturating_sub(fetch_start))
        } else {
            height.saturating_add(margin.saturating_mul(2))
        };
        self.lines = file.read_window(fetch_start, fetch_count)?;
        self.start = fetch_start;

        let start = top - self.start;
        let end = requested_end
            .saturating_sub(self.start)
            .min(self.lines.len());
        Ok(LineWindow {
            lines: &self.lines[start..end],
        })
    }
}

#[derive(Debug, Default)]
pub(in crate::viewer) struct RenderedLineCache {
    pub(in crate::viewer) request: Option<RenderRequest>,
    pub(in crate::viewer) lines: HashMap<usize, CachedRenderedLine>,
    pub(in crate::viewer) order: VecDeque<usize>,
}

#[derive(Debug, Clone)]
pub(in crate::viewer) struct RenderedVisualRow {
    pub(in crate::viewer) line: Line<'static>,
    pub(in crate::viewer) end_byte: usize,
    pub(in crate::viewer) line_end: bool,
}

#[derive(Debug, Default)]
pub(in crate::viewer) struct CachedRenderedLine {
    pub(in crate::viewer) chunks: VecDeque<RenderedLineChunk>,
    pub(in crate::viewer) total_rows: Option<usize>,
    pub(in crate::viewer) index: LineRenderIndex,
}

#[derive(Debug)]
pub(in crate::viewer) struct RenderedLineChunk {
    pub(in crate::viewer) start_row: usize,
    pub(in crate::viewer) rows: Vec<RenderedVisualRow>,
}

#[derive(Debug, Clone, Copy)]
pub(in crate::viewer) struct RenderedLineStatus {
    pub(in crate::viewer) known_rows: usize,
    pub(in crate::viewer) total_rows: Option<usize>,
}

#[derive(Debug, Default)]
pub(in crate::viewer) struct LineRenderIndex {
    pub(in crate::viewer) wrap: WrapCheckpointIndex,
    pub(in crate::viewer) highlight: HighlightCheckpointIndex,
}

impl RenderedLineCache {
    pub(in crate::viewer) fn get_or_render(
        &mut self,
        line: &str,
        line_number: usize,
        request: RenderRequest,
    ) -> Vec<Line<'static>> {
        self.get_or_render_window(line, line_number, 0, request.row_limit, request)
            .into_iter()
            .map(|row| row.line)
            .collect()
    }

    pub(in crate::viewer) fn get_or_render_window(
        &mut self,
        line: &str,
        line_number: usize,
        row_start: usize,
        max_rows: usize,
        request: RenderRequest,
    ) -> Vec<RenderedVisualRow> {
        if self.request != Some(request) {
            self.request = Some(request);
            self.lines.clear();
            self.order.clear();
        }

        if max_rows == 0 {
            return Vec::new();
        }

        if !self.lines.contains_key(&line_number) {
            self.evict_until_room();
            self.order.push_back(line_number);
        }

        match self.lines.entry(line_number) {
            Entry::Occupied(mut entry) => {
                entry
                    .get_mut()
                    .render_window(line, line_number, row_start, max_rows, request)
            }
            Entry::Vacant(entry) => {
                let mut cached = CachedRenderedLine::default();
                let rows = cached.render_window(line, line_number, row_start, max_rows, request);
                entry.insert(cached);
                rows
            }
        }
    }

    pub(in crate::viewer) fn status(&self, line_number: usize) -> RenderedLineStatus {
        self.lines
            .get(&line_number)
            .map(CachedRenderedLine::status)
            .unwrap_or(RenderedLineStatus {
                known_rows: 0,
                total_rows: None,
            })
    }

    pub(in crate::viewer) fn evict_until_room(&mut self) {
        while self.lines.len() >= RENDER_CACHE_MAX_LINES {
            if let Some(line_number) = self.order.pop_front() {
                self.lines.remove(&line_number);
            } else {
                break;
            }
        }
    }
}

impl CachedRenderedLine {
    pub(in crate::viewer) fn render_window(
        &mut self,
        line: &str,
        line_number: usize,
        row_start: usize,
        max_rows: usize,
        request: RenderRequest,
    ) -> Vec<RenderedVisualRow> {
        if let Some(rows) = self.cached_window(row_start, max_rows) {
            return rows;
        }

        if self
            .total_rows
            .is_some_and(|total_rows| row_start >= total_rows)
        {
            return Vec::new();
        }

        let chunk_rows = if request.context.wrap {
            max_rows.max(WRAP_RENDER_CHUNK_ROWS)
        } else {
            max_rows
        };
        let rendered = render_logical_line_window_with_status_indexed(
            line,
            line_number,
            row_start,
            chunk_rows,
            request.context,
            &mut self.index,
        );
        if let Some(total_rows) = rendered.total_rows {
            self.total_rows = Some(total_rows);
        }
        if !rendered.rows.is_empty() {
            self.chunks.push_back(RenderedLineChunk {
                start_row: row_start,
                rows: rendered.rows,
            });
            while self.chunks.len() > WRAP_RENDER_CHUNKS_PER_LINE {
                self.chunks.pop_front();
            }
        }

        self.cached_window(row_start, max_rows).unwrap_or_default()
    }

    pub(in crate::viewer) fn cached_window(
        &self,
        row_start: usize,
        max_rows: usize,
    ) -> Option<Vec<RenderedVisualRow>> {
        let desired_end = row_start.saturating_add(max_rows);
        self.chunks.iter().find_map(|chunk| {
            let chunk_end = chunk.start_row.saturating_add(chunk.rows.len());
            if row_start < chunk.start_row || row_start >= chunk_end {
                return None;
            }
            if chunk_end < desired_end
                && self
                    .total_rows
                    .is_none_or(|total_rows| total_rows > chunk_end)
            {
                return None;
            }
            let start = row_start - chunk.start_row;
            let end = start.saturating_add(max_rows).min(chunk.rows.len());
            Some(chunk.rows[start..end].to_vec())
        })
    }

    pub(in crate::viewer) fn status(&self) -> RenderedLineStatus {
        let known_rows = self
            .chunks
            .iter()
            .map(|chunk| chunk.start_row.saturating_add(chunk.rows.len()))
            .max()
            .unwrap_or(0);
        RenderedLineStatus {
            known_rows,
            total_rows: self.total_rows,
        }
    }
}