#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[derive(Default)]
#[non_exhaustive]
pub enum Safety {
#[default]
Safe,
PotentiallyUnsafe,
Unsafe,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextRange {
pub start: u32,
pub end: u32,
}
impl TextRange {
#[inline(always)]
#[must_use]
pub fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
#[inline(always)]
#[must_use]
pub fn len(&self) -> u32 {
self.end - self.start
}
#[inline(always)]
#[must_use]
pub fn is_empty(&self) -> bool {
self.start == self.end
}
#[inline(always)]
#[must_use]
#[allow(clippy::suspicious_operation_groupings)]
pub fn overlaps(&self, other: &TextRange) -> bool {
match (self.is_empty(), other.is_empty()) {
(true, true) => false,
(true, false) => self.start > other.start && self.start < other.end,
(false, true) => other.start > self.start && other.start < self.end,
(false, false) => self.start < other.end && other.start < self.end,
}
}
#[inline(always)]
#[must_use]
pub fn contains(&self, offset: u32) -> bool {
offset >= self.start && offset < self.end
}
}
impl<T> From<T> for TextRange
where
T: std::ops::RangeBounds<u32>,
{
#[inline(always)]
fn from(r: T) -> Self {
let start = match r.start_bound() {
std::ops::Bound::Included(&s) => s,
std::ops::Bound::Excluded(&s) => s + 1,
std::ops::Bound::Unbounded => 0,
};
let end = match r.end_bound() {
std::ops::Bound::Included(&e) => e + 1,
std::ops::Bound::Excluded(&e) => e,
std::ops::Bound::Unbounded => u32::MAX, };
Self::new(start, end)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextEdit {
pub range: TextRange,
pub new_text: Vec<u8>,
pub safety: Safety,
}
impl TextEdit {
#[inline]
#[must_use]
pub fn delete(range: impl Into<TextRange>) -> Self {
Self { range: range.into(), new_text: Vec::new(), safety: Safety::Safe }
}
#[inline]
#[must_use]
pub fn insert(offset: u32, text: impl Into<Vec<u8>>) -> Self {
Self { range: TextRange::new(offset, offset), new_text: text.into(), safety: Safety::Safe }
}
#[inline]
#[must_use]
pub fn replace(range: impl Into<TextRange>, text: impl Into<Vec<u8>>) -> Self {
Self { range: range.into(), new_text: text.into(), safety: Safety::Safe }
}
#[inline]
#[must_use]
pub fn with_safety(mut self, safety: Safety) -> Self {
self.safety = safety;
self
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ApplyResult {
Applied,
OutOfBounds,
Overlap,
Rejected,
Unsafe,
PotentiallyUnsafe,
}
#[derive(Debug, Clone)]
pub struct TextEditor<'src> {
original_text: &'src [u8],
original_len: u32,
edits: Vec<TextEdit>,
safety_threshold: Safety,
}
impl<'src> TextEditor<'src> {
#[inline]
#[must_use]
pub fn new(text: &'src [u8]) -> Self {
Self {
original_text: text,
original_len: text.len() as u32,
edits: Vec::new(),
safety_threshold: Safety::Unsafe,
}
}
#[inline]
#[must_use]
pub fn with_safety(text: &'src [u8], threshold: Safety) -> Self {
Self { original_text: text, original_len: text.len() as u32, edits: Vec::new(), safety_threshold: threshold }
}
#[inline]
fn check_safety(&self, edit_safety: Safety) -> Option<ApplyResult> {
if edit_safety > self.safety_threshold {
Some(match edit_safety {
Safety::Unsafe => ApplyResult::Unsafe,
Safety::PotentiallyUnsafe => ApplyResult::PotentiallyUnsafe,
Safety::Safe => ApplyResult::Unsafe,
})
} else {
None
}
}
#[inline]
pub fn apply<F>(&mut self, edit: TextEdit, checker: Option<F>) -> ApplyResult
where
F: FnOnce(&[u8]) -> bool,
{
if let Some(rejection) = self.check_safety(edit.safety) {
return rejection;
}
if edit.range.end > self.original_len || edit.range.start > edit.range.end {
return ApplyResult::OutOfBounds;
}
let search_idx = self.edits.partition_point(|e| e.range.end <= edit.range.start);
if let Some(existing) = self.edits.get(search_idx)
&& existing.range.overlaps(&edit.range)
{
return ApplyResult::Overlap;
}
if let Some(check_fn) = checker {
let simulated_str = stitch_one(self.original_text, &self.edits, &edit);
if !check_fn(&simulated_str) {
return ApplyResult::Rejected;
}
}
self.edits.insert(search_idx, edit);
ApplyResult::Applied
}
#[inline]
pub fn apply_batch<F>(&mut self, mut new_edits: Vec<TextEdit>, checker: Option<F>) -> ApplyResult
where
F: FnOnce(&[u8]) -> bool,
{
if new_edits.is_empty() {
return ApplyResult::Applied;
}
for edit in &new_edits {
if let Some(rejection) = self.check_safety(edit.safety) {
return rejection;
}
}
new_edits.sort_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
for i in 0..new_edits.len() {
let edit = &new_edits[i];
if edit.range.end > self.original_len || edit.range.start > edit.range.end {
return ApplyResult::OutOfBounds;
}
if i > 0 && new_edits[i - 1].range.overlaps(&edit.range) {
return ApplyResult::Overlap;
}
}
{
let mut old_iter = self.edits.iter();
let mut new_iter = new_edits.iter();
let mut next_old = old_iter.next();
let mut next_new = new_iter.next();
while let (Some(old), Some(new)) = (next_old, next_new) {
if old.range.overlaps(&new.range) {
return ApplyResult::Overlap;
}
if old.range.start < new.range.start {
next_old = old_iter.next();
} else {
next_new = new_iter.next();
}
}
}
if let Some(check_fn) = checker {
let simulated_str = stitch_merged(self.original_text, &self.edits, &new_edits);
if !check_fn(&simulated_str) {
return ApplyResult::Rejected;
}
}
self.edits.reserve(new_edits.len());
self.edits.extend(new_edits);
self.edits.sort_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
ApplyResult::Applied
}
#[inline]
#[must_use]
pub fn finish(self) -> Vec<u8> {
stitch(self.original_text, &self.edits)
}
#[inline]
#[must_use]
pub fn get_edits(&self) -> &[TextEdit] {
&self.edits
}
#[inline]
#[must_use]
pub fn safety_threshold(&self) -> Safety {
self.safety_threshold
}
}
fn stitch(original: &[u8], edits: &[TextEdit]) -> Vec<u8> {
let mut final_len = original.len();
for edit in edits {
final_len = final_len.saturating_sub(edit.range.len() as usize).saturating_add(edit.new_text.len());
}
let mut output = Vec::with_capacity(final_len);
let mut last_processed = 0;
for edit in edits {
let start = edit.range.start as usize;
let end = edit.range.end as usize;
if start > last_processed {
output.extend_from_slice(&original[last_processed..start]);
}
output.extend_from_slice(&edit.new_text);
last_processed = end;
}
if last_processed < original.len() {
output.extend_from_slice(&original[last_processed..]);
}
output
}
fn stitch_one(original: &[u8], existing_edits: &[TextEdit], new_edit: &TextEdit) -> Vec<u8> {
let slice = std::slice::from_ref(new_edit);
stitch_merged(original, existing_edits, slice)
}
fn stitch_merged(original: &[u8], old_edits: &[TextEdit], new_edits: &[TextEdit]) -> Vec<u8> {
let mut final_len = original.len();
for e in old_edits {
final_len = final_len - e.range.len() as usize + e.new_text.len();
}
for e in new_edits {
final_len = final_len - e.range.len() as usize + e.new_text.len();
}
let mut output = Vec::with_capacity(final_len);
let mut last_processed = 0;
let mut old_iter = old_edits.iter();
let mut new_iter = new_edits.iter();
let mut next_old = old_iter.next();
let mut next_new = new_iter.next();
loop {
let next_edit = match (next_old, next_new) {
(Some(o), Some(n)) => {
if (o.range.start, o.range.end) <= (n.range.start, n.range.end) {
next_old = old_iter.next();
o
} else {
next_new = new_iter.next();
n
}
}
(Some(o), None) => {
next_old = old_iter.next();
o
}
(None, Some(n)) => {
next_new = new_iter.next();
n
}
(None, None) => break,
};
let start = next_edit.range.start as usize;
let end = next_edit.range.end as usize;
if start > last_processed {
output.extend_from_slice(&original[last_processed..start]);
}
output.extend_from_slice(&next_edit.new_text);
last_processed = end;
}
if last_processed < original.len() {
output.extend_from_slice(&original[last_processed..]);
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_single() {
let mut editor = TextEditor::new(b"hello world");
editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&[u8]) -> bool>);
assert_eq!(editor.finish(), b"hi world");
}
#[test]
fn test_checker_fail() {
let mut editor = TextEditor::new(b"abc");
let res = editor.apply(TextEdit::delete(0..1), Some(|s: &[u8]| s.len() > 10));
assert_eq!(res, ApplyResult::Rejected);
assert_eq!(editor.finish(), b"abc"); }
#[test]
fn test_overlap_search() {
let mut editor = TextEditor::new(b"0123456789");
editor.apply(TextEdit::replace(2..4, "x"), None::<fn(&[u8]) -> bool>);
assert_eq!(editor.apply(TextEdit::replace(3..5, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
assert_eq!(editor.apply(TextEdit::replace(1..3, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Overlap);
assert_eq!(editor.apply(TextEdit::replace(4..5, "y"), None::<fn(&[u8]) -> bool>), ApplyResult::Applied);
assert_eq!(editor.finish(), b"01xy56789");
}
#[test]
fn test_batch_apply_ordering() {
let mut editor = TextEditor::new(b"abcdef");
let batch = vec![
TextEdit::replace(4..5, "E"), TextEdit::replace(0..1, "A"), ];
editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
assert_eq!(editor.finish(), b"AbcdEf");
}
#[test]
fn test_safety_default_is_safe() {
let edit = TextEdit::replace(0..1, b"x");
assert_eq!(edit.safety, Safety::Safe);
}
#[test]
fn test_with_safety_builder() {
let edit = TextEdit::replace(0..1, b"x").with_safety(Safety::Unsafe);
assert_eq!(edit.safety, Safety::Unsafe);
let edit = TextEdit::delete(0..1).with_safety(Safety::PotentiallyUnsafe);
assert_eq!(edit.safety, Safety::PotentiallyUnsafe);
}
#[test]
fn test_safety_threshold_safe_mode() {
let mut editor = TextEditor::with_safety(b"hello world", Safety::Safe);
let res = editor.apply(TextEdit::replace(0..5, b"hi"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(
TextEdit::replace(6..11, b"there").with_safety(Safety::PotentiallyUnsafe),
None::<fn(&[u8]) -> bool>,
);
assert_eq!(res, ApplyResult::PotentiallyUnsafe);
let res =
editor.apply(TextEdit::replace(6..11, b"there").with_safety(Safety::Unsafe), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Unsafe);
assert_eq!(editor.finish(), b"hi world"); }
#[test]
fn test_safety_threshold_potentially_unsafe_mode() {
let mut editor = TextEditor::with_safety(b"hello world", Safety::PotentiallyUnsafe);
let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor
.apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"hi there");
}
#[test]
fn test_safety_threshold_unsafe_mode() {
let mut editor = TextEditor::with_safety(b"hello world", Safety::Unsafe);
let res = editor.apply(TextEdit::replace(0..1, b"H"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor
.apply(TextEdit::replace(1..2, b"E").with_safety(Safety::PotentiallyUnsafe), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::replace(2..3, b"L").with_safety(Safety::Unsafe), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"HELlo world");
}
#[test]
fn test_batch_safety_rejection() {
let mut editor = TextEditor::with_safety(b"hello", Safety::Safe);
let batch = vec![
TextEdit::replace(0..1, "H"), TextEdit::replace(1..2, "E").with_safety(Safety::Unsafe), ];
let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Unsafe);
assert_eq!(editor.finish(), b"hello");
}
#[test]
fn test_safety_ordering() {
assert!(Safety::Safe < Safety::PotentiallyUnsafe);
assert!(Safety::PotentiallyUnsafe < Safety::Unsafe);
assert!(Safety::Safe < Safety::Unsafe);
}
#[test]
fn test_insert_at_start_of_replace_applies_before_replacement() {
let mut editor = TextEditor::new(b"0123456789");
let res = editor.apply(TextEdit::replace(2..8, "replaced"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::insert(2, "inserted"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"01insertedreplaced89");
}
#[test]
fn test_insert_after_replace_at_different_offset() {
let mut editor = TextEditor::new(b"0123456789");
let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::insert(6, "X"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"01ABC5X6789");
}
#[test]
fn test_insert_at_start_of_replace_coexists() {
let mut editor = TextEditor::new(b"0123456789");
let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::insert(2, "X"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"01XABC56789");
}
#[test]
fn test_batch_insert_and_replace_at_same_offset_coexist() {
let mut editor = TextEditor::new(b"0123456789");
let batch = vec![
TextEdit::insert(2, "inserted"), TextEdit::replace(2..5, "ABC"), ];
let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"01insertedABC56789");
}
#[test]
fn test_multiple_inserts_at_same_offset_stack_in_insertion_order() {
let mut editor = TextEditor::new(b"ABC");
let batch = vec![TextEdit::insert(0, "X"), TextEdit::insert(0, "Y"), TextEdit::insert(0, "Z")];
let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"XYZABC");
}
#[test]
fn test_insert_at_end_of_replace_applies_after_replacement() {
let mut editor = TextEditor::new(b"0123456789");
let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::insert(5, "X"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"01ABCX56789");
}
#[test]
fn test_insert_inside_replace_overlaps() {
let mut editor = TextEditor::new(b"0123456789");
let res = editor.apply(TextEdit::replace(2..8, "ABCDEF"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
let res = editor.apply(TextEdit::insert(5, "X"), None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Overlap);
}
#[test]
fn test_issue_828_regression_both_edits_apply_correctly() {
let mut editor = TextEditor::new(b"function ($v) { return $v; }");
let batch = vec![TextEdit::insert(0, "static "), TextEdit::replace(0..8, "fn")];
let res = editor.apply_batch(batch, None::<fn(&[u8]) -> bool>);
assert_eq!(res, ApplyResult::Applied);
assert_eq!(editor.finish(), b"static fn ($v) { return $v; }");
}
#[test]
fn test_checker_simulation_matches_final_output_for_stacked_inserts() {
let mut editor = TextEditor::new(b"ABC");
editor.apply(TextEdit::insert(0, b"X"), None::<fn(&[u8]) -> bool>);
let simulated: std::cell::RefCell<Option<Vec<u8>>> = std::cell::RefCell::new(None);
let checker = |s: &[u8]| {
*simulated.borrow_mut() = Some(s.to_vec());
true
};
let batch = vec![TextEdit::insert(0, b"Y")];
assert_eq!(editor.apply_batch(batch, Some(checker)), ApplyResult::Applied);
#[allow(clippy::expect_used)]
let simulated = simulated.borrow().clone().expect("checker called");
let final_str = editor.finish();
assert_eq!(simulated, final_str);
assert_eq!(final_str, b"XYABC");
}
#[test]
fn test_touching_non_empty_ranges_do_not_overlap() {
let range1 = TextRange::new(0, 5);
let range2 = TextRange::new(5, 10);
assert!(!range1.overlaps(&range2));
assert!(!range2.overlaps(&range1));
}
#[test]
fn test_insert_at_boundary_of_replace_does_not_overlap() {
let insert_at_start = TextRange::new(5, 5);
let insert_at_end = TextRange::new(10, 10);
let replace_range = TextRange::new(5, 10);
assert!(!insert_at_start.overlaps(&replace_range));
assert!(!replace_range.overlaps(&insert_at_start));
assert!(!insert_at_end.overlaps(&replace_range));
assert!(!replace_range.overlaps(&insert_at_end));
}
#[test]
fn test_insert_inside_non_empty_range_overlaps() {
let insert = TextRange::new(7, 7);
let replace = TextRange::new(5, 10);
assert!(insert.overlaps(&replace));
assert!(replace.overlaps(&insert));
}
#[test]
fn test_two_empty_ranges_at_same_offset_do_not_overlap() {
let a = TextRange::new(5, 5);
let b = TextRange::new(5, 5);
assert!(!a.overlaps(&b));
assert!(!b.overlaps(&a));
}
}