example.rs

  1use std::{
  2    borrow::Cow,
  3    cell::RefCell,
  4    env,
  5    fmt::{self, Display},
  6    fs,
  7    io::Write,
  8    mem,
  9    ops::Range,
 10    path::{Path, PathBuf},
 11    sync::Arc,
 12};
 13
 14use anyhow::{Context as _, Result};
 15use clap::ValueEnum;
 16use collections::{HashMap, HashSet};
 17use futures::{
 18    AsyncWriteExt as _,
 19    lock::{Mutex, OwnedMutexGuard},
 20};
 21use gpui::{AsyncApp, Entity, http_client::Url};
 22use language::Buffer;
 23use project::{Project, ProjectPath};
 24use pulldown_cmark::CowStr;
 25use serde::{Deserialize, Serialize};
 26
 27const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff";
 28const EDIT_HISTORY_HEADING: &str = "Edit History";
 29const CURSOR_POSITION_HEADING: &str = "Cursor Position";
 30const EXPECTED_PATCH_HEADING: &str = "Expected Patch";
 31const EXPECTED_EXCERPTS_HEADING: &str = "Expected Excerpts";
 32const REPOSITORY_URL_FIELD: &str = "repository_url";
 33const REVISION_FIELD: &str = "revision";
 34
 35#[derive(Debug, Clone)]
 36pub struct NamedExample {
 37    pub name: String,
 38    pub example: Example,
 39}
 40
 41#[derive(Clone, Debug, Serialize, Deserialize)]
 42pub struct Example {
 43    pub repository_url: String,
 44    pub revision: String,
 45    pub uncommitted_diff: String,
 46    pub cursor_path: PathBuf,
 47    pub cursor_position: String,
 48    pub edit_history: String,
 49    pub expected_patch: String,
 50    pub expected_excerpts: Vec<ExpectedExcerpt>,
 51}
 52
 53pub type ExpectedExcerpt = Excerpt;
 54pub type ActualExcerpt = Excerpt;
 55
 56#[derive(Clone, Debug, Serialize, Deserialize)]
 57pub struct Excerpt {
 58    pub path: PathBuf,
 59    pub text: String,
 60}
 61
 62#[derive(ValueEnum, Debug, Clone)]
 63pub enum ExampleFormat {
 64    Json,
 65    Toml,
 66    Md,
 67}
 68
 69impl NamedExample {
 70    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
 71        let path = path.as_ref();
 72        let content = std::fs::read_to_string(path)?;
 73        let ext = path.extension();
 74
 75        match ext.and_then(|s| s.to_str()) {
 76            Some("json") => Ok(Self {
 77                name: path.file_stem().unwrap_or_default().display().to_string(),
 78                example: serde_json::from_str(&content)?,
 79            }),
 80            Some("toml") => Ok(Self {
 81                name: path.file_stem().unwrap_or_default().display().to_string(),
 82                example: toml::from_str(&content)?,
 83            }),
 84            Some("md") => Self::parse_md(&content),
 85            Some(_) => {
 86                anyhow::bail!("Unrecognized example extension: {}", ext.unwrap().display());
 87            }
 88            None => {
 89                anyhow::bail!(
 90                    "Failed to determine example type since the file does not have an extension."
 91                );
 92            }
 93        }
 94    }
 95
 96    pub fn parse_md(input: &str) -> Result<Self> {
 97        use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Parser, Tag, TagEnd};
 98
 99        let parser = Parser::new(input);
