Move Fs trait into its own module

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

zed/src/worktree.rs    | 456 -------------------------------------------
zed/src/worktree/fs.rs | 455 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 460 insertions(+), 451 deletions(-)

Detailed changes

zed/src/worktree.rs 🔗

@@ -1,4 +1,5 @@
 mod char_bag;
+mod fs;
 mod fuzzy;
 mod ignore;
 
@@ -12,9 +13,8 @@ use crate::{
     util::Bias,
 };
 use ::ignore::gitignore::Gitignore;
-use anyhow::{anyhow, Context, Result};
-use atomic::Ordering::SeqCst;
-use fsevent::EventStream;
+use anyhow::{anyhow, Result};
+pub use fs::*;
 use futures::{Stream, StreamExt};
 pub use fuzzy::{match_paths, PathMatch};
 use gpui::{
@@ -27,10 +27,7 @@ use postage::{
     prelude::{Sink as _, Stream as _},
     watch,
 };
-use smol::{
-    channel::{self, Sender},
-    io::{AsyncReadExt, AsyncWriteExt},
-};
+use smol::channel::{self, Sender};
 use std::{
     cmp::{self, Ordering},
     collections::HashMap,
@@ -38,15 +35,9 @@ use std::{
     ffi::{OsStr, OsString},
     fmt,
     future::Future,
-    io,
     ops::Deref,
-    os::unix::fs::MetadataExt,
     path::{Path, PathBuf},
-    pin::Pin,
-    sync::{
-        atomic::{self, AtomicUsize},
-        Arc,
-    },
+    sync::{atomic::AtomicUsize, Arc},
     time::{Duration, SystemTime},
 };
 use zrpc::{ForegroundRouter, PeerId, TypedEnvelope};
@@ -66,443 +57,6 @@ pub fn init(cx: &mut MutableAppContext, rpc: &rpc::Client, router: &mut Foregrou
     rpc.on_message(router, remote::save_buffer, cx);
 }
 
-#[async_trait::async_trait]
-pub trait Fs: Send + Sync {
-    async fn entry(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &AtomicUsize,
-        path: Arc<Path>,
-        abs_path: &Path,
-    ) -> Result<Option<Entry>>;
-    async fn child_entries<'a>(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &'a AtomicUsize,
-        path: &'a Path,
-        abs_path: &'a Path,
-    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>>;
-    async fn load(&self, path: &Path) -> Result<String>;
-    async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
-    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
-    async fn is_file(&self, path: &Path) -> bool;
-    async fn watch(
-        &self,
-        path: &Path,
-        latency: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
-    fn is_fake(&self) -> bool;
-}
-
-pub struct RealFs;
-
-#[async_trait::async_trait]
-impl Fs for RealFs {
-    async fn entry(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &AtomicUsize,
-        path: Arc<Path>,
-        abs_path: &Path,
-    ) -> Result<Option<Entry>> {
-        let metadata = match smol::fs::metadata(&abs_path).await {
-            Err(err) => {
-                return match (err.kind(), err.raw_os_error()) {
-                    (io::ErrorKind::NotFound, _) => Ok(None),
-                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
-                    _ => Err(anyhow::Error::new(err)),
-                }
-            }
-            Ok(metadata) => metadata,
-        };
-        let inode = metadata.ino();
-        let mtime = metadata.modified()?;
-        let is_symlink = smol::fs::symlink_metadata(&abs_path)
-            .await
-            .context("failed to read symlink metadata")?
-            .file_type()
-            .is_symlink();
-
-        let entry = Entry {
-            id: next_entry_id.fetch_add(1, SeqCst),
-            kind: if metadata.file_type().is_dir() {
-                EntryKind::PendingDir
-            } else {
-                EntryKind::File(char_bag_for_path(root_char_bag, &path))
-            },
-            path: Arc::from(path),
-            inode,
-            mtime,
-            is_symlink,
-            is_ignored: false,
-        };
-
-        Ok(Some(entry))
-    }
-
-    async fn child_entries<'a>(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &'a AtomicUsize,
-        path: &'a Path,
-        abs_path: &'a Path,
-    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>> {
-        let entries = smol::fs::read_dir(abs_path).await?;
-        Ok(entries
-            .then(move |entry| async move {
-                let child_entry = entry?;
-                let child_name = child_entry.file_name();
-                let child_path: Arc<Path> = path.join(&child_name).into();
-                let child_abs_path = abs_path.join(&child_name);
-                let child_is_symlink = child_entry.metadata().await?.file_type().is_symlink();
-                let child_metadata = smol::fs::metadata(child_abs_path).await?;
-                let child_inode = child_metadata.ino();
-                let child_mtime = child_metadata.modified()?;
-                Ok(Entry {
-                    id: next_entry_id.fetch_add(1, SeqCst),
-                    kind: if child_metadata.file_type().is_dir() {
-                        EntryKind::PendingDir
-                    } else {
-                        EntryKind::File(char_bag_for_path(root_char_bag, &child_path))
-                    },
-                    path: child_path,
-                    inode: child_inode,
-                    mtime: child_mtime,
-                    is_symlink: child_is_symlink,
-                    is_ignored: false,
-                })
-            })
-            .boxed())
-    }
-
-    async fn load(&self, path: &Path) -> Result<String> {
-        let mut file = smol::fs::File::open(path).await?;
-        let mut text = String::new();
-        file.read_to_string(&mut text).await?;
-        Ok(text)
-    }
-
-    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
-        let buffer_size = text.summary().bytes.min(10 * 1024);
-        let file = smol::fs::File::create(path).await?;
-        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
-        for chunk in text.chunks() {
-            writer.write_all(chunk.as_bytes()).await?;
-        }
-        writer.flush().await?;
-        Ok(())
-    }
-
-    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
-        Ok(smol::fs::canonicalize(path).await?)
-    }
-
-    async fn is_file(&self, path: &Path) -> bool {
-        smol::fs::metadata(path)
-            .await
-            .map_or(false, |metadata| metadata.is_file())
-    }
-
-    async fn watch(
-        &self,
-        path: &Path,
-        latency: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
-        let (mut tx, rx) = postage::mpsc::channel(64);
-        let (stream, handle) = EventStream::new(&[path], latency);
-        std::mem::forget(handle);
-        std::thread::spawn(move || {
-            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
-        });
-        Box::pin(rx)
-    }
-
-    fn is_fake(&self) -> bool {
-        false
-    }
-}
-
-#[derive(Clone, Debug)]
-struct FakeFsEntry {
-    inode: u64,
-    mtime: SystemTime,
-    is_dir: bool,
-    is_symlink: bool,
-    content: Option<String>,
-}
-
-#[cfg(any(test, feature = "test-support"))]
-struct FakeFsState {
-    entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
-    next_inode: u64,
-    events_tx: postage::broadcast::Sender<Vec<fsevent::Event>>,
-}
-
-#[cfg(any(test, feature = "test-support"))]
-impl FakeFsState {
-    fn validate_path(&self, path: &Path) -> Result<()> {
-        if path.is_absolute()
-            && path
-                .parent()
-                .and_then(|path| self.entries.get(path))
-                .map_or(false, |e| e.is_dir)
-        {
-            Ok(())
-        } else {
-            Err(anyhow!("invalid path {:?}", path))
-        }
-    }
-
-    async fn emit_event(&mut self, paths: &[&Path]) {
-        let events = paths
-            .iter()
-            .map(|path| fsevent::Event {
-                event_id: 0,
-                flags: fsevent::StreamFlags::empty(),
-                path: path.to_path_buf(),
-            })
-            .collect();
-
-        let _ = self.events_tx.send(events).await;
-    }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-pub struct FakeFs {
-    // Use an unfair lock to ensure tests are deterministic.
-    state: futures::lock::Mutex<FakeFsState>,
-}
-
-#[cfg(any(test, feature = "test-support"))]
-impl FakeFs {
-    pub fn new() -> Self {
-        let (events_tx, _) = postage::broadcast::channel(2048);
-        let mut entries = std::collections::BTreeMap::new();
-        entries.insert(
-            Path::new("/").to_path_buf(),
-            FakeFsEntry {
-                inode: 0,
-                mtime: SystemTime::now(),
-                is_dir: true,
-                is_symlink: false,
-                content: None,
-            },
-        );
-        Self {
-            state: futures::lock::Mutex::new(FakeFsState {
-                entries,
-                next_inode: 1,
-                events_tx,
-            }),
-        }
-    }
-
-    pub async fn insert_dir(&self, path: impl AsRef<Path>) -> Result<()> {
-        let mut state = self.state.lock().await;
-        let path = path.as_ref();
-        state.validate_path(path)?;
-
-        let inode = state.next_inode;
-        state.next_inode += 1;
-        state.entries.insert(
-            path.to_path_buf(),
-            FakeFsEntry {
-                inode,
-                mtime: SystemTime::now(),
-                is_dir: true,
-                is_symlink: false,
-                content: None,
-            },
-        );
-        state.emit_event(&[path]).await;
-        Ok(())
-    }
-
-    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
-        let mut state = self.state.lock().await;
-        let path = path.as_ref();
-        state.validate_path(path)?;
-
-        let inode = state.next_inode;
-        state.next_inode += 1;
-        state.entries.insert(
-            path.to_path_buf(),
-            FakeFsEntry {
-                inode,
-                mtime: SystemTime::now(),
-                is_dir: false,
-                is_symlink: false,
-                content: Some(content),
-            },
-        );
-        state.emit_event(&[path]).await;
-        Ok(())
-    }
-
-    pub async fn remove(&self, path: &Path) -> Result<()> {
-        let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        state.entries.retain(|path, _| !path.starts_with(path));
-        state.emit_event(&[path]).await;
-        Ok(())
-    }
-
-    pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> {
-        let mut state = self.state.lock().await;
-        state.validate_path(source)?;
-        state.validate_path(target)?;
-        if state.entries.contains_key(target) {
-            Err(anyhow!("target path already exists"))
-        } else {
-            let mut removed = Vec::new();
-            state.entries.retain(|path, entry| {
-                if let Ok(relative_path) = path.strip_prefix(source) {
-                    removed.push((relative_path.to_path_buf(), entry.clone()));
-                    false
-                } else {
-                    true
-                }
-            });
-
-            for (relative_path, entry) in removed {
-                let new_path = target.join(relative_path);
-                state.entries.insert(new_path, entry);
-            }
-
-            state.emit_event(&[source, target]).await;
-            Ok(())
-        }
-    }
-}
-
-#[cfg(any(test, feature = "test-support"))]
-#[async_trait::async_trait]
-impl Fs for FakeFs {
-    async fn entry(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &AtomicUsize,
-        path: Arc<Path>,
-        abs_path: &Path,
-    ) -> Result<Option<Entry>> {
-        let state = self.state.lock().await;
-        if let Some(entry) = state.entries.get(abs_path) {
-            Ok(Some(Entry {
-                id: next_entry_id.fetch_add(1, SeqCst),
-                kind: if entry.is_dir {
-                    EntryKind::PendingDir
-                } else {
-                    EntryKind::File(char_bag_for_path(root_char_bag, &path))
-                },
-                path: Arc::from(path),
-                inode: entry.inode,
-                mtime: entry.mtime,
-                is_symlink: entry.is_symlink,
-                is_ignored: false,
-            }))
-        } else {
-            Ok(None)
-        }
-    }
-
-    async fn child_entries<'a>(
-        &self,
-        root_char_bag: CharBag,
-        next_entry_id: &'a AtomicUsize,
-        path: &'a Path,
-        abs_path: &'a Path,
-    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>> {
-        use futures::{future, stream};
-
-        let state = self.state.lock().await;
-        Ok(stream::iter(state.entries.clone())
-            .filter(move |(child_path, _)| future::ready(child_path.parent() == Some(abs_path)))
-            .then(move |(child_abs_path, child_entry)| async move {
-                smol::future::yield_now().await;
-                let child_path = Arc::from(path.join(child_abs_path.file_name().unwrap()));
-                Ok(Entry {
-                    id: next_entry_id.fetch_add(1, SeqCst),
-                    kind: if child_entry.is_dir {
-                        EntryKind::PendingDir
-                    } else {
-                        EntryKind::File(char_bag_for_path(root_char_bag, &child_path))
-                    },
-                    path: child_path,
-                    inode: child_entry.inode,
-                    mtime: child_entry.mtime,
-                    is_symlink: child_entry.is_symlink,
-                    is_ignored: false,
-                })
-            })
-            .boxed())
-    }
-
-    async fn load(&self, path: &Path) -> Result<String> {
-        let state = self.state.lock().await;
-        let text = state
-            .entries
-            .get(path)
-            .and_then(|e| e.content.as_ref())
-            .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
-        Ok(text.clone())
-    }
-
-    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
-        let mut state = self.state.lock().await;
-        state.validate_path(path)?;
-        if let Some(entry) = state.entries.get_mut(path) {
-            if entry.is_dir {
-                Err(anyhow!("cannot overwrite a directory with a file"))
-            } else {
-                entry.content = Some(text.chunks().collect());
-                entry.mtime = SystemTime::now();
-                state.emit_event(&[path]).await;
-                Ok(())
-            }
-        } else {
-            let inode = state.next_inode;
-            state.next_inode += 1;
-            let entry = FakeFsEntry {
-                inode,
-                mtime: SystemTime::now(),
-                is_dir: false,
-                is_symlink: false,
-                content: Some(text.chunks().collect()),
-            };
-            state.entries.insert(path.to_path_buf(), entry);
-            state.emit_event(&[path]).await;
-            Ok(())
-        }
-    }
-
-    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
-        Ok(path.to_path_buf())
-    }
-
-    async fn is_file(&self, path: &Path) -> bool {
-        let state = self.state.lock().await;
-        state.entries.get(path).map_or(false, |entry| !entry.is_dir)
-    }
-
-    async fn watch(
-        &self,
-        path: &Path,
-        _: Duration,
-    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
-        let state = self.state.lock().await;
-        let rx = state.events_tx.subscribe();
-        let path = path.to_path_buf();
-        Box::pin(futures::StreamExt::filter(rx, move |events| {
-            let result = events.iter().any(|event| event.path.starts_with(&path));
-            async move { result }
-        }))
-    }
-
-    fn is_fake(&self) -> bool {
-        true
-    }
-}
-
 #[derive(Clone, Debug)]
 enum ScanState {
     Idle,

zed/src/worktree/fs.rs 🔗

@@ -0,0 +1,455 @@
+use super::{char_bag::CharBag, char_bag_for_path, Entry, EntryKind, Rope};
+use anyhow::{anyhow, Context, Result};
+use atomic::Ordering::SeqCst;
+use fsevent::EventStream;
+use futures::{Stream, StreamExt};
+use postage::prelude::Sink as _;
+use smol::io::{AsyncReadExt, AsyncWriteExt};
+use std::{
+    io,
+    os::unix::fs::MetadataExt,
+    path::{Path, PathBuf},
+    pin::Pin,
+    sync::{
+        atomic::{self, AtomicUsize},
+        Arc,
+    },
+    time::{Duration, SystemTime},
+};
+
+#[async_trait::async_trait]
+pub trait Fs: Send + Sync {
+    async fn entry(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &AtomicUsize,
+        path: Arc<Path>,
+        abs_path: &Path,
+    ) -> Result<Option<Entry>>;
+    async fn child_entries<'a>(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &'a AtomicUsize,
+        path: &'a Path,
+        abs_path: &'a Path,
+    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>>;
+    async fn load(&self, path: &Path) -> Result<String>;
+    async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
+    async fn is_file(&self, path: &Path) -> bool;
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
+    fn is_fake(&self) -> bool;
+}
+
+pub struct RealFs;
+
+#[async_trait::async_trait]
+impl Fs for RealFs {
+    async fn entry(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &AtomicUsize,
+        path: Arc<Path>,
+        abs_path: &Path,
+    ) -> Result<Option<Entry>> {
+        let metadata = match smol::fs::metadata(&abs_path).await {
+            Err(err) => {
+                return match (err.kind(), err.raw_os_error()) {
+                    (io::ErrorKind::NotFound, _) => Ok(None),
+                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
+                    _ => Err(anyhow::Error::new(err)),
+                }
+            }
+            Ok(metadata) => metadata,
+        };
+        let inode = metadata.ino();
+        let mtime = metadata.modified()?;
+        let is_symlink = smol::fs::symlink_metadata(&abs_path)
+            .await
+            .context("failed to read symlink metadata")?
+            .file_type()
+            .is_symlink();
+
+        let entry = Entry {
+            id: next_entry_id.fetch_add(1, SeqCst),
+            kind: if metadata.file_type().is_dir() {
+                EntryKind::PendingDir
+            } else {
+                EntryKind::File(char_bag_for_path(root_char_bag, &path))
+            },
+            path: Arc::from(path),
+            inode,
+            mtime,
+            is_symlink,
+            is_ignored: false,
+        };
+
+        Ok(Some(entry))
+    }
+
+    async fn child_entries<'a>(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &'a AtomicUsize,
+        path: &'a Path,
+        abs_path: &'a Path,
+    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>> {
+        let entries = smol::fs::read_dir(abs_path).await?;
+        Ok(entries
+            .then(move |entry| async move {
+                let child_entry = entry?;
+                let child_name = child_entry.file_name();
+                let child_path: Arc<Path> = path.join(&child_name).into();
+                let child_abs_path = abs_path.join(&child_name);
+                let child_is_symlink = child_entry.metadata().await?.file_type().is_symlink();
+                let child_metadata = smol::fs::metadata(child_abs_path).await?;
+                let child_inode = child_metadata.ino();
+                let child_mtime = child_metadata.modified()?;
+                Ok(Entry {
+                    id: next_entry_id.fetch_add(1, SeqCst),
+                    kind: if child_metadata.file_type().is_dir() {
+                        EntryKind::PendingDir
+                    } else {
+                        EntryKind::File(char_bag_for_path(root_char_bag, &child_path))
+                    },
+                    path: child_path,
+                    inode: child_inode,
+                    mtime: child_mtime,
+                    is_symlink: child_is_symlink,
+                    is_ignored: false,
+                })
+            })
+            .boxed())
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let mut file = smol::fs::File::open(path).await?;
+        let mut text = String::new();
+        file.read_to_string(&mut text).await?;
+        Ok(text)
+    }
+
+    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
+        let buffer_size = text.summary().bytes.min(10 * 1024);
+        let file = smol::fs::File::create(path).await?;
+        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
+        for chunk in text.chunks() {
+            writer.write_all(chunk.as_bytes()).await?;
+        }
+        writer.flush().await?;
+        Ok(())
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        Ok(smol::fs::canonicalize(path).await?)
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        smol::fs::metadata(path)
+            .await
+            .map_or(false, |metadata| metadata.is_file())
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        latency: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        let (mut tx, rx) = postage::mpsc::channel(64);
+        let (stream, handle) = EventStream::new(&[path], latency);
+        std::mem::forget(handle);
+        std::thread::spawn(move || {
+            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
+        });
+        Box::pin(rx)
+    }
+
+    fn is_fake(&self) -> bool {
+        false
+    }
+}
+
+#[derive(Clone, Debug)]
+struct FakeFsEntry {
+    inode: u64,
+    mtime: SystemTime,
+    is_dir: bool,
+    is_symlink: bool,
+    content: Option<String>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+struct FakeFsState {
+    entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
+    next_inode: u64,
+    events_tx: postage::broadcast::Sender<Vec<fsevent::Event>>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFsState {
+    fn validate_path(&self, path: &Path) -> Result<()> {
+        if path.is_absolute()
+            && path
+                .parent()
+                .and_then(|path| self.entries.get(path))
+                .map_or(false, |e| e.is_dir)
+        {
+            Ok(())
+        } else {
+            Err(anyhow!("invalid path {:?}", path))
+        }
+    }
+
+    async fn emit_event(&mut self, paths: &[&Path]) {
+        let events = paths
+            .iter()
+            .map(|path| fsevent::Event {
+                event_id: 0,
+                flags: fsevent::StreamFlags::empty(),
+                path: path.to_path_buf(),
+            })
+            .collect();
+
+        let _ = self.events_tx.send(events).await;
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeFs {
+    // Use an unfair lock to ensure tests are deterministic.
+    state: futures::lock::Mutex<FakeFsState>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeFs {
+    pub fn new() -> Self {
+        let (events_tx, _) = postage::broadcast::channel(2048);
+        let mut entries = std::collections::BTreeMap::new();
+        entries.insert(
+            Path::new("/").to_path_buf(),
+            FakeFsEntry {
+                inode: 0,
+                mtime: SystemTime::now(),
+                is_dir: true,
+                is_symlink: false,
+                content: None,
+            },
+        );
+        Self {
+            state: futures::lock::Mutex::new(FakeFsState {
+                entries,
+                next_inode: 1,
+                events_tx,
+            }),
+        }
+    }
+
+    pub async fn insert_dir(&self, path: impl AsRef<Path>) -> Result<()> {
+        let mut state = self.state.lock().await;
+        let path = path.as_ref();
+        state.validate_path(path)?;
+
+        let inode = state.next_inode;
+        state.next_inode += 1;
+        state.entries.insert(
+            path.to_path_buf(),
+            FakeFsEntry {
+                inode,
+                mtime: SystemTime::now(),
+                is_dir: true,
+                is_symlink: false,
+                content: None,
+            },
+        );
+        state.emit_event(&[path]).await;
+        Ok(())
+    }
+
+    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+        let mut state = self.state.lock().await;
+        let path = path.as_ref();
+        state.validate_path(path)?;
+
+        let inode = state.next_inode;
+        state.next_inode += 1;
+        state.entries.insert(
+            path.to_path_buf(),
+            FakeFsEntry {
+                inode,
+                mtime: SystemTime::now(),
+                is_dir: false,
+                is_symlink: false,
+                content: Some(content),
+            },
+        );
+        state.emit_event(&[path]).await;
+        Ok(())
+    }
+
+    pub async fn remove(&self, path: &Path) -> Result<()> {
+        let mut state = self.state.lock().await;
+        state.validate_path(path)?;
+        state.entries.retain(|path, _| !path.starts_with(path));
+        state.emit_event(&[path]).await;
+        Ok(())
+    }
+
+    pub async fn rename(&self, source: &Path, target: &Path) -> Result<()> {
+        let mut state = self.state.lock().await;
+        state.validate_path(source)?;
+        state.validate_path(target)?;
+        if state.entries.contains_key(target) {
+            Err(anyhow!("target path already exists"))
+        } else {
+            let mut removed = Vec::new();
+            state.entries.retain(|path, entry| {
+                if let Ok(relative_path) = path.strip_prefix(source) {
+                    removed.push((relative_path.to_path_buf(), entry.clone()));
+                    false
+                } else {
+                    true
+                }
+            });
+
+            for (relative_path, entry) in removed {
+                let new_path = target.join(relative_path);
+                state.entries.insert(new_path, entry);
+            }
+
+            state.emit_event(&[source, target]).await;
+            Ok(())
+        }
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+#[async_trait::async_trait]
+impl Fs for FakeFs {
+    async fn entry(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &AtomicUsize,
+        path: Arc<Path>,
+        abs_path: &Path,
+    ) -> Result<Option<Entry>> {
+        let state = self.state.lock().await;
+        if let Some(entry) = state.entries.get(abs_path) {
+            Ok(Some(Entry {
+                id: next_entry_id.fetch_add(1, SeqCst),
+                kind: if entry.is_dir {
+                    EntryKind::PendingDir
+                } else {
+                    EntryKind::File(char_bag_for_path(root_char_bag, &path))
+                },
+                path: Arc::from(path),
+                inode: entry.inode,
+                mtime: entry.mtime,
+                is_symlink: entry.is_symlink,
+                is_ignored: false,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    async fn child_entries<'a>(
+        &self,
+        root_char_bag: CharBag,
+        next_entry_id: &'a AtomicUsize,
+        path: &'a Path,
+        abs_path: &'a Path,
+    ) -> Result<Pin<Box<dyn 'a + Stream<Item = Result<Entry>> + Send>>> {
+        use futures::{future, stream};
+
+        let state = self.state.lock().await;
+        Ok(stream::iter(state.entries.clone())
+            .filter(move |(child_path, _)| future::ready(child_path.parent() == Some(abs_path)))
+            .then(move |(child_abs_path, child_entry)| async move {
+                smol::future::yield_now().await;
+                let child_path = Arc::from(path.join(child_abs_path.file_name().unwrap()));
+                Ok(Entry {
+                    id: next_entry_id.fetch_add(1, SeqCst),
+                    kind: if child_entry.is_dir {
+                        EntryKind::PendingDir
+                    } else {
+                        EntryKind::File(char_bag_for_path(root_char_bag, &child_path))
+                    },
+                    path: child_path,
+                    inode: child_entry.inode,
+                    mtime: child_entry.mtime,
+                    is_symlink: child_entry.is_symlink,
+                    is_ignored: false,
+                })
+            })
+            .boxed())
+    }
+
+    async fn load(&self, path: &Path) -> Result<String> {
+        let state = self.state.lock().await;
+        let text = state
+            .entries
+            .get(path)
+            .and_then(|e| e.content.as_ref())
+            .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
+        Ok(text.clone())
+    }
+
+    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
+        let mut state = self.state.lock().await;
+        state.validate_path(path)?;
+        if let Some(entry) = state.entries.get_mut(path) {
+            if entry.is_dir {
+                Err(anyhow!("cannot overwrite a directory with a file"))
+            } else {
+                entry.content = Some(text.chunks().collect());
+                entry.mtime = SystemTime::now();
+                state.emit_event(&[path]).await;
+                Ok(())
+            }
+        } else {
+            let inode = state.next_inode;
+            state.next_inode += 1;
+            let entry = FakeFsEntry {
+                inode,
+                mtime: SystemTime::now(),
+                is_dir: false,
+                is_symlink: false,
+                content: Some(text.chunks().collect()),
+            };
+            state.entries.insert(path.to_path_buf(), entry);
+            state.emit_event(&[path]).await;
+            Ok(())
+        }
+    }
+
+    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
+        Ok(path.to_path_buf())
+    }
+
+    async fn is_file(&self, path: &Path) -> bool {
+        let state = self.state.lock().await;
+        state.entries.get(path).map_or(false, |entry| !entry.is_dir)
+    }
+
+    async fn watch(
+        &self,
+        path: &Path,
+        _: Duration,
+    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
+        let state = self.state.lock().await;
+        let rx = state.events_tx.subscribe();
+        let path = path.to_path_buf();
+        Box::pin(futures::StreamExt::filter(rx, move |events| {
+            let result = events.iter().any(|event| event.path.starts_with(&path));
+            async move { result }
+        }))
+    }
+
+    fn is_fake(&self) -> bool {
+        true
+    }
+}