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