1use smallvec::SmallVec;
2
3use crate::{Edit, PartialEdit};
4
5/// Events emitted by `ToolEditParser` as tool call input streams in.
6#[derive(Debug, PartialEq, Eq)]
7pub enum ToolEditEvent {
8 /// A chunk of `old_text` for an edit operation.
9 OldTextChunk {
10 edit_index: usize,
11 chunk: String,
12 done: bool,
13 },
14 /// A chunk of `new_text` for an edit operation.
15 NewTextChunk {
16 edit_index: usize,
17 chunk: String,
18 done: bool,
19 },
20 /// A chunk of content for write/overwrite mode.
21 ContentChunk { chunk: String },
22}
23
24/// Tracks the streaming state of a single edit to detect deltas.
25#[derive(Default, Debug)]
26struct EditStreamState {
27 old_text_emitted_len: usize,
28 old_text_done: bool,
29 new_text_emitted_len: usize,
30 new_text_done: bool,
31}
32
33/// Converts incrementally-growing tool call JSON into a stream of chunk events.
34///
35/// The tool call streaming infrastructure delivers partial JSON objects where
36/// string fields grow over time. This parser compares consecutive partials,
37/// computes the deltas, and emits `ToolEditEvent`s that downstream pipeline
38/// stages (`StreamingFuzzyMatcher` for old_text, `StreamingDiff` for new_text)
39/// can consume incrementally.
40///
41/// Because partial JSON comes through a fixer (`partial-json-fixer`) that
42/// closes incomplete escape sequences, a string can temporarily contain wrong
43/// trailing characters (e.g. a literal `\` instead of `\n`). We handle this
44/// by holding back trailing backslash characters in non-finalized chunks: if
45/// a partial string ends with `\` (0x5C), that byte is not emitted until the
46/// next partial confirms or corrects it. This avoids feeding corrupted bytes
47/// to downstream consumers.
48#[derive(Default, Debug)]
49pub struct ToolEditParser {
50 edit_states: Vec<EditStreamState>,
51 content_emitted_len: usize,
52}
53
54impl ToolEditParser {
55 /// Push a new set of partial edits (from edit mode) and return any events.
56 ///
57 /// Each call should pass the *entire current* edits array as seen in the
58 /// latest partial input. The parser will diff it against its internal state
59 /// to produce only the new events.
60 pub fn push_edits(&mut self, edits: &[PartialEdit]) -> SmallVec<[ToolEditEvent; 4]> {
61 let mut events = SmallVec::new();
62
63 for (index, partial) in edits.iter().enumerate() {
64 if index >= self.edit_states.len() {
65 // A new edit appeared — finalize the previous one if there was one.
66 if let Some(previous) = self.finalize_previous_edit(index) {
67 events.extend(previous);
68 }
69 self.edit_states.push(EditStreamState::default());
70 }
71
72 let state = &mut self.edit_states[index];
73
74 // Process old_text changes.
75 if let Some(old_text) = &partial.old_text
76 && !state.old_text_done
77 {
78 if partial.new_text.is_some() {
79 // new_text appeared, so old_text is done — emit everything.
80 let start = state.old_text_emitted_len.min(old_text.len());
81 let chunk = old_text[start..].to_string();
82 state.old_text_done = true;
83 state.old_text_emitted_len = old_text.len();
84 events.push(ToolEditEvent::OldTextChunk {
85 edit_index: index,
86 chunk,
87 done: true,
88 });
89 } else {
90 let safe_end = safe_emit_end(old_text);
91 if safe_end > state.old_text_emitted_len {
92 let chunk = old_text[state.old_text_emitted_len..safe_end].to_string();
93 state.old_text_emitted_len = safe_end;
94 events.push(ToolEditEvent::OldTextChunk {
95 edit_index: index,
96 chunk,
97 done: false,
98 });
99 }
100 }
101 }
102
103 // Process new_text changes.
104 if let Some(new_text) = &partial.new_text
105 && !state.new_text_done
106 {
107 let safe_end = safe_emit_end(new_text);
108 if safe_end > state.new_text_emitted_len {
109 let chunk = new_text[state.new_text_emitted_len..safe_end].to_string();
110 state.new_text_emitted_len = safe_end;
111 events.push(ToolEditEvent::NewTextChunk {
112 edit_index: index,
113 chunk,
114 done: false,
115 });
116 }
117 }
118 }
119
120 events
121 }
122
123 /// Push new content and return any events.
124 ///
125 /// Each call should pass the *entire current* content string. The parser
126 /// will diff it against its internal state to emit only the new chunk.
127 pub fn push_content(&mut self, content: &str) -> SmallVec<[ToolEditEvent; 1]> {
128 let mut events = SmallVec::new();
129
130 let safe_end = safe_emit_end(content);
131 if safe_end > self.content_emitted_len {
132 let chunk = content[self.content_emitted_len..safe_end].to_string();
133 self.content_emitted_len = safe_end;
134 events.push(ToolEditEvent::ContentChunk { chunk });
135 }
136
137 events
138 }
139
140 /// Finalize all edits with the complete input. This emits `done: true`
141 /// events for any in-progress old_text or new_text that hasn't been
142 /// finalized yet.
143 ///
144 /// `final_edits` should be the fully deserialized final edits array. The
145 /// parser compares against its tracked state and emits any remaining deltas
146 /// with `done: true`.
147 pub fn finalize_edits(&mut self, edits: &[Edit]) -> SmallVec<[ToolEditEvent; 4]> {
148 let mut events = SmallVec::new();
149
150 for (index, edit) in edits.iter().enumerate() {
151 if index >= self.edit_states.len() {
152 // This edit was never seen in partials — emit it fully.
153 if let Some(previous) = self.finalize_previous_edit(index) {
154 events.extend(previous);
155 }
156 self.edit_states.push(EditStreamState::default());
157 }
158
159 let state = &mut self.edit_states[index];
160
161 if !state.old_text_done {
162 let start = state.old_text_emitted_len.min(edit.old_text.len());
163 let chunk = edit.old_text[start..].to_string();
164 state.old_text_done = true;
165 state.old_text_emitted_len = edit.old_text.len();
166 events.push(ToolEditEvent::OldTextChunk {
167 edit_index: index,
168 chunk,
169 done: true,
170 });
171 }
172
173 if !state.new_text_done {
174 let start = state.new_text_emitted_len.min(edit.new_text.len());
175 let chunk = edit.new_text[start..].to_string();
176 state.new_text_done = true;
177 state.new_text_emitted_len = edit.new_text.len();
178 events.push(ToolEditEvent::NewTextChunk {
179 edit_index: index,
180 chunk,
181 done: true,
182 });
183 }
184 }
185
186 events
187 }
188
189 /// Finalize content with the complete input.
190 pub fn finalize_content(&mut self, content: &str) -> SmallVec<[ToolEditEvent; 1]> {
191 let mut events = SmallVec::new();
192
193 let start = self.content_emitted_len.min(content.len());
194 if content.len() > start {
195 let chunk = content[start..].to_string();
196 self.content_emitted_len = content.len();
197 events.push(ToolEditEvent::ContentChunk { chunk });
198 }
199
200 events
201 }
202
203 /// When a new edit appears at `index`, finalize the edit at `index - 1`
204 /// by emitting a `NewTextChunk { done: true }` if it hasn't been finalized.
205 fn finalize_previous_edit(&mut self, new_index: usize) -> Option<SmallVec<[ToolEditEvent; 2]>> {
206 if new_index == 0 || self.edit_states.is_empty() {
207 return None;
208 }
209
210 let previous_index = new_index - 1;
211 if previous_index >= self.edit_states.len() {
212 return None;
213 }
214
215 let state = &mut self.edit_states[previous_index];
216 let mut events = SmallVec::new();
217
218 // If old_text was never finalized, finalize it now with an empty done chunk.
219 if !state.old_text_done {
220 state.old_text_done = true;
221 events.push(ToolEditEvent::OldTextChunk {
222 edit_index: previous_index,
223 chunk: String::new(),
224 done: true,
225 });
226 }
227
228 // Emit a done event for new_text if not already finalized.
229 if !state.new_text_done {
230 state.new_text_done = true;
231 events.push(ToolEditEvent::NewTextChunk {
232 edit_index: previous_index,
233 chunk: String::new(),
234 done: true,
235 });
236 }
237
238 Some(events)
239 }
240}
241
242/// Returns the byte position up to which it is safe to emit from a partial
243/// string. If the string ends with a backslash (`\`, 0x5C), that byte is
244/// held back because it may be an artifact of the partial JSON fixer closing
245/// an incomplete escape sequence (e.g. turning a half-received `\n` into `\\`).
246/// The next partial will reveal the correct character.
247fn safe_emit_end(text: &str) -> usize {
248 if text.as_bytes().last() == Some(&b'\\') {
249 text.len() - 1
250 } else {
251 text.len()
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_single_edit_streamed_incrementally() {
261 let mut parser = ToolEditParser::default();
262
263 // old_text arrives in chunks: "hell" → "hello w" → "hello world"
264 let events = parser.push_edits(&[PartialEdit {
265 old_text: Some("hell".into()),
266 new_text: None,
267 }]);
268 assert_eq!(
269 events.as_slice(),
270 &[ToolEditEvent::OldTextChunk {
271 edit_index: 0,
272 chunk: "hell".into(),
273 done: false,
274 }]
275 );
276
277 let events = parser.push_edits(&[PartialEdit {
278 old_text: Some("hello w".into()),
279 new_text: None,
280 }]);
281 assert_eq!(
282 events.as_slice(),
283 &[ToolEditEvent::OldTextChunk {
284 edit_index: 0,
285 chunk: "o w".into(),
286 done: false,
287 }]
288 );
289
290 // new_text appears → old_text finalizes
291 let events = parser.push_edits(&[PartialEdit {
292 old_text: Some("hello world".into()),
293 new_text: Some("good".into()),
294 }]);
295 assert_eq!(
296 events.as_slice(),
297 &[
298 ToolEditEvent::OldTextChunk {
299 edit_index: 0,
300 chunk: "orld".into(),
301 done: true,
302 },
303 ToolEditEvent::NewTextChunk {
304 edit_index: 0,
305 chunk: "good".into(),
306 done: false,
307 },
308 ]
309 );
310
311 // new_text grows
312 let events = parser.push_edits(&[PartialEdit {
313 old_text: Some("hello world".into()),
314 new_text: Some("goodbye world".into()),
315 }]);
316 assert_eq!(
317 events.as_slice(),
318 &[ToolEditEvent::NewTextChunk {
319 edit_index: 0,
320 chunk: "bye world".into(),
321 done: false,
322 }]
323 );
324
325 // Finalize
326 let events = parser.finalize_edits(&[Edit {
327 old_text: "hello world".into(),
328 new_text: "goodbye world".into(),
329 }]);
330 assert_eq!(
331 events.as_slice(),
332 &[ToolEditEvent::NewTextChunk {
333 edit_index: 0,
334 chunk: "".into(),
335 done: true,
336 }]
337 );
338 }
339
340 #[test]
341 fn test_multiple_edits_sequential() {
342 let mut parser = ToolEditParser::default();
343
344 // First edit streams in
345 let events = parser.push_edits(&[PartialEdit {
346 old_text: Some("first old".into()),
347 new_text: None,
348 }]);
349 assert_eq!(
350 events.as_slice(),
351 &[ToolEditEvent::OldTextChunk {
352 edit_index: 0,
353 chunk: "first old".into(),
354 done: false,
355 }]
356 );
357
358 let events = parser.push_edits(&[PartialEdit {
359 old_text: Some("first old".into()),
360 new_text: Some("first new".into()),
361 }]);
362 assert_eq!(
363 events.as_slice(),
364 &[
365 ToolEditEvent::OldTextChunk {
366 edit_index: 0,
367 chunk: "".into(),
368 done: true,
369 },
370 ToolEditEvent::NewTextChunk {
371 edit_index: 0,
372 chunk: "first new".into(),
373 done: false,
374 },
375 ]
376 );
377
378 // Second edit appears → first edit's new_text is finalized
379 let events = parser.push_edits(&[
380 PartialEdit {
381 old_text: Some("first old".into()),
382 new_text: Some("first new".into()),
383 },
384 PartialEdit {
385 old_text: Some("second".into()),
386 new_text: None,
387 },
388 ]);
389 assert_eq!(
390 events.as_slice(),
391 &[
392 ToolEditEvent::NewTextChunk {
393 edit_index: 0,
394 chunk: "".into(),
395 done: true,
396 },
397 ToolEditEvent::OldTextChunk {
398 edit_index: 1,
399 chunk: "second".into(),
400 done: false,
401 },
402 ]
403 );
404
405 // Finalize everything
406 let events = parser.finalize_edits(&[
407 Edit {
408 old_text: "first old".into(),
409 new_text: "first new".into(),
410 },
411 Edit {
412 old_text: "second old".into(),
413 new_text: "second new".into(),
414 },
415 ]);
416 assert_eq!(
417 events.as_slice(),
418 &[
419 ToolEditEvent::OldTextChunk {
420 edit_index: 1,
421 chunk: " old".into(),
422 done: true,
423 },
424 ToolEditEvent::NewTextChunk {
425 edit_index: 1,
426 chunk: "second new".into(),
427 done: true,
428 },
429 ]
430 );
431 }
432
433 #[test]
434 fn test_content_streamed_incrementally() {
435 let mut parser = ToolEditParser::default();
436
437 let events = parser.push_content("hello");
438 assert_eq!(
439 events.as_slice(),
440 &[ToolEditEvent::ContentChunk {
441 chunk: "hello".into(),
442 }]
443 );
444
445 let events = parser.push_content("hello world");
446 assert_eq!(
447 events.as_slice(),
448 &[ToolEditEvent::ContentChunk {
449 chunk: " world".into(),
450 }]
451 );
452
453 // No change
454 let events = parser.push_content("hello world");
455 assert!(events.is_empty());
456
457 let events = parser.push_content("hello world!");
458 assert_eq!(
459 events.as_slice(),
460 &[ToolEditEvent::ContentChunk { chunk: "!".into() }]
461 );
462
463 // Finalize with no additional content
464 let events = parser.finalize_content("hello world!");
465 assert!(events.is_empty());
466 }
467
468 #[test]
469 fn test_finalize_content_with_remaining() {
470 let mut parser = ToolEditParser::default();
471
472 parser.push_content("partial");
473 let events = parser.finalize_content("partial content here");
474 assert_eq!(
475 events.as_slice(),
476 &[ToolEditEvent::ContentChunk {
477 chunk: " content here".into(),
478 }]
479 );
480 }
481
482 #[test]
483 fn test_content_trailing_backslash_held_back() {
484 let mut parser = ToolEditParser::default();
485
486 // Partial JSON fixer turns incomplete \n into \\ (literal backslash).
487 // The trailing backslash is held back.
488 let events = parser.push_content("hello,\\");
489 assert_eq!(
490 events.as_slice(),
491 &[ToolEditEvent::ContentChunk {
492 chunk: "hello,".into(),
493 }]
494 );
495
496 // Next partial corrects the escape to an actual newline.
497 // The held-back byte was wrong; the correct newline is emitted.
498 let events = parser.push_content("hello,\n");
499 assert_eq!(
500 events.as_slice(),
501 &[ToolEditEvent::ContentChunk { chunk: "\n".into() }]
502 );
503
504 // Normal growth.
505 let events = parser.push_content("hello,\nworld");
506 assert_eq!(
507 events.as_slice(),
508 &[ToolEditEvent::ContentChunk {
509 chunk: "world".into(),
510 }]
511 );
512 }
513
514 #[test]
515 fn test_content_finalize_with_trailing_backslash() {
516 let mut parser = ToolEditParser::default();
517
518 // Stream a partial with a fixer-corrupted trailing backslash.
519 // The backslash is held back.
520 parser.push_content("abc\\");
521
522 // Finalize reveals the correct character.
523 let events = parser.finalize_content("abc\n");
524 assert_eq!(
525 events.as_slice(),
526 &[ToolEditEvent::ContentChunk { chunk: "\n".into() }]
527 );
528 }
529
530 #[test]
531 fn test_no_partials_direct_finalize() {
532 let mut parser = ToolEditParser::default();
533
534 let events = parser.finalize_edits(&[Edit {
535 old_text: "old".into(),
536 new_text: "new".into(),
537 }]);
538 assert_eq!(
539 events.as_slice(),
540 &[
541 ToolEditEvent::OldTextChunk {
542 edit_index: 0,
543 chunk: "old".into(),
544 done: true,
545 },
546 ToolEditEvent::NewTextChunk {
547 edit_index: 0,
548 chunk: "new".into(),
549 done: true,
550 },
551 ]
552 );
553 }
554
555 #[test]
556 fn test_no_partials_direct_finalize_multiple() {
557 let mut parser = ToolEditParser::default();
558
559 let events = parser.finalize_edits(&[
560 Edit {
561 old_text: "first old".into(),
562 new_text: "first new".into(),
563 },
564 Edit {
565 old_text: "second old".into(),
566 new_text: "second new".into(),
567 },
568 ]);
569 assert_eq!(
570 events.as_slice(),
571 &[
572 ToolEditEvent::OldTextChunk {
573 edit_index: 0,
574 chunk: "first old".into(),
575 done: true,
576 },
577 ToolEditEvent::NewTextChunk {
578 edit_index: 0,
579 chunk: "first new".into(),
580 done: true,
581 },
582 ToolEditEvent::OldTextChunk {
583 edit_index: 1,
584 chunk: "second old".into(),
585 done: true,
586 },
587 ToolEditEvent::NewTextChunk {
588 edit_index: 1,
589 chunk: "second new".into(),
590 done: true,
591 },
592 ]
593 );
594 }
595
596 #[test]
597 fn test_old_text_no_growth() {
598 let mut parser = ToolEditParser::default();
599
600 let events = parser.push_edits(&[PartialEdit {
601 old_text: Some("same".into()),
602 new_text: None,
603 }]);
604 assert_eq!(
605 events.as_slice(),
606 &[ToolEditEvent::OldTextChunk {
607 edit_index: 0,
608 chunk: "same".into(),
609 done: false,
610 }]
611 );
612
613 // Same old_text, no new_text → no events
614 let events = parser.push_edits(&[PartialEdit {
615 old_text: Some("same".into()),
616 new_text: None,
617 }]);
618 assert!(events.is_empty());
619 }
620
621 #[test]
622 fn test_old_text_none_then_appears() {
623 let mut parser = ToolEditParser::default();
624
625 // Edit exists but old_text is None (field hasn't arrived yet)
626 let events = parser.push_edits(&[PartialEdit {
627 old_text: None,
628 new_text: None,
629 }]);
630 assert!(events.is_empty());
631
632 // old_text appears
633 let events = parser.push_edits(&[PartialEdit {
634 old_text: Some("text".into()),
635 new_text: None,
636 }]);
637 assert_eq!(
638 events.as_slice(),
639 &[ToolEditEvent::OldTextChunk {
640 edit_index: 0,
641 chunk: "text".into(),
642 done: false,
643 }]
644 );
645 }
646
647 #[test]
648 fn test_empty_old_text_with_new_text() {
649 let mut parser = ToolEditParser::default();
650
651 // old_text is empty, new_text appears immediately
652 let events = parser.push_edits(&[PartialEdit {
653 old_text: Some("".into()),
654 new_text: Some("inserted".into()),
655 }]);
656 assert_eq!(
657 events.as_slice(),
658 &[
659 ToolEditEvent::OldTextChunk {
660 edit_index: 0,
661 chunk: "".into(),
662 done: true,
663 },
664 ToolEditEvent::NewTextChunk {
665 edit_index: 0,
666 chunk: "inserted".into(),
667 done: false,
668 },
669 ]
670 );
671 }
672
673 #[test]
674 fn test_three_edits_streamed() {
675 let mut parser = ToolEditParser::default();
676
677 // Stream first edit
678 parser.push_edits(&[PartialEdit {
679 old_text: Some("a".into()),
680 new_text: Some("A".into()),
681 }]);
682
683 // Second edit appears
684 parser.push_edits(&[
685 PartialEdit {
686 old_text: Some("a".into()),
687 new_text: Some("A".into()),
688 },
689 PartialEdit {
690 old_text: Some("b".into()),
691 new_text: Some("B".into()),
692 },
693 ]);
694
695 // Third edit appears
696 let events = parser.push_edits(&[
697 PartialEdit {
698 old_text: Some("a".into()),
699 new_text: Some("A".into()),
700 },
701 PartialEdit {
702 old_text: Some("b".into()),
703 new_text: Some("B".into()),
704 },
705 PartialEdit {
706 old_text: Some("c".into()),
707 new_text: None,
708 },
709 ]);
710
711 // Should finalize edit 1 (index=1) and start edit 2 (index=2)
712 assert_eq!(
713 events.as_slice(),
714 &[
715 ToolEditEvent::NewTextChunk {
716 edit_index: 1,
717 chunk: "".into(),
718 done: true,
719 },
720 ToolEditEvent::OldTextChunk {
721 edit_index: 2,
722 chunk: "c".into(),
723 done: false,
724 },
725 ]
726 );
727
728 // Finalize
729 let events = parser.finalize_edits(&[
730 Edit {
731 old_text: "a".into(),
732 new_text: "A".into(),
733 },
734 Edit {
735 old_text: "b".into(),
736 new_text: "B".into(),
737 },
738 Edit {
739 old_text: "c".into(),
740 new_text: "C".into(),
741 },
742 ]);
743 assert_eq!(
744 events.as_slice(),
745 &[
746 ToolEditEvent::OldTextChunk {
747 edit_index: 2,
748 chunk: "".into(),
749 done: true,
750 },
751 ToolEditEvent::NewTextChunk {
752 edit_index: 2,
753 chunk: "C".into(),
754 done: true,
755 },
756 ]
757 );
758 }
759
760 #[test]
761 fn test_finalize_with_unseen_old_text() {
762 let mut parser = ToolEditParser::default();
763
764 // Only saw partial old_text, never saw new_text in partials
765 parser.push_edits(&[PartialEdit {
766 old_text: Some("partial".into()),
767 new_text: None,
768 }]);
769
770 let events = parser.finalize_edits(&[Edit {
771 old_text: "partial old text".into(),
772 new_text: "replacement".into(),
773 }]);
774 assert_eq!(
775 events.as_slice(),
776 &[
777 ToolEditEvent::OldTextChunk {
778 edit_index: 0,
779 chunk: " old text".into(),
780 done: true,
781 },
782 ToolEditEvent::NewTextChunk {
783 edit_index: 0,
784 chunk: "replacement".into(),
785 done: true,
786 },
787 ]
788 );
789 }
790
791 #[test]
792 fn test_finalize_with_partially_seen_new_text() {
793 let mut parser = ToolEditParser::default();
794
795 parser.push_edits(&[PartialEdit {
796 old_text: Some("old".into()),
797 new_text: Some("partial".into()),
798 }]);
799
800 let events = parser.finalize_edits(&[Edit {
801 old_text: "old".into(),
802 new_text: "partial new text".into(),
803 }]);
804 assert_eq!(
805 events.as_slice(),
806 &[ToolEditEvent::NewTextChunk {
807 edit_index: 0,
808 chunk: " new text".into(),
809 done: true,
810 }]
811 );
812 }
813
814 #[test]
815 fn test_repeated_pushes_with_no_change() {
816 let mut parser = ToolEditParser::default();
817
818 let events = parser.push_edits(&[PartialEdit {
819 old_text: Some("stable".into()),
820 new_text: Some("also stable".into()),
821 }]);
822 assert_eq!(events.len(), 2); // old done + new chunk
823
824 // Push the exact same data again
825 let events = parser.push_edits(&[PartialEdit {
826 old_text: Some("stable".into()),
827 new_text: Some("also stable".into()),
828 }]);
829 assert!(events.is_empty());
830
831 // And again
832 let events = parser.push_edits(&[PartialEdit {
833 old_text: Some("stable".into()),
834 new_text: Some("also stable".into()),
835 }]);
836 assert!(events.is_empty());
837 }
838
839 #[test]
840 fn test_old_text_trailing_backslash_held_back() {
841 let mut parser = ToolEditParser::default();
842
843 // Partial-json-fixer produces a literal backslash when the JSON stream
844 // cuts in the middle of an escape sequence like \n. The parser holds
845 // back the trailing backslash instead of emitting it.
846 let events = parser.push_edits(&[PartialEdit {
847 old_text: Some("hello,\\".into()), // fixer closed incomplete \n as \\
848 new_text: None,
849 }]);
850 // The trailing `\` is held back — only "hello," is emitted.
851 assert_eq!(
852 events.as_slice(),
853 &[ToolEditEvent::OldTextChunk {
854 edit_index: 0,
855 chunk: "hello,".into(),
856 done: false,
857 }]
858 );
859
860 // Next partial: the fixer corrects the escape to \n.
861 // The held-back byte was wrong, but we never emitted it. Now the
862 // correct newline at that position is emitted normally.
863 let events = parser.push_edits(&[PartialEdit {
864 old_text: Some("hello,\n".into()),
865 new_text: None,
866 }]);
867 assert_eq!(
868 events.as_slice(),
869 &[ToolEditEvent::OldTextChunk {
870 edit_index: 0,
871 chunk: "\n".into(),
872 done: false,
873 }]
874 );
875
876 // Continue normally.
877 let events = parser.push_edits(&[PartialEdit {
878 old_text: Some("hello,\nworld".into()),
879 new_text: None,
880 }]);
881 assert_eq!(
882 events.as_slice(),
883 &[ToolEditEvent::OldTextChunk {
884 edit_index: 0,
885 chunk: "world".into(),
886 done: false,
887 }]
888 );
889 }
890
891 #[test]
892 fn test_multiline_old_and_new_text() {
893 let mut parser = ToolEditParser::default();
894
895 let events = parser.push_edits(&[PartialEdit {
896 old_text: Some("line1\nline2".into()),
897 new_text: None,
898 }]);
899 assert_eq!(
900 events.as_slice(),
901 &[ToolEditEvent::OldTextChunk {
902 edit_index: 0,
903 chunk: "line1\nline2".into(),
904 done: false,
905 }]
906 );
907
908 let events = parser.push_edits(&[PartialEdit {
909 old_text: Some("line1\nline2\nline3".into()),
910 new_text: Some("LINE1\n".into()),
911 }]);
912 assert_eq!(
913 events.as_slice(),
914 &[
915 ToolEditEvent::OldTextChunk {
916 edit_index: 0,
917 chunk: "\nline3".into(),
918 done: true,
919 },
920 ToolEditEvent::NewTextChunk {
921 edit_index: 0,
922 chunk: "LINE1\n".into(),
923 done: false,
924 },
925 ]
926 );
927
928 let events = parser.push_edits(&[PartialEdit {
929 old_text: Some("line1\nline2\nline3".into()),
930 new_text: Some("LINE1\nLINE2\nLINE3".into()),
931 }]);
932 assert_eq!(
933 events.as_slice(),
934 &[ToolEditEvent::NewTextChunk {
935 edit_index: 0,
936 chunk: "LINE2\nLINE3".into(),
937 done: false,
938 }]
939 );
940 }
941}