aboutsummaryrefslogtreecommitdiff
path: root/crates/vfs-notify
diff options
context:
space:
mode:
Diffstat (limited to 'crates/vfs-notify')
-rw-r--r--crates/vfs-notify/Cargo.toml17
-rw-r--r--crates/vfs-notify/src/include.rs43
-rw-r--r--crates/vfs-notify/src/lib.rs243
3 files changed, 303 insertions, 0 deletions
diff --git a/crates/vfs-notify/Cargo.toml b/crates/vfs-notify/Cargo.toml
new file mode 100644
index 000000000..4737a52a7
--- /dev/null
+++ b/crates/vfs-notify/Cargo.toml
@@ -0,0 +1,17 @@
1[package]
2name = "vfs-notify"
3version = "0.1.0"
4authors = ["rust-analyzer developers"]
5edition = "2018"
6
7[dependencies]
8log = "0.4.8"
9rustc-hash = "1.0"
10jod-thread = "0.1.0"
11walkdir = "2.3.1"
12globset = "0.4.5"
13crossbeam-channel = "0.4.0"
14notify = "5.0.0-pre.3"
15
16vfs = { path = "../vfs" }
17paths = { path = "../paths" }
diff --git a/crates/vfs-notify/src/include.rs b/crates/vfs-notify/src/include.rs
new file mode 100644
index 000000000..7378766f5
--- /dev/null
+++ b/crates/vfs-notify/src/include.rs
@@ -0,0 +1,43 @@
1//! See `Include`.
2
3use std::convert::TryFrom;
4
5use globset::{Glob, GlobSet, GlobSetBuilder};
6use paths::{RelPath, RelPathBuf};
7
8/// `Include` is the opposite of .gitignore.
9///
10/// It describes the set of files inside some directory.
11///
12/// The current implementation is very limited, it allows white-listing file
13/// globs and black-listing directories.
14#[derive(Debug, Clone)]
15pub(crate) struct Include {
16 include_files: GlobSet,
17 exclude_dirs: Vec<RelPathBuf>,
18}
19
20impl Include {
21 pub(crate) fn new(include: Vec<String>) -> Include {
22 let mut include_files = GlobSetBuilder::new();
23 let mut exclude_dirs = Vec::new();
24
25 for glob in include {
26 if glob.starts_with("!/") {
27 if let Ok(path) = RelPathBuf::try_from(&glob["!/".len()..]) {
28 exclude_dirs.push(path)
29 }
30 } else {
31 include_files.add(Glob::new(&glob).unwrap());
32 }
33 }
34 let include_files = include_files.build().unwrap();
35 Include { include_files, exclude_dirs }
36 }
37 pub(crate) fn include_file(&self, path: &RelPath) -> bool {
38 self.include_files.is_match(path)
39 }
40 pub(crate) fn exclude_dir(&self, path: &RelPath) -> bool {
41 self.exclude_dirs.iter().any(|excluded| path.starts_with(excluded))
42 }
43}
diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs
new file mode 100644
index 000000000..25ba8d798
--- /dev/null
+++ b/crates/vfs-notify/src/lib.rs
@@ -0,0 +1,243 @@
1//! An implementation of `loader::Handle`, based on `walkdir` and `notify`.
2//!
3//! The file watching bits here are untested and quite probably buggy. For this
4//! reason, by default we don't watch files and rely on editor's file watching
5//! capabilities.
6//!
7//! Hopefully, one day a reliable file watching/walking crate appears on
8//! crates.io, and we can reduce this to trivial glue code.
9mod include;
10
11use std::convert::{TryFrom, TryInto};
12
13use crossbeam_channel::{select, unbounded, Receiver};
14use notify::{RecommendedWatcher, RecursiveMode, Watcher};
15use paths::{AbsPath, AbsPathBuf};
16use rustc_hash::FxHashSet;
17use vfs::loader;
18use walkdir::WalkDir;
19
20use crate::include::Include;
21
22#[derive(Debug)]
23pub struct NotifyHandle {
24 // Relative order of fields below is significant.
25 sender: crossbeam_channel::Sender<Message>,
26 _thread: jod_thread::JoinHandle,
27}
28
29#[derive(Debug)]
30enum Message {
31 Config(loader::Config),
32 Invalidate(AbsPathBuf),
33}
34
35impl loader::Handle for NotifyHandle {
36 fn spawn(sender: loader::Sender) -> NotifyHandle {
37 let actor = NotifyActor::new(sender);
38 let (sender, receiver) = unbounded::<Message>();
39 let thread = jod_thread::spawn(move || actor.run(receiver));
40 NotifyHandle { sender, _thread: thread }
41 }
42 fn set_config(&mut self, config: loader::Config) {
43 self.sender.send(Message::Config(config)).unwrap()
44 }
45 fn invalidate(&mut self, path: AbsPathBuf) {
46 self.sender.send(Message::Invalidate(path)).unwrap();
47 }
48 fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>> {
49 read(path)
50 }
51}
52
53type NotifyEvent = notify::Result<notify::Event>;
54
55struct NotifyActor {
56 sender: loader::Sender,
57 config: Vec<(AbsPathBuf, Include, bool)>,
58 watched_paths: FxHashSet<AbsPathBuf>,
59 // Drop order of fields bellow is significant,
60 watcher: Option<RecommendedWatcher>,
61 watcher_receiver: Receiver<NotifyEvent>,
62}
63
64#[derive(Debug)]
65enum Event {
66 Message(Message),
67 NotifyEvent(NotifyEvent),
68}
69
70impl NotifyActor {
71 fn new(sender: loader::Sender) -> NotifyActor {
72 let (watcher_sender, watcher_receiver) = unbounded();
73 let watcher = log_notify_error(Watcher::new_immediate(move |event| {
74 watcher_sender.send(event).unwrap()
75 }));
76
77 NotifyActor {
78 sender,
79 config: Vec::new(),
80 watched_paths: FxHashSet::default(),
81 watcher,
82 watcher_receiver,
83 }
84 }
85 fn next_event(&self, receiver: &Receiver<Message>) -> Option<Event> {
86 select! {
87 recv(receiver) -> it => it.ok().map(Event::Message),
88 recv(&self.watcher_receiver) -> it => Some(Event::NotifyEvent(it.unwrap())),
89 }
90 }
91 fn run(mut self, inbox: Receiver<Message>) {
92 while let Some(event) = self.next_event(&inbox) {
93 log::debug!("vfs-notify event: {:?}", event);
94 match event {
95 Event::Message(msg) => match msg {
96 Message::Config(config) => {
97 let n_total = config.load.len();
98 self.send(loader::Message::Progress { n_total, n_done: 0 });
99
100 self.unwatch_all();
101 self.config.clear();
102
103 for (i, entry) in config.load.into_iter().enumerate() {
104 let watch = config.watch.contains(&i);
105 let files = self.load_entry(entry, watch);
106 self.send(loader::Message::Loaded { files });
107 self.send(loader::Message::Progress { n_total, n_done: i + 1 });
108 }
109 self.config.sort_by(|x, y| x.0.cmp(&y.0));
110 }
111 Message::Invalidate(path) => {
112 let contents = read(path.as_path());
113 let files = vec![(path, contents)];
114 self.send(loader::Message::Loaded { files });
115 }
116 },
117 Event::NotifyEvent(event) => {
118 if let Some(event) = log_notify_error(event) {
119 let files = event
120 .paths
121 .into_iter()
122 .map(|path| AbsPathBuf::try_from(path).unwrap())
123 .filter_map(|path| {
124 let is_dir = path.is_dir();
125 let is_file = path.is_file();
126
127 let config_idx =
128 match self.config.binary_search_by(|it| it.0.cmp(&path)) {
129 Ok(it) => it,
130 Err(it) => it.saturating_sub(1),
131 };
132 let include = self.config.get(config_idx).and_then(|it| {
133 let rel_path = path.strip_prefix(&it.0)?;
134 Some((rel_path, &it.1))
135 });
136
137 if let Some((rel_path, include)) = include {
138 if is_dir && include.exclude_dir(&rel_path)
139 || is_file && !include.include_file(&rel_path)
140 {
141 return None;
142 }
143 }
144
145 if is_dir {
146 self.watch(path);
147 return None;
148 }
149 if !is_file {
150 return None;
151 }
152 let contents = read(&path);
153 Some((path, contents))
154 })
155 .collect();
156 self.send(loader::Message::Loaded { files })
157 }
158 }
159 }
160 }
161 }
162 fn load_entry(
163 &mut self,
164 entry: loader::Entry,
165 watch: bool,
166 ) -> Vec<(AbsPathBuf, Option<Vec<u8>>)> {
167 match entry {
168 loader::Entry::Files(files) => files
169 .into_iter()
170 .map(|file| {
171 if watch {
172 self.watch(file.clone())
173 }
174 let contents = read(file.as_path());
175 (file, contents)
176 })
177 .collect::<Vec<_>>(),
178 loader::Entry::Directory { path, include } => {
179 let include = Include::new(include);
180 self.config.push((path.clone(), include.clone(), watch));
181
182 let files = WalkDir::new(&path)
183 .into_iter()
184 .filter_entry(|entry| {
185 let abs_path: &AbsPath = entry.path().try_into().unwrap();
186 match abs_path.strip_prefix(&path) {
187 Some(rel_path) => {
188 !(entry.file_type().is_dir() && include.exclude_dir(rel_path))
189 }
190 None => false,
191 }
192 })
193 .filter_map(|entry| entry.ok())
194 .filter_map(|entry| {
195 let is_dir = entry.file_type().is_dir();
196 let is_file = entry.file_type().is_file();
197 let abs_path = AbsPathBuf::try_from(entry.into_path()).unwrap();
198 if is_dir && watch {
199 self.watch(abs_path.clone());
200 }
201 let rel_path = abs_path.strip_prefix(&path)?;
202 if is_file && include.include_file(&rel_path) {
203 Some(abs_path)
204 } else {
205 None
206 }
207 });
208
209 files
210 .map(|file| {
211 let contents = read(file.as_path());
212 (file, contents)
213 })
214 .collect()
215 }
216 }
217 }
218
219 fn watch(&mut self, path: AbsPathBuf) {
220 if let Some(watcher) = &mut self.watcher {
221 log_notify_error(watcher.watch(&path, RecursiveMode::NonRecursive));
222 self.watched_paths.insert(path);
223 }
224 }
225 fn unwatch_all(&mut self) {
226 if let Some(watcher) = &mut self.watcher {
227 for path in self.watched_paths.drain() {
228 log_notify_error(watcher.unwatch(path));
229 }
230 }
231 }
232 fn send(&mut self, msg: loader::Message) {
233 (self.sender)(msg)
234 }
235}
236
237fn read(path: &AbsPath) -> Option<Vec<u8>> {
238 std::fs::read(path).ok()
239}
240
241fn log_notify_error<T>(res: notify::Result<T>) -> Option<T> {
242 res.map_err(|err| log::warn!("notify error: {}", err)).ok()
243}