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