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 .expect("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 .expect("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 (start_line_old, header) = header.strip_prefix('-')?.split_once(',')?;
395 let mut parts = header.split_ascii_whitespace();
396 let count_old = parts.next()?;
397 let (start_line_new, count_new) = parts.next()?.strip_prefix('+')?.split_once(',')?;
398
399 Some(Self::HunkHeader(Some(HunkLocation {
400 start_line_old: start_line_old.parse::<u32>().ok()?.saturating_sub(1),
401 count_old: count_old.parse().ok()?,
402 start_line_new: start_line_new.parse::<u32>().ok()?.saturating_sub(1),
403 count_new: count_new.parse().ok()?,
404 })))
405 } else if let Some(deleted_header) = line.strip_prefix("-") {
406 Some(Self::Deletion(deleted_header))
407 } else if line.is_empty() {
408 Some(Self::Context(""))
409 } else if let Some(context) = line.strip_prefix(" ") {
410 Some(Self::Context(context))
411 } else {
412 Some(Self::Addition(line.strip_prefix("+")?))
413 }
414 }
415}
416
417impl<'a> Display for DiffLine<'a> {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 match self {
420 DiffLine::OldPath { path } => write!(f, "--- {path}"),
421 DiffLine::NewPath { path } => write!(f, "+++ {path}"),
422 DiffLine::HunkHeader(Some(hunk_location)) => {
423 write!(
424 f,
425 "@@ -{},{} +{},{} @@",
426 hunk_location.start_line_old + 1,
427 hunk_location.count_old,
428 hunk_location.start_line_new + 1,
429 hunk_location.count_new
430 )
431 }
432 DiffLine::HunkHeader(None) => write!(f, "@@ ... @@"),
433 DiffLine::Context(content) => write!(f, " {content}"),
434 DiffLine::Deletion(content) => write!(f, "-{content}"),
435 DiffLine::Addition(content) => write!(f, "+{content}"),
436 DiffLine::Garbage(line) => write!(f, "{line}"),
437 }
438 }
439}
440
441fn parse_header_path<'a>(strip_prefix: &'static str, header: &'a str) -> Cow<'a, str> {
442 if !header.contains(['"', '\\']) {
443 let path = header.split_ascii_whitespace().next().unwrap_or(header);
444 return Cow::Borrowed(path.strip_prefix(strip_prefix).unwrap_or(path));
445 }
446
447 let mut path = String::with_capacity(header.len());
448 let mut in_quote = false;
449 let mut chars = header.chars().peekable();
450 let mut strip_prefix = Some(strip_prefix);
451
452 while let Some(char) = chars.next() {
453 if char == '"' {
454 in_quote = !in_quote;
455 } else if char == '\\' {
456 let Some(&next_char) = chars.peek() else {
457 break;
458 };
459 chars.next();
460 path.push(next_char);
461 } else if char.is_ascii_whitespace() && !in_quote {
462 break;
463 } else {
464 path.push(char);
465 }
466
467 if let Some(prefix) = strip_prefix
468 && path == prefix
469 {
470 strip_prefix.take();
471 path.clear();
472 }
473 }
474
475 Cow::Owned(path)
476}
477
478fn eat_required_whitespace(header: &str) -> Option<&str> {
479 let trimmed = header.trim_ascii_start();
480
481 if trimmed.len() == header.len() {
482 None
483 } else {
484 Some(trimmed)
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use gpui::TestAppContext;
492 use indoc::indoc;
493 use language::Point;
494 use pretty_assertions::assert_eq;
495 use project::{FakeFs, Project};
496 use serde_json::json;
497 use settings::SettingsStore;
498 use util::path;
499
500 #[test]
501 fn parse_lines_simple() {
502 let input = indoc! {"
503 diff --git a/text.txt b/text.txt
504 index 86c770d..a1fd855 100644
505 --- a/file.txt
506 +++ b/file.txt
507 @@ -1,2 +1,3 @@
508 context
509 -deleted
510 +inserted
511 garbage
512
513 --- b/file.txt
514 +++ a/file.txt
515 "};
516
517 let lines = input.lines().map(DiffLine::parse).collect::<Vec<_>>();
518
519 pretty_assertions::assert_eq!(
520 lines,
521 &[
522 DiffLine::Garbage("diff --git a/text.txt b/text.txt"),
523 DiffLine::Garbage("index 86c770d..a1fd855 100644"),
524 DiffLine::OldPath {
525 path: "file.txt".into()
526 },
527 DiffLine::NewPath {
528 path: "file.txt".into()
529 },
530 DiffLine::HunkHeader(Some(HunkLocation {
531 start_line_old: 0,
532 count_old: 2,
533 start_line_new: 0,
534 count_new: 3
535 })),
536 DiffLine::Context("context"),
537 DiffLine::Deletion("deleted"),
538 DiffLine::Addition("inserted"),
539 DiffLine::Garbage("garbage"),
540 DiffLine::Context(""),
541 DiffLine::OldPath {
542 path: "b/file.txt".into()
543 },
544 DiffLine::NewPath {
545 path: "a/file.txt".into()
546 },
547 ]
548 );
549 }
550
551 #[test]
552 fn file_header_extra_space() {
553 let options = ["--- file", "--- file", "---\tfile"];
554
555 for option in options {
556 pretty_assertions::assert_eq!(
557 DiffLine::parse(option),
558 DiffLine::OldPath {
559 path: "file".into()
560 },
561 "{option}",
562 );
563 }
564 }
565
566 #[test]
567 fn hunk_header_extra_space() {
568 let options = [
569 "@@ -1,2 +1,3 @@",
570 "@@ -1,2 +1,3 @@",
571 "@@\t-1,2\t+1,3\t@@",
572 "@@ -1,2 +1,3 @@",
573 "@@ -1,2 +1,3 @@",
574 "@@ -1,2 +1,3 @@",
575 "@@ -1,2 +1,3 @@ garbage",
576 ];
577
578 for option in options {
579 pretty_assertions::assert_eq!(
580 DiffLine::parse(option),
581 DiffLine::HunkHeader(Some(HunkLocation {
582 start_line_old: 0,
583 count_old: 2,
584 start_line_new: 0,
585 count_new: 3
586 })),
587 "{option}",
588 );
589 }
590 }
591
592 #[test]
593 fn hunk_header_without_location() {
594 pretty_assertions::assert_eq!(DiffLine::parse("@@ ... @@"), DiffLine::HunkHeader(None));
595 }
596
597 #[test]
598 fn test_parse_path() {
599 assert_eq!(parse_header_path("a/", "foo.txt"), "foo.txt");
600 assert_eq!(
601 parse_header_path("a/", "foo/bar/baz.txt"),
602 "foo/bar/baz.txt"
603 );
604 assert_eq!(parse_header_path("a/", "a/foo.txt"), "foo.txt");
605 assert_eq!(
606 parse_header_path("a/", "a/foo/bar/baz.txt"),
607 "foo/bar/baz.txt"
608 );
609
610 // Extra
611 assert_eq!(
612 parse_header_path("a/", "a/foo/bar/baz.txt 2025"),
613 "foo/bar/baz.txt"
614 );
615 assert_eq!(
616 parse_header_path("a/", "a/foo/bar/baz.txt\t2025"),
617 "foo/bar/baz.txt"
618 );
619 assert_eq!(
620 parse_header_path("a/", "a/foo/bar/baz.txt \""),
621 "foo/bar/baz.txt"
622 );
623
624 // Quoted
625 assert_eq!(
626 parse_header_path("a/", "a/foo/bar/\"baz quox.txt\""),
627 "foo/bar/baz quox.txt"
628 );
629 assert_eq!(
630 parse_header_path("a/", "\"a/foo/bar/baz quox.txt\""),
631 "foo/bar/baz quox.txt"
632 );
633 assert_eq!(
634 parse_header_path("a/", "\"foo/bar/baz quox.txt\""),
635 "foo/bar/baz quox.txt"
636 );
637 assert_eq!(parse_header_path("a/", "\"whatever 🤷\""), "whatever 🤷");
638 assert_eq!(
639 parse_header_path("a/", "\"foo/bar/baz quox.txt\" 2025"),
640 "foo/bar/baz quox.txt"
641 );
642 // unescaped quotes are dropped
643 assert_eq!(parse_header_path("a/", "foo/\"bar\""), "foo/bar");
644
645 // Escaped
646 assert_eq!(
647 parse_header_path("a/", "\"foo/\\\"bar\\\"/baz.txt\""),
648 "foo/\"bar\"/baz.txt"
649 );
650 assert_eq!(
651 parse_header_path("a/", "\"C:\\\\Projects\\\\My App\\\\old file.txt\""),
652 "C:\\Projects\\My App\\old file.txt"
653 );
654 }
655
656 #[test]
657 fn test_parse_diff_with_leading_and_trailing_garbage() {
658 let diff = indoc! {"
659 I need to make some changes.
660
661 I'll change the following things:
662 - one
663 - two
664 - three
665
666 ```
667 --- a/file.txt
668 +++ b/file.txt
669 one
670 +AND
671 two
672 ```
673
674 Summary of what I did:
675 - one
676 - two
677 - three
678
679 That's about it.
680 "};
681
682 let mut events = Vec::new();
683 let mut parser = DiffParser::new(diff);
684 while let Some(event) = parser.next().unwrap() {
685 events.push(event);
686 }
687
688 assert_eq!(
689 events,
690 &[
691 DiffEvent::Hunk {
692 path: "file.txt".into(),
693 hunk: Hunk {
694 context: "one\ntwo\n".into(),
695 edits: vec![Edit {
696 range: 4..4,
697 text: "AND\n".into()
698 }],
699 }
700 },
701 DiffEvent::FileEnd { renamed_to: None }
702 ],
703 )
704 }
705
706 #[gpui::test]
707 async fn test_apply_diff_successful(cx: &mut TestAppContext) {
708 let fs = init_test(cx);
709
710 let buffer_1_text = indoc! {r#"
711 one
712 two
713 three
714 four
715 five
716 "# };
717
718 let buffer_1_text_final = indoc! {r#"
719 3
720 4
721 5
722 "# };
723
724 let buffer_2_text = indoc! {r#"
725 six
726 seven
727 eight
728 nine
729 ten
730 "# };
731
732 let buffer_2_text_final = indoc! {r#"
733 5
734 six
735 seven
736 7.5
737 eight
738 nine
739 ten
740 11
741 "# };
742
743 fs.insert_tree(
744 path!("/root"),
745 json!({
746 "file1": buffer_1_text,
747 "file2": buffer_2_text,
748 }),
749 )
750 .await;
751
752 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
753
754 let diff = indoc! {r#"
755 --- a/root/file1
756 +++ b/root/file1
757 one
758 two
759 -three
760 +3
761 four
762 five
763 --- a/root/file1
764 +++ b/root/file1
765 3
766 -four
767 -five
768 +4
769 +5
770 --- a/root/file1
771 +++ b/root/file1
772 -one
773 -two
774 3
775 4
776 --- a/root/file2
777 +++ b/root/file2
778 +5
779 six
780 --- a/root/file2
781 +++ b/root/file2
782 seven
783 +7.5
784 eight
785 --- a/root/file2
786 +++ b/root/file2
787 ten
788 +11
789 "#};
790
791 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
792 .await
793 .unwrap();
794 let buffer_1 = project
795 .update(cx, |project, cx| {
796 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
797 project.open_buffer(project_path, cx)
798 })
799 .await
800 .unwrap();
801
802 buffer_1.read_with(cx, |buffer, _cx| {
803 assert_eq!(buffer.text(), buffer_1_text_final);
804 });
805 let buffer_2 = project
806 .update(cx, |project, cx| {
807 let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
808 project.open_buffer(project_path, cx)
809 })
810 .await
811 .unwrap();
812
813 buffer_2.read_with(cx, |buffer, _cx| {
814 assert_eq!(buffer.text(), buffer_2_text_final);
815 });
816 }
817
818 #[gpui::test]
819 async fn test_apply_diff_non_unique(cx: &mut TestAppContext) {
820 let fs = init_test(cx);
821
822 let buffer_1_text = indoc! {r#"
823 one
824 two
825 three
826 four
827 five
828 one
829 two
830 three
831 four
832 five
833 "# };
834
835 fs.insert_tree(
836 path!("/root"),
837 json!({
838 "file1": buffer_1_text,
839 }),
840 )
841 .await;
842
843 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
844 let buffer = project
845 .update(cx, |project, cx| {
846 project.open_local_buffer(path!("/root/file1"), cx)
847 })
848 .await
849 .unwrap();
850 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
851
852 let diff = indoc! {r#"
853 --- a/root/file1
854 +++ b/root/file1
855 one
856 two
857 -three
858 +3
859 four
860 five
861 "#};
862
863 let final_text = indoc! {r#"
864 one
865 two
866 three
867 four
868 five
869 one
870 two
871 3
872 four
873 five
874 "#};
875
876 apply_diff(diff, &project, &mut cx.to_async())
877 .await
878 .expect_err("Non-unique edits should fail");
879
880 let ranges = [buffer_snapshot.anchor_before(Point::new(1, 0))
881 ..buffer_snapshot.anchor_after(buffer_snapshot.max_point())];
882
883 let (edited_snapshot, edits) = parse_diff(diff, |_path| Some((&buffer_snapshot, &ranges)))
884 .await
885 .unwrap();
886
887 assert_eq!(edited_snapshot.remote_id(), buffer_snapshot.remote_id());
888 buffer.update(cx, |buffer, cx| {
889 buffer.edit(edits, None, cx);
890 assert_eq!(buffer.text(), final_text);
891 });
892 }
893
894 #[gpui::test]
895 async fn test_parse_diff_with_edits_within_line(cx: &mut TestAppContext) {
896 let fs = init_test(cx);
897
898 let buffer_1_text = indoc! {r#"
899 one two three four
900 five six seven eight
901 nine ten eleven twelve
902 "# };
903
904 fs.insert_tree(
905 path!("/root"),
906 json!({
907 "file1": buffer_1_text,
908 }),
909 )
910 .await;
911
912 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
913 let buffer = project
914 .update(cx, |project, cx| {
915 project.open_local_buffer(path!("/root/file1"), cx)
916 })
917 .await
918 .unwrap();
919 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
920
921 let diff = indoc! {r#"
922 --- a/root/file1
923 +++ b/root/file1
924 one two three four
925 -five six seven eight
926 +five SIX seven eight!
927 nine ten eleven twelve
928 "#};
929
930 let (buffer, edits) = parse_diff(diff, |_path| {
931 Some((&buffer_snapshot, &[(Anchor::MIN..Anchor::MAX)] as &[_]))
932 })
933 .await
934 .unwrap();
935
936 let edits = edits
937 .into_iter()
938 .map(|(range, text)| (range.to_point(&buffer), text))
939 .collect::<Vec<_>>();
940 assert_eq!(
941 edits,
942 &[
943 (Point::new(1, 5)..Point::new(1, 8), "SIX".into()),
944 (Point::new(1, 20)..Point::new(1, 20), "!".into())
945 ]
946 );
947 }
948
949 #[gpui::test]
950 async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
951 let fs = init_test(cx);
952
953 let start = indoc! {r#"
954 one
955 two
956 three
957 four
958 five
959
960 four
961 five
962 "# };
963
964 let end = indoc! {r#"
965 one
966 two
967 3
968 four
969 5
970
971 four
972 five
973 "# };
974
975 fs.insert_tree(
976 path!("/root"),
977 json!({
978 "file1": start,
979 }),
980 )
981 .await;
982
983 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
984
985 let diff = indoc! {r#"
986 --- a/root/file1
987 +++ b/root/file1
988 one
989 two
990 -three
991 +3
992 four
993 -five
994 +5
995 "#};
996
997 let _buffers = apply_diff(diff, &project, &mut cx.to_async())
998 .await
999 .unwrap();
1000
1001 let buffer_1 = project
1002 .update(cx, |project, cx| {
1003 let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
1004 project.open_buffer(project_path, cx)
1005 })
1006 .await
1007 .unwrap();
1008
1009 buffer_1.read_with(cx, |buffer, _cx| {
1010 assert_eq!(buffer.text(), end);
1011 });
1012 }
1013
1014 fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
1015 cx.update(|cx| {
1016 let settings_store = SettingsStore::test(cx);
1017 cx.set_global(settings_store);
1018 });
1019
1020 FakeFs::new(cx.background_executor.clone())
1021 }
1022}