fmtview 0.2.1

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use super::{
    cache::RenderedLineCache,
    line::rendered_row_count,
    search::apply_search_highlight,
    types::{RenderContext, RenderRequest, RenderedViewport, ViewPosition, ViewportBottom},
};

pub(in crate::viewer) fn render_viewport(
    lines: &[String],
    first_line_number: usize,
    top_row_offset: usize,
    height: usize,
    request: RenderRequest,
    cache: &mut RenderedLineCache,
    search_query: Option<&str>,
) -> RenderedViewport {
    let mut rendered = Vec::with_capacity(height);
    let mut last_line_number = None;

    let Some((top_line, remaining_lines)) = lines.split_first() else {
        return RenderedViewport {
            lines: rendered,
            last_line_number,
            bottom: None,
        };
    };

    let mut bottom = None;
    if height > 0 {
        let top_rows = cache.get_or_render_window(
            top_line,
            first_line_number,
            top_row_offset,
            height.saturating_add(1),
            request,
        );
        if !top_rows.is_empty() {
            last_line_number = Some(first_line_number);
        }
        if search_query.is_some() {
            for row in top_rows.into_iter().take(height) {
                bottom = Some(ViewportBottom {
                    line_index: first_line_number - 1,
                    byte_end: row.end_byte,
                    line_end: row.line_end,
                });
                rendered.push(apply_search_highlight(
                    row.line,
                    search_query,
                    request.context.gutter_digits,
                ));
            }
        } else {
            for row in top_rows.into_iter().take(height) {
                bottom = Some(ViewportBottom {
                    line_index: first_line_number - 1,
                    byte_end: row.end_byte,
                    line_end: row.line_end,
                });
                rendered.push(row.line);
            }
        }
    }

    for (index, line) in remaining_lines.iter().enumerate() {
        if rendered.len() >= height {
            break;
        }

        let remaining = height - rendered.len();
        let line_number = first_line_number + index + 1;
        let rows = cache.get_or_render_window(line, line_number, 0, remaining, request);
        let taken = rows.len().min(remaining);
        if taken > 0 {
            last_line_number = Some(line_number);
        }
        if search_query.is_some() {
            for row in rows.into_iter().take(remaining) {
                bottom = Some(ViewportBottom {
                    line_index: line_number - 1,
                    byte_end: row.end_byte,
                    line_end: row.line_end,
                });
                rendered.push(apply_search_highlight(
                    row.line,
                    search_query,
                    request.context.gutter_digits,
                ));
            }
        } else {
            for row in rows.into_iter().take(remaining) {
                bottom = Some(ViewportBottom {
                    line_index: line_number - 1,
                    byte_end: row.end_byte,
                    line_end: row.line_end,
                });
                rendered.push(row.line);
            }
        }
    }

    RenderedViewport {
        lines: rendered,
        last_line_number,
        bottom,
    }
}

#[cfg(test)]
pub(in crate::viewer) fn viewport_reaches_file_end(
    viewport: &RenderedViewport,
    line_count: usize,
) -> bool {
    viewport
        .bottom
        .is_some_and(|bottom| bottom.line_end && bottom.line_index + 1 >= line_count)
}

pub(in crate::viewer) fn exact_top_line_tail_offset(
    lines: &[String],
    visible_height: usize,
    context: RenderContext,
) -> usize {
    if visible_height == 0 || !context.wrap {
        return 0;
    }

    let Some(line) = lines.first() else {
        return 0;
    };

    rendered_row_count(line, context).saturating_sub(visible_height)
}

pub(in crate::viewer) fn effective_top_row_offset(
    line_number: usize,
    visible_height: usize,
    context: RenderContext,
    cache: &RenderedLineCache,
    tail: Option<ViewPosition>,
) -> usize {
    let mut max_offset = top_line_tail_offset(line_number, visible_height, context, cache);
    if context.wrap
        && let Some(tail) = tail
        && tail.top + 1 == line_number
    {
        max_offset = max_offset.max(tail.row_offset);
    }
    max_offset
}

pub(in crate::viewer) fn top_line_tail_offset(
    line_number: usize,
    visible_height: usize,
    context: RenderContext,
    cache: &RenderedLineCache,
) -> usize {
    if visible_height == 0 || !context.wrap {
        return 0;
    }

    let status = cache.status(line_number);
    match status.total_rows {
        Some(rows) => rows.saturating_sub(visible_height),
        None if status.known_rows > 0 => usize::MAX,
        None => 0,
    }
}