diffs-rs 0.1.1

Fast terminal diff viewer and embeddable Rust diff engine
Documentation
use std::{
    cell::RefCell,
    collections::{HashMap, VecDeque},
    rc::Rc,
};

use crate::{
    storage::{ByteStore, Document, SourceId, SourceRef},
    text::{Glyph, display_text, glyphs, glyphs_from},
};

const CACHE_BYTES: usize = 16 * 1024 * 1024;
const CACHE_ENTRIES: usize = 1024;
const CHECKPOINT_GLYPHS: usize = 256;

#[derive(Clone, Copy, Default)]
struct Boundary {
    byte: usize,
    column: usize,
}

#[cfg(all(test, feature = "owner-census"))]
mod census_tests {
    use super::*;
    use crate::storage::StoreWriter;

    #[test]
    fn inspection_preserves_wrapped_geometry_and_releases_owners() {
        let mut writer = StoreWriter::new().unwrap();
        let bytes = "\tlet café = \"日本 👩‍💻\";".repeat(400);
        writer.append(bytes.as_bytes()).unwrap();
        let store = writer.finish();
        let source = store.source_ref(0, bytes.len()).unwrap();
        let cache = Cache::default();
        let lines: Vec<_> = [None, Some(24), Some(80)]
            .into_iter()
            .map(|width| cache.get_source(&store, source, width).unwrap())
            .collect();
        let addresses: Vec<_> = lines
            .iter()
            .map(|line| {
                (
                    line.text.as_ptr(),
                    line.starts.as_ptr(),
                    line.checkpoints.as_ptr(),
                    Rc::strong_count(line),
                )
            })
            .collect();
        let pages = store.cache_census().unwrap();
        let census = cache.census().unwrap();
        assert_eq!(census["entries"], 3);
        assert_eq!(
            census["payload_counter_bytes"],
            lines.iter().map(|line| line.bytes()).sum::<usize>()
        );
        assert_eq!(
            census["payload_counter_bytes"],
            census["recomputed_payload_bytes"]
        );
        assert_eq!(cache.census().unwrap(), census);
        assert_eq!(store.cache_census().unwrap(), pages);
        for (line, expected) in lines.iter().zip(addresses) {
            assert_eq!(
                (
                    line.text.as_ptr(),
                    line.starts.as_ptr(),
                    line.checkpoints.as_ptr(),
                    Rc::strong_count(line)
                ),
                expected
            );
        }
        let weak: Vec<_> = lines.iter().map(Rc::downgrade).collect();
        drop(lines);
        drop(cache);
        assert!(weak.iter().all(|line| line.upgrade().is_none()));
    }
}

pub struct Line {
    text: Box<str>,
    columns: usize,
    checkpoints: Vec<Boundary>,
    starts: Vec<Boundary>,
}

impl Line {
    fn new(text: String, width: Option<usize>) -> Self {
        let mut checkpoints = Vec::new();
        let mut starts = vec![Boundary::default()];
        let mut used = 0;
        let mut columns = 0;
        for (index, glyph) in glyphs(&text, 4).enumerate() {
            columns = glyph.column + glyph.width;
            let boundary = Boundary {
                byte: glyph.byte,
                column: glyph.column,
            };
            if index % CHECKPOINT_GLYPHS == 0 {
                checkpoints.push(boundary);
            }
            if let Some(width) = width {
                if used > 0 && used + glyph.width > width {
                    starts.push(boundary);
                    used = 0;
                }
                used += glyph.width;
            }
        }
        Self {
            text: text.into_boxed_str(),
            columns,
            checkpoints,
            starts,
        }
    }

    fn bytes(&self) -> usize {
        size_of::<Self>()
            + self.text.len()
            + (self.checkpoints.capacity() + self.starts.capacity()) * size_of::<Boundary>()
    }

    pub fn height(&self) -> usize {
        self.starts.len()
    }

    pub fn columns(&self) -> usize {
        self.columns
    }

    pub fn start_column(&self, part: usize) -> Option<usize> {
        self.starts.get(part).map(|start| start.column)
    }

    pub fn part_for_column(&self, column: usize) -> usize {
        self.starts
            .partition_point(|start| start.column <= column)
            .saturating_sub(1)
    }

