From a422d480a188a28c6b5e7862fbf07817eb2c7447 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 18 Dec 2018 16:38:05 +0300 Subject: implement vfs events handling --- crates/ra_lsp_server/tests/heavy_tests/support.rs | 4 +- crates/ra_vfs/Cargo.toml | 3 + crates/ra_vfs/src/arena.rs | 7 +- crates/ra_vfs/src/io.rs | 43 +++--- crates/ra_vfs/src/lib.rs | 153 ++++++++++++++++++++-- crates/ra_vfs/tests/vfs.rs | 101 ++++++++++++++ 6 files changed, 268 insertions(+), 43 deletions(-) create mode 100644 crates/ra_vfs/tests/vfs.rs (limited to 'crates') diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs index 07a878a26..c14d287ca 100644 --- a/crates/ra_lsp_server/tests/heavy_tests/support.rs +++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs @@ -174,11 +174,11 @@ impl Server { impl Drop for Server { fn drop(&mut self) { self.send_request::(666, ()); - let receiver = self.worker.take().unwrap().stop(); + let receiver = self.worker.take().unwrap().shutdown(); while let Some(msg) = recv_timeout(&receiver) { drop(msg); } - self.watcher.take().unwrap().stop().unwrap(); + self.watcher.take().unwrap().shutdown().unwrap(); } } diff --git a/crates/ra_vfs/Cargo.toml b/crates/ra_vfs/Cargo.toml index 9ce619a77..ccea8a866 100644 --- a/crates/ra_vfs/Cargo.toml +++ b/crates/ra_vfs/Cargo.toml @@ -12,3 +12,6 @@ crossbeam-channel = "0.2.4" log = "0.4.6" thread_worker = { path = "../thread_worker" } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/ra_vfs/src/arena.rs b/crates/ra_vfs/src/arena.rs index d6fad753b..6b42ae26d 100644 --- a/crates/ra_vfs/src/arena.rs +++ b/crates/ra_vfs/src/arena.rs @@ -1,5 +1,4 @@ use std::{ - hash::{Hash, Hasher}, marker::PhantomData, ops::{Index, IndexMut}, }; @@ -21,6 +20,12 @@ impl Arena { self.data.push(value); ID::from_u32(id) } + pub fn iter<'a>(&'a self) -> impl Iterator { + self.data + .iter() + .enumerate() + .map(|(idx, value)| (ID::from_u32(idx as u32), value)) + } } impl Default for Arena { diff --git a/crates/ra_vfs/src/io.rs b/crates/ra_vfs/src/io.rs index c46760583..178c9beff 100644 --- a/crates/ra_vfs/src/io.rs +++ b/crates/ra_vfs/src/io.rs @@ -1,35 +1,26 @@ use std::{ fs, path::{Path, PathBuf}, - thread::JoinHandle, }; use walkdir::{DirEntry, WalkDir}; -use crossbeam_channel::{Sender, Receiver}; use thread_worker::{WorkerHandle}; +use relative_path::RelativePathBuf; use crate::VfsRoot; -pub(crate) enum Task { - ScanRoot { - root: VfsRoot, - path: PathBuf, - filter: Box bool + Send>, - }, -} - -#[derive(Debug)] -pub(crate) struct FileEvent { +pub(crate) struct Task { + pub(crate) root: VfsRoot, pub(crate) path: PathBuf, - pub(crate) kind: FileEventKind, + pub(crate) filter: Box bool + Send>, } -#[derive(Debug)] -pub(crate) enum FileEventKind { - Add(String), +pub struct TaskResult { + pub(crate) root: VfsRoot, + pub(crate) files: Vec<(RelativePathBuf, String)>, } -pub(crate) type Worker = thread_worker::Worker)>; +pub(crate) type Worker = thread_worker::Worker; pub(crate) fn start() -> (Worker, WorkerHandle) { thread_worker::spawn("vfs", 128, |input_receiver, output_sender| { @@ -39,17 +30,17 @@ pub(crate) fn start() -> (Worker, WorkerHandle) { }) } -fn handle_task(task: Task) -> (PathBuf, Vec) { - let Task::ScanRoot { path, .. } = task; +fn handle_task(task: Task) -> TaskResult { + let Task { root, path, filter } = task; log::debug!("loading {} ...", path.as_path().display()); - let events = load_root(path.as_path()); + let files = load_root(path.as_path(), &*filter); log::debug!("... loaded {}", path.as_path().display()); - (path, events) + TaskResult { root, files } } -fn load_root(path: &Path) -> Vec { +fn load_root(root: &Path, filter: &dyn Fn(&DirEntry) -> bool) -> Vec<(RelativePathBuf, String)> { let mut res = Vec::new(); - for entry in WalkDir::new(path) { + for entry in WalkDir::new(root).into_iter().filter_entry(filter) { let entry = match entry { Ok(entry) => entry, Err(e) => { @@ -71,10 +62,8 @@ fn load_root(path: &Path) -> Vec { continue; } }; - res.push(FileEvent { - path: path.to_owned(), - kind: FileEventKind::Add(text), - }) + let path = RelativePathBuf::from_path(path.strip_prefix(root).unwrap()).unwrap(); + res.push((path.to_owned(), text)) } res } diff --git a/crates/ra_vfs/src/lib.rs b/crates/ra_vfs/src/lib.rs index 8ce6b6ee0..792f722a7 100644 --- a/crates/ra_vfs/src/lib.rs +++ b/crates/ra_vfs/src/lib.rs @@ -15,14 +15,19 @@ mod arena; mod io; use std::{ + mem, thread, cmp::Reverse, 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 crate::{ @@ -40,15 +45,25 @@ impl RootFilter { fn new(root: PathBuf) -> RootFilter { RootFilter { root, - file_filter: rs_extension_filter, + file_filter: has_rs_extension, } } - fn can_contain(&self, path: &Path) -> bool { - (self.file_filter)(path) && path.starts_with(&self.root) + /// 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 { + 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 rs_extension_filter(p: &Path) -> bool { +fn has_rs_extension(p: &Path) -> bool { p.extension() == Some(OsStr::new("rs")) } @@ -82,10 +97,11 @@ struct VfsFileData { text: Arc, } -struct Vfs { +pub struct Vfs { roots: Arena, files: Arena, - // pending_changes: Vec, + root2files: FxHashMap>, + pending_changes: Vec, worker: io::Worker, worker_handle: WorkerHandle, } @@ -97,33 +113,144 @@ impl Vfs { 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 path in roots { - res.roots.alloc(RootFilter::new(path)); + for (i, path) in roots.iter().enumerate() { + let root = res.roots.alloc(RootFilter::new(path.clone())); + let nested = roots[..i] + .iter() + .filter(|it| it.starts_with(path)) + .map(|it| it.clone()) + .collect::>(); + 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); } res } - pub fn add_file_overlay(&mut self, path: &Path, content: String) {} + pub fn task_receiver(&self) -> &Receiver { + &self.worker.out + } + + pub fn handle_task(&mut self, task: io::TaskResult) { + let mut files = Vec::new(); + for (path, text) in task.files { + 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 change_file_overlay(&mut self, path: &Path, new_content: String) {} + pub fn add_file_overlay(&mut self, path: &Path, text: String) { + if let Some((root, path, file)) = self.find_root(path) { + let text = Arc::new(text); + let change = if let Some(file) = file { + self.change_file(file, Arc::clone(&text)); + VfsChange::ChangeFile { file, text } + } else { + let file = self.add_file(root, path.clone(), Arc::clone(&text)); + VfsChange::AddFile { + file, + text, + root, + path, + } + }; + self.pending_changes.push(change); + } + } - pub fn remove_file_overlay(&mut self, path: &Path) {} + 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); + } + } + + pub fn remove_file_overlay(&mut self, path: &Path) { + 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); + 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 { file } + }; + self.pending_changes.push(change); + } + } pub fn commit_changes(&mut self) -> Vec { - unimplemented!() + mem::replace(&mut self.pending_changes, Vec::new()) } pub fn shutdown(self) -> thread::Result<()> { let _ = self.worker.shutdown(); self.worker_handle.shutdown() } + + fn add_file(&mut self, root: VfsRoot, path: RelativePathBuf, text: Arc) -> VfsFile { + let data = VfsFileData { root, path, text }; + let file = self.files.alloc(data); + self.root2files + .entry(root) + .or_insert_with(FxHashSet::default) + .insert(file); + file + } + + fn change_file(&mut self, file: VfsFile, new_text: Arc) { + self.files[file].text = new_text; + } + + 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)] diff --git a/crates/ra_vfs/tests/vfs.rs b/crates/ra_vfs/tests/vfs.rs new file mode 100644 index 000000000..4f44215c8 --- /dev/null +++ b/crates/ra_vfs/tests/vfs.rs @@ -0,0 +1,101 @@ +use std::{ + fs, + collections::HashSet, +}; + +use tempfile::tempdir; + +use ra_vfs::{Vfs, VfsChange}; + +#[test] +fn test_vfs_works() -> std::io::Result<()> { + let files = [ + ("a/foo.rs", "hello"), + ("a/bar.rs", "world"), + ("a/b/baz.rs", "nested hello"), + ]; + + let dir = tempdir()?; + for (path, text) in files.iter() { + let file_path = dir.path().join(path); + fs::create_dir_all(file_path.parent().unwrap())?; + fs::write(file_path, text)? + } + + let a_root = dir.path().join("a"); + let b_root = dir.path().join("a/b"); + + let mut vfs = Vfs::new(vec![a_root, b_root]); + for _ in 0..2 { + let task = vfs.task_receiver().recv().unwrap(); + vfs.handle_task(task); + } + { + let files = vfs + .commit_changes() + .into_iter() + .flat_map(|change| { + let files = match change { + VfsChange::AddRoot { files, .. } => files, + _ => panic!("unexpected change"), + }; + files.into_iter().map(|(_id, path, text)| { + let text: String = (&*text).clone(); + (format!("{}", path.display()), text) + }) + }) + .collect::>(); + + let expected_files = [ + ("foo.rs", "hello"), + ("bar.rs", "world"), + ("baz.rs", "nested hello"), + ] + .iter() + .map(|(path, text)| (path.to_string(), text.to_string())) + .collect::>(); + + assert_eq!(files, expected_files); + } + + vfs.add_file_overlay(&dir.path().join("a/b/baz.rs"), "quux".to_string()); + let change = vfs.commit_changes().pop().unwrap(); + match change { + VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "quux"), + _ => panic!("unexpected change"), + } + + vfs.change_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); + let change = vfs.commit_changes().pop().unwrap(); + match change { + VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "m"), + _ => panic!("unexpected change"), + } + + vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); + let change = vfs.commit_changes().pop().unwrap(); + match change { + VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "nested hello"), + _ => panic!("unexpected change"), + } + + vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); + let change = vfs.commit_changes().pop().unwrap(); + match change { + VfsChange::AddFile { text, path, .. } => { + assert_eq!(&*text, "spam"); + assert_eq!(path, "spam.rs"); + } + _ => panic!("unexpected change"), + } + + vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); + let change = vfs.commit_changes().pop().unwrap(); + match change { + VfsChange::RemoveFile { .. } => (), + _ => panic!("unexpected change"), + } + + vfs.shutdown().unwrap(); + Ok(()) +} -- cgit v1.2.3