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