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