1use anyhow::{anyhow, Result};
2use fsevent::EventStream;
3use futures::{future::BoxFuture, Stream, StreamExt};
4use language::git::libgit::{Repository, RepositoryOpenFlags};
5use language::LineEnding;
6use smol::io::{AsyncReadExt, AsyncWriteExt};
7use std::{
8 ffi::OsStr,
9 io,
10 os::unix::fs::MetadataExt,
11 path::{Component, Path, PathBuf},
12 pin::Pin,
13 time::{Duration, SystemTime},
14};
15
16use text::Rope;
17
18#[cfg(any(test, feature = "test-support"))]
19use collections::{btree_map, BTreeMap};
20#[cfg(any(test, feature = "test-support"))]
21use futures::lock::Mutex;
22#[cfg(any(test, feature = "test-support"))]
23use std::sync::{Arc, Weak};
24
25use crate::git_repository::{FakeGitRepository, GitRepository, RealGitRepository};
26
27#[async_trait::async_trait]
28pub trait Fs: Send + Sync {
29 async fn create_dir(&self, path: &Path) -> Result<()>;
30 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
31 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
32 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
33 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
34 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
35 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
36 async fn load(&self, path: &Path) -> Result<String>;
37 async fn load_head_text(&self, path: &Path) -> Option<String>;
38 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
39 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
40 async fn is_file(&self, path: &Path) -> bool;
41 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
42 async fn read_dir(
43 &self,
44 path: &Path,
45 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
46 async fn watch(
47 &self,
48 path: &Path,
49 latency: Duration,
50 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
51 fn open_git_repository(
52 &self,
53 abs_dotgit_path: &Path,
54 content_path: &Arc<Path>,
55 ) -> Option<Box<dyn GitRepository>>;
56 fn is_fake(&self) -> bool;
57 #[cfg(any(test, feature = "test-support"))]
58 fn as_fake(&self) -> &FakeFs;
59}
60
61#[derive(Copy, Clone, Default)]
62pub struct CreateOptions {
63 pub overwrite: bool,
64 pub ignore_if_exists: bool,
65}
66
67#[derive(Copy, Clone, Default)]
68pub struct CopyOptions {
69 pub overwrite: bool,
70 pub ignore_if_exists: bool,
71}
72
73#[derive(Copy, Clone, Default)]
74pub struct RenameOptions {
75 pub overwrite: bool,
76 pub ignore_if_exists: bool,
77}
78
79#[derive(Copy, Clone, Default)]
80pub struct RemoveOptions {
81 pub recursive: bool,
82 pub ignore_if_not_exists: bool,
83}
84
85#[derive(Clone, Debug)]
86pub struct Metadata {
87 pub inode: u64,
88 pub mtime: SystemTime,
89 pub is_symlink: bool,
90 pub is_dir: bool,
91}
92
93pub struct RealFs;
94
95#[async_trait::async_trait]
96impl Fs for RealFs {
97 async fn create_dir(&self, path: &Path) -> Result<()> {
98 Ok(smol::fs::create_dir_all(path).await?)
99 }
100
101 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
102 let mut open_options = smol::fs::OpenOptions::new();
103 open_options.write(true).create(true);
104 if options.overwrite {
105 open_options.truncate(true);
106 } else if !options.ignore_if_exists {
107 open_options.create_new(true);
108 }
109 open_options.open(path).await?;
110 Ok(())
111 }
112
113 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
114 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
115 if options.ignore_if_exists {
116 return Ok(());
117 } else {
118 return Err(anyhow!("{target:?} already exists"));
119 }
120 }
121
122 smol::fs::copy(source, target).await?;
123 Ok(())
124 }
125
126 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
127 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
128 if options.ignore_if_exists {
129 return Ok(());
130 } else {
131 return Err(anyhow!("{target:?} already exists"));
132 }
133 }
134
135 smol::fs::rename(source, target).await?;
136 Ok(())
137 }
138
139 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
140 let result = if options.recursive {
141 smol::fs::remove_dir_all(path).await
142 } else {
143 smol::fs::remove_dir(path).await
144 };
145 match result {
146 Ok(()) => Ok(()),
147 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
148 Ok(())
149 }
150 Err(err) => Err(err)?,
151 }
152 }
153
154 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
155 match smol::fs::remove_file(path).await {
156 Ok(()) => Ok(()),
157 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
158 Ok(())
159 }
160 Err(err) => Err(err)?,
161 }
162 }
163
164 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
165 Ok(Box::new(std::fs::File::open(path)?))
166 }
167
168 async fn load(&self, path: &Path) -> Result<String> {
169 let mut file = smol::fs::File::open(path).await?;
170 let mut text = String::new();
171 file.read_to_string(&mut text).await?;
172 Ok(text)
173 }
174
175 async fn load_head_text(&self, path: &Path) -> Option<String> {
176 fn logic(path: &Path) -> Result<Option<String>> {
177 let repo = Repository::open_ext(path, RepositoryOpenFlags::empty(), &[OsStr::new("")])?;
178 assert!(repo.path().ends_with(".git"));
179 let repo_root_path = match repo.path().parent() {
180 Some(root) => root,
181 None => return Ok(None),
182 };
183
184 let relative_path = path.strip_prefix(repo_root_path)?;
185 let object = repo
186 .head()?
187 .peel_to_tree()?
188 .get_path(relative_path)?
189 .to_object(&repo)?;
190
191 let content = match object.as_blob() {
192 Some(blob) => blob.content().to_owned(),
193 None => return Ok(None),
194 };
195
196 let head_text = String::from_utf8(content.to_owned())?;
197 Ok(Some(head_text))
198 }
199
200 match logic(path) {
201 Ok(value) => return value,
202 Err(err) => log::error!("Error loading head text: {:?}", err),
203 }
204 None
205 }
206
207 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
208 let buffer_size = text.summary().len.min(10 * 1024);
209 let file = smol::fs::File::create(path).await?;
210 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
211 for chunk in chunks(text, line_ending) {
212 writer.write_all(chunk.as_bytes()).await?;
213 }
214 writer.flush().await?;
215 Ok(())
216 }
217
218 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
219 Ok(smol::fs::canonicalize(path).await?)
220 }
221
222 async fn is_file(&self, path: &Path) -> bool {
223 smol::fs::metadata(path)
224 .await
225 .map_or(false, |metadata| metadata.is_file())
226 }
227
228 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
229 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
230 Ok(metadata) => metadata,
231 Err(err) => {
232 return match (err.kind(), err.raw_os_error()) {
233 (io::ErrorKind::NotFound, _) => Ok(None),
234 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
235 _ => Err(anyhow::Error::new(err)),
236 }
237 }
238 };
239
240 let is_symlink = symlink_metadata.file_type().is_symlink();
241 let metadata = if is_symlink {
242 smol::fs::metadata(path).await?
243 } else {
244 symlink_metadata
245 };
246 Ok(Some(Metadata {
247 inode: metadata.ino(),
248 mtime: metadata.modified().unwrap(),
249 is_symlink,
250 is_dir: metadata.file_type().is_dir(),
251 }))
252 }
253
254 async fn read_dir(
255 &self,
256 path: &Path,
257 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
258 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
259 Ok(entry) => Ok(entry.path()),
260 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
261 });
262 Ok(Box::pin(result))
263 }
264
265 async fn watch(
266 &self,
267 path: &Path,
268 latency: Duration,
269 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
270 let (tx, rx) = smol::channel::unbounded();
271 let (stream, handle) = EventStream::new(&[path], latency);
272 std::thread::spawn(move || {
273 stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
274 });
275 Box::pin(rx.chain(futures::stream::once(async move {
276 drop(handle);
277 vec![]
278 })))
279 }
280
281 fn open_git_repository(
282 &self,
283 abs_dotgit_path: &Path,
284 content_path: &Arc<Path>,
285 ) -> Option<Box<dyn GitRepository>> {
286 RealGitRepository::open(abs_dotgit_path, content_path)
287 }
288
289 fn is_fake(&self) -> bool {
290 false
291 }
292 #[cfg(any(test, feature = "test-support"))]
293 fn as_fake(&self) -> &FakeFs {
294 panic!("called `RealFs::as_fake`")
295 }
296}
297
298#[cfg(any(test, feature = "test-support"))]
299pub struct FakeFs {
300 // Use an unfair lock to ensure tests are deterministic.
301 state: Mutex<FakeFsState>,
302 executor: Weak<gpui::executor::Background>,
303}
304
305#[cfg(any(test, feature = "test-support"))]
306struct FakeFsState {
307 root: Arc<Mutex<FakeFsEntry>>,
308 next_inode: u64,
309 event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
310}
311
312#[cfg(any(test, feature = "test-support"))]
313#[derive(Debug)]
314enum FakeFsEntry {
315 File {
316 inode: u64,
317 mtime: SystemTime,
318 content: String,
319 },
320 Dir {
321 inode: u64,
322 mtime: SystemTime,
323 entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
324 },
325 Symlink {
326 target: PathBuf,
327 },
328}
329
330#[cfg(any(test, feature = "test-support"))]
331impl FakeFsState {
332 async fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
333 Ok(self
334 .try_read_path(target)
335 .await
336 .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
337 .0)
338 }
339
340 async fn try_read_path<'a>(
341 &'a self,
342 target: &Path,
343 ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
344 let mut path = target.to_path_buf();
345 let mut real_path = PathBuf::new();
346 let mut entry_stack = Vec::new();
347 'outer: loop {
348 let mut path_components = path.components().collect::<collections::VecDeque<_>>();
349 while let Some(component) = path_components.pop_front() {
350 match component {
351 Component::Prefix(_) => panic!("prefix paths aren't supported"),
352 Component::RootDir => {
353 entry_stack.clear();
354 entry_stack.push(self.root.clone());
355 real_path.clear();
356 real_path.push("/");
357 }
358 Component::CurDir => {}
359 Component::ParentDir => {
360 entry_stack.pop()?;
361 real_path.pop();
362 }
363 Component::Normal(name) => {
364 let current_entry = entry_stack.last().cloned()?;
365 let current_entry = current_entry.lock().await;
366 if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
367 let entry = entries.get(name.to_str().unwrap()).cloned()?;
368 let _entry = entry.lock().await;
369 if let FakeFsEntry::Symlink { target, .. } = &*_entry {
370 let mut target = target.clone();
371 target.extend(path_components);
372 path = target;
373 continue 'outer;
374 } else {
375 entry_stack.push(entry.clone());
376 real_path.push(name);
377 }
378 } else {
379 return None;
380 }
381 }
382 }
383 }
384 break;
385 }
386 entry_stack.pop().map(|entry| (entry, real_path))
387 }
388
389 async fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
390 where
391 Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
392 {
393 let path = normalize_path(path);
394 let filename = path
395 .file_name()
396 .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
397 let parent_path = path.parent().unwrap();
398
399 let parent = self.read_path(parent_path).await?;
400 let mut parent = parent.lock().await;
401 let new_entry = parent
402 .dir_entries(parent_path)?
403 .entry(filename.to_str().unwrap().into());
404 callback(new_entry)
405 }
406
407 fn emit_event<I, T>(&mut self, paths: I)
408 where
409 I: IntoIterator<Item = T>,
410 T: Into<PathBuf>,
411 {
412 let events = paths
413 .into_iter()
414 .map(|path| fsevent::Event {
415 event_id: 0,
416 flags: fsevent::StreamFlags::empty(),
417 path: path.into(),
418 })
419 .collect::<Vec<_>>();
420
421 self.event_txs.retain(|tx| {
422 let _ = tx.try_send(events.clone());
423 !tx.is_closed()
424 });
425 }
426}
427
428#[cfg(any(test, feature = "test-support"))]
429impl FakeFs {
430 pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
431 Arc::new(Self {
432 executor: Arc::downgrade(&executor),
433 state: Mutex::new(FakeFsState {
434 root: Arc::new(Mutex::new(FakeFsEntry::Dir {
435 inode: 0,
436 mtime: SystemTime::now(),
437 entries: Default::default(),
438 })),
439 next_inode: 1,
440 event_txs: Default::default(),
441 }),
442 })
443 }
444
445 pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
446 let mut state = self.state.lock().await;
447 let path = path.as_ref();
448 let inode = state.next_inode;
449 state.next_inode += 1;
450 let file = Arc::new(Mutex::new(FakeFsEntry::File {
451 inode,
452 mtime: SystemTime::now(),
453 content,
454 }));
455 state
456 .write_path(path, move |entry| {
457 match entry {
458 btree_map::Entry::Vacant(e) => {
459 e.insert(file);
460 }
461 btree_map::Entry::Occupied(mut e) => {
462 *e.get_mut() = file;
463 }
464 }
465 Ok(())
466 })
467 .await
468 .unwrap();
469 state.emit_event(&[path]);
470 }
471
472 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
473 let mut state = self.state.lock().await;
474 let path = path.as_ref();
475 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
476 state
477 .write_path(path.as_ref(), move |e| match e {
478 btree_map::Entry::Vacant(e) => {
479 e.insert(file);
480 Ok(())
481 }
482 btree_map::Entry::Occupied(mut e) => {
483 *e.get_mut() = file;
484 Ok(())
485 }
486 })
487 .await
488 .unwrap();
489 state.emit_event(&[path]);
490 }
491
492 #[must_use]
493 pub fn insert_tree<'a>(
494 &'a self,
495 path: impl 'a + AsRef<Path> + Send,
496 tree: serde_json::Value,
497 ) -> futures::future::BoxFuture<'a, ()> {
498 use futures::FutureExt as _;
499 use serde_json::Value::*;
500
501 async move {
502 let path = path.as_ref();
503
504 match tree {
505 Object(map) => {
506 self.create_dir(path).await.unwrap();
507 for (name, contents) in map {
508 let mut path = PathBuf::from(path);
509 path.push(name);
510 self.insert_tree(&path, contents).await;
511 }
512 }
513 Null => {
514 self.create_dir(path).await.unwrap();
515 }
516 String(contents) => {
517 self.insert_file(&path, contents).await;
518 }
519 _ => {
520 panic!("JSON object must contain only objects, strings, or null");
521 }
522 }
523 }
524 .boxed()
525 }
526
527 pub async fn files(&self) -> Vec<PathBuf> {
528 let mut result = Vec::new();
529 let mut queue = collections::VecDeque::new();
530 queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
531 while let Some((path, entry)) = queue.pop_front() {
532 let e = entry.lock().await;
533 match &*e {
534 FakeFsEntry::File { .. } => result.push(path),
535 FakeFsEntry::Dir { entries, .. } => {
536 for (name, entry) in entries {
537 queue.push_back((path.join(name), entry.clone()));
538 }
539 }
540 FakeFsEntry::Symlink { .. } => {}
541 }
542 }
543 result
544 }
545
546 async fn simulate_random_delay(&self) {
547 self.executor
548 .upgrade()
549 .expect("executor has been dropped")
550 .simulate_random_delay()
551 .await;
552 }
553}
554
555#[cfg(any(test, feature = "test-support"))]
556impl FakeFsEntry {
557 fn is_file(&self) -> bool {
558 matches!(self, Self::File { .. })
559 }
560
561 fn file_content(&self, path: &Path) -> Result<&String> {
562 if let Self::File { content, .. } = self {
563 Ok(content)
564 } else {
565 Err(anyhow!("not a file: {}", path.display()))
566 }
567 }
568
569 fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
570 if let Self::File { content, mtime, .. } = self {
571 *mtime = SystemTime::now();
572 *content = new_content;
573 Ok(())
574 } else {
575 Err(anyhow!("not a file: {}", path.display()))
576 }
577 }
578
579 fn dir_entries(
580 &mut self,
581 path: &Path,
582 ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
583 if let Self::Dir { entries, .. } = self {
584 Ok(entries)
585 } else {
586 Err(anyhow!("not a directory: {}", path.display()))
587 }
588 }
589}
590
591#[cfg(any(test, feature = "test-support"))]
592#[async_trait::async_trait]
593impl Fs for FakeFs {
594 async fn create_dir(&self, path: &Path) -> Result<()> {
595 self.simulate_random_delay().await;
596 let mut state = self.state.lock().await;
597
598 let mut created_dirs = Vec::new();
599 let mut cur_path = PathBuf::new();
600 for component in path.components() {
601 cur_path.push(component);
602 if cur_path == Path::new("/") {
603 continue;
604 }
605
606 let inode = state.next_inode;
607 state.next_inode += 1;
608 state
609 .write_path(&cur_path, |entry| {
610 entry.or_insert_with(|| {
611 created_dirs.push(cur_path.clone());
612 Arc::new(Mutex::new(FakeFsEntry::Dir {
613 inode,
614 mtime: SystemTime::now(),
615 entries: Default::default(),
616 }))
617 });
618 Ok(())
619 })
620 .await?;
621 }
622
623 state.emit_event(&created_dirs);
624 Ok(())
625 }
626
627 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
628 self.simulate_random_delay().await;
629 let mut state = self.state.lock().await;
630 let inode = state.next_inode;
631 state.next_inode += 1;
632 let file = Arc::new(Mutex::new(FakeFsEntry::File {
633 inode,
634 mtime: SystemTime::now(),
635 content: String::new(),
636 }));
637 state
638 .write_path(path, |entry| {
639 match entry {
640 btree_map::Entry::Occupied(mut e) => {
641 if options.overwrite {
642 *e.get_mut() = file;
643 } else if !options.ignore_if_exists {
644 return Err(anyhow!("path already exists: {}", path.display()));
645 }
646 }
647 btree_map::Entry::Vacant(e) => {
648 e.insert(file);
649 }
650 }
651 Ok(())
652 })
653 .await?;
654 state.emit_event(&[path]);
655 Ok(())
656 }
657
658 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
659 let old_path = normalize_path(old_path);
660 let new_path = normalize_path(new_path);
661 let mut state = self.state.lock().await;
662 let moved_entry = state
663 .write_path(&old_path, |e| {
664 if let btree_map::Entry::Occupied(e) = e {
665 Ok(e.remove())
666 } else {
667 Err(anyhow!("path does not exist: {}", &old_path.display()))
668 }
669 })
670 .await?;
671 state
672 .write_path(&new_path, |e| {
673 match e {
674 btree_map::Entry::Occupied(mut e) => {
675 if options.overwrite {
676 *e.get_mut() = moved_entry;
677 } else if !options.ignore_if_exists {
678 return Err(anyhow!("path already exists: {}", new_path.display()));
679 }
680 }
681 btree_map::Entry::Vacant(e) => {
682 e.insert(moved_entry);
683 }
684 }
685 Ok(())
686 })
687 .await?;
688 state.emit_event(&[old_path, new_path]);
689 Ok(())
690 }
691
692 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
693 let source = normalize_path(source);
694 let target = normalize_path(target);
695 let mut state = self.state.lock().await;
696 let source_entry = state.read_path(&source).await?;
697 let content = source_entry.lock().await.file_content(&source)?.clone();
698 let entry = state
699 .write_path(&target, |e| match e {
700 btree_map::Entry::Occupied(e) => {
701 if options.overwrite {
702 Ok(Some(e.get().clone()))
703 } else if !options.ignore_if_exists {
704 return Err(anyhow!("{target:?} already exists"));
705 } else {
706 Ok(None)
707 }
708 }
709 btree_map::Entry::Vacant(e) => Ok(Some(
710 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
711 inode: 0,
712 mtime: SystemTime::now(),
713 content: String::new(),
714 })))
715 .clone(),
716 )),
717 })
718 .await?;
719 if let Some(entry) = entry {
720 entry.lock().await.set_file_content(&target, content)?;
721 }
722 state.emit_event(&[target]);
723 Ok(())
724 }
725
726 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
727 let path = normalize_path(path);
728 let parent_path = path
729 .parent()
730 .ok_or_else(|| anyhow!("cannot remove the root"))?;
731 let base_name = path.file_name().unwrap();
732
733 let state = self.state.lock().await;
734 let parent_entry = state.read_path(parent_path).await?;
735 let mut parent_entry = parent_entry.lock().await;
736 let entry = parent_entry
737 .dir_entries(parent_path)?
738 .entry(base_name.to_str().unwrap().into());
739
740 match entry {
741 btree_map::Entry::Vacant(_) => {
742 if !options.ignore_if_not_exists {
743 return Err(anyhow!("{path:?} does not exist"));
744 }
745 }
746 btree_map::Entry::Occupied(e) => {
747 {
748 let mut entry = e.get().lock().await;
749 let children = entry.dir_entries(&path)?;
750 if !options.recursive && !children.is_empty() {
751 return Err(anyhow!("{path:?} is not empty"));
752 }
753 }
754 e.remove();
755 }
756 }
757
758 Ok(())
759 }
760
761 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
762 let path = normalize_path(path);
763 let parent_path = path
764 .parent()
765 .ok_or_else(|| anyhow!("cannot remove the root"))?;
766 let base_name = path.file_name().unwrap();
767 let mut state = self.state.lock().await;
768 let parent_entry = state.read_path(parent_path).await?;
769 let mut parent_entry = parent_entry.lock().await;
770 let entry = parent_entry
771 .dir_entries(parent_path)?
772 .entry(base_name.to_str().unwrap().into());
773 match entry {
774 btree_map::Entry::Vacant(_) => {
775 if !options.ignore_if_not_exists {
776 return Err(anyhow!("{path:?} does not exist"));
777 }
778 }
779 btree_map::Entry::Occupied(e) => {
780 e.get().lock().await.file_content(&path)?;
781 e.remove();
782 }
783 }
784 state.emit_event(&[path]);
785 Ok(())
786 }
787
788 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
789 let text = self.load(path).await?;
790 Ok(Box::new(io::Cursor::new(text)))
791 }
792
793 async fn load(&self, path: &Path) -> Result<String> {
794 let path = normalize_path(path);
795 self.simulate_random_delay().await;
796 let state = self.state.lock().await;
797 let entry = state.read_path(&path).await?;
798 let entry = entry.lock().await;
799 entry.file_content(&path).cloned()
800 }
801
802 async fn load_head_text(&self, _: &Path) -> Option<String> {
803 None
804 }
805
806 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
807 self.simulate_random_delay().await;
808 let path = normalize_path(path);
809 let content = chunks(text, line_ending).collect();
810 self.insert_file(path, content).await;
811 Ok(())
812 }
813
814 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
815 let path = normalize_path(path);
816 self.simulate_random_delay().await;
817 let state = self.state.lock().await;
818 if let Some((_, real_path)) = state.try_read_path(&path).await {
819 Ok(real_path)
820 } else {
821 Err(anyhow!("path does not exist: {}", path.display()))
822 }
823 }
824
825 async fn is_file(&self, path: &Path) -> bool {
826 let path = normalize_path(path);
827 self.simulate_random_delay().await;
828 let state = self.state.lock().await;
829 if let Some((entry, _)) = state.try_read_path(&path).await {
830 entry.lock().await.is_file()
831 } else {
832 false
833 }
834 }
835
836 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
837 self.simulate_random_delay().await;
838 let path = normalize_path(path);
839 let state = self.state.lock().await;
840 if let Some((entry, real_path)) = state.try_read_path(&path).await {
841 let entry = entry.lock().await;
842 let is_symlink = real_path != path;
843
844 Ok(Some(match &*entry {
845 FakeFsEntry::File { inode, mtime, .. } => Metadata {
846 inode: *inode,
847 mtime: *mtime,
848 is_dir: false,
849 is_symlink,
850 },
851 FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
852 inode: *inode,
853 mtime: *mtime,
854 is_dir: true,
855 is_symlink,
856 },
857 FakeFsEntry::Symlink { .. } => unreachable!(),
858 }))
859 } else {
860 Ok(None)
861 }
862 }
863
864 async fn read_dir(
865 &self,
866 path: &Path,
867 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
868 self.simulate_random_delay().await;
869 let path = normalize_path(path);
870 let state = self.state.lock().await;
871 let entry = state.read_path(&path).await?;
872 let mut entry = entry.lock().await;
873 let children = entry.dir_entries(&path)?;
874 let paths = children
875 .keys()
876 .map(|file_name| Ok(path.join(file_name)))
877 .collect::<Vec<_>>();
878 Ok(Box::pin(futures::stream::iter(paths)))
879 }
880
881 async fn watch(
882 &self,
883 path: &Path,
884 _: Duration,
885 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
886 let mut state = self.state.lock().await;
887 self.simulate_random_delay().await;
888 let (tx, rx) = smol::channel::unbounded();
889 state.event_txs.push(tx);
890 let path = path.to_path_buf();
891 let executor = self.executor.clone();
892 Box::pin(futures::StreamExt::filter(rx, move |events| {
893 let result = events.iter().any(|event| event.path.starts_with(&path));
894 let executor = executor.clone();
895 async move {
896 if let Some(executor) = executor.clone().upgrade() {
897 executor.simulate_random_delay().await;
898 }
899 result
900 }
901 }))
902 }
903
904 fn open_git_repository(
905 &self,
906 abs_dotgit_path: &Path,
907 content_path: &Arc<Path>,
908 ) -> Option<Box<dyn GitRepository>> {
909 Some(Box::new(FakeGitRepository::new(
910 abs_dotgit_path,
911 content_path,
912 )))
913 }
914
915 fn is_fake(&self) -> bool {
916 true
917 }
918
919 #[cfg(any(test, feature = "test-support"))]
920 fn as_fake(&self) -> &FakeFs {
921 self
922 }
923}
924
925fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
926 rope.chunks().flat_map(move |chunk| {
927 let mut newline = false;
928 chunk.split('\n').flat_map(move |line| {
929 let ending = if newline {
930 Some(line_ending.as_str())
931 } else {
932 None
933 };
934 newline = true;
935 ending.into_iter().chain([line])
936 })
937 })
938}
939
940pub fn normalize_path(path: &Path) -> PathBuf {
941 let mut components = path.components().peekable();
942 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
943 components.next();
944 PathBuf::from(c.as_os_str())
945 } else {
946 PathBuf::new()
947 };
948
949 for component in components {
950 match component {
951 Component::Prefix(..) => unreachable!(),
952 Component::RootDir => {
953 ret.push(component.as_os_str());
954 }
955 Component::CurDir => {}
956 Component::ParentDir => {
957 ret.pop();
958 }
959 Component::Normal(c) => {
960 ret.push(c);
961 }
962 }
963 }
964 ret
965}
966
967pub fn copy_recursive<'a>(
968 fs: &'a dyn Fs,
969 source: &'a Path,
970 target: &'a Path,
971 options: CopyOptions,
972) -> BoxFuture<'a, Result<()>> {
973 use futures::future::FutureExt;
974
975 async move {
976 let metadata = fs
977 .metadata(source)
978 .await?
979 .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
980 if metadata.is_dir {
981 if !options.overwrite && fs.metadata(target).await.is_ok() {
982 if options.ignore_if_exists {
983 return Ok(());
984 } else {
985 return Err(anyhow!("{target:?} already exists"));
986 }
987 }
988
989 let _ = fs
990 .remove_dir(
991 target,
992 RemoveOptions {
993 recursive: true,
994 ignore_if_not_exists: true,
995 },
996 )
997 .await;
998 fs.create_dir(target).await?;
999 let mut children = fs.read_dir(source).await?;
1000 while let Some(child_path) = children.next().await {
1001 if let Ok(child_path) = child_path {
1002 if let Some(file_name) = child_path.file_name() {
1003 let child_target_path = target.join(file_name);
1004 copy_recursive(fs, &child_path, &child_target_path, options).await?;
1005 }
1006 }
1007 }
1008
1009 Ok(())
1010 } else {
1011 fs.copy_file(source, target, options).await
1012 }
1013 }
1014 .boxed()
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020 use gpui::TestAppContext;
1021 use serde_json::json;
1022
1023 #[gpui::test]
1024 async fn test_fake_fs(cx: &mut TestAppContext) {
1025 let fs = FakeFs::new(cx.background());
1026
1027 fs.insert_tree(
1028 "/root",
1029 json!({
1030 "dir1": {
1031 "a": "A",
1032 "b": "B"
1033 },
1034 "dir2": {
1035 "c": "C",
1036 "dir3": {
1037 "d": "D"
1038 }
1039 }
1040 }),
1041 )
1042 .await;
1043
1044 assert_eq!(
1045 fs.files().await,
1046 vec![
1047 PathBuf::from("/root/dir1/a"),
1048 PathBuf::from("/root/dir1/b"),
1049 PathBuf::from("/root/dir2/c"),
1050 PathBuf::from("/root/dir2/dir3/d"),
1051 ]
1052 );
1053
1054 fs.insert_symlink("/root/dir2/link-to-dir3", "./dir3".into())
1055 .await;
1056
1057 assert_eq!(
1058 fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
1059 .await
1060 .unwrap(),
1061 PathBuf::from("/root/dir2/dir3"),
1062 );
1063 assert_eq!(
1064 fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
1065 .await
1066 .unwrap(),
1067 PathBuf::from("/root/dir2/dir3/d"),
1068 );
1069 assert_eq!(
1070 fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
1071 "D",
1072 );
1073 }
1074}