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