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