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