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