colla 0.1.0

Operational Transformation library for nested documents with text and rich-text
Documentation
use crate::attrs::{AttrPatch, Attrs};
use crate::change::{Change, ListOp, RichTextOp, TextOp};
use crate::richtext::RichInsert;
use crate::Value;

#[derive(Clone, Copy)]
pub(super) enum TextOpRef<'a> {
    Retain(usize),
    Insert { text: &'a str, len: usize },
    Delete(usize),
}

pub(super) struct TextOpReader<'a> {
    ops: &'a [TextOp],
    index: usize,
    offset: usize,
    byte_offset: usize,
    remaining: usize,
}

impl<'a> TextOpReader<'a> {
    pub(super) fn new(ops: &'a [TextOp]) -> Self {
        let mut reader = Self {
            ops,
            index: 0,
            offset: 0,
            byte_offset: 0,
            remaining: 0,
        };
        reader.load();
        reader
    }

    pub(super) fn peek(&self) -> Option<TextOpRef<'a>> {
        match self.ops.get(self.index)? {
            TextOp::Retain(_) => Some(TextOpRef::Retain(self.remaining)),
            TextOp::Insert(text) => Some(TextOpRef::Insert {
                text: &text[self.byte_offset..],
                len: self.remaining,
            }),
            TextOp::Delete(_) => Some(TextOpRef::Delete(self.remaining)),
        }
    }

    pub(super) fn consume(&mut self, len: usize) {
        assert!(len > 0 && len <= self.remaining);
        if len == self.remaining {
            self.index += 1;
            self.offset = 0;
            self.byte_offset = 0;
            self.remaining = 0;
            self.load();
            return;
        }
        if let Some(TextOp::Insert(text)) = self.ops.get(self.index) {
            let rest = &text[self.byte_offset..];
            self.byte_offset += char_prefix_bytes(rest, len);
        } else {
            self.offset += len;
        }
        self.remaining -= len;
    }

    fn load(&mut self) {
        while let Some(op) = self.ops.get(self.index) {
            self.remaining = match op {
                TextOp::Retain(len) | TextOp::Delete(len) => len.saturating_sub(self.offset),
                TextOp::Insert(text) => text[self.byte_offset..].chars().count(),
            };
            if self.remaining > 0 {
                break;
            }
            self.index += 1;
            self.offset = 0;
            self.byte_offset = 0;
        }
    }
}

#[derive(Clone, Copy)]
pub(super) enum ListOpRef<'a> {
    Retain(usize),
    Insert(&'a [Value]),
    Delete(usize),
    Modify(&'a Change),
}

pub(super) struct ListOpReader<'a> {
    ops: &'a [ListOp],
    index: usize,
    offset: usize,
    remaining: usize,
}

impl<'a> ListOpReader<'a> {
    pub(super) fn new(ops: &'a [ListOp]) -> Self {
        let mut reader = Self {
            ops,
            index: 0,
            offset: 0,
            remaining: 0,
        };
        reader.load();
        reader
    }

    pub(super) fn peek(&self) -> Option<ListOpRef<'a>> {
        match self.ops.get(self.index)? {
            ListOp::Retain(_) => Some(ListOpRef::Retain(self.remaining)),
            ListOp::Insert(values) => Some(ListOpRef::Insert(&values[self.offset..])),
            ListOp::Delete(_) => Some(ListOpRef::Delete(self.remaining)),
            ListOp::Modify(change) => Some(ListOpRef::Modify(change)),
        }
    }

    pub(super) fn consume(&mut self, len: usize) {
        assert!(len > 0 && len <= self.remaining);
        self.offset += len;
        self.remaining -= len;
        if self.remaining == 0 {
            self.index += 1;
            self.offset = 0;
            self.load();
        }
    }

    fn load(&mut self) {
        while let Some(op) = self.ops.get(self.index) {
            self.remaining = match op {
                ListOp::Retain(len) | ListOp::Delete(len) => len.saturating_sub(self.offset),
                ListOp::Insert(values) => values.len().saturating_sub(self.offset),
                ListOp::Modify(_) => 1usize.saturating_sub(self.offset),
            };
            if self.remaining > 0 {
                break;
            }
            self.index += 1;
            self.offset = 0;
        }
    }
}

