From 411e7df931d438f938fa25a4dbcbac846ea2a6cd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 13 Jul 2021 12:52:42 -0600 Subject: [PATCH] Move Fs trait into its own module Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 456 +---------------------------------------- zed/src/worktree/fs.rs | 455 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 460 insertions(+), 451 deletions(-) create mode 100644 zed/src/worktree/fs.rs diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index fff16084ccc8f7f3255afc688670bd0860ac1eec..25f2a1d42215100c376b4e11a469f332ef356650 100644 --- a/zed/src/worktree.rs +++ b/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, - abs_path: &Path, - ) -> Result>; - async fn child_entries<'a>( - &self, - root_char_bag: CharBag, - next_entry_id: &'a AtomicUsize, - path: &'a Path, - abs_path: &'a Path, - ) -> Result> + Send>>>; - async fn load(&self, path: &Path) -> Result; - async fn save(&self, path: &Path, text: &Rope) -> Result<()>; - async fn canonicalize(&self, path: &Path) -> Result; - async fn is_file(&self, path: &Path) -> bool; - async fn watch( - &self, - path: &Path, - latency: Duration, - ) -> Pin>>>; - 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, - abs_path: &Path, - ) -> Result> { - 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> + 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.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 { - 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 { - 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>>> { - 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, -} - -#[cfg(any(test, feature = "test-support"))] -struct FakeFsState { - entries: std::collections::BTreeMap, - next_inode: u64, - events_tx: postage::broadcast::Sender>, -} - -#[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, -} - -#[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) -> 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, 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, - abs_path: &Path, - ) -> Result> { - 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> + 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 { - 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 { - 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>>> { - 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, diff --git a/zed/src/worktree/fs.rs b/zed/src/worktree/fs.rs new file mode 100644 index 0000000000000000000000000000000000000000..5f108b784a74bcd445b36d9347f38db9701c1dfa --- /dev/null +++ b/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, + abs_path: &Path, + ) -> Result>; + async fn child_entries<'a>( + &self, + root_char_bag: CharBag, + next_entry_id: &'a AtomicUsize, + path: &'a Path, + abs_path: &'a Path, + ) -> Result> + Send>>>; + async fn load(&self, path: &Path) -> Result; + async fn save(&self, path: &Path, text: &Rope) -> Result<()>; + async fn canonicalize(&self, path: &Path) -> Result; + async fn is_file(&self, path: &Path) -> bool; + async fn watch( + &self, + path: &Path, + latency: Duration, + ) -> Pin>>>; + 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, + abs_path: &Path, + ) -> Result> { + 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> + 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.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 { + 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 { + 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>>> { + 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, +} + +#[cfg(any(test, feature = "test-support"))] +struct FakeFsState { + entries: std::collections::BTreeMap, + next_inode: u64, + events_tx: postage::broadcast::Sender>, +} + +#[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, +} + +#[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) -> 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, 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, + abs_path: &Path, + ) -> Result> { + 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> + 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 { + 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 { + 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>>> { + 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 + } +}