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)]
54 pub fn overlaps(&self, other: &TextRange) -> bool {
55 self.start < other.end && other.start < self.end || (self.start == other.start && self.end == other.end)
56 }
57
58 #[inline(always)]
60 pub fn contains(&self, offset: u32) -> bool {
61 offset >= self.start && offset < self.end
62 }
63}
64
65impl<T> From<T> for TextRange
66where
67 T: std::ops::RangeBounds<u32>,
68{
69 #[inline(always)]
70 fn from(r: T) -> Self {
71 let start = match r.start_bound() {
72 std::ops::Bound::Included(&s) => s,
73 std::ops::Bound::Excluded(&s) => s + 1,
74 std::ops::Bound::Unbounded => 0,
75 };
76
77 let end = match r.end_bound() {
78 std::ops::Bound::Included(&e) => e + 1,
79 std::ops::Bound::Excluded(&e) => e,
80 std::ops::Bound::Unbounded => u32::MAX, };
82
83 Self::new(start, end)
84 }
85}
86
87#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
92pub struct TextEdit {
93 pub range: TextRange,
95 pub new_text: String,
97 pub safety: Safety,
99}
100
101impl TextEdit {
102 pub fn delete(range: impl Into<TextRange>) -> Self {
104 Self { range: range.into(), new_text: String::new(), safety: Safety::Safe }
105 }
106
107 pub fn insert(offset: u32, text: impl Into<String>) -> Self {
109 Self { range: TextRange::new(offset, offset), new_text: text.into(), safety: Safety::Safe }
110 }
111
112 pub fn replace(range: impl Into<TextRange>, text: impl Into<String>) -> Self {
114 Self { range: range.into(), new_text: text.into(), safety: Safety::Safe }
115 }
116
117 #[must_use]
127 pub fn with_safety(mut self, safety: Safety) -> Self {
128 self.safety = safety;
129 self
130 }
131}
132
133#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
134pub enum ApplyResult {
135 Applied,
137 OutOfBounds,
139 Overlap,
141 Rejected,
143 Unsafe,
145 PotentiallyUnsafe,
147}
148
149#[derive(Debug, Clone)]
154pub struct TextEditor<'a> {
155 original_text: &'a str,
156 original_len: u32,
157 edits: Vec<TextEdit>,
158 safety_threshold: Safety,
160}
161
162impl<'a> TextEditor<'a> {
163 pub fn new(text: &'a str) -> Self {
165 Self {
166 original_text: text,
167 original_len: text.len() as u32,
168 edits: Vec::new(),
169 safety_threshold: Safety::Unsafe,
170 }
171 }
172
173 pub fn with_safety(text: &'a str, threshold: Safety) -> Self {
185 Self { original_text: text, original_len: text.len() as u32, edits: Vec::new(), safety_threshold: threshold }
186 }
187
188 #[inline]
191 fn check_safety(&self, edit_safety: Safety) -> Option<ApplyResult> {
192 if edit_safety > self.safety_threshold {
193 Some(match edit_safety {
194 Safety::Unsafe => ApplyResult::Unsafe,
195 Safety::PotentiallyUnsafe => ApplyResult::PotentiallyUnsafe,
196 Safety::Safe => unreachable!(),
197 })
198 } else {
199 None
200 }
201 }
202
203 pub fn apply<F>(&mut self, edit: TextEdit, checker: Option<F>) -> ApplyResult
208 where
209 F: FnOnce(&str) -> bool,
210 {
211 if let Some(rejection) = self.check_safety(edit.safety) {
213 return rejection;
214 }
215
216 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
217 return ApplyResult::OutOfBounds;
218 }
219
220 let search_idx = self.edits.partition_point(|e| e.range.end <= edit.range.start);
221
222 if let Some(existing) = self.edits.get(search_idx)
223 && existing.range.overlaps(&edit.range)
224 {
225 return ApplyResult::Overlap;
226 }
227
228 if let Some(check_fn) = checker {
229 let simulated_str = stitch_one(self.original_text, &self.edits, &edit);
230 if !check_fn(&simulated_str) {
231 return ApplyResult::Rejected;
232 }
233 }
234
235 self.edits.insert(search_idx, edit);
236
237 ApplyResult::Applied
238 }
239
240 pub fn apply_batch<F>(&mut self, mut new_edits: Vec<TextEdit>, checker: Option<F>) -> ApplyResult
245 where
246 F: FnOnce(&str) -> bool,
247 {
248 if new_edits.is_empty() {
249 return ApplyResult::Applied;
250 }
251
252 for edit in &new_edits {
254 if let Some(rejection) = self.check_safety(edit.safety) {
255 return rejection;
256 }
257 }
258
259 new_edits
260 .sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start).then_with(|| a.range.end.cmp(&b.range.end)));
261
262 for i in 0..new_edits.len() {
263 let edit = &new_edits[i];
264
265 if edit.range.end > self.original_len || edit.range.start > edit.range.end {
266 return ApplyResult::OutOfBounds;
267 }
268
269 if i > 0 && new_edits[i - 1].range.overlaps(&edit.range) {
270 return ApplyResult::Overlap;
271 }
272 }
273
274 {
275 let mut old_iter = self.edits.iter();
276 let mut new_iter = new_edits.iter();
277 let mut next_old = old_iter.next();
278 let mut next_new = new_iter.next();
279
280 while let (Some(old), Some(new)) = (next_old, next_new) {
281 if old.range.overlaps(&new.range) {
282 return ApplyResult::Overlap;
283 }
284 if old.range.start < new.range.start {
285 next_old = old_iter.next();
286 } else {
287 next_new = new_iter.next();
288 }
289 }
290 }
291
292 if let Some(check_fn) = checker {
293 let simulated_str = stitch_merged(self.original_text, &self.edits, &new_edits);
294 if !check_fn(&simulated_str) {
295 return ApplyResult::Rejected;
296 }
297 }
298
299 self.edits.reserve(new_edits.len());
300 self.edits.extend(new_edits);
301 self.edits.sort_by(|a, b| a.range.start.cmp(&b.range.start));
302
303 ApplyResult::Applied
304 }
305
306 pub fn finish(self) -> String {
308 stitch(self.original_text, &self.edits)
309 }
310
311 pub fn get_edits(&self) -> &[TextEdit] {
313 &self.edits
314 }
315
316 pub fn safety_threshold(&self) -> Safety {
318 self.safety_threshold
319 }
320}
321
322fn stitch(original: &str, edits: &[TextEdit]) -> String {
325 let mut final_len = original.len();
326 for edit in edits {
327 final_len = final_len.saturating_sub(edit.range.len() as usize).saturating_add(edit.new_text.len());
328 }
329
330 let mut output = String::with_capacity(final_len);
331 let mut last_processed = 0;
332
333 for edit in edits {
334 let start = edit.range.start as usize;
335 let end = edit.range.end as usize;
336
337 if start > last_processed {
338 output.push_str(&original[last_processed..start]);
339 }
340 output.push_str(&edit.new_text);
341 last_processed = end;
342 }
343
344 if last_processed < original.len() {
345 output.push_str(&original[last_processed..]);
346 }
347
348 output
349}
350
351fn stitch_one(original: &str, existing_edits: &[TextEdit], new_edit: &TextEdit) -> String {
353 let slice = std::slice::from_ref(new_edit);
354 stitch_merged(original, existing_edits, slice)
355}
356
357fn stitch_merged(original: &str, old_edits: &[TextEdit], new_edits: &[TextEdit]) -> String {
360 let mut final_len = original.len();
361 for e in old_edits {
362 final_len = final_len - e.range.len() as usize + e.new_text.len();
363 }
364 for e in new_edits {
365 final_len = final_len - e.range.len() as usize + e.new_text.len();
366 }
367
368 let mut output = String::with_capacity(final_len);
369 let mut last_processed = 0;
370
371 let mut old_iter = old_edits.iter();
372 let mut new_iter = new_edits.iter();
373 let mut next_old = old_iter.next();
374 let mut next_new = new_iter.next();
375
376 loop {
377 let next_edit = match (next_old, next_new) {
378 (Some(o), Some(n)) => {
379 if o.range.start < n.range.start {
380 next_old = old_iter.next();
381 o
382 } else {
383 next_new = new_iter.next();
384 n
385 }
386 }
387 (Some(o), None) => {
388 next_old = old_iter.next();
389 o
390 }
391 (None, Some(n)) => {
392 next_new = new_iter.next();
393 n
394 }
395 (None, None) => break,
396 };
397
398 let start = next_edit.range.start as usize;
399 let end = next_edit.range.end as usize;
400
401 if start > last_processed {
402 output.push_str(&original[last_processed..start]);
403 }
404 output.push_str(&next_edit.new_text);
405 last_processed = end;
406 }
407
408 if last_processed < original.len() {
409 output.push_str(&original[last_processed..]);
410 }
411
412 output
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_apply_single() {
421 let mut editor = TextEditor::new("hello world");
422 editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
423 assert_eq!(editor.finish(), "hi world");
424 }
425
426 #[test]
427 fn test_checker_fail() {
428 let mut editor = TextEditor::new("abc");
429 let res = editor.apply(TextEdit::delete(0..1), Some(|s: &str| s.len() > 10));
432 assert_eq!(res, ApplyResult::Rejected);
434 assert_eq!(editor.finish(), "abc"); }
436
437 #[test]
438 fn test_overlap_search() {
439 let mut editor = TextEditor::new("0123456789");
440 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);
444
445 assert_eq!(editor.apply(TextEdit::replace(1..3, "y"), None::<fn(&str) -> bool>), ApplyResult::Overlap);
447
448 assert_eq!(editor.apply(TextEdit::replace(4..5, "y"), None::<fn(&str) -> bool>), ApplyResult::Applied);
450
451 assert_eq!(editor.finish(), "01xy56789");
452 }
453
454 #[test]
455 fn test_batch_apply_ordering() {
456 let mut editor = TextEditor::new("abcdef");
457
458 let batch = vec![
460 TextEdit::replace(4..5, "E"), TextEdit::replace(0..1, "A"), ];
463
464 editor.apply_batch(batch, None::<fn(&str) -> bool>);
465 assert_eq!(editor.finish(), "AbcdEf");
466 }
467
468 #[test]
469 fn test_safety_default_is_safe() {
470 let edit = TextEdit::replace(0..1, "x");
471 assert_eq!(edit.safety, Safety::Safe);
472 }
473
474 #[test]
475 fn test_with_safety_builder() {
476 let edit = TextEdit::replace(0..1, "x").with_safety(Safety::Unsafe);
477 assert_eq!(edit.safety, Safety::Unsafe);
478
479 let edit = TextEdit::delete(0..1).with_safety(Safety::PotentiallyUnsafe);
480 assert_eq!(edit.safety, Safety::PotentiallyUnsafe);
481 }
482
483 #[test]
484 fn test_safety_threshold_safe_mode() {
485 let mut editor = TextEditor::with_safety("hello world", Safety::Safe);
486
487 let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
489 assert_eq!(res, ApplyResult::Applied);
490
491 let res = editor
493 .apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
494 assert_eq!(res, ApplyResult::PotentiallyUnsafe);
495
496 let res = editor.apply(TextEdit::replace(6..11, "there").with_safety(Safety::Unsafe), None::<fn(&str) -> bool>);
498 assert_eq!(res, ApplyResult::Unsafe);
499
500 assert_eq!(editor.finish(), "hi world"); }
502
503 #[test]
504 fn test_safety_threshold_potentially_unsafe_mode() {
505 let mut editor = TextEditor::with_safety("hello world", Safety::PotentiallyUnsafe);
506
507 let res = editor.apply(TextEdit::replace(0..5, "hi"), None::<fn(&str) -> bool>);
509 assert_eq!(res, ApplyResult::Applied);
510
511 let res = editor
513 .apply(TextEdit::replace(6..11, "there").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
514 assert_eq!(res, ApplyResult::Applied);
515
516 assert_eq!(editor.finish(), "hi there");
517 }
518
519 #[test]
520 fn test_safety_threshold_unsafe_mode() {
521 let mut editor = TextEditor::with_safety("hello world", Safety::Unsafe);
522
523 let res = editor.apply(TextEdit::replace(0..1, "H"), None::<fn(&str) -> bool>);
525 assert_eq!(res, ApplyResult::Applied);
526
527 let res =
528 editor.apply(TextEdit::replace(1..2, "E").with_safety(Safety::PotentiallyUnsafe), None::<fn(&str) -> bool>);
529 assert_eq!(res, ApplyResult::Applied);
530
531 let res = editor.apply(TextEdit::replace(2..3, "L").with_safety(Safety::Unsafe), None::<fn(&str) -> bool>);
532 assert_eq!(res, ApplyResult::Applied);
533
534 assert_eq!(editor.finish(), "HELlo world");
535 }
536
537 #[test]
538 fn test_batch_safety_rejection() {
539 let mut editor = TextEditor::with_safety("hello", Safety::Safe);
540
541 let batch = vec![
543 TextEdit::replace(0..1, "H"), TextEdit::replace(1..2, "E").with_safety(Safety::Unsafe), ];
546
547 let res = editor.apply_batch(batch, None::<fn(&str) -> bool>);
548 assert_eq!(res, ApplyResult::Unsafe);
549
550 assert_eq!(editor.finish(), "hello");
552 }
553
554 #[test]
555 fn test_safety_ordering() {
556 assert!(Safety::Safe < Safety::PotentiallyUnsafe);
558 assert!(Safety::PotentiallyUnsafe < Safety::Unsafe);
559 assert!(Safety::Safe < Safety::Unsafe);
560 }
561}