use serde::{Deserialize, Serialize};
use crate::position::ByteOffset;
use crate::text::Document;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edit {
pub start: ByteOffset,
pub end: ByteOffset,
pub text: String,
}
impl Edit {
pub fn new(start: ByteOffset, end: ByteOffset, text: impl Into<String>) -> Self {
Self {
start,
end,
text: text.into(),
}
}
fn is_noop(&self) -> bool {
self.start == self.end && self.text.is_empty()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Bias {
Before,
After,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeSet {
base_len: ByteOffset,
edits: Vec<Edit>,
}
impl ChangeSet {
pub fn new(base_len: ByteOffset, mut edits: Vec<Edit>) -> Self {
edits.retain(|e| !e.is_noop());
edits.sort_by_key(|e| (e.start, e.end));
let mut out: Vec<Edit> = Vec::with_capacity(edits.len());
for e in edits {
debug_assert!(e.start <= e.end && e.end <= base_len);
match out.last_mut() {
Some(last) if e.start <= last.end => {
debug_assert!(
e.start == last.end || (last.text.is_empty() && e.text.is_empty()),
"overlapping edits that insert text: {last:?} {e:?}"
);
last.end = last.end.max(e.end);
last.text.push_str(&e.text);
}
_ => out.push(e),
}
}
Self {
base_len,
edits: out,
}
}
pub fn edits(&self) -> &[Edit] {
&self.edits
}
pub fn fits(&self, doc: &Document) -> bool {
let char_boundary = |p: ByteOffset| p <= doc.len() && doc.is_char_boundary(p);
self.base_len == doc.len()
&& self
.edits
.iter()
.all(|e| e.start <= e.end && char_boundary(e.start) && char_boundary(e.end))
&& self.edits.windows(2).all(|w| w[0].end < w[1].start)
}
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub fn base_len(&self) -> ByteOffset {
self.base_len
}
pub(crate) fn extend_base(&mut self, by: usize) {
self.base_len = ByteOffset(self.base_len.0 + by);
}
pub fn new_len(&self) -> ByteOffset {
let delta: isize = self
.edits
.iter()
.map(|e| e.text.len() as isize - (e.end.0 - e.start.0) as isize)
.sum();
ByteOffset((self.base_len.0 as isize + delta) as usize)
}
pub fn apply(&self, doc: &mut Document) {
debug_assert_eq!(doc.len(), self.base_len);
for e in self.edits.iter().rev() {
doc.replace(e.start..e.end, &e.text);
}
}
pub fn invert(&self, doc: &Document) -> ChangeSet {
let mut delta: isize = 0;
let mut edits = Vec::with_capacity(self.edits.len());
for e in &self.edits {
let start = ByteOffset((e.start.0 as isize + delta) as usize);
edits.push(Edit::new(
start,
ByteOffset(start.0 + e.text.len()),
doc.slice(e.start..e.end),
));
delta += e.text.len() as isize - (e.end.0 - e.start.0) as isize;
}
ChangeSet {
base_len: self.new_len(),
edits,
}
}
pub fn map_position(&self, pos: ByteOffset, bias: Bias) -> ByteOffset {
let mut delta: isize = 0;
for e in &self.edits {
let new_start = (e.start.0 as isize + delta) as usize;
let new_end = new_start + e.text.len();
if pos < e.start {
break;
}
if pos <= e.end {
let after = if e.start == e.end {
bias == Bias::After
} else if pos == e.start {
false
} else if pos == e.end {
true
} else {
bias == Bias::After
};
return ByteOffset(if after { new_end } else { new_start });
}
delta += e.text.len() as isize - (e.end.0 - e.start.0) as isize;
}
ByteOffset((pos.0 as isize + delta) as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn b(n: usize) -> ByteOffset {
ByteOffset(n)
}
#[test]
fn apply_and_invert() {
let mut doc = Document::new("hello world");
let cs = ChangeSet::new(
doc.len(),
vec![Edit::new(b(6), b(11), "there"), Edit::new(b(0), b(5), "hi")],
);
let inv = cs.invert(&doc);
cs.apply(&mut doc);
assert_eq!(doc.to_string(), "hi there");
assert_eq!(cs.new_len(), doc.len());
inv.apply(&mut doc);
assert_eq!(doc.to_string(), "hello world");
}
#[test]
fn merges_touching_and_overlapping_deletes() {
let cs = ChangeSet::new(
b(10),
vec![
Edit::new(b(2), b(3), ""),
Edit::new(b(3), b(4), ""),
Edit::new(b(3), b(5), ""),
Edit::new(b(7), b(7), ""),
],
);
assert_eq!(cs.edits(), &[Edit::new(b(2), b(5), "")]);
}
#[test]
fn map_position_bias() {
let cs = ChangeSet::new(
b(10),
vec![Edit::new(b(2), b(2), "XY"), Edit::new(b(5), b(7), "Z")],
);
let m = |p, bias| cs.map_position(b(p), bias).0;
assert_eq!(m(1, Bias::After), 1);
assert_eq!(m(2, Bias::Before), 2);
assert_eq!(m(2, Bias::After), 4);
assert_eq!(m(4, Bias::Before), 6);
assert_eq!(m(5, Bias::After), 7); assert_eq!(m(6, Bias::Before), 7);
assert_eq!(m(6, Bias::After), 8);
assert_eq!(m(7, Bias::Before), 8); assert_eq!(m(10, Bias::Before), 11);
}
}