diff options
Diffstat (limited to 'crates/ra_vfs')
-rw-r--r-- | crates/ra_vfs/Cargo.toml | 4 | ||||
-rw-r--r-- | crates/ra_vfs/src/io.rs | 131 | ||||
-rw-r--r-- | crates/ra_vfs/src/io/watcher.rs | 200 | ||||
-rw-r--r-- | crates/ra_vfs/src/lib.rs | 359 | ||||
-rw-r--r-- | crates/ra_vfs/tests/vfs.rs | 156 |
5 files changed, 647 insertions, 203 deletions
diff --git a/crates/ra_vfs/Cargo.toml b/crates/ra_vfs/Cargo.toml index e637063c9..383381d2a 100644 --- a/crates/ra_vfs/Cargo.toml +++ b/crates/ra_vfs/Cargo.toml | |||
@@ -10,9 +10,13 @@ relative-path = "0.4.0" | |||
10 | rustc-hash = "1.0" | 10 | rustc-hash = "1.0" |
11 | crossbeam-channel = "0.3.5" | 11 | crossbeam-channel = "0.3.5" |
12 | log = "0.4.6" | 12 | log = "0.4.6" |
13 | notify = "4.0.7" | ||
14 | drop_bomb = "0.1.0" | ||
15 | parking_lot = "0.7.0" | ||
13 | 16 | ||
14 | thread_worker = { path = "../thread_worker" } | 17 | thread_worker = { path = "../thread_worker" } |
15 | ra_arena = { path = "../ra_arena" } | 18 | ra_arena = { path = "../ra_arena" } |
16 | 19 | ||
17 | [dev-dependencies] | 20 | [dev-dependencies] |
18 | tempfile = "3" | 21 | tempfile = "3" |
22 | flexi_logger = "0.10.0" | ||
diff --git a/crates/ra_vfs/src/io.rs b/crates/ra_vfs/src/io.rs index 80328ad18..7ca1e9835 100644 --- a/crates/ra_vfs/src/io.rs +++ b/crates/ra_vfs/src/io.rs | |||
@@ -1,55 +1,109 @@ | |||
1 | use std::{ | 1 | use std::{fs, sync::Arc, thread}; |
2 | fmt, | ||
3 | fs, | ||
4 | path::{Path, PathBuf}, | ||
5 | }; | ||
6 | 2 | ||
7 | use walkdir::{DirEntry, WalkDir}; | 3 | use crossbeam_channel::{Receiver, Sender}; |
8 | use thread_worker::{WorkerHandle}; | ||
9 | use relative_path::RelativePathBuf; | 4 | use relative_path::RelativePathBuf; |
5 | use thread_worker::WorkerHandle; | ||
6 | use walkdir::WalkDir; | ||
10 | 7 | ||
11 | use crate::{VfsRoot, has_rs_extension}; | 8 | mod watcher; |
9 | use watcher::Watcher; | ||
12 | 10 | ||
13 | pub(crate) struct Task { | 11 | use crate::{RootFilter, Roots, VfsRoot}; |
14 | pub(crate) root: VfsRoot, | 12 | |
15 | pub(crate) path: PathBuf, | 13 | pub(crate) enum Task { |
16 | pub(crate) filter: Box<Fn(&DirEntry) -> bool + Send>, | 14 | AddRoot { |
15 | root: VfsRoot, | ||
16 | filter: Arc<RootFilter>, | ||
17 | }, | ||
17 | } | 18 | } |
18 | 19 | ||
19 | pub struct TaskResult { | 20 | #[derive(Debug)] |
20 | pub(crate) root: VfsRoot, | 21 | pub enum TaskResult { |
21 | pub(crate) files: Vec<(RelativePathBuf, String)>, | 22 | BulkLoadRoot { |
23 | root: VfsRoot, | ||
24 | files: Vec<(RelativePathBuf, String)>, | ||
25 | }, | ||
26 | AddSingleFile { | ||
27 | root: VfsRoot, | ||
28 | path: RelativePathBuf, | ||
29 | text: String, | ||
30 | }, | ||
31 | ChangeSingleFile { | ||
32 | root: VfsRoot, | ||
33 | path: RelativePathBuf, | ||
34 | text: String, | ||
35 | }, | ||
36 | RemoveSingleFile { | ||
37 | root: VfsRoot, | ||
38 | path: RelativePathBuf, | ||
39 | }, | ||
22 | } | 40 | } |
23 | 41 | ||
24 | impl fmt::Debug for TaskResult { | 42 | pub(crate) struct Worker { |
25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | 43 | worker: thread_worker::Worker<Task, TaskResult>, |
26 | f.write_str("TaskResult { ... }") | 44 | worker_handle: WorkerHandle, |
27 | } | ||
28 | } | 45 | } |
29 | 46 | ||
30 | pub(crate) type Worker = thread_worker::Worker<Task, TaskResult>; | 47 | impl Worker { |
48 | pub(crate) fn start(roots: Arc<Roots>) -> Worker { | ||
49 | let (worker, worker_handle) = | ||
50 | thread_worker::spawn("vfs", 128, move |input_receiver, output_sender| { | ||
51 | let mut watcher = match Watcher::start(roots, output_sender.clone()) { | ||
52 | Ok(w) => Some(w), | ||
53 | Err(e) => { | ||
54 | log::error!("could not start watcher: {}", e); | ||
55 | None | ||
56 | } | ||
57 | }; | ||
58 | let res = input_receiver | ||
59 | .into_iter() | ||
60 | .filter_map(|t| handle_task(t, &mut watcher)) | ||
61 | .try_for_each(|it| output_sender.send(it)); | ||
62 | if let Some(watcher) = watcher { | ||
63 | let _ = watcher.shutdown(); | ||
64 | } | ||
65 | res.unwrap() | ||
66 | }); | ||
67 | Worker { | ||
68 | worker, | ||
69 | worker_handle, | ||
70 | } | ||
71 | } | ||
72 | |||
73 | pub(crate) fn sender(&self) -> &Sender<Task> { | ||
74 | &self.worker.inp | ||
75 | } | ||
76 | |||
77 | pub(crate) fn receiver(&self) -> &Receiver<TaskResult> { | ||
78 | &self.worker.out | ||
79 | } | ||
31 | 80 | ||
32 | pub(crate) fn start() -> (Worker, WorkerHandle) { | 81 | pub(crate) fn shutdown(self) -> thread::Result<()> { |
33 | thread_worker::spawn("vfs", 128, |input_receiver, output_sender| { | 82 | let _ = self.worker.shutdown(); |
34 | input_receiver | 83 | self.worker_handle.shutdown() |
35 | .into_iter() | 84 | } |
36 | .map(handle_task) | ||
37 | .try_for_each(|it| output_sender.send(it)) | ||
38 | .unwrap() | ||
39 | }) | ||
40 | } | 85 | } |
41 | 86 | ||
42 | fn handle_task(task: Task) -> TaskResult { | 87 | fn handle_task(task: Task, watcher: &mut Option<Watcher>) -> Option<TaskResult> { |
43 | let Task { root, path, filter } = task; | 88 | match task { |
44 | log::debug!("loading {} ...", path.as_path().display()); | 89 | Task::AddRoot { root, filter } => { |
45 | let files = load_root(path.as_path(), &*filter); | 90 | if let Some(watcher) = watcher { |
46 | log::debug!("... loaded {}", path.as_path().display()); | 91 | watcher.watch_root(&filter) |
47 | TaskResult { root, files } | 92 | } |
93 | log::debug!("loading {} ...", filter.root.as_path().display()); | ||
94 | let files = load_root(filter.as_ref()); | ||
95 | log::debug!("... loaded {}", filter.root.as_path().display()); | ||
96 | Some(TaskResult::BulkLoadRoot { root, files }) | ||
97 | } | ||
98 | } | ||
48 | } | 99 | } |
49 | 100 | ||
50 | fn load_root(root: &Path, filter: &dyn Fn(&DirEntry) -> bool) -> Vec<(RelativePathBuf, String)> { | 101 | fn load_root(filter: &RootFilter) -> Vec<(RelativePathBuf, String)> { |
51 | let mut res = Vec::new(); | 102 | let mut res = Vec::new(); |
52 | for entry in WalkDir::new(root).into_iter().filter_entry(filter) { | 103 | for entry in WalkDir::new(&filter.root) |
104 | .into_iter() | ||
105 | .filter_entry(filter.entry_filter()) | ||
106 | { | ||
53 | let entry = match entry { | 107 | let entry = match entry { |
54 | Ok(entry) => entry, | 108 | Ok(entry) => entry, |
55 | Err(e) => { | 109 | Err(e) => { |
@@ -61,9 +115,6 @@ fn load_root(root: &Path, filter: &dyn Fn(&DirEntry) -> bool) -> Vec<(RelativePa | |||
61 | continue; | 115 | continue; |
62 | } | 116 | } |
63 | let path = entry.path(); | 117 | let path = entry.path(); |
64 | if !has_rs_extension(path) { | ||
65 | continue; | ||
66 | } | ||
67 | let text = match fs::read_to_string(path) { | 118 | let text = match fs::read_to_string(path) { |
68 | Ok(text) => text, | 119 | Ok(text) => text, |
69 | Err(e) => { | 120 | Err(e) => { |
@@ -71,7 +122,7 @@ fn load_root(root: &Path, filter: &dyn Fn(&DirEntry) -> bool) -> Vec<(RelativePa | |||
71 | continue; | 122 | continue; |
72 | } | 123 | } |
73 | }; | 124 | }; |
74 | let path = RelativePathBuf::from_path(path.strip_prefix(root).unwrap()).unwrap(); | 125 | let path = RelativePathBuf::from_path(path.strip_prefix(&filter.root).unwrap()).unwrap(); |
75 | res.push((path.to_owned(), text)) | 126 | res.push((path.to_owned(), text)) |
76 | } | 127 | } |
77 | res | 128 | res |
diff --git a/crates/ra_vfs/src/io/watcher.rs b/crates/ra_vfs/src/io/watcher.rs new file mode 100644 index 000000000..ff6775f59 --- /dev/null +++ b/crates/ra_vfs/src/io/watcher.rs | |||
@@ -0,0 +1,200 @@ | |||
1 | use crate::{io, RootFilter, Roots, VfsRoot}; | ||
2 | use crossbeam_channel::Sender; | ||
3 | use drop_bomb::DropBomb; | ||
4 | use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; | ||
5 | use parking_lot::Mutex; | ||
6 | use std::{ | ||
7 | fs, | ||
8 | path::{Path, PathBuf}, | ||
9 | sync::{mpsc, Arc}, | ||
10 | thread, | ||
11 | time::Duration, | ||
12 | }; | ||
13 | use walkdir::WalkDir; | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | enum ChangeKind { | ||
17 | Create, | ||
18 | Write, | ||
19 | Remove, | ||
20 | } | ||
21 | |||
22 | const WATCHER_DELAY: Duration = Duration::from_millis(250); | ||
23 | |||
24 | pub(crate) struct Watcher { | ||
25 | thread: thread::JoinHandle<()>, | ||
26 | bomb: DropBomb, | ||
27 | watcher: Arc<Mutex<Option<RecommendedWatcher>>>, | ||
28 | } | ||
29 | |||
30 | impl Watcher { | ||
31 | pub(crate) fn start( | ||
32 | roots: Arc<Roots>, | ||
33 | output_sender: Sender<io::TaskResult>, | ||
34 | ) -> Result<Watcher, Box<std::error::Error>> { | ||
35 | let (input_sender, input_receiver) = mpsc::channel(); | ||
36 | let watcher = Arc::new(Mutex::new(Some(notify::watcher( | ||
37 | input_sender, | ||
38 | WATCHER_DELAY, | ||
39 | )?))); | ||
40 | let sender = output_sender.clone(); | ||
41 | let watcher_clone = watcher.clone(); | ||
42 | let thread = thread::spawn(move || { | ||
43 | let worker = WatcherWorker { | ||
44 | roots, | ||
45 | watcher: watcher_clone, | ||
46 | sender, | ||
47 | }; | ||
48 | input_receiver | ||
49 | .into_iter() | ||
50 | // forward relevant events only | ||
51 | .try_for_each(|change| worker.handle_debounced_event(change)) | ||
52 | .unwrap() | ||
53 | }); | ||
54 | Ok(Watcher { | ||
55 | thread, | ||
56 | watcher, | ||
57 | bomb: DropBomb::new(format!("Watcher was not shutdown")), | ||
58 | }) | ||
59 | } | ||
60 | |||
61 | pub fn watch_root(&mut self, filter: &RootFilter) { | ||
62 | for res in WalkDir::new(&filter.root) | ||
63 | .into_iter() | ||
64 | .filter_entry(filter.entry_filter()) | ||
65 | { | ||
66 | match res { | ||
67 | Ok(entry) => { | ||
68 | if entry.file_type().is_dir() { | ||
69 | watch_one(self.watcher.as_ref(), entry.path()); | ||
70 | } | ||
71 | } | ||
72 | Err(e) => log::warn!("watcher error: {}", e), | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | |||
77 | pub fn shutdown(mut self) -> thread::Result<()> { | ||
78 | self.bomb.defuse(); | ||
79 | drop(self.watcher.lock().take()); | ||
80 | let res = self.thread.join(); | ||
81 | match &res { | ||
82 | Ok(()) => log::info!("... Watcher terminated with ok"), | ||
83 | Err(_) => log::error!("... Watcher terminated with err"), | ||
84 | } | ||
85 | res | ||
86 | } | ||
87 | } | ||
88 | |||
89 | struct WatcherWorker { | ||
90 | watcher: Arc<Mutex<Option<RecommendedWatcher>>>, | ||
91 | roots: Arc<Roots>, | ||
92 | sender: Sender<io::TaskResult>, | ||
93 | } | ||
94 | |||
95 | impl WatcherWorker { | ||
96 | fn handle_debounced_event(&self, ev: DebouncedEvent) -> Result<(), Box<std::error::Error>> { | ||
97 | match ev { | ||
98 | DebouncedEvent::NoticeWrite(_) | ||
99 | | DebouncedEvent::NoticeRemove(_) | ||
100 | | DebouncedEvent::Chmod(_) => { | ||
101 | // ignore | ||
102 | } | ||
103 | DebouncedEvent::Rescan => { | ||
104 | // TODO rescan all roots | ||
105 | } | ||
106 | DebouncedEvent::Create(path) => { | ||
107 | self.handle_change(path, ChangeKind::Create); | ||
108 | } | ||
109 | DebouncedEvent::Write(path) => { | ||
110 | self.handle_change(path, ChangeKind::Write); | ||
111 | } | ||
112 | DebouncedEvent::Remove(path) => { | ||
113 | self.handle_change(path, ChangeKind::Remove); | ||
114 | } | ||
115 | DebouncedEvent::Rename(src, dst) => { | ||
116 | self.handle_change(src, ChangeKind::Remove); | ||
117 | self.handle_change(dst, ChangeKind::Create); | ||
118 | } | ||
119 | DebouncedEvent::Error(err, path) => { | ||
120 | // TODO should we reload the file contents? | ||
121 | log::warn!("watcher error \"{}\", {:?}", err, path); | ||
122 | } | ||
123 | } | ||
124 | Ok(()) | ||
125 | } | ||
126 | |||
127 | fn handle_change(&self, path: PathBuf, kind: ChangeKind) { | ||
128 | if let Err(e) = self.try_handle_change(path, kind) { | ||
129 | log::warn!("watcher error: {}", e) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | fn try_handle_change( | ||
134 | &self, | ||
135 | path: PathBuf, | ||
136 | kind: ChangeKind, | ||
137 | ) -> Result<(), Box<std::error::Error>> { | ||
138 | let (root, rel_path) = match self.roots.find(&path) { | ||
139 | Some(x) => x, | ||
140 | None => return Ok(()), | ||
141 | }; | ||
142 | match kind { | ||
143 | ChangeKind::Create => { | ||
144 | if path.is_dir() { | ||
145 | self.watch_recursive(&path, root); | ||
146 | } else { | ||
147 | let text = fs::read_to_string(&path)?; | ||
148 | self.sender.send(io::TaskResult::AddSingleFile { | ||
149 | root, | ||
150 | path: rel_path, | ||
151 | text, | ||
152 | })? | ||
153 | } | ||
154 | } | ||
155 | ChangeKind::Write => { | ||
156 | let text = fs::read_to_string(&path)?; | ||
157 | self.sender.send(io::TaskResult::ChangeSingleFile { | ||
158 | root, | ||
159 | path: rel_path, | ||
160 | text, | ||
161 | })? | ||
162 | } | ||
163 | ChangeKind::Remove => self.sender.send(io::TaskResult::RemoveSingleFile { | ||
164 | root, | ||
165 | path: rel_path, | ||
166 | })?, | ||
167 | } | ||
168 | Ok(()) | ||
169 | } | ||
170 | |||
171 | fn watch_recursive(&self, dir: &Path, root: VfsRoot) { | ||
172 | let filter = &self.roots[root]; | ||
173 | for res in WalkDir::new(dir) | ||
174 | .into_iter() | ||
175 | .filter_entry(filter.entry_filter()) | ||
176 | { | ||
177 | match res { | ||
178 | Ok(entry) => { | ||
179 | if entry.file_type().is_dir() { | ||
180 | watch_one(self.watcher.as_ref(), entry.path()); | ||
181 | } else { | ||
182 | // emit only for files otherwise we will cause watch_recursive to be called again with a dir that we are already watching | ||
183 | // emit as create because we haven't seen it yet | ||
184 | self.handle_change(entry.path().to_path_buf(), ChangeKind::Create); | ||
185 | } | ||
186 | } | ||
187 | Err(e) => log::warn!("watcher error: {}", e), | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | } | ||
192 | |||
193 | fn watch_one(watcher: &Mutex<Option<RecommendedWatcher>>, dir: &Path) { | ||
194 | if let Some(watcher) = watcher.lock().as_mut() { | ||
195 | match watcher.watch(dir, RecursiveMode::NonRecursive) { | ||
196 | Ok(()) => log::debug!("watching \"{}\"", dir.display()), | ||
197 | Err(e) => log::warn!("could not watch \"{}\": {}", dir.display(), e), | ||
198 | } | ||
199 | } | ||
200 | } | ||
diff --git a/crates/ra_vfs/src/lib.rs b/crates/ra_vfs/src/lib.rs index cdea18d73..d1b0222e7 100644 --- a/crates/ra_vfs/src/lib.rs +++ b/crates/ra_vfs/src/lib.rs | |||
@@ -16,52 +16,77 @@ | |||
16 | mod io; | 16 | mod io; |
17 | 17 | ||
18 | use std::{ | 18 | use std::{ |
19 | fmt, | ||
20 | mem, | ||
21 | thread, | ||
22 | cmp::Reverse, | 19 | cmp::Reverse, |
20 | fmt, fs, mem, | ||
21 | ops::{Deref, DerefMut}, | ||
23 | path::{Path, PathBuf}, | 22 | path::{Path, PathBuf}, |
24 | ffi::OsStr, | ||
25 | sync::Arc, | 23 | sync::Arc, |
26 | fs, | 24 | thread, |
27 | }; | 25 | }; |
28 | 26 | ||
29 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
30 | use relative_path::RelativePathBuf; | ||
31 | use crossbeam_channel::Receiver; | 27 | use crossbeam_channel::Receiver; |
28 | use ra_arena::{impl_arena_id, Arena, RawId}; | ||
29 | use relative_path::{Component, RelativePath, RelativePathBuf}; | ||
30 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
32 | use walkdir::DirEntry; | 31 | use walkdir::DirEntry; |
33 | use thread_worker::WorkerHandle; | ||
34 | use ra_arena::{Arena, RawId, impl_arena_id}; | ||
35 | 32 | ||
36 | pub use crate::io::TaskResult as VfsTask; | 33 | pub use crate::io::TaskResult as VfsTask; |
34 | use io::{TaskResult, Worker}; | ||
37 | 35 | ||
38 | /// `RootFilter` is a predicate that checks if a file can belong to a root. If | 36 | /// `RootFilter` is a predicate that checks if a file can belong to a root. If |
39 | /// several filters match a file (nested dirs), the most nested one wins. | 37 | /// several filters match a file (nested dirs), the most nested one wins. |
40 | struct RootFilter { | 38 | pub(crate) struct RootFilter { |
41 | root: PathBuf, | 39 | root: PathBuf, |
42 | file_filter: fn(&Path) -> bool, | 40 | filter: fn(&Path, &RelativePath) -> bool, |
41 | excluded_dirs: Vec<PathBuf>, | ||
43 | } | 42 | } |
44 | 43 | ||
45 | impl RootFilter { | 44 | impl RootFilter { |
46 | fn new(root: PathBuf) -> RootFilter { | 45 | fn new(root: PathBuf, excluded_dirs: Vec<PathBuf>) -> RootFilter { |
47 | RootFilter { | 46 | RootFilter { |
48 | root, | 47 | root, |
49 | file_filter: has_rs_extension, | 48 | filter: default_filter, |
49 | excluded_dirs, | ||
50 | } | 50 | } |
51 | } | 51 | } |
52 | /// Check if this root can contain `path`. NB: even if this returns | 52 | /// Check if this root can contain `path`. NB: even if this returns |
53 | /// true, the `path` might actually be conained in some nested root. | 53 | /// true, the `path` might actually be conained in some nested root. |
54 | fn can_contain(&self, path: &Path) -> Option<RelativePathBuf> { | 54 | pub(crate) fn can_contain(&self, path: &Path) -> Option<RelativePathBuf> { |
55 | if !(self.file_filter)(path) { | 55 | let rel_path = path.strip_prefix(&self.root).ok()?; |
56 | let rel_path = RelativePathBuf::from_path(rel_path).ok()?; | ||
57 | if !(self.filter)(path, rel_path.as_relative_path()) { | ||
56 | return None; | 58 | return None; |
57 | } | 59 | } |
58 | let path = path.strip_prefix(&self.root).ok()?; | 60 | Some(rel_path) |
59 | RelativePathBuf::from_path(path).ok() | 61 | } |
62 | |||
63 | pub(crate) fn entry_filter<'a>(&'a self) -> impl FnMut(&DirEntry) -> bool + 'a { | ||
64 | move |entry: &DirEntry| { | ||
65 | if entry.file_type().is_dir() && self.excluded_dirs.iter().any(|it| it == entry.path()) | ||
66 | { | ||
67 | // do not walk nested roots | ||
68 | false | ||
69 | } else { | ||
70 | self.can_contain(entry.path()).is_some() | ||
71 | } | ||
72 | } | ||
60 | } | 73 | } |
61 | } | 74 | } |
62 | 75 | ||
63 | fn has_rs_extension(p: &Path) -> bool { | 76 | pub(crate) fn default_filter(path: &Path, rel_path: &RelativePath) -> bool { |
64 | p.extension() == Some(OsStr::new("rs")) | 77 | if path.is_dir() { |
78 | for (i, c) in rel_path.components().enumerate() { | ||
79 | if let Component::Normal(c) = c { | ||
80 | // TODO hardcoded for now | ||
81 | if (i == 0 && c == "target") || c == ".git" || c == "node_modules" { | ||
82 | return false; | ||
83 | } | ||
84 | } | ||
85 | } | ||
86 | true | ||
87 | } else { | ||
88 | rel_path.extension() == Some("rs") | ||
89 | } | ||
65 | } | 90 | } |
66 | 91 | ||
67 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] | 92 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
@@ -75,16 +100,58 @@ impl_arena_id!(VfsFile); | |||
75 | struct VfsFileData { | 100 | struct VfsFileData { |
76 | root: VfsRoot, | 101 | root: VfsRoot, |
77 | path: RelativePathBuf, | 102 | path: RelativePathBuf, |
103 | is_overlayed: bool, | ||
78 | text: Arc<String>, | 104 | text: Arc<String>, |
79 | } | 105 | } |
80 | 106 | ||
107 | pub(crate) struct Roots { | ||
108 | roots: Arena<VfsRoot, Arc<RootFilter>>, | ||
109 | } | ||
110 | |||
111 | impl Roots { | ||
112 | pub(crate) fn new(mut paths: Vec<PathBuf>) -> Roots { | ||
113 | let mut roots = Arena::default(); | ||
114 | // A hack to make nesting work. | ||
115 | paths.sort_by_key(|it| Reverse(it.as_os_str().len())); | ||
116 | for (i, path) in paths.iter().enumerate() { | ||
117 | let nested_roots = paths[..i] | ||
118 | .iter() | ||
119 | .filter(|it| it.starts_with(path)) | ||
120 | .map(|it| it.clone()) | ||
121 | .collect::<Vec<_>>(); | ||
122 | |||
123 | let root_filter = Arc::new(RootFilter::new(path.clone(), nested_roots)); | ||
124 | |||
125 | roots.alloc(root_filter.clone()); | ||
126 | } | ||
127 | Roots { roots } | ||
128 | } | ||
129 | pub(crate) fn find(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf)> { | ||
130 | self.roots | ||
131 | .iter() | ||
132 | .find_map(|(root, data)| data.can_contain(path).map(|it| (root, it))) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | impl Deref for Roots { | ||
137 | type Target = Arena<VfsRoot, Arc<RootFilter>>; | ||
138 | fn deref(&self) -> &Self::Target { | ||
139 | &self.roots | ||
140 | } | ||
141 | } | ||
142 | |||
143 | impl DerefMut for Roots { | ||
144 | fn deref_mut(&mut self) -> &mut Self::Target { | ||
145 | &mut self.roots | ||
146 | } | ||
147 | } | ||
148 | |||
81 | pub struct Vfs { | 149 | pub struct Vfs { |
82 | roots: Arena<VfsRoot, RootFilter>, | 150 | roots: Arc<Roots>, |
83 | files: Arena<VfsFile, VfsFileData>, | 151 | files: Arena<VfsFile, VfsFileData>, |
84 | root2files: FxHashMap<VfsRoot, FxHashSet<VfsFile>>, | 152 | root2files: FxHashMap<VfsRoot, FxHashSet<VfsFile>>, |
85 | pending_changes: Vec<VfsChange>, | 153 | pending_changes: Vec<VfsChange>, |
86 | worker: io::Worker, | 154 | worker: Worker, |
87 | worker_handle: WorkerHandle, | ||
88 | } | 155 | } |
89 | 156 | ||
90 | impl fmt::Debug for Vfs { | 157 | impl fmt::Debug for Vfs { |
@@ -94,44 +161,30 @@ impl fmt::Debug for Vfs { | |||
94 | } | 161 | } |
95 | 162 | ||
96 | impl Vfs { | 163 | impl Vfs { |
97 | pub fn new(mut roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) { | 164 | pub fn new(roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) { |
98 | let (worker, worker_handle) = io::start(); | 165 | let roots = Arc::new(Roots::new(roots)); |
99 | 166 | let worker = io::Worker::start(roots.clone()); | |
100 | let mut res = Vfs { | 167 | let mut root2files = FxHashMap::default(); |
101 | roots: Arena::default(), | 168 | |
169 | for (root, filter) in roots.iter() { | ||
170 | root2files.insert(root, Default::default()); | ||
171 | worker | ||
172 | .sender() | ||
173 | .send(io::Task::AddRoot { | ||
174 | root, | ||
175 | filter: filter.clone(), | ||
176 | }) | ||
177 | .unwrap(); | ||
178 | } | ||
179 | let res = Vfs { | ||
180 | roots, | ||
102 | files: Arena::default(), | 181 | files: Arena::default(), |
103 | root2files: FxHashMap::default(), | 182 | root2files, |
104 | worker, | 183 | worker, |
105 | worker_handle, | ||
106 | pending_changes: Vec::new(), | 184 | pending_changes: Vec::new(), |
107 | }; | 185 | }; |
108 | 186 | let vfs_roots = res.roots.iter().map(|(id, _)| id).collect(); | |
109 | // A hack to make nesting work. | 187 | (res, vfs_roots) |
110 | roots.sort_by_key(|it| Reverse(it.as_os_str().len())); | ||
111 | for (i, path) in roots.iter().enumerate() { | ||
112 | let root = res.roots.alloc(RootFilter::new(path.clone())); | ||
113 | res.root2files.insert(root, Default::default()); | ||
114 | let nested = roots[..i] | ||
115 | .iter() | ||
116 | .filter(|it| it.starts_with(path)) | ||
117 | .map(|it| it.clone()) | ||
118 | .collect::<Vec<_>>(); | ||
119 | let filter = move |entry: &DirEntry| { | ||
120 | if entry.file_type().is_file() { | ||
121 | has_rs_extension(entry.path()) | ||
122 | } else { | ||
123 | nested.iter().all(|it| it != entry.path()) | ||
124 | } | ||
125 | }; | ||
126 | let task = io::Task { | ||
127 | root, | ||
128 | path: path.clone(), | ||
129 | filter: Box::new(filter), | ||
130 | }; | ||
131 | res.worker.inp.send(task).unwrap(); | ||
132 | } | ||
133 | let roots = res.roots.iter().map(|(id, _)| id).collect(); | ||
134 | (res, roots) | ||
135 | } | 188 | } |
136 | 189 | ||
137 | pub fn root2path(&self, root: VfsRoot) -> PathBuf { | 190 | pub fn root2path(&self, root: VfsRoot) -> PathBuf { |
@@ -165,7 +218,7 @@ impl Vfs { | |||
165 | } else { | 218 | } else { |
166 | let text = fs::read_to_string(path).unwrap_or_default(); | 219 | let text = fs::read_to_string(path).unwrap_or_default(); |
167 | let text = Arc::new(text); | 220 | let text = Arc::new(text); |
168 | let file = self.add_file(root, rel_path.clone(), Arc::clone(&text)); | 221 | let file = self.add_file(root, rel_path.clone(), Arc::clone(&text), false); |
169 | let change = VfsChange::AddFile { | 222 | let change = VfsChange::AddFile { |
170 | file, | 223 | file, |
171 | text, | 224 | text, |
@@ -180,85 +233,130 @@ impl Vfs { | |||
180 | } | 233 | } |
181 | 234 | ||
182 | pub fn task_receiver(&self) -> &Receiver<io::TaskResult> { | 235 | pub fn task_receiver(&self) -> &Receiver<io::TaskResult> { |
183 | &self.worker.out | 236 | self.worker.receiver() |
184 | } | 237 | } |
185 | 238 | ||
186 | pub fn handle_task(&mut self, task: io::TaskResult) { | 239 | pub fn handle_task(&mut self, task: io::TaskResult) { |
187 | let mut files = Vec::new(); | 240 | match task { |
188 | // While we were scanning the root in the backgound, a file might have | 241 | TaskResult::BulkLoadRoot { root, files } => { |
189 | // been open in the editor, so we need to account for that. | 242 | let mut cur_files = Vec::new(); |
190 | let exising = self.root2files[&task.root] | 243 | // While we were scanning the root in the backgound, a file might have |
191 | .iter() | 244 | // been open in the editor, so we need to account for that. |
192 | .map(|&file| (self.files[file].path.clone(), file)) | 245 | let exising = self.root2files[&root] |
193 | .collect::<FxHashMap<_, _>>(); | 246 | .iter() |
194 | for (path, text) in task.files { | 247 | .map(|&file| (self.files[file].path.clone(), file)) |
195 | if let Some(&file) = exising.get(&path) { | 248 | .collect::<FxHashMap<_, _>>(); |
196 | let text = Arc::clone(&self.files[file].text); | 249 | for (path, text) in files { |
197 | files.push((file, path, text)); | 250 | if let Some(&file) = exising.get(&path) { |
198 | continue; | 251 | let text = Arc::clone(&self.files[file].text); |
252 | cur_files.push((file, path, text)); | ||
253 | continue; | ||
254 | } | ||
255 | let text = Arc::new(text); | ||
256 | let file = self.add_file(root, path.clone(), Arc::clone(&text), false); | ||
257 | cur_files.push((file, path, text)); | ||
258 | } | ||
259 | |||
260 | let change = VfsChange::AddRoot { | ||
261 | root, | ||
262 | files: cur_files, | ||
263 | }; | ||
264 | self.pending_changes.push(change); | ||
265 | } | ||
266 | TaskResult::AddSingleFile { root, path, text } => { | ||
267 | self.do_add_file(root, path, text, false); | ||
268 | } | ||
269 | TaskResult::ChangeSingleFile { root, path, text } => { | ||
270 | if let Some(file) = self.find_file(root, &path) { | ||
271 | self.do_change_file(file, text, false); | ||
272 | } else { | ||
273 | self.do_add_file(root, path, text, false); | ||
274 | } | ||
275 | } | ||
276 | TaskResult::RemoveSingleFile { root, path } => { | ||
277 | if let Some(file) = self.find_file(root, &path) { | ||
278 | self.do_remove_file(root, path, file, false); | ||
279 | } | ||
199 | } | 280 | } |
200 | let text = Arc::new(text); | ||
201 | let file = self.add_file(task.root, path.clone(), Arc::clone(&text)); | ||
202 | files.push((file, path, text)); | ||
203 | } | 281 | } |
282 | } | ||
204 | 283 | ||
205 | let change = VfsChange::AddRoot { | 284 | fn do_add_file( |
206 | root: task.root, | 285 | &mut self, |
207 | files, | 286 | root: VfsRoot, |
208 | }; | 287 | path: RelativePathBuf, |
209 | self.pending_changes.push(change); | 288 | text: String, |
289 | is_overlay: bool, | ||
290 | ) -> Option<VfsFile> { | ||
291 | let text = Arc::new(text); | ||
292 | let file = self.add_file(root, path.clone(), text.clone(), is_overlay); | ||
293 | self.pending_changes.push(VfsChange::AddFile { | ||
294 | file, | ||
295 | root, | ||
296 | path, | ||
297 | text, | ||
298 | }); | ||
299 | Some(file) | ||
300 | } | ||
301 | |||
302 | fn do_change_file(&mut self, file: VfsFile, text: String, is_overlay: bool) { | ||
303 | if !is_overlay && self.files[file].is_overlayed { | ||
304 | return; | ||
305 | } | ||
306 | let text = Arc::new(text); | ||
307 | self.change_file(file, text.clone(), is_overlay); | ||
308 | self.pending_changes | ||
309 | .push(VfsChange::ChangeFile { file, text }); | ||
310 | } | ||
311 | |||
312 | fn do_remove_file( | ||
313 | &mut self, | ||
314 | root: VfsRoot, | ||
315 | path: RelativePathBuf, | ||
316 | file: VfsFile, | ||
317 | is_overlay: bool, | ||
318 | ) { | ||
319 | if !is_overlay && self.files[file].is_overlayed { | ||
320 | return; | ||
321 | } | ||
322 | self.remove_file(file); | ||
323 | self.pending_changes | ||
324 | .push(VfsChange::RemoveFile { root, path, file }); | ||
210 | } | 325 | } |
211 | 326 | ||
212 | pub fn add_file_overlay(&mut self, path: &Path, text: String) -> Option<VfsFile> { | 327 | pub fn add_file_overlay(&mut self, path: &Path, text: String) -> Option<VfsFile> { |
213 | let mut res = None; | 328 | if let Some((root, rel_path, file)) = self.find_root(path) { |
214 | if let Some((root, path, file)) = self.find_root(path) { | 329 | if let Some(file) = file { |
215 | let text = Arc::new(text); | 330 | self.do_change_file(file, text, true); |
216 | let change = if let Some(file) = file { | 331 | Some(file) |
217 | res = Some(file); | ||
218 | self.change_file(file, Arc::clone(&text)); | ||
219 | VfsChange::ChangeFile { file, text } | ||
220 | } else { | 332 | } else { |
221 | let file = self.add_file(root, path.clone(), Arc::clone(&text)); | 333 | self.do_add_file(root, rel_path, text, true) |
222 | res = Some(file); | 334 | } |
223 | VfsChange::AddFile { | 335 | } else { |
224 | file, | 336 | None |
225 | text, | ||
226 | root, | ||
227 | path, | ||
228 | } | ||
229 | }; | ||
230 | self.pending_changes.push(change); | ||
231 | } | 337 | } |
232 | res | ||
233 | } | 338 | } |
234 | 339 | ||
235 | pub fn change_file_overlay(&mut self, path: &Path, new_text: String) { | 340 | pub fn change_file_overlay(&mut self, path: &Path, new_text: String) { |
236 | if let Some((_root, _path, file)) = self.find_root(path) { | 341 | if let Some((_root, _path, file)) = self.find_root(path) { |
237 | let file = file.expect("can't change a file which wasn't added"); | 342 | let file = file.expect("can't change a file which wasn't added"); |
238 | let text = Arc::new(new_text); | 343 | self.do_change_file(file, new_text, true); |
239 | self.change_file(file, Arc::clone(&text)); | ||
240 | let change = VfsChange::ChangeFile { file, text }; | ||
241 | self.pending_changes.push(change); | ||
242 | } | 344 | } |
243 | } | 345 | } |
244 | 346 | ||
245 | pub fn remove_file_overlay(&mut self, path: &Path) -> Option<VfsFile> { | 347 | pub fn remove_file_overlay(&mut self, path: &Path) -> Option<VfsFile> { |
246 | let mut res = None; | ||
247 | if let Some((root, path, file)) = self.find_root(path) { | 348 | if let Some((root, path, file)) = self.find_root(path) { |
248 | let file = file.expect("can't remove a file which wasn't added"); | 349 | let file = file.expect("can't remove a file which wasn't added"); |
249 | res = Some(file); | ||
250 | let full_path = path.to_path(&self.roots[root].root); | 350 | let full_path = path.to_path(&self.roots[root].root); |
251 | let change = if let Ok(text) = fs::read_to_string(&full_path) { | 351 | if let Ok(text) = fs::read_to_string(&full_path) { |
252 | let text = Arc::new(text); | 352 | self.do_change_file(file, text, true); |
253 | self.change_file(file, Arc::clone(&text)); | ||
254 | VfsChange::ChangeFile { file, text } | ||
255 | } else { | 353 | } else { |
256 | self.remove_file(file); | 354 | self.do_remove_file(root, path, file, true); |
257 | VfsChange::RemoveFile { root, file, path } | 355 | } |
258 | }; | 356 | Some(file) |
259 | self.pending_changes.push(change); | 357 | } else { |
358 | None | ||
260 | } | 359 | } |
261 | res | ||
262 | } | 360 | } |
263 | 361 | ||
264 | pub fn commit_changes(&mut self) -> Vec<VfsChange> { | 362 | pub fn commit_changes(&mut self) -> Vec<VfsChange> { |
@@ -267,19 +365,31 @@ impl Vfs { | |||
267 | 365 | ||
268 | /// Sutdown the VFS and terminate the background watching thread. | 366 | /// Sutdown the VFS and terminate the background watching thread. |
269 | pub fn shutdown(self) -> thread::Result<()> { | 367 | pub fn shutdown(self) -> thread::Result<()> { |
270 | let _ = self.worker.shutdown(); | 368 | self.worker.shutdown() |
271 | self.worker_handle.shutdown() | ||
272 | } | 369 | } |
273 | 370 | ||
274 | fn add_file(&mut self, root: VfsRoot, path: RelativePathBuf, text: Arc<String>) -> VfsFile { | 371 | fn add_file( |
275 | let data = VfsFileData { root, path, text }; | 372 | &mut self, |
373 | root: VfsRoot, | ||
374 | path: RelativePathBuf, | ||
375 | text: Arc<String>, | ||
376 | is_overlayed: bool, | ||
377 | ) -> VfsFile { | ||
378 | let data = VfsFileData { | ||
379 | root, | ||
380 | path, | ||
381 | text, | ||
382 | is_overlayed, | ||
383 | }; | ||
276 | let file = self.files.alloc(data); | 384 | let file = self.files.alloc(data); |
277 | self.root2files.get_mut(&root).unwrap().insert(file); | 385 | self.root2files.get_mut(&root).unwrap().insert(file); |
278 | file | 386 | file |
279 | } | 387 | } |
280 | 388 | ||
281 | fn change_file(&mut self, file: VfsFile, new_text: Arc<String>) { | 389 | fn change_file(&mut self, file: VfsFile, new_text: Arc<String>, is_overlayed: bool) { |
282 | self.files[file].text = new_text; | 390 | let mut file_data = &mut self.files[file]; |
391 | file_data.text = new_text; | ||
392 | file_data.is_overlayed = is_overlayed; | ||
283 | } | 393 | } |
284 | 394 | ||
285 | fn remove_file(&mut self, file: VfsFile) { | 395 | fn remove_file(&mut self, file: VfsFile) { |
@@ -292,15 +402,16 @@ impl Vfs { | |||
292 | } | 402 | } |
293 | 403 | ||
294 | fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> { | 404 | fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> { |
295 | let (root, path) = self | 405 | let (root, path) = self.roots.find(&path)?; |
296 | .roots | 406 | let file = self.find_file(root, &path); |
297 | .iter() | 407 | Some((root, path, file)) |
298 | .find_map(|(root, data)| data.can_contain(path).map(|it| (root, it)))?; | 408 | } |
299 | let file = self.root2files[&root] | 409 | |
410 | fn find_file(&self, root: VfsRoot, path: &RelativePath) -> Option<VfsFile> { | ||
411 | self.root2files[&root] | ||
300 | .iter() | 412 | .iter() |
301 | .map(|&it| it) | 413 | .map(|&it| it) |
302 | .find(|&file| self.files[file].path == path); | 414 | .find(|&file| self.files[file].path == path) |
303 | Some((root, path, file)) | ||
304 | } | 415 | } |
305 | } | 416 | } |
306 | 417 | ||
diff --git a/crates/ra_vfs/tests/vfs.rs b/crates/ra_vfs/tests/vfs.rs index f56fc4603..357e1c775 100644 --- a/crates/ra_vfs/tests/vfs.rs +++ b/crates/ra_vfs/tests/vfs.rs | |||
@@ -1,24 +1,47 @@ | |||
1 | use std::{ | 1 | use std::{collections::HashSet, fs, time::Duration}; |
2 | fs, | ||
3 | collections::HashSet, | ||
4 | }; | ||
5 | 2 | ||
3 | // use flexi_logger::Logger; | ||
4 | use crossbeam_channel::RecvTimeoutError; | ||
5 | use ra_vfs::{Vfs, VfsChange}; | ||
6 | use tempfile::tempdir; | 6 | use tempfile::tempdir; |
7 | 7 | ||
8 | use ra_vfs::{Vfs, VfsChange}; | 8 | fn process_tasks(vfs: &mut Vfs, num_tasks: u32) { |
9 | for _ in 0..num_tasks { | ||
10 | let task = vfs | ||
11 | .task_receiver() | ||
12 | .recv_timeout(Duration::from_secs(3)) | ||
13 | .unwrap(); | ||
14 | log::debug!("{:?}", task); | ||
15 | vfs.handle_task(task); | ||
16 | } | ||
17 | } | ||
18 | |||
19 | macro_rules! assert_match { | ||
20 | ($x:expr, $pat:pat) => { | ||
21 | assert_match!($x, $pat, ()) | ||
22 | }; | ||
23 | ($x:expr, $pat:pat, $assert:expr) => { | ||
24 | match $x { | ||
25 | $pat => $assert, | ||
26 | x => assert!(false, "Expected {}, got {:?}", stringify!($pat), x), | ||
27 | }; | ||
28 | }; | ||
29 | } | ||
9 | 30 | ||
10 | #[test] | 31 | #[test] |
11 | fn test_vfs_works() -> std::io::Result<()> { | 32 | fn test_vfs_works() -> std::io::Result<()> { |
33 | // Logger::with_str("vfs=debug,ra_vfs=debug").start().unwrap(); | ||
34 | |||
12 | let files = [ | 35 | let files = [ |
13 | ("a/foo.rs", "hello"), | 36 | ("a/foo.rs", "hello"), |
14 | ("a/bar.rs", "world"), | 37 | ("a/bar.rs", "world"), |
15 | ("a/b/baz.rs", "nested hello"), | 38 | ("a/b/baz.rs", "nested hello"), |
16 | ]; | 39 | ]; |
17 | 40 | ||
18 | let dir = tempdir()?; | 41 | let dir = tempdir().unwrap(); |
19 | for (path, text) in files.iter() { | 42 | for (path, text) in files.iter() { |
20 | let file_path = dir.path().join(path); | 43 | let file_path = dir.path().join(path); |
21 | fs::create_dir_all(file_path.parent().unwrap())?; | 44 | fs::create_dir_all(file_path.parent().unwrap()).unwrap(); |
22 | fs::write(file_path, text)? | 45 | fs::write(file_path, text)? |
23 | } | 46 | } |
24 | 47 | ||
@@ -26,10 +49,7 @@ fn test_vfs_works() -> std::io::Result<()> { | |||
26 | let b_root = dir.path().join("a/b"); | 49 | let b_root = dir.path().join("a/b"); |
27 | 50 | ||
28 | let (mut vfs, _) = Vfs::new(vec![a_root, b_root]); | 51 | let (mut vfs, _) = Vfs::new(vec![a_root, b_root]); |
29 | for _ in 0..2 { | 52 | process_tasks(&mut vfs, 2); |
30 | let task = vfs.task_receiver().recv().unwrap(); | ||
31 | vfs.handle_task(task); | ||
32 | } | ||
33 | { | 53 | { |
34 | let files = vfs | 54 | let files = vfs |
35 | .commit_changes() | 55 | .commit_changes() |
@@ -58,43 +78,101 @@ fn test_vfs_works() -> std::io::Result<()> { | |||
58 | assert_eq!(files, expected_files); | 78 | assert_eq!(files, expected_files); |
59 | } | 79 | } |
60 | 80 | ||
61 | vfs.add_file_overlay(&dir.path().join("a/b/baz.rs"), "quux".to_string()); | 81 | fs::write(&dir.path().join("a/b/baz.rs"), "quux").unwrap(); |
62 | let change = vfs.commit_changes().pop().unwrap(); | 82 | process_tasks(&mut vfs, 1); |
63 | match change { | 83 | assert_match!( |
64 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "quux"), | 84 | vfs.commit_changes().as_slice(), |
65 | _ => panic!("unexpected change"), | 85 | [VfsChange::ChangeFile { text, .. }], |
66 | } | 86 | assert_eq!(text.as_str(), "quux") |
87 | ); | ||
67 | 88 | ||
68 | vfs.change_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); | 89 | vfs.add_file_overlay(&dir.path().join("a/b/baz.rs"), "m".to_string()); |
69 | let change = vfs.commit_changes().pop().unwrap(); | 90 | assert_match!( |
70 | match change { | 91 | vfs.commit_changes().as_slice(), |
71 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "m"), | 92 | [VfsChange::ChangeFile { text, .. }], |
72 | _ => panic!("unexpected change"), | 93 | assert_eq!(text.as_str(), "m") |
73 | } | 94 | ); |
74 | 95 | ||
96 | // changing file on disk while overlayed doesn't generate a VfsChange | ||
97 | fs::write(&dir.path().join("a/b/baz.rs"), "corge").unwrap(); | ||
98 | process_tasks(&mut vfs, 1); | ||
99 | assert_match!(vfs.commit_changes().as_slice(), []); | ||
100 | |||
101 | // removing overlay restores data on disk | ||
75 | vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); | 102 | vfs.remove_file_overlay(&dir.path().join("a/b/baz.rs")); |
76 | let change = vfs.commit_changes().pop().unwrap(); | 103 | assert_match!( |
77 | match change { | 104 | vfs.commit_changes().as_slice(), |
78 | VfsChange::ChangeFile { text, .. } => assert_eq!(&*text, "nested hello"), | 105 | [VfsChange::ChangeFile { text, .. }], |
79 | _ => panic!("unexpected change"), | 106 | assert_eq!(text.as_str(), "corge") |
80 | } | 107 | ); |
81 | 108 | ||
82 | vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); | 109 | vfs.add_file_overlay(&dir.path().join("a/b/spam.rs"), "spam".to_string()); |
83 | let change = vfs.commit_changes().pop().unwrap(); | 110 | assert_match!( |
84 | match change { | 111 | vfs.commit_changes().as_slice(), |
85 | VfsChange::AddFile { text, path, .. } => { | 112 | [VfsChange::AddFile { text, path, .. }], |
86 | assert_eq!(&*text, "spam"); | 113 | { |
114 | assert_eq!(text.as_str(), "spam"); | ||
87 | assert_eq!(path, "spam.rs"); | 115 | assert_eq!(path, "spam.rs"); |
88 | } | 116 | } |
89 | _ => panic!("unexpected change"), | 117 | ); |
90 | } | ||
91 | 118 | ||
92 | vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); | 119 | vfs.remove_file_overlay(&dir.path().join("a/b/spam.rs")); |
93 | let change = vfs.commit_changes().pop().unwrap(); | 120 | assert_match!( |
94 | match change { | 121 | vfs.commit_changes().as_slice(), |
95 | VfsChange::RemoveFile { .. } => (), | 122 | [VfsChange::RemoveFile { path, .. }], |
96 | _ => panic!("unexpected change"), | 123 | assert_eq!(path, "spam.rs") |
97 | } | 124 | ); |
125 | |||
126 | fs::create_dir_all(dir.path().join("a/sub1/sub2")).unwrap(); | ||
127 | fs::write(dir.path().join("a/sub1/sub2/new.rs"), "new hello").unwrap(); | ||
128 | process_tasks(&mut vfs, 1); | ||
129 | assert_match!( | ||
130 | vfs.commit_changes().as_slice(), | ||
131 | [VfsChange::AddFile { text, path, .. }], | ||
132 | { | ||
133 | assert_eq!(text.as_str(), "new hello"); | ||
134 | assert_eq!(path, "sub1/sub2/new.rs"); | ||
135 | } | ||
136 | ); | ||
137 | |||
138 | fs::rename( | ||
139 | &dir.path().join("a/sub1/sub2/new.rs"), | ||
140 | &dir.path().join("a/sub1/sub2/new1.rs"), | ||
141 | ) | ||
142 | .unwrap(); | ||
143 | process_tasks(&mut vfs, 2); | ||
144 | assert_match!( | ||
145 | vfs.commit_changes().as_slice(), | ||
146 | [VfsChange::RemoveFile { | ||
147 | path: removed_path, .. | ||
148 | }, VfsChange::AddFile { | ||
149 | text, | ||
150 | path: added_path, | ||
151 | .. | ||
152 | }], | ||
153 | { | ||
154 | assert_eq!(removed_path, "sub1/sub2/new.rs"); | ||
155 | assert_eq!(added_path, "sub1/sub2/new1.rs"); | ||
156 | assert_eq!(text.as_str(), "new hello"); | ||
157 | } | ||
158 | ); | ||
159 | |||
160 | fs::remove_file(&dir.path().join("a/sub1/sub2/new1.rs")).unwrap(); | ||
161 | process_tasks(&mut vfs, 1); | ||
162 | assert_match!( | ||
163 | vfs.commit_changes().as_slice(), | ||
164 | [VfsChange::RemoveFile { path, .. }], | ||
165 | assert_eq!(path, "sub1/sub2/new1.rs") | ||
166 | ); | ||
167 | |||
168 | // should be ignored | ||
169 | fs::create_dir_all(dir.path().join("a/target")).unwrap(); | ||
170 | fs::write(&dir.path().join("a/target/new.rs"), "ignore me").unwrap(); | ||
171 | |||
172 | assert_match!( | ||
173 | vfs.task_receiver().recv_timeout(Duration::from_millis(300)), // slightly more than watcher debounce delay | ||
174 | Err(RecvTimeoutError::Timeout) | ||
175 | ); | ||
98 | 176 | ||
99 | vfs.shutdown().unwrap(); | 177 | vfs.shutdown().unwrap(); |
100 | Ok(()) | 178 | Ok(()) |