100
101        let mut named = NamedExample {
102            name: String::new(),
103            example: Example {
104                repository_url: String::new(),
105                revision: String::new(),
106                uncommitted_diff: String::new(),
107                cursor_path: PathBuf::new(),
108                cursor_position: String::new(),
109                edit_history: String::new(),
110                expected_patch: String::new(),
111                expected_excerpts: Vec::new(),
112            },
113        };
114
115        let mut text = String::new();
116        let mut current_section = String::new();
117        let mut block_info: CowStr = "".into();
118
119        for event in parser {
120            match event {
121                Event::Text(line) => {
122                    text.push_str(&line);
123
124                    if !named.name.is_empty()
125                        && current_section.is_empty()
126                        // in h1 section
127                        && let Some((field, value)) = line.split_once('=')
128                    {
129                        match field.trim() {
130                            REPOSITORY_URL_FIELD => {
131                                named.example.repository_url = value.trim().to_string();
132                            }
133                            REVISION_FIELD => {
134                                named.example.revision = value.trim().to_string();
135                            }
136                            _ => {
137                                eprintln!("Warning: Unrecognized field `{field}`");
138                            }
139                        }
140                    }
141                }
142                Event::End(TagEnd::Heading(HeadingLevel::H1)) => {
143                    if !named.name.is_empty() {
144                        anyhow::bail!(
145                            "Found multiple H1 headings. There should only be one with the name of the example."
146                        );
147                    }
148                    named.name = mem::take(&mut text);
149                }
150                Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
151                    current_section = mem::take(&mut text);
152                }
153                Event::End(TagEnd::Heading(level)) => {
154                    anyhow::bail!("Unexpected heading level: {level}");
155                }
156                Event::Start(Tag::CodeBlock(kind)) => {
157                    match kind {
158                        CodeBlockKind::Fenced(info) => {
159                            block_info = info;
160                        }
161                        CodeBlockKind::Indented => {
162                            anyhow::bail!("Unexpected indented codeblock");
163                        }
164                    };
165                }
166                Event::Start(_) => {
167                    text.clear();
168                    block_info = "".into();
169                }
170                Event::End(TagEnd::CodeBlock) => {
171                    let block_info = block_info.trim();
172                    if current_section.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) {
173                        named.example.uncommitted_diff = mem::take(&mut text);
174                    } else if current_section.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) {
175                        named.example.edit_history.push_str(&mem::take(&mut text));
176                    } else if current_section.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) {
177                        named.example.cursor_path = block_info.into();
178                        named.example.cursor_position = mem::take(&mut text);
179                    } else if current_section.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) {
180                        named.example.expected_patch = mem::take(&mut text);
181                    } else if current_section.eq_ignore_ascii_case(EXPECTED_EXCERPTS_HEADING) {
182                        // TODO: "…" should not be a part of the excerpt
183                        named.example.expected_excerpts.push(ExpectedExcerpt {
184                            path: block_info.into(),
185                            text: mem::take(&mut text),
186                        });
187                    } else {
188                        eprintln!("Warning: Unrecognized section `{current_section:?}`")
189                    }
190                }
191                _ => {}
192            }
193        }
194
195        if named.example.cursor_path.as_path() == Path::new("")
196            || named.example.cursor_position.is_empty()
197        {
198            anyhow::bail!("Missing cursor position codeblock");
199        }
200
201        Ok(named)
202    }
203
204    pub fn write(&self, format: ExampleFormat, mut out: impl Write) -> Result<()> {
205        match format {
206            ExampleFormat::Json => Ok(serde_json::to_writer(out, &self.example)?),
207            ExampleFormat::Toml => {
208                Ok(out.write_all(toml::to_string_pretty(&self.example)?.as_bytes())?)
209            }
210            ExampleFormat::Md => Ok(write!(out, "{}", self)?),
211        }
212    }
213
214    pub async fn setup_worktree(&self) -> Result<PathBuf> {
215        let (repo_owner, repo_name) = self.repo_name()?;
216        let file_name = self.file_name();
217
218        let worktrees_dir = env::current_dir()?.join("target").join("zeta-worktrees");
219        let repos_dir = env::current_dir()?.join("target").join("zeta-repos");
220        fs::create_dir_all(&repos_dir)?;
221        fs::create_dir_all(&worktrees_dir)?;
222
223        let repo_dir = repos_dir.join(repo_owner.as_ref()).join(repo_name.as_ref());
224        let repo_lock = lock_repo(&repo_dir).await;
225
226        if !repo_dir.is_dir() {
227            fs::create_dir_all(&repo_dir)?;
228            run_git(&repo_dir, &["init"]).await?;
229            run_git(
230                &repo_dir,
231                &["remote", "add", "origin", &self.example.repository_url],
232            )
233            .await?;
234        }
235
236        // Resolve the example to a revision, fetching it if needed.
237        let revision = run_git(
238            &repo_dir,
239            &[
240                "rev-parse",
241                &format!("{}^{{commit}}", &self.example.revision),
242            ],
243        )
244        .await;
245        let revision = if let Ok(revision) = revision {
246            revision
247        } else {
248            run_git(
249                &repo_dir,
250                &["fetch", "--depth", "2", "origin", &self.example.revision],
251            )
252            .await?;
253            let revision = run_git(&repo_dir, &["rev-parse", "FETCH_HEAD"]).await?;
254            if revision != self.example.revision {
255                run_git(&repo_dir, &["tag", &self.example.revision, &revision]).await?;
256            }
257            revision
258        };
259
260        // Create the worktree for this example if needed.
261        let worktree_path = worktrees_dir.join(&file_name);
262        if worktree_path.is_dir() {
263            run_git(&worktree_path, &["clean", "--force", "-d"]).await?;
264            run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?;
265            run_git(&worktree_path, &["checkout", revision.as_str()]).await?;
266        } else {
267            let worktree_path_string = worktree_path.to_string_lossy();
268            run_git(&repo_dir, &["branch", "-f", &file_name, revision.as_str()]).await?;
269            run_git(
270                &repo_dir,
271                &["worktree", "add", "-f", &worktree_path_string, &file_name],
272            )
273            .await?;
274        }
275        drop(repo_lock);
276
277        // Apply the uncommitted diff for this example.
278        if !self.example.uncommitted_diff.is_empty() {
279            let mut apply_process = smol::process::Command::new("git")
280                .current_dir(&worktree_path)
281                .args(&["apply", "-"])
282                .stdin(std::process::Stdio::piped())
283                .spawn()?;
284
285            let mut stdin = apply_process.stdin.take().unwrap();
286            stdin
287                .write_all(self.example.uncommitted_diff.as_bytes())
288                .await?;
289            stdin.close().await?;
290            drop(stdin);
291
292            let apply_result = apply_process.output().await?;
293            if !apply_result.status.success() {
294                anyhow::bail!(
295                    "Failed to apply uncommitted diff patch with status: {}\nstderr:\n{}\nstdout:\n{}",
296                    apply_result.status,
297                    String::from_utf8_lossy(&apply_result.stderr),
298                    String::from_utf8_lossy(&apply_result.stdout),
299                );
300            }
301        }
302
303        Ok(worktree_path)
304    }
305
306    fn file_name(&self) -> String {
307        self.name
308            .chars()
309            .map(|c| {
310                if c.is_whitespace() {
311                    '-'
312                } else {
313                    c.to_ascii_lowercase()
314                }
315            })
316            .collect()
317    }
318
319    #[allow(unused)]
320    fn repo_name(&self) -> Result<(Cow<'_, str>, Cow<'_, str>)> {
321        // git@github.com:owner/repo.git
322        if self.example.repository_url.contains('@') {
323            let (owner, repo) = self
324                .example
325                .repository_url
326                .split_once(':')
327                .context("expected : in git url")?
328                .1
329                .split_once('/')
330                .context("expected / in git url")?;
331            Ok((
332                Cow::Borrowed(owner),
333                Cow::Borrowed(repo.trim_end_matches(".git")),
334            ))
335        // http://github.com/owner/repo.git
336        } else {
337            let url = Url::parse(&self.example.repository_url)?;
338            let mut segments = url.path_segments().context("empty http url")?;
339            let owner = segments
340                .next()
341                .context("expected owner path segment")?
342                .to_string();
343            let repo = segments
344                .next()
345                .context("expected repo path segment")?
346                .trim_end_matches(".git")
347                .to_string();
348            assert!(segments.next().is_none());
349
350            Ok((owner.into(), repo.into()))
351        }
352    }
353
354    #[must_use]
355    pub async fn apply_edit_history(
356        &self,
357        project: &Entity<Project>,
358        cx: &mut AsyncApp,
359    ) -> Result<HashSet<Entity<Buffer>>> {
360        apply_diff(&self.example.edit_history, project, cx).await
361    }
362}
363
364async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
365    let output = smol::process::Command::new("git")
366        .current_dir(repo_path)
367        .args(args)
368        .output()
369        .await?;
370
371    anyhow::ensure!(
372        output.status.success(),
373        "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}",
374        args.join(" "),
375        repo_path.display(),
376        output.status,
377        String::from_utf8_lossy(&output.stderr),
378        String::from_utf8_lossy(&output.stdout),
379    );
380    Ok(String::from_utf8(output.stdout)?.trim().to_string())
381}
382
383impl Display for NamedExample {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        write!(f, "# {}\n\n", self.name)?;
386        write!(
387            f,
388            "{REPOSITORY_URL_FIELD} = {}\n",
389            self.example.repository_url
390        )?;
391        write!(f, "{REVISION_FIELD} = {}\n\n", self.example.revision)?;
392
393        write!(f, "## {UNCOMMITTED_DIFF_HEADING}\n\n")?;
394        write!(f, "`````diff\n")?;
395        write!(f, "{}", self.example.uncommitted_diff)?;
396        write!(f, "`````\n")?;
397
398        if !self.example.edit_history.is_empty() {
399            write!(f, "`````diff\n{}`````\n", self.example.edit_history)?;
400        }
401
402        write!(
403            f,
404            "## {CURSOR_POSITION_HEADING}\n\n`````{}\n{}`````\n",
405            self.example.cursor_path.display(),
406            self.example.cursor_position
407        )?;
408        write!(f, "## {EDIT_HISTORY_HEADING}\n\n")?;
409
410        if !self.example.expected_patch.is_empty() {
411            write!(
412                f,
413                "\n## {EXPECTED_PATCH_HEADING}\n\n`````diff\n{}`````\n",
414                self.example.expected_patch
415            )?;
416        }
417
418        if !self.example.expected_excerpts.is_empty() {
419            write!(f, "\n## {EXPECTED_EXCERPTS_HEADING}\n\n")?;
420
421            for excerpt in &self.example.expected_excerpts {
422                write!(
423                    f,
424                    "`````{}{}\n{}`````\n\n",
425                    excerpt
426                        .path
427                        .extension()
428                        .map(|ext| format!("{} ", ext.to_string_lossy()))
429                        .unwrap_or_default(),
430                    excerpt.path.display(),
431                    excerpt.text
432                )?;
433            }
434        }
435
436        Ok(())
437    }
438}
439
440thread_local! {
441    static REPO_LOCKS: RefCell<HashMap<PathBuf, Arc<Mutex<()>>>> = RefCell::new(HashMap::default());
442}
443
444#[must_use]
445pub async fn lock_repo(path: impl AsRef<Path>) -> OwnedMutexGuard<()> {
446    REPO_LOCKS
447        .with(|cell| {
448            cell.borrow_mut()
449                .entry(path.as_ref().to_path_buf())
450                .or_default()
451                .clone()
452        })
453        .lock_owned()
454        .await
455}
456
457#[must_use]
458pub async fn apply_diff(
459    diff: &str,
460    project: &Entity<Project>,
461    cx: &mut AsyncApp,
462) -> Result<HashSet<Entity<Buffer>>> {
463    use cloud_llm_client::udiff::DiffLine;
464    use std::fmt::Write;
465
466    #[derive(Debug, Default)]
467    struct HunkState {
468        context: String,
469        edits: Vec<Edit>,
470    }
471
472    #[derive(Debug)]
473    struct Edit {
474        range: Range<usize>,
475        text: String,
476    }
477
478    let mut old_path = None;
479    let mut new_path = None;
480    let mut hunk = HunkState::default();
481    let mut diff_lines = diff.lines().map(DiffLine::parse).peekable();
482    let mut open_buffers = HashSet::default();
483
484    while let Some(diff_line) = diff_lines.next() {
485        match diff_line {
486            DiffLine::OldPath { path } => old_path = Some(path),
487            DiffLine::NewPath { path } => {
488                if old_path.is_none() {
489                    anyhow::bail!(
490                        "Found a new path header (`+++`) before an (`---`) old path header"
491                    );
492                }
493                new_path = Some(path)
494            }
495            DiffLine::Context(ctx) => {
496                writeln!(&mut hunk.context, "{ctx}")?;
497            }
498            DiffLine::Deletion(del) => {
499                let range = hunk.context.len()..hunk.context.len() + del.len() + '\n'.len_utf8();
500                if let Some(last_edit) = hunk.edits.last_mut()
501                    && last_edit.range.end == range.start
502                {
503                    last_edit.range.end = range.end;
504                } else {
505                    hunk.edits.push(Edit {
506                        range,
507                        text: String::new(),
508                    });
509                }
510                writeln!(&mut hunk.context, "{del}")?;
511            }
512            DiffLine::Addition(add) => {
513                let range = hunk.context.len()..hunk.context.len();
514                if let Some(last_edit) = hunk.edits.last_mut()
515                    && last_edit.range.end == range.start
516                {
517                    writeln!(&mut last_edit.text, "{add}").unwrap();
518                } else {
519                    hunk.edits.push(Edit {
520                        range,
521                        text: format!("{add}\n"),
522                    });
523                }
524            }
525            DiffLine::HunkHeader(_) | DiffLine::Garbage(_) => {}
526        }
527
528        let at_hunk_end = match diff_lines.peek() {
529            Some(DiffLine::OldPath { .. }) | Some(DiffLine::HunkHeader(_)) | None => true,
530            _ => false,
531        };
532
533        if at_hunk_end {
534            let hunk = mem::take(&mut hunk);
535
536            let Some(old_path) = old_path.as_deref() else {
537                anyhow::bail!("Missing old path (`---`) header")
538            };
539
540            let Some(new_path) = new_path.as_deref() else {
541                anyhow::bail!("Missing new path (`+++`) header")
542            };
543
544            let buffer = project
545                .update(cx, |project, cx| {
546                    let project_path = project
547                        .find_project_path(old_path, cx)
548                        .context("Failed to find old_path in project")?;
549
550                    anyhow::Ok(project.open_buffer(project_path, cx))
551                })??
552                .await?;
553            open_buffers.insert(buffer.clone());
554
555            if old_path != new_path {
556                project
557                    .update(cx, |project, cx| {
558                        let project_file = project::File::from_dyn(buffer.read(cx).file()).unwrap();
559                        let new_path = ProjectPath {
560                            worktree_id: project_file.worktree_id(cx),
561                            path: project_file.path.clone(),
562                        };
563                        project.rename_entry(project_file.entry_id.unwrap(), new_path, cx)
564                    })?
565                    .await?;
566            }
567
568            // TODO is it worth using project search?
569            buffer.update(cx, |buffer, cx| {
570                let context_offset = if hunk.context.is_empty() {
571                    0
572                } else {
573                    let text = buffer.text();
574                    if let Some(offset) = text.find(&hunk.context) {
575                        if text[offset + 1..].contains(&hunk.context) {
576                            anyhow::bail!("Context is not unique enough:\n{}", hunk.context);
577                        }
578                        offset
579                    } else {
580                        anyhow::bail!(
581                            "Failed to match context:\n{}\n\nBuffer:\n{}",
582                            hunk.context,
583                            text
584                        );
585                    }
586                };
587
588                buffer.edit(
589                    hunk.edits.into_iter().map(|edit| {
590                        (
591                            context_offset + edit.range.start..context_offset + edit.range.end,
592                            edit.text,
593                        )
594                    }),
595                    None,
596                    cx,
597                );
598
599                anyhow::Ok(())
600            })??;
601        }
602    }
603
604    anyhow::Ok(open_buffers)
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use ::fs::FakeFs;
611    use gpui::TestAppContext;
612    use indoc::indoc;
613    use pretty_assertions::assert_eq;
614    use project::Project;
615    use serde_json::json;
616    use settings::SettingsStore;
617    use util::path;
618
619    #[gpui::test]
620    async fn test_apply_diff_successful(cx: &mut TestAppContext) {
621        let buffer_1_text = indoc! {r#"
622            one
623            two
624            three
625            four
626            five
627        "# };
628
629        let buffer_1_text_final = indoc! {r#"
630            3
631            4
632            5
633        "# };
634
635        let buffer_2_text = indoc! {r#"
636            six
637            seven
638            eight
639            nine
640            ten
641        "# };
642
643        let buffer_2_text_final = indoc! {r#"
644            5
645            six
646            seven
647            7.5
648            eight
649            nine
650            ten
651            11
652        "# };
653
654        cx.update(|cx| {
655            let settings_store = SettingsStore::test(cx);
656            cx.set_global(settings_store);
657            Project::init_settings(cx);
658            language::init(cx);
659        });
660
661        let fs = FakeFs::new(cx.background_executor.clone());
662        fs.insert_tree(
663            path!("/root"),
664            json!({
665                "file1": buffer_1_text,
666                "file2": buffer_2_text,
667            }),
668        )
669        .await;
670
671        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
672
673        let diff = indoc! {r#"
674            --- a/root/file1
675            +++ b/root/file1
676             one
677             two
678            -three
679            +3
680             four
681             five
682            --- a/root/file1
683            +++ b/root/file1
684             3
685            -four
686            -five
687            +4
688            +5
689            --- a/root/file1
690            +++ b/root/file1
691            -one
692            -two
693             3
694             4
695            --- a/root/file2
696            +++ b/root/file2
697            +5
698             six
699            --- a/root/file2
700            +++ b/root/file2
701             seven
702            +7.5
703             eight
704            --- a/root/file2
705            +++ b/root/file2
706             ten
707            +11
708        "#};
709
710        let _buffers = apply_diff(diff, &project, &mut cx.to_async())
711            .await
712            .unwrap();
713        let buffer_1 = project
714            .update(cx, |project, cx| {
715                let project_path = project.find_project_path(path!("/root/file1"), cx).unwrap();
716                project.open_buffer(project_path, cx)
717            })
718            .await
719            .unwrap();
720
721        buffer_1.read_with(cx, |buffer, _cx| {
722            assert_eq!(buffer.text(), buffer_1_text_final);
723        });
724        let buffer_2 = project
725            .update(cx, |project, cx| {
726                let project_path = project.find_project_path(path!("/root/file2"), cx).unwrap();
727                project.open_buffer(project_path, cx)
728            })
729            .await
730            .unwrap();
731
732        buffer_2.read_with(cx, |buffer, _cx| {
733            assert_eq!(buffer.text(), buffer_2_text_final);
734        });
735    }
736
737    #[gpui::test]
738    async fn test_apply_diff_non_unique(cx: &mut TestAppContext) {
739        let buffer_1_text = indoc! {r#"
740            one
741            two
742            three
743            four
744            five
745            one
746            two
747            three
748            four
749            five
750        "# };
751
752        cx.update(|cx| {
753            let settings_store = SettingsStore::test(cx);
754            cx.set_global(settings_store);
755            Project::init_settings(cx);
756            language::init(cx);
757        });
758
759        let fs = FakeFs::new(cx.background_executor.clone());
760        fs.insert_tree(
761            path!("/root"),
762            json!({
763                "file1": buffer_1_text,
764            }),
765        )
766        .await;
767
768        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
769
770        let diff = indoc! {r#"
771            --- a/root/file1
772            +++ b/root/file1
773             one
774             two
775            -three
776            +3
777             four
778             five
779        "#};
780
781        apply_diff(diff, &project, &mut cx.to_async())
782            .await
783            .expect_err("Non-unique edits should fail");
784    }
785
786    #[gpui::test]
787    async fn test_apply_diff_unique_via_previous_context(cx: &mut TestAppContext) {
788        let start = indoc! {r#"
789            one
790            two
791            three
792            four
793            five
794
795            four
796            five
797        "# };
798
799        let end = indoc! {r#"
800            one
801            two
802            3
803            four
804            5
805
806            four
807            five
808        "# };
809
810        cx.update(|cx| {
811            let settings_store = SettingsStore::test(cx);
812            cx.set_global(settings_store);
813            Project::init_settings(cx);
814            language::init(cx);
815        });
816
817        let fs = FakeFs::new(cx.background_executor.clone());
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}