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