mod notify;
pub(crate) mod set;
use std::path::{Path, PathBuf};
use alba_core::{Project, SourceMap};
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;
use crate::EngineError;
use crate::affected::Selection;
use crate::event::RunEvent;
use crate::scheduler::{Executors, RunOptions};
use crate::session::session;
pub use notify::NotifyWatcher;
pub enum WatchBatch {
Paths(Vec<PathBuf>),
Rescan,
}
#[async_trait::async_trait]
pub trait Watcher: Send {
async fn next_batch(&mut self) -> Option<WatchBatch>;
}
pub enum WatchExit {
Interrupted,
WatcherClosed,
}
pub enum SessionError {
Load(alba_core::LoadError),
Run {
error: EngineError,
sources: SourceMap,
},
}
#[allow(clippy::too_many_arguments)]
pub async fn watch(
beamfile: &Path,
project: Project,
sources: SourceMap,
selection: Selection,
options: RunOptions,
executors: Executors,
events: UnboundedSender<RunEvent>,
cancel: CancellationToken,
watcher: Box<dyn Watcher>,
render_error: &mut (dyn FnMut(&SessionError) -> String + Send),
) -> WatchExit {
session(
beamfile,
project,
sources,
selection,
options,
executors,
events,
None,
true,
cancel,
watcher,
render_error,
)
.await
}
pub fn beamfile_dir(beamfile: &Path) -> PathBuf {
let dir = beamfile
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf())
}
pub(crate) fn display_paths(root: &Path, paths: &[PathBuf]) -> Vec<String> {
let canonical_root = root.canonicalize();
let mut display: Vec<String> = paths
.iter()
.map(|path| {
relative_to(root, canonical_root.as_deref().ok(), path)
.to_string_lossy()
.replace('\\', "/")
})
.collect();
display.sort();
display.dedup();
display
}
fn relative_to(root: &Path, canonical_root: Option<&Path>, path: &Path) -> PathBuf {
if let Ok(relative) = path.strip_prefix(root) {
return relative.to_path_buf();
}
if let Some(base) = canonical_root
&& let Ok(relative) = path.strip_prefix(base)
{
return relative.to_path_buf();
}
if let Some(base) = canonical_root
&& let Ok(canonical) = path.canonicalize()
&& let Ok(relative) = canonical.strip_prefix(base)
{
return relative.to_path_buf();
}
path.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_beamfile_roots_the_session_at_the_current_directory() {
let reported = std::env::current_dir().unwrap().join("src/input.txt");
let root = beamfile_dir(Path::new("Beamfile"));
assert_eq!(display_paths(&root, &[reported]), vec!["src/input.txt"]);
}
#[test]
fn a_named_beamfile_roots_the_session_at_its_own_directory() {
let dir = tempfile::tempdir().unwrap();
let reported = dir.path().join("src/input.txt");
let root = beamfile_dir(&dir.path().join("Beamfile"));
assert_eq!(display_paths(&root, &[reported]), vec!["src/input.txt"]);
}
#[test]
fn an_empty_root_shortens_nothing() {
let path = Path::new("/project/src/input.txt");
assert_eq!(relative_to(Path::new(""), None, path), path);
}
#[test]
fn a_relative_root_shortens_a_path_spelled_from_it() {
assert_eq!(
relative_to(
Path::new("project"),
None,
Path::new("project/src/input.txt")
),
Path::new("src/input.txt")
);
}
#[test]
fn a_path_outside_the_root_is_left_as_it_came() {
let root = tempfile::tempdir().unwrap();
let elsewhere = tempfile::tempdir().unwrap();
let path = elsewhere.path().join("shared/input.txt");
assert_eq!(
relative_to(
root.path(),
root.path().canonicalize().ok().as_deref(),
&path
),
path
);
}
#[test]
fn a_path_that_no_longer_exists_is_still_shortened() {
let root = tempfile::tempdir().unwrap();
let deleted = root.path().join("src/deleted.txt");
assert!(!deleted.exists(), "precondition");
assert_eq!(
relative_to(
root.path(),
root.path().canonicalize().ok().as_deref(),
&deleted
),
Path::new("src/deleted.txt")
);
}
#[test]
fn a_root_spelled_through_a_symlink_still_shortens_what_the_watcher_reports() {
let root = tempfile::tempdir().unwrap();
let canonical = root.path().canonicalize().unwrap();
let reported = canonical.join("src/input.txt");
assert_eq!(
relative_to(root.path(), Some(&canonical), &reported),
Path::new("src/input.txt")
);
}
}