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

Integrate clap-markdown in ghciwatch #248

Merged
merged 1 commit into from
May 20, 2024
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ edition = "2021"
authors = [
"Rebecca Turner <[email protected]>"
]
description = "ghci-based file watcher and recompiler for Haskell projects"
description = "Ghciwatch loads a GHCi session for a Haskell project and reloads it when source files change."
readme = "README.md"
homepage = "https://github.com/MercuryTechnologies/ghciwatch"
repository = "https://github.com/MercuryTechnologies/ghciwatch"
Expand All @@ -41,6 +41,7 @@ backoff = { version = "0.4.0", default-features = false }
camino = "1.1.4"
# Clap 4.4 is the last version supporting Rust 1.72.
clap = { version = "~4.4", features = ["derive", "wrap_help", "env", "string"] }
clap-markdown = { path = "clap-markdown", optional = true }
clearscreen = "2.0.1"
command-group = { version = "2.1.0", features = ["tokio", "with-tokio"] }
crossterm = { version = "0.27.0", features = ["event-stream"] }
Expand All @@ -51,7 +52,7 @@ indoc = "1.0.6"
itertools = "0.11.0"
line-span = "0.1.5"
miette = { version = "5.9.0", features = ["fancy"] }
nix = { version = "0.26.2", default_features = false, features = ["process", "signal"] }
nix = { version = "0.26.2", default-features = false, features = ["process", "signal"] }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a warning about this, maybe a toolchain update caught it? IDK.

notify-debouncer-full = "0.3.1"
once_cell = "1.18.0"
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
Expand Down
95 changes: 75 additions & 20 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
//! Command-line argument parser and argument access.
#![allow(rustdoc::bare_urls)]

use std::time::Duration;

use camino::Utf8PathBuf;
Expand All @@ -14,10 +12,47 @@ use crate::clonable_command::ClonableCommand;
use crate::ignore::GlobMatcher;
use crate::normal_path::NormalPath;

/// A `ghci`-based file watcher and Haskell recompiler.
/// Ghciwatch loads a GHCi session for a Haskell project and reloads it
/// when source files change.
///
/// ## Examples
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some examples. This is a sort of weird compromise; these look decent in --help output unprocessed and also render as Markdown.

clap doesn't have any hooks to customize how the docstrings are preprocessed, so we have to turn off preprocessing (or it'll line-wrap the examples). Unfortunately, that loses us all formatting for this text...

///
/// Load `cabal v2-repl` and watch for changes in `src`:
///
/// ghciwatch
///
/// Load a custom GHCi session and watch for changes in multiple locations:
///
/// ghciwatch --command "cabal v2-repl lib:test-dev" \
/// --watch src --watch test
///
/// Run tests after reloads:
///
/// ghciwatch --test-ghci TestMain.testMain \
/// --after-startup-ghci ':set args "--match=/OnlyRunSomeTests/"'
///
/// Use `hpack` to regenerate `.cabal` files:
///
/// ghciwatch --before-startup-shell hpack \
/// --restart-glob '**/package.yaml'
///
/// Also reload the session when `.persistentmodels` change:
///
/// ghciwatch --watch config/modelsFiles \
/// --reload-glob '**/*.persistentmodels'
///
/// Don't reload for `README.md` files:
///
/// ghciwatch --reload-glob '!src/**/README.md'
#[allow(rustdoc::invalid_rust_codeblocks)]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And rustdoc doesn't like that the indented shell examples aren't valid Rust code. (You're supposed to use triple backticks annotated as plain or something for that, but of course we don't want it to show up in the output.)