    pub fn glyphs_from_column(&self, column: usize) -> impl Iterator<Item = Glyph<'_>> {
        let checkpoint = self
            .checkpoints
            .get(
                self.checkpoints
                    .partition_point(|point| point.column <= column)
                    .saturating_sub(1),
            )
            .copied()
            .unwrap_or_default();
        glyphs_from(&self.text, 4, checkpoint.byte, checkpoint.column)
    }

    pub fn byte_at_column(&self, column: usize) -> usize {
        self.glyphs_from_column(column)
            .find(|glyph| glyph.column + glyph.width > column)
            .map_or(self.text.len(), |glyph| glyph.byte)
    }

    pub fn column_at_byte(&self, byte: usize) -> (usize, usize) {
        let checkpoint = self
            .checkpoints
            .get(
                self.checkpoints
                    .partition_point(|point| point.byte <= byte)
                    .saturating_sub(1),
            )
            .copied()
            .unwrap_or_default();
        glyphs_from(&self.text, 4, checkpoint.byte, checkpoint.column)
            .take_while(|glyph| glyph.byte <= byte)
            .last()
            .map_or((0, 1), |glyph| (glyph.column, glyph.width))
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum SourceKey {
    Patch(SourceId),
    Stored(SourceRef),
}

type Key = (SourceKey, Option<usize>);

#[derive(Default)]
struct Entries {
    lines: HashMap<Key, Rc<Line>>,
    order: VecDeque<Key>,
    bytes: usize,
}

#[derive(Default)]
pub struct Cache {
    entries: RefCell<Entries>,
}

impl Cache {
    #[cfg(feature = "owner-census")]
    pub(crate) fn census(&self) -> anyhow::Result<serde_json::Value> {
        let entries = self.entries.try_borrow()?;
        Ok(serde_json::json!({
            "entries": entries.lines.len(), "map_capacity": entries.lines.capacity(),
            "payload_counter_bytes": entries.bytes,
            "recomputed_payload_bytes": entries.lines.values().map(|line| line.bytes()).sum::<usize>(),
            "payload_limit_bytes": CACHE_BYTES, "entry_limit": CACHE_ENTRIES,
            "text_bytes": entries.lines.values().map(|line| line.text.len()).sum::<usize>(),
            "occupied_map_element_bytes": entries.lines.len() * size_of::<(Key, Rc<Line>)>(),
            "fifo_length": entries.order.len(), "fifo_capacity": entries.order.capacity(),
            "fifo_element_size_bytes": size_of::<Key>(),
        }))
    }

    pub fn get(&self, doc: &Document, id: usize, width: Option<usize>) -> anyhow::Result<Rc<Line>> {
        let file = doc.file_for_source(id);
        self.get_or_insert((SourceKey::Patch(file.source_id(id)?), width), || {
            Ok(Line::new(display_text(&file.bytes(id)?), width))
        })
    }

    pub(crate) fn get_source(
        &self,
        store: &ByteStore,
        source: SourceRef,
        width: Option<usize>,
    ) -> anyhow::Result<Rc<Line>> {
        self.get_or_insert((SourceKey::Stored(source), width), || {
            Ok(Line::new(display_text(&store.read_source(source)?), width))
        })
    }

    fn get_or_insert(
        &self,
        key: Key,
        build: impl FnOnce() -> anyhow::Result<Line>,
    ) -> anyhow::Result<Rc<Line>> {
        if let Some(line) = self.entries.borrow().lines.get(&key) {
            return Ok(line.clone());
        }
        let line = Rc::new(build()?);
        let bytes = line.bytes();
        if bytes <= CACHE_BYTES {
            let mut entries = self.entries.borrow_mut();
            while entries.bytes + bytes > CACHE_BYTES || entries.lines.len() >= CACHE_ENTRIES {
                if let Some(old) = entries.order.pop_front()
                    && let Some(line) = entries.lines.remove(&old)
                {
                    entries.bytes -= line.bytes();
                }
            }
            entries.bytes += bytes;
            entries.order.push_back(key);
            entries.lines.insert(key, line.clone());
        }
        Ok(line)
    }
}