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