fmtview 0.2.1

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

use super::super::{
    WRAP_GUTTER_MAJOR_TICK_ROWS, WRAP_GUTTER_MINOR_TICK_ROWS,
    palette::{gutter_style, plain_style},
};

pub(in crate::viewer) fn line_number_gutter(
    line_number: usize,
    gutter_digits: usize,
) -> Span<'static> {
    Span::styled(format!("{line_number:>gutter_digits$} │ "), gutter_style())
}

pub(in crate::viewer) fn continuation_gutter(
    row_index: usize,
    gutter_digits: usize,
) -> Span<'static> {
    let marker = continuation_gutter_marker(row_index);
    Span::styled(format!("{:>gutter_digits$} {marker} ", ""), gutter_style())
}

pub(in crate::viewer) fn continuation_gutter_marker(row_index: usize) -> char {
    if row_index > 0 && row_index % WRAP_GUTTER_MAJOR_TICK_ROWS == 0 {
        ''
    } else if row_index > 0 && row_index % WRAP_GUTTER_MINOR_TICK_ROWS == 0 {
        ''
    } else {
        ''
    }
}

pub(in crate::viewer) fn format_count(value: usize) -> String {
    let raw = value.to_string();
    let mut formatted = String::with_capacity(raw.len() + raw.len() / 3);
    let first_group = raw.len() % 3;
    for (index, ch) in raw.chars().enumerate() {
        if index > 0
            && (index == first_group || (index > first_group && (index - first_group) % 3 == 0))
        {
            formatted.push(',');
        }
        formatted.push(ch);
    }
    formatted
}

pub(in crate::viewer) fn slice_spans(
    spans: &[Span<'static>],
    start: usize,
    end: usize,
) -> Vec<Span<'static>> {
    if end <= start {
        return Vec::new();
    }

    let mut sliced = Vec::new();
    let mut cursor = 0;

    for span in spans {
        let text = span.content.as_ref();
        let len = char_count(text);
        let span_start = cursor;
        let span_end = cursor + len;
        cursor = span_end;

        let overlap_start = start.max(span_start);
        let overlap_end = end.min(span_end);
        if overlap_start >= overlap_end {
            continue;
        }

        let text = slice_chars(text, overlap_start - span_start, overlap_end - span_start);
        push_styled_span(&mut sliced, text, span.style);
    }

    sliced
}

pub(in crate::viewer) fn push_styled_span(
    spans: &mut Vec<Span<'static>>,
    text: String,
    style: Style,
) {
    let style = if style == plain_style() {
        Style::default()
    } else {
        style
    };

    if text.is_empty() {
        return;
    }

    if let Some(previous) = spans.last_mut()
        && previous.style == style
    {
        previous.content.to_mut().push_str(&text);
        return;
    }

    spans.push(Span::styled(text, style));
}

pub(in crate::viewer) fn slice_chars(text: &str, start: usize, end: usize) -> String {
    if end <= start {
        return String::new();
    }

    if text.is_ascii() {
        let start = start.min(text.len());
        let end = end.min(text.len());
        if end <= start {
            return String::new();
        }
        return text[start..end].to_owned();
    }

    text.chars().skip(start).take(end - start).collect()
}

pub(in crate::viewer) fn char_count(text: &str) -> usize {
    if text.is_ascii() {
        text.len()
    } else {
        text.chars().count()
    }
}

pub(in crate::viewer) fn byte_index_for_char(line: &str, char_index: usize) -> usize {
    if line.is_ascii() {
        return char_index.min(line.len());
    }

    line.char_indices()
        .nth(char_index)
        .map(|(index, _)| index)
        .unwrap_or(line.len())
}