fmtview 0.2.1

Fast terminal formatter and viewer for JSON, JSONL, XML-compatible markup, and formatted diffs
Documentation
use std::ops::Range;

use ratatui::text::{Line, Span};

use super::super::palette::search_match_bg;
use super::text::{char_count, push_styled_span, slice_chars};

pub(in crate::viewer) fn apply_search_highlight(
    line: Line<'static>,
    query: Option<&str>,
    gutter_digits: usize,
) -> Line<'static> {
    let Some(query) = query else {
        return line;
    };
    if query.is_empty() {
        return line;
    }

    let visual_text = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();
    let ranges = search_match_ranges(&visual_text, query, gutter_digits + 3);
    if ranges.is_empty() {
        return line;
    }

    Line {
        style: line.style,
        alignment: line.alignment,
        spans: apply_search_ranges_to_spans(&line.spans, &ranges),
    }
}

pub(in crate::viewer) fn search_match_ranges(
    text: &str,
    query: &str,
    start_char: usize,
) -> Vec<Range<usize>> {
    if query.is_empty() {
        return Vec::new();
    }

    let total_chars = char_count(text);
    if start_char >= total_chars {
        return Vec::new();
    }

    let search_text = slice_chars(text, start_char, total_chars);
    let query_len = char_count(query);
    search_text
        .match_indices(query)
        .map(|(byte_index, _)| {
            let start = start_char + char_count(&search_text[..byte_index]);
            start..start + query_len
        })
        .collect()
}

pub(in crate::viewer) fn apply_search_ranges_to_spans(
    spans: &[Span<'static>],
    ranges: &[Range<usize>],
) -> Vec<Span<'static>> {
    let mut highlighted = 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 split_points = search_split_points(span_start, span_end, ranges);
        for window in split_points.windows(2) {
            let start = window[0];
            let end = window[1];
            if start == end {
                continue;
            }

            let mut style = span.style;
            if range_is_highlighted(start, end, ranges) {
                style = style.bg(search_match_bg());
            }
            push_styled_span(
                &mut highlighted,
                slice_chars(text, start - span_start, end - span_start),
                style,
            );
        }
    }

    highlighted
}

pub(in crate::viewer) fn search_split_points(
    span_start: usize,
    span_end: usize,
    ranges: &[Range<usize>],
) -> Vec<usize> {
    let mut points = vec![span_start, span_end];
    for range in ranges {
        let start = range.start.max(span_start).min(span_end);
        let end = range.end.max(span_start).min(span_end);
        if start < end {
            points.push(start);
            points.push(end);
        }
    }
    points.sort_unstable();
    points.dedup();
    points
}

pub(in crate::viewer) fn range_is_highlighted(
    start: usize,
    end: usize,
    ranges: &[Range<usize>],
) -> bool {
    ranges
        .iter()
        .any(|range| start >= range.start && end <= range.end)
}