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}