use vize_l0::{SourceRoot, Span, String};
use crate::embed::{EmbedSource, SourceError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditError {
SnapshotMismatch,
StaleVersion,
SourceMismatch,
InvalidRange,
NonUtf8Boundary,
Projection(SourceError),
Unordered,
Overlap,
OutputTooLarge,
AllocationFailed,
}
#[derive(Debug, Clone, Copy)]
pub struct VersionedSource<'source, K> {
key: K,
version: i32,
root: SourceRoot<'source>,
}
impl<'source, K: Copy + Eq> VersionedSource<'source, K> {
#[must_use]
pub const fn new(key: K, version: i32, root: SourceRoot<'source>) -> Self {
Self { key, version, root }
}
#[must_use]
pub const fn key(self) -> K {
self.key
}
#[must_use]
pub const fn version(self) -> i32 {
self.version
}
#[must_use]
pub const fn root(self) -> SourceRoot<'source> {
self.root
}
pub fn edit<'replacement>(
self,
span: Span,
replacement: &'replacement str,
) -> Result<SpanEdit<'source, 'replacement, K>, EditError> {
let source = self.root.source();
if span.start > span.end || span.end as usize > source.len() {
return Err(EditError::InvalidRange);
}
if !source.is_char_boundary(span.start as usize)
|| !source.is_char_boundary(span.end as usize)
{
return Err(EditError::NonUtf8Boundary);
}
Ok(SpanEdit {
snapshot: self,
span,
replacement,
})
}
pub fn project_edit<'replacement>(
self,
source: EmbedSource<'_>,
relative: Span,
replacement: &'replacement str,
) -> Result<SpanEdit<'source, 'replacement, K>, EditError> {
if !same_buffer(self.root.source(), source.authored_root()) {
return Err(EditError::SourceMismatch);
}
let span = source
.authored_span(relative)
.map_err(EditError::Projection)?;
self.edit(span, replacement)
}
fn check(self, current: VersionedSource<'_, K>) -> Result<(), EditError> {
if self.key != current.key {
return Err(EditError::SnapshotMismatch);
}
if self.version != current.version {
return Err(EditError::StaleVersion);
}
if !same_buffer(self.root.source(), current.root.source()) {
return Err(EditError::SourceMismatch);
}
Ok(())
}
}
fn same_buffer(left: &str, right: &str) -> bool {
left.as_ptr() == right.as_ptr() && left.len() == right.len()
}
#[derive(Debug, Clone, Copy)]
pub struct SpanEdit<'source, 'replacement, K> {
snapshot: VersionedSource<'source, K>,
span: Span,
replacement: &'replacement str,
}
impl<'source, 'replacement, K: Copy + Eq> SpanEdit<'source, 'replacement, K> {
#[must_use]
pub const fn snapshot(self) -> VersionedSource<'source, K> {
self.snapshot
}
#[must_use]
pub const fn span(self) -> Span {
self.span
}
#[must_use]
pub const fn replacement(self) -> &'replacement str {
self.replacement
}
}
#[derive(Debug, Clone, Copy)]
pub struct EditSet<'edits, 'source, 'replacement, K> {
snapshot: VersionedSource<'source, K>,
edits: &'edits [SpanEdit<'source, 'replacement, K>],
output_len: usize,
}
impl<'edits, 'source, 'replacement, K: Copy + Eq> EditSet<'edits, 'source, 'replacement, K> {
pub fn new(
snapshot: VersionedSource<'source, K>,
edits: &'edits [SpanEdit<'source, 'replacement, K>],
) -> Result<Self, EditError> {
let mut previous: Option<Span> = None;
let mut removed = 0usize;
let mut added = 0usize;
for edit in edits {
snapshot.check(edit.snapshot)?;
if let Some(prior) = previous {
if edit.span.start < prior.start {
return Err(EditError::Unordered);
}
if edit.span.start < prior.end || (prior.start == prior.end && edit.span == prior) {
return Err(EditError::Overlap);
}
}
removed = removed
.checked_add((edit.span.end - edit.span.start) as usize)
.ok_or(EditError::OutputTooLarge)?;
added = added
.checked_add(edit.replacement.len())
.ok_or(EditError::OutputTooLarge)?;
previous = Some(edit.span);
}
let output_len = snapshot
.root
.source()
.len()
.checked_sub(removed)
.and_then(|length| length.checked_add(added))
.ok_or(EditError::OutputTooLarge)?;
if output_len > u32::MAX as usize {
return Err(EditError::OutputTooLarge);
}
Ok(Self {
snapshot,
edits,
output_len,
})
}
#[must_use]
pub const fn snapshot(self) -> VersionedSource<'source, K> {
self.snapshot
}
#[must_use]
pub const fn edits(self) -> &'edits [SpanEdit<'source, 'replacement, K>] {
self.edits
}
pub fn apply(self, current: VersionedSource<'_, K>) -> Result<String, EditError> {
self.snapshot.check(current)?;
let source = self.snapshot.root.source();
let mut output =
String::try_with_capacity(self.output_len).map_err(|_| EditError::AllocationFailed)?;
let mut cursor = 0usize;
for edit in self.edits {
let unchanged = source
.get(cursor..edit.span.start as usize)
.ok_or(EditError::InvalidRange)?;
output.push_str(unchanged);
output.push_str(edit.replacement);
cursor = edit.span.end as usize;
}
output.push_str(source.get(cursor..).ok_or(EditError::InvalidRange)?);
Ok(output)
}
}
#[cfg(test)]
mod tests;