Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement --debounce and --poll #3

Merged
merged 3 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
448 changes: 204 additions & 244 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +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"
itertools = "0.11.0"
miette = { version = "5.9.0", features = ["fancy"] }
nix = { version = "0.26.2", default_features = false, features = ["process"] }
Expand Down
4 changes: 2 additions & 2 deletions src/clap/camino.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
}
Expand All @@ -36,6 +36,6 @@ impl ValueParserFactory for Utf8PathBufValueParserFactory {
type Parser = Utf8PathBufValueParser;

fn value_parser() -> Self::Parser {
Utf8PathBufValueParser::default()
Self::Parser::default()
}
}
29 changes: 29 additions & 0 deletions src/clap/error_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::fmt::Display;

use owo_colors::OwoColorize;
use owo_colors::Stream::Stdout;

/// Construct a [`clap::Error`] formatted like the builtin error messages, which are constructed
/// with a private API. (!)
///
/// This is a sad little hack while the maintainer blocks my PRs:
/// <https://github.com/clap-rs/clap/issues/5065>
pub fn value_validation_error(
arg: Option<&clap::Arg>,
bad_value: &str,
message: impl Display,
) -> clap::Error {
clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
format!(
"invalid value '{bad_value}' for '{arg}': {message}\n\n\
For more information, try '{help}'.\n",
bad_value = bad_value.if_supports_color(Stdout, |text| text.yellow()),
arg = arg
.map(ToString::to_string)
.unwrap_or_else(|| "...".to_owned())
.if_supports_color(Stdout, |text| text.bold()),
help = "--help".if_supports_color(Stdout, |text| text.bold()),
),
)
}
95 changes: 95 additions & 0 deletions src/clap/humantime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! 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 clap::error::ContextKind;
use clap::error::ContextValue;
use humantime::DurationError;
use miette::LabeledSpan;
use miette::MietteDiagnostic;
use miette::Report;

use super::value_validation_error;

/// 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::Value, clap::Error> {
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(index) => {
if &str_value[*index..*index + 1] == "." {
Some("Decimals are not supported".to_owned())
} else {
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.clone());
value_validation_error(arg, &str_value, format!("{diagnostic:?}"))
})
})
}
}

struct DurationValueParserFactory;

impl ValueParserFactory for DurationValueParserFactory {
type Parser = DurationValueParser;

fn value_parser() -> Self::Parser {
Self::Parser::default()
}
}
6 changes: 5 additions & 1 deletion src/clap/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! [`clap`] compatability and value parsers.
//! Adapters for parsing [`clap`] arguments to various types.

mod camino;
mod error_message;
mod humantime;
mod rust_backtrace;

pub use rust_backtrace::RustBacktrace;
pub use self::humantime::DurationValueParser;
pub use error_message::value_validation_error;
39 changes: 25 additions & 14 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Command-line argument parser and argument access.

use std::time::Duration;

use camino::Utf8PathBuf;
use clap::Parser;

Expand All @@ -16,16 +18,16 @@ pub struct Opts {
#[arg(long)]
pub command: Option<String>,

/// A path to watch for changes. Can be given multiple times.
#[arg(long)]
pub watch: Vec<Utf8PathBuf>,

/// 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<String>,

/// Options to modify file watching.
#[command(flatten)]
pub watch: WatchOpts,

/// Options to modify logging and error-handling behavior.
#[command(flatten)]
pub logging: LoggingOpts,
Expand All @@ -35,19 +37,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<Duration>,

/// 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<Utf8PathBuf>,
}

// TODO: Possibly set `RUST_LIB_BACKTRACE` from `RUST_BACKTRACE` as well, so that `full`
Expand Down Expand Up @@ -79,8 +90,8 @@ impl Opts {
/// Perform late initialization of the command-line arguments. If `init` isn't called before
/// the arguments are used, the behavior is undefined.
pub fn init(&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.
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ async fn main() -> miette::Result<()> {
));

let ghci = Ghci::new(ghci_command).await?;
let watcher = Watcher::new(ghci, &opts.watch)?;
let watcher = Watcher::new(
ghci,
&opts.watch.paths,
opts.watch.debounce,
opts.watch.poll,
)?;

watcher.handle.await.into_diagnostic()??;

Expand Down
13 changes: 11 additions & 2 deletions src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ pub struct Watcher {

impl Watcher {
/// Create a new [`Watcher`] from a [`Ghci`] session.
pub fn new(ghci: Arc<Mutex<Ghci>>, watch: &[Utf8PathBuf]) -> miette::Result<Self> {
pub fn new(
ghci: Arc<Mutex<Ghci>>,
watch: &[Utf8PathBuf],
debounce: Duration,
poll: Option<Duration>,
) -> miette::Result<Self> {
let mut init_config = InitConfig::default();
init_config.on_error(PrintDebug(std::io::stderr()));

Expand All @@ -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();
Expand Down