tool_edit_parser.rs

  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}