Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 16 additions & 1 deletion argv/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

use core::fmt::Write as _;

use crate::spec::{ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta};
use crate::spec::{
AdmonitionKind, AdmonitionMeta, ArgMeta, CommandMeta, Example, FlagMeta, Spec, ViewMeta,
};
use crate::Command;
use crate::DoubleDash;

Expand Down Expand Up @@ -1919,6 +1921,7 @@ fn long_sections(
width,
meta.next_line_help,
);
admonitions(out, a.admonitions);
long_annotations(
out,
if a.hide_possible_values {
Expand Down Expand Up @@ -1959,6 +1962,7 @@ fn long_sections(
width,
meta.next_line_help,
);
admonitions(out, f.admonitions);
long_annotations(
out,
if f.hide_possible_values {
Expand Down Expand Up @@ -1988,6 +1992,7 @@ fn long_sections(
|out, (f, usage)| {
let text = f.long_help.or(f.help);
entry(out, usage, text, flag_col, width, meta.next_line_help);
admonitions(out, f.admonitions);
long_annotations(
out,
if f.hide_possible_values {
Expand Down Expand Up @@ -2166,6 +2171,16 @@ fn long_annotations(
}
}

fn admonitions(out: &mut String, blocks: &[AdmonitionMeta<'_>]) {
for block in blocks {
let label = match block.kind {
AdmonitionKind::Note => "Note",
AdmonitionKind::Warning => "Warning",
};
write_indented(out, &format!("{label}: {}", block.text), 4);
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

fn deprecation_label(
message: Option<&str>,
warn_at: Option<&str>,
Expand Down
38 changes: 38 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,20 @@ pub struct FlattenGroup<'a> {
pub help_heading: Option<&'a str>,
}

/// The significance of an explanatory block attached to an argument.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdmonitionKind {
Note,
Warning,
}

/// Structured explanatory text that each renderer adapts to its output format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdmonitionMeta<'a> {
pub kind: AdmonitionKind,
pub text: &'a str,
}

/// What a flag knows about itself beyond how it parses.
#[derive(Debug, Clone, Copy)]
pub struct FlagMeta<'a> {
Expand All @@ -994,6 +1008,8 @@ pub struct FlagMeta<'a> {
pub help: Option<&'a str>,
/// Long help, shown by `--help`.
pub long_help: Option<&'a str>,
/// Notes and warnings shown after the extended help text.
pub admonitions: &'a [AdmonitionMeta<'a>],
/// Why this flag is deprecated, plus optional release milestones.
pub deprecated: Option<&'a str>,
pub deprecated_warn_at: Option<&'a str>,
Expand Down Expand Up @@ -1111,6 +1127,7 @@ impl FlagMeta<'_> {
hidden_longs: &[],
help: None,
long_help: None,
admonitions: &[],
deprecated: None,
deprecated_warn_at: None,
deprecated_remove_at: None,
Expand Down Expand Up @@ -1197,6 +1214,8 @@ pub struct ArgMeta<'a> {
pub value_names: &'a [&'a str],
pub help: Option<&'a str>,
pub long_help: Option<&'a str>,
/// Notes and warnings shown after the extended help text.
pub admonitions: &'a [AdmonitionMeta<'a>],
pub env: Option<&'a str>,
pub env_fallback: &'a [&'a str],
pub deprecated_env: &'a [&'a str],
Expand Down Expand Up @@ -1260,6 +1279,7 @@ impl ArgMeta<'_> {
value_names: &[],
help: None,
long_help: None,
admonitions: &[],
env: None,
env_fallback: &[],
deprecated_env: &[],
Expand Down Expand Up @@ -2390,6 +2410,7 @@ fn write_flag(
write_single_list(out, "required_unless_all", meta.required_unless_all)?;

let has_children = meta.long_help.is_some()
|| !meta.admonitions.is_empty()
|| !meta.hidden_shorts.is_empty()
|| !meta.hidden_longs.is_empty()
|| meta.flag.takes_value
Expand Down Expand Up @@ -2419,6 +2440,14 @@ fn write_flag(
indent(out, inner)?;
writeln!(out, "long_help {}", quoted(long_help))?;
}
for admonition in meta.admonitions {
indent(out, inner)?;
let kind = match admonition.kind {
AdmonitionKind::Note => "note",
AdmonitionKind::Warning => "warning",
};
writeln!(out, "{kind} {}", quoted(admonition.text))?;
}
if !meta.hidden_shorts.is_empty() || !meta.hidden_longs.is_empty() {
indent(out, inner)?;
out.push_str("alias");
Expand Down Expand Up @@ -2685,6 +2714,7 @@ fn write_arg(out: &mut String, meta: &ArgMeta<'_>, depth: usize) -> core::fmt::R
write_single_list(out, "required_unless_all", meta.required_unless_all)?;

let has_children = meta.long_help.is_some()
|| !meta.admonitions.is_empty()
|| !meta.choices.is_empty()
|| !meta.accepted_choices.is_empty()
|| !meta.choice_details.is_empty()
Expand All @@ -2710,6 +2740,14 @@ fn write_arg(out: &mut String, meta: &ArgMeta<'_>, depth: usize) -> core::fmt::R
indent(out, inner)?;
writeln!(out, "long_help {}", quoted(long_help))?;
}
for admonition in meta.admonitions {
indent(out, inner)?;
let kind = match admonition.kind {
AdmonitionKind::Note => "note",
AdmonitionKind::Warning => "warning",
};
writeln!(out, "{kind} {}", quoted(admonition.text))?;
}
if meta.conflicts.len() > 1 {
indent(out, inner)?;
out.push_str("conflicts");
Expand Down
22 changes: 20 additions & 2 deletions conformance/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ use usage::{
SpecGroup, SpecOutput,
};
use usage_argv::spec::{
ArgMeta, ChoiceAliasMeta, ChoiceMeta, CommandMeta, DefaultIf, Effect, Example, ExitCodeMeta,
FlagMeta, Framing as ArgvFraming, GroupMeta, OutputMeta, RequiredIfEq, RequiresIf,
AdmonitionKind, AdmonitionMeta, ArgMeta, ChoiceAliasMeta, ChoiceMeta, CommandMeta, DefaultIf,
Effect, Example, ExitCodeMeta, FlagMeta, Framing as ArgvFraming, GroupMeta, OutputMeta,
RequiredIfEq, RequiresIf,
};
use usage_argv::{Arg, Command, DoubleDash, Flag, UnknownFlags as ArgvUnknownFlags};

Expand Down Expand Up @@ -379,6 +380,7 @@ fn flag_meta(
hidden_longs: strs(&f.hidden_aliases),
help: opt(&f.help),
long_help: opt(&f.help_long),
admonitions: admonitions(&f.admonitions),
deprecated: opt(&f.deprecated),
deprecated_warn_at: opt(&f.deprecated_warn_at),
deprecated_remove_at: opt(&f.deprecated_remove_at),
Expand Down Expand Up @@ -488,6 +490,7 @@ fn arg_meta(
value_names: strs(&a.value_names),
help: opt(&a.help),
long_help: opt(&a.help_long),
admonitions: admonitions(&a.admonitions),
env: opt(&a.env),
env_fallback: strs(&a.env_fallback),
deprecated_env: strs(&a.deprecated_env),
Expand Down Expand Up @@ -613,6 +616,21 @@ fn strs(list: &[String]) -> &'static [&'static str] {
)
}

fn admonitions(list: &[usage::SpecAdmonition]) -> &'static [AdmonitionMeta<'static>] {
Box::leak(
list.iter()
.map(|block| AdmonitionMeta {
kind: match block.kind {
usage::SpecAdmonitionKind::Note => AdmonitionKind::Note,
usage::SpecAdmonitionKind::Warning => AdmonitionKind::Warning,
},
text: leak(&block.text),
})
.collect::<Vec<_>>()
.into_boxed_slice(),
)
}

fn bytes(list: &[char]) -> &'static [u8] {
Box::leak(
list.iter()
Expand Down
75 changes: 75 additions & 0 deletions conformance/tests/admonitions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use usage::docs::markdown::MarkdownRenderer;
use usage_argv::help;
use usage_derive::Cli;

#[derive(Cli)]
#[usage(bin = "ex")]
#[allow(dead_code)]
struct Ex {
/// Override the discovered configuration.
#[usage(
long,
note = "Only changes import resolution.",
warning = "Type-aware linting discovers its own configuration."
)]
config: Option<String>,

/// File to inspect.
#[usage(note = "Directories are traversed recursively.")]
file: Option<String>,
}

#[test]
fn semantic_blocks_adapt_to_terminal_help_and_markdown() {
let terminal = help::render(Ex::spec(), Ex::spec().root.cmd, true).expect("long help");
assert!(
terminal.contains("Note: Only changes import resolution."),
"{terminal}"
);
assert!(
terminal.contains("Warning: Type-aware linting discovers its own configuration."),
"{terminal}"
);
assert!(
terminal.contains("Note: Directories are traversed recursively."),
"{terminal}"
);
assert!(!terminal.contains("::: warning"), "{terminal}");

let kdl = Ex::to_kdl();
assert!(
kdl.contains("note \"Only changes import resolution.\""),
"{kdl}"
);
assert!(
kdl.contains("warning \"Type-aware linting discovers its own configuration.\""),
"{kdl}"
);

let spec: usage::Spec = kdl.parse().expect("generated spec");
let portable_terminal = usage::docs::cli::render_help(&spec, &spec.cmd, true);
assert!(
portable_terminal.contains("Note: Only changes import resolution."),
"{portable_terminal}"
);
assert!(
portable_terminal.contains("Warning: Type-aware linting discovers its own configuration."),
"{portable_terminal}"
);
assert!(
portable_terminal.contains("Note: Directories are traversed recursively."),
"{portable_terminal}"
);

let markdown = MarkdownRenderer::new(spec.clone())
.render_cmd(&spec.cmd)
.expect("markdown page");
assert!(
markdown.contains("> **Note:** Only changes import resolution."),
"{markdown}"
);
assert!(
markdown.contains("> **Warning:** Type-aware linting discovers its own configuration."),
"{markdown}"
);
}
22 changes: 19 additions & 3 deletions derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ use quote::{format_ident, quote};

use crate::crate_name::{crate_name, FoundCrate};
use crate::model::{
rendered_path, to_kebab, type_name, ArgGroup, ArgGroupMember, Cli, ConditionalDefault,
Dispatch, DoubleDash, ExampleDecl, Field, Kind, SchemaSource, Shape, Subcommands, ValueEnum,
Variant, ViewDecl,
rendered_path, to_kebab, type_name, AdmonitionKind, ArgGroup, ArgGroupMember, Cli,
ConditionalDefault, Dispatch, DoubleDash, ExampleDecl, Field, Kind, SchemaSource, Shape,
Subcommands, ValueEnum, Variant, ViewDecl,
};

fn admonitions(field: &Field) -> TokenStream {
let entries = field.admonitions.iter().map(|admonition| {
let text = &admonition.text;
let kind = match admonition.kind {
AdmonitionKind::Note => quote!(usage_argv::spec::AdmonitionKind::Note),
AdmonitionKind::Warning => quote!(usage_argv::spec::AdmonitionKind::Warning),
};
quote!(usage_argv::spec::AdmonitionMeta { kind: #kind, text: #text })
});
quote!(&[#(#entries),*])
}

/// Construct the user's command type after its generated partial has been checked.
///
/// An empty named struct is `Self {}`, while a unit struct is just `Self`. Keeping
Expand Down Expand Up @@ -2524,6 +2536,7 @@ fn flag_meta(cli: &Cli, i: usize, field: &Field, owner: &syn::Ident) -> TokenStr
let table = format_ident!("FLAG_{i}");
let help = option_str(field.help.as_deref());
let long_help = option_str(field.long_help.as_deref());
let admonitions = admonitions(field);
let env = option_str(field.env.as_deref());
let env_fallback = &field.env_fallback;
let deprecated_env = &field.deprecated_env;
Expand Down Expand Up @@ -2662,6 +2675,7 @@ fn flag_meta(cli: &Cli, i: usize, field: &Field, owner: &syn::Ident) -> TokenStr
display_order: #display_order,
help: #help,
long_help: #long_help,
admonitions: #admonitions,
deprecated: #deprecated,
deprecated_warn_at: #deprecated_warn_at,
deprecated_remove_at: #deprecated_remove_at,
Expand Down Expand Up @@ -2721,6 +2735,7 @@ fn arg_meta(cli: &Cli, i: usize, field: &Field, owner: &syn::Ident) -> TokenStre
let table = format_ident!("ARG_{i}");
let help = option_str(field.help.as_deref());
let long_help = option_str(field.long_help.as_deref());
let admonitions = admonitions(field);
let env = option_str(field.env.as_deref());
let env_fallback = &field.env_fallback;
let deprecated_env = &field.deprecated_env;
Expand Down Expand Up @@ -2799,6 +2814,7 @@ fn arg_meta(cli: &Cli, i: usize, field: &Field, owner: &syn::Ident) -> TokenStre
value_names: &[#(#value_names),*],
help: #help,
long_help: #long_help,
admonitions: #admonitions,
env: #env,
env_fallback: &[#(#env_fallback),*],
deprecated_env: &[#(#deprecated_env),*],
Expand Down
2 changes: 2 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@
//! | `default_fn = function` | compute one typed default at parse time without claiming a concrete portable value |
//! | `default_note = "x"` | describe a `default_fn` in help; the note is prose, not a value |
//! | `help_heading = "x"` | the section to list this under in help output |
//! | `note = "x"` | a semantic note shown in long help and generated documentation |
//! | `warning = "x"` | a semantic warning shown in long help and generated documentation |
//! | `display_order = n` | explicit help order; positional parsing still follows declaration order |
//! | `verbatim_doc_comment` | preserve line breaks and whitespace in the doc comment instead of flowing its first paragraph |
//! | `hide` | keep it out of help and completions |
Expand Down
Loading
Loading