From 8ab94059685260d948040019d6e468be93622313 Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Fri, 7 Jul 2023 12:14:11 -0700 Subject: [PATCH] Implement `--debounce` and `--poll` --- Cargo.lock | 7 ++ Cargo.toml | 2 +- src/{clap_camino.rs => clap/camino.rs} | 4 +- src/clap/humantime.rs | 91 ++++++++++++++++++++++++++ src/clap/mod.rs | 6 ++ src/cli.rs | 38 +++++++---- src/lib.rs | 2 +- src/main.rs | 9 ++- src/watcher.rs | 13 +++- 9 files changed, 151 insertions(+), 21 deletions(-) rename src/{clap_camino.rs => clap/camino.rs} (94%) create mode 100644 src/clap/humantime.rs create mode 100644 src/clap/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4457e4f5..4eeb5fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,6 +564,7 @@ dependencies = [ "camino", "clap", "expect-test", + "humantime", "indoc", "itertools", "miette", @@ -865,6 +866,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "ignore" version = "0.4.20" diff --git a/Cargo.toml b/Cargo.toml index aa9e0634..73332927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ categories = ["command-line-utilities", "development-tools"] aho-corasick = "1.0.2" camino = "1.1.4" clap = { version = "4.3.2", features = ["derive", "wrap_help", "env"] } -# humantime = "2.1.0" +humantime = "2.1.0" itertools = "0.11.0" miette = { version = "5.9.0", features = ["fancy"] } nix = { version = "0.26.2", default_features = false, features = ["process"] } diff --git a/src/clap_camino.rs b/src/clap/camino.rs similarity index 94% rename from src/clap_camino.rs rename to src/clap/camino.rs index 12fcb34c..e6dc242b 100644 --- a/src/clap_camino.rs +++ b/src/clap/camino.rs @@ -24,7 +24,7 @@ impl TypedValueParser for Utf8PathBufValueParser { clap::Error::raw( clap::error::ErrorKind::InvalidUtf8, format!("Path isn't UTF-8: {path_buf:?}"), - ) + ).with_cmd(cmd) }) }) } @@ -36,6 +36,6 @@ impl ValueParserFactory for Utf8PathBufValueParserFactory { type Parser = Utf8PathBufValueParser; fn value_parser() -> Self::Parser { - Utf8PathBufValueParser::default() + Self::Parser::default() } } diff --git a/src/clap/humantime.rs b/src/clap/humantime.rs new file mode 100644 index 00000000..4bfa14d0 --- /dev/null +++ b/src/clap/humantime.rs @@ -0,0 +1,91 @@ +//! Adapter for parsing [`Duration`] with a [`clap::builder::Arg::value_parser`]. + +use std::time::Duration; + +use clap::builder::StringValueParser; +use clap::builder::TypedValueParser; +use clap::builder::ValueParserFactory; +use humantime::DurationError; +use miette::LabeledSpan; +use miette::MietteDiagnostic; +use miette::Report; + +/// Adapter for parsing [`Duration`] with a [`clap::builder::Arg::value_parser`]. +#[derive(Default, Clone)] +pub struct DurationValueParser { + inner: StringValueParser, +} + +impl TypedValueParser for DurationValueParser { + type Value = Duration; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + self.inner.parse_ref(cmd, arg, value).and_then(|str_value| { + humantime::parse_duration(&str_value).map_err(|err| { + let diagnostic = Report::new(MietteDiagnostic { + message: match &err { + DurationError::InvalidCharacter(_) => "Invalid character".to_owned(), + DurationError::NumberExpected(_) => "Expected number".to_owned(), + DurationError::UnknownUnit { unit, .. } => format!("Unknown unit `{unit}`"), + DurationError::NumberOverflow => "Duration is too long".to_owned(), + DurationError::Empty => "No duration given".to_owned(), + }, + code: None, + severity: None, + help: match &err { + DurationError::InvalidCharacter(_) => { + Some("Non-alphanumeric characters are prohibited".to_owned()) + } + DurationError::NumberExpected(_) => { + Some("Did you split a unit into multiple words?".to_owned()) + } + DurationError::UnknownUnit { .. } => Some( + "Valid units include `ms` (milliseconds) and `s` (seconds)".to_owned(), + ), + DurationError::NumberOverflow => None, + DurationError::Empty => None, + }, + url: None, + labels: match err { + DurationError::InvalidCharacter(offset) => Some(vec![LabeledSpan::at( + offset..offset + 1, + "Invalid character", + )]), + DurationError::NumberExpected(offset) => { + Some(vec![LabeledSpan::at(offset..offset + 1, "Expected number")]) + } + DurationError::UnknownUnit { + start, + end, + unit: _, + value: _, + } => Some(vec![LabeledSpan::at(start..end, "Unknown unit")]), + DurationError::NumberOverflow => None, + DurationError::Empty => None, + }, + }) + .with_source_code(str_value); + clap::Error::raw( + clap::error::ErrorKind::ValueValidation, + format!("{diagnostic:?}"), + ) + .with_cmd(cmd) + }) + }) + } +} + +struct DurationValueParserFactory; + +impl ValueParserFactory for DurationValueParserFactory { + type Parser = DurationValueParser; + + fn value_parser() -> Self::Parser { + Self::Parser::default() + } +} diff --git a/src/clap/mod.rs b/src/clap/mod.rs new file mode 100644 index 00000000..fb9a8250 --- /dev/null +++ b/src/clap/mod.rs @@ -0,0 +1,6 @@ +//! Adapters for parsing [`clap`] arguments to various types. + +mod camino; +mod humantime; + +pub use self::humantime::DurationValueParser; diff --git a/src/cli.rs b/src/cli.rs index dd305c67..534b996d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,7 @@ //! To access arguments at any point in the program, use [`with_opts`] or [`with_opts_mut`]. use std::sync::Mutex; +use std::time::Duration; use camino::Utf8PathBuf; use clap::Parser; @@ -24,16 +25,16 @@ pub struct Opts { #[arg(long)] pub command: Option, - /// A path to watch for changes. Can be given multiple times. - #[arg(long)] - pub watch: Vec, - /// A shell command which runs tests. If given, this command will be run after reloads. /// /// May contain quoted arguments which will be parsed in a `sh`-like manner. #[arg(long)] pub test: Option, + /// Options to modify file watching. + #[command(flatten)] + pub watch: WatchOpts, + /// Options to modify logging and error-handling behavior. #[command(flatten)] pub logging: LoggingOpts, @@ -43,19 +44,28 @@ pub struct Opts { #[derive(Debug, Clone, clap::Args)] #[clap(next_help_heading = "File watching options")] pub struct WatchOpts { - /// Use polling rather than notification-based file watching. Polling tends to be more reliable - /// and less performant. - #[arg(long)] - pub poll: bool, + /// Use polling with the given interval rather than notification-based file watching. Polling + /// tends to be more reliable and less performant. + #[arg(long, value_name = "DURATION", value_parser = crate::clap::DurationValueParser::default())] + pub poll: Option, /// Debounce file events; wait this duration after receiving an event before attempting to /// reload. /// /// Defaults to 0.5 seconds. - /// - /// TODO: Parse this into a duration. - #[arg(long, default_value = "500ms")] - pub debounce: String, + // Why do we need to use `value_parser` with this argument but not with the `Utf8PathBuf` + // arguments? I have no clue! + #[arg( + long, + default_value = "500ms", + value_name = "DURATION", + value_parser = crate::clap::DurationValueParser::default(), + )] + pub debounce: Duration, + + /// A path to watch for changes. Can be given multiple times. + #[arg(long = "watch")] + pub paths: Vec, } // TODO: Possibly set `RUST_LIB_BACKTRACE` from `RUST_BACKTRACE` as well, so that `full` @@ -93,8 +103,8 @@ impl Opts { /// /// Also sets environment variables according to the arguments. pub fn set_opts(mut self) { - if self.watch.is_empty() { - self.watch.push("src".into()); + if self.watch.paths.is_empty() { + self.watch.paths.push("src".into()); } // These help our libraries (particularly `color-eyre`) see these options. diff --git a/src/lib.rs b/src/lib.rs index 12821798..e8e38a24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ pub mod aho_corasick; pub mod buffers; -pub mod clap_camino; +pub mod clap; pub mod cli; pub mod command; pub mod event_filter; diff --git a/src/main.rs b/src/main.rs index 2a9d7b28..5e3299c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,14 @@ async fn main() -> miette::Result<()> { )); let ghci = Ghci::new(ghci_command).await?; - let watcher = cli::with_opts(|opts| Watcher::new(ghci, &opts.watch))?; + let watcher = cli::with_opts(|opts| { + Watcher::new( + ghci, + &opts.watch.paths, + opts.watch.debounce, + opts.watch.poll, + ) + })?; watcher.handle.await.into_diagnostic()??; diff --git a/src/watcher.rs b/src/watcher.rs index d019125b..a7811dec 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -36,7 +36,12 @@ pub struct Watcher { impl Watcher { /// Create a new [`Watcher`] from a [`Ghci`] session. - pub fn new(ghci: Arc>, watch: &[Utf8PathBuf]) -> miette::Result { + pub fn new( + ghci: Arc>, + watch: &[Utf8PathBuf], + debounce: Duration, + poll: Option, + ) -> miette::Result { let mut init_config = InitConfig::default(); init_config.on_error(PrintDebug(std::io::stderr())); @@ -45,9 +50,13 @@ impl Watcher { let mut runtime_config = RuntimeConfig::default(); runtime_config .pathset(watch) - .action_throttle(Duration::from_millis(500)) + .action_throttle(debounce) .on_action(action_handler); + if let Some(interval) = poll { + runtime_config.file_watcher(watchexec::fs::Watcher::Poll(interval)); + } + let watcher = Watchexec::new(init_config, runtime_config.clone())?; let watcher_handle = watcher.main();