import crate::build_command::build_command
import notify::EventKind
import notify::RecursiveMode
import notify::Watcher
import notify::event::ModifyKind
import std::path::Path
import std::path::PathBuf
import std::sync::mpsc
export(crate) watch_command : &[String] -> Result<(), String>
watch_command args =
input = (.ok_or_else (\-> .to_string "error: no input file\n\n`voltaic watch` takes a .vlt file or a directory.") (.find (\a -> !.starts_with '-' a) (.iter args)))?
target = (Target::resolve (Path::new input))?
plain = .any (== "--plain") (.iter args)
report (&target) plain
channel = mpsc::channel ()
tx = channel.0
rx = channel.1
mut watcher = (.map_err (\e -> format! \-> "error: cannot start the file watcher: #{e}") (notify::recommended_watcher (move \event -> drop (.send event tx))))?
dir_shown = target.dir.display ().to_string ()
watched = watcher.watch (&target.dir) RecursiveMode::Recursive
(.map_err (\e -> format! \-> "error: cannot watch #{dir_shown}: #{e}") watched)?
eprintln! \-> "watching #{dir_shown} — press ctrl-c to stop"
for received in rx
case received of
Ok event ->
if is_change (&event.kind) && .any (\p -> is_relevant p) (.iter event.paths)
report (&target) plain
Err e -> eprintln! \-> "error: the file watcher reported: #{e}"
Ok ()
export(crate) is_change : &EventKind -> bool
is_change kind = case kind of
EventKind::Create _ or EventKind::Remove _ -> true
EventKind::Any or EventKind::Other -> true
EventKind::Modify inner -> case inner of
ModifyKind::Data _ or ModifyKind::Name _ -> true
_ -> false
EventKind::Access _ -> false
test "reading a file is not a change"
assert! \-> !is_change (&EventKind::Access (notify::event::AccessKind::Open notify::event::AccessMode::Any))
assert! \-> !is_change (&EventKind::Access notify::event::AccessKind::Read)
assert! \-> !is_change (&EventKind::Access (notify::event::AccessKind::Close notify::event::AccessMode::Read))
assert! \-> !is_change (&EventKind::Access notify::event::AccessKind::Any)
test "writing data is a change"
assert! \-> is_change (&EventKind::Modify (ModifyKind::Data notify::event::DataChange::Any))
assert! \-> is_change (&EventKind::Modify (ModifyKind::Data notify::event::DataChange::Content))
test "renaming is a change"
assert! \-> is_change (&EventKind::Modify (ModifyKind::Name notify::event::RenameMode::To))
assert! \-> is_change (&EventKind::Modify (ModifyKind::Name notify::event::RenameMode::Both))
test "creating and removing are changes"
assert! \-> is_change (&EventKind::Create notify::event::CreateKind::File)
assert! \-> is_change (&EventKind::Remove notify::event::RemoveKind::File)
test "metadata alone is not a change"
assert! \-> !is_change (&EventKind::Modify (ModifyKind::Metadata notify::event::MetadataKind::Any))
assert! \-> !is_change (&EventKind::Modify (ModifyKind::Metadata notify::event::MetadataKind::AccessTime))
test "an unknown kind is treated as a change"
assert! \-> is_change (&EventKind::Any)
assert! \-> is_change (&EventKind::Other)
export(crate) is_relevant : &Path -> bool
is_relevant path =
guard .map (\n -> .to_string (.to_string_lossy n)) (.file_name path) is Some name else
return false
if name == "4913" || .ends_with '~' name || .starts_with ".#" name || .starts_with '#' name && .ends_with '#' name
return false
extension = .map (\e -> .to_string (.to_string_lossy e)) (.extension path)
case .as_deref extension of
Some e -> (e == "vlt" || e == "sdx") && !.starts_with '.' name
_ -> false
test "a vlt file is relevant"
assert! \-> is_relevant (Path::new "src/main.vlt")
assert! \-> is_relevant (Path::new "main.vlt")
test "another extension is not"
for name in ["main.rs", "main.vrs", "Cargo.toml", "README.md", "main"]
assert! \-> !is_relevant (Path::new name) \-> "#{name}"
test "the generated output is not relevant"
assert! \-> !is_relevant (Path::new "main.vrs")
assert! \-> !is_relevant (Path::new "main.vrs.map")
test "editor temporaries are ignored"
for name in ["4913", "main.vlt~", ".main.vlt.swp", ".\#main.vlt", "\#main.vlt\#", ".main.vlt"]
assert! \-> !is_relevant (Path::new name) \-> "#{name} should be ignored"
test "a path with no file name is not relevant"
assert! \-> !is_relevant (Path::new "/")
assert! \-> !is_relevant (Path::new "..")
test "an unfamiliar name still compiles if it is a vlt"
assert! \-> is_relevant (Path::new "weird-editor-artifact.vlt")
export(crate) report : &Target -> bool -> ()
report target plain =
case &target.file of
Some file ->
shown = file.display ().to_string ()
run_build (&shown) plain
None ->
for source in sources (&target.dir)
run_build (&source) plain
()
export(crate) run_build : &str -> bool -> ()
run_build input plain =
mut args = vec! \-> .to_string input
if plain
args.push (.to_string "--plain")
case build_command (&args) of
Ok _ -> eprintln! \-> "built #{input}"
Err message -> eprintln! \-> "#{message}"
()
export(crate) sources : &Path -> Vec<String>
sources dir =
guard std::fs::read_dir dir is Ok entries else
return Vec::new ()
mut found : Vec<String> = ((((entries.filter_map Result::ok).map (\e -> .path e)).filter (\p -> .is_file p && .is_some_and (\x -> x == "vlt" || x == "sdx") (.extension p))).map (\p -> .to_string (.display p))).collect ()
.sort found
found
test "finds and sorts vlt files"
pid = std::process::id ()
dir = .join (format! \-> "voltaic-watch-src-#{pid}") (std::env::temp_dir ())
.expect "should create" $ std::fs::create_dir_all (&dir)
for name in ["b.vlt", "a.vlt", "ignored.rs"]
.expect "should write" $ std::fs::write (.join name dir) "main : ()\nmain =\n println! \\-> \"\#{1}\"\n"
found = sources (&dir)
assert_eq! \-> .len found \-> 2 \-> "#{found:?}"
assert! \-> .ends_with "a.vlt" found[0] && .ends_with "b.vlt" found[1] \-> "#{found:?}"
_dropped = .ok $ std::fs::remove_dir_all (&dir)
test "an unreadable directory yields nothing"
found = sources $ std::path::Path::new "/nonexistent/voltaic"
assert! \-> .is_empty found
export(crate) data Target { export(crate) dir PathBuf, export(crate) file Option<PathBuf> }
deriving (Debug, Clone, PartialEq, Eq)
instance Target
export(crate) resolve : &Path -> Result<Self, String>
resolve arg =
if .is_dir arg
return Ok (Target { dir = .to_path_buf arg, file = None })
if !.exists arg
shown = arg.display ().to_string ()
return Err (format! \-> "error: #{shown} does not exist\n\n`voltaic watch` takes a .vlt file or a directory of them.")
parent = .filter (\p -> !.is_empty (.as_os_str p)) (.parent arg)
dir = (parent.unwrap_or (Path::new ".")).to_path_buf ()
Ok (Target { dir, file = Some (.to_path_buf arg) })
#[cfg(test)]
newtype Dir = Dir PathBuf
#[cfg(test)]
static NEXT : std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new 0
#[cfg(test)]
instance Dir
new : &str -> Self
new tag =
n = .fetch_add 1 std::sync::atomic::Ordering::Relaxed NEXT
pid = std::process::id ()
dir = .join (format! \-> "voltaic-target-#{tag}-#{pid}-#{n}") (std::env::temp_dir ())
.expect "should create" $ std::fs::create_dir_all (&dir)
Dir dir
#[cfg(test)]
instance Drop Dir
drop : &mut self -> ()
drop self =
_dropped = .ok $ std::fs::remove_dir_all (&self.0)
()
test "a directory is watched whole"
dir = Dir::new "whole"
target = .expect "should resolve" $ Target::resolve (&dir.0)
assert_eq! \-> target.dir \-> dir.0
assert_eq! \-> target.file \-> None \-> "a directory compiles all of itself"
test "a file is watched through its parent"
dir = Dir::new "parent"
file = .join "main.vlt" dir.0
.expect "should write" $ std::fs::write (&file) "main : ()\nmain =\n println! \\-> \"\#{1}\"\n"
target = .expect "should resolve" $ Target::resolve (&file)
assert_eq! \-> target.dir \-> dir.0 \-> "the parent directory is what is watched"
assert_eq! \-> target.file \-> Some file
test "a missing path is an error that says what is accepted"
err = .expect_err "should fail" $ Target::resolve $ Path::new "/nonexistent/main.vlt"
assert! \-> .contains "does not exist" err \-> "#{err}"
assert! \-> .contains ".vlt file or a directory" err \-> "#{err}"
test "a bare filename resolves to the current directory"
dir = Dir::new "bare"
file = .join "main.vlt" dir.0
.expect "should write" $ std::fs::write (&file) "main : ()\nmain =\n println! \\-> \"\#{1}\"\n"
previous = .expect "a cwd" $ std::env::current_dir ()
.expect "should chdir" $ std::env::set_current_dir (&dir.0)
target = .expect "should resolve" $ Target::resolve $ Path::new "main.vlt"
.expect "should restore" $ std::env::set_current_dir previous
assert_eq! \-> target.dir \-> Path::new "." \-> "not the empty path"