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