clankerdiff-markdown 0.1.6

Portable semantic Markdown parsing and review state
Documentation
use crate::{
    document::MarkdownDocument,
    incremental::{IncrementalDocument, MarkdownParseStats},
};
use std::{
    ops::Range,
    sync::atomic::{AtomicU64, Ordering},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarkdownStreamUpdate {
    pub revision: u64,
    pub changed_source: Range<usize>,
    pub changed_blocks: Range<usize>,
    pub reset: bool,
    pub finished: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MarkdownStreamChanges {
    pub first_block: usize,
    pub replaced: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MarkdownStreamIdentity(u64);

impl Default for MarkdownStreamIdentity {
    fn default() -> Self {
        static NEXT: AtomicU64 = AtomicU64::new(0);
        Self(NEXT.fetch_add(1, Ordering::Relaxed))
    }
}

#[derive(Debug, Default)]
pub struct MarkdownStream {
    inner: IncrementalDocument,
    revision: u64,
    source_revision: u64,
    identity: MarkdownStreamIdentity,
    finished: bool,
    changes: Vec<(u64, usize)>,
    replaced_at: u64,
}

impl Clone for MarkdownStream {
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            revision: self.revision,
            source_revision: self.source_revision,
            identity: MarkdownStreamIdentity::default(),
            finished: self.finished,
            changes: self.changes.clone(),
            replaced_at: self.replaced_at,
        }
    }
}

impl MarkdownStream {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn push(&mut self, chunk: &str) -> MarkdownStreamUpdate {
        if chunk.is_empty() && !self.finished {
            return self.unchanged();
        }
        let start = self.source().len();
        let reset = std::mem::take(&mut self.finished);
        let mut first_block = self.inner.document().blocks().len();
        if !chunk.is_empty() {
            first_block = self.inner.append(chunk);
            self.source_revision = self.source_revision.wrapping_add(1);
        }
        self.revision = self.revision.wrapping_add(1);
        self.record(first_block);
        self.update(start..self.source().len(), first_block, reset)
    }

    pub fn replace(&mut self, source: impl Into<String>) -> MarkdownStreamUpdate {
        let source = source.into();
        self.inner.replace(&source);
        self.source_revision = self.source_revision.wrapping_add(1);
        self.finished = false;
        self.revision = self.revision.wrapping_add(1);
        self.changes.clear();
        self.replaced_at = self.revision;
        self.update(0..self.source().len(), 0, true)
    }

    pub fn finish(&mut self) -> MarkdownStreamUpdate {
        if self.finished {
            return self.unchanged();
        }
        self.finished = true;
        self.revision = self.revision.wrapping_add(1);
        let len = self.source().len();
        self.update(len..len, self.block_count(), false)
    }

    #[must_use]
    pub fn source(&self) -> &str {
        self.inner.document().source()
    }

    #[must_use]
    pub const fn document(&self) -> &MarkdownDocument {
        self.inner.document()
    }

    #[must_use]
    pub const fn revision(&self) -> u64 {
        self.revision
    }

    #[must_use]
    pub const fn source_revision(&self) -> u64 {
        self.source_revision
    }

    #[must_use]
    pub const fn identity(&self) -> MarkdownStreamIdentity {
        self.identity
    }

    #[must_use]
    pub const fn is_finished(&self) -> bool {
        self.finished
    }

    #[must_use]
    pub const fn settled_blocks(&self) -> usize {
        self.inner.settled_blocks()
    }

    #[must_use]
    pub fn open_code_block(&self) -> Option<usize> {
        self.inner.open_code_block()
    }

    #[must_use]
    pub const fn parse_stats(&self) -> MarkdownParseStats {
        self.inner.stats()
    }

    #[must_use]
    pub fn changes_since(&self, revision: u64) -> MarkdownStreamChanges {
        if revision < self.replaced_at {
            return MarkdownStreamChanges {
                first_block: 0,
                replaced: true,
            };
        }
        let position = self
            .changes
            .partition_point(|(changed_at, _)| *changed_at <= revision);
        MarkdownStreamChanges {
            first_block: self
                .changes
                .get(position)
                .map_or_else(|| self.block_count(), |(_, block)| *block),
            replaced: false,
        }
    }

    fn block_count(&self) -> usize {
        self.inner.document().blocks().len()
    }

    fn record(&mut self, first_block: usize) {
        if first_block >= self.block_count() {
            return;
        }
        while self
            .changes
            .last()
            .is_some_and(|(_, block)| *block >= first_block)
        {
            self.changes.pop();
        }
        self.changes.push((self.revision, first_block));
    }

    fn update(
        &self,
        changed_source: Range<usize>,
        first_block: usize,
        reset: bool,
    ) -> MarkdownStreamUpdate {
        MarkdownStreamUpdate {
            revision: self.revision,
            changed_source,
            changed_blocks: first_block.min(self.block_count())..self.block_count(),
            reset,
            finished: self.finished,
        }
    }

    fn unchanged(&self) -> MarkdownStreamUpdate {
        let len = self.source().len();
        self.update(len..len, self.block_count(), false)
    }
}