1use std::{
2 mem::take,
3 ops::Range,
4 path::{Path, PathBuf},
5};
6use util::ResultExt;
7
8/// Represents an edit action to be performed on a file.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum EditAction {
11 /// Replace specific content in a file with new content
12 Replace {
13 file_path: PathBuf,
14 old: String,
15 new: String,
16 },
17 /// Write content to a file (create or overwrite)
18 Write { file_path: PathBuf, content: String },
19}
20
21impl EditAction {
22 pub fn file_path(&self) -> &Path {
23 match self {
24 EditAction::Replace { file_path, .. } => file_path,
25 EditAction::Write { file_path, .. } => file_path,
26 }
27 }
28}
29
30/// Parses edit actions from an LLM response.
31/// See system.md for more details on the format.
32#[derive(Debug)]
33pub struct EditActionParser {
34 state: State,
35 line: usize,
36 column: usize,
37 marker_ix: usize,
38 action_source: Vec<u8>,
39 fence_start_offset: usize,
40 block_range: Range<usize>,
41 old_range: Range<usize>,
42 new_range: Range<usize>,
43 errors: Vec<ParseError>,
44}
45
46#[derive(Debug, PartialEq, Eq)]
47enum State {
48 /// Anywhere outside an action
49 Default,
50 /// After opening ```, in optional language tag
51 OpenFence,
52 /// In SEARCH marker
53 SearchMarker,
54 /// In search block or divider
55 SearchBlock,
56 /// In replace block or REPLACE marker
57 ReplaceBlock,
58 /// In closing ```
59 CloseFence,
60}
61
62impl EditActionParser {
63 /// Creates a new `EditActionParser`
64 pub fn new() -> Self {
65 Self {
66 state: State::Default,
67 line: 1,
68 column: 0,
69 action_source: Vec::new(),
70 fence_start_offset: 0,
71 marker_ix: 0,
72 block_range: Range::default(),
73 old_range: Range::default(),
74 new_range: Range::default(),
75 errors: Vec::new(),
76 }
77 }
78
79 /// Processes a chunk of input text and returns any completed edit actions.
80 ///
81 /// This method can be called repeatedly with fragments of input. The parser
82 /// maintains its state between calls, allowing you to process streaming input
83 /// as it becomes available. Actions are only inserted once they are fully parsed.
84 ///
85 /// If a block fails to parse, it will simply be skipped and an error will be recorded.
86 /// All errors can be accessed through the `EditActionsParser::errors` method.
87 pub fn parse_chunk(&mut self, input: &str) -> Vec<(EditAction, String)> {
88 use State::*;
89
90 const FENCE: &[u8] = b"```";
91 const SEARCH_MARKER: &[u8] = b"<<<<<<< SEARCH";
92 const DIVIDER: &[u8] = b"=======";
93 const NL_DIVIDER: &[u8] = b"\n=======";
94 const REPLACE_MARKER: &[u8] = b">>>>>>> REPLACE";
95 const NL_REPLACE_MARKER: &[u8] = b"\n>>>>>>> REPLACE";
96
97 let mut actions = Vec::new();
98
99 for byte in input.bytes() {
100 // Update line and column tracking
101 if byte == b'\n' {
102 self.line += 1;
103 self.column = 0;
104 } else {
105 self.column += 1;
106 }
107
108 let action_offset = self.action_source.len();
109
110 match &self.state {
111 Default => match self.match_marker(byte, FENCE, false) {
112 MarkerMatch::Complete => {
113 self.fence_start_offset = action_offset + 1 - FENCE.len();
114 self.to_state(OpenFence);
115 }
116 MarkerMatch::Partial => {}
117 MarkerMatch::None => {
118 if self.marker_ix > 0 {
119 self.marker_ix = 0;
120 } else if self.action_source.ends_with(b"\n") {
121 self.action_source.clear();
122 }
123 }
124 },
125 OpenFence => {
126 // skip language tag
127 if byte == b'\n' {
128 self.to_state(SearchMarker);
129 }
130 }
131 SearchMarker => {
132 if self.expect_marker(byte, SEARCH_MARKER, true) {
133 self.to_state(SearchBlock);
134 }
135 }
136 SearchBlock => {
137 if self.extend_block_range(byte, DIVIDER, NL_DIVIDER) {
138 self.old_range = take(&mut self.block_range);
139 self.to_state(ReplaceBlock);
140 }
141 }
142 ReplaceBlock => {
143 if self.extend_block_range(byte, REPLACE_MARKER, NL_REPLACE_MARKER) {
144 self.new_range = take(&mut self.block_range);
145 self.to_state(CloseFence);
146 }
147 }
148 CloseFence => {
149 if self.expect_marker(byte, FENCE, false) {
150 self.action_source.push(byte);
151
152 if let Some(action) = self.action() {
153 actions.push(action);
154 }
155
156 self.errors();
157 self.reset();
158
159 continue;
160 }
161 }
162 };
163
164 self.action_source.push(byte);
165 }
166
167 actions
168 }
169
170 /// Returns a reference to the errors encountered during parsing.
171 pub fn errors(&self) -> &[ParseError] {
172 &self.errors
173 }
174
175 fn action(&mut self) -> Option<(EditAction, String)> {
176 let old_range = take(&mut self.old_range);
177 let new_range = take(&mut self.new_range);
178
179 let action_source = take(&mut self.action_source);
180 let action_source = String::from_utf8(action_source).log_err()?;
181
182 let mut file_path_bytes = action_source[..self.fence_start_offset].to_owned();
183
184 if file_path_bytes.ends_with("\n") {
185 file_path_bytes.pop();
186 if file_path_bytes.ends_with("\r") {
187 file_path_bytes.pop();
188 }
189 }
190
191 let file_path = PathBuf::from(file_path_bytes);
192
193 if old_range.is_empty() && new_range.is_empty() {
194 self.push_error(ParseErrorKind::NoOp);
195 return None;
196 }
197
198 if old_range.is_empty() {
199 return Some((
200 EditAction::Write {
201 file_path,
202 content: action_source[new_range].to_owned(),
203 },
204 action_source,
205 ));
206 }
207
208 let old = action_source[old_range].to_owned();
209 let new = action_source[new_range].to_owned();
210
211 let action = EditAction::Replace {
212 file_path,
213 old,
214 new,
215 };
216
217 Some((action, action_source))
218 }
219
220 fn to_state(&mut self, state: State) {
221 self.state = state;
222 self.marker_ix = 0;
223 }
224
225 fn reset(&mut self) {
226 self.action_source.clear();
227 self.block_range = Range::default();
228 self.old_range = Range::default();
229 self.new_range = Range::default();
230 self.fence_start_offset = 0;
231 self.marker_ix = 0;
232 self.to_state(State::Default);
233 }
234
235 fn push_error(&mut self, kind: ParseErrorKind) {
236 self.errors.push(ParseError {
237 line: self.line,
238 column: self.column,
239 kind,
240 });
241 }
242
243 fn expect_marker(&mut self, byte: u8, marker: &'static [u8], trailing_newline: bool) -> bool {
244 match self.match_marker(byte, marker, trailing_newline) {
245 MarkerMatch::Complete => true,
246 MarkerMatch::Partial => false,
247 MarkerMatch::None => {
248 self.push_error(ParseErrorKind::ExpectedMarker {
249 expected: marker,
250 found: byte,
251 });
252 self.reset();
253 false
254 }
255 }
256 }
257
258 fn extend_block_range(&mut self, byte: u8, marker: &[u8], nl_marker: &[u8]) -> bool {
259 let marker = if self.block_range.is_empty() {
260 // do not require another newline if block is empty
261 marker
262 } else {
263 nl_marker
264 };
265
266 let offset = self.action_source.len();
267
268 match self.match_marker(byte, marker, true) {
269 MarkerMatch::Complete => {
270 if self.action_source[self.block_range.clone()].ends_with(b"\r") {
271 self.block_range.end -= 1;
272 }
273
274 true
275 }
276 MarkerMatch::Partial => false,
277 MarkerMatch::None => {
278 if self.marker_ix > 0 {
279 self.marker_ix = 0;
280 self.block_range.end = offset;
281
282 // The beginning of marker might match current byte
283 match self.match_marker(byte, marker, true) {
284 MarkerMatch::Complete => return true,
285 MarkerMatch::Partial => return false,
286 MarkerMatch::None => { /* no match, keep collecting */ }
287 }
288 }
289
290 if self.block_range.is_empty() {
291 self.block_range.start = offset;
292 }
293 self.block_range.end = offset + 1;
294
295 false
296 }
297 }
298 }
299
300 fn match_marker(&mut self, byte: u8, marker: &[u8], trailing_newline: bool) -> MarkerMatch {
301 if trailing_newline && self.marker_ix >= marker.len() {
302 if byte == b'\n' {
303 MarkerMatch::Complete
304 } else if byte == b'\r' {
305 MarkerMatch::Partial
306 } else {
307 MarkerMatch::None
308 }
309 } else if byte == marker[self.marker_ix] {
310 self.marker_ix += 1;
311
312 if self.marker_ix < marker.len() || trailing_newline {
313 MarkerMatch::Partial
314 } else {
315 MarkerMatch::Complete
316 }
317 } else {
318 MarkerMatch::None
319 }
320 }
321}
322
323#[derive(Debug)]
324enum MarkerMatch {
325 None,
326 Partial,
327 Complete,
328}
329
330#[derive(Debug, PartialEq, Eq)]
331pub struct ParseError {
332 line: usize,
333 column: usize,
334 kind: ParseErrorKind,
335}
336
337#[derive(Debug, PartialEq, Eq)]
338pub enum ParseErrorKind {
339 ExpectedMarker { expected: &'static [u8], found: u8 },
340 NoOp,
341}
342
343impl std::fmt::Display for ParseErrorKind {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 match self {
346 ParseErrorKind::ExpectedMarker { expected, found } => {
347 write!(
348 f,
349 "Expected marker {:?}, found {:?}",
350 String::from_utf8_lossy(expected),
351 *found as char
352 )
353 }
354 ParseErrorKind::NoOp => {
355 write!(f, "No search or replace")
356 }
357 }
358 }
359}
360
361impl std::fmt::Display for ParseError {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "input:{}:{}: {}", self.line, self.column, self.kind)
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use rand::prelude::*;
371 use util::line_endings;
372
373 #[test]
374 fn test_simple_edit_action() {
375 let input = r#"src/main.rs
376```
377<<<<<<< SEARCH
378fn original() {}
379=======
380fn replacement() {}
381>>>>>>> REPLACE
382```
383"#;
384
385 let mut parser = EditActionParser::new();
386 let actions = parser.parse_chunk(input);
387
388 assert_no_errors(&parser);
389 assert_eq!(actions.len(), 1);
390 assert_eq!(
391 actions[0].0,
392 EditAction::Replace {
393 file_path: PathBuf::from("src/main.rs"),
394 old: "fn original() {}".to_string(),
395 new: "fn replacement() {}".to_string(),
396 }
397 );
398 }
399
400 #[test]
401 fn test_with_language_tag() {
402 let input = r#"src/main.rs
403```rust
404<<<<<<< SEARCH
405fn original() {}
406=======
407fn replacement() {}
408>>>>>>> REPLACE
409```
410"#;
411
412 let mut parser = EditActionParser::new();
413 let actions = parser.parse_chunk(input);
414
415 assert_no_errors(&parser);
416 assert_eq!(actions.len(), 1);
417 assert_eq!(
418 actions[0].0,
419 EditAction::Replace {
420 file_path: PathBuf::from("src/main.rs"),
421 old: "fn original() {}".to_string(),
422 new: "fn replacement() {}".to_string(),
423 }
424 );
425 }
426
427 #[test]
428 fn test_with_surrounding_text() {
429 let input = r#"Here's a modification I'd like to make to the file:
430
431src/main.rs
432```rust
433<<<<<<< SEARCH
434fn original() {}
435=======
436fn replacement() {}
437>>>>>>> REPLACE
438```
439
440This change makes the function better.
441"#;
442
443 let mut parser = EditActionParser::new();
444 let actions = parser.parse_chunk(input);
445
446 assert_no_errors(&parser);
447 assert_eq!(actions.len(), 1);
448 assert_eq!(
449 actions[0].0,
450 EditAction::Replace {
451 file_path: PathBuf::from("src/main.rs"),
452 old: "fn original() {}".to_string(),
453 new: "fn replacement() {}".to_string(),
454 }
455 );
456 }
457
458 #[test]
459 fn test_multiple_edit_actions() {
460 let input = r#"First change:
461src/main.rs
462```
463<<<<<<< SEARCH
464fn original() {}
465=======
466fn replacement() {}
467>>>>>>> REPLACE
468```
469
470Second change:
471src/utils.rs
472```rust
473<<<<<<< SEARCH
474fn old_util() -> bool { false }
475=======
476fn new_util() -> bool { true }
477>>>>>>> REPLACE
478```
479"#;
480
481 let mut parser = EditActionParser::new();
482 let actions = parser.parse_chunk(input);
483
484 assert_no_errors(&parser);
485 assert_eq!(actions.len(), 2);
486
487 let (action, _) = &actions[0];
488 assert_eq!(
489 action,
490 &EditAction::Replace {
491 file_path: PathBuf::from("src/main.rs"),
492 old: "fn original() {}".to_string(),
493 new: "fn replacement() {}".to_string(),
494 }
495 );
496 let (action2, _) = &actions[1];
497 assert_eq!(
498 action2,
499 &EditAction::Replace {
500 file_path: PathBuf::from("src/utils.rs"),
501 old: "fn old_util() -> bool { false }".to_string(),
502 new: "fn new_util() -> bool { true }".to_string(),
503 }
504 );
505 }
506
507 #[test]
508 fn test_multiline() {
509 let input = r#"src/main.rs
510```rust
511<<<<<<< SEARCH
512fn original() {
513 println!("This is the original function");
514 let x = 42;
515 if x > 0 {
516 println!("Positive number");
517 }
518}
519=======
520fn replacement() {
521 println!("This is the replacement function");
522 let x = 100;
523 if x > 50 {
524 println!("Large number");
525 } else {
526 println!("Small number");
527 }
528}
529>>>>>>> REPLACE
530```
531"#;
532
533 let mut parser = EditActionParser::new();
534 let actions = parser.parse_chunk(input);
535
536 assert_no_errors(&parser);
537 assert_eq!(actions.len(), 1);
538
539 let (action, _) = &actions[0];
540 assert_eq!(
541 action,
542 &EditAction::Replace {
543 file_path: PathBuf::from("src/main.rs"),
544 old: "fn original() {\n println!(\"This is the original function\");\n let x = 42;\n if x > 0 {\n println!(\"Positive number\");\n }\n}".to_string(),
545 new: "fn replacement() {\n println!(\"This is the replacement function\");\n let x = 100;\n if x > 50 {\n println!(\"Large number\");\n } else {\n println!(\"Small number\");\n }\n}".to_string(),
546 }
547 );
548 }
549
550 #[test]
551 fn test_write_action() {
552 let input = r#"Create a new main.rs file:
553
554src/main.rs
555```rust
556<<<<<<< SEARCH
557=======
558fn new_function() {
559 println!("This function is being added");
560}
561>>>>>>> REPLACE
562```
563"#;
564
565 let mut parser = EditActionParser::new();
566 let actions = parser.parse_chunk(input);
567
568 assert_no_errors(&parser);
569 assert_eq!(actions.len(), 1);
570 assert_eq!(
571 actions[0].0,
572 EditAction::Write {
573 file_path: PathBuf::from("src/main.rs"),
574 content: "fn new_function() {\n println!(\"This function is being added\");\n}"
575 .to_string(),
576 }
577 );
578 }
579
580 #[test]
581 fn test_empty_replace() {
582 let input = r#"src/main.rs
583```rust
584<<<<<<< SEARCH
585fn this_will_be_deleted() {
586 println!("Deleting this function");
587}
588=======
589>>>>>>> REPLACE
590```
591"#;
592
593 let mut parser = EditActionParser::new();
594 let actions = parser.parse_chunk(&input);
595
596 assert_no_errors(&parser);
597 assert_eq!(actions.len(), 1);
598 assert_eq!(
599 actions[0].0,
600 EditAction::Replace {
601 file_path: PathBuf::from("src/main.rs"),
602 old: "fn this_will_be_deleted() {\n println!(\"Deleting this function\");\n}"
603 .to_string(),
604 new: "".to_string(),
605 }
606 );
607
608 let mut parser = EditActionParser::new();
609 let actions = parser.parse_chunk(&input.replace("\n", "\r\n"));
610 assert_no_errors(&parser);
611 assert_eq!(actions.len(), 1);
612 assert_eq!(
613 actions[0].0,
614 EditAction::Replace {
615 file_path: PathBuf::from("src/main.rs"),
616 old:
617 "fn this_will_be_deleted() {\r\n println!(\"Deleting this function\");\r\n}"
618 .to_string(),
619 new: "".to_string(),
620 }
621 );
622 }
623
624 #[test]
625 fn test_empty_both() {
626 let input = r#"src/main.rs
627```rust
628<<<<<<< SEARCH
629=======
630>>>>>>> REPLACE
631```
632"#;
633
634 let mut parser = EditActionParser::new();
635 let actions = parser.parse_chunk(input);
636
637 // Should not create an action when both sections are empty
638 assert_eq!(actions.len(), 0);
639
640 // Check that the NoOp error was added
641 assert_eq!(parser.errors().len(), 1);
642 match parser.errors()[0].kind {
643 ParseErrorKind::NoOp => {}
644 _ => panic!("Expected NoOp error"),
645 }
646 }
647
648 #[test]
649 fn test_resumability() {
650 let input_part1 = r#"src/main.rs
651```rust
652<<<<<<< SEARCH
653fn ori"#;
654
655 let input_part2 = r#"ginal() {}
656=======
657fn replacement() {}"#;
658
659 let input_part3 = r#"
660>>>>>>> REPLACE
661```
662"#;
663
664 let mut parser = EditActionParser::new();
665 let actions1 = parser.parse_chunk(input_part1);
666 assert_no_errors(&parser);
667 assert_eq!(actions1.len(), 0);
668
669 let actions2 = parser.parse_chunk(input_part2);
670 // No actions should be complete yet
671 assert_no_errors(&parser);
672 assert_eq!(actions2.len(), 0);
673
674 let actions3 = parser.parse_chunk(input_part3);
675 // The third chunk should complete the action
676 assert_no_errors(&parser);
677 assert_eq!(actions3.len(), 1);
678 let (action, _) = &actions3[0];
679 assert_eq!(
680 action,
681 &EditAction::Replace {
682 file_path: PathBuf::from("src/main.rs"),
683 old: "fn original() {}".to_string(),
684 new: "fn replacement() {}".to_string(),
685 }
686 );
687 }
688
689 #[test]
690 fn test_parser_state_preservation() {
691 let mut parser = EditActionParser::new();
692 let actions1 = parser.parse_chunk("src/main.rs\n```rust\n<<<<<<< SEARCH\n");
693
694 // Check parser is in the correct state
695 assert_no_errors(&parser);
696 assert_eq!(parser.state, State::SearchBlock);
697 assert_eq!(
698 parser.action_source,
699 b"src/main.rs\n```rust\n<<<<<<< SEARCH\n"
700 );
701
702 // Continue parsing
703 let actions2 = parser.parse_chunk("original code\n=======\n");
704
705 assert_no_errors(&parser);
706 assert_eq!(parser.state, State::ReplaceBlock);
707 assert_eq!(
708 &parser.action_source[parser.old_range.clone()],
709 b"original code"
710 );
711
712 let actions3 = parser.parse_chunk("replacement code\n>>>>>>> REPLACE\n```\n");
713
714 // After complete parsing, state should reset
715 assert_no_errors(&parser);
716 assert_eq!(parser.state, State::Default);
717 assert_eq!(parser.action_source, b"\n");
718 assert!(parser.old_range.is_empty());
719 assert!(parser.new_range.is_empty());
720
721 assert_eq!(actions1.len(), 0);
722 assert_eq!(actions2.len(), 0);
723 assert_eq!(actions3.len(), 1);
724 }
725
726 #[test]
727 fn test_invalid_search_marker() {
728 let input = r#"src/main.rs
729```rust
730<<<<<<< WRONG_MARKER
731fn original() {}
732=======
733fn replacement() {}
734>>>>>>> REPLACE
735```
736"#;
737
738 let mut parser = EditActionParser::new();
739 let actions = parser.parse_chunk(input);
740 assert_eq!(actions.len(), 0);
741
742 assert_eq!(parser.errors().len(), 1);
743 let error = &parser.errors()[0];
744
745 assert_eq!(
746 error.to_string(),
747 "input:3:9: Expected marker \"<<<<<<< SEARCH\", found 'W'"
748 );
749 }
750
751 #[test]
752 fn test_missing_closing_fence() {
753 let input = r#"src/main.rs
754```rust
755<<<<<<< SEARCH
756fn original() {}
757=======
758fn replacement() {}
759>>>>>>> REPLACE
760<!-- Missing closing fence -->
761
762src/utils.rs
763```rust
764<<<<<<< SEARCH
765fn utils_func() {}
766=======
767fn new_utils_func() {}
768>>>>>>> REPLACE
769```
770"#;
771
772 let mut parser = EditActionParser::new();
773 let actions = parser.parse_chunk(input);
774
775 // Only the second block should be parsed
776 assert_eq!(actions.len(), 1);
777 let (action, _) = &actions[0];
778 assert_eq!(
779 action,
780 &EditAction::Replace {
781 file_path: PathBuf::from("src/utils.rs"),
782 old: "fn utils_func() {}".to_string(),
783 new: "fn new_utils_func() {}".to_string(),
784 }
785 );
786 assert_eq!(parser.errors().len(), 1);
787 assert_eq!(
788 parser.errors()[0].to_string(),
789 "input:8:1: Expected marker \"```\", found '<'".to_string()
790 );
791
792 // The parser should continue after an error
793 assert_eq!(parser.state, State::Default);
794 }
795
796 const SYSTEM_PROMPT: &str = include_str!("./edit_prompt.md");
797
798 #[test]
799 fn test_parse_examples_in_system_prompt() {
800 let mut parser = EditActionParser::new();
801 let actions = parser.parse_chunk(SYSTEM_PROMPT);
802 assert_examples_in_system_prompt(&actions, parser.errors());
803 }
804
805 #[gpui::test(iterations = 10)]
806 fn test_random_chunking_of_system_prompt(mut rng: StdRng) {
807 let mut parser = EditActionParser::new();
808 let mut remaining = SYSTEM_PROMPT;
809 let mut actions = Vec::with_capacity(5);
810
811 while !remaining.is_empty() {
812 let chunk_size = rng.gen_range(1..=std::cmp::min(remaining.len(), 100));
813
814 let (chunk, rest) = remaining.split_at(chunk_size);
815
816 let chunk_actions = parser.parse_chunk(chunk);
817 actions.extend(chunk_actions);
818 remaining = rest;
819 }
820
821 assert_examples_in_system_prompt(&actions, parser.errors());
822 }
823
824 fn assert_examples_in_system_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
825 assert_eq!(actions.len(), 5);
826
827 assert_eq!(
828 actions[0].0,
829 EditAction::Replace {
830 file_path: PathBuf::from("mathweb/flask/app.py"),
831 old: "from flask import Flask".to_string(),
832 new: line_endings!("import math\nfrom flask import Flask").to_string(),
833 },
834 );
835
836 assert_eq!(
837 actions[1].0,
838 EditAction::Replace {
839 file_path: PathBuf::from("mathweb/flask/app.py"),
840 old: line_endings!("def factorial(n):\n \"compute factorial\"\n\n if n == 0:\n return 1\n else:\n return n * factorial(n-1)\n").to_string(),
841 new: "".to_string(),
842 }
843 );
844
845 assert_eq!(
846 actions[2].0,
847 EditAction::Replace {
848 file_path: PathBuf::from("mathweb/flask/app.py"),
849 old: " return str(factorial(n))".to_string(),
850 new: " return str(math.factorial(n))".to_string(),
851 },
852 );
853
854 assert_eq!(
855 actions[3].0,
856 EditAction::Write {
857 file_path: PathBuf::from("hello.py"),
858 content: line_endings!(
859 "def hello():\n \"print a greeting\"\n\n print(\"hello\")"
860 )
861 .to_string(),
862 },
863 );
864
865 assert_eq!(
866 actions[4].0,
867 EditAction::Replace {
868 file_path: PathBuf::from("main.py"),
869 old: line_endings!(
870 "def hello():\n \"print a greeting\"\n\n print(\"hello\")"
871 )
872 .to_string(),
873 new: "from hello import hello".to_string(),
874 },
875 );
876
877 // The system prompt includes some text that would produce errors
878 assert_eq!(
879 errors[0].to_string(),
880 "input:102:1: Expected marker \"<<<<<<< SEARCH\", found '3'"
881 );
882 #[cfg(not(windows))]
883 assert_eq!(
884 errors[1].to_string(),
885 "input:109:0: Expected marker \"<<<<<<< SEARCH\", found '\\n'"
886 );
887 #[cfg(windows)]
888 assert_eq!(
889 errors[1].to_string(),
890 "input:108:1: Expected marker \"<<<<<<< SEARCH\", found '\\r'"
891 );
892 }
893
894 #[test]
895 fn test_print_error() {
896 let input = r#"src/main.rs
897```rust
898<<<<<<< WRONG_MARKER
899fn original() {}
900=======
901fn replacement() {}
902>>>>>>> REPLACE
903```
904"#;
905
906 let mut parser = EditActionParser::new();
907 parser.parse_chunk(input);
908
909 assert_eq!(parser.errors().len(), 1);
910 let error = &parser.errors()[0];
911 let expected_error = r#"input:3:9: Expected marker "<<<<<<< SEARCH", found 'W'"#;
912
913 assert_eq!(format!("{}", error), expected_error);
914 }
915
916 // helpers
917
918 fn assert_no_errors(parser: &EditActionParser) {
919 let errors = parser.errors();
920
921 assert!(
922 errors.is_empty(),
923 "Expected no errors, but found:\n\n{}",
924 errors
925 .iter()
926 .map(|e| e.to_string())
927 .collect::<Vec<String>>()
928 .join("\n")
929 );
930 }
931}