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