//! need to get it into the memory in the first place somehow. VFS is the
//! component which does this.
//!
-//! It also is responsible for watching the disk for changes, and for merging
+//! It is also responsible for watching the disk for changes, and for merging
//! editor state (modified, unsaved files) with disk state.
//!
+//! TODO: Some LSP clients support watching the disk, so this crate should to
+//! support custom watcher events (related to
+//! <https://github.com/rust-analyzer/rust-analyzer/issues/131>)
+//!
//! VFS is based on a concept of roots: a set of directories on the file system
-//! whihc are watched for changes. Typically, there will be a root for each
+//! which are watched for changes. Typically, there will be a root for each
//! Cargo package.
-mod arena;
+mod roots;
mod io;
use std::{
- fmt,
- mem,
- thread,
- cmp::Reverse,
+ fmt, fs, mem,
path::{Path, PathBuf},
- ffi::OsStr,
sync::Arc,
- fs,
};
-use rustc_hash::{FxHashMap, FxHashSet};
-use relative_path::RelativePathBuf;
use crossbeam_channel::Receiver;
-use walkdir::DirEntry;
-use thread_worker::{WorkerHandle};
+use relative_path::{RelativePath, RelativePathBuf};
+use rustc_hash::{FxHashMap, FxHashSet};
use crate::{
- arena::{ArenaId, Arena},
+ io::{TaskResult, Worker},
+ roots::Roots,
};
-pub use crate::io::TaskResult as VfsTask;
-
-/// `RootFilter` is a predicate that checks if a file can belong to a root. If
-/// several filters match a file (nested dirs), the most nested one wins.
-struct RootFilter {
- root: PathBuf,
- file_filter: fn(&Path) -> bool,
-}
-
-impl RootFilter {
- fn new(root: PathBuf) -> RootFilter {
- RootFilter {
- root,
- file_filter: has_rs_extension,
- }
- }
- /// Check if this root can contain `path`. NB: even if this returns
- /// true, the `path` might actually be conained in some nested root.
- fn can_contain(&self, path: &Path) -> Option<RelativePathBuf> {
- if !(self.file_filter)(path) {
- return None;
- }
- if !(path.starts_with(&self.root)) {
- return None;
- }
- let path = path.strip_prefix(&self.root).unwrap();
- let path = RelativePathBuf::from_path(path).unwrap();
- Some(path)
- }
-}
-
-fn has_rs_extension(p: &Path) -> bool {
- p.extension() == Some(OsStr::new("rs"))
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
-pub struct VfsRoot(pub u32);
-
-impl ArenaId for VfsRoot {
- fn from_u32(idx: u32) -> VfsRoot {
- VfsRoot(idx)
- }
- fn to_u32(self) -> u32 {
- self.0
- }
-}
+pub use crate::{
+ io::TaskResult as VfsTask,
+ roots::VfsRoot,
+};
-#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VfsFile(pub u32);
-impl ArenaId for VfsFile {
- fn from_u32(idx: u32) -> VfsFile {
- VfsFile(idx)
- }
- fn to_u32(self) -> u32 {
- self.0
- }
-}
-
struct VfsFileData {
root: VfsRoot,
path: RelativePathBuf,
+ is_overlayed: bool,
text: Arc<String>,
}
pub struct Vfs {
- roots: Arena<VfsRoot, RootFilter>,
- files: Arena<VfsFile, VfsFileData>,
+ roots: Arc<Roots>,
+ files: Vec<VfsFileData>,
root2files: FxHashMap<VfsRoot, FxHashSet<VfsFile>>,
pending_changes: Vec<VfsChange>,
- worker: io::Worker,
- worker_handle: WorkerHandle,
+ worker: Worker,
}
impl fmt::Debug for Vfs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- f.write_str("Vfs { ... }")
+ f.debug_struct("Vfs")
+ .field("n_roots", &self.roots.len())
+ .field("n_files", &self.files.len())
+ .field("n_pending_changes", &self.pending_changes.len())
+ .finish()
}
}
+#[derive(Debug, Clone)]
+pub enum VfsChange {
+ AddRoot { root: VfsRoot, files: Vec<(VfsFile, RelativePathBuf, Arc<String>)> },
+ AddFile { root: VfsRoot, file: VfsFile, path: RelativePathBuf, text: Arc<String> },
+ RemoveFile { root: VfsRoot, file: VfsFile, path: RelativePathBuf },
+ ChangeFile { file: VfsFile, text: Arc<String> },
+}
+
impl Vfs {
- pub fn new(mut roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) {
- let (worker, worker_handle) = io::start();
-
- let mut res = Vfs {
- roots: Arena::default(),
- files: Arena::default(),
- root2files: FxHashMap::default(),
- worker,
- worker_handle,
- pending_changes: Vec::new(),
- };
-
- // A hack to make nesting work.
- roots.sort_by_key(|it| Reverse(it.as_os_str().len()));
- for (i, path) in roots.iter().enumerate() {
- let root = res.roots.alloc(RootFilter::new(path.clone()));
- res.root2files.insert(root, Default::default());
- let nested = roots[..i]
- .iter()
- .filter(|it| it.starts_with(path))
- .map(|it| it.clone())
- .collect::<Vec<_>>();
- let filter = move |entry: &DirEntry| {
- if entry.file_type().is_file() {
- has_rs_extension(entry.path())
- } else {
- nested.iter().all(|it| it != entry.path())
- }
- };
- let task = io::Task {
- root,
- path: path.clone(),
- filter: Box::new(filter),
- };
- res.worker.inp.send(task);
+ pub fn new(roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) {
+ let roots = Arc::new(Roots::new(roots));
+ let worker = io::start(Arc::clone(&roots));
+ let mut root2files = FxHashMap::default();
+
+ for root in roots.iter() {
+ root2files.insert(root, Default::default());
+ worker.sender().send(io::Task::AddRoot { root }).unwrap();
}
- let roots = res.roots.iter().map(|(id, _)| id).collect();
- (res, roots)
+ let res = Vfs { roots, files: Vec::new(), root2files, worker, pending_changes: Vec::new() };
+ let vfs_roots = res.roots.iter().collect();
+ (res, vfs_roots)
}
pub fn root2path(&self, root: VfsRoot) -> PathBuf {
- self.roots[root].root.clone()
+ self.roots.path(root).to_path_buf()
}
pub fn path2file(&self, path: &Path) -> Option<VfsFile> {
}
pub fn file2path(&self, file: VfsFile) -> PathBuf {
- let rel_path = &self.files[file].path;
- let root_path = &self.roots[self.files[file].root].root;
+ let rel_path = &self.file(file).path;
+ let root_path = &self.roots.path(self.file(file).root);
rel_path.to_path(root_path)
}
- pub fn file_for_path(&self, path: &Path) -> Option<VfsFile> {
- if let Some((_root, _path, Some(file))) = self.find_root(path) {
- return Some(file);
- }
- None
+ pub fn n_roots(&self) -> usize {
+ self.roots.len()
}
pub fn load(&mut self, path: &Path) -> Option<VfsFile> {
} else {
let text = fs::read_to_string(path).unwrap_or_default();
let text = Arc::new(text);
- let file = self.add_file(root, rel_path.clone(), Arc::clone(&text));
- let change = VfsChange::AddFile {
- file,
- text,
- root,
- path: rel_path,
- };
+ let file = self.raw_add_file(root, rel_path.clone(), Arc::clone(&text), false);
+ let change = VfsChange::AddFile { file, text, root, path: rel_path };
self.pending_changes.push(change);
Some(file)
};
None
}
- pub fn task_receiver(&self) -> &Receiver<io::TaskResult> {
- &self.worker.out
- }
-
- pub fn handle_task(&mut self, task: io::TaskResult) {
- let mut files = Vec::new();
- // While we were scanning the root in the backgound, a file might have
- // been open in the editor, so we need to account for that.
- let exising = self.root2files[&task.root]
- .iter()
- .map(|&file| (self.files[file].path.clone(), file))
- .collect::<FxHashMap<_, _>>();
- for (path, text) in task.files {
- if let Some(&file) = exising.get(&path) {
- let text = Arc::clone(&self.files[file].text);
- files.push((file, path, text));
- continue;
- }
- let text = Arc::new(text);
- let file = self.add_file(task.root, path.clone(), Arc::clone(&text));
- files.push((file, path, text));
- }
-
- let change = VfsChange::AddRoot {
- root: task.root,
- files,
- };
- self.pending_changes.push(change);
- }
-
pub fn add_file_overlay(&mut self, path: &Path, text: String) -> Option<VfsFile> {
- let mut res = None;
- if let Some((root, path, file)) = self.find_root(path) {
- let text = Arc::new(text);
- let change = if let Some(file) = file {
- res = Some(file);
- self.change_file(file, Arc::clone(&text));
- VfsChange::ChangeFile { file, text }
- } else {
- let file = self.add_file(root, path.clone(), Arc::clone(&text));
- res = Some(file);
- VfsChange::AddFile {
- file,
- text,
- root,
- path,
- }
- };
- self.pending_changes.push(change);
+ let (root, rel_path, file) = self.find_root(path)?;
+ if let Some(file) = file {
+ self.change_file_event(file, text, true);
+ Some(file)
+ } else {
+ self.add_file_event(root, rel_path, text, true)
}
- res
}
pub fn change_file_overlay(&mut self, path: &Path, new_text: String) {
if let Some((_root, _path, file)) = self.find_root(path) {
let file = file.expect("can't change a file which wasn't added");
- let text = Arc::new(new_text);
- self.change_file(file, Arc::clone(&text));
- let change = VfsChange::ChangeFile { file, text };
- self.pending_changes.push(change);
+ self.change_file_event(file, new_text, true);
}
}
pub fn remove_file_overlay(&mut self, path: &Path) -> Option<VfsFile> {
- let mut res = None;
- if let Some((root, path, file)) = self.find_root(path) {
- let file = file.expect("can't remove a file which wasn't added");
- res = Some(file);
- let full_path = path.to_path(&self.roots[root].root);
- let change = if let Ok(text) = fs::read_to_string(&full_path) {
- let text = Arc::new(text);
- self.change_file(file, Arc::clone(&text));
- VfsChange::ChangeFile { file, text }
- } else {
- self.remove_file(file);
- VfsChange::RemoveFile { root, file, path }
- };
- self.pending_changes.push(change);
+ let (root, rel_path, file) = self.find_root(path)?;
+ let file = file.expect("can't remove a file which wasn't added");
+ let full_path = rel_path.to_path(&self.roots.path(root));
+ if let Ok(text) = fs::read_to_string(&full_path) {
+ self.change_file_event(file, text, false);
+ } else {
+ self.remove_file_event(root, rel_path, file);
}
- res
+ Some(file)
}
pub fn commit_changes(&mut self) -> Vec<VfsChange> {
mem::replace(&mut self.pending_changes, Vec::new())
}
- /// Sutdown the VFS and terminate the background watching thread.
- pub fn shutdown(self) -> thread::Result<()> {
- let _ = self.worker.shutdown();
- self.worker_handle.shutdown()
+ pub fn task_receiver(&self) -> &Receiver<io::TaskResult> {
+ self.worker.receiver()
}
- fn add_file(&mut self, root: VfsRoot, path: RelativePathBuf, text: Arc<String>) -> VfsFile {
- let data = VfsFileData { root, path, text };
- let file = self.files.alloc(data);
+ pub fn handle_task(&mut self, task: io::TaskResult) {
+ match task {
+ TaskResult::BulkLoadRoot { root, files } => {
+ let mut cur_files = Vec::new();
+ // While we were scanning the root in the background, a file might have
+ // been open in the editor, so we need to account for that.
+ let existing = self.root2files[&root]
+ .iter()
+ .map(|&file| (self.file(file).path.clone(), file))
+ .collect::<FxHashMap<_, _>>();
+ for (path, text) in files {
+ if let Some(&file) = existing.get(&path) {
+ let text = Arc::clone(&self.file(file).text);
+ cur_files.push((file, path, text));
+ continue;
+ }
+ let text = Arc::new(text);
+ let file = self.raw_add_file(root, path.clone(), Arc::clone(&text), false);
+ cur_files.push((file, path, text));
+ }
+
+ let change = VfsChange::AddRoot { root, files: cur_files };
+ self.pending_changes.push(change);
+ }
+ TaskResult::SingleFile { root, path, text } => {
+ let existing_file = self.find_file(root, &path);
+ if existing_file.map(|file| self.file(file).is_overlayed) == Some(true) {
+ return;
+ }
+ match (existing_file, text) {
+ (Some(file), None) => {
+ self.remove_file_event(root, path, file);
+ }
+ (None, Some(text)) => {
+ self.add_file_event(root, path, text, false);
+ }
+ (Some(file), Some(text)) => {
+ self.change_file_event(file, text, false);
+ }
+ (None, None) => (),
+ }
+ }
+ }
+ }
+
+ // *_event calls change the state of VFS and push a change onto pending
+ // changes array.
+
+ fn add_file_event(
+ &mut self,
+ root: VfsRoot,
+ path: RelativePathBuf,
+ text: String,
+ is_overlay: bool,
+ ) -> Option<VfsFile> {
+ let text = Arc::new(text);
+ let file = self.raw_add_file(root, path.clone(), text.clone(), is_overlay);
+ self.pending_changes.push(VfsChange::AddFile { file, root, path, text });
+ Some(file)
+ }
+
+ fn change_file_event(&mut self, file: VfsFile, text: String, is_overlay: bool) {
+ let text = Arc::new(text);
+ self.raw_change_file(file, text.clone(), is_overlay);
+ self.pending_changes.push(VfsChange::ChangeFile { file, text });
+ }
+
+ fn remove_file_event(&mut self, root: VfsRoot, path: RelativePathBuf, file: VfsFile) {
+ self.raw_remove_file(file);
+ self.pending_changes.push(VfsChange::RemoveFile { root, path, file });
+ }
+
+ // raw_* calls change the state of VFS, but **do not** emit events.
+
+ fn raw_add_file(
+ &mut self,
+ root: VfsRoot,
+ path: RelativePathBuf,
+ text: Arc<String>,
+ is_overlayed: bool,
+ ) -> VfsFile {
+ let data = VfsFileData { root, path, text, is_overlayed };
+ let file = VfsFile(self.files.len() as u32);
+ self.files.push(data);
self.root2files.get_mut(&root).unwrap().insert(file);
file
}
- fn change_file(&mut self, file: VfsFile, new_text: Arc<String>) {
- self.files[file].text = new_text;
+ fn raw_change_file(&mut self, file: VfsFile, new_text: Arc<String>, is_overlayed: bool) {
+ let mut file_data = &mut self.file_mut(file);
+ file_data.text = new_text;
+ file_data.is_overlayed = is_overlayed;
}
- fn remove_file(&mut self, file: VfsFile) {
- //FIXME: use arena with removal
- self.files[file].text = Default::default();
- self.files[file].path = Default::default();
- let root = self.files[file].root;
+ fn raw_remove_file(&mut self, file: VfsFile) {
+ // FIXME: use arena with removal
+ self.file_mut(file).text = Default::default();
+ self.file_mut(file).path = Default::default();
+ let root = self.file(file).root;
let removed = self.root2files.get_mut(&root).unwrap().remove(&file);
assert!(removed);
}
fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> {
- let (root, path) = self
- .roots
- .iter()
- .find_map(|(root, data)| data.can_contain(path).map(|it| (root, it)))?;
- let file = self.root2files[&root]
- .iter()
- .map(|&it| it)
- .find(|&file| self.files[file].path == path);
+ let (root, path) = self.roots.find(&path)?;
+ let file = self.find_file(root, &path);
Some((root, path, file))
}
-}
-#[derive(Debug, Clone)]
-pub enum VfsChange {
- AddRoot {
- root: VfsRoot,
- files: Vec<(VfsFile, RelativePathBuf, Arc<String>)>,
- },
- AddFile {
- root: VfsRoot,
- file: VfsFile,
- path: RelativePathBuf,
- text: Arc<String>,
- },
- RemoveFile {
- root: VfsRoot,
- file: VfsFile,
- path: RelativePathBuf,
- },
- ChangeFile {
- file: VfsFile,
- text: Arc<String>,
- },
+ fn find_file(&self, root: VfsRoot, path: &RelativePath) -> Option<VfsFile> {
+ self.root2files[&root].iter().map(|&it| it).find(|&file| self.file(file).path == path)
+ }
+
+ fn file(&self, file: VfsFile) -> &VfsFileData {
+ &self.files[file.0 as usize]
+ }
+
+ fn file_mut(&mut self, file: VfsFile) -> &mut VfsFileData {
+ &mut self.files[file.0 as usize]
+ }
}