//! VFS stands for Virtual File System. //! //! When doing analysis, we don't want to do any IO, we want to keep all source //! code in memory. However, the actual source code is stored on disk, so you //! need to get it into the memory in the first place somehow. VFS is the //! component which does this. //! //! 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 //! which are watched for changes. Typically, there will be a root for each //! Cargo package. mod io; use std::{ cmp::Reverse, fmt, fs, mem, path::{Path, PathBuf}, sync::Arc, thread, }; use crossbeam_channel::Receiver; use ra_arena::{impl_arena_id, Arena, RawId}; use relative_path::{Component, RelativePath, RelativePathBuf}; use rustc_hash::{FxHashMap, FxHashSet}; pub use crate::io::TaskResult as VfsTask; use io::{Task, TaskResult, WatcherChange, WatcherChangeData, Worker}; /// `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. pub(crate) struct RootFilter { root: PathBuf, filter: fn(&Path, &RelativePath) -> bool, } impl RootFilter { fn new(root: PathBuf) -> RootFilter { RootFilter { root, filter: default_filter, } } /// Check if this root can contain `path`. NB: even if this returns /// true, the `path` might actually be conained in some nested root. pub(crate) fn can_contain(&self, path: &Path) -> Option { let rel_path = path.strip_prefix(&self.root).ok()?; let rel_path = RelativePathBuf::from_path(rel_path).ok()?; if !(self.filter)(path, rel_path.as_relative_path()) { return None; } Some(rel_path) } } pub(crate) fn default_filter(path: &Path, rel_path: &RelativePath) -> bool { if path.is_dir() { for (i, c) in rel_path.components().enumerate() { if let Component::Normal(c) = c { // hardcoded for now if (i == 0 && c == "target") || c == ".git" || c == "node_modules" { return false; } } } true } else { rel_path.extension() == Some("rs") } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct VfsRoot(pub RawId); impl_arena_id!(VfsRoot); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct VfsFile(pub RawId); impl_arena_id!(VfsFile); struct VfsFileData { root: VfsRoot, path: RelativePathBuf, is_overlayed: bool, text: Arc, } pub struct Vfs { roots: Arena>, files: Arena, root2files: FxHashMap>, pending_changes: Vec, worker: Worker, } impl fmt::Debug for Vfs { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("Vfs { ... }") } } impl Vfs { pub fn new(mut roots: Vec) -> (Vfs, Vec) { let worker = io::Worker::start(); let mut res = Vfs { roots: Arena::default(), files: Arena::default(), root2files: FxHashMap::default(), worker, 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_filter = Arc::new(RootFilter::new(path.clone())); let root = res.roots.alloc(root_filter.clone()); res.root2files.insert(root, Default::default()); let nested_roots = roots[..i] .iter() .filter(|it| it.starts_with(path)) .map(|it| it.clone()) .collect::>(); let task = io::Task::AddRoot { root, path: path.clone(), root_filter, nested_roots, }; res.worker.sender().send(task).unwrap(); } let roots = res.roots.iter().map(|(id, _)| id).collect(); (res, roots) } pub fn root2path(&self, root: VfsRoot) -> PathBuf { self.roots[root].root.clone() } pub fn path2file(&self, path: &Path) -> Option { if let Some((_root, _path, Some(file))) = self.find_root(path) { return Some(file); } None } pub fn file2path(&self, file: VfsFile) -> PathBuf { let rel_path = &self.files[file].path; let root_path = &self.roots[self.files[file].root].root; rel_path.to_path(root_path) } pub fn file_for_path(&self, path: &Path) -> Option { if let Some((_root, _path, Some(file))) = self.find_root(path) { return Some(file); } None } pub fn load(&mut self, path: &Path) -> Option { if let Some((root, rel_path, file)) = self.find_root(path) { return if let Some(file) = file { Some(file) } 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), 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 { self.worker.receiver() } pub fn handle_task(&mut self, task: io::TaskResult) { match task { TaskResult::AddRoot(task) => { 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::>(); 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), false); files.push((file, path, text)); } let change = VfsChange::AddRoot { root: task.root, files, }; self.pending_changes.push(change); } TaskResult::HandleChange(change) => match &change { WatcherChange::Create(path) if path.is_dir() => { if let Some((root, _path, _file)) = self.find_root(&path) { let root_filter = self.roots[root].clone(); self.worker .sender() .send(Task::Watch { dir: path.to_path_buf(), root_filter, }) .unwrap() } } WatcherChange::Create(path) | WatcherChange::Remove(path) | WatcherChange::Write(path) => { if self.should_handle_change(&path) { self.worker.sender().send(Task::LoadChange(change)).unwrap() } } WatcherChange::Rescan => { // TODO we should reload all files } }, TaskResult::LoadChange(change) => match change { WatcherChangeData::Create { path, text } | WatcherChangeData::Write { path, text } => { if let Some((root, path, file)) = self.find_root(&path) { if let Some(file) = file { self.do_change_file(file, text, false); } else { self.do_add_file(root, path, text, false); } } } WatcherChangeData::Remove { path } => { if let Some((root, path, file)) = self.find_root(&path) { if let Some(file) = file { self.do_remove_file(root, path, file, false); } } } }, TaskResult::NoOp => {} } } fn should_handle_change(&self, path: &Path) -> bool { if let Some((_root, _rel_path, file)) = self.find_root(&path) { if let Some(file) = file { if self.files[file].is_overlayed { // file is overlayed log::debug!("skipping overlayed \"{}\"", path.display()); return false; } } true } else { // file doesn't belong to any root false } } fn do_add_file( &mut self, root: VfsRoot, path: RelativePathBuf, text: String, is_overlay: bool, ) -> Option { let text = Arc::new(text); let file = self.add_file(root, path.clone(), text.clone(), is_overlay); self.pending_changes.push(VfsChange::AddFile { file, root, path, text, }); Some(file) } fn do_change_file(&mut self, file: VfsFile, text: String, is_overlay: bool) { if !is_overlay && self.files[file].is_overlayed { return; } let text = Arc::new(text); self.change_file(file, text.clone(), is_overlay); self.pending_changes .push(VfsChange::ChangeFile { file, text }); } fn do_remove_file( &mut self, root: VfsRoot, path: RelativePathBuf, file: VfsFile, is_overlay: bool, ) { if !is_overlay && self.files[file].is_overlayed { return; } self.remove_file(file); self.pending_changes .push(VfsChange::RemoveFile { root, path, file }); } pub fn add_file_overlay(&mut self, path: &Path, text: String) -> Option { if let Some((root, rel_path, file)) = self.find_root(path) { if let Some(file) = file { self.do_change_file(file, text, true); Some(file) } else { self.do_add_file(root, rel_path, text, true) } } else { None } } 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"); self.do_change_file(file, new_text, true); } } pub fn remove_file_overlay(&mut self, path: &Path) -> Option { if let Some((root, path, file)) = self.find_root(path) { let file = file.expect("can't remove a file which wasn't added"); let full_path = path.to_path(&self.roots[root].root); if let Ok(text) = fs::read_to_string(&full_path) { self.do_change_file(file, text, true); } else { self.do_remove_file(root, path, file, true); } Some(file) } else { None } } pub fn commit_changes(&mut self) -> Vec { mem::replace(&mut self.pending_changes, Vec::new()) } /// Sutdown the VFS and terminate the background watching thread. pub fn shutdown(self) -> thread::Result<()> { self.worker.shutdown() } fn add_file( &mut self, root: VfsRoot, path: RelativePathBuf, text: Arc, is_overlayed: bool, ) -> VfsFile { let data = VfsFileData { root, path, text, is_overlayed, }; let file = self.files.alloc(data); self.root2files.get_mut(&root).unwrap().insert(file); file } fn change_file(&mut self, file: VfsFile, new_text: Arc, is_overlayed: bool) { let mut file_data = &mut self.files[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; let removed = self.root2files.get_mut(&root).unwrap().remove(&file); assert!(removed); } fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option)> { 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); Some((root, path, file)) } } #[derive(Debug, Clone)] pub enum VfsChange { AddRoot { root: VfsRoot, files: Vec<(VfsFile, RelativePathBuf, Arc)>, }, AddFile { root: VfsRoot, file: VfsFile, path: RelativePathBuf, text: Arc, }, RemoveFile { root: VfsRoot, file: VfsFile, path: RelativePathBuf, }, ChangeFile { file: VfsFile, text: Arc, }, }