diff --git a/server/src/tests.rs b/server/src/tests.rs index 041a5adcea..0f35b5161b 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -15,11 +15,12 @@ use sqlx::{ use std::{path::Path, sync::Arc}; use zed::{ editor::Editor, + fs::{FakeFs, Fs as _}, language::LanguageRegistry, rpc::Client, settings, test::Channel, - worktree::{FakeFs, Fs as _, Worktree}, + worktree::Worktree, }; use zrpc::{ForegroundRouter, Peer, Router}; diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 6c1a0af436..c0ce0af2d7 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -2732,9 +2732,10 @@ impl ToPoint for usize { mod tests { use super::*; use crate::{ + fs::RealFs, test::{build_app_state, temp_tree}, util::RandomCharIter, - worktree::{RealFs, Worktree, WorktreeHandle}, + worktree::{Worktree, WorktreeHandle as _}, }; use gpui::ModelHandle; use rand::prelude::*; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 02ea62eaf6..2f5ad01101 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -457,9 +457,9 @@ mod tests { use super::*; use crate::{ editor, + fs::FakeFs, test::{build_app_state, temp_tree}, workspace::Workspace, - worktree::FakeFs, }; use serde_json::json; use std::fs; diff --git a/zed/src/worktree/fs.rs b/zed/src/fs.rs similarity index 63% rename from zed/src/worktree/fs.rs rename to zed/src/fs.rs index 405580ab97..e9d6d9230b 100644 --- a/zed/src/worktree/fs.rs +++ b/zed/src/fs.rs @@ -1,8 +1,7 @@ -use super::{char_bag::CharBag, char_bag_for_path, Entry, EntryKind, Rope}; -use anyhow::{anyhow, Context, Result}; -use atomic::Ordering::SeqCst; +use super::editor::Rope; +use anyhow::{anyhow, Result}; use fsevent::EventStream; -use futures::{future::BoxFuture, Stream, StreamExt}; +use futures::{Stream, StreamExt}; use postage::prelude::Sink as _; use smol::io::{AsyncReadExt, AsyncWriteExt}; use std::{ @@ -10,33 +9,20 @@ use std::{ 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 metadata(&self, path: &Path) -> Result>; + async fn read_dir( + &self, + path: &Path, + ) -> Result>>>>; async fn watch( &self, path: &Path, @@ -45,87 +31,18 @@ pub trait Fs: Send + Sync { fn is_fake(&self) -> bool; } +#[derive(Clone, Debug)] +pub struct Metadata { + pub inode: u64, + pub mtime: SystemTime, + pub is_symlink: bool, + pub is_dir: 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(); @@ -154,6 +71,43 @@ impl Fs for RealFs { .map_or(false, |metadata| metadata.is_file()) } + async fn metadata(&self, path: &Path) -> Result> { + let symlink_metadata = match smol::fs::symlink_metadata(path).await { + Ok(metadata) => metadata, + 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)), + } + } + }; + + let is_symlink = symlink_metadata.file_type().is_symlink(); + let metadata = if is_symlink { + smol::fs::metadata(path).await? + } else { + symlink_metadata + }; + Ok(Some(Metadata { + inode: metadata.ino(), + mtime: metadata.modified().unwrap(), + is_symlink, + is_dir: metadata.file_type().is_dir(), + })) + } + + async fn read_dir( + &self, + path: &Path, + ) -> Result>>>> { + let result = smol::fs::read_dir(path).await?.map(|entry| match entry { + Ok(entry) => Ok(entry.path()), + Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)), + }); + Ok(Box::pin(result)) + } + async fn watch( &self, path: &Path, @@ -175,10 +129,7 @@ impl Fs for RealFs { #[derive(Clone, Debug)] struct FakeFsEntry { - inode: u64, - mtime: SystemTime, - is_dir: bool, - is_symlink: bool, + metadata: Metadata, content: Option, } @@ -196,7 +147,7 @@ impl FakeFsState { && path .parent() .and_then(|path| self.entries.get(path)) - .map_or(false, |e| e.is_dir) + .map_or(false, |e| e.metadata.is_dir) { Ok(()) } else { @@ -232,10 +183,12 @@ impl FakeFs { entries.insert( Path::new("/").to_path_buf(), FakeFsEntry { - inode: 0, - mtime: SystemTime::now(), - is_dir: true, - is_symlink: false, + metadata: Metadata { + inode: 0, + mtime: SystemTime::now(), + is_dir: true, + is_symlink: false, + }, content: None, }, ); @@ -258,10 +211,12 @@ impl FakeFs { state.entries.insert( path.to_path_buf(), FakeFsEntry { - inode, - mtime: SystemTime::now(), - is_dir: true, - is_symlink: false, + metadata: Metadata { + inode, + mtime: SystemTime::now(), + is_dir: true, + is_symlink: false, + }, content: None, }, ); @@ -279,10 +234,12 @@ impl FakeFs { state.entries.insert( path.to_path_buf(), FakeFsEntry { - inode, - mtime: SystemTime::now(), - is_dir: false, - is_symlink: false, + metadata: Metadata { + inode, + mtime: SystemTime::now(), + is_dir: false, + is_symlink: false, + }, content: Some(content), }, ); @@ -295,7 +252,7 @@ impl FakeFs { &'a self, path: impl 'a + AsRef + Send, tree: serde_json::Value, - ) -> BoxFuture<'a, ()> { + ) -> futures::future::BoxFuture<'a, ()> { use futures::FutureExt as _; use serde_json::Value::*; @@ -364,65 +321,6 @@ impl FakeFs { #[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 @@ -437,11 +335,11 @@ impl Fs for FakeFs { let mut state = self.state.lock().await; state.validate_path(path)?; if let Some(entry) = state.entries.get_mut(path) { - if entry.is_dir { + if entry.metadata.is_dir { Err(anyhow!("cannot overwrite a directory with a file")) } else { entry.content = Some(text.chunks().collect()); - entry.mtime = SystemTime::now(); + entry.metadata.mtime = SystemTime::now(); state.emit_event(&[path]).await; Ok(()) } @@ -449,10 +347,12 @@ impl Fs for FakeFs { let inode = state.next_inode; state.next_inode += 1; let entry = FakeFsEntry { - inode, - mtime: SystemTime::now(), - is_dir: false, - is_symlink: false, + metadata: Metadata { + inode, + mtime: SystemTime::now(), + is_dir: false, + is_symlink: false, + }, content: Some(text.chunks().collect()), }; state.entries.insert(path.to_path_buf(), entry); @@ -467,7 +367,33 @@ impl Fs for FakeFs { 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) + state + .entries + .get(path) + .map_or(false, |entry| !entry.metadata.is_dir) + } + + async fn metadata(&self, path: &Path) -> Result> { + let state = self.state.lock().await; + Ok(state.entries.get(path).map(|entry| entry.metadata.clone())) + } + + async fn read_dir( + &self, + abs_path: &Path, + ) -> Result>>>> { + use futures::{future, stream}; + let state = self.state.lock().await; + let abs_path = abs_path.to_path_buf(); + Ok(Box::pin(stream::iter(state.entries.clone()).filter_map( + move |(child_path, _)| { + future::ready(if child_path.parent() == Some(&abs_path) { + Some(Ok(child_path)) + } else { + None + }) + }, + ))) } async fn watch( diff --git a/zed/src/lib.rs b/zed/src/lib.rs index d550c0ee49..efa0dd8288 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -3,6 +3,7 @@ use zrpc::ForegroundRouter; pub mod assets; pub mod editor; pub mod file_finder; +pub mod fs; pub mod language; pub mod menus; mod operation_queue; @@ -21,7 +22,7 @@ pub struct AppState { pub languages: std::sync::Arc, pub rpc_router: std::sync::Arc, pub rpc: rpc::Client, - pub fs: std::sync::Arc, + pub fs: std::sync::Arc, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index 4630f215cb..d69818a541 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -6,9 +6,11 @@ use log::LevelFilter; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ - self, assets, editor, file_finder, language, menus, rpc, settings, + self, assets, editor, file_finder, + fs::RealFs, + language, menus, rpc, settings, workspace::{self, OpenParams}, - worktree::{self, RealFs}, + worktree::{self}, AppState, }; use zrpc::ForegroundRouter; diff --git a/zed/src/test.rs b/zed/src/test.rs index c08f65937c..adc75100a0 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -1,6 +1,4 @@ -use crate::{ - language::LanguageRegistry, rpc, settings, time::ReplicaId, worktree::RealFs, AppState, -}; +use crate::{fs::RealFs, language::LanguageRegistry, rpc, settings, time::ReplicaId, AppState}; use gpui::AppContext; use std::{ path::{Path, PathBuf}, diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 28aea860dc..4086eddd82 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -3,10 +3,11 @@ pub mod pane_group; use crate::{ editor::{Buffer, Editor}, + fs::Fs, language::LanguageRegistry, rpc, settings::Settings, - worktree::{File, Fs, Worktree}, + worktree::{File, Worktree}, AppState, }; use anyhow::{anyhow, Result}; @@ -921,8 +922,9 @@ mod tests { use super::*; use crate::{ editor::Editor, + fs::FakeFs, test::{build_app_state, temp_tree}, - worktree::{FakeFs, WorktreeHandle}, + worktree::WorktreeHandle, }; use serde_json::json; use std::{collections::HashSet, fs}; diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 5580894110..8bbf7b4e41 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,11 +1,11 @@ mod char_bag; -mod fs; mod fuzzy; mod ignore; use self::{char_bag::CharBag, ignore::IgnoreStack}; use crate::{ editor::{self, Buffer, History, Operation, Rope}, + fs::{self, Fs}, language::LanguageRegistry, rpc::{self, proto}, sum_tree::{self, Cursor, Edit, SumTree}, @@ -14,7 +14,6 @@ use crate::{ }; use ::ignore::gitignore::Gitignore; use anyhow::{anyhow, Result}; -pub use fs::*; use futures::{Stream, StreamExt}; pub use fuzzy::{match_paths, PathMatch}; use gpui::{ @@ -37,7 +36,10 @@ use std::{ future::Future, ops::Deref, path::{Path, PathBuf}, - sync::{atomic::AtomicUsize, Arc}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, time::{Duration, SystemTime}, }; use zrpc::{ForegroundRouter, PeerId, TypedEnvelope}; @@ -588,12 +590,11 @@ impl LocalWorktree { .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()); let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); - let entry = fs - .entry(root_char_bag, &next_entry_id, path.clone(), &abs_path) + let metadata = fs + .metadata(&abs_path) .await? .ok_or_else(|| anyhow!("root entry does not exist"))?; - let is_dir = entry.is_dir(); - if is_dir { + if metadata.is_dir { root_name.push('/'); } @@ -612,7 +613,12 @@ impl LocalWorktree { removed_entry_ids: Default::default(), next_entry_id: Arc::new(next_entry_id), }; - snapshot.insert_entry(entry); + snapshot.insert_entry(Entry::new( + path.into(), + &metadata, + &snapshot.next_entry_id, + snapshot.root_char_bag, + )); let tree = Self { snapshot: snapshot.clone(), @@ -1558,6 +1564,27 @@ pub enum EntryKind { } impl Entry { + fn new( + path: Arc, + metadata: &fs::Metadata, + next_entry_id: &AtomicUsize, + root_char_bag: CharBag, + ) -> Self { + Self { + id: next_entry_id.fetch_add(1, SeqCst), + kind: if metadata.is_dir { + EntryKind::PendingDir + } else { + EntryKind::File(char_bag_for_path(root_char_bag, &path)) + }, + path, + inode: metadata.inode, + mtime: metadata.mtime, + is_symlink: metadata.is_symlink, + is_ignored: false, + } + } + pub fn path(&self) -> &Arc { &self.path } @@ -1878,32 +1905,27 @@ impl BackgroundScanner { let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore = None; - let mut child_entries = self - .fs - .child_entries( - root_char_bag, - next_entry_id.as_ref(), - &job.path, - &job.abs_path, - ) - .await?; - while let Some(child_entry) = child_entries.next().await { - let mut child_entry = match child_entry { - Ok(child_entry) => child_entry, + let mut child_paths = self.fs.read_dir(&job.abs_path).await?; + while let Some(child_abs_path) = child_paths.next().await { + let child_abs_path = match child_abs_path { + Ok(child_abs_path) => child_abs_path, Err(error) => { log::error!("error processing entry {:?}", error); continue; } }; - let child_name = child_entry.path.file_name().unwrap(); - let child_abs_path = job.abs_path.join(&child_name); - let child_path = child_entry.path.clone(); + let child_name = child_abs_path.file_name().unwrap(); + let child_path: Arc = job.path.join(child_name).into(); + let child_metadata = match self.fs.metadata(&child_abs_path).await? { + Some(metadata) => metadata, + None => continue, + }; // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored if child_name == *GITIGNORE { let (ignore, err) = Gitignore::new(&child_abs_path); if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", child_entry.path, err); + log::error!("error in ignore file {:?} - {:?}", child_name, err); } let ignore = Arc::new(ignore); ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); @@ -1926,7 +1948,14 @@ impl BackgroundScanner { } } - if child_entry.is_dir() { + let mut child_entry = Entry::new( + child_path.clone(), + &child_metadata, + &next_entry_id, + root_char_bag, + ); + + if child_metadata.is_dir { let is_ignored = ignore_stack.is_path_ignored(&child_path, true); child_entry.is_ignored = is_ignored; new_entries.push(child_entry); @@ -1999,22 +2028,18 @@ impl BackgroundScanner { } }; - match self - .fs - .entry( - snapshot.root_char_bag, - &next_entry_id, - path.clone(), - &event.path, - ) - .await - { - Ok(Some(mut fs_entry)) => { - let is_dir = fs_entry.is_dir(); - let ignore_stack = snapshot.ignore_stack_for_path(&path, is_dir); + match self.fs.metadata(&event.path).await { + Ok(Some(metadata)) => { + let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir); + let mut fs_entry = Entry::new( + path.clone(), + &metadata, + snapshot.next_entry_id.as_ref(), + snapshot.root_char_bag, + ); fs_entry.is_ignored = ignore_stack.is_all(); snapshot.insert_entry(fs_entry); - if is_dir { + if metadata.is_dir { scan_queue_tx .send(ScanJob { abs_path: event.path, @@ -2166,10 +2191,14 @@ async fn refresh_entry( root_char_bag = snapshot.root_char_bag; next_entry_id = snapshot.next_entry_id.clone(); } - let entry = fs - .entry(root_char_bag, &next_entry_id, path, abs_path) - .await? - .ok_or_else(|| anyhow!("could not read saved file metadata"))?; + let entry = Entry::new( + path, + &fs.metadata(abs_path) + .await? + .ok_or_else(|| anyhow!("could not read saved file metadata"))?, + &next_entry_id, + root_char_bag, + ); Ok(snapshot.lock().insert_entry(entry)) } @@ -2534,6 +2563,7 @@ mod tests { use super::*; use crate::test::*; use anyhow::Result; + use fs::RealFs; use rand::prelude::*; use serde_json::json; use std::time::UNIX_EPOCH; @@ -2918,16 +2948,14 @@ mod tests { root_char_bag: Default::default(), next_entry_id: next_entry_id.clone(), }; - initial_snapshot.insert_entry( - smol::block_on(fs.entry( - Default::default(), - &next_entry_id, - Path::new("").into(), - root_dir.path().into(), - )) - .unwrap() - .unwrap(), - ); + initial_snapshot.insert_entry(Entry::new( + Path::new("").into(), + &smol::block_on(fs.metadata(root_dir.path())) + .unwrap() + .unwrap(), + &next_entry_id, + Default::default(), + )); let mut scanner = BackgroundScanner::new( Arc::new(Mutex::new(initial_snapshot.clone())), notify_tx,