Implement a basic watch command

This adds the following command
```
aiken watch
```

There are some open questions to answer, though:
- I really like the ergonomics of `aiken watch`; but it also makes sense
  as a flag to `aiken check` or `aiken build` etc.; should we just
  support the flag, the command, or both?
- Right now I duplicated the with_project method, because it forces
  process::exit(1); Should we refactor this, and if so, how?
- Are there other configuration options we want?
This commit is contained in:
Pi Lanningham 2023-11-09 14:53:41 -05:00 committed by KtorZ
parent 45177cd08b
commit 689a41ded4
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
6 changed files with 163 additions and 1 deletions

View File

@ -25,6 +25,7 @@ hex = "0.4.3"
ignore = "0.4.20" ignore = "0.4.20"
indoc = "2.0" indoc = "2.0"
miette = { version = "5.5.0", features = ["fancy"] } miette = { version = "5.5.0", features = ["fancy"] }
notify = "6.1.1"
owo-colors = { version = "3.5.0", features = ["supports-colors"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] }
pallas-addresses = "0.18.0" pallas-addresses = "0.18.0"
pallas-codec = "0.18.0" pallas-codec = "0.18.0"

View File

@ -18,6 +18,10 @@ pub struct Args {
#[clap(long)] #[clap(long)]
debug: bool, debug: bool,
// When enabled, re-run the command on file changes instead of exiting
#[clap(long)]
watch: bool,
/// Only run tests if they match any of these strings. /// Only run tests if they match any of these strings.
/// You can match a module with `-m aiken/list` or `-m list`. /// You can match a module with `-m aiken/list` or `-m list`.
/// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"`
@ -43,6 +47,7 @@ pub fn exec(
match_tests, match_tests,
exact_match, exact_match,
no_traces, no_traces,
watch,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
crate::with_project(directory, deny, |p| { crate::with_project(directory, deny, |p| {

View File

@ -4,6 +4,7 @@ use clap::Parser;
pub mod blueprint; pub mod blueprint;
pub mod build; pub mod build;
pub mod check; pub mod check;
pub mod watch;
pub mod completion; pub mod completion;
pub mod docs; pub mod docs;
pub mod fmt; pub mod fmt;
@ -23,6 +24,7 @@ pub enum Cmd {
Build(build::Args), Build(build::Args),
Address(blueprint::address::Args), Address(blueprint::address::Args),
Check(check::Args), Check(check::Args),
Watch(watch::Args),
Docs(docs::Args), Docs(docs::Args),
Add(packages::add::Args), Add(packages::add::Args),

View File

@ -0,0 +1,71 @@
use notify::{RecursiveMode, Watcher, Event, event::EventAttributes};
use std::{path::Path, error::Error, time::SystemTime, collections::VecDeque, sync::{Mutex, Arc}};
#[derive(clap::Args)]
/// Type-check an Aiken project
pub struct Args {
/// Clear the screen between each run
#[clap(long)]
clear: bool,
}
pub fn exec(Args { clear }: Args) -> miette::Result<()> {
let project = Path::new("../sundae-contracts/aiken").to_path_buf().canonicalize().expect("");
let build = project.join("build").canonicalize().expect("");
let lock = project.join("aiken.lock");
let queue = Arc::new(Mutex::new(VecDeque::new()));
queue.lock().unwrap().push_back(Event { kind: notify::EventKind::Any, paths: vec![], attrs: EventAttributes::new() });
let queue_write = queue.clone();
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
match res {
Ok(event) => {
match event.kind {
notify::EventKind::Create(_) |
notify::EventKind::Modify(_) |
notify::EventKind::Remove(_) => {
let mut queue = queue_write.lock().expect("lock queue");
queue.push_back(event.clone());
drop(queue);
}
_ => {}
}
},
Err(e) => {
println!("watch error: {:?}", e)
},
};
}).expect("watcher");
let _ = watcher.watch(Path::new("../sundae-contracts/aiken"), RecursiveMode::Recursive);
let queue_read = queue.clone();
let mut last_evt = SystemTime::UNIX_EPOCH;
loop {
std::thread::sleep(std::time::Duration::from_millis(300));
let mut queue = queue_read.lock().expect("lock queue");
let mut latest = None;
while let Some(evt) = queue.pop_back() {
if evt.paths.iter().any(|p| {
let p = p.canonicalize().expect("");
p.starts_with(&build) || p.starts_with(&lock)
}) {
continue;
}
latest = Some(evt);
}
drop(queue);
if let Some(evt) = latest {
if clear {
println!("{esc}c", esc = 27 as char);
}
let _ = crate::with_project_ok(Some(project.clone()), false, |p| {
p.check(
false,
None,
true,
false,
false.into(),
)
});
}
}
}

View File

@ -94,6 +94,88 @@ where
Ok(()) Ok(())
} }
// TODO: we probably want to rework with_project slightly to avoid duplication here,
// but this is a quick hack to get the aiken watch working
pub fn with_project_ok<A>(directory: Option<PathBuf>, deny: bool, mut action: A) -> miette::Result<()>
where
A: FnMut(&mut Project<Terminal>) -> Result<(), Vec<aiken_project::error::Error>>,
{
let project_path = if let Some(d) = directory {
d
} else {
env::current_dir().into_diagnostic()?
};
let mut project = match Project::new(project_path, Terminal) {
Ok(p) => p,
Err(e) => {
e.report();
return Ok(());
}
};
let build_result = action(&mut project);
let warnings = project.warnings();
let warning_count = warnings.len();
for warning in &warnings {
warning.report()
}
let plural = if warning_count == 1 { "" } else { "s" };
if let Err(errs) = build_result {
for err in &errs {
err.report()
}
eprintln!(
"\n{}",
"Summary"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold())
);
let warning_text = format!("{warning_count} warning{plural}");
let plural = if errs.len() == 1 { "" } else { "s" };
let error_text = format!("{} error{}", errs.len(), plural);
let full_summary = format!(
" {}, {}",
error_text.if_supports_color(Stderr, |s| s.red()),
warning_text.if_supports_color(Stderr, |s| s.yellow())
);
eprintln!("{full_summary}");
return Ok(());
} else {
eprintln!(
"\n{}",
"Summary"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold())
);
let warning_text = format!("{warning_count} warning{plural}");
eprintln!(
" 0 errors, {}",
warning_text.if_supports_color(Stderr, |s| s.yellow()),
);
}
if warning_count > 0 && deny {
return Ok(());
}
Ok(())
}
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub struct Terminal; pub struct Terminal;

View File

@ -1,6 +1,6 @@
use aiken::cmd::{ use aiken::cmd::{
blueprint::{self, address}, blueprint::{self, address},
build, check, completion, docs, fmt, lsp, new, build, check, watch, completion, docs, fmt, lsp, new,
packages::{self, add}, packages::{self, add},
tx, uplc, Cmd, tx, uplc, Cmd,
}; };
@ -17,6 +17,7 @@ fn main() -> miette::Result<()> {
Cmd::Build(args) => build::exec(args), Cmd::Build(args) => build::exec(args),
Cmd::Address(args) => address::exec(args), Cmd::Address(args) => address::exec(args),
Cmd::Check(args) => check::exec(args), Cmd::Check(args) => check::exec(args),
Cmd::Watch(args) => watch::exec(args),
Cmd::Docs(args) => docs::exec(args), Cmd::Docs(args) => docs::exec(args),
Cmd::Add(args) => add::exec(args), Cmd::Add(args) => add::exec(args),
Cmd::Blueprint(args) => blueprint::exec(args), Cmd::Blueprint(args) => blueprint::exec(args),