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