2020-06-11 04:04:09 -05:00
|
|
|
//! An implementation of `loader::Handle`, based on `walkdir` and `notify`.
|
|
|
|
//!
|
|
|
|
//! The file watching bits here are untested and quite probably buggy. For this
|
|
|
|
//! reason, by default we don't watch files and rely on editor's file watching
|
|
|
|
//! capabilities.
|
|
|
|
//!
|
|
|
|
//! Hopefully, one day a reliable file watching/walking crate appears on
|
|
|
|
//! crates.io, and we can reduce this to trivial glue code.
|
|
|
|
mod include;
|
|
|
|
|
|
|
|
use std::convert::{TryFrom, TryInto};
|
|
|
|
|
|
|
|
use crossbeam_channel::{select, unbounded, Receiver};
|
|
|
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
|
|
|
use paths::{AbsPath, AbsPathBuf};
|
|
|
|
use rustc_hash::FxHashSet;
|
|
|
|
use vfs::loader;
|
|
|
|
use walkdir::WalkDir;
|
|
|
|
|
|
|
|
use crate::include::Include;
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2020-06-25 01:59:55 -05:00
|
|
|
pub struct NotifyHandle {
|
2020-06-11 04:04:09 -05:00
|
|
|
// Relative order of fields below is significant.
|
|
|
|
sender: crossbeam_channel::Sender<Message>,
|
|
|
|
_thread: jod_thread::JoinHandle,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum Message {
|
|
|
|
Config(loader::Config),
|
|
|
|
Invalidate(AbsPathBuf),
|
|
|
|
}
|
|
|
|
|
2020-06-25 01:59:55 -05:00
|
|
|
impl loader::Handle for NotifyHandle {
|
|
|
|
fn spawn(sender: loader::Sender) -> NotifyHandle {
|
|
|
|
let actor = NotifyActor::new(sender);
|
2020-06-11 04:04:09 -05:00
|
|
|
let (sender, receiver) = unbounded::<Message>();
|
|
|
|
let thread = jod_thread::spawn(move || actor.run(receiver));
|
2020-06-25 01:59:55 -05:00
|
|
|
NotifyHandle { sender, _thread: thread }
|
2020-06-11 04:04:09 -05:00
|
|
|
}
|
|
|
|
fn set_config(&mut self, config: loader::Config) {
|
|
|
|
self.sender.send(Message::Config(config)).unwrap()
|
|
|
|
}
|
|
|
|
fn invalidate(&mut self, path: AbsPathBuf) {
|
|
|
|
self.sender.send(Message::Invalidate(path)).unwrap();
|
|
|
|
}
|
2020-06-24 08:52:07 -05:00
|
|
|
fn load_sync(&mut self, path: &AbsPath) -> Option<Vec<u8>> {
|
2020-06-11 04:04:09 -05:00
|
|
|
read(path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type NotifyEvent = notify::Result<notify::Event>;
|
|
|
|
|
2020-06-25 01:59:55 -05:00
|
|
|
struct NotifyActor {
|
|
|
|
sender: loader::Sender,
|
2020-06-11 04:04:09 -05:00
|
|
|
config: Vec<(AbsPathBuf, Include, bool)>,
|
|
|
|
watched_paths: FxHashSet<AbsPathBuf>,
|
|
|
|
// Drop order of fields bellow is significant,
|
|
|
|
watcher: Option<RecommendedWatcher>,
|
|
|
|
watcher_receiver: Receiver<NotifyEvent>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum Event {
|
|
|
|
Message(Message),
|
|
|
|
NotifyEvent(NotifyEvent),
|
|
|
|
}
|
|
|
|
|
2020-06-25 01:59:55 -05:00
|
|
|
impl NotifyActor {
|
|
|
|
fn new(sender: loader::Sender) -> NotifyActor {
|
2020-06-11 04:04:09 -05:00
|
|
|
let (watcher_sender, watcher_receiver) = unbounded();
|
|
|
|
let watcher = log_notify_error(Watcher::new_immediate(move |event| {
|
|
|
|
watcher_sender.send(event).unwrap()
|
|
|
|
}));
|
|
|
|
|
2020-06-25 01:59:55 -05:00
|
|
|
NotifyActor {
|
2020-06-11 04:04:09 -05:00
|
|
|
sender,
|
|
|
|
config: Vec::new(),
|
2020-06-25 01:59:55 -05:00
|
|
|
watched_paths: FxHashSet::default(),
|
|
|
|
watcher,
|
|
|
|
watcher_receiver,
|
2020-06-11 04:04:09 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-25 06:47:22 -05:00
|
|
|
fn run(mut self, inbox: Receiver<Message>) {
|
|
|
|
while let Some(event) = self.next_event(&inbox) {
|
2020-06-11 04:04:09 -05:00
|
|
|
log::debug!("vfs-notify event: {:?}", event);
|
|
|
|
match event {
|
|
|
|
Event::Message(msg) => match msg {
|
|
|
|
Message::Config(config) => {
|
2020-06-24 09:58:49 -05:00
|
|
|
let n_total = config.load.len();
|
|
|
|
self.send(loader::Message::Progress { n_total, n_done: 0 });
|
2020-06-11 04:04:09 -05:00
|
|
|
|
|
|
|
self.unwatch_all();
|
|
|
|
self.config.clear();
|
|
|
|
|
|
|
|
for (i, entry) in config.load.into_iter().enumerate() {
|
|
|
|
let watch = config.watch.contains(&i);
|
|
|
|
let files = self.load_entry(entry, watch);
|
|
|
|
self.send(loader::Message::Loaded { files });
|
2020-06-24 09:58:49 -05:00
|
|
|
self.send(loader::Message::Progress { n_total, n_done: i + 1 });
|
2020-06-11 04:04:09 -05:00
|
|
|
}
|
|
|
|
self.config.sort_by(|x, y| x.0.cmp(&y.0));
|
|
|
|
}
|
|
|
|
Message::Invalidate(path) => {
|
|
|
|
let contents = read(path.as_path());
|
|
|
|
let files = vec![(path, contents)];
|
|
|
|
self.send(loader::Message::Loaded { files });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Event::NotifyEvent(event) => {
|
|
|
|
if let Some(event) = log_notify_error(event) {
|
|
|
|
let files = event
|
|
|
|
.paths
|
|
|
|
.into_iter()
|
|
|
|
.map(|path| AbsPathBuf::try_from(path).unwrap())
|
|
|
|
.filter_map(|path| {
|
|
|
|
let is_dir = path.is_dir();
|
|
|
|
let is_file = path.is_file();
|
|
|
|
|
|
|
|
let config_idx =
|
|
|
|
match self.config.binary_search_by(|it| it.0.cmp(&path)) {
|
|
|
|
Ok(it) => it,
|
|
|
|
Err(it) => it.saturating_sub(1),
|
|
|
|
};
|
|
|
|
let include = self.config.get(config_idx).and_then(|it| {
|
|
|
|
let rel_path = path.strip_prefix(&it.0)?;
|
|
|
|
Some((rel_path, &it.1))
|
|
|
|
});
|
|
|
|
|
|
|
|
if let Some((rel_path, include)) = include {
|
|
|
|
if is_dir && include.exclude_dir(&rel_path)
|
|
|
|
|| is_file && !include.include_file(&rel_path)
|
|
|
|
{
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if is_dir {
|
|
|
|
self.watch(path);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
if !is_file {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let contents = read(&path);
|
|
|
|
Some((path, contents))
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
self.send(loader::Message::Loaded { files })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn next_event(&self, receiver: &Receiver<Message>) -> Option<Event> {
|
|
|
|
select! {
|
|
|
|
recv(receiver) -> it => it.ok().map(Event::Message),
|
|
|
|
recv(&self.watcher_receiver) -> it => Some(Event::NotifyEvent(it.unwrap())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn load_entry(
|
|
|
|
&mut self,
|
|
|
|
entry: loader::Entry,
|
|
|
|
watch: bool,
|
|
|
|
) -> Vec<(AbsPathBuf, Option<Vec<u8>>)> {
|
|
|
|
match entry {
|
|
|
|
loader::Entry::Files(files) => files
|
|
|
|
.into_iter()
|
|
|
|
.map(|file| {
|
|
|
|
if watch {
|
|
|
|
self.watch(file.clone())
|
|
|
|
}
|
|
|
|
let contents = read(file.as_path());
|
|
|
|
(file, contents)
|
|
|
|
})
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
loader::Entry::Directory { path, include } => {
|
|
|
|
let include = Include::new(include);
|
|
|
|
self.config.push((path.clone(), include.clone(), watch));
|
|
|
|
|
|
|
|
let files = WalkDir::new(&path)
|
|
|
|
.into_iter()
|
|
|
|
.filter_entry(|entry| {
|
|
|
|
let abs_path: &AbsPath = entry.path().try_into().unwrap();
|
|
|
|
match abs_path.strip_prefix(&path) {
|
|
|
|
Some(rel_path) => {
|
|
|
|
!(entry.file_type().is_dir() && include.exclude_dir(rel_path))
|
|
|
|
}
|
|
|
|
None => false,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter_map(|entry| entry.ok())
|
|
|
|
.filter_map(|entry| {
|
|
|
|
let is_dir = entry.file_type().is_dir();
|
|
|
|
let is_file = entry.file_type().is_file();
|
|
|
|
let abs_path = AbsPathBuf::try_from(entry.into_path()).unwrap();
|
2020-06-24 10:11:07 -05:00
|
|
|
if is_dir && watch {
|
2020-06-11 04:04:09 -05:00
|
|
|
self.watch(abs_path.clone());
|
|
|
|
}
|
|
|
|
let rel_path = abs_path.strip_prefix(&path)?;
|
|
|
|
if is_file && include.include_file(&rel_path) {
|
|
|
|
Some(abs_path)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
files
|
|
|
|
.map(|file| {
|
|
|
|
let contents = read(file.as_path());
|
|
|
|
(file, contents)
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn watch(&mut self, path: AbsPathBuf) {
|
|
|
|
if let Some(watcher) = &mut self.watcher {
|
|
|
|
log_notify_error(watcher.watch(&path, RecursiveMode::NonRecursive));
|
|
|
|
self.watched_paths.insert(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn unwatch_all(&mut self) {
|
|
|
|
if let Some(watcher) = &mut self.watcher {
|
|
|
|
for path in self.watched_paths.drain() {
|
|
|
|
log_notify_error(watcher.unwatch(path));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn send(&mut self, msg: loader::Message) {
|
|
|
|
(self.sender)(msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read(path: &AbsPath) -> Option<Vec<u8>> {
|
|
|
|
std::fs::read(path).ok()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn log_notify_error<T>(res: notify::Result<T>) -> Option<T> {
|
|
|
|
res.map_err(|err| log::warn!("notify error: {}", err)).ok()
|
|
|
|
}
|