fmtview 0.2.1

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use ratatui::text::{Line, Span};

use super::super::{
    ViewMode,
    highlight::{highlight_content, highlight_content_window_indexed},
    palette::plain_style,
};
use super::{
    cache::{LineRenderIndex, RenderedVisualRow},
    text::{
        byte_index_for_char, char_count, continuation_gutter, line_number_gutter, push_styled_span,
        slice_chars, slice_spans,
    },
    types::RenderContext,
    wrap::{continuation_indent, next_wrap_end, wrap_ranges_window_indexed},
};

#[cfg(test)]
pub(in crate::viewer) fn render_logical_line(
    line: &str,
    line_number: usize,
    max_rows: usize,
    context: RenderContext,
) -> Vec<Line<'static>> {
    render_logical_line_window(line, line_number, 0, max_rows, context)
}

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

#[cfg(test)]
pub(in crate::viewer) fn render_logical_line_window(
    line: &str,
    line_number: usize,
    row_start: usize,
    max_rows: usize,
    context: RenderContext,
) -> Vec<Line<'static>> {
    render_logical_line_window_with_status(line, line_number, row_start, max_rows, context)
        .rows
        .into_iter()
        .map(|row| row.line)
        .collect()
}

#[cfg(test)]
pub(in crate::viewer) fn render_logical_line_window_with_status(
    line: &str,
    line_number: usize,
    row_start: usize,
    max_rows: usize,
    context: RenderContext,
) -> RenderedLineWindow {
    let mut index = LineRenderIndex::default();
    render_logical_line_window_with_status_indexed(
        line,
        line_number,
        row_start,
        max_rows,
        context,
        &mut index,
    )
}

pub(in crate::viewer) fn render_logical_line_window_with_status_indexed(
    line: &str,
    line_number: usize,
    row_start: usize,
    max_rows: usize,
    context: RenderContext,
    index: &mut LineRenderIndex,
) -> RenderedLineWindow {
    if max_rows == 0 {
        return RenderedLineWindow {
            rows: Vec::new(),
            total_rows: None,
        };
    }

    if !context.wrap {
        if row_start > 0 {
            return RenderedLineWindow {
                rows: Vec::new(),
                total_rows: Some(1),
            };
        }
        let line_chars = char_count(line);
        return RenderedLineWindow {
            rows: vec![RenderedVisualRow {
                line: styled_segment(
                    line_number_gutter(line_number, context.gutter_digits),
                    line,
                    context.x,
                    context.x.saturating_add(context.width),
                    context.mode,
                ),
                end_byte: byte_index_for_char(
                    line,
                    context.x.saturating_add(context.width).min(line_chars),
                ),
                line_end: context.x.saturating_add(context.width) >= line_chars,
            }],
            total_rows: Some(1),
        };
    }

    let wrap_window = wrap_ranges_window_indexed(
        line,
        context.width,
        continuation_indent(line, context.width),
        row_start,
        max_rows,
        Some(&mut index.wrap),
    );
    let visible_ranges = wrap_window.ranges;
    let total_rows = wrap_window.total_rows;
    let highlight_end_byte = visible_ranges
        .iter()
        .map(|range| range.end_byte)
        .max()
        .unwrap_or(0);
    let Some(first_range) = visible_ranges.first() else {
        return RenderedLineWindow {
            rows: Vec::new(),
            total_rows,
        };
    };
    let highlight_start_byte = first_range.start_byte;
    let highlight_start_char = first_range.start_char;
    let spans = highlight_content_window_indexed(
        line,
        context.mode,
        highlight_start_byte,
        highlight_end_byte,
        Some(&mut index.highlight),
    );
    let rows = visible_ranges
        .iter()
        .enumerate()
        .map(|(index, range)| {
            let row_index = row_start + index;
            let gutter = if row_index == 0 {
                line_number_gutter(line_number, context.gutter_digits)
            } else {
                continuation_gutter(row_index, context.gutter_digits)
            };
            let mut line_spans = vec![gutter];
            if range.continuation_indent > 0 {
                push_styled_span(
                    &mut line_spans,
                    " ".repeat(range.continuation_indent),
                    plain_style(),
                );
            }
            line_spans.extend(slice_spans(
                &spans,
                range.start_char - highlight_start_char,
                range.end_char - highlight_start_char,
            ));
            RenderedVisualRow {
                line: Line::from(line_spans),
                end_byte: range.end_byte,
                line_end: range.end_byte >= line.len(),
            }
        })
        .collect();
    RenderedLineWindow { rows, total_rows }
}

pub(in crate::viewer) fn rendered_row_count(line: &str, context: RenderContext) -> usize {
    if !context.wrap {
        return 1;
    }

    wrapped_row_count(
        line,
        context.width,
        continuation_indent(line, context.width),
    )
}

pub(in crate::viewer) fn wrapped_row_count(
    line: &str,
    width: usize,
    continuation_indent: usize,
) -> usize {
    if line.is_empty() || width == 0 {
        return 1;
    }

    let mut rows = 0_usize;
    let mut start_byte = 0_usize;
    let mut start_char = 0_usize;
    while start_byte < line.len() {
        let continuation = rows > 0;
        let indent = if continuation {
            continuation_indent.min(width.saturating_sub(1))
        } else {
            0
        };
        let row_width = width.saturating_sub(indent).max(1);
        let (end_byte, end_char) = next_wrap_end(line, start_byte, start_char, row_width);
        start_byte = end_byte.max(start_byte + 1).min(line.len());
        start_char = end_char.max(start_char + 1);
        rows = rows.saturating_add(1);
    }

    rows
}

pub(in crate::viewer) fn styled_segment(
    gutter: Span<'static>,
    line: &str,
    start: usize,
    end: usize,
    mode: ViewMode,
) -> Line<'static> {
    let mut spans = Vec::new();
    spans.push(gutter);
    let highlight_prefix = slice_chars(line, 0, end);
    spans.extend(slice_spans(
        &highlight_content(&highlight_prefix, mode),
        start,
        end,
    ));
    Line::from(spans)
}