use std::ops::Range;
use crate::buffer::Buffer;
use crate::coords::Bias;
use crate::patch::{Edit, Patch};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct EditOp {
pub range: Range<u32>,
pub text: String,
}
impl EditOp {
#[must_use]
pub fn new(range: Range<u32>, text: impl Into<String>) -> Self {
Self { range, text: text.into() }
}
#[must_use]
pub fn insert(at: u32, text: impl Into<String>) -> Self {
Self { range: at..at, text: text.into() }
}
#[must_use]
pub fn delete(range: Range<u32>) -> Self {
Self { range, text: String::new() }
}
}
#[derive(Clone, Debug, Default)]
pub struct Committed {
patch: Patch,
inverse: Vec<EditOp>,
forward: Vec<EditOp>,
}
impl Committed {
#[must_use]
pub fn patch(&self) -> &Patch {
&self.patch
}
pub(crate) fn from_patch(patch: Patch) -> Self {
Self { patch, inverse: Vec::new(), forward: Vec::new() }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.patch.is_empty()
}
#[must_use]
pub(crate) fn inverse_ops(&self) -> &[EditOp] {
&self.inverse
}
#[must_use]
pub(crate) fn forward_ops(&self) -> &[EditOp] {
&self.forward
}
pub(crate) fn into_ops(self) -> (Patch, Vec<EditOp>, Vec<EditOp>) {
(self.patch, self.forward, self.inverse)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TransactionError {
#[error("overlapping edit ranges: {first:?} and {second:?}")]
Overlap {
first: Range<u32>,
second: Range<u32>,
},
#[error("edit would grow the document to {len} bytes, past the u32 offset space")]
WouldOverflow {
len: u64,
},
}
fn post_len(current: u32, ops: &[EditOp]) -> u64 {
let delta: i64 =
ops.iter().map(|op| op.text.len() as i64 - (op.range.end - op.range.start) as i64).sum();
let len = current as i64 + delta;
debug_assert!(len >= 0, "deletes cannot exceed the document");
len as u64
}
pub(crate) fn apply(buffer: &mut Buffer, ops: Vec<EditOp>) -> Result<Committed, TransactionError> {
let mut norm: Vec<EditOp> = Vec::with_capacity(ops.len());
for op in ops {
let (a, b) = (op.range.start.min(op.range.end), op.range.start.max(op.range.end));
let start = buffer.clip_offset(a, Bias::Left);
let end = buffer.clip_offset(b, Bias::Right);
let text = if op.text.as_bytes().contains(&b'\r') {
op.text.replace("\r\n", "\n").replace('\r', "\n")
} else {
op.text
};
if start == end && text.is_empty() {
continue; }
norm.push(EditOp { range: start..end, text });
}
if norm.is_empty() {
return Ok(Committed::default()); }
norm.sort_by_key(|op| op.range.start);
for w in norm.windows(2) {
if w[0].range.end > w[1].range.start {
return Err(TransactionError::Overlap {
first: w[0].range.clone(),
second: w[1].range.clone(),
});
}
}
let len = post_len(buffer.len(), &norm);
if len >= u32::MAX as u64 {
return Err(TransactionError::WouldOverflow { len });
}
let mut patch = Patch::new();
let mut inverse = Vec::with_capacity(norm.len());
let mut delta: i64 = 0;
for op in &norm {
let (s, e) = (op.range.start, op.range.end);
let old_text = buffer.slice(s..e).into_owned();
let ns = (s as i64 + delta) as u32; let ne = ns + op.text.len() as u32; patch.push(Edit { old: s..e, new: ns..ne });
inverse.push(EditOp { range: ns..ne, text: old_text });
delta += op.text.len() as i64 - (e - s) as i64;
}
{
let batch: Vec<(Range<u32>, &str)> =
norm.iter().map(|op| (op.range.clone(), op.text.as_str())).collect();
buffer.edit_many(&batch);
}
buffer.bump_revision();
Ok(Committed { patch, inverse, forward: norm })
}
#[cfg(test)]
mod tests {
use super::*;
fn buf(s: &str) -> Buffer {
Buffer::new(s).unwrap()
}
#[test]
fn single_insert() {
let mut b = buf("ac");
let c = apply(&mut b, vec![EditOp::insert(1, "b")]).unwrap();
assert_eq!(b.text(), "abc");
assert_eq!(b.revision().0, 1);
assert!(!c.is_empty());
}
#[test]
fn single_delete_and_replace() {
let mut b = buf("hello");
apply(&mut b, vec![EditOp::delete(1..3)]).unwrap();
assert_eq!(b.text(), "hlo");
apply(&mut b, vec![EditOp::new(0..1, "H")]).unwrap();
assert_eq!(b.text(), "Hlo");
assert_eq!(b.revision().0, 2);
}
#[test]
fn multi_range_batch_is_atomic() {
let mut b = buf("a b a");
apply(&mut b, vec![EditOp::new(0..1, "X"), EditOp::new(4..5, "Y")]).unwrap();
assert_eq!(b.text(), "X b Y");
assert_eq!(b.revision().0, 1); }
#[test]
fn same_offset_insertions_apply_in_caller_order() {
let mut b = buf("");
apply(&mut b, vec![EditOp::insert(0, "A"), EditOp::insert(0, "B")]).unwrap();
assert_eq!(b.text(), "AB");
}
#[test]
fn overlap_is_rejected() {
let mut b = buf("abcdef");
let err = apply(&mut b, vec![EditOp::new(0..3, "x"), EditOp::new(2..5, "y")]);
assert!(matches!(err, Err(TransactionError::Overlap { .. })));
assert_eq!(b.text(), "abcdef"); assert_eq!(b.revision().0, 0);
}
#[test]
fn post_len_overflow_guard_is_pure() {
let ops = vec![EditOp::new(0..3, "xxxxx"), EditOp::delete(5..9)];
assert_eq!(post_len(20, &ops), 20 + 2 - 4);
assert_eq!(post_len(0, &[]), 0);
let huge = vec![EditOp::insert(0, "x".repeat(16))];
assert!(post_len(u32::MAX - 8, &huge) >= u32::MAX as u64, "this batch would be refused");
}
#[test]
fn empty_and_noop_batches_do_not_move_the_revision() {
let mut b = buf("abc");
assert!(apply(&mut b, vec![]).unwrap().is_empty());
assert!(apply(&mut b, vec![EditOp::new(1..1, "")]).unwrap().is_empty());
assert_eq!(b.revision().0, 0);
assert_eq!(b.text(), "abc");
}
#[test]
fn text_is_normalized_to_lf() {
let mut b = buf("");
apply(&mut b, vec![EditOp::insert(0, "a\r\nb")]).unwrap();
assert_eq!(b.text(), "a\nb");
assert!(!b.text().contains('\r'));
assert_eq!(b.line_count(), 2);
}
#[test]
fn inverse_round_trips_text_and_revision_parity() {
let mut b = buf("the quick brown fox");
let before = b.text().into_owned();
let committed =
apply(&mut b, vec![EditOp::new(4..9, "SLOW"), EditOp::insert(0, ">> ")]).unwrap();
assert_ne!(b.text(), before);
let redo = apply(&mut b, committed.inverse_ops().to_vec()).unwrap();
assert_eq!(b.text(), before, "inverse must restore the pre-edit text");
assert_eq!(b.revision().0, 2); apply(&mut b, redo.inverse_ops().to_vec()).unwrap();
assert_ne!(b.text(), before);
}
#[test]
fn patch_maps_positions_across_the_transaction() {
let mut b = buf("hello world");
let c = apply(&mut b, vec![EditOp::delete(0..6)]).unwrap();
assert_eq!(b.text(), "world");
assert_eq!(c.patch().map_offset(6, Bias::Left), 0);
assert_eq!(c.patch().map_offset(8, Bias::Left), 2); }
}