1pub mod repository;
2
3use anyhow::{anyhow, Result};
4use fsevent::EventStream;
5use futures::{future::BoxFuture, Stream, StreamExt};
6use git2::Repository as LibGitRepository;
7use parking_lot::Mutex;
8use repository::GitRepository;
9use rope::Rope;
10use smol::io::{AsyncReadExt, AsyncWriteExt};
11use std::io::Write;
12use std::sync::Arc;
13use std::{
14 io,
15 os::unix::fs::MetadataExt,
16 path::{Component, Path, PathBuf},
17 pin::Pin,
18 time::{Duration, SystemTime},
19};
20use tempfile::NamedTempFile;
21use text::LineEnding;
22use util::ResultExt;
23
24#[cfg(any(test, feature = "test-support"))]
25use collections::{btree_map, BTreeMap};
26#[cfg(any(test, feature = "test-support"))]
27use repository::{FakeGitRepositoryState, GitFileStatus};
28#[cfg(any(test, feature = "test-support"))]
29use std::ffi::OsStr;
30#[cfg(any(test, feature = "test-support"))]
31use std::sync::Weak;
32
33#[async_trait::async_trait]
34pub trait Fs: Send + Sync {
35 async fn create_dir(&self, path: &Path) -> Result<()>;
36 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
37 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
38 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
39 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
40 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
41 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
42 async fn load(&self, path: &Path) -> Result<String>;
43 async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
44 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
45 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
46 async fn is_file(&self, path: &Path) -> bool;
47 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
48 async fn read_link(&self, path: &Path) -> Result<PathBuf>;
49 async fn read_dir(
50 &self,
51 path: &Path,
52 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
53 async fn watch(
54 &self,
55 path: &Path,
56 latency: Duration,
57 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
58 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
59 fn is_fake(&self) -> bool;
60 #[cfg(any(test, feature = "test-support"))]
61 fn as_fake(&self) -> &FakeFs;
62}
63
64#[derive(Copy, Clone, Default)]
65pub struct CreateOptions {
66 pub overwrite: bool,
67 pub ignore_if_exists: bool,
68}
69
70#[derive(Copy, Clone, Default)]
71pub struct CopyOptions {
72 pub overwrite: bool,
73 pub ignore_if_exists: bool,
74}
75
76#[derive(Copy, Clone, Default)]
77pub struct RenameOptions {
78 pub overwrite: bool,
79 pub ignore_if_exists: bool,
80}
81
82#[derive(Copy, Clone, Default)]
83pub struct RemoveOptions {
84 pub recursive: bool,
85 pub ignore_if_not_exists: bool,
86}
87
88#[derive(Clone, Debug)]
89pub struct Metadata {
90 pub inode: u64,
91 pub mtime: SystemTime,
92 pub is_symlink: bool,
93 pub is_dir: bool,
94}
95
96impl From<lsp::CreateFileOptions> for CreateOptions {
97 fn from(options: lsp::CreateFileOptions) -> Self {
98 Self {
99 overwrite: options.overwrite.unwrap_or(false),
100 ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
101 }
102 }
103}
104
105impl From<lsp::RenameFileOptions> for RenameOptions {
106 fn from(options: lsp::RenameFileOptions) -> Self {
107 Self {
108 overwrite: options.overwrite.unwrap_or(false),
109 ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
110 }
111 }
112}
113
114impl From<lsp::DeleteFileOptions> for RemoveOptions {
115 fn from(options: lsp::DeleteFileOptions) -> Self {
116 Self {
117 recursive: options.recursive.unwrap_or(false),
118 ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
119 }
120 }
121}
122
123pub struct RealFs;
124
125#[async_trait::async_trait]
126impl Fs for RealFs {
127 async fn create_dir(&self, path: &Path) -> Result<()> {
128 Ok(smol::fs::create_dir_all(path).await?)
129 }
130
131 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
132 let mut open_options = smol::fs::OpenOptions::new();
133 open_options.write(true).create(true);
134 if options.overwrite {
135 open_options.truncate(true);
136 } else if !options.ignore_if_exists {
137 open_options.create_new(true);
138 }
139 open_options.open(path).await?;
140 Ok(())
141 }
142
143 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
144 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
145 if options.ignore_if_exists {
146 return Ok(());
147 } else {
148 return Err(anyhow!("{target:?} already exists"));
149 }
150 }
151
152 smol::fs::copy(source, target).await?;
153 Ok(())
154 }
155
156 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
157 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
158 if options.ignore_if_exists {
159 return Ok(());
160 } else {
161 return Err(anyhow!("{target:?} already exists"));
162 }
163 }
164
165 smol::fs::rename(source, target).await?;
166 Ok(())
167 }
168
169 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
170 let result = if options.recursive {
171 smol::fs::remove_dir_all(path).await
172 } else {
173 smol::fs::remove_dir(path).await
174 };
175 match result {
176 Ok(()) => Ok(()),
177 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
178 Ok(())
179 }
180 Err(err) => Err(err)?,
181 }
182 }
183
184 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
185 match smol::fs::remove_file(path).await {
186 Ok(()) => Ok(()),
187 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
188 Ok(())
189 }
190 Err(err) => Err(err)?,
191 }
192 }
193
194 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
195 Ok(Box::new(std::fs::File::open(path)?))
196 }
197
198 async fn load(&self, path: &Path) -> Result<String> {
199 let mut file = smol::fs::File::open(path).await?;
200 let mut text = String::new();
201 file.read_to_string(&mut text).await?;
202 Ok(text)
203 }
204
205 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
206 smol::unblock(move || {
207 let mut tmp_file = NamedTempFile::new()?;
208 tmp_file.write_all(data.as_bytes())?;
209 tmp_file.persist(path)?;
210 Ok::<(), anyhow::Error>(())
211 })
212 .await?;
213
214 Ok(())
215 }
216
217 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
218 let buffer_size = text.summary().len.min(10 * 1024);
219 if let Some(path) = path.parent() {
220 self.create_dir(path).await?;
221 }
222 let file = smol::fs::File::create(path).await?;
223 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
224 for chunk in chunks(text, line_ending) {
225 writer.write_all(chunk.as_bytes()).await?;
226 }
227 writer.flush().await?;
228 Ok(())
229 }
230
231 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
232 Ok(smol::fs::canonicalize(path).await?)
233 }
234
235 async fn is_file(&self, path: &Path) -> bool {
236 smol::fs::metadata(path)
237 .await
238 .map_or(false, |metadata| metadata.is_file())
239 }
240
241 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
242 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
243 Ok(metadata) => metadata,
244 Err(err) => {
245 return match (err.kind(), err.raw_os_error()) {
246 (io::ErrorKind::NotFound, _) => Ok(None),
247 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
248 _ => Err(anyhow::Error::new(err)),
249 }
250 }
251 };
252
253 let is_symlink = symlink_metadata.file_type().is_symlink();
254 let metadata = if is_symlink {
255 smol::fs::metadata(path).await?
256 } else {
257 symlink_metadata
258 };
259 Ok(Some(Metadata {
260 inode: metadata.ino(),
261 mtime: metadata.modified().unwrap(),
262 is_symlink,
263 is_dir: metadata.file_type().is_dir(),
264 }))
265 }
266
267 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
268 let path = smol::fs::read_link(path).await?;
269 Ok(path)
270 }
271
272 async fn read_dir(
273 &self,
274 path: &Path,
275 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
276 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
277 Ok(entry) => Ok(entry.path()),
278 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
279 });
280 Ok(Box::pin(result))
281 }
282
283 async fn watch(
284 &self,
285 path: &Path,
286 latency: Duration,
287 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
288 let (tx, rx) = smol::channel::unbounded();
289 let (stream, handle) = EventStream::new(&[path], latency);
290 std::thread::spawn(move || {
291 stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
292 });
293 Box::pin(rx.chain(futures::stream::once(async move {
294 drop(handle);
295 vec![]
296 })))
297 }
298
299 fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
300 LibGitRepository::open(&dotgit_path)
301 .log_err()
302 .and_then::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
303 Some(Arc::new(Mutex::new(libgit_repository)))
304 })
305 }
306
307 fn is_fake(&self) -> bool {
308 false
309 }
310 #[cfg(any(test, feature = "test-support"))]
311 fn as_fake(&self) -> &FakeFs {
312 panic!("called `RealFs::as_fake`")
313 }
314}
315
316#[cfg(any(test, feature = "test-support"))]
317pub struct FakeFs {
318 // Use an unfair lock to ensure tests are deterministic.
319 state: Mutex<FakeFsState>,
320 executor: Weak<gpui::executor::Background>,
321}
322
323#[cfg(any(test, feature = "test-support"))]
324struct FakeFsState {
325 root: Arc<Mutex<FakeFsEntry>>,
326 next_inode: u64,
327 next_mtime: SystemTime,
328 event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
329 events_paused: bool,
330 buffered_events: Vec<fsevent::Event>,
331 metadata_call_count: usize,
332 read_dir_call_count: usize,
333}
334
335#[cfg(any(test, feature = "test-support"))]
336#[derive(Debug)]
337enum FakeFsEntry {
338 File {
339 inode: u64,
340 mtime: SystemTime,
341 content: String,
342 },
343 Dir {
344 inode: u64,
345 mtime: SystemTime,
346 entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
347 git_repo_state: Option<Arc<Mutex<repository::FakeGitRepositoryState>>>,
348 },
349 Symlink {
350 target: PathBuf,
351 },
352}
353
354#[cfg(any(test, feature = "test-support"))]
355impl FakeFsState {
356 fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
357 Ok(self
358 .try_read_path(target, true)
359 .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
360 .0)
361 }
362
363 fn try_read_path<'a>(
364 &'a self,
365 target: &Path,
366 follow_symlink: bool,
367 ) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
368 let mut path = target.to_path_buf();
369 let mut canonical_path = PathBuf::new();
370 let mut entry_stack = Vec::new();
371 'outer: loop {
372 let mut path_components = path.components().peekable();
373 while let Some(component) = path_components.next() {
374 match component {
375 Component::Prefix(_) => panic!("prefix paths aren't supported"),
376 Component::RootDir => {
377 entry_stack.clear();
378 entry_stack.push(self.root.clone());
379 canonical_path.clear();
380 canonical_path.push("/");
381 }
382 Component::CurDir => {}
383 Component::ParentDir => {
384 entry_stack.pop()?;
385 canonical_path.pop();
386 }
387 Component::Normal(name) => {
388 let current_entry = entry_stack.last().cloned()?;
389 let current_entry = current_entry.lock();
390 if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
391 let entry = entries.get(name.to_str().unwrap()).cloned()?;
392 if path_components.peek().is_some() || follow_symlink {
393 let entry = entry.lock();
394 if let FakeFsEntry::Symlink { target, .. } = &*entry {
395 let mut target = target.clone();
396 target.extend(path_components);
397 path = target;
398 continue 'outer;
399 }
400 }
401 entry_stack.push(entry.clone());
402 canonical_path.push(name);
403 } else {
404 return None;
405 }
406 }
407 }
408 }
409 break;
410 }
411 Some((entry_stack.pop()?, canonical_path))
412 }
413
414 fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
415 where
416 Fn: FnOnce(btree_map::Entry<String, Arc<Mutex<FakeFsEntry>>>) -> Result<T>,
417 {
418 let path = normalize_path(path);
419 let filename = path
420 .file_name()
421 .ok_or_else(|| anyhow!("cannot overwrite the root"))?;
422 let parent_path = path.parent().unwrap();
423
424 let parent = self.read_path(parent_path)?;
425 let mut parent = parent.lock();
426 let new_entry = parent
427 .dir_entries(parent_path)?
428 .entry(filename.to_str().unwrap().into());
429 callback(new_entry)
430 }
431
432 fn emit_event<I, T>(&mut self, paths: I)
433 where
434 I: IntoIterator<Item = T>,
435 T: Into<PathBuf>,
436 {
437 self.buffered_events
438 .extend(paths.into_iter().map(|path| fsevent::Event {
439 event_id: 0,
440 flags: fsevent::StreamFlags::empty(),
441 path: path.into(),
442 }));
443
444 if !self.events_paused {
445 self.flush_events(self.buffered_events.len());
446 }
447 }
448
449 fn flush_events(&mut self, mut count: usize) {
450 count = count.min(self.buffered_events.len());
451 let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
452 self.event_txs.retain(|tx| {
453 let _ = tx.try_send(events.clone());
454 !tx.is_closed()
455 });
456 }
457}
458
459#[cfg(any(test, feature = "test-support"))]
460lazy_static::lazy_static! {
461 pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
462}
463
464#[cfg(any(test, feature = "test-support"))]
465impl FakeFs {
466 pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
467 Arc::new(Self {
468 executor: Arc::downgrade(&executor),
469 state: Mutex::new(FakeFsState {
470 root: Arc::new(Mutex::new(FakeFsEntry::Dir {
471 inode: 0,
472 mtime: SystemTime::UNIX_EPOCH,
473 entries: Default::default(),
474 git_repo_state: None,
475 })),
476 next_mtime: SystemTime::UNIX_EPOCH,
477 next_inode: 1,
478 event_txs: Default::default(),
479 buffered_events: Vec::new(),
480 events_paused: false,
481 read_dir_call_count: 0,
482 metadata_call_count: 0,
483 }),
484 })
485 }
486
487 pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
488 self.write_file_internal(path, content).unwrap()
489 }
490
491 pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
492 let mut state = self.state.lock();
493 let path = path.as_ref();
494 let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target }));
495 state
496 .write_path(path.as_ref(), move |e| match e {
497 btree_map::Entry::Vacant(e) => {
498 e.insert(file);
499 Ok(())
500 }
501 btree_map::Entry::Occupied(mut e) => {
502 *e.get_mut() = file;
503 Ok(())
504 }
505 })
506 .unwrap();
507 state.emit_event(&[path]);
508 }
509
510 pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
511 let mut state = self.state.lock();
512 let path = path.as_ref();
513 let inode = state.next_inode;
514 let mtime = state.next_mtime;
515 state.next_inode += 1;
516 state.next_mtime += Duration::from_nanos(1);
517 let file = Arc::new(Mutex::new(FakeFsEntry::File {
518 inode,
519 mtime,
520 content,
521 }));
522 state.write_path(path, move |entry| {
523 match entry {
524 btree_map::Entry::Vacant(e) => {
525 e.insert(file);
526 }
527 btree_map::Entry::Occupied(mut e) => {
528 *e.get_mut() = file;
529 }
530 }
531 Ok(())
532 })?;
533 state.emit_event(&[path]);
534 Ok(())
535 }
536
537 pub fn pause_events(&self) {
538 self.state.lock().events_paused = true;
539 }
540
541 pub fn buffered_event_count(&self) -> usize {
542 self.state.lock().buffered_events.len()
543 }
544
545 pub fn flush_events(&self, count: usize) {
546 self.state.lock().flush_events(count);
547 }
548
549 #[must_use]
550 pub fn insert_tree<'a>(
551 &'a self,
552 path: impl 'a + AsRef<Path> + Send,
553 tree: serde_json::Value,
554 ) -> futures::future::BoxFuture<'a, ()> {
555 use futures::FutureExt as _;
556 use serde_json::Value::*;
557
558 async move {
559 let path = path.as_ref();
560
561 match tree {
562 Object(map) => {
563 self.create_dir(path).await.unwrap();
564 for (name, contents) in map {
565 let mut path = PathBuf::from(path);
566 path.push(name);
567 self.insert_tree(&path, contents).await;
568 }
569 }
570 Null => {
571 self.create_dir(path).await.unwrap();
572 }
573 String(contents) => {
574 self.insert_file(&path, contents).await;
575 }
576 _ => {
577 panic!("JSON object must contain only objects, strings, or null");
578 }
579 }
580 }
581 .boxed()
582 }
583
584 pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
585 where
586 F: FnOnce(&mut FakeGitRepositoryState),
587 {
588 let mut state = self.state.lock();
589 let entry = state.read_path(dot_git).unwrap();
590 let mut entry = entry.lock();
591
592 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
593 let repo_state = git_repo_state.get_or_insert_with(Default::default);
594 let mut repo_state = repo_state.lock();
595
596 f(&mut repo_state);
597
598 if emit_git_event {
599 state.emit_event([dot_git]);
600 }
601 } else {
602 panic!("not a directory");
603 }
604 }
605
606 pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
607 self.with_git_state(dot_git, true, |state| {
608 state.branch_name = branch.map(Into::into)
609 })
610 }
611
612 pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
613 self.with_git_state(dot_git, true, |state| {
614 state.index_contents.clear();
615 state.index_contents.extend(
616 head_state
617 .iter()
618 .map(|(path, content)| (path.to_path_buf(), content.clone())),
619 );
620 });
621 }
622
623 pub fn set_status_for_repo_via_working_copy_change(
624 &self,
625 dot_git: &Path,
626 statuses: &[(&Path, GitFileStatus)],
627 ) {
628 self.with_git_state(dot_git, false, |state| {
629 state.worktree_statuses.clear();
630 state.worktree_statuses.extend(
631 statuses
632 .iter()
633 .map(|(path, content)| ((**path).into(), content.clone())),
634 );
635 });
636 self.state.lock().emit_event(
637 statuses
638 .iter()
639 .map(|(path, _)| dot_git.parent().unwrap().join(path)),
640 );
641 }
642
643 pub fn set_status_for_repo_via_git_operation(
644 &self,
645 dot_git: &Path,
646 statuses: &[(&Path, GitFileStatus)],
647 ) {
648 self.with_git_state(dot_git, true, |state| {
649 state.worktree_statuses.clear();
650 state.worktree_statuses.extend(
651 statuses
652 .iter()
653 .map(|(path, content)| ((**path).into(), content.clone())),
654 );
655 });
656 }
657
658 pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
659 let mut result = Vec::new();
660 let mut queue = collections::VecDeque::new();
661 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
662 while let Some((path, entry)) = queue.pop_front() {
663 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
664 for (name, entry) in entries {
665 queue.push_back((path.join(name), entry.clone()));
666 }
667 }
668 if include_dot_git
669 || !path
670 .components()
671 .any(|component| component.as_os_str() == *FS_DOT_GIT)
672 {
673 result.push(path);
674 }
675 }
676 result
677 }
678
679 pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
680 let mut result = Vec::new();
681 let mut queue = collections::VecDeque::new();
682 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
683 while let Some((path, entry)) = queue.pop_front() {
684 if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() {
685 for (name, entry) in entries {
686 queue.push_back((path.join(name), entry.clone()));
687 }
688 if include_dot_git
689 || !path
690 .components()
691 .any(|component| component.as_os_str() == *FS_DOT_GIT)
692 {
693 result.push(path);
694 }
695 }
696 }
697 result
698 }
699
700 pub fn files(&self) -> Vec<PathBuf> {
701 let mut result = Vec::new();
702 let mut queue = collections::VecDeque::new();
703 queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
704 while let Some((path, entry)) = queue.pop_front() {
705 let e = entry.lock();
706 match &*e {
707 FakeFsEntry::File { .. } => result.push(path),
708 FakeFsEntry::Dir { entries, .. } => {
709 for (name, entry) in entries {
710 queue.push_back((path.join(name), entry.clone()));
711 }
712 }
713 FakeFsEntry::Symlink { .. } => {}
714 }
715 }
716 result
717 }
718
719 /// How many `read_dir` calls have been issued.
720 pub fn read_dir_call_count(&self) -> usize {
721 self.state.lock().read_dir_call_count
722 }
723
724 /// How many `metadata` calls have been issued.
725 pub fn metadata_call_count(&self) -> usize {
726 self.state.lock().metadata_call_count
727 }
728
729 async fn simulate_random_delay(&self) {
730 self.executor
731 .upgrade()
732 .expect("executor has been dropped")
733 .simulate_random_delay()
734 .await;
735 }
736}
737
738#[cfg(any(test, feature = "test-support"))]
739impl FakeFsEntry {
740 fn is_file(&self) -> bool {
741 matches!(self, Self::File { .. })
742 }
743
744 fn is_symlink(&self) -> bool {
745 matches!(self, Self::Symlink { .. })
746 }
747
748 fn file_content(&self, path: &Path) -> Result<&String> {
749 if let Self::File { content, .. } = self {
750 Ok(content)
751 } else {
752 Err(anyhow!("not a file: {}", path.display()))
753 }
754 }
755
756 fn set_file_content(&mut self, path: &Path, new_content: String) -> Result<()> {
757 if let Self::File { content, mtime, .. } = self {
758 *mtime = SystemTime::now();
759 *content = new_content;
760 Ok(())
761 } else {
762 Err(anyhow!("not a file: {}", path.display()))
763 }
764 }
765
766 fn dir_entries(
767 &mut self,
768 path: &Path,
769 ) -> Result<&mut BTreeMap<String, Arc<Mutex<FakeFsEntry>>>> {
770 if let Self::Dir { entries, .. } = self {
771 Ok(entries)
772 } else {
773 Err(anyhow!("not a directory: {}", path.display()))
774 }
775 }
776}
777
778#[cfg(any(test, feature = "test-support"))]
779#[async_trait::async_trait]
780impl Fs for FakeFs {
781 async fn create_dir(&self, path: &Path) -> Result<()> {
782 self.simulate_random_delay().await;
783
784 let mut created_dirs = Vec::new();
785 let mut cur_path = PathBuf::new();
786 for component in path.components() {
787 let mut state = self.state.lock();
788 cur_path.push(component);
789 if cur_path == Path::new("/") {
790 continue;
791 }
792
793 let inode = state.next_inode;
794 let mtime = state.next_mtime;
795 state.next_mtime += Duration::from_nanos(1);
796 state.next_inode += 1;
797 state.write_path(&cur_path, |entry| {
798 entry.or_insert_with(|| {
799 created_dirs.push(cur_path.clone());
800 Arc::new(Mutex::new(FakeFsEntry::Dir {
801 inode,
802 mtime,
803 entries: Default::default(),
804 git_repo_state: None,
805 }))
806 });
807 Ok(())
808 })?
809 }
810
811 self.state.lock().emit_event(&created_dirs);
812 Ok(())
813 }
814
815 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
816 self.simulate_random_delay().await;
817 let mut state = self.state.lock();
818 let inode = state.next_inode;
819 let mtime = state.next_mtime;
820 state.next_mtime += Duration::from_nanos(1);
821 state.next_inode += 1;
822 let file = Arc::new(Mutex::new(FakeFsEntry::File {
823 inode,
824 mtime,
825 content: String::new(),
826 }));
827 state.write_path(path, |entry| {
828 match entry {
829 btree_map::Entry::Occupied(mut e) => {
830 if options.overwrite {
831 *e.get_mut() = file;
832 } else if !options.ignore_if_exists {
833 return Err(anyhow!("path already exists: {}", path.display()));
834 }
835 }
836 btree_map::Entry::Vacant(e) => {
837 e.insert(file);
838 }
839 }
840 Ok(())
841 })?;
842 state.emit_event(&[path]);
843 Ok(())
844 }
845
846 async fn rename(&self, old_path: &Path, new_path: &Path, options: RenameOptions) -> Result<()> {
847 self.simulate_random_delay().await;
848
849 let old_path = normalize_path(old_path);
850 let new_path = normalize_path(new_path);
851
852 let mut state = self.state.lock();
853 let moved_entry = state.write_path(&old_path, |e| {
854 if let btree_map::Entry::Occupied(e) = e {
855 Ok(e.get().clone())
856 } else {
857 Err(anyhow!("path does not exist: {}", &old_path.display()))
858 }
859 })?;
860
861 state.write_path(&new_path, |e| {
862 match e {
863 btree_map::Entry::Occupied(mut e) => {
864 if options.overwrite {
865 *e.get_mut() = moved_entry;
866 } else if !options.ignore_if_exists {
867 return Err(anyhow!("path already exists: {}", new_path.display()));
868 }
869 }
870 btree_map::Entry::Vacant(e) => {
871 e.insert(moved_entry);
872 }
873 }
874 Ok(())
875 })?;
876
877 state
878 .write_path(&old_path, |e| {
879 if let btree_map::Entry::Occupied(e) = e {
880 Ok(e.remove())
881 } else {
882 unreachable!()
883 }
884 })
885 .unwrap();
886
887 state.emit_event(&[old_path, new_path]);
888 Ok(())
889 }
890
891 async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
892 self.simulate_random_delay().await;
893
894 let source = normalize_path(source);
895 let target = normalize_path(target);
896 let mut state = self.state.lock();
897 let mtime = state.next_mtime;
898 let inode = util::post_inc(&mut state.next_inode);
899 state.next_mtime += Duration::from_nanos(1);
900 let source_entry = state.read_path(&source)?;
901 let content = source_entry.lock().file_content(&source)?.clone();
902 let entry = state.write_path(&target, |e| match e {
903 btree_map::Entry::Occupied(e) => {
904 if options.overwrite {
905 Ok(Some(e.get().clone()))
906 } else if !options.ignore_if_exists {
907 return Err(anyhow!("{target:?} already exists"));
908 } else {
909 Ok(None)
910 }
911 }
912 btree_map::Entry::Vacant(e) => Ok(Some(
913 e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
914 inode,
915 mtime,
916 content: String::new(),
917 })))
918 .clone(),
919 )),
920 })?;
921 if let Some(entry) = entry {
922 entry.lock().set_file_content(&target, content)?;
923 }
924 state.emit_event(&[target]);
925 Ok(())
926 }
927
928 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
929 self.simulate_random_delay().await;
930
931 let path = normalize_path(path);
932 let parent_path = path
933 .parent()
934 .ok_or_else(|| anyhow!("cannot remove the root"))?;
935 let base_name = path.file_name().unwrap();
936
937 let mut state = self.state.lock();
938 let parent_entry = state.read_path(parent_path)?;
939 let mut parent_entry = parent_entry.lock();
940 let entry = parent_entry
941 .dir_entries(parent_path)?
942 .entry(base_name.to_str().unwrap().into());
943
944 match entry {
945 btree_map::Entry::Vacant(_) => {
946 if !options.ignore_if_not_exists {
947 return Err(anyhow!("{path:?} does not exist"));
948 }
949 }
950 btree_map::Entry::Occupied(e) => {
951 {
952 let mut entry = e.get().lock();
953 let children = entry.dir_entries(&path)?;
954 if !options.recursive && !children.is_empty() {
955 return Err(anyhow!("{path:?} is not empty"));
956 }
957 }
958 e.remove();
959 }
960 }
961 state.emit_event(&[path]);
962 Ok(())
963 }
964
965 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
966 self.simulate_random_delay().await;
967
968 let path = normalize_path(path);
969 let parent_path = path
970 .parent()
971 .ok_or_else(|| anyhow!("cannot remove the root"))?;
972 let base_name = path.file_name().unwrap();
973 let mut state = self.state.lock();
974 let parent_entry = state.read_path(parent_path)?;
975 let mut parent_entry = parent_entry.lock();
976 let entry = parent_entry
977 .dir_entries(parent_path)?
978 .entry(base_name.to_str().unwrap().into());
979 match entry {
980 btree_map::Entry::Vacant(_) => {
981 if !options.ignore_if_not_exists {
982 return Err(anyhow!("{path:?} does not exist"));
983 }
984 }
985 btree_map::Entry::Occupied(e) => {
986 e.get().lock().file_content(&path)?;
987 e.remove();
988 }
989 }
990 state.emit_event(&[path]);
991 Ok(())
992 }
993
994 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
995 let text = self.load(path).await?;
996 Ok(Box::new(io::Cursor::new(text)))
997 }
998
999 async fn load(&self, path: &Path) -> Result<String> {
1000 let path = normalize_path(path);
1001 self.simulate_random_delay().await;
1002 let state = self.state.lock();
1003 let entry = state.read_path(&path)?;
1004 let entry = entry.lock();
1005 entry.file_content(&path).cloned()
1006 }
1007
1008 async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
1009 self.simulate_random_delay().await;
1010 let path = normalize_path(path.as_path());
1011 self.write_file_internal(path, data.to_string())?;
1012
1013 Ok(())
1014 }
1015
1016 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
1017 self.simulate_random_delay().await;
1018 let path = normalize_path(path);
1019 let content = chunks(text, line_ending).collect();
1020 if let Some(path) = path.parent() {
1021 self.create_dir(path).await?;
1022 }
1023 self.write_file_internal(path, content)?;
1024 Ok(())
1025 }
1026
1027 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
1028 let path = normalize_path(path);
1029 self.simulate_random_delay().await;
1030 let state = self.state.lock();
1031 if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
1032 Ok(canonical_path)
1033 } else {
1034 Err(anyhow!("path does not exist: {}", path.display()))
1035 }
1036 }
1037
1038 async fn is_file(&self, path: &Path) -> bool {
1039 let path = normalize_path(path);
1040 self.simulate_random_delay().await;
1041 let state = self.state.lock();
1042 if let Some((entry, _)) = state.try_read_path(&path, true) {
1043 entry.lock().is_file()
1044 } else {
1045 false
1046 }
1047 }
1048
1049 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
1050 self.simulate_random_delay().await;
1051 let path = normalize_path(path);
1052 let mut state = self.state.lock();
1053 state.metadata_call_count += 1;
1054 if let Some((mut entry, _)) = state.try_read_path(&path, false) {
1055 let is_symlink = entry.lock().is_symlink();
1056 if is_symlink {
1057 if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
1058 entry = e;
1059 } else {
1060 return Ok(None);
1061 }
1062 }
1063
1064 let entry = entry.lock();
1065 Ok(Some(match &*entry {
1066 FakeFsEntry::File { inode, mtime, .. } => Metadata {
1067 inode: *inode,
1068 mtime: *mtime,
1069 is_dir: false,
1070 is_symlink,
1071 },
1072 FakeFsEntry::Dir { inode, mtime, .. } => Metadata {
1073 inode: *inode,
1074 mtime: *mtime,
1075 is_dir: true,
1076 is_symlink,
1077 },
1078 FakeFsEntry::Symlink { .. } => unreachable!(),
1079 }))
1080 } else {
1081 Ok(None)
1082 }
1083 }
1084
1085 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
1086 self.simulate_random_delay().await;
1087 let path = normalize_path(path);
1088 let state = self.state.lock();
1089 if let Some((entry, _)) = state.try_read_path(&path, false) {
1090 let entry = entry.lock();
1091 if let FakeFsEntry::Symlink { target } = &*entry {
1092 Ok(target.clone())
1093 } else {
1094 Err(anyhow!("not a symlink: {}", path.display()))
1095 }
1096 } else {
1097 Err(anyhow!("path does not exist: {}", path.display()))
1098 }
1099 }
1100
1101 async fn read_dir(
1102 &self,
1103 path: &Path,
1104 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
1105 self.simulate_random_delay().await;
1106 let path = normalize_path(path);
1107 let mut state = self.state.lock();
1108 state.read_dir_call_count += 1;
1109 let entry = state.read_path(&path)?;
1110 let mut entry = entry.lock();
1111 let children = entry.dir_entries(&path)?;
1112 let paths = children
1113 .keys()
1114 .map(|file_name| Ok(path.join(file_name)))
1115 .collect::<Vec<_>>();
1116 Ok(Box::pin(futures::stream::iter(paths)))
1117 }
1118
1119 async fn watch(
1120 &self,
1121 path: &Path,
1122 _: Duration,
1123 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
1124 self.simulate_random_delay().await;
1125 let (tx, rx) = smol::channel::unbounded();
1126 self.state.lock().event_txs.push(tx);
1127 let path = path.to_path_buf();
1128 let executor = self.executor.clone();
1129 Box::pin(futures::StreamExt::filter(rx, move |events| {
1130 let result = events.iter().any(|event| event.path.starts_with(&path));
1131 let executor = executor.clone();
1132 async move {
1133 if let Some(executor) = executor.clone().upgrade() {
1134 executor.simulate_random_delay().await;
1135 }
1136 result
1137 }
1138 }))
1139 }
1140
1141 fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
1142 let state = self.state.lock();
1143 let entry = state.read_path(abs_dot_git).unwrap();
1144 let mut entry = entry.lock();
1145 if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
1146 let state = git_repo_state
1147 .get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
1148 .clone();
1149 Some(repository::FakeGitRepository::open(state))
1150 } else {
1151 None
1152 }
1153 }
1154
1155 fn is_fake(&self) -> bool {
1156 true
1157 }
1158
1159 #[cfg(any(test, feature = "test-support"))]
1160 fn as_fake(&self) -> &FakeFs {
1161 self
1162 }
1163}
1164
1165fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
1166 rope.chunks().flat_map(move |chunk| {
1167 let mut newline = false;
1168 chunk.split('\n').flat_map(move |line| {
1169 let ending = if newline {
1170 Some(line_ending.as_str())
1171 } else {
1172 None
1173 };
1174 newline = true;
1175 ending.into_iter().chain([line])
1176 })
1177 })
1178}
1179
1180pub fn normalize_path(path: &Path) -> PathBuf {
1181 let mut components = path.components().peekable();
1182 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
1183 components.next();
1184 PathBuf::from(c.as_os_str())
1185 } else {
1186 PathBuf::new()
1187 };
1188
1189 for component in components {
1190 match component {
1191 Component::Prefix(..) => unreachable!(),
1192 Component::RootDir => {
1193 ret.push(component.as_os_str());
1194 }
1195 Component::CurDir => {}
1196 Component::ParentDir => {
1197 ret.pop();
1198 }
1199 Component::Normal(c) => {
1200 ret.push(c);
1201 }
1202 }
1203 }
1204 ret
1205}
1206
1207pub fn copy_recursive<'a>(
1208 fs: &'a dyn Fs,
1209 source: &'a Path,
1210 target: &'a Path,
1211 options: CopyOptions,
1212) -> BoxFuture<'a, Result<()>> {
1213 use futures::future::FutureExt;
1214
1215 async move {
1216 let metadata = fs
1217 .metadata(source)
1218 .await?
1219 .ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
1220 if metadata.is_dir {
1221 if !options.overwrite && fs.metadata(target).await.is_ok() {
1222 if options.ignore_if_exists {
1223 return Ok(());
1224 } else {
1225 return Err(anyhow!("{target:?} already exists"));
1226 }
1227 }
1228
1229 let _ = fs
1230 .remove_dir(
1231 target,
1232 RemoveOptions {
1233 recursive: true,
1234 ignore_if_not_exists: true,
1235 },
1236 )
1237 .await;
1238 fs.create_dir(target).await?;
1239 let mut children = fs.read_dir(source).await?;
1240 while let Some(child_path) = children.next().await {
1241 if let Ok(child_path) = child_path {
1242 if let Some(file_name) = child_path.file_name() {
1243 let child_target_path = target.join(file_name);
1244 copy_recursive(fs, &child_path, &child_target_path, options).await?;
1245 }
1246 }
1247 }
1248
1249 Ok(())
1250 } else {
1251 fs.copy_file(source, target, options).await
1252 }
1253 }
1254 .boxed()
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259 use super::*;
1260 use gpui::TestAppContext;
1261 use serde_json::json;
1262
1263 #[gpui::test]
1264 async fn test_fake_fs(cx: &mut TestAppContext) {
1265 let fs = FakeFs::new(cx.background());
1266
1267 fs.insert_tree(
1268 "/root",
1269 json!({
1270 "dir1": {
1271 "a": "A",
1272 "b": "B"
1273 },
1274 "dir2": {
1275 "c": "C",
1276 "dir3": {
1277 "d": "D"
1278 }
1279 }
1280 }),
1281 )
1282 .await;
1283
1284 assert_eq!(
1285 fs.files(),
1286 vec![
1287 PathBuf::from("/root/dir1/a"),
1288 PathBuf::from("/root/dir1/b"),
1289 PathBuf::from("/root/dir2/c"),
1290 PathBuf::from("/root/dir2/dir3/d"),
1291 ]
1292 );
1293
1294 fs.insert_symlink("/root/dir2/link-to-dir3", "./dir3".into())
1295 .await;
1296
1297 assert_eq!(
1298 fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
1299 .await
1300 .unwrap(),
1301 PathBuf::from("/root/dir2/dir3"),
1302 );
1303 assert_eq!(
1304 fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
1305 .await
1306 .unwrap(),
1307 PathBuf::from("/root/dir2/dir3/d"),
1308 );
1309 assert_eq!(
1310 fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
1311 "D",
1312 );
1313 }
1314}