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