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