clankerdiff-ratatui 0.1.7

Embeddable Ratatui diff review widget
Documentation
use ratatui::{
    style::Style,
    text::{Line, Span},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

#[derive(Clone, Copy)]
pub(crate) struct FitOptions<'a> {
    pub width: usize,
    pub wrap: bool,
    pub tab_width: usize,
    pub continuation: &'a str,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct FitPosition {
    pub byte: usize,
    pub tab_remaining: usize,
}

pub(crate) fn fit_spans(spans: Vec<Span<'_>>, options: FitOptions<'_>) -> Vec<Line<'static>> {
    fit_spans_from(spans, options, FitPosition::default())
        .into_iter()
        .map(|(line, _)| line)
        .collect()
}

pub(crate) fn fit_spans_from(
    spans: Vec<Span<'_>>,
    options: FitOptions<'_>,
    from: FitPosition,
) -> Vec<(Line<'static>, FitPosition)> {
    let text: String = spans.iter().map(|span| span.content.as_ref()).collect();
    if options.width == 0 {
        return vec![(
            Line::default(),
            FitPosition {
                byte: text.len(),
                tab_remaining: 0,
            },
        )];
    }
    let mut boundaries = Vec::with_capacity(spans.len());
    let mut offset = 0;
    for span in spans {
        offset += span.content.len();
        boundaries.push((offset, span.style));
    }
    let mut lines = Vec::new();
    let mut current = Vec::new();
    let mut used = 0;
    let mut clipped = false;
    let mut position = from;
    let mut resume = from != FitPosition::default();
    for (offset, grapheme) in text
        .grapheme_indices(true)
        .filter(|(offset, _)| *offset >= from.byte)
    {
        let index = boundaries.partition_point(|(end, _)| *end <= offset);
        let style = boundaries
            .get(index)
            .map_or(Style::default(), |(_, style)| *style);
        let next = FitPosition {
            byte: offset + grapheme.len(),
            tab_remaining: 0,
        };
        if resume {
            used = push_continuation(&mut current, options, style, grapheme.width());
            resume = false;
        }
        if matches!(grapheme, "\n" | "\r\n" | "\r") {
            lines.push((Line::from(std::mem::take(&mut current)), next));
            used = 0;
            clipped = false;
            position = next;
            continue;
        }
        if clipped {
            position = next;
            continue;
        }
        let expanded;
        let is_tab = grapheme == "\t";
        let grapheme = if is_tab {
            let remaining = if offset == from.byte && from.tab_remaining > 0 {
                from.tab_remaining
            } else {
                options.tab_width.max(1) - used % options.tab_width.max(1)
            };
            expanded = " ".repeat(remaining);
            expanded.as_str()
        } else {
            grapheme
        };
        let expanded_len = grapheme.len();
        for (part, grapheme) in grapheme.grapheme_indices(true) {
            let width = grapheme.width();
            let at = FitPosition {
                byte: offset,
                tab_remaining: if is_tab { expanded_len - part } else { 0 },
            };
            if used + width > options.width {
                if !options.wrap {
                    clipped = true;
                    break;
                }
                if used > 0 {
                    lines.push((Line::from(std::mem::take(&mut current)), at));
                    used = push_continuation(&mut current, options, style, width);
                }
                if width > options.width {
                    continue;
                }
            }
            push_span(&mut current, grapheme, style);
            used += width;
        }
        position = next;
    }
    lines.push((Line::from(current), position));
    lines
}

fn push_continuation(
    spans: &mut Vec<Span<'static>>,
    options: FitOptions<'_>,
    style: Style,
    next_width: usize,
) -> usize {
    let mut used = 0;
    for prefix in options.continuation.graphemes(true) {
        let width = prefix.width();
        if used + width + next_width > options.width {
            break;
        }
        push_span(spans, prefix, style);
        used += width;
    }
    used
}

fn push_span(spans: &mut Vec<Span<'static>>, text: &str, style: Style) {
    if let Some(last) = spans.last_mut().filter(|span| span.style == style) {
        last.content.to_mut().push_str(text);
    } else {
        spans.push(Span::styled(text.to_owned(), style));
    }
}