fmtview 0.2.1

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

use super::super::super::{
    ViewMode,
    highlight::highlight_content_window,
    palette::{gutter_style, plain_style},
    render::{
        byte_index_for_char, char_count, continuation_indent, wrap_ranges_window_indexed,
        wrapped_row_count,
    },
};
use super::styles::{DiffCellStyle, push_diff_span_segments, slice_char_range};

pub(super) struct NumberedCell<'a> {
    pub(super) number: Option<usize>,
    pub(super) digits: usize,
    pub(super) content: Option<&'a str>,
    pub(super) diff_style: Option<DiffCellStyle>,
}

impl NumberedCell<'_> {
    fn bg_style(&self) -> Option<Style> {
        self.diff_style.map(|style| style.line_style())
    }

    fn marker(&self) -> char {
        self.diff_style
            .filter(|_| self.content.is_some())
            .map(|style| style.marker())
            .unwrap_or(' ')
    }

    fn marker_style(&self) -> Style {
        self.diff_style
            .filter(|_| self.content.is_some())
            .map(|style| style.marker_style())
            .unwrap_or_else(gutter_style)
    }
}

pub(super) fn push_numbered_cell(
    spans: &mut Vec<Span<'static>>,
    cell: NumberedCell<'_>,
    content_width: usize,
    x: usize,
) {
    let bg_style = cell.bg_style();
    push_number(spans, cell.number, cell.digits, bg_style);
    push_styled_text(spans, " ", gutter_style(), bg_style);
    push_styled_text(
        spans,
        &cell.marker().to_string(),
        cell.marker_style(),
        bg_style,
    );
    push_styled_text(spans, " ", gutter_style(), bg_style);

    let written = cell
        .content
        .map(|content| push_structured_content(spans, content, x, content_width, cell.diff_style))
        .unwrap_or(0);
    fill_row(spans, content_width.saturating_sub(written), bg_style);
}

pub(super) fn push_numbered_cell_window(
    spans: &mut Vec<Span<'static>>,
    cell: NumberedCell<'_>,
    content_width: usize,
    row_offset: usize,
) {
    let bg_style = cell.bg_style();
    if row_offset == 0 {
        push_number(spans, cell.number, cell.digits, bg_style);
        push_styled_text(spans, " ", gutter_style(), bg_style);
        push_styled_text(
            spans,
            &cell.marker().to_string(),
            cell.marker_style(),
            bg_style,
        );
        push_styled_text(spans, " ", gutter_style(), bg_style);
    } else {
        push_styled_text(spans, &" ".repeat(cell.digits), gutter_style(), bg_style);
        push_styled_text(spans, "   ", gutter_style(), bg_style);
    }

    let Some(content) = cell.content else {
        fill_row(spans, content_width, bg_style);
        return;
    };
    push_wrapped_content_slice(
        spans,
        content,
        content_width,
        row_offset,
        cell.diff_style,
        bg_style,
        true,
    );
}

pub(super) fn wrapped_content_visual_count(content: Option<&str>, width: usize) -> usize {
    content
        .map(|content| {
            let width = width.max(1);
            wrapped_row_count(content, width, continuation_indent(content, width))
        })
        .unwrap_or(1)
}

pub(super) fn push_number(
    spans: &mut Vec<Span<'static>>,
    number: Option<usize>,
    digits: usize,
    bg_style: Option<Style>,
) {
    let text = number
        .map(|number| format!("{number:>digits$}"))
        .unwrap_or_else(|| " ".repeat(digits));
    push_styled_text(spans, &text, gutter_style(), bg_style);
}

pub(super) fn push_structured_content(
    spans: &mut Vec<Span<'static>>,
    content: &str,
    x: usize,
    width: usize,
    diff_style: Option<DiffCellStyle>,
) -> usize {
    if width == 0 {
        return 0;
    }

    let start = byte_index_for_char(content, x);
    let end = byte_index_for_char(content, x.saturating_add(width));
    let highlighted = highlight_content_window(content, ViewMode::Plain, start, end);
    let mut cursor = x;
    let mut written = 0_usize;
    for span in highlighted {
        let text = span.content.as_ref();
        let count = char_count(text);
        push_diff_span_segments(spans, text, cursor, span.style, diff_style);
        cursor = cursor.saturating_add(count);
        written = written.saturating_add(count);
    }
    written
}

pub(super) fn push_structured_content_range(
    spans: &mut Vec<Span<'static>>,
    content: &str,
    start_char: usize,
    end_char: usize,
    start_byte: usize,
    end_byte: usize,
    diff_style: Option<DiffCellStyle>,
) -> usize {
    if start_char >= end_char {
        return 0;
    }

    let highlighted = highlight_content_window(content, ViewMode::Plain, start_byte, end_byte);
    let mut cursor = start_char;
    let mut written = 0_usize;
    for span in highlighted {
        let text = span.content.as_ref();
        let count = char_count(text);
        push_diff_span_segments(spans, text, cursor, span.style, diff_style);
        cursor = cursor.saturating_add(count);
        written = written.saturating_add(count);
    }
    written
}

pub(super) fn push_wrapped_content_slice(
    spans: &mut Vec<Span<'static>>,
    content: &str,
    content_width: usize,
    row_offset: usize,
    diff_style: Option<DiffCellStyle>,
    bg_style: Option<Style>,
    fill: bool,
) -> usize {
    let content_width = content_width.max(1);
    let indent = continuation_indent(content, content_width);
    let wrap_window =
        wrap_ranges_window_indexed(content, content_width, indent, row_offset, 1, None);
    let Some(range) = wrap_window.ranges.first() else {
        if fill {
            fill_row(spans, content_width, bg_style);
        }
        return 0;
    };
    if range.continuation_indent > 0 {
        push_styled_text(
            spans,
            &" ".repeat(range.continuation_indent),
            plain_style(),
            bg_style,
        );
    }
    let written = range
        .continuation_indent
        .saturating_add(push_structured_content_range(
            spans,
            content,
            range.start_char,
            range.end_char,
            range.start_byte,
            range.end_byte,
            diff_style,
        ));
    if fill {
        fill_row(spans, content_width.saturating_sub(written), bg_style);
    }
    written
}

pub(super) fn push_styled_text(
    spans: &mut Vec<Span<'static>>,
    text: &str,
    style: Style,
    bg_style: Option<Style>,
) {
    let style = bg_style
        .map(|bg_style| style.patch(bg_style))
        .unwrap_or(style);
    spans.push(Span::styled(text.to_owned(), style));
}

pub(super) fn fill_row(spans: &mut Vec<Span<'static>>, count: usize, bg_style: Option<Style>) {
    if count == 0 {
        return;
    }

    spans.push(Span::styled(
        " ".repeat(count),
        bg_style.unwrap_or_default(),
    ));
}

pub(super) fn styled_text_line(text: &str, width: usize, style: Style) -> Line<'static> {
    let end = byte_index_for_char(text, width);
    Line::from(vec![Span::styled(text[..end].to_owned(), style)])
}

pub(super) fn render_message_window(
    text: &str,
    row_offset: usize,
    height: usize,
    width: usize,
    style: Style,
) -> Vec<Line<'static>> {
    let width = width.max(1);
    let window = wrap_ranges_window_indexed(text, width, 0, row_offset, height, None);
    window
        .ranges
        .iter()
        .map(|range| {
            Line::from(vec![Span::styled(
                slice_char_range(text, range.start_char, range.end_char),
                style,
            )])
        })
        .collect()
}