udiff.rs

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