From 689a41ded42178e485bf6cf37044ea72d4114120 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Thu, 9 Nov 2023 14:53:41 -0500 Subject: [PATCH] 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? --- crates/aiken/Cargo.toml | 1 + crates/aiken/src/cmd/check.rs | 5 +++ crates/aiken/src/cmd/mod.rs | 2 + crates/aiken/src/cmd/watch.rs | 71 ++++++++++++++++++++++++++++++ crates/aiken/src/lib.rs | 82 +++++++++++++++++++++++++++++++++++ crates/aiken/src/main.rs | 3 +- 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 crates/aiken/src/cmd/watch.rs diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index 6b87830a..f6b24621 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -25,6 +25,7 @@ hex = "0.4.3" ignore = "0.4.20" indoc = "2.0" miette = { version = "5.5.0", features = ["fancy"] } +notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas-addresses = "0.18.0" pallas-codec = "0.18.0" diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 5b9aa278..d34f31ed 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -18,6 +18,10 @@ pub struct Args { #[clap(long)] 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. /// 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}"` @@ -43,6 +47,7 @@ pub fn exec( match_tests, exact_match, no_traces, + watch, }: Args, ) -> miette::Result<()> { crate::with_project(directory, deny, |p| { diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 83f59ff4..58356166 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -4,6 +4,7 @@ use clap::Parser; pub mod blueprint; pub mod build; pub mod check; +pub mod watch; pub mod completion; pub mod docs; pub mod fmt; @@ -23,6 +24,7 @@ pub enum Cmd { Build(build::Args), Address(blueprint::address::Args), Check(check::Args), + Watch(watch::Args), Docs(docs::Args), Add(packages::add::Args), diff --git a/crates/aiken/src/cmd/watch.rs b/crates/aiken/src/cmd/watch.rs new file mode 100644 index 00000000..f00f9e82 --- /dev/null +++ b/crates/aiken/src/cmd/watch.rs @@ -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| { + 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(), + ) + }); + } + } +} diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs index 474a48d3..7aad48ad 100644 --- a/crates/aiken/src/lib.rs +++ b/crates/aiken/src/lib.rs @@ -94,6 +94,88 @@ where 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(directory: Option, deny: bool, mut action: A) -> miette::Result<()> +where + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + 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)] pub struct Terminal; diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 777766c8..62e9828e 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,6 +1,6 @@ use aiken::cmd::{ blueprint::{self, address}, - build, check, completion, docs, fmt, lsp, new, + build, check, watch, completion, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, Cmd, }; @@ -17,6 +17,7 @@ fn main() -> miette::Result<()> { Cmd::Build(args) => build::exec(args), Cmd::Address(args) => address::exec(args), Cmd::Check(args) => check::exec(args), + Cmd::Watch(args) => watch::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args), Cmd::Blueprint(args) => blueprint::exec(args),