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}