clankerdiff-ratatui 0.1.1

Embeddable Ratatui diff review widget
Documentation
use clankerdiff_markdown::{MarkdownDocument, MarkdownTargetId, SourceRange};
use ratatui::text::Line;
use std::{
    collections::HashMap,
    ops::Range,
    sync::{Arc, OnceLock},
};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum MarkdownPresentation {
    #[default]
    Rendered,
    SourceLines,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct MarkdownLayoutOptions {
    pub width: u16,
    pub block_spacing: bool,
    pub presentation: MarkdownPresentation,
    pub wrap: bool,
    pub heading_markers: bool,
    pub preserve_source_gaps: bool,
    pub tab_width: u16,
}
impl Default for MarkdownLayoutOptions {
    fn default() -> Self {
        Self {
            width: 80,
            block_spacing: true,
            presentation: MarkdownPresentation::Rendered,
            wrap: true,
            heading_markers: true,
            preserve_source_gaps: false,
            tab_width: 4,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarkdownRow {
    pub line: Line<'static>,
    pub source: Option<SourceRange>,
    pub target: Option<MarkdownTargetId>,
}

pub(crate) type RowChunk = Arc<[Arc<MarkdownRow>]>;
#[derive(Debug, Default)]
pub(crate) struct RowStore {
    chunks: Vec<RowChunk>,
    ends: Vec<usize>,
    len: usize,
}
impl RowStore {
    pub(crate) fn push(&mut self, chunk: RowChunk) {
        if chunk.is_empty() {
            return;
        }
        self.len += chunk.len();
        self.ends.push(self.len);
        self.chunks.push(chunk);
    }
}

#[derive(Debug, Clone, Default)]
pub struct MarkdownRows {
    store: Arc<RowStore>,
    range: Range<usize>,
}
impl MarkdownRows {
    #[must_use]
    pub fn len(&self) -> usize {
        self.range.len()
    }
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.range.is_empty()
    }
    #[must_use]
    pub fn get(&self, index: usize) -> Option<&Arc<MarkdownRow>> {
        if index >= self.len() {
            return None;
        }
        let index = self.range.start + index;
        let chunk = self.store.ends.partition_point(|end| *end <= index);
        let start = chunk
            .checked_sub(1)
            .map_or(0, |previous| self.store.ends[previous]);
        self.store.chunks[chunk].get(index - start)
    }
    #[must_use]
    pub fn slice(&self, range: Range<usize>) -> Self {
        assert!(range.start <= range.end && range.end <= self.len());
        Self {
            store: Arc::clone(&self.store),
            range: self.range.start + range.start..self.range.start + range.end,
        }
    }
    #[must_use]
    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Arc<MarkdownRow>> {
        let start = self
            .store
            .ends
            .partition_point(|end| *end <= self.range.start);
        let end = self.store.ends.partition_point(|end| *end < self.range.end)
            + usize::from(!self.range.is_empty());
        (start..end).flat_map(move |index| {
            let offset = index
                .checked_sub(1)
                .map_or(0, |previous| self.store.ends[previous]);
            let chunk = &self.store.chunks[index];
            let first = self.range.start.saturating_sub(offset);
            let last = self.range.end.saturating_sub(offset).min(chunk.len());
            chunk[first..last].iter()
        })
    }
}

#[derive(Debug, Clone, Default)]
pub struct MarkdownLayout {
    rows: MarkdownRows,
    flat: Arc<OnceLock<Arc<[Line<'static>]>>>,
    targets: Arc<HashMap<MarkdownTargetId, SourceRange>>,
}
impl MarkdownLayout {
    pub(crate) fn new(store: RowStore, document: &MarkdownDocument) -> Self {
        let range = 0..store.len;
        Self {
            rows: MarkdownRows {
                store: Arc::new(store),
                range,
            },
            flat: Arc::default(),
            targets: Arc::new(
                document
                    .targets()
                    .iter()
                    .map(|target| (target.id, target.source.clone()))
                    .collect(),
            ),
        }
    }
    #[must_use]
    pub fn row_count(&self) -> usize {
        self.rows.len()
    }
    #[must_use]
    pub fn row(&self, index: usize) -> Option<&Arc<MarkdownRow>> {
        self.rows.get(index)
    }
    #[must_use]
    pub const fn rows(&self) -> &MarkdownRows {
        &self.rows
    }
    #[must_use]
    pub fn rows_for_source_line(&self, line: usize) -> Option<Range<usize>> {
        self.matching_rows(|row| {
            row.source
                .as_ref()
                .is_some_and(|source| source.lines.start <= line && source.lines.end >= line)
        })
    }
    #[must_use]
    pub fn rows_for_target(&self, target: MarkdownTargetId) -> Option<Range<usize>> {
        self.matching_rows(|row| row.target == Some(target))
            .or_else(|| {
                let target = self.targets.get(&target)?;
                self.matching_rows(|row| {
                    row.source.as_ref().is_some_and(|source| {
                        source.bytes.start >= target.bytes.start
                            && source.bytes.end <= target.bytes.end
                    })
                })
            })
    }
    fn matching_rows(&self, matches: impl Fn(&MarkdownRow) -> bool) -> Option<Range<usize>> {
        let mut matching = self.rows.iter().enumerate().filter(|(_, row)| matches(row));
        let first = matching.next()?.0;
        let last = matching.last().map_or(first, |(index, _)| index);
        Some(first..last + 1)
    }
    #[must_use]
    pub fn materialize(&self) -> Arc<[Line<'static>]> {
        Arc::clone(
            self.flat
                .get_or_init(|| self.rows.iter().map(|row| row.line.clone()).collect()),
        )
    }
    pub(crate) fn is_materialized(&self) -> bool {
        self.flat.get().is_some()
    }
}

#[derive(Debug, Clone)]
pub struct MarkdownRowUpdate {
    pub base_revision: u64,
    pub revision: u64,
    pub first_changed_row: usize,
    pub replacement: MarkdownRows,
    pub reset: bool,
}
impl MarkdownRowUpdate {
    #[must_use]
    pub fn total_rows(&self) -> usize {
        self.first_changed_row + self.replacement.len()
    }
}