1use anyhow::{anyhow, Result};
2use git::GitHostingProviderRegistry;
3
4#[cfg(target_os = "linux")]
5use ashpd::desktop::trash;
6#[cfg(target_os = "linux")]
7use std::{fs::File, os::fd::AsFd};
8
9#[cfg(unix)]
10use std::os::unix::fs::MetadataExt;
11
12use async_tar::Archive;
13use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
14use git::repository::{GitRepository, RealGitRepository};
15use gpui::{AppContext, Global, ReadGlobal};
16use rope::Rope;
17use smol::io::AsyncWriteExt;
18use std::{
19 io::{self, Write},
20 path::{Component, Path, PathBuf},
21 pin::Pin,
22 sync::Arc,
23 time::{Duration, SystemTime},
24};
25use tempfile::{NamedTempFile, TempDir};
26use text::LineEnding;
27use util::ResultExt;
28
29#[cfg(any(test, feature = "test-support"))]
30use collections::{btree_map, BTreeMap};
31#[cfg(any(test, feature = "test-support"))]
32use git::repository::{FakeGitRepositoryState, GitFileStatus};
33#[cfg(any(test, feature = "test-support"))]
34use parking_lot::Mutex;
35#[cfg(any(test, feature = "test-support"))]
36use smol::io::AsyncReadExt;
37#[cfg(any(test, feature = "test-support"))]
38use std::ffi::OsStr;
39
40pub trait Watcher: Send + Sync {
41 fn add(&self, path: &Path) -> Result<()>;
42 fn remove(&self, path: &Path) -> Result<()>;
43}
44
45#[async_trait::async_trait]
46pub trait Fs: Send + Sync {
47 async fn create_dir(&self, path: &Path) -> Result<()>;
48 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>;
49 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
50 async fn create_file_with(
51 &self,
52 path: &Path,
53 content: Pin<&mut (dyn AsyncRead + Send)>,
54 ) -> Result<()>;
55 async fn extract_tar_file(
56 &self,
57 path: &Path,
58 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
59 ) -> Result<()>;
60 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
61 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
62 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
63 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
64 self.remove_dir(path, options).await
65 }
66 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
67 async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
68 self.remove_file(path, options).await
69 }
70 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
71 async fn load(&self, path: &Path) -> Result<String> {
72 Ok(String::from_utf8(self.load_bytes(path).await?)?)
73 }
74 async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
75 async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
76 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
77 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
78 async fn is_file(&self, path: &Path) -> bool;
79 async fn is_dir(&self, path: &Path) -> bool;
80 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
81 async fn read_link(&self, path: &Path) -> Result<PathBuf>;
82 async fn read_dir(
83 &self,
84 path: &Path,
85 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
86
87 async fn watch(
88 &self,
89 path: &Path,
90 latency: Duration,
91 ) -> (
92 Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
93 Arc<dyn Watcher>,
94 );
95
96 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
97 fn is_fake(&self) -> bool;
98 async fn is_case_sensitive(&self) -> Result<bool>;
99
100 #[cfg(any(test, feature = "test-support"))]
101 fn as_fake(&self) -> &FakeFs {
102 panic!("called as_fake on a real fs");
103 }
104}
105
106struct GlobalFs(Arc<dyn Fs>);
107
108impl Global for GlobalFs {}
109
110impl dyn Fs {
111 /// Returns the global [`Fs`].
112 pub fn global(cx: &AppContext) -> Arc<Self> {
113 GlobalFs::global(cx).0.clone()
114 }
115
116 /// Sets the global [`Fs`].
117 pub fn set_global(fs: Arc<Self>, cx: &mut AppContext) {
118 cx.set_global(GlobalFs(fs));
119 }
120}
121
122#[derive(Copy, Clone, Default)]
123pub struct CreateOptions {
124 pub overwrite: bool,
125 pub ignore_if_exists: bool,
126}
127
128#[derive(Copy, Clone, Default)]
129pub struct CopyOptions {
130 pub overwrite: bool,
131 pub ignore_if_exists: bool,
132}
133
134#[derive(Copy, Clone, Default)]
135pub struct RenameOptions {
136 pub overwrite: bool,
137 pub ignore_if_exists: bool,
138}
139
140#[derive(Copy, Clone, Default)]
141pub struct RemoveOptions {
142 pub recursive: bool,
143 pub ignore_if_not_exists: bool,
144}
145
146#[derive(Copy, Clone, Debug)]
147pub struct Metadata {
148 pub inode: u64,
149 pub mtime: SystemTime,
150 pub is_symlink: bool,
151 pub is_dir: bool,
152}
153
154#[derive(Default)]
155pub struct RealFs {
156 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
157 git_binary_path: Option<PathBuf>,
158}
159
160pub struct RealWatcher {}
161
162impl RealFs {
163 pub fn new(
164 git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
165 git_binary_path: Option<PathBuf>,
166 ) -> Self {
167 Self {
168 git_hosting_provider_registry,
169 git_binary_path,
170 }
171 }
172}
173
174#[async_trait::async_trait]
175impl Fs for RealFs {
176 async fn create_dir(&self, path: &Path) -> Result<()> {
177 Ok(smol::fs::create_dir_all(path).await?)
178 }
179
180 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
181 #[cfg(unix)]
182 smol::fs::unix::symlink(target, path).await?;
183
184 #[cfg(windows)]
185 if smol::fs::metadata(&target).await?.is_dir() {
186 smol::fs::windows::symlink_dir(target, path).await?
187 } else {
188 smol::fs::windows::symlink_file(target, path).await?
189 }
190
191 Ok(())
192 }
193
194 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
195 let mut open_options = smol::fs::OpenOptions::new();
196 open_options.write(true).create(true);
197 if options.overwrite {
198 open_options.truncate(true);
199 } else if !options.ignore_if_exists {
200 open_options.create_new(true);
201 }
202 open_options.open(path).await?;
203 Ok(())
204 }
205
206 async fn create_file_with(
207 &self,
208 path: &Path,
209 content: Pin<&mut (dyn AsyncRead + Send)>,
210 ) -> Result<()> {
211 let mut file = smol::fs::File::create(&path).await?;
212 futures::io::copy(content, &mut file).await?;
213 Ok(())
214 }
215
216 async fn extract_tar_file(
217 &self,
218 path: &Path,
219 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
220 ) -> Result<()> {
221 content.unpack(path).await?;
222 Ok(())
223 }
224
225 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
226 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
227 if options.ignore_if_exists {
228 return Ok(());
229 } else {
230 return Err(anyhow!("{target:?} already exists"));
231 }
232 }
233
234 smol::fs::copy(source, target).await?;
235 Ok(())
236 }
237
238 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
239 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
240 if options.ignore_if_exists {
241 return Ok(());
242 } else {
243 return Err(anyhow!("{target:?} already exists"));
244 }
245 }
246
247 smol::fs::rename(source, target).await?;
248 Ok(())
249 }
250
251 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
252 let result = if options.recursive {
253 smol::fs::remove_dir_all(path).await
254 } else {
255 smol::fs::remove_dir(path).await
256 };
257 match result {
258 Ok(()) => Ok(()),
259 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
260 Ok(())
261 }
262 Err(err) => Err(err)?,
263 }
264 }
265
266 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
267 #[cfg(windows)]
268 if let Ok(Some(metadata)) = self.metadata(path).await {
269 if metadata.is_symlink && metadata.is_dir {
270 self.remove_dir(
271 path,
272 RemoveOptions {
273 recursive: false,
274 ignore_if_not_exists: true,
275 },
276 )
277 .await?;
278 return Ok(());
279 }
280 }
281
282 match smol::fs::remove_file(path).await {
283 Ok(()) => Ok(()),
284 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
285 Ok(())
286 }
287 Err(err) => Err(err)?,
288 }
289 }
290
291 #[cfg(target_os = "macos")]
292 async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
293 use cocoa::{
294 base::{id, nil},
295 foundation::{NSAutoreleasePool, NSString},
296 };
297 use objc::{class, msg_send, sel, sel_impl};
298
299 unsafe {
300 unsafe fn ns_string(string: &str) -> id {
301 NSString::alloc(nil).init_str(string).autorelease()
302 }
303
304 let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
305 let array: id = msg_send![class!(NSArray), arrayWithObject: url];
306 let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
307
308 let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
309 }
310 Ok(())
311 }
312
313 #[cfg(target_os = "linux")]
314 async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
315 let file = File::open(path)?;
316 match trash::trash_file(&file.as_fd()).await {
317 Ok(_) => Ok(()),
318 Err(err) => Err(anyhow::Error::new(err)),
319 }
320 }
321
322 #[cfg(target_os = "macos")]
323 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
324 self.trash_file(path, options).await
325 }
326
327 #[cfg(target_os = "linux")]
328 async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
329 self.trash_file(path, options).await
330 }
331
332 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
333 Ok(Box::new(std::fs::File::open(path)?))
334 }
335
336 async fn load(&self, path: &Path) -> Result<String> {
337 let path = path.to_path_buf();
338 let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
339 Ok(text)
340 }
341 async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
342 let path = path.to_path_buf();
343 let bytes = smol::unblock(|| std::fs::read(path)).await?;
344 Ok(bytes)
345 }
346
347 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
348 smol::unblock(move || {
349 let mut tmp_file = if cfg!(target_os = "linux") {
350 // Use the directory of the destination as temp dir to avoid
351 // invalid cross-device link error, and XDG_CACHE_DIR for fallback.
352 // See https://github.com/zed-industries/zed/pull/8437 for more details.
353 NamedTempFile::new_in(path.parent().unwrap_or(&paths::temp_dir()))
354 } else {
355 NamedTempFile::new()
356 }?;
357 tmp_file.write_all(data.as_bytes())?;
358 tmp_file.persist(path)?;
359 Ok::<(), anyhow::Error>(())
360 })
361 .await?;
362
363 Ok(())
364 }
365
366 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
367 let buffer_size = text.summary().len.min(10 * 1024);
368 if let Some(path) = path.parent() {
369 self.create_dir(path).await?;
370 }
371 let file = smol::fs::File::create(path).await?;
372 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
373 for chunk in chunks(text, line_ending) {
374 writer.write_all(chunk.as_bytes()).await?;
375 }
376 writer.flush().await?;
377 Ok(())
378 }
379
380 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
381 Ok(smol::fs::canonicalize(path).await?)
382 }
383
384 async fn is_file(&self, path: &Path) -> bool {
385 smol::fs::metadata(path)
386 .await
387 .map_or(false, |metadata| metadata.is_file())
388 }
389
390 async fn is_dir(&self, path: &Path) -> bool {
391 smol::fs::metadata(path)
392 .await
393 .map_or(false, |metadata| metadata.is_dir())
394 }
395
396 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
397 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
398 Ok(metadata) => metadata,
399 Err(err) => {
400 return match (err.kind(), err.raw_os_error()) {
401 (io::ErrorKind::NotFound, _) => Ok(None),
402 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
403 _ => Err(anyhow::Error::new(err)),
404 }
405 }
406 };
407
408 let is_symlink = symlink_metadata.file_type().is_symlink();
409 let metadata = if is_symlink {
410 smol::fs::metadata(path).await?
411 } else {
412 symlink_metadata
413 };
414
415 #[cfg(unix)]
416 let inode = metadata.ino();
417
418 #[cfg(windows)]
419 let inode = file_id(path).await?;
420
421 Ok(Some(Metadata {
422 inode,
423 mtime: metadata.modified().unwrap(),
424 is_symlink,
425 is_dir: metadata.file_type().is_dir(),
426 }))
427 }
428
429 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
430 let path = smol::fs::read_link(path).await?;
431 Ok(path)
432 }
433
434 async fn read_dir(
435 &self,
436 path: &Path,
437 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
438 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
439 Ok(entry) => Ok(entry.path()),
440 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
441 });
442 Ok(Box::pin(result))
443 }
444
445 #[cfg(target_os = "macos")]
446 async fn watch(
447 &self,
448 path: &Path,
449 latency: Duration,
450 ) -> (
451 Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
452 Arc<dyn Watcher>,
453 ) {
454 use fsevent::EventStream;
455
456 let (tx, rx) = smol::channel::unbounded();
457 let (stream, handle) = EventStream::new(&[path], latency);
458 std::thread::spawn(move || {
459 stream.run(move |events| {
460 smol::block_on(tx.send(events.into_iter().map(|event| event.path).collect()))
461 .is_ok()
462 });
463 });
464
465 (
466 Box::pin(rx.chain(futures::stream::once(async move {
467 drop(handle);
468 vec![]
469 }))),
470 Arc::new(RealWatcher {}),
471 )
472 }
473
474 #[cfg(target_os = "linux")]
475 async fn watch(
476 &self,
477 path: &Path,
478 latency: Duration,
479 ) -> (
480 Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
481 Arc<dyn Watcher>,
482 ) {
483 use parking_lot::Mutex;
484
485 let (tx, rx) = smol::channel::unbounded();
486 let pending_paths: Arc<Mutex<Vec<PathBuf>>> = Default::default();
487 let root_path = path.to_path_buf();
488
489 watcher::global(|g| {
490 let tx = tx.clone();
491 let pending_paths = pending_paths.clone();
492 g.add(move |event: ¬ify::Event| {
493 let mut paths = event
494 .paths
495 .iter()
496 .filter(|path| path.starts_with(&root_path))
497 .cloned()
498 .collect::<Vec<_>>();
499 if !paths.is_empty() {
500 paths.sort();
501 let mut pending_paths = pending_paths.lock();
502 if pending_paths.is_empty() {
503 tx.try_send(()).ok();
504 }
505 util::extend_sorted(&mut *pending_paths, paths, usize::MAX, PathBuf::cmp);
506 }
507 })
508 })
509 .log_err();
510
511 let watcher = Arc::new(RealWatcher {});
512
513 watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
514
515 // watch the parent dir so we can tell when settings.json is created
516 if let Some(parent) = path.parent() {
517 watcher.add(parent).log_err();
518 }
519
520 (
521 Box::pin(rx.filter_map({
522 let watcher = watcher.clone();
523 move |_| {
524 let _ = watcher.clone();
525 let pending_paths = pending_paths.clone();
526 async move {
527 smol::Timer::after(latency).await;
528 let paths = std::mem::take(&mut *pending_paths.lock());
529 (!paths.is_empty()).then_some(paths)
530 }
531 }
532 })),
533 watcher,
534 )
535 }
536
537 #[cfg(target_os = "windows")]
538 async fn watch(
539 &self,
540 path: &Path,
541 _latency: Duration,
542 ) -> (
543 Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
544 Arc<dyn Watcher>,
545 ) {
546 use notify::Watcher;
547
548 let (tx, rx) = smol::channel::unbounded();
549
550 let mut file_watcher = notify::recommended_watcher({
551 let tx = tx.clone();
552 move |event: Result<notify::Event, _>| {
553 if let Some(event) = event.log_err() {
554 tx.try_send(event.paths).ok();
555 }
556 }
557 })
558 .expect("Could not start file watcher");
559
560 file_watcher
561 .watch(path, notify::RecursiveMode::Recursive)
562 .log_err();
563
564 (
565 Box::pin(rx.chain(futures::stream::once(async move {
566 drop(file_watcher);
567 vec![]
568 }))),
569 Arc::new(RealWatcher {}),
570 )
571 }
572
573 fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
574 let repo = git2::Repository::open(dotgit_path).log_err()?;
575 Some(Arc::new(RealGitRepository::new(
576 repo,
577 self.git_binary_path.clone(),
578 self.git_hosting_provider_registry.clone(),
579 )))
580 }
581
582 fn is_fake(&self) -> bool {
583 false
584 }
585
586 /// Checks whether the file system is case sensitive by attempting to create two files
587 /// that have the same name except for the casing.
588 ///
589 /// It creates both files in a temporary directory it removes at the end.
590 async fn is_case_sensitive(&self) -> Result<bool> {
591 let temp_dir = TempDir::new()?;
592 let test_file_1 = temp_dir.path().join("case_sensitivity_test.tmp");
593 let test_file_2 = temp_dir.path().join("CASE_SENSITIVITY_TEST.TMP");
594
595 let create_opts = CreateOptions {
596 overwrite: false,
597 ignore_if_exists: false,
598 };
599
600 // Create file1
601 self.create_file(&test_file_1, create_opts).await?;
602
603 // Now check whether it's possible to create file2
604 let case_sensitive = match self.create_file(&test_file_2, create_opts).await {
605 Ok(_) => Ok(true),
606 Err(e) => {
607 if let Some(io_error) = e.downcast_ref::<io::Error>() {
608 if io_error.kind() == io::ErrorKind::AlreadyExists {
609 Ok(false)
610 } else {
611 Err(e)
612 }
613 } else {
614 Err(e)
615 }
616 }
617 };
618
619 temp_dir.close()?;
620 case_sensitive
621 }
622}
623
624#[cfg(not(target_os = "linux"))]
625impl Watcher for RealWatcher {
626 fn add(&self, _: &Path) -> Result<()> {
627 Ok(())
628 }
629
630 fn remove(&self, _: &Path) -> Result<()> {
631 Ok(())
632 }
633}
634
635#[cfg(target_os = "linux")]
636impl Watcher for RealWatcher {
637 fn add(&self, path: &Path) -> Result<()> {
638 use notify::Watcher;
639 Ok(watcher::global(|w| {
640 w.inotify
641 .lock()
642 .watch(path, notify::RecursiveMode::NonRecursive)
643 })??)
644 }
645
646 fn remove(&self, path: &Path) -> Result<()> {
647 use notify::Watcher;
648 Ok(watcher::global(|w| w.inotify.lock().unwatch(path))??)
649 }
650}
651
652#[cfg(any(test, feature = "test-support"))]
653pub struct FakeFs {
654 // Use an unfair lock to ensure tests are deterministic.
655 state: Mutex<FakeFsState>,
656 executor: gpui::BackgroundExecutor,
657}
658
659#[cfg(any(test, feature = "test-support"))]
660struct FakeFsState {
661 root: Arc<Mutex<FakeFsEntry>>,
662 next_inode: u64,
663 next_mtime: SystemTime,
664 event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
665 events_paused: bool,
666 buffered_events: Vec<PathBuf>,
667 metadata_call_count: usize,
668 read_dir_call_count: usize,
669}
670
671#[cfg(any(test, feature = "test-support"))]
672#[derive(Debug)]
673enum FakeFsEntry {
674 File {
675 inode: u64,
676 mtime: SystemTime,
677 content: Vec<u8>,
678 },
679 Dir {
680 inode: u64,
681 mtime: SystemTime,
682 entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
683 git_repo_state: Option<Arc<Mutex<git::repository::FakeGitRepositoryState>>>,
684 },
685 Symlink {
686 target: PathBuf,
687 },
688}
689
690#[cfg(any(test, feature = "test-support"))]
691impl FakeFsState {
692 fn read_path(&self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
693 Ok(self
694 .try_read_path(target, true)
695 .ok_or_else(|| {
696 anyhow!(io::Error::new(
697 io::ErrorKind::NotFound,
698 format!("not found: {}", target.display())
699 ))
700 })?
701 .0)
702 }
703
704 fn try_read_path(
705 &self,
706 target: &Path,
707 follow_symlink: bool,
708 ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
709 let mut path = target.to_path_buf();
710 let mut canonical_path = PathBuf::new();
711 let mut entry_stack = Vec::new();
712 'outer: loop {
713 let mut path_components = path.components().peekable();
714 while let Some(component) = path_components.next() {
715 match component {
716 Component::Prefix(_) => panic!("prefix paths aren't supported"),
717 Component::RootDir => {
718 entry_stack.clear();
719 entry_stack.push(self.root.clone());
720 canonical_path.clear();
721 canonical_path.push("/");
722 }
723 Component::CurDir => {}
724 Component::ParentDir => {
725 entry_stack.pop()?;
726 canonical_path.pop();
727 }
728 Component::Normal(name) => {
729 let current_entry = entry_stack.last().cloned()?;
730 let current_entry = current_entry.lock();
731 if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
732 let entry = entries.get(name.to_str().unwrap()).cloned()?;
733 if path_components.peek().is_some() || follow_symlink {
734 let entry = entry.lock();
735 if let FakeFsEntry::Symlink { target, .. } = &*entry {
736 let mut target = target.clone();
737 target.extend(path_components);
738 path = target;
739 continue 'outer;
740 }
741 }
742 entry_stack.push(entry.clone());
743 canonical_path.push(name);
744 } else {
745 return None;
746 }
747 }
748 }
749 }
750 break;
751 }
752 Some((entry_stack.pop()?, canonical_path))
753 }
754
755 fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
756 where
757 Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
758 {
759 let path = normalize_path(path);
760 let filename = path
761 .file_name()
762 .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
763 let parent_path = path.parent().unwrap();
764
765 let parent = self.read_path(parent_path)?;
766 let mut parent = parent.lock();
767 let new_entry = parent
768 .dir_entries(parent_path)?
769 .entry(filename.to_str().unwrap().into());
770 callback(new_entry)
771 }
772
773 fn emit_event<I, T>(&mut self, paths: I)
774 where
775 I: IntoIterator<Item = T>,
776 T: Into<PathBuf>,
777 {
778 self.buffered_events
779 .extend(paths.into_iter().map(Into::into));
780
781 if !self.events_paused {
782 self.flush_events(self.buffered_events.len());
783 }
784 }
785
786 fn flush_events(&mut self, mut count: usize) {
787 count = count.min(self.buffered_events.len());
788 let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
789 self.event_txs.retain(|tx| {
790 let _ = tx.try_send(events.clone());
791 !tx.is_closed()
792 });
793 }
794}
795
796#[cfg(any(test, feature = "test-support"))]
797lazy_static::lazy_static! {
798 pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
799}
800
801#[cfg(any(test, feature = "test-support"))]
802impl FakeFs {
803 pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
804 Arc::new(Self {
805 executor,
806 state: Mutex::new(FakeFsState {
807 root: Arc::new(Mutex::new(FakeFsEntry::Dir {
808 inode: 0,
809 mtime: SystemTime::UNIX_EPOCH,
810 entries: Default::default(),
811 git_repo_state: None,
812 })),
813 next_mtime: SystemTime::UNIX_EPOCH,
814 next_inode: 1,
815 event_txs: Default::default(),
816 buffered_events: Vec::new(),
817 events_paused: false,
818 read_dir_call_count: 0,
819 metadata_call_count: 0,
820 }),
821 })
822 }
823
824 pub fn set_next_mtime(&self, next_mtime: SystemTime) {
825 let mut state = self.state.lock();
826 state.next_mtime = next_mtime;
827 }
828
829 pub async fn touch_path(&self, path: impl AsRef<Path>) {
830 let mut state = self.state.lock();
831 let path = path.as_ref();
832 let new_mtime = state.next_mtime;
833 let new_inode = state.next_inode;
834 state.next_inode += 1;
835 state.next_mtime += Duration::from_nanos(1);
836 state
837 .write_path(path, move |entry| {
838 match entry {
839 btree_map::Entry::Vacant(e) => {
840 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
841 inode: new_inode,
842 mtime: new_mtime,
843 content: Vec::new(),
844 })));
845 }
846 btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut().lock() {
847 FakeFsEntry::File { mtime, .. } => *mtime = new_mtime,
848 FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime,
849 FakeFsEntry::Symlink { .. } => {}
850 },
851 }
852 Ok(())
853 })
854 .unwrap();
855 state.emit_event([path.to_path_buf()]);
856 }
857
858 pub async fn insert_file(&self, path: impl AsRef<Path>, content: Vec<u8>) {
859 self.write_file_internal(path, content).unwrap()
860 }
861
862 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
863 let mut state = self.state.lock();
864 let path = path.as_ref();
865 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
866 state
867 .write_path(path.as_ref(), move |e| match e {
868 btree_map::Entry::Vacant(e) => {
869 e.insert(file);
870 Ok(())
871 }
872 btree_map::Entry::Occupied(mut e) => {
873 *e.get_mut() = file;
874 Ok(())
875 }
876 })
877 .unwrap();
878 state.emit_event([path]);
879 }
880
881 fn write_file_internal(&self, path: impl AsRef<Path>, content: Vec<u8>) -> Result<()> {
882 let mut state = self.state.lock();
883 let path = path.as_ref();
884 let inode = state.next_inode;
885 let mtime = state.next_mtime;
886 state.next_inode += 1;
887 state.next_mtime += Duration::from_nanos(1);
888 let file = Arc::new(Mutex::new(FakeFsEntry::File {
889 inode,
890 mtime,
891 content,
892 }));
893 state.write_path(path, move |entry| {
894 match entry {
895 btree_map::Entry::Vacant(e) => {
896 e.insert(file);
897 }
898 btree_map::Entry::Occupied(mut e) => {
899 *e.get_mut() = file;
900 }
901 }
902 Ok(())
903 })?;
904 state.emit_event([path]);
905 Ok(())
906 }
907
908 pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
909 let path = path.as_ref();
910 let path = normalize_path(path);
911 let state = self.state.lock();
912 let entry = state.read_path(&path)?;
913 let entry = entry.lock();
914 entry.file_content(&path).cloned()
915 }
916
917 async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
918 let path = path.as_ref();
919 let path = normalize_path(path);
920 self.simulate_random_delay().await;
921 let state = self.state.lock();
922 let entry = state.read_path(&path)?;
923 let entry = entry.lock();
924 entry.file_content(&path).cloned()
925 }
926
927 pub fn pause_events(&self) {
928 self.state.lock().events_paused = true;
929 }
930
931 pub fn buffered_event_count(&self) -> usize {
932 self.state.lock().buffered_events.len()
933 }
934
935 pub fn flush_events(&self, count: usize) {
936 self.state.lock().flush_events(count);
937 }
938
939 #[must_use]
940 pub fn insert_tree<'a>(
941 &'a self,
942 path: impl 'a + AsRef<Path> + Send,
943 tree: serde_json::Value,
944 ) -> futures::future::BoxFuture<'a, ()> {
945 use futures::FutureExt as _;
946 use serde_json::Value::*;
947
948 async move {
949 let path = path.as_ref();
950
951 match tree {
952 Object(map) => {
953 self.create_dir(path).await.unwrap();
954 for (name, contents) in map {
955 let mut path = PathBuf::from(path);
956 path.push(name);
957 self.insert_tree(&path, contents).await;
958 }
959 }
960 Null => {
961 self.create_dir(path).await.unwrap();
962 }
963 String(contents) => {
964 self.insert_file(&path, contents.into_bytes()).await;
965 }
966 _ => {
967 panic!("JSON object must contain only objects, strings, or null");
968 }
969 }
970 }
971 .boxed()
972 }
973
974 pub fn insert_tree_from_real_fs<'a>(
975 &'a self,
976 path: impl 'a + AsRef<Path> + Send,
977 src_path: impl 'a + AsRef<Path> + Send,
978 ) -> futures::future::BoxFuture<'a, ()> {
979 use futures::FutureExt as _;
980
981 async move {
982 let path = path.as_ref();
983 if std::fs::metadata(&src_path).unwrap().is_file() {
984 let contents = std::fs::read(src_path).unwrap();
985 self.insert_file(path, contents).await;
986 } else {
987 self.create_dir(path).await.unwrap();
988 for entry in std::fs::read_dir(&src_path).unwrap() {
989 let entry = entry.unwrap();
990 self.insert_tree_from_real_fs(&path.join(entry.file_name()), &entry.path())
991 .await;
992 }
993 }
994 }
995 .boxed()
996 }
997
998 pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
999 where
1000 F: FnOnce(&mut FakeGitRepositoryState),
1001 {
1002 let mut state = self.state.lock();
1003 let entry = state.read_path(dot_git).unwrap();
1004 let mut entry = entry.lock();
1005
1006 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
1007 let repo_state = git_repo_state.get_or_insert_with(Default::default);
1008 let mut repo_state = repo_state.lock();
1009
1010 f(&mut repo_state);
1011
1012 if emit_git_event {
1013 state.emit_event([dot_git]);
1014 }
1015 } else {
1016 panic!("not a directory");
1017 }
1018 }
1019
1020 pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
1021 self.with_git_state(dot_git, true, |state| {
1022 state.branch_name = branch.map(Into::into)
1023 })
1024 }
1025
1026 pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
1027 self.with_git_state(dot_git, true, |state| {
1028 state.index_contents.clear();
1029 state.index_contents.extend(
1030 head_state
1031 .iter()
1032 .map(|(path, content)| (path.to_path_buf(), content.clone())),
1033 );
1034 });
1035 }
1036
1037 pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
1038 self.with_git_state(dot_git, true, |state| {
1039 state.blames.clear();
1040 state.blames.extend(
1041 blames
1042 .into_iter()
1043 .map(|(path, blame)| (path.to_path_buf(), blame)),
1044 );
1045 });
1046 }
1047
1048 pub fn set_status_for_repo_via_working_copy_change(
1049 &self,
1050 dot_git: &Path,
1051 statuses: &[(&Path, GitFileStatus)],
1052 ) {
1053 self.with_git_state(dot_git, false, |state| {
1054 state.worktree_statuses.clear();
1055 state.worktree_statuses.extend(
1056 statuses
1057 .iter()
1058 .map(|(path, content)| ((**path).into(), *content)),
1059 );
1060 });
1061 self.state.lock().emit_event(
1062 statuses
1063 .iter()
1064 .map(|(path, _)| dot_git.parent().unwrap().join(path)),
1065 );
1066 }
1067
1068 pub fn set_status_for_repo_via_git_operation(
1069 &self,
1070 dot_git: &Path,
1071 statuses: &[(&Path, GitFileStatus)],
1072 ) {
1073 self.with_git_state(dot_git, true, |state| {
1074 state.worktree_statuses.clear();
1075 state.worktree_statuses.extend(
1076 statuses
1077 .iter()
1078 .map(|(path, content)| ((**path).into(), *content)),
1079 );
1080 });
1081 }
1082
1083 pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
1084 let mut result = Vec::new();
1085 let mut queue = collections::VecDeque::new();
1086 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
1087 while let Some((path, entry)) = queue.pop_front() {
1088 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
1089 for (name, entry) in entries {
1090 queue.push_back((path.join(name), entry.clone()));
1091 }
1092 }
1093 if include_dot_git
1094 || !path
1095 .components()
1096 .any(|component| component.as_os_str() == *FS_DOT_GIT)
1097 {
1098 result.push(path);
1099 }
1100 }
1101 result
1102 }
1103
1104 pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
1105 let mut result = Vec::new();
1106 let mut queue = collections::VecDeque::new();
1107 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
1108 while let Some((path, entry)) = queue.pop_front() {
1109 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
1110 for (name, entry) in entries {
1111 queue.push_back((path.join(name), entry.clone()));
1112 }
1113 if include_dot_git
1114 || !path
1115 .components()
1116 .any(|component| component.as_os_str() == *FS_DOT_GIT)
1117 {
1118 result.push(path);
1119 }
1120 }
1121 }
1122 result
1123 }
1124
1125 pub fn files(&self) -> Vec<PathBuf> {
1126 let mut result = Vec::new();
1127 let mut queue = collections::VecDeque::new();
1128 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
1129 while let Some((path, entry)) = queue.pop_front() {
1130 let e = entry.lock();
1131 match &*e {
1132 FakeFsEntry::File { .. } => result.push(path),
1133 FakeFsEntry::Dir { entries, .. } => {
1134 for (name, entry) in entries {
1135 queue.push_back((path.join(name), entry.clone()));
1136 }
1137 }
1138 FakeFsEntry::Symlink { .. } => {}
1139 }
1140 }
1141 result
1142 }
1143
1144 /// How many `read_dir` calls have been issued.
1145 pub fn read_dir_call_count(&self) -> usize {
1146 self.state.lock().read_dir_call_count
1147 }
1148
1149 /// How many `metadata` calls have been issued.
1150 pub fn metadata_call_count(&self) -> usize {
1151 self.state.lock().metadata_call_count
1152 }
1153
1154 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
1155 self.executor.simulate_random_delay()
1156 }
1157}
1158
1159#[cfg(any(test, feature = "test-support"))]
1160impl FakeFsEntry {
1161 fn is_file(&self) -> bool {
1162 matches!(self, Self::File { .. })
1163 }
1164
1165 fn is_symlink(&self) -> bool {
1166 matches!(self, Self::Symlink { .. })
1167 }
1168
1169 fn file_content(&self, path: &Path) -> Result<&Vec<u8>> {
1170 if let Self::File { content, .. } = self {
1171 Ok(content)
1172 } else {
1173 Err(anyhow!("not a file: {}", path.display()))
1174 }
1175 }
1176
1177 fn set_file_content(&mut self, path: &Path, new_content: Vec<u8>) -> Result<()> {
1178 if let Self::File { content, mtime, .. } = self {
1179 *mtime = SystemTime::now();
1180 *content = new_content;
1181 Ok(())
1182 } else {
1183 Err(anyhow!("not a file: {}", path.display()))
1184 }
1185 }
1186
1187 fn dir_entries(
1188 &mut self,
1189 path: &Path,
1190 ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
1191 if let Self::Dir { entries, .. } = self {
1192 Ok(entries)
1193 } else {
1194 Err(anyhow!("not a directory: {}", path.display()))
1195 }
1196 }
1197}
1198
1199#[cfg(any(test, feature = "test-support"))]
1200struct FakeWatcher {}
1201
1202#[cfg(any(test, feature = "test-support"))]
1203impl Watcher for FakeWatcher {
1204 fn add(&self, _: &Path) -> Result<()> {
1205 Ok(())
1206 }
1207
1208 fn remove(&self, _: &Path) -> Result<()> {
1209 Ok(())
1210 }
1211}
1212
1213#[cfg(any(test, feature = "test-support"))]
1214#[async_trait::async_trait]
1215impl Fs for FakeFs {
1216 async fn create_dir(&self, path: &Path) -> Result<()> {
1217 self.simulate_random_delay().await;
1218
1219 let mut created_dirs = Vec::new();
1220 let mut cur_path = PathBuf::new();
1221 for component in path.components() {
1222 let mut state = self.state.lock();
1223 cur_path.push(component);
1224 if cur_path == Path::new("/") {
1225 continue;
1226 }
1227
1228 let inode = state.next_inode;
1229 let mtime = state.next_mtime;
1230 state.next_mtime += Duration::from_nanos(1);
1231 state.next_inode += 1;
1232 state.write_path(&cur_path, |entry| {
1233 entry.or_insert_with(|| {
1234 created_dirs.push(cur_path.clone());
1235 Arc::new(Mutex::new(FakeFsEntry::Dir {
1236 inode,
1237 mtime,
1238 entries: Default::default(),
1239 git_repo_state: None,
1240 }))
1241 });
1242 Ok(())
1243 })?
1244 }
1245
1246 self.state.lock().emit_event(&created_dirs);
1247 Ok(())
1248 }
1249
1250 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
1251 self.simulate_random_delay().await;
1252 let mut state = self.state.lock();
1253 let inode = state.next_inode;
1254 let mtime = state.next_mtime;
1255 state.next_mtime += Duration::from_nanos(1);
1256 state.next_inode += 1;
1257 let file = Arc::new(Mutex::new(FakeFsEntry::File {
1258 inode,
1259 mtime,
1260 content: Vec::new(),
1261 }));
1262 state.write_path(path, |entry| {
1263 match entry {
1264 btree_map::Entry::Occupied(mut e) => {
1265 if options.overwrite {
1266 *e.get_mut() = file;
1267 } else if !options.ignore_if_exists {
1268 return Err(anyhow!("path already exists: {}", path.display()));
1269 }
1270 }
1271 btree_map::Entry::Vacant(e) => {
1272 e.insert(file);
1273 }
1274 }
1275 Ok(())
1276 })?;
1277 state.emit_event([path]);
1278 Ok(())
1279 }
1280
1281 async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> {
1282 let mut state = self.state.lock();
1283 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
1284 state
1285 .write_path(path.as_ref(), move |e| match e {
1286 btree_map::Entry::Vacant(e) => {
1287 e.insert(file);
1288 Ok(())
1289 }
1290 btree_map::Entry::Occupied(mut e) => {
1291 *e.get_mut() = file;
1292 Ok(())
1293 }
1294 })
1295 .unwrap();
1296 state.emit_event(&[path]);
1297 Ok(())
1298 }
1299
1300 async fn create_file_with(
1301 &self,
1302 path: &Path,
1303 mut content: Pin<&mut (dyn AsyncRead + Send)>,
1304 ) -> Result<()> {
1305 let mut bytes = Vec::new();
1306 content.read_to_end(&mut bytes).await?;
1307 self.write_file_internal(path, bytes)?;
1308 Ok(())
1309 }
1310
1311 async fn extract_tar_file(
1312 &self,
1313 path: &Path,
1314 content: Archive<Pin<&mut (dyn AsyncRead + Send)>>,
1315 ) -> Result<()> {
1316 let mut entries = content.entries()?;
1317 while let Some(entry) = entries.next().await {
1318 let mut entry = entry?;
1319 if entry.header().entry_type().is_file() {
1320 let path = path.join(entry.path()?.as_ref());
1321 let mut bytes = Vec::new();
1322 entry.read_to_end(&mut bytes).await?;
1323 self.create_dir(path.parent().unwrap()).await?;
1324 self.write_file_internal(&path, bytes)?;
1325 }
1326 }
1327 Ok(())
1328 }
1329
1330 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
1331 self.simulate_random_delay().await;
1332
1333 let old_path = normalize_path(old_path);
1334 let new_path = normalize_path(new_path);
1335
1336 let mut state = self.state.lock();
1337 let moved_entry = state.write_path(&old_path, |e| {
1338 if let btree_map::Entry::Occupied(e) = e {
1339 Ok(e.get().clone())
1340 } else {
1341 Err(anyhow!("path does not exist: {}", &old_path.display()))
1342 }
1343 })?;
1344
1345 state.write_path(&new_path, |e| {
1346 match e {
1347 btree_map::Entry::Occupied(mut e) => {
1348 if options.overwrite {
1349 *e.get_mut() = moved_entry;
1350 } else if !options.ignore_if_exists {
1351 return Err(anyhow!("path already exists: {}", new_path.display()));
1352 }
1353 }
1354 btree_map::Entry::Vacant(e) => {
1355 e.insert(moved_entry);
1356 }
1357 }
1358 Ok(())
1359 })?;
1360
1361 state
1362 .write_path(&old_path, |e| {
1363 if let btree_map::Entry::Occupied(e) = e {
1364 Ok(e.remove())
1365 } else {
1366 unreachable!()
1367 }
1368 })
1369 .unwrap();
1370
1371 state.emit_event(&[old_path, new_path]);
1372 Ok(())
1373 }
1374
1375 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
1376 self.simulate_random_delay().await;
1377
1378 let source = normalize_path(source);
1379 let target = normalize_path(target);
1380 let mut state = self.state.lock();
1381 let mtime = state.next_mtime;
1382 let inode = util::post_inc(&mut state.next_inode);
1383 state.next_mtime += Duration::from_nanos(1);
1384 let source_entry = state.read_path(&source)?;
1385 let content = source_entry.lock().file_content(&source)?.clone();
1386 let entry = state.write_path(&target, |e| match e {
1387 btree_map::Entry::Occupied(e) => {
1388 if options.overwrite {
1389 Ok(Some(e.get().clone()))
1390 } else if !options.ignore_if_exists {
1391 return Err(anyhow!("{target:?} already exists"));
1392 } else {
1393 Ok(None)
1394 }
1395 }
1396 btree_map::Entry::Vacant(e) => Ok(Some(
1397 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
1398 inode,
1399 mtime,
1400 content: Vec::new(),
1401 })))
1402 .clone(),
1403 )),
1404 })?;
1405 if let Some(entry) = entry {
1406 entry.lock().set_file_content(&target, content)?;
1407 }
1408 state.emit_event(&[target]);
1409 Ok(())
1410 }
1411
1412 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1413 self.simulate_random_delay().await;
1414
1415 let path = normalize_path(path);
1416 let parent_path = path
1417 .parent()
1418 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1419 let base_name = path.file_name().unwrap();
1420
1421 let mut state = self.state.lock();
1422 let parent_entry = state.read_path(parent_path)?;
1423 let mut parent_entry = parent_entry.lock();
1424 let entry = parent_entry
1425 .dir_entries(parent_path)?
1426 .entry(base_name.to_str().unwrap().into());
1427
1428 match entry {
1429 btree_map::Entry::Vacant(_) => {
1430 if !options.ignore_if_not_exists {
1431 return Err(anyhow!("{path:?} does not exist"));
1432 }
1433 }
1434 btree_map::Entry::Occupied(e) => {
1435 {
1436 let mut entry = e.get().lock();
1437 let children = entry.dir_entries(&path)?;
1438 if !options.recursive && !children.is_empty() {
1439 return Err(anyhow!("{path:?} is not empty"));
1440 }
1441 }
1442 e.remove();
1443 }
1444 }
1445 state.emit_event(&[path]);
1446 Ok(())
1447 }
1448
1449 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
1450 self.simulate_random_delay().await;
1451
1452 let path = normalize_path(path);
1453 let parent_path = path
1454 .parent()
1455 .ok_or_else(|| anyhow!("cannot remove the root"))?;
1456 let base_name = path.file_name().unwrap();
1457 let mut state = self.state.lock();
1458 let parent_entry = state.read_path(parent_path)?;
1459 let mut parent_entry = parent_entry.lock();
1460 let entry = parent_entry
1461 .dir_entries(parent_path)?
1462 .entry(base_name.to_str().unwrap().into());
1463 match entry {
1464 btree_map::Entry::Vacant(_) => {
1465 if !options.ignore_if_not_exists {
1466 return Err(anyhow!("{path:?} does not exist"));
1467 }
1468 }
1469 btree_map::Entry::Occupied(e) => {
1470 e.get().lock().file_content(&path)?;
1471 e.remove();
1472 }
1473 }
1474 state.emit_event(&[path]);
1475 Ok(())
1476 }
1477
1478 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
1479 let bytes = self.load_internal(path).await?;
1480 Ok(Box::new(io::Cursor::new(bytes)))
1481 }
1482
1483 async fn load(&self, path: &Path) -> Result<String> {
1484 let content = self.load_internal(path).await?;
1485 Ok(String::from_utf8(content.clone())?)
1486 }
1487
1488 async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
1489 self.load_internal(path).await
1490 }
1491
1492 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
1493 self.simulate_random_delay().await;
1494 let path = normalize_path(path.as_path());
1495 self.write_file_internal(path, data.into_bytes())?;
1496 Ok(())
1497 }
1498
1499 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
1500 self.simulate_random_delay().await;
1501 let path = normalize_path(path);
1502 let content = chunks(text, line_ending).collect::<String>();
1503 if let Some(path) = path.parent() {
1504 self.create_dir(path).await?;
1505 }
1506 self.write_file_internal(path, content.into_bytes())?;
1507 Ok(())
1508 }
1509
1510 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
1511 let path = normalize_path(path);
1512 self.simulate_random_delay().await;
1513 let state = self.state.lock();
1514 if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
1515 Ok(canonical_path)
1516 } else {
1517 Err(anyhow!("path does not exist: {}", path.display()))
1518 }
1519 }
1520
1521 async fn is_file(&self, path: &Path) -> bool {
1522 let path = normalize_path(path);
1523 self.simulate_random_delay().await;
1524 let state = self.state.lock();
1525 if let Some((entry, _)) = state.try_read_path(&path, true) {
1526 entry.lock().is_file()
1527 } else {
1528 false
1529 }
1530 }
1531
1532 async fn is_dir(&self, path: &Path) -> bool {
1533 self.metadata(path)
1534 .await
1535 .is_ok_and(|metadata| metadata.is_some_and(|metadata| metadata.is_dir))
1536 }
1537
1538 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
1539 self.simulate_random_delay().await;
1540 let path = normalize_path(path);
1541 let mut state = self.state.lock();
1542 state.metadata_call_count += 1;
1543 if let Some((mut entry, _)) = state.try_read_path(&path, false) {
1544 let is_symlink = entry.lock().is_symlink();
1545 if is_symlink {
1546 if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
1547 entry = e;
1548 } else {
1549 return Ok(None);
1550 }
1551 }
1552
1553 let entry = entry.lock();
1554 Ok(Some(match &*entry {
1555 FakeFsEntry::File { inode, mtime, .. } => Metadata {
1556 inode: *inode,
1557 mtime: *mtime,
1558 is_dir: false,
1559 is_symlink,
1560 },
1561 FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
1562 inode: *inode,
1563 mtime: *mtime,
1564 is_dir: true,
1565 is_symlink,
1566 },
1567 FakeFsEntry::Symlink { .. } => unreachable!(),
1568 }))
1569 } else {
1570 Ok(None)
1571 }
1572 }
1573
1574 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
1575 self.simulate_random_delay().await;
1576 let path = normalize_path(path);
1577 let state = self.state.lock();
1578 if let Some((entry, _)) = state.try_read_path(&path, false) {
1579 let entry = entry.lock();
1580 if let FakeFsEntry::Symlink { target } = &*entry {
1581 Ok(target.clone())
1582 } else {
1583 Err(anyhow!("not a symlink: {}", path.display()))
1584 }
1585 } else {
1586 Err(anyhow!("path does not exist: {}", path.display()))
1587 }
1588 }
1589
1590 async fn read_dir(
1591 &self,
1592 path: &Path,
1593 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
1594 self.simulate_random_delay().await;
1595 let path = normalize_path(path);
1596 let mut state = self.state.lock();
1597 state.read_dir_call_count += 1;
1598 let entry = state.read_path(&path)?;
1599 let mut entry = entry.lock();
1600 let children = entry.dir_entries(&path)?;
1601 let paths = children
1602 .keys()
1603 .map(|file_name| Ok(path.join(file_name)))
1604 .collect::<Vec<_>>();
1605 Ok(Box::pin(futures::stream::iter(paths)))
1606 }
1607
1608 async fn watch(
1609 &self,
1610 path: &Path,
1611 _: Duration,
1612 ) -> (
1613 Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
1614 Arc<dyn Watcher>,
1615 ) {
1616 self.simulate_random_delay().await;
1617 let (tx, rx) = smol::channel::unbounded();
1618 self.state.lock().event_txs.push(tx);
1619 let path = path.to_path_buf();
1620 let executor = self.executor.clone();
1621 (
1622 Box::pin(futures::StreamExt::filter(rx, move |events| {
1623 let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
1624 let executor = executor.clone();
1625 async move {
1626 executor.simulate_random_delay().await;
1627 result
1628 }
1629 })),
1630 Arc::new(FakeWatcher {}),
1631 )
1632 }
1633
1634 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
1635 let state = self.state.lock();
1636 let entry = state.read_path(abs_dot_git).unwrap();
1637 let mut entry = entry.lock();
1638 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
1639 let state = git_repo_state
1640 .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
1641 .clone();
1642 Some(git::repository::FakeGitRepository::open(state))
1643 } else {
1644 None
1645 }
1646 }
1647
1648 fn is_fake(&self) -> bool {
1649 true
1650 }
1651
1652 async fn is_case_sensitive(&self) -> Result<bool> {
1653 Ok(true)
1654 }
1655
1656 #[cfg(any(test, feature = "test-support"))]
1657 fn as_fake(&self) -> &FakeFs {
1658 self
1659 }
1660}
1661
1662fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
1663 rope.chunks().flat_map(move |chunk| {
1664 let mut newline = false;
1665 chunk.split('\n').flat_map(move |line| {
1666 let ending = if newline {
1667 Some(line_ending.as_str())
1668 } else {
1669 None
1670 };
1671 newline = true;
1672 ending.into_iter().chain([line])
1673 })
1674 })
1675}
1676
1677pub fn normalize_path(path: &Path) -> PathBuf {
1678 let mut components = path.components().peekable();
1679 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1680 components.next();
1681 PathBuf::from(c.as_os_str())
1682 } else {
1683 PathBuf::new()
1684 };
1685
1686 for component in components {
1687 match component {
1688 Component::Prefix(..) => unreachable!(),
1689 Component::RootDir => {
1690 ret.push(component.as_os_str());
1691 }
1692 Component::CurDir => {}
1693 Component::ParentDir => {
1694 ret.pop();
1695 }
1696 Component::Normal(c) => {
1697 ret.push(c);
1698 }
1699 }
1700 }
1701 ret
1702}
1703
1704pub fn copy_recursive<'a>(
1705 fs: &'a dyn Fs,
1706 source: &'a Path,
1707 target: &'a Path,
1708 options: CopyOptions,
1709) -> BoxFuture<'a, Result<()>> {
1710 use futures::future::FutureExt;
1711
1712 async move {
1713 let metadata = fs
1714 .metadata(source)
1715 .await?
1716 .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
1717 if metadata.is_dir {
1718 if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
1719 if options.ignore_if_exists {
1720 return Ok(());
1721 } else {
1722 return Err(anyhow!("{target:?} already exists"));
1723 }
1724 }
1725
1726 let _ = fs
1727 .remove_dir(
1728 target,
1729 RemoveOptions {
1730 recursive: true,
1731 ignore_if_not_exists: true,
1732 },
1733 )
1734 .await;
1735 fs.create_dir(target).await?;
1736 let mut children = fs.read_dir(source).await?;
1737 while let Some(child_path) = children.next().await {
1738 if let Ok(child_path) = child_path {
1739 if let Some(file_name) = child_path.file_name() {
1740 let child_target_path = target.join(file_name);
1741 copy_recursive(fs, &child_path, &child_target_path, options).await?;
1742 }
1743 }
1744 }
1745
1746 Ok(())
1747 } else {
1748 fs.copy_file(source, target, options).await
1749 }
1750 }
1751 .boxed()
1752}
1753
1754// todo(windows)
1755// can we get file id not open the file twice?
1756// https://github.com/rust-lang/rust/issues/63010
1757#[cfg(target_os = "windows")]
1758async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
1759 use std::os::windows::io::AsRawHandle;
1760
1761 use smol::fs::windows::OpenOptionsExt;
1762 use windows::Win32::{
1763 Foundation::HANDLE,
1764 Storage::FileSystem::{
1765 GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
1766 },
1767 };
1768
1769 let file = smol::fs::OpenOptions::new()
1770 .read(true)
1771 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS.0)
1772 .open(path)
1773 .await?;
1774
1775 let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
1776 // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
1777 // This function supports Windows XP+
1778 smol::unblock(move || {
1779 unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle() as _), &mut info)? };
1780
1781 Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
1782 })
1783 .await
1784}
1785
1786#[cfg(test)]
1787mod tests {
1788 use super::*;
1789 use gpui::BackgroundExecutor;
1790 use serde_json::json;
1791
1792 #[gpui::test]
1793 async fn test_fake_fs(executor: BackgroundExecutor) {
1794 let fs = FakeFs::new(executor.clone());
1795 fs.insert_tree(
1796 "/root",
1797 json!({
1798 "dir1": {
1799 "a": "A",
1800 "b": "B"
1801 },
1802 "dir2": {
1803 "c": "C",
1804 "dir3": {
1805 "d": "D"
1806 }
1807 }
1808 }),
1809 )
1810 .await;
1811
1812 assert_eq!(
1813 fs.files(),
1814 vec![
1815 PathBuf::from("/root/dir1/a"),
1816 PathBuf::from("/root/dir1/b"),
1817 PathBuf::from("/root/dir2/c"),
1818 PathBuf::from("/root/dir2/dir3/d"),
1819 ]
1820 );
1821
1822 fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
1823 .await
1824 .unwrap();
1825
1826 assert_eq!(
1827 fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
1828 .await
1829 .unwrap(),
1830 PathBuf::from("/root/dir2/dir3"),
1831 );
1832 assert_eq!(
1833 fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
1834 .await
1835 .unwrap(),
1836 PathBuf::from("/root/dir2/dir3/d"),
1837 );
1838 assert_eq!(
1839 fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
1840 "D",
1841 );
1842 }
1843}
1844
1845#[cfg(target_os = "linux")]
1846pub mod watcher {
1847 use std::sync::OnceLock;
1848
1849 use parking_lot::Mutex;
1850 use util::ResultExt;
1851
1852 pub struct GlobalWatcher {
1853 // two mutexes because calling inotify.add triggers an inotify.event, which needs watchers.
1854 pub(super) inotify: Mutex<notify::INotifyWatcher>,
1855 pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
1856 }
1857
1858 impl GlobalWatcher {
1859 pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) {
1860 self.watchers.lock().push(Box::new(cb))
1861 }
1862 }
1863
1864 static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
1865 OnceLock::new();
1866
1867 fn handle_event(event: Result<notify::Event, notify::Error>) {
1868 let Some(event) = event.log_err() else { return };
1869 global::<()>(move |watcher| {
1870 for f in watcher.watchers.lock().iter() {
1871 f(&event)
1872 }
1873 })
1874 .log_err();
1875 }
1876
1877 pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
1878 let result = INOTIFY_INSTANCE.get_or_init(|| {
1879 notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
1880 inotify: Mutex::new(file_watcher),
1881 watchers: Default::default(),
1882 })
1883 });
1884 match result {
1885 Ok(g) => Ok(f(g)),
1886 Err(e) => Err(anyhow::anyhow!("{}", e)),
1887 }
1888 }
1889}