1use serde::Deserialize;
2use serde::Serialize;
3
4#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
8#[serde(rename_all = "lowercase")]
9#[derive(Default)]
10pub enum Safety {
11 #[default]
14 Safe,
15 PotentiallyUnsafe,
18 Unsafe,
21}
22
23#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
25pub struct TextRange {
26 pub start: u32,
27 pub end: u32,
28}
29
30impl TextRange {
31 #[inline(always)]
32 pub fn new(start: u32, end: u32) -> Self {
33 Self { start, end }
34 }
35
36 #[inline(always)]
38 pub fn len(&self) -> u32 {
39 self.end - self.start
40 }
41
42 #[inline(always)]
44 pub fn is_empty(&self) -> bool {
45 self.start == self.end
46 }
47
48 #[inline(always)]
57 pub fn overlaps(&self, other: &TextRange) -> bool {
58 self.start <= other.end && other.start <= self.end
59 }
60
61 #[inline(always)]
63 pub fn contains(&self, offset: u32) -> bool {
64 offset >= self.start && offset < self.end
65 }
66}
67
68impl<T> From<T> for TextRange
69where
70 T: std::ops::RangeBounds<u32>,
71{
72 #[inline(always)]
73 fn from(r: T) -> Self {
74 let start = match r.start_bound() {
75 std::ops::Bound::Included(&s) => s,
76 std::ops::Bound::Excluded(&s) => s + 1,
77 std::ops::Bound::Unbounded => 0,
78 };
79
80 let end = match r.end_bound() {
81 std::ops::Bound::Included(&e) => e + 1,
82 std::ops::Bound::Excluded(&e) => e,
83 std::ops::Bound::Unbounded => u32::MAX, };
85
86 Self::new(start, end)
87 }
88}
89
90#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
95pub struct TextEdit {
96 pub range: TextRange,
98 pub new_text: String,
100 pub safety: Safety,
102}
103
104impl TextEdit {
105 pub fn delete(range: impl Into<TextRange>) -> Self {
107 Self { range: range.into(), new_text: String::new(), safety: Safety::Safe }
108 }
109
110 pub fn insert(offset: u32, text: impl Into<String>) -> Self {
112 Self { range: TextRange::new(offset, offset), new_text: text.into(), safety: Safety::Safe }
113 }
114
115 pub fn replace(range: impl Into<TextRange>, text: impl Into<String>) -> Self {
117 Self { range: range.into(), new_text: text.into(), safety: Safety::Safe }
118 }
119
120 #[must_use]
130 pub fn with_safety(mut self, safety: Safety) -> Self {
131 self.safety = safety;
132 self
133 }
134}
135
136#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
137pub enum ApplyResult {
138 Applied,
140 OutOfBounds,
142 Overlap,
144 Rejected,
146 Unsafe,
148 PotentiallyUnsafe,
150}
151
152#[derive(Debug, Clone)]
157pub struct TextEditor<'a> {
158 original_text: &'a str,
159 original_len: u32,
160 edits: Vec<TextEdit>,
161 safety_threshold: Safety,
163}
164
165impl<'a> TextEditor<'a> {
166 pub fn new(text: &'a str) -> Self {
168 Self {
169 original_text: text,
170 original_len: text.len() as u32,
171 edits: Vec::new(),
172 safety_threshold: Safety::Unsafe,
173 }
174 }
175
176 pub fn with_safety(text: &'a str, threshold: Safety) -> Self {
188 Self { original_text: text, original_len: text.len() as u32, edits: Vec::new(), safety_threshold: threshold }
189 }
190
191 #[inline]
194 fn check_safety(&self, edit_safety: Safety) -> Option<ApplyResult> {
195 if edit_safety > self.safety_threshold {
196 Some(match edit_safety {
197 Safety::Unsafe => ApplyResult::Unsafe,
198 Safety::PotentiallyUnsafe => ApplyResult::PotentiallyUnsafe,
199 Safety::Safe => unreachable!(),
200 })
201 } else {
202 None
203 }
204 }
205
206 pub fn apply<F>(&mut self, edit: TextEdit, checker: Option<F>) -> ApplyResult
211 where
212 F: FnOnce(&str) -> bool,
213 {
214 if let Some(rejection) = self.check_safety(edit.safety) {
216 return rejection;
217 }
218
219 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
220 return ApplyResult::OutOfBounds;
221 }
222
223 let search_idx = self.edits.partition_point(|e| e.range.end <= edit.range.start);
224
225 if let Some(existing) = self.edits.get(search_idx)
226 && existing.range.overlaps(&edit.range)
227 {
228 return ApplyResult::Overlap;
229 }
230
231 if let Some(check_fn) = checker {
232 let simulated_str = stitch_one(self.original_text, &self.edits, &edit);
233 if !check_fn(&simulated_str) {
234 return ApplyResult::Rejected;
235 }
236 }
237
238 self.edits.insert(search_idx, edit);
239
240 ApplyResult::Applied
241 }
242
243 pub fn apply_batch<F>(&mut self, mut new_edits: Vec<TextEdit>, checker: Option<F>) -> ApplyResult
248 where
249 F: FnOnce(&str) -> bool,
250 {
251 if new_edits.is_empty() {
252 return ApplyResult::Applied;
253 }
254
255 for edit in &new_edits {
257 if let Some(rejection) = self.check_safety(edit.safety) {
258 return rejection;
259 }
260 }
261
262 new_edits
263 .sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
264
265 for i in 0..new_edits.len() {
266 let edit = &new_edits[i];
267
268 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
269 return ApplyResult::OutOfBounds;
270 }
271
272 if i > 0 && new_edits[i - 1].range.overlaps(&edit.range) {
273 return ApplyResult::Overlap;
274 }
275 }
276
277 {
278 let mut old_iter = self.edits.iter();
279 let mut new_iter = new_edits.iter();
280 let mut next_old = old_iter.next();
281 let mut next_new = new_iter.next();
282
283 while let (Some(old), Some(new)) = (next_old, next_new) {
284 if old.range.overlaps(&new.range) {
285 return ApplyResult::Overlap;
286 }
287 if old.range.start < new.range.start {
288 next_old = old_iter.next();
289 } else {
290 next_new = new_iter.next();
291 }
292 }
293 }
294
295 if let Some(check_fn) = checker {
296 let simulated_str = stitch_merged(self.original_text, &self.edits, &new_edits);
297 if !check_fn(&simulated_str) {
298 return ApplyResult::Rejected;
299 }
300 }
301
302 self.edits.reserve(new_edits.len());
303 self.edits.extend(new_edits);
304 self.edits.sort_by(|a, b| a.range.start.cmp(&b.range.start));
305
306 ApplyResult::Applied
307 }
308
309 pub fn finish(self) -> String {
311 stitch(self.original_text, &self.edits)
312 }
313
314 pub fn get_edits(&self) -> &[TextEdit] {
316 &self.edits
317 }
318
319 pub fn safety_threshold(&self) -> Safety {
321 self.safety_threshold
322 }
323}
324
325fn stitch(original: &str, edits: &[TextEdit]) -> String {
328 let mut final_len = original.len();
329 for edit in edits {
330 final_len = final_len.saturating_sub(edit.range.len() as usize).saturating_add(edit.new_text.len());
331 }
332
333 let mut output = String::with_capacity(final_len);
334 let mut last_processed = 0;
335
336 for edit in edits {
337 let start = edit.range.start as usize;
338 let end = edit.range.end as usize;
339
340 if start > last_processed {
341 output.push_str(&original[last_processed..start]);
342 }
343 output.push_str(&edit.new_text);
344 last_processed = end;
345 }
346
347 if last_processed < original.len() {
348 output.push_str(&original[last_processed..]);
349 }
350
351 output
352}
353
354fn stitch_one(original: &str, existing_edits: &[TextEdit], new_edit: &TextEdit) -> String {
356 let slice = std::slice::from_ref(new_edit);
357 stitch_merged(original, existing_edits, slice)
358}
359
360fn stitch_merged(original: &str, old_edits: &[TextEdit], new_edits: &[TextEdit]) -> String {
363 let mut final_len = original.len();
364 for e in old_edits {
365 final_len = final_len - e.range.len() as usize + e.new_text.len();
366 }
367 for e in new_edits {
368 final_len = final_len - e.range.len() as usize + e.new_text.len();
369 }
370
371 let mut output = String::with_capacity(final_len);
372 let mut last_processed = 0;
373
374 let mut old_iter = old_edits.iter();
375 let mut new_iter = new_edits.iter();
376 let mut next_old = old_iter.next();
377 let mut next_new = new_iter.next();
378
379 loop {
380 let next_edit = match (next_old, next_new) {
381 (Some(o), Some(n)) => {
382 if o.range.start < n.range.start {
383 next_old = old_iter.next();
384 o
385 } else {
386 next_new = new_iter.next();
387 n
388 }
389 }
390 (Some(o), None) => {
391 next_old = old_iter.next();
392 o
393 }
394 (None, Some(n)) => {
395 next_new = new_iter.next();
396 n
397 }
398 (None, None) => break,
399 };
400
401 let start = next_edit.range.start as usize;
402 let end = next_edit.range.end as usize;
403
404 if start > last_processed {
405 output.push_str(&original[last_processed..start]);
406 }
407 output.push_str(&next_edit.new_text);
408 last_processed = end;
409 }
410
411 if last_processed < original.len() {
412 output.push_str(&original[last_processed..]);
413 }
414
415 output
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_apply_single() {
424 let mut editor = TextEditor::new("hello world");
425 editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
426 assert_eq!(editor.finish(), "hi world");
427 }
428
429 #[test]
430 fn test_checker_fail() {
431 let mut editor = TextEditor::new("abc");
432 let res = editor.apply(TextEdit::delete(0..1), Some(|s: &str| s.len() > 10));
435 assert_eq!(res, ApplyResult::Rejected);
437 assert_eq!(editor.finish(), "abc"); }
439
440 #[test]
441 fn test_overlap_search() {
442 let mut editor = TextEditor::new("0123456789");
443 editor.apply(TextEdit::replace(2..4, "x"), None::<fn(&str) -> bool>); assert_eq!(editor.apply(TextEdit::replace(3..5, "y"), None::<fn(&str) -> bool>), ApplyResult::Overlap);
447
448 assert_eq!(editor.apply(TextEdit::replace(1..3, "y"), None::<fn(&str) -> bool>), ApplyResult::Overlap);
450
451 assert_eq!(editor.apply(TextEdit::replace(4..5, "y"), None::<fn(&str) -> bool>), ApplyResult::Applied);
453
454 assert_eq!(editor.finish(), "01xy56789");
455 }
456
457 #[test]
458 fn test_batch_apply_ordering() {
459 let mut editor = TextEditor::new("abcdef");
460
461 let batch = vec![
463 TextEdit::replace(4..5, "E"), TextEdit::replace(0..1, "A"), ];
466
467 editor.apply_batch(batch, None::<fn(&str) -> bool>);
468 assert_eq!(editor.finish(), "AbcdEf");
469 }
470
471 #[test]
472 fn test_safety_default_is_safe() {
473 let edit = TextEdit::replace(0..1, "x");
474 assert_eq!(edit.safety, Safety::Safe);
475 }
476
477 #[test]
478 fn test_with_safety_builder() {
479 let edit = TextEdit::replace(0..1, "x").with_safety(Safety::Unsafe);
480 assert_eq!(edit.safety, Safety::Unsafe);
481
482 let edit = TextEdit::delete(0..1).with_safety(Safety::PotentiallyUnsafe);
483 assert_eq!(edit.safety, Safety::PotentiallyUnsafe);
484 }
485
486 #[test]
487 fn test_safety_threshold_safe_mode() {
488 let mut editor = TextEditor::with_safety("hello world", Safety::Safe);
489
490 let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
492 assert_eq!(res, ApplyResult::Applied);
493
494 let res = editor
496 .apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
497 assert_eq!(res, ApplyResult::PotentiallyUnsafe);
498
499 let res = editor.apply(TextEdit::replace(6..11, "there").with_safety(Safety::Unsafe), None::<fn(&str) -> bool>);
501 assert_eq!(res, ApplyResult::Unsafe);
502
503 assert_eq!(editor.finish(), "hi world"); }
505
506 #[test]
507 fn test_safety_threshold_potentially_unsafe_mode() {
508 let mut editor = TextEditor::with_safety("hello world", Safety::PotentiallyUnsafe);
509
510 let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
512 assert_eq!(res, ApplyResult::Applied);
513
514 let res = editor
516 .apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
517 assert_eq!(res, ApplyResult::Applied);
518
519 assert_eq!(editor.finish(), "hi there");
520 }
521
522 #[test]
523 fn test_safety_threshold_unsafe_mode() {
524 let mut editor = TextEditor::with_safety("hello world", Safety::Unsafe);
525
526 let res = editor.apply(TextEdit::replace(0..1, "H"), None::<fn(&str) -> bool>);
528 assert_eq!(res, ApplyResult::Applied);
529
530 let res =
531 editor.apply(TextEdit::replace(1..2, "E").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
532 assert_eq!(res, ApplyResult::Applied);
533
534 let res = editor.apply(TextEdit::replace(2..3, "L").with_safety(Safety::Unsafe), None::<fn(&str) -> bool>);
535 assert_eq!(res, ApplyResult::Applied);
536
537 assert_eq!(editor.finish(), "HELlo world");
538 }
539
540 #[test]
541 fn test_batch_safety_rejection() {
542 let mut editor = TextEditor::with_safety("hello", Safety::Safe);
543
544 let batch = vec![
546 TextEdit::replace(0..1, "H"), TextEdit::replace(1..2, "E").with_safety(Safety::Unsafe), ];
549
550 let res = editor.apply_batch(batch, None::<fn(&str) -> bool>);
551 assert_eq!(res, ApplyResult::Unsafe);
552
553 assert_eq!(editor.finish(), "hello");
555 }
556
557 #[test]
558 fn test_safety_ordering() {
559 assert!(Safety::Safe < Safety::PotentiallyUnsafe);
561 assert!(Safety::PotentiallyUnsafe < Safety::Unsafe);
562 assert!(Safety::Safe < Safety::Unsafe);
563 }
564
565 #[test]
566 fn test_insert_at_same_offset_overlaps_replace() {
567 let mut editor = TextEditor::new("0123456789");
568
569 let res = editor.apply(TextEdit::replace(2..8, "replaced"), None::<fn(&str) -> bool>);
570 assert_eq!(res, ApplyResult::Applied);
571
572 let res = editor.apply(TextEdit::insert(2, "inserted"), None::<fn(&str) -> bool>);
573 assert_eq!(res, ApplyResult::Overlap);
574
575 assert_eq!(editor.finish(), "01replaced89");
576 }
577
578 #[test]
579 fn test_insert_after_replace_at_different_offset() {
580 let mut editor = TextEditor::new("0123456789");
581
582 let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&str) -> bool>);
583 assert_eq!(res, ApplyResult::Applied);
584
585 let res = editor.apply(TextEdit::insert(6, "X"), None::<fn(&str) -> bool>);
586 assert_eq!(res, ApplyResult::Applied);
587
588 assert_eq!(editor.finish(), "01ABC5X6789");
589 }
590
591 #[test]
592 fn test_insert_at_start_of_replace_overlaps() {
593 let mut editor = TextEditor::new("0123456789");
596
597 let res = editor.apply(TextEdit::replace(2..5, "ABC"), None::<fn(&str) -> bool>);
598 assert_eq!(res, ApplyResult::Applied);
599
600 let res = editor.apply(TextEdit::insert(2, "X"), None::<fn(&str) -> bool>);
601 assert_eq!(res, ApplyResult::Overlap);
602
603 assert_eq!(editor.finish(), "01ABC56789");
604 }
605
606 #[test]
607 fn test_batch_insert_and_replace_at_same_offset_overlap() {
608 let mut editor = TextEditor::new("0123456789");
609
610 let batch = vec![
611 TextEdit::insert(2, "inserted"), TextEdit::replace(2..5, "ABC"), ];
614
615 let res = editor.apply_batch(batch, None::<fn(&str) -> bool>);
616 assert_eq!(res, ApplyResult::Overlap);
617
618 assert_eq!(editor.finish(), "0123456789");
619 }
620
621 #[test]
622 fn test_touching_ranges_overlap() {
623 let range1 = TextRange::new(0, 5);
624 let range2 = TextRange::new(5, 10);
625 assert!(range1.overlaps(&range2));
626 assert!(range2.overlaps(&range1));
627 }
628
629 #[test]
630 fn test_insert_range_overlaps_with_adjacent_range() {
631 let insert_range = TextRange::new(5, 5);
632 let replace_range = TextRange::new(5, 10);
633 assert!(insert_range.overlaps(&replace_range));
634 assert!(replace_range.overlaps(&insert_range));
635 }
636}