#[derive(Debug, Clone, Parser)]
#[command(version, author, about)]
#[command(max_term_width = 100)]
#[command(
version,
author,
verbatim_doc_comment,
max_term_width = 100,
override_usage = "ghciwatch [--command SHELL_COMMAND] [--watch PATH] [OPTIONS ...]"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--command and --watch are the really important options so I pulled them out; the default usage string was ghciwatch [OPTIONS] which isn't very useful.

)]
pub struct Opts {
/// A shell command which starts a `ghci` REPL, e.g. `ghci` or `cabal v2-repl` or similar.
///
Expand All @@ -27,11 +62,13 @@ pub struct Opts {
#[arg(long, value_name = "SHELL_COMMAND")]
pub command: Option<ClonableCommand>,

/// A file to write compilation errors to. This is analogous to `ghcid.txt`.
/// A file to write compilation errors to.
///
/// The output format is compatible with `ghcid`'s `--outputfile` option.
#[arg(long, alias = "outputfile", alias = "errors")]
pub error_file: Option<Utf8PathBuf>,

/// Enable evaluating commands.
/// Evaluate Haskell code in comments.
///
/// This parses line commands starting with `-- $>` or multiline commands delimited by `{- $>`
/// and `<$ -}` and evaluates them after reloads.
Expand All @@ -52,6 +89,11 @@ pub struct Opts {
#[arg(long, hide = true)]
pub tui: bool,

/// Generate Markdown CLI documentation.
#[cfg(feature = "clap-markdown")]
#[arg(long, hide = true)]
pub generate_markdown_help: bool,

/// Lifecycle hooks and commands to run at various points.
#[command(flatten)]
pub hooks: crate::hooks::HookOpts,
Expand All @@ -69,8 +111,10 @@ pub struct Opts {
#[derive(Debug, Clone, clap::Args)]
#[clap(next_help_heading = "File watching options")]
pub struct WatchOpts {
/// Use polling with the given interval rather than notification-based file watching. Polling
/// tends to be more reliable and less performant.
/// Use polling with the given interval rather than notification-based file watching.
///
/// Polling tends to be more reliable and less performant. In particular, notification-based
/// watching often misses updates on macOS.
#[arg(long, value_name = "DURATION", value_parser = crate::clap::DurationValueParser::default())]
pub poll: Option<Duration>,

Expand All @@ -88,33 +132,40 @@ pub struct WatchOpts {
)]
pub debounce: Duration,

/// A path to watch for changes. Directories are watched recursively. Can be given multiple times.
#[arg(long = "watch")]
/// A path to watch for changes.
///
/// Directories are watched recursively. Can be given multiple times.
#[arg(long = "watch", value_name = "PATH")]
pub paths: Vec<NormalPath>,

/// Reload the `ghci` session when paths matching this glob change. Can be given multiple
/// times. The last matching glob will determine if a reload is triggered.
/// Reload the `ghci` session when paths matching this glob change.
///
/// By default, only changes to Haskell source files trigger reloads. If you'd like to exclude
/// some files from that, you can add an ignore glob here, like `!src/my-special-dir/**/*.hs`.
///
/// Globs provided here have precisely the same semantics as a single line in a `gitignore`
/// file (`man gitignore`), where the meaning of `!` is inverted: namely, `!` at the beginning
/// of a glob will ignore a file.
///
/// The last matching glob will determine if a reload is triggered.
///
/// Can be given multiple times.
#[arg(long = "reload-glob")]
pub reload_globs: Vec<String>,

/// Restart the `ghci` session when paths matching this glob change. Can be given multiple
/// times.
/// Restart the `ghci` session when paths matching this glob change.
///
/// By default, only changes to `.cabal` or `.ghci` files or Haskell source files being
/// moved/removed will trigger restarts.
///
/// Due to a `ghci` bug, the `ghci` session must be restarted when Haskell modules are removed
/// or renamed: https://gitlab.haskell.org/ghc/ghc/-/issues/11596
/// Due to [a `ghci` bug][1], the `ghci` session must be restarted when Haskell modules are removed
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda clumsy to have markdown link syntax in the --help output like this but it looks OK enough and it renders correctly as Markdown.

It'd be nice to have a system that let us do some more advanced templating to add more detail in the manual...

/// or renamed.
///
/// See `--reload-globs` for more details.
#[allow(rustdoc::bare_urls)]
///
/// Can be given multiple times.
///
/// [1]: https://gitlab.haskell.org/ghc/ghc/-/issues/11596
#[arg(long = "restart-glob")]
pub restart_globs: Vec<String>,
}
Expand Down Expand Up @@ -147,9 +198,11 @@ pub struct LoggingOpts {
/// The grammar is: `target[span{field=value}]=level`, where `target` is a module path, `span`
/// is a span name, and `level` is one of the levels listed above.
///
/// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
/// See [documentation in `tracing-subscriber`][1].
///
/// A nice value is `ghciwatch=debug`.
///
/// A nice value is "ghciwatch=debug".
/// [1]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html
#[arg(long, default_value = "ghciwatch=info")]
pub log_filter: String,

Expand All @@ -169,6 +222,8 @@ pub struct LoggingOpts {
pub trace_spans: Vec<FmtSpan>,

/// Path to write JSON logs to.
///
/// JSON logs are not yet stable and the format may change on any release.
#[arg(long, value_name = "PATH")]
pub log_json: Option<Utf8PathBuf>,
}
Expand Down
12 changes: 9 additions & 3 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ impl LifecycleEvent {
),
LifecycleEvent::Restart(_) => indoc!(
"
`ghci` is restarted when modules are removed or renamed.
See: https://gitlab.haskell.org/ghc/ghc/-/issues/11596
Due to [a `ghci` bug][1], the `ghci` session must be restarted when Haskell modules
are removed or renamed.

[1]: https://gitlab.haskell.org/ghc/ghc/-/issues/11596
"
),
}.trim_end_matches('\n')
Expand Down Expand Up @@ -275,8 +277,12 @@ impl Hook<CommandKind> {
long.push_str("\n\n");
long.push_str(event.get_message());

if let CommandKind::Shell = command {
long.push_str("\n\nCommands starting with `async:` will be run in the background.");
}

if let Some(extra_help) = self.extra_help() {
long.push('\n');
long.push_str("\n\n");
long.push_str(extra_help);
}

Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ async fn main() -> miette::Result<()> {
opts.init()?;
let (maybe_tracing_reader, _tracing_guard) = TracingOpts::from_cli(&opts).install()?;

#[cfg(feature = "clap-markdown")]
if opts.generate_markdown_help {
println!("{}", clap_markdown::help_markdown::<cli::Opts>());
return Ok(());
}

std::env::set_var("IN_GHCIWATCH", "1");

let (ghci_sender, ghci_receiver) = mpsc::channel(32);
Expand Down
Loading