1use anyhow::{anyhow, Result};
2use fsevent::EventStream;
3use futures::{Stream, StreamExt};
4use language::LineEnding;
5use smol::io::{AsyncReadExt, AsyncWriteExt};
6use std::{
7 io,
8 os::unix::fs::MetadataExt,
9 path::{Component, Path, PathBuf},
10 pin::Pin,
11 time::{Duration, SystemTime},
12};
13use text::Rope;
14
15#[async_trait::async_trait]
16pub trait Fs: Send + Sync {
17 async fn create_dir(&self, path: &Path) -> Result<()>;
18 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
19 async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
20 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
21 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
22 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
23 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
24 async fn load(&self, path: &Path) -> Result<String>;
25 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
26 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
27 async fn is_file(&self, path: &Path) -> bool;
28 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
29 async fn read_dir(
30 &self,
31 path: &Path,
32 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
33 async fn watch(
34 &self,
35 path: &Path,
36 latency: Duration,
37 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
38 fn is_fake(&self) -> bool;
39 #[cfg(any(test, feature = "test-support"))]
40 fn as_fake(&self) -> &FakeFs;
41}
42
43#[derive(Copy, Clone, Default)]
44pub struct CreateOptions {
45 pub overwrite: bool,
46 pub ignore_if_exists: bool,
47}
48
49#[derive(Copy, Clone, Default)]
50pub struct CopyOptions {
51 pub overwrite: bool,
52 pub ignore_if_exists: bool,
53}
54
55#[derive(Copy, Clone, Default)]
56pub struct RenameOptions {
57 pub overwrite: bool,
58 pub ignore_if_exists: bool,
59}
60
61#[derive(Copy, Clone, Default)]
62pub struct RemoveOptions {
63 pub recursive: bool,
64 pub ignore_if_not_exists: bool,
65}
66
67#[derive(Clone, Debug)]
68pub struct Metadata {
69 pub inode: u64,
70 pub mtime: SystemTime,
71 pub is_symlink: bool,
72 pub is_dir: bool,
73}
74
75pub struct RealFs;
76
77#[async_trait::async_trait]
78impl Fs for RealFs {
79 async fn create_dir(&self, path: &Path) -> Result<()> {
80 Ok(smol::fs::create_dir_all(path).await?)
81 }
82
83 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
84 let mut open_options = smol::fs::OpenOptions::new();
85 open_options.write(true).create(true);
86 if options.overwrite {
87 open_options.truncate(true);
88 } else if !options.ignore_if_exists {
89 open_options.create_new(true);
90 }
91 open_options.open(path).await?;
92 Ok(())
93 }
94
95 async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
96 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
97 if options.ignore_if_exists {
98 return Ok(());
99 } else {
100 return Err(anyhow!("{target:?} already exists"));
101 }
102 }
103
104 let metadata = smol::fs::metadata(source).await?;
105 let _ = smol::fs::remove_dir_all(target).await;
106 if metadata.is_dir() {
107 self.create_dir(target).await?;
108 let mut children = smol::fs::read_dir(source).await?;
109 while let Some(child) = children.next().await {
110 if let Ok(child) = child {
111 let child_source_path = child.path();
112 let child_target_path = target.join(child.file_name());
113 self.copy(&child_source_path, &child_target_path, options)
114 .await?;
115 }
116 }
117 } else {
118 smol::fs::copy(source, target).await?;
119 }
120
121 Ok(())
122 }
123
124 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
125 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
126 if options.ignore_if_exists {
127 return Ok(());
128 } else {
129 return Err(anyhow!("{target:?} already exists"));
130 }
131 }
132
133 smol::fs::rename(source, target).await?;
134 Ok(())
135 }
136
137 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
138 let result = if options.recursive {
139 smol::fs::remove_dir_all(path).await
140 } else {
141 smol::fs::remove_dir(path).await
142 };
143 match result {
144 Ok(()) => Ok(()),
145 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
146 Ok(())
147 }
148 Err(err) => Err(err)?,
149 }
150 }
151
152 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
153 match smol::fs::remove_file(path).await {
154 Ok(()) => Ok(()),
155 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
156 Ok(())
157 }
158 Err(err) => Err(err)?,
159 }
160 }
161
162 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
163 Ok(Box::new(std::fs::File::open(path)?))
164 }
165
166 async fn load(&self, path: &Path) -> Result<String> {
167 let mut file = smol::fs::File::open(path).await?;
168 let mut text = String::new();
169 file.read_to_string(&mut text).await?;
170 Ok(text)
171 }
172
173 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
174 let buffer_size = text.summary().bytes.min(10 * 1024);
175 let file = smol::fs::File::create(path).await?;
176 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
177 for chunk in chunks(text, line_ending) {
178 writer.write_all(chunk.as_bytes()).await?;
179 }
180 writer.flush().await?;
181 Ok(())
182 }
183
184 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
185 Ok(smol::fs::canonicalize(path).await?)
186 }
187
188 async fn is_file(&self, path: &Path) -> bool {
189 smol::fs::metadata(path)
190 .await
191 .map_or(false, |metadata| metadata.is_file())
192 }
193
194 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
195 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
196 Ok(metadata) => metadata,
197 Err(err) => {
198 return match (err.kind(), err.raw_os_error()) {
199 (io::ErrorKind::NotFound, _) => Ok(None),
200 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
201 _ => Err(anyhow::Error::new(err)),
202 }
203 }
204 };
205
206 let is_symlink = symlink_metadata.file_type().is_symlink();
207 let metadata = if is_symlink {
208 smol::fs::metadata(path).await?
209 } else {
210 symlink_metadata
211 };
212 Ok(Some(Metadata {
213 inode: metadata.ino(),
214 mtime: metadata.modified().unwrap(),
215 is_symlink,
216 is_dir: metadata.file_type().is_dir(),
217 }))
218 }
219
220 async fn read_dir(
221 &self,
222 path: &Path,
223 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
224 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
225 Ok(entry) => Ok(entry.path()),
226 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
227 });
228 Ok(Box::pin(result))
229 }
230
231 async fn watch(
232 &self,
233 path: &Path,
234 latency: Duration,
235 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
236 let (tx, rx) = smol::channel::unbounded();
237 let (stream, handle) = EventStream::new(&[path], latency);
238 std::mem::forget(handle);
239 std::thread::spawn(move || {
240 stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
241 });
242 Box::pin(rx)
243 }
244
245 fn is_fake(&self) -> bool {
246 false
247 }
248 #[cfg(any(test, feature = "test-support"))]
249 fn as_fake(&self) -> &FakeFs {
250 panic!("called `RealFs::as_fake`")
251 }
252}
253
254#[cfg(any(test, feature = "test-support"))]
255#[derive(Clone, Debug)]
256struct FakeFsEntry {
257 metadata: Metadata,
258 content: Option<String>,
259}
260
261#[cfg(any(test, feature = "test-support"))]
262struct FakeFsState {
263 entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
264 next_inode: u64,
265 event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
266}
267
268#[cfg(any(test, feature = "test-support"))]
269impl FakeFsState {
270 fn validate_path(&self, path: &Path) -> Result<()> {
271 if path.is_absolute()
272 && path
273 .parent()
274 .and_then(|path| self.entries.get(path))
275 .map_or(false, |e| e.metadata.is_dir)
276 {
277 Ok(())
278 } else {
279 Err(anyhow!("invalid path {:?}", path))
280 }
281 }
282
283 async fn emit_event<I, T>(&mut self, paths: I)
284 where
285 I: IntoIterator<Item = T>,
286 T: Into<PathBuf>,
287 {
288 let events = paths
289 .into_iter()
290 .map(|path| fsevent::Event {
291 event_id: 0,
292 flags: fsevent::StreamFlags::empty(),
293 path: path.into(),
294 })
295 .collect::<Vec<_>>();
296
297 self.event_txs.retain(|tx| {
298 let _ = tx.try_send(events.clone());
299 !tx.is_closed()
300 });
301 }
302}
303
304#[cfg(any(test, feature = "test-support"))]
305pub struct FakeFs {
306 // Use an unfair lock to ensure tests are deterministic.
307 state: futures::lock::Mutex<FakeFsState>,
308 executor: std::sync::Weak<gpui::executor::Background>,
309}
310
311#[cfg(any(test, feature = "test-support"))]
312impl FakeFs {
313 pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
314 let mut entries = std::collections::BTreeMap::new();
315 entries.insert(
316 Path::new("/").to_path_buf(),
317 FakeFsEntry {
318 metadata: Metadata {
319 inode: 0,
320 mtime: SystemTime::now(),
321 is_dir: true,
322 is_symlink: false,
323 },
324 content: None,
325 },
326 );
327 std::sync::Arc::new(Self {
328 executor: std::sync::Arc::downgrade(&executor),
329 state: futures::lock::Mutex::new(FakeFsState {
330 entries,
331 next_inode: 1,
332 event_txs: Default::default(),
333 }),
334 })
335 }
336
337 pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
338 let mut state = self.state.lock().await;
339 let path = path.as_ref();
340 state.validate_path(path).unwrap();
341
342 let inode = state.next_inode;
343 state.next_inode += 1;
344 state.entries.insert(
345 path.to_path_buf(),
346 FakeFsEntry {
347 metadata: Metadata {
348 inode,
349 mtime: SystemTime::now(),
350 is_dir: false,
351 is_symlink: false,
352 },
353 content: Some(content),
354 },
355 );
356 state.emit_event(&[path]).await;
357 }
358
359 #[must_use]
360 pub fn insert_tree<'a>(
361 &'a self,
362 path: impl 'a + AsRef<Path> + Send,
363 tree: serde_json::Value,
364 ) -> futures::future::BoxFuture<'a, ()> {
365 use futures::FutureExt as _;
366 use serde_json::Value::*;
367
368 async move {
369 let path = path.as_ref();
370
371 match tree {
372 Object(map) => {
373 self.create_dir(path).await.unwrap();
374 for (name, contents) in map {
375 let mut path = PathBuf::from(path);
376 path.push(name);
377 self.insert_tree(&path, contents).await;
378 }
379 }
380 Null => {
381 self.create_dir(&path).await.unwrap();
382 }
383 String(contents) => {
384 self.insert_file(&path, contents).await;
385 }
386 _ => {
387 panic!("JSON object must contain only objects, strings, or null");
388 }
389 }
390 }
391 .boxed()
392 }
393
394 pub async fn files(&self) -> Vec<PathBuf> {
395 self.state
396 .lock()
397 .await
398 .entries
399 .iter()
400 .filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone()))
401 .collect()
402 }
403
404 async fn simulate_random_delay(&self) {
405 self.executor
406 .upgrade()
407 .expect("executor has been dropped")
408 .simulate_random_delay()
409 .await;
410 }
411}
412
413#[cfg(any(test, feature = "test-support"))]
414#[async_trait::async_trait]
415impl Fs for FakeFs {
416 async fn create_dir(&self, path: &Path) -> Result<()> {
417 self.simulate_random_delay().await;
418 let state = &mut *self.state.lock().await;
419 let path = normalize_path(path);
420 let mut ancestor_path = PathBuf::new();
421 let mut created_dir_paths = Vec::new();
422 for component in path.components() {
423 ancestor_path.push(component);
424 let entry = state
425 .entries
426 .entry(ancestor_path.clone())
427 .or_insert_with(|| {
428 let inode = state.next_inode;
429 state.next_inode += 1;
430 created_dir_paths.push(ancestor_path.clone());
431 FakeFsEntry {
432 metadata: Metadata {
433 inode,
434 mtime: SystemTime::now(),
435 is_dir: true,
436 is_symlink: false,
437 },
438 content: None,
439 }
440 });
441 if !entry.metadata.is_dir {
442 return Err(anyhow!(
443 "cannot create directory because {:?} is a file",
444 ancestor_path
445 ));
446 }
447 }
448 state.emit_event(&created_dir_paths).await;
449
450 Ok(())
451 }
452
453 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
454 self.simulate_random_delay().await;
455 let mut state = self.state.lock().await;
456 let path = normalize_path(path);
457 state.validate_path(&path)?;
458 if let Some(entry) = state.entries.get_mut(&path) {
459 if entry.metadata.is_dir || entry.metadata.is_symlink {
460 return Err(anyhow!(
461 "cannot create file because {:?} is a dir or a symlink",
462 path
463 ));
464 }
465
466 if options.overwrite {
467 entry.metadata.mtime = SystemTime::now();
468 entry.content = Some(Default::default());
469 } else if !options.ignore_if_exists {
470 return Err(anyhow!(
471 "cannot create file because {:?} already exists",
472 &path
473 ));
474 }
475 } else {
476 let inode = state.next_inode;
477 state.next_inode += 1;
478 let entry = FakeFsEntry {
479 metadata: Metadata {
480 inode,
481 mtime: SystemTime::now(),
482 is_dir: false,
483 is_symlink: false,
484 },
485 content: Some(Default::default()),
486 };
487 state.entries.insert(path.to_path_buf(), entry);
488 }
489 state.emit_event(&[path]).await;
490
491 Ok(())
492 }
493
494 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
495 let source = normalize_path(source);
496 let target = normalize_path(target);
497
498 let mut state = self.state.lock().await;
499 state.validate_path(&source)?;
500 state.validate_path(&target)?;
501
502 if !options.overwrite && state.entries.contains_key(&target) {
503 if options.ignore_if_exists {
504 return Ok(());
505 } else {
506 return Err(anyhow!("{target:?} already exists"));
507 }
508 }
509
510 let mut removed = Vec::new();
511 state.entries.retain(|path, entry| {
512 if let Ok(relative_path) = path.strip_prefix(&source) {
513 removed.push((relative_path.to_path_buf(), entry.clone()));
514 false
515 } else {
516 true
517 }
518 });
519
520 for (relative_path, entry) in removed {
521 let new_path = normalize_path(&target.join(relative_path));
522 state.entries.insert(new_path, entry);
523 }
524
525 state.emit_event(&[source, target]).await;
526 Ok(())
527 }
528
529 async fn copy(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> {
530 let source = normalize_path(source);
531 let target = normalize_path(target);
532
533 let mut state = self.state.lock().await;
534 state.validate_path(&source)?;
535 state.validate_path(&target)?;
536
537 if !options.overwrite && state.entries.contains_key(&target) {
538 if options.ignore_if_exists {
539 return Ok(());
540 } else {
541 return Err(anyhow!("{target:?} already exists"));
542 }
543 }
544
545 let mut new_entries = Vec::new();
546 for (path, entry) in &state.entries {
547 if let Ok(relative_path) = path.strip_prefix(&source) {
548 new_entries.push((relative_path.to_path_buf(), entry.clone()));
549 }
550 }
551
552 let mut events = Vec::new();
553 for (relative_path, entry) in new_entries {
554 let new_path = normalize_path(&target.join(relative_path));
555 events.push(new_path.clone());
556 state.entries.insert(new_path, entry);
557 }
558
559 state.emit_event(&events).await;
560 Ok(())
561 }
562
563 async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
564 let dir_path = normalize_path(dir_path);
565 let mut state = self.state.lock().await;
566 state.validate_path(&dir_path)?;
567 if let Some(entry) = state.entries.get(&dir_path) {
568 if !entry.metadata.is_dir {
569 return Err(anyhow!(
570 "cannot remove {dir_path:?} because it is not a dir"
571 ));
572 }
573
574 if !options.recursive {
575 let descendants = state
576 .entries
577 .keys()
578 .filter(|path| path.starts_with(path))
579 .count();
580 if descendants > 1 {
581 return Err(anyhow!("{dir_path:?} is not empty"));
582 }
583 }
584
585 state.entries.retain(|path, _| !path.starts_with(&dir_path));
586 state.emit_event(&[dir_path]).await;
587 } else if !options.ignore_if_not_exists {
588 return Err(anyhow!("{dir_path:?} does not exist"));
589 }
590
591 Ok(())
592 }
593
594 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
595 let path = normalize_path(path);
596 let mut state = self.state.lock().await;
597 state.validate_path(&path)?;
598 if let Some(entry) = state.entries.get(&path) {
599 if entry.metadata.is_dir {
600 return Err(anyhow!("cannot remove {path:?} because it is not a file"));
601 }
602
603 state.entries.remove(&path);
604 state.emit_event(&[path]).await;
605 } else if !options.ignore_if_not_exists {
606 return Err(anyhow!("{path:?} does not exist"));
607 }
608 Ok(())
609 }
610
611 async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
612 let text = self.load(path).await?;
613 Ok(Box::new(io::Cursor::new(text)))
614 }
615
616 async fn load(&self, path: &Path) -> Result<String> {
617 let path = normalize_path(path);
618 self.simulate_random_delay().await;
619 let state = self.state.lock().await;
620 let text = state
621 .entries
622 .get(&path)
623 .and_then(|e| e.content.as_ref())
624 .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
625 Ok(text.clone())
626 }
627
628 async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
629 self.simulate_random_delay().await;
630 let mut state = self.state.lock().await;
631 let path = normalize_path(path);
632 state.validate_path(&path)?;
633 let content = chunks(text, line_ending).collect();
634 if let Some(entry) = state.entries.get_mut(&path) {
635 if entry.metadata.is_dir {
636 Err(anyhow!("cannot overwrite a directory with a file"))
637 } else {
638 entry.content = Some(content);
639 entry.metadata.mtime = SystemTime::now();
640 state.emit_event(&[path]).await;
641 Ok(())
642 }
643 } else {
644 let inode = state.next_inode;
645 state.next_inode += 1;
646 let entry = FakeFsEntry {
647 metadata: Metadata {
648 inode,
649 mtime: SystemTime::now(),
650 is_dir: false,
651 is_symlink: false,
652 },
653 content: Some(content),
654 };
655 state.entries.insert(path.to_path_buf(), entry);
656 state.emit_event(&[path]).await;
657 Ok(())
658 }
659 }
660
661 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
662 self.simulate_random_delay().await;
663 Ok(normalize_path(path))
664 }
665
666 async fn is_file(&self, path: &Path) -> bool {
667 let path = normalize_path(path);
668 self.simulate_random_delay().await;
669 let state = self.state.lock().await;
670 state
671 .entries
672 .get(&path)
673 .map_or(false, |entry| !entry.metadata.is_dir)
674 }
675
676 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
677 self.simulate_random_delay().await;
678 let state = self.state.lock().await;
679 let path = normalize_path(path);
680 Ok(state.entries.get(&path).map(|entry| entry.metadata.clone()))
681 }
682
683 async fn read_dir(
684 &self,
685 abs_path: &Path,
686 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
687 use futures::{future, stream};
688 self.simulate_random_delay().await;
689 let state = self.state.lock().await;
690 let abs_path = normalize_path(abs_path);
691 Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
692 move |(child_path, _)| {
693 future::ready(if child_path.parent() == Some(&abs_path) {
694 Some(Ok(child_path))
695 } else {
696 None
697 })
698 },
699 )))
700 }
701
702 async fn watch(
703 &self,
704 path: &Path,
705 _: Duration,
706 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
707 let mut state = self.state.lock().await;
708 self.simulate_random_delay().await;
709 let (tx, rx) = smol::channel::unbounded();
710 state.event_txs.push(tx);
711 let path = path.to_path_buf();
712 let executor = self.executor.clone();
713 Box::pin(futures::StreamExt::filter(rx, move |events| {
714 let result = events.iter().any(|event| event.path.starts_with(&path));
715 let executor = executor.clone();
716 async move {
717 if let Some(executor) = executor.clone().upgrade() {
718 executor.simulate_random_delay().await;
719 }
720 result
721 }
722 }))
723 }
724
725 fn is_fake(&self) -> bool {
726 true
727 }
728
729 #[cfg(any(test, feature = "test-support"))]
730 fn as_fake(&self) -> &FakeFs {
731 self
732 }
733}
734
735fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
736 rope.chunks().flat_map(move |chunk| {
737 let mut newline = false;
738 chunk.split('\n').flat_map(move |line| {
739 let ending = if newline {
740 Some(line_ending.as_str())
741 } else {
742 None
743 };
744 newline = true;
745 ending.into_iter().chain([line])
746 })
747 })
748}
749
750pub fn normalize_path(path: &Path) -> PathBuf {
751 let mut components = path.components().peekable();
752 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
753 components.next();
754 PathBuf::from(c.as_os_str())
755 } else {
756 PathBuf::new()
757 };
758
759 for component in components {
760 match component {
761 Component::Prefix(..) => unreachable!(),
762 Component::RootDir => {
763 ret.push(component.as_os_str());
764 }
765 Component::CurDir => {}
766 Component::ParentDir => {
767 ret.pop();
768 }
769 Component::Normal(c) => {
770 ret.push(c);
771 }
772 }
773 }
774 ret
775}