#[derive(Clone, Copy)]
pub(super) enum RichInsertRef<'a> {
    Text { text: &'a str, len: usize },
    Embed(&'a Value),
}

impl RichInsertRef<'_> {
    pub(super) fn len(self) -> usize {
        match self {
            Self::Text { len, .. } => len,
            Self::Embed(_) => 1,
        }
    }

    pub(super) fn prefix(self, len: usize) -> RichInsert {
        match self {
            Self::Text { text, .. } => RichInsert::text(&text[..char_prefix_bytes(text, len)]),
            Self::Embed(value) => {
                assert_eq!(len, 1);
                RichInsert::embed(value.clone())
            }
        }
    }
}

#[derive(Clone, Copy)]
pub(super) enum RichTextOpRef<'a> {
    Retain {
        len: usize,
        attrs: &'a AttrPatch,
    },
    Insert {
        content: RichInsertRef<'a>,
        attrs: &'a Attrs,
    },
    Delete(usize),
}

pub(super) struct RichTextOpReader<'a> {
    ops: &'a [RichTextOp],
    index: usize,
    offset: usize,
    byte_offset: usize,
    remaining: usize,
}

impl<'a> RichTextOpReader<'a> {
    pub(super) fn new(ops: &'a [RichTextOp]) -> Self {
        let mut reader = Self {
            ops,
            index: 0,
            offset: 0,
            byte_offset: 0,
            remaining: 0,
        };
        reader.load();
        reader
    }

    pub(super) fn peek(&self) -> Option<RichTextOpRef<'a>> {
        match self.ops.get(self.index)? {
            RichTextOp::Retain { attrs, .. } => Some(RichTextOpRef::Retain {
                len: self.remaining,
                attrs,
            }),
            RichTextOp::Insert {
                content: RichInsert::Text(text),
                attrs,
            } => Some(RichTextOpRef::Insert {
                content: RichInsertRef::Text {
                    text: &text[self.byte_offset..],
                    len: self.remaining,
                },
                attrs,
            }),
            RichTextOp::Insert {
                content: RichInsert::Embed(value),
                attrs,
            } => Some(RichTextOpRef::Insert {
                content: RichInsertRef::Embed(value),
                attrs,
            }),
            RichTextOp::Delete(_) => Some(RichTextOpRef::Delete(self.remaining)),
        }
    }

    pub(super) fn consume(&mut self, len: usize) {
        assert!(len > 0 && len <= self.remaining);
        if len == self.remaining {
            self.index += 1;
            self.offset = 0;
            self.byte_offset = 0;
            self.remaining = 0;
            self.load();
            return;
        }
        match self.ops.get(self.index) {
            Some(RichTextOp::Insert {
                content: RichInsert::Text(text),
                ..
            }) => {
                let rest = &text[self.byte_offset..];
                self.byte_offset += char_prefix_bytes(rest, len);
            }
            _ => self.offset += len,
        }
        self.remaining -= len;
    }

    fn load(&mut self) {
        while let Some(op) = self.ops.get(self.index) {
            self.remaining = match op {
                RichTextOp::Retain { len, .. } | RichTextOp::Delete(len) => {
                    len.saturating_sub(self.offset)
                }
                RichTextOp::Insert {
                    content: RichInsert::Text(text),
                    ..
                } => text[self.byte_offset..].chars().count(),
                RichTextOp::Insert {
                    content: RichInsert::Embed(_),
                    ..
                } => 1usize.saturating_sub(self.offset),
            };
            if self.remaining > 0 {
                break;
            }
            self.index += 1;
            self.offset = 0;
            self.byte_offset = 0;
        }
    }
}

pub(super) fn text_prefix(text: &str, len: usize) -> &str {
    &text[..char_prefix_bytes(text, len)]
}

fn char_prefix_bytes(text: &str, len: usize) -> usize {
    if len == 0 {
        return 0;
    }
    text.char_indices()
        .nth(len)
        .map_or(text.len(), |(index, _)| index)
}