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 crate::diff::{DiffModel, UnifiedDiffRow};

use super::super::super::{
    palette::{diff_added_style, diff_removed_style, gutter_style, plain_style},
    render::wrapped_row_count,
};
use super::styles::DiffSide;
use super::{
    DiffCellStyle, fill_row, push_number, push_structured_content, push_styled_text,
    push_wrapped_content_slice, render_message_window, styled_text_line,
    wrapped_content_visual_count,
};

fn render_unified_row(
    row: &UnifiedDiffRow,
    model: &DiffModel,
    width: usize,
    x: usize,
) -> Line<'static> {
    match row {
        UnifiedDiffRow::Message { text } => styled_text_line(text, width, plain_style()),
        UnifiedDiffRow::Context {
            left,
            right,
            content,
        } => render_unified_content(
            Some(*left),
            Some(*right),
            ' ',
            gutter_style(),
            content,
            model,
            width,
            x,
            None,
        ),
        UnifiedDiffRow::Delete {
            left,
            content,
            change,
        } => render_unified_content(
            Some(*left),
            None,
            '-',
            diff_removed_style(),
            content,
            model,
            width,
            x,
            Some(DiffCellStyle {
                side: DiffSide::Removed,
                change: *change,
            }),
        ),
        UnifiedDiffRow::Insert {
            right,
            content,
            change,
        } => render_unified_content(
            None,
            Some(*right),
            '+',
            diff_added_style(),
            content,
            model,
            width,
            x,
            Some(DiffCellStyle {
                side: DiffSide::Added,
                change: *change,
            }),
        ),
    }
}

pub(super) fn render_unified_row_window(
    row: &UnifiedDiffRow,
    model: &DiffModel,
    row_offset: usize,
    height: usize,
    width: usize,
    x: usize,
    wrap: bool,
) -> Vec<Line<'static>> {
    if !wrap {
        return (row_offset == 0)
            .then(|| render_unified_row(row, model, width, x))
            .into_iter()
            .collect();
    }

    match row {
        UnifiedDiffRow::Message { text } => {
            render_message_window(text, row_offset, height, width, plain_style())
        }
        UnifiedDiffRow::Context {
            left,
            right,
            content,
        } => render_unified_content_window(
            Some(*left),
            Some(*right),
            ' ',
            gutter_style(),
            content,
            model,
            row_offset,
            height,
            width,
            None,
        ),
        UnifiedDiffRow::Delete {
            left,
            content,
            change,
        } => render_unified_content_window(
            Some(*left),
            None,
            '-',
            diff_removed_style(),
            content,
            model,
            row_offset,
            height,
            width,
            Some(DiffCellStyle {
                side: DiffSide::Removed,
                change: *change,
            }),
        ),
        UnifiedDiffRow::Insert {
            right,
            content,
            change,
        } => render_unified_content_window(
            None,
            Some(*right),
            '+',
            diff_added_style(),
            content,
            model,
            row_offset,
            height,
            width,
            Some(DiffCellStyle {
                side: DiffSide::Added,
                change: *change,
            }),
        ),
    }
}

#[allow(clippy::too_many_arguments)]
fn render_unified_content(
    left: Option<usize>,
    right: Option<usize>,
    marker: char,
    marker_style: Style,
    content: &str,
    model: &DiffModel,
    width: usize,
    x: usize,
    diff_style: Option<DiffCellStyle>,
) -> Line<'static> {
    let mut spans = Vec::new();
    let bg_style = diff_style.map(|style| style.line_style());
    push_number(&mut spans, left, model.left_digits(), bg_style);
    push_styled_text(&mut spans, " ", gutter_style(), bg_style);
    push_number(&mut spans, right, model.right_digits(), bg_style);
    push_styled_text(&mut spans, " ", gutter_style(), bg_style);
    push_styled_text(&mut spans, &marker.to_string(), marker_style, bg_style);
    push_styled_text(&mut spans, " ", gutter_style(), bg_style);

    let prefix_width = model.left_digits() + model.right_digits() + 4;
    let content_width = width.saturating_sub(prefix_width);
    let used = prefix_width.saturating_add(push_structured_content(
        &mut spans,
        content,
        x,
        content_width,
        diff_style,
    ));
    if bg_style.is_some() {
        fill_row(&mut spans, width.saturating_sub(used), bg_style);
    }
    Line::from(spans)
}

#[allow(clippy::too_many_arguments)]
fn render_unified_content_window(
    left: Option<usize>,
    right: Option<usize>,
    marker: char,
    marker_style: Style,
    content: &str,
    model: &DiffModel,
    row_offset: usize,
    height: usize,
    width: usize,
    diff_style: Option<DiffCellStyle>,
) -> Vec<Line<'static>> {
    let prefix_width = model.left_digits() + model.right_digits() + 4;
    let content_width = width.saturating_sub(prefix_width).max(1);
    let bg_style = diff_style.map(|style| style.line_style());
    let visual_count = wrapped_content_visual_count(Some(content), content_width);
    let end = row_offset.saturating_add(height).min(visual_count);

    (row_offset..end)
        .map(|visual_row| {
            let mut spans = Vec::new();
            if visual_row == 0 {
                push_unified_prefix(
                    &mut spans,
                    left,
                    right,
                    marker,
                    marker_style,
                    model,
                    bg_style,
                );
            } else {
                push_unified_continuation_prefix(&mut spans, model, bg_style);
            }
            push_wrapped_content_slice(
                &mut spans,
                content,
                content_width,
                visual_row,
                diff_style,
                bg_style,
                bg_style.is_some(),
            );
            Line::from(spans)
        })
        .collect()
}

fn push_unified_prefix(
    spans: &mut Vec<Span<'static>>,
    left: Option<usize>,
    right: Option<usize>,
    marker: char,
    marker_style: Style,
    model: &DiffModel,
    bg_style: Option<Style>,
) {
    push_number(spans, left, model.left_digits(), bg_style);
    push_styled_text(spans, " ", gutter_style(), bg_style);
    push_number(spans, right, model.right_digits(), bg_style);
    push_styled_text(spans, " ", gutter_style(), bg_style);
    push_styled_text(spans, &marker.to_string(), marker_style, bg_style);
    push_styled_text(spans, " ", gutter_style(), bg_style);
}

fn push_unified_continuation_prefix(
    spans: &mut Vec<Span<'static>>,
    model: &DiffModel,
    bg_style: Option<Style>,
) {
    push_styled_text(
        spans,
        &" ".repeat(model.left_digits()),
        gutter_style(),
        bg_style,
    );
    push_styled_text(spans, " ", gutter_style(), bg_style);
    push_styled_text(
        spans,
        &" ".repeat(model.right_digits()),
        gutter_style(),
        bg_style,
    );
    push_styled_text(spans, "   ", gutter_style(), bg_style);
}

pub(super) fn unified_row_visual_count(
    row: &UnifiedDiffRow,
    model: &DiffModel,
    width: usize,
) -> usize {
    match row {
        UnifiedDiffRow::Message { text } => wrapped_row_count(text, width.max(1), 0),
        UnifiedDiffRow::Context { content, .. }
        | UnifiedDiffRow::Delete { content, .. }
        | UnifiedDiffRow::Insert { content, .. } => {
            let prefix_width = model.left_digits() + model.right_digits() + 4;
            let content_width = width.saturating_sub(prefix_width).max(1);
            wrapped_content_visual_count(Some(content), content_width)
        }
    }
}