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