1use std::borrow::Cow;
2use std::fmt::Display;
3use std::sync::Arc;
4use std::{
5 fmt::{Debug, Write},
6 mem,
7 ops::Range,
8 path::Path,
9};
10
11use anyhow::Context as _;
12use anyhow::Result;
13use anyhow::anyhow;
14use collections::HashMap;
15use gpui::AsyncApp;
16use gpui::Entity;
17use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, TextBufferSnapshot};
18use project::Project;
19
20pub async fn parse_diff<'a>(
21 diff_str: &'a str,
22 get_buffer: impl Fn(&Path) -> Option<(&'a BufferSnapshot, &'a [Range<Anchor>])> + Send,
23) -> Result<(&'a BufferSnapshot, Vec<(Range<Anchor>, Arc<str>)>)> {
24 let mut diff = DiffParser::new(diff_str);
25 let mut edited_buffer = None;
26 let mut edits = Vec::new();
27
28 while let Some(event) = diff.next()? {
29 match event {
30 DiffEvent::Hunk {
31 path: file_path,
32 hunk,
33 } => {
34 let (buffer, ranges) = match edited_buffer {
35 None => {
36 edited_buffer = get_buffer(&Path::new(file_path.as_ref()));
37 edited_buffer
38 .as_ref()
39 .context("Model tried to edit a file that wasn't included")?
40 }
41 Some(ref current) => current,
42 };
43
44 edits.extend(
45 resolve_hunk_edits_in_buffer(hunk, &buffer.text, ranges)
46 .with_context(|| format!("Diff:\n{diff_str}"))?,
47 );
48 }
49 DiffEvent::FileEnd { renamed_to } => {
50 let (buffer, _) = edited_buffer
51 .take()
52 .context("Got a FileEnd event before an Hunk event")?;
53
54 if renamed_to.is_some() {
55 anyhow::bail!("edit predictions cannot rename files");
56 }
57
58 if diff.next()?.is_some() {
59 anyhow::bail!("Edited more than one file");
60 }
61
62 return Ok((buffer, edits));
63 }
64 }
65 }
66
67 Err(anyhow::anyhow!("No EOF"))
68}
69
70#[derive(Debug)]
71pub struct OpenedBuffers<'a>(#[allow(unused)] HashMap<Cow<'a, str>, Entity<Buffer>>);
72
73#[must_use]
74pub async fn apply_diff<'a>(
75 diff_str: &'a str,
76 project: &Entity<Project>,
77 cx: &mut AsyncApp,
78) -> Result<OpenedBuffers<'a>> {
79 let mut included_files = HashMap::default();
80
81 for line in diff_str.lines() {
82 let diff_line = DiffLine::parse(line);
83
84 if let DiffLine::OldPath { path } = diff_line {
85 let buffer = project
86 .update(cx, |project, cx| {
87 let project_path =
88 project
89 .find_project_path(path.as_ref(), cx)
90 .with_context(|| {
91 format!("Failed to find worktree for new path: {}", path)
92 })?;
93 anyhow::Ok(project.open_buffer(project_path, cx))
94 })??
95 .await?;
96
97 included_files.insert(path, buffer);
98 }
99 }
100
101 let ranges = [Anchor::MIN..Anchor::MAX];
102
103 let mut diff = DiffParser::new(diff_str);
104 let mut current_file = None;
105 let mut edits = vec![];
106
107 while let Some(event) = diff.next()? {
108 match event {
109 DiffEvent::Hunk {
110 path: file_path,
111 hunk,
112 } => {
113 let (buffer, ranges) = match current_file {
114 None => {
115 let buffer = included_files
116 .get_mut(&file_path)
117 .expect("Opened all files in diff");
118
119 current_file = Some((buffer, ranges.as_slice()));
120 current_file.as_ref().unwrap()
121 }
122 Some(ref current) => current,
123 };
124
125 buffer.read_with(cx, |buffer, _| {
126 edits.extend(
127 resolve_hunk_edits_in_buffer(hunk, buffer, ranges)
128 .with_context(|| format!("Diff:\n{diff_str}"))?,
129 );
130 anyhow::Ok(())
131 })??;
132 }
133 DiffEvent::FileEnd { renamed_to } => {
134 let (buffer, _) = current_file
135 .take()
136 .context("Got a FileEnd event before an Hunk event")?;
137
138 if let Some(renamed_to) = renamed_to {
139 project
140 .update(cx, |project, cx| {
141 let new_project_path = project
142 .find_project_path(Path::new(renamed_to.as_ref()), cx)
143 .with_context(|| {
144 format!("Failed to find worktree for new path: {}", renamed_to)
145 })?;
146
147 let project_file = project::File::from_dyn(buffer.read(cx).file())
148 .expect("Wrong file type");
149
150 anyhow::Ok(project.rename_entry(
151 project_file.entry_id.unwrap(),
152 new_project_path,
153 cx,
154 ))
155 })??
156 .await?;
157 }
158
159 let edits = mem::take(&mut edits);
160 buffer.update(cx, |buffer, cx| {
161 buffer.edit(edits, None, cx);
162 })?;
163 }
164 }
165 }
166
167 Ok(OpenedBuffers(included_files))
168}
169
170struct PatchFile<'a> {
171 old_path: Cow<'a, str>,
172 new_path: Cow<'a, str>,
173}
174
175struct DiffParser<'a> {
176 current_file: Option<PatchFile<'a>>,
177 current_line: Option<(&'a str, DiffLine<'a>)>,
178 hunk: Hunk,
179 diff: std::str::Lines<'a>,
180}
181
182#[derive(Debug, PartialEq)]
183enum DiffEvent<'a> {
184 Hunk { path: Cow<'a, str>, hunk: Hunk },
185 FileEnd { renamed_to: Option<Cow<'a, str>> },
186}
187
188#[derive(Debug, Default, PartialEq)]
189struct Hunk {
190 context: String,
191 edits: Vec<Edit>,
192}
193
194impl Hunk {
195 fn is_empty(&self) -> bool {
196 self.context.is_empty() && self.edits.is_empty()
197 }
198}
199
200#[derive(Debug, PartialEq)]
201struct Edit {
202 range: Range<usize>,
203 text: String,
204}
205
206impl<'a> DiffParser<'a> {
207 fn new(diff: &'a str) -> Self {
208 let mut diff = diff.lines();
209 let current_line = diff.next().map(|line| (line, DiffLine::parse(line)));
210 DiffParser {
211 current_file: None,
212 hunk: Hunk::default(),
213 current_line,
214 diff,
215 }
216 }
217
218 fn next(&mut self) -> Result<Option<DiffEvent<'a>>> {
219 loop {
220 let (hunk_done, file_done) = match self.current_line.as_ref().map(|e| &e.1) {
221 Some(DiffLine::OldPath { .. }) | Some(DiffLine::Garbage(_)) | None => (true, true),
222 Some(DiffLine::HunkHeader(_)) => (true, false),
223 _ => (false, false),
224 };
225
226 if hunk_done {
227 if let Some(file) = &self.current_file
228 && !self.hunk.is_empty()
229 {
230 return Ok(Some(DiffEvent::Hunk {
231 path: file.old_path.clone(),
232 hunk: mem::take(&mut self.hunk),
233 }));
234 }
235 }
236
237 if file_done {
238 if let Some(PatchFile { old_path, new_path }) = self.current_file.take() {
239 return Ok(Some(DiffEvent::FileEnd {
240 renamed_to: if old_path != new_path {
241 Some(new_path)
242 } else {
243 None
244 },
245 }));
246 }
247 }
248
249 let Some((line, parsed_line)) = self.current_line.take() else {
250 break;
251 };
252
253 util::maybe!({
254 match parsed_line {
255 DiffLine::OldPath { path } => {
256 self.current_file = Some(PatchFile {
257 old_path: path,
258 new_path: "".into(),
259 });
260 }
261 DiffLine::NewPath { path } => {
262 if let Some(current_file) = &mut self.current_file {
263 current_file.new_path = path
264 }
265 }
266 DiffLine::HunkHeader(_) => {}
267 DiffLine::Context(ctx) => {
268 if self.current_file.is_some() {
269 writeln!(&mut self.hunk.context, "{ctx}")?;
270 }
271 }
272 DiffLine::Deletion(del) => {
273 if self.current_file.is_some() {
274 let range = self.hunk.context.len()
275 ..self.hunk.context.len() + del.len() + '\n'.len_utf8();
276 if let Some(last_edit) = self.hunk.edits.last_mut()
277 && last_edit.range.end == range.start
278 {
279 last_edit.range.end = range.end;
280 } else {
281 self.hunk.edits.push(Edit {
282 range,
283 text: String::new(),
284 });
285 }
286 writeln!(&mut self.hunk.context, "{del}")?;
287 }
288 }
289 DiffLine::Addition(add) => {
290 if self.current_file.is_some() {
291 let range = self.hunk.context.len()..self.hunk.context.len();
292 if let Some(last_edit) = self.hunk.edits.last_mut()
293 && last_edit.range.end == range.start
294 {
295 writeln!(&mut last_edit.text, "{add}").unwrap();
296 } else {
297 self.hunk.edits.push(Edit {
298 range,
299 text: format!("{add}\n"),
300 });
301 }
302 }
303 }
304 DiffLine::Garbage(_) => {}
305 }
306
307 anyhow::Ok(())
308 })
309 .with_context(|| format!("on line:\n\n```\n{}```", line))?;
310
311 self.current_line = self.diff.next().map(|line| (line, DiffLine::parse(line)));
312 }
313
314 anyhow::Ok(None)
315 }
316}
317
318fn resolve_hunk_edits_in_buffer(
319 hunk: Hunk,
320 buffer: &TextBufferSnapshot,
321 ranges: &[Range<Anchor>],
322) -> Result<impl Iterator<Item = (Range<Anchor>, Arc<str>)>, anyhow::Error> {
323 let context_offset = if hunk.context.is_empty() {
324 Ok(0)
325 } else {
326 let mut offset = None;
327 for range in ranges {
328 let range = range.to_offset(buffer);
329 let text = buffer.text_for_range(range.clone()).collect::<String>();
330 for (ix, _) in text.match_indices(&hunk.context) {
331 if offset.is_some() {
332 anyhow::bail!("Context is not unique enough:\n{}", hunk.context);
333 }
334 offset = Some(range.start + ix);
335 }
336 }
337 offset.ok_or_else(|| anyhow!("Failed to match context:\n{}", hunk.context))
338 }?;
339 let iter = hunk.edits.into_iter().flat_map(move |edit| {
340 let old_text = buffer
341 .text_for_range(context_offset + edit.range.start..context_offset + edit.range.end)
342 .collect::<String>();
343 let edits_within_hunk = language::text_diff(&old_text, &edit.text);
344 edits_within_hunk
345 .into_iter()
346 .map(move |(inner_range, inner_text)| {
347 (
348 buffer.anchor_after(context_offset + edit.range.start + inner_range.start)
349 ..buffer.anchor_before(context_offset + edit.range.start + inner_range.end),
350 inner_text,
351 )
352 })
353 });
354 Ok(iter)
355}
356
357#[derive(Debug, PartialEq)]
358pub enum DiffLine<'a> {
359 OldPath { path: Cow<'a, str> },
360 NewPath { path: Cow<'a, str> },
361 HunkHeader(Option<HunkLocation>),
362 Context(&'a str),
363 Deletion(&'a str),
364 Addition(&'a str),
365 Garbage(&'a str),
366}
367
368#[derive(Debug, PartialEq)]
369pub struct HunkLocation {
370 start_line_old: u32,
371 count_old: u32,
372 start_line_new: u32,
373 count_new: u32,
374}
375
376impl<'a> DiffLine<'a> {
377 pub fn parse(line: &'a str) -> Self {
378 Self::try_parse(line).unwrap_or(Self::Garbage(line))
379 }
380
381 fn try_parse(line: &'a str) -> Option<Self> {
382 if let Some(header) = line.strip_prefix("---").and_then(eat_required_whitespace) {
383 let path = parse_header_path("a/", header);
384 Some(Self::OldPath { path })
385 } else if let Some(header) = line.strip_prefix("+++").and_then(eat_required_whitespace) {
386 Some(Self::NewPath {
387 path: parse_header_path("b/", header),
388 })
389 } else if let Some(header) = line.strip_prefix("@@").and_then(eat_required_whitespace) {
390 if header.starts_with("...") {
391 return Some(Self::HunkHeader(None));
392 }
393
394 let mut tokens = header.split_whitespace();
395 let old_range = tokens.next()?.strip_prefix('-')?;
396 let new_range = tokens.next()?.strip_prefix('+')?;
397
398 let (start_line_old, count_old) = old_range.split_once(',').unwrap_or((old_range, "1"));
399 let (start_line_new, count_new) = new_range.split_once(',').unwrap_or((new_range, "1"));
400
401 Some(Self::HunkHeader(Some(HunkLocation {
402 start_line_old: start_line_old.parse::<u32>().ok()?.saturating_sub(1),
403 count_old: count_old.parse().ok()?,
404 start_line_new: start_line_new.parse::<u32>().ok()?.saturating_sub(1),
405 count_new: count_new.parse().ok()?,
406 })))
407 } else if let Some(deleted_header) = line.strip_prefix("-") {
408 Some(Self::Deletion(deleted_header))
409 } else if line.is_empty() {
410 Some(Self::Context(""))
411 } else if let Some(context) = line.strip_prefix(" ") {
412 Some(Self::Context(context))
413 } else {
414 Some(Self::Addition(line.strip_prefix("+")?))
415 }
416 }
417}
418
419impl<'a> Display for DiffLine<'a> {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 match self {
422 DiffLine::OldPath { path } => write!(f, "--- {path}"),
423 DiffLine::NewPath { path } => write!(f, "+++ {path}"),
424 DiffLine::HunkHeader(Some(hunk_location)) => {
425 write!(
426 f,
427 "@@ -{},{} +{},{} @@",
428 hunk_location.start_line_old + 1,
429 hunk_location.count_old,
430 hunk_location.start_line_new + 1,
431 hunk_location.count_new
432 )
433 }
434 DiffLine::HunkHeader(None) => write!(f, "@@ ... @@"),
435 DiffLine::Context(content) => write!(f, " {content}"),
436 DiffLine::Deletion(content) => write!(f, "-{content}"),
437 DiffLine::Addition(content) => write!(f, "+{content}"),
438 DiffLine::Garbage(line) => write!(f, "{line}"),
439 }
440 }
441}
442
443fn parse_header_path<'a>(strip_prefix: &'static str, header: &'a str) -> Cow<'a, str> {
444 if !header.contains(['"', '\\']) {
445 let path = header.split_ascii_whitespace().next().unwrap_or(header);
446 return Cow::Borrowed(path.strip_prefix(strip_prefix).unwrap_or(path));
447 }
448
449 let mut path = String::with_capacity(header.len());
450 let mut in_quote = false;
451 let mut chars = header.chars().peekable();
452 let mut strip_prefix = Some(strip_prefix);
453
454 while let Some(char) = chars.next() {
455 if char == '"' {
456 in_quote = !in_quote;
457 } else if char == '\\' {
458 let Some(&next_char) = chars.peek() else {
459 break;
460 };
461 chars.next();
462 path.push(next_char);
463 } else if char.is_ascii_whitespace() && !in_quote {
464 break;
465 } else {
466 path.push(char);
467 }
468
469 if let Some(prefix) = strip_prefix
470 && path == prefix
471 {
472 strip_prefix.take();
473 path.clear();
474 }
475 }
476
477 Cow::Owned(path)
478}
479
480fn eat_required_whitespace(header: &str) -> Option<&str> {
481 let trimmed = header.trim_ascii_start();
482
483 if trimmed.len() == header.len() {
484 None
485 } else {
486 Some(trimmed)
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use gpui::TestAppContext;
494 use indoc::indoc;
495 use language::Point;
496 use pretty_assertions::assert_eq;
497 use project::{FakeFs, Project};
498 use serde_json::json;
499 use settings::SettingsStore;
500 use util::path;
501
502 #[test]
503 fn parse_lines_simple() {
504 let input = indoc! {"
505 diff --git a/text.txt b/text.txt
506 index 86c770d..a1fd855 100644
507 --- a/file.txt
508 +++ b/file.txt
509 @@ -1,2 +1,3 @@
510 context
511 -deleted
512 +inserted
513 garbage
514
515 --- b/file.txt
516 +++ a/file.txt
517 "};
518
519 let lines = input.lines().map(DiffLine::parse).collect::<Vec<_>>();
520
521 pretty_assertions::assert_eq!(
522 lines,
523 &[
524 DiffLine::Garbage("diff --git a/text.txt b/text.txt"),
525 DiffLine::Garbage("index 86c770d..a1fd855 100644"),
526 DiffLine::OldPath {
527 path: "file.txt".into()
528 },
529 DiffLine::NewPath {
530 path: "file.txt".into()
531 },
532 DiffLine::HunkHeader(Some(HunkLocation {
533 start_line_old: 0,
534 count_old: 2,
535 start_line_new: 0,
536 count_new: 3
537 })),
538 DiffLine::Context("context"),
539 DiffLine::Deletion("deleted"),
540 DiffLine::Addition("inserted"),
541 DiffLine::Garbage("garbage"),
542 DiffLine::Context(""),
543 DiffLine::OldPath {
544 path: "b/file.txt".into()
545 },
546 DiffLine::NewPath {
547 path: "a/file.txt".into()
548 },
549 ]
550 );
551 }
552
553 #[test]
554 fn file_header_extra_space() {
555 let options = ["--- file", "--- file", "---\tfile"];
556
557 for option in options {
558 pretty_assertions::assert_eq!(
559 DiffLine::parse(option),
560 DiffLine::OldPath {
561 path: "file".into()
562 },
563 "{option}",
564 );
565 }
566 }
567
568 #[test]
569 fn hunk_header_extra_space() {
570 let options = [
571 "@@ -1,2 +1,3 @@",
572 "@@ -1,2 +1,3 @@",
573 "@@\t-1,2\t+1,3\t@@",
574 "@@ -1,2 +1,3 @@",
575 "@@ -1,2 +1,3 @@",
576 "@@ -1,2 +1,3 @@",
577 "@@ -1,2 +1,3 @@ garbage",
578 ];
579
580 for option in options {
581 pretty_assertions::assert_eq!(
582 DiffLine::parse(option),
583 DiffLine::HunkHeader(Some(HunkLocation {
584 start_line_old: 0,
585 count_old: 2,
586 start_line_new: 0,
587 count_new: 3
588 })),
589 "{option}",
590 );
591 }
592 }
593
594 #[test]
595 fn hunk_header_without_location() {
596 pretty_assertions::assert_eq!(DiffLine::parse("@@ ... @@"), DiffLine::HunkHeader(None));
597 }
598
599 #[test]
600 fn test_parse_path() {
601 assert_eq!(parse_header_path("a/", "foo.txt"), "foo.txt");
602 assert_eq!(
603 parse_header_path("a/", "foo/bar/baz.txt"),
604 "foo/bar/baz.txt"
605 );
606 assert_eq!(parse_header_path("a/", "a/foo.txt"), "foo.txt");
607 assert_eq!(
608 parse_header_path("a/", "a/foo/bar/baz.txt"),
609 "foo/bar/baz.txt"
610 );
611
612 // Extra
613 assert_eq!(
614 parse_header_path("a/", "a/foo/bar/baz.txt 2025"),
615 "foo/bar/baz.txt"
616 );
617 assert_eq!(
618 parse_header_path("a/", "a/foo/bar/baz.txt\t2025"),
619 "foo/bar/baz.txt"
620 );
621 assert_eq!(
622 parse_header_path("a/", "a/foo/bar/baz.txt \""),
623 "foo/bar/baz.txt"
624 );
625
626 // Quoted
627 assert_eq!(
628 parse_header_path("a/", "a/foo/bar/\"baz quox.txt\""),
629 "foo/bar/baz quox.txt"
630 );
631 assert_eq!(
632 parse_header_path("a/", "\"a/foo/bar/baz quox.txt\""),
633 "foo/bar/baz quox.txt"
634 );
635 assert_eq!(
636 parse_header_path("a/", "\"foo/bar/baz quox.txt\""),
637 "foo/bar/baz quox.txt"
638 );
639 assert_eq!(parse_header_path("a/", "\"whatever 🤷\""), "whatever 🤷");
640 assert_eq!(
641 parse_header_path("a/", "\"foo/bar/baz quox.txt\" 2025"),
642 "foo/bar/baz quox.txt"
643 );
644 // unescaped quotes are dropped
645 assert_eq!(parse_header_path("a/", "foo/\"bar\""), "foo/bar");
646
647 // Escaped
648 assert_eq!(
649 parse_header_path("a/", "\"foo/\\\"bar\\\"/baz.txt\""),
650 "foo/\"bar\"/baz.txt"
651 );
652 assert_eq!(
653 parse_header_path("a/", "\"C:\\\\Projects\\\\My App\\\\old file.txt\""),
654 "C:\\Projects\\My App\\old file.txt"
655 );
656 }
657
658 #[test]
659 fn test_parse_diff_with_leading_and_trailing_garbage() {
660 let diff = indoc! {"
661 I need to make some changes.
662
663 I'll change the following things:
664 - one
665 - two
666 - three
667
668 ```
669 --- a/file.txt
670 +++ b/file.txt
671 one
672 +AND
673 two
674 ```
675
676 Summary of what I did:
677 - one
678 - two
679 - three
680
681 That's about it.
682 "};
683
684 let mut events = Vec::new();
685 let mut parser = DiffParser::new(diff);
686 while let Some(event) = parser.next().unwrap() {
687 events.push(event);
688 }
689
690 assert_eq!(
691 events,
692 &[
693 DiffEvent::Hunk {
694 path: "file.txt".into(),
695 hunk: Hunk {
696 context: "one\ntwo\n".into(),
697 edits: vec![Edit {
698 range: 4..4,
699 text: "AND\n".into()
700 }],
701 }
702 },
703 DiffEvent::FileEnd { renamed_to: None }
704 ],
705 )
706 }
707
708 #[gpui::test]
709 async fn test_apply_diff_successful(cx: &mut TestAppContext) {
710 let fs = init_test(cx);
711
712 let buffer_1_text = indoc! {r#"
713 one
714 two
715 three
716 four
717 five
718 "# };
719
720 let buffer_1_text_final = indoc! {r#"
721 3
722 4
723 5
724 "# };
725
726 let buffer_2_text = indoc! {r#"
727 six
728 seven
729 eight
730 nine
731 ten
732 "# };
733
734 let buffer_2_text_final = indoc! {r#"
735 5
736 six
737 seven
738 7.5
739 eight
740 nine
741 ten
742 11
743 "# };
744
745 fs.insert_tree(
746 path!("/root"),
747 json!({
748 "file1": buffer_1_text,
749 "file2": buffer_2_text,
750 }),
751 )
752 .await;
753
754 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
755
756 let diff = indoc! {r#"
757 --- a/root/file1
758 +++ b/root/file1
759 one
760 two
761 -three
762 +3
763 four
764 five
765 --- a/root/file1
766 +++ b/root/file1
767 3
768 -four
769 -five
770 +4
771 +5
772 --- a/root/file1
773 +++ b/root/file1
774 -one
775 -two
776 3
777 4
778 --- a/root/file2
779 +++ b/root/file2
780 +5
781 six
782 --- a/root/file2
783 +++ b/root/file2
784 seven
785 +7.5
786 eight
787 --- a/root/file2
788 +++ b/root/file2
789 ten
790 +11
791 "#};
792
793 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
794 .await
795 .unwrap();
796 let buffer_1 = project
797 .update(cx, |project, cx| {
798 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
799 project.open_buffer(project_path, cx)
800 })
801 .await
802 .unwrap();
803
804 buffer_1.read_with(cx, |buffer, _cx| {
805 assert_eq!(buffer.text(), buffer_1_text_final);
806 });
807 let buffer_2 = project
808 .update(cx, |project, cx| {
809 let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
810 project.open_buffer(project_path, cx)
811 })
812 .await
813 .unwrap();
814
815 buffer_2.read_with(cx, |buffer, _cx| {
816 assert_eq!(buffer.text(), buffer_2_text_final);
817 });
818 }
819
820 #[gpui::test]
821 async fn test_apply_diff_non_unique(cx: &mut TestAppContext) {
822 let fs = init_test(cx);
823
824 let buffer_1_text = indoc! {r#"
825 one
826 two
827 three
828 four
829 five
830 one
831 two
832 three
833 four
834 five
835 "# };
836
837 fs.insert_tree(
838 path!("/root"),
839 json!({
840 "file1": buffer_1_text,
841 }),
842 )
843 .await;
844
845 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
846 let buffer = project
847 .update(cx, |project, cx| {
848 project.open_local_buffer(path!("/root/file1"), cx)
849 })
850 .await
851 .unwrap();
852 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
853
854 let diff = indoc! {r#"
855 --- a/root/file1
856 +++ b/root/file1
857 one
858 two
859 -three
860 +3
861 four
862 five
863 "#};
864
865 let final_text = indoc! {r#"
866 one
867 two
868 three
869 four
870 five
871 one
872 two
873 3
874 four
875 five
876 "#};
877
878 apply_diff(diff, &project, &mut cx.to_async())
879 .await
880 .expect_err("Non-unique edits should fail");
881
882 let ranges = [buffer_snapshot.anchor_before(Point::new(1, 0))
883 ..buffer_snapshot.anchor_after(buffer_snapshot.max_point())];
884
885 let (edited_snapshot, edits) = parse_diff(diff, |_path| Some((&buffer_snapshot, &ranges)))
886 .await
887 .unwrap();
888
889 assert_eq!(edited_snapshot.remote_id(), buffer_snapshot.remote_id());
890 buffer.update(cx, |buffer, cx| {
891 buffer.edit(edits, None, cx);
892 assert_eq!(buffer.text(), final_text);
893 });
894 }
895
896 #[gpui::test]
897 async fn test_parse_diff_with_edits_within_line(cx: &mut TestAppContext) {
898 let fs = init_test(cx);
899
900 let buffer_1_text = indoc! {r#"
901 one two three four
902 five six seven eight
903 nine ten eleven twelve
904 "# };
905
906 fs.insert_tree(
907 path!("/root"),
908 json!({
909 "file1": buffer_1_text,
910 }),
911 )
912 .await;
913
914 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
915 let buffer = project
916 .update(cx, |project, cx| {
917 project.open_local_buffer(path!("/root/file1"), cx)
918 })
919 .await
920 .unwrap();
921 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
922
923 let diff = indoc! {r#"
924 --- a/root/file1
925 +++ b/root/file1
926 one two three four
927 -five six seven eight
928 +five SIX seven eight!
929 nine ten eleven twelve
930 "#};
931
932 let (buffer, edits) = parse_diff(diff, |_path| {
933 Some((&buffer_snapshot, &[(Anchor::MIN..Anchor::MAX)] as &[_]))
934 })
935 .await
936 .unwrap();
937
938 let edits = edits
939 .into_iter()
940 .map(|(range, text)| (range.to_point(&buffer), text))
941 .collect::<Vec<_>>();
942 assert_eq!(
943 edits,
944 &[
945 (Point::new(1, 5)..Point::new(1, 8), "SIX".into()),
946 (Point::new(1, 20)..Point::new(1, 20), "!".into())
947 ]
948 );
949 }
950
951 #[gpui::test]
952 async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
953 let fs = init_test(cx);
954
955 let start = indoc! {r#"
956 one
957 two
958 three
959 four
960 five
961
962 four
963 five
964 "# };
965
966 let end = indoc! {r#"
967 one
968 two
969 3
970 four
971 5
972
973 four
974 five
975 "# };
976
977 fs.insert_tree(
978 path!("/root"),
979 json!({
980 "file1": start,
981 }),
982 )
983 .await;
984
985 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
986
987 let diff = indoc! {r#"
988 --- a/root/file1
989 +++ b/root/file1
990 one
991 two
992 -three
993 +3
994 four
995 -five
996 +5
997 "#};
998
999 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
1000 .await
1001 .unwrap();
1002
1003 let buffer_1 = project
1004 .update(cx, |project, cx| {
1005 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
1006 project.open_buffer(project_path, cx)
1007 })
1008 .await
1009 .unwrap();
1010
1011 buffer_1.read_with(cx, |buffer, _cx| {
1012 assert_eq!(buffer.text(), end);
1013 });
1014 }
1015
1016 fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
1017 cx.update(|cx| {
1018 let settings_store = SettingsStore::test(cx);
1019 cx.set_global(settings_store);
1020 });
1021
1022 FakeFs::new(cx.background_executor.clone())
1023 }
1024}