]> git.lizzy.rs Git - rust.git/blob - crates/ra_vfs/src/lib.rs
refactor, put watcher with `io::Worker`
[rust.git] / crates / ra_vfs / src / lib.rs
1 //! VFS stands for Virtual File System.
2 //!
3 //! When doing analysis, we don't want to do any IO, we want to keep all source
4 //! code in memory. However, the actual source code is stored on disk, so you
5 //! need to get it into the memory in the first place somehow. VFS is the
6 //! component which does this.
7 //!
8 //! It is also responsible for watching the disk for changes, and for merging
9 //! editor state (modified, unsaved files) with disk state.
10 //! TODO: Some LSP clients support watching the disk, so this crate should
11 //! to support custom watcher events (related to https://github.com/rust-analyzer/rust-analyzer/issues/131)
12 //!
13 //! VFS is based on a concept of roots: a set of directories on the file system
14 //! which are watched for changes. Typically, there will be a root for each
15 //! Cargo package.
16 mod io;
17 mod watcher;
18
19 use std::{
20     cmp::Reverse,
21     ffi::OsStr,
22     fmt, fs, mem,
23     path::{Component, Path, PathBuf},
24     sync::Arc,
25     thread,
26 };
27
28 use crossbeam_channel::Receiver;
29 use ra_arena::{impl_arena_id, Arena, RawId};
30 use relative_path::RelativePathBuf;
31 use rustc_hash::{FxHashMap, FxHashSet};
32 use walkdir::DirEntry;
33
34 pub use crate::io::TaskResult as VfsTask;
35 pub use crate::watcher::WatcherChange;
36
37 /// `RootFilter` is a predicate that checks if a file can belong to a root. If
38 /// several filters match a file (nested dirs), the most nested one wins.
39 pub(crate) struct RootFilter {
40     root: PathBuf,
41     filter: fn(RootEntry) -> bool,
42 }
43
44 pub(crate) struct RootEntry<'a, 'b> {
45     root: &'a Path,
46     path: &'b Path,
47 }
48
49 impl RootFilter {
50     fn new(root: PathBuf) -> RootFilter {
51         RootFilter {
52             root,
53             filter: default_filter,
54         }
55     }
56     /// Check if this root can contain `path`. NB: even if this returns
57     /// true, the `path` might actually be conained in some nested root.
58     pub(crate) fn can_contain(&self, path: &Path) -> Option<RelativePathBuf> {
59         if !(self.filter)(RootEntry {
60             root: &self.root,
61             path,
62         }) {
63             return None;
64         }
65         let path = path.strip_prefix(&self.root).ok()?;
66         RelativePathBuf::from_path(path).ok()
67     }
68 }
69
70 pub(crate) fn default_filter(entry: RootEntry) -> bool {
71     if entry.path.is_dir() {
72         // first component relative to root is "target"
73         entry
74             .path
75             .strip_prefix(entry.root)
76             .map(|p| p.components().next() != Some(Component::Normal(OsStr::new("target"))))
77             .unwrap_or(false)
78     } else {
79         entry.path.extension() == Some(OsStr::new("rs"))
80     }
81 }
82
83 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84 pub struct VfsRoot(pub RawId);
85 impl_arena_id!(VfsRoot);
86
87 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
88 pub struct VfsFile(pub RawId);
89 impl_arena_id!(VfsFile);
90
91 struct VfsFileData {
92     root: VfsRoot,
93     path: RelativePathBuf,
94     is_overlayed: bool,
95     text: Arc<String>,
96 }
97
98 pub struct Vfs {
99     roots: Arena<VfsRoot, Arc<RootFilter>>,
100     files: Arena<VfsFile, VfsFileData>,
101     root2files: FxHashMap<VfsRoot, FxHashSet<VfsFile>>,
102     pending_changes: Vec<VfsChange>,
103     worker: io::Worker,
104 }
105
106 impl fmt::Debug for Vfs {
107     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108         f.write_str("Vfs { ... }")
109     }
110 }
111
112 impl Vfs {
113     pub fn new(mut roots: Vec<PathBuf>) -> (Vfs, Vec<VfsRoot>) {
114         let worker = io::Worker::start();
115
116         let mut res = Vfs {
117             roots: Arena::default(),
118             files: Arena::default(),
119             root2files: FxHashMap::default(),
120             worker,
121             pending_changes: Vec::new(),
122         };
123
124         // A hack to make nesting work.
125         roots.sort_by_key(|it| Reverse(it.as_os_str().len()));
126         for (i, path) in roots.iter().enumerate() {
127             let root_filter = Arc::new(RootFilter::new(path.clone()));
128
129             let root = res.roots.alloc(root_filter.clone());
130             res.root2files.insert(root, Default::default());
131
132             let nested = roots[..i]
133                 .iter()
134                 .filter(|it| it.starts_with(path))
135                 .map(|it| it.clone())
136                 .collect::<Vec<_>>();
137
138             let filter = move |entry: &DirEntry| {
139                 if entry.file_type().is_dir() && nested.iter().any(|it| it == entry.path()) {
140                     false
141                 } else {
142                     root_filter.can_contain(entry.path()).is_some()
143                 }
144             };
145             let task = io::Task::AddRoot {
146                 root,
147                 path: path.clone(),
148                 filter: Box::new(filter),
149             };
150             res.worker.sender().send(task).unwrap();
151         }
152         let roots = res.roots.iter().map(|(id, _)| id).collect();
153         (res, roots)
154     }
155
156     pub fn root2path(&self, root: VfsRoot) -> PathBuf {
157         self.roots[root].root.clone()
158     }
159
160     pub fn path2file(&self, path: &Path) -> Option<VfsFile> {
161         if let Some((_root, _path, Some(file))) = self.find_root(path) {
162             return Some(file);
163         }
164         None
165     }
166
167     pub fn file2path(&self, file: VfsFile) -> PathBuf {
168         let rel_path = &self.files[file].path;
169         let root_path = &self.roots[self.files[file].root].root;
170         rel_path.to_path(root_path)
171     }
172
173     pub fn file_for_path(&self, path: &Path) -> Option<VfsFile> {
174         if let Some((_root, _path, Some(file))) = self.find_root(path) {
175             return Some(file);
176         }
177         None
178     }
179
180     pub fn load(&mut self, path: &Path) -> Option<VfsFile> {
181         if let Some((root, rel_path, file)) = self.find_root(path) {
182             return if let Some(file) = file {
183                 Some(file)
184             } else {
185                 let text = fs::read_to_string(path).unwrap_or_default();
186                 let text = Arc::new(text);
187                 let file = self.add_file(root, rel_path.clone(), Arc::clone(&text), false);
188                 let change = VfsChange::AddFile {
189                     file,
190                     text,
191                     root,
192                     path: rel_path,
193                 };
194                 self.pending_changes.push(change);
195                 Some(file)
196             };
197         }
198         None
199     }
200
201     pub fn task_receiver(&self) -> &Receiver<io::TaskResult> {
202         self.worker.receiver()
203     }
204
205     pub fn handle_task(&mut self, task: io::TaskResult) {
206         match task {
207             io::TaskResult::AddRoot(task) => {
208                 let mut files = Vec::new();
209                 // While we were scanning the root in the backgound, a file might have
210                 // been open in the editor, so we need to account for that.
211                 let exising = self.root2files[&task.root]
212                     .iter()
213                     .map(|&file| (self.files[file].path.clone(), file))
214                     .collect::<FxHashMap<_, _>>();
215                 for (path, text) in task.files {
216                     if let Some(&file) = exising.get(&path) {
217                         let text = Arc::clone(&self.files[file].text);
218                         files.push((file, path, text));
219                         continue;
220                     }
221                     let text = Arc::new(text);
222                     let file = self.add_file(task.root, path.clone(), Arc::clone(&text), false);
223                     files.push((file, path, text));
224                 }
225
226                 let change = VfsChange::AddRoot {
227                     root: task.root,
228                     files,
229                 };
230                 self.pending_changes.push(change);
231             }
232             io::TaskResult::HandleChange(change) => match &change {
233                 watcher::WatcherChange::Create(path) if path.is_dir() => {
234                     if let Some((root, _path, _file)) = self.find_root(&path) {
235                         let root_filter = self.roots[root].clone();
236                         let filter =
237                             move |entry: &DirEntry| root_filter.can_contain(entry.path()).is_some();
238                         self.worker
239                             .sender()
240                             .send(io::Task::Watch {
241                                 dir: path.to_path_buf(),
242                                 filter: Box::new(filter),
243                             })
244                             .unwrap()
245                     }
246                 }
247                 watcher::WatcherChange::Create(path)
248                 | watcher::WatcherChange::Remove(path)
249                 | watcher::WatcherChange::Write(path) => {
250                     if self.should_handle_change(&path) {
251                         self.worker
252                             .sender()
253                             .send(io::Task::LoadChange(change))
254                             .unwrap()
255                     }
256                 }
257                 watcher::WatcherChange::Rescan => {
258                     // TODO we should reload all files
259                 }
260             },
261             io::TaskResult::LoadChange(change) => match change {
262                 io::WatcherChangeData::Create { path, text }
263                 | io::WatcherChangeData::Write { path, text } => {
264                     if let Some((root, path, file)) = self.find_root(&path) {
265                         if let Some(file) = file {
266                             self.do_change_file(file, text, false);
267                         } else {
268                             self.do_add_file(root, path, text, false);
269                         }
270                     }
271                 }
272                 io::WatcherChangeData::Remove { path } => {
273                     if let Some((root, path, file)) = self.find_root(&path) {
274                         if let Some(file) = file {
275                             self.do_remove_file(root, path, file, false);
276                         }
277                     }
278                 }
279             },
280             io::TaskResult::NoOp => {}
281         }
282     }
283
284     fn should_handle_change(&self, path: &Path) -> bool {
285         if let Some((_root, _rel_path, file)) = self.find_root(&path) {
286             if let Some(file) = file {
287                 if self.files[file].is_overlayed {
288                     // file is overlayed
289                     log::debug!("skipping overlayed \"{}\"", path.display());
290                     return false;
291                 }
292             }
293             true
294         } else {
295             // file doesn't belong to any root
296             false
297         }
298     }
299
300     fn do_add_file(
301         &mut self,
302         root: VfsRoot,
303         path: RelativePathBuf,
304         text: String,
305         is_overlay: bool,
306     ) -> Option<VfsFile> {
307         let text = Arc::new(text);
308         let file = self.add_file(root, path.clone(), text.clone(), is_overlay);
309         self.pending_changes.push(VfsChange::AddFile {
310             file,
311             root,
312             path,
313             text,
314         });
315         Some(file)
316     }
317
318     fn do_change_file(&mut self, file: VfsFile, text: String, is_overlay: bool) {
319         if !is_overlay && self.files[file].is_overlayed {
320             return;
321         }
322         let text = Arc::new(text);
323         self.change_file(file, text.clone(), is_overlay);
324         self.pending_changes
325             .push(VfsChange::ChangeFile { file, text });
326     }
327
328     fn do_remove_file(
329         &mut self,
330         root: VfsRoot,
331         path: RelativePathBuf,
332         file: VfsFile,
333         is_overlay: bool,
334     ) {
335         if !is_overlay && self.files[file].is_overlayed {
336             return;
337         }
338         self.remove_file(file);
339         self.pending_changes
340             .push(VfsChange::RemoveFile { root, path, file });
341     }
342
343     pub fn add_file_overlay(&mut self, path: &Path, text: String) -> Option<VfsFile> {
344         if let Some((root, rel_path, file)) = self.find_root(path) {
345             if let Some(file) = file {
346                 self.do_change_file(file, text, true);
347                 Some(file)
348             } else {
349                 self.do_add_file(root, rel_path, text, true)
350             }
351         } else {
352             None
353         }
354     }
355
356     pub fn change_file_overlay(&mut self, path: &Path, new_text: String) {
357         if let Some((_root, _path, file)) = self.find_root(path) {
358             let file = file.expect("can't change a file which wasn't added");
359             self.do_change_file(file, new_text, true);
360         }
361     }
362
363     pub fn remove_file_overlay(&mut self, path: &Path) -> Option<VfsFile> {
364         if let Some((root, path, file)) = self.find_root(path) {
365             let file = file.expect("can't remove a file which wasn't added");
366             let full_path = path.to_path(&self.roots[root].root);
367             if let Ok(text) = fs::read_to_string(&full_path) {
368                 self.do_change_file(file, text, true);
369             } else {
370                 self.do_remove_file(root, path, file, true);
371             }
372             Some(file)
373         } else {
374             None
375         }
376     }
377
378     pub fn commit_changes(&mut self) -> Vec<VfsChange> {
379         mem::replace(&mut self.pending_changes, Vec::new())
380     }
381
382     /// Sutdown the VFS and terminate the background watching thread.
383     pub fn shutdown(self) -> thread::Result<()> {
384         self.worker.shutdown()
385     }
386
387     fn add_file(
388         &mut self,
389         root: VfsRoot,
390         path: RelativePathBuf,
391         text: Arc<String>,
392         is_overlayed: bool,
393     ) -> VfsFile {
394         let data = VfsFileData {
395             root,
396             path,
397             text,
398             is_overlayed,
399         };
400         let file = self.files.alloc(data);
401         self.root2files.get_mut(&root).unwrap().insert(file);
402         file
403     }
404
405     fn change_file(&mut self, file: VfsFile, new_text: Arc<String>, is_overlayed: bool) {
406         let mut file_data = &mut self.files[file];
407         file_data.text = new_text;
408         file_data.is_overlayed = is_overlayed;
409     }
410
411     fn remove_file(&mut self, file: VfsFile) {
412         //FIXME: use arena with removal
413         self.files[file].text = Default::default();
414         self.files[file].path = Default::default();
415         let root = self.files[file].root;
416         let removed = self.root2files.get_mut(&root).unwrap().remove(&file);
417         assert!(removed);
418     }
419
420     fn find_root(&self, path: &Path) -> Option<(VfsRoot, RelativePathBuf, Option<VfsFile>)> {
421         let (root, path) = self
422             .roots
423             .iter()
424             .find_map(|(root, data)| data.can_contain(path).map(|it| (root, it)))?;
425         let file = self.root2files[&root]
426             .iter()
427             .map(|&it| it)
428             .find(|&file| self.files[file].path == path);
429         Some((root, path, file))
430     }
431 }
432
433 #[derive(Debug, Clone)]
434 pub enum VfsChange {
435     AddRoot {
436         root: VfsRoot,
437         files: Vec<(VfsFile, RelativePathBuf, Arc<String>)>,
438     },
439     AddFile {
440         root: VfsRoot,
441         file: VfsFile,
442         path: RelativePathBuf,
443         text: Arc<String>,
444     },
445     RemoveFile {
446         root: VfsRoot,
447         file: VfsFile,
448         path: RelativePathBuf,
449     },
450     ChangeFile {
451         file: VfsFile,
452         text: Arc<String>,
453     },
454 }