Skip to content

Commit 16ded80

Browse files
committed
feat(complete): Show help in dynamic completions
1 parent 4f9cf6b commit 16ded80

File tree

13 files changed

+118
-107
lines changed

13 files changed

+118
-107
lines changed

clap_complete/src/dynamic/completer.rs

+19-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::ffi::OsStr;
22
use std::ffi::OsString;
33

4+
use clap::builder::StyledStr;
45
use clap_lex::OsStrExt as _;
56

67
/// Shell-specific completions
@@ -31,7 +32,7 @@ pub fn complete(
3132
args: Vec<std::ffi::OsString>,
3233
arg_index: usize,
3334
current_dir: Option<&std::path::Path>,
34-
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
35+
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
3536
cmd.build();
3637

3738
let raw_args = clap_lex::RawArgs::new(args.into_iter());
@@ -90,7 +91,7 @@ fn complete_arg(
9091
current_dir: Option<&std::path::Path>,
9192
pos_index: usize,
9293
is_escaped: bool,
93-
) -> Result<Vec<std::ffi::OsString>, std::io::Error> {
94+
) -> Result<Vec<(std::ffi::OsString, Option<StyledStr>)>, std::io::Error> {
9495
debug!(
9596
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
9697
arg,
@@ -109,17 +110,19 @@ fn complete_arg(
109110
completions.extend(
110111
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
111112
.into_iter()
112-
.map(|os| {
113+
.map(|(os, help)| {
113114
// HACK: Need better `OsStr` manipulation
114-
format!("--{}={}", flag, os.to_string_lossy()).into()
115+
(format!("--{}={}", flag, os.to_string_lossy()).into(), help)
115116
}),
116117
)
117118
}
118119
} else {
119120
completions.extend(
120121
crate::generator::utils::longs_and_visible_aliases(cmd)
121122
.into_iter()
122-
.filter_map(|f| f.starts_with(flag).then(|| format!("--{f}").into())),
123+
.filter_map(|(f, help)| {
124+
f.starts_with(flag).then(|| (format!("--{f}").into(), help))
125+
}),
123126
);
124127
}
125128
}
@@ -128,7 +131,7 @@ fn complete_arg(
128131
completions.extend(
129132
crate::generator::utils::longs_and_visible_aliases(cmd)
130133
.into_iter()
131-
.map(|f| format!("--{f}").into()),
134+
.map(|(f, help)| (format!("--{f}").into(), help)),
132135
);
133136
}
134137

@@ -143,7 +146,7 @@ fn complete_arg(
143146
crate::generator::utils::shorts_and_visible_aliases(cmd)
144147
.into_iter()
145148
// HACK: Need better `OsStr` manipulation
146-
.map(|f| format!("{}{}", dash_or_arg, f).into()),
149+
.map(|(f, help)| (format!("{}{}", dash_or_arg, f).into(), help)),
147150
);
148151
}
149152
}
@@ -166,15 +169,16 @@ fn complete_arg_value(
166169
value: Result<&str, &OsStr>,
167170
arg: &clap::Arg,
168171
current_dir: Option<&std::path::Path>,
169-
) -> Vec<OsString> {
172+
) -> Vec<(OsString, Option<StyledStr>)> {
170173
let mut values = Vec::new();
171174
debug!("complete_arg_value: arg={arg:?}, value={value:?}");
172175

173176
if let Some(possible_values) = crate::generator::utils::possible_values(arg) {
174177
if let Ok(value) = value {
175178
values.extend(possible_values.into_iter().filter_map(|p| {
176179
let name = p.get_name();
177-
name.starts_with(value).then(|| name.into())
180+
name.starts_with(value)
181+
.then(|| (name.into(), p.get_help().cloned()))
178182
}));
179183
}
180184
} else {
@@ -223,7 +227,7 @@ fn complete_path(
223227
value_os: &OsStr,
224228
current_dir: Option<&std::path::Path>,
225229
is_wanted: impl Fn(&std::path::Path) -> bool,
226-
) -> Vec<OsString> {
230+
) -> Vec<(OsString, Option<StyledStr>)> {
227231
let mut completions = Vec::new();
228232

229233
let current_dir = match current_dir {
@@ -255,20 +259,20 @@ fn complete_path(
255259
let path = entry.path();
256260
let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
257261
suggestion.push(""); // Ensure trailing `/`
258-
completions.push(suggestion.as_os_str().to_owned());
262+
completions.push((suggestion.as_os_str().to_owned(), None));
259263
} else {
260264
let path = entry.path();
261265
if is_wanted(&path) {
262266
let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
263-
completions.push(suggestion.as_os_str().to_owned());
267+
completions.push((suggestion.as_os_str().to_owned(), None));
264268
}
265269
}
266270
}
267271

268272
completions
269273
}
270274

271-
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
275+
fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<(OsString, Option<StyledStr>)> {
272276
debug!(
273277
"complete_subcommand: cmd={:?}, value={:?}",
274278
cmd.get_name(),
@@ -278,9 +282,9 @@ fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<OsString> {
278282
let mut scs = crate::generator::utils::subcommands(cmd)
279283
.into_iter()
280284
.filter(|x| x.0.starts_with(value))
281-
.map(|x| OsString::from(&x.0))
285+
.map(|x| (OsString::from(&x.0), x.2))
282286
.collect::<Vec<_>>();
283287
scs.sort();
284-
scs.dedup();
288+
// TODO: is this needed, doesn't work with styled string scs.dedup();
285289
scs
286290
}

clap_complete/src/dynamic/shells/bash.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ complete -o nospace -o bashdefault -F _clap_complete_NAME BIN
7373
let ifs: Option<String> = std::env::var("IFS").ok().and_then(|i| i.parse().ok());
7474
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;
7575

76-
for (i, completion) in completions.iter().enumerate() {
76+
for (i, (completion, _)) in completions.iter().enumerate() {
7777
if i != 0 {
7878
write!(buf, "{}", ifs.as_deref().unwrap_or("\n"))?;
7979
}

clap_complete/src/dynamic/shells/fish.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ impl crate::dynamic::Completer for Fish {
3030
let index = args.len() - 1;
3131
let completions = crate::dynamic::complete(cmd, args, index, current_dir)?;
3232

33-
for completion in completions {
34-
writeln!(buf, "{}", completion.to_string_lossy())?;
33+
for (completion, help) in completions {
34+
write!(buf, "{}", completion.to_string_lossy())?;
35+
if let Some(help) = help {
36+
write!(buf, "\t{help}")?;
37+
}
38+
writeln!(buf)?;
3539
}
3640
Ok(())
3741
}

clap_complete/src/generator/utils.rs

+72-71
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Helpers for writing generators
22
3-
use clap::{Arg, Command};
3+
use clap::{builder::StyledStr, Arg, Command};
44

55
/// Gets all subcommands including child subcommands in the form of `("name", "bin_name")`.
66
///
77
/// Subcommand `rustup toolchain install` would be converted to
88
/// `("install", "rustup toolchain install")`.
9-
pub fn all_subcommands(cmd: &Command) -> Vec<(String, String)> {
9+
pub fn all_subcommands(cmd: &Command) -> Vec<(String, String, Option<StyledStr>)> {
1010
let mut subcmds: Vec<_> = subcommands(cmd);
1111

1212
for sc_v in cmd.get_subcommands().map(all_subcommands) {
@@ -33,7 +33,7 @@ pub fn find_subcommand_with_path<'cmd>(p: &'cmd Command, path: Vec<&str>) -> &'c
3333
///
3434
/// Subcommand `rustup toolchain install` would be converted to
3535
/// `("install", "rustup toolchain install")`.
36-
pub fn subcommands(p: &Command) -> Vec<(String, String)> {
36+
pub fn subcommands(p: &Command) -> Vec<(String, String, Option<StyledStr>)> {
3737
debug!("subcommands: name={}", p.get_name());
3838
debug!("subcommands: Has subcommands...{:?}", p.has_subcommands());
3939

@@ -48,62 +48,42 @@ pub fn subcommands(p: &Command) -> Vec<(String, String)> {
4848
sc_bin_name
4949
);
5050

51-
subcmds.push((sc.get_name().to_string(), sc_bin_name.to_string()));
51+
subcmds.push((
52+
sc.get_name().to_string(),
53+
sc_bin_name.to_string(),
54+
sc.get_about().cloned(),
55+
));
5256
}
5357

5458
subcmds
5559
}
5660

5761
/// Gets all the short options, their visible aliases and flags of a [`clap::Command`].
5862
/// Includes `h` and `V` depending on the [`clap::Command`] settings.
59-
pub fn shorts_and_visible_aliases(p: &Command) -> Vec<char> {
63+
pub fn shorts_and_visible_aliases(p: &Command) -> Vec<(char, Option<StyledStr>)> {
6064
debug!("shorts: name={}", p.get_name());
6165

6266
p.get_arguments()
6367
.filter_map(|a| {
64-
if !a.is_positional() {
65-
if a.get_visible_short_aliases().is_some() && a.get_short().is_some() {
66-
let mut shorts_and_visible_aliases = a.get_visible_short_aliases().unwrap();
67-
shorts_and_visible_aliases.push(a.get_short().unwrap());
68-
Some(shorts_and_visible_aliases)
69-
} else if a.get_visible_short_aliases().is_none() && a.get_short().is_some() {
70-
Some(vec![a.get_short().unwrap()])
71-
} else {
72-
None
73-
}
74-
} else {
75-
None
76-
}
68+
a.get_short_and_visible_aliases()
69+
.map(|shorts| shorts.into_iter().map(|s| (s, a.get_help().cloned())))
7770
})
7871
.flatten()
7972
.collect()
8073
}
8174

8275
/// Gets all the long options, their visible aliases and flags of a [`clap::Command`].
8376
/// Includes `help` and `version` depending on the [`clap::Command`] settings.
84-
pub fn longs_and_visible_aliases(p: &Command) -> Vec<String> {
77+
pub fn longs_and_visible_aliases(p: &Command) -> Vec<(String, Option<StyledStr>)> {
8578
debug!("longs: name={}", p.get_name());
8679

8780
p.get_arguments()
8881
.filter_map(|a| {
89-
if !a.is_positional() {
90-
if a.get_visible_aliases().is_some() && a.get_long().is_some() {
91-
let mut visible_aliases: Vec<_> = a
92-
.get_visible_aliases()
93-
.unwrap()
94-
.into_iter()
95-
.map(|s| s.to_string())
96-
.collect();
97-
visible_aliases.push(a.get_long().unwrap().to_string());
98-
Some(visible_aliases)
99-
} else if a.get_visible_aliases().is_none() && a.get_long().is_some() {
100-
Some(vec![a.get_long().unwrap().to_string()])
101-
} else {
102-
None
103-
}
104-
} else {
105-
None
106-
}
82+
a.get_long_and_visible_aliases().map(|longs| {
83+
longs
84+
.into_iter()
85+
.map(|s| (s.to_string(), a.get_help().cloned()))
86+
})
10787
})
10888
.flatten()
10989
.collect()
@@ -136,6 +116,19 @@ mod tests {
136116
use clap::Arg;
137117
use clap::ArgAction;
138118

119+
const HELP: &str = "Print this message or the help of the given subcommand(s)";
120+
121+
macro_rules! assert_option {
122+
($option:expr, $value:expr) => {
123+
assert_eq!($option.0, $value);
124+
assert!($option.1.is_none());
125+
};
126+
($option:expr, $value:expr, $help:expr) => {
127+
assert_eq!($option.0, $value);
128+
assert_eq!($option.1.as_ref().unwrap().to_string(), $help);
129+
};
130+
}
131+
139132
fn common_app() -> Command {
140133
Command::new("myapp")
141134
.subcommand(
@@ -171,36 +164,44 @@ mod tests {
171164
fn test_subcommands() {
172165
let cmd = built_with_version();
173166

174-
assert_eq!(
175-
subcommands(&cmd),
176-
vec![
177-
("test".to_string(), "my-cmd test".to_string()),
178-
("hello".to_string(), "my-cmd hello".to_string()),
179-
("help".to_string(), "my-cmd help".to_string()),
180-
]
181-
);
167+
for (actual, expected) in subcommands(&cmd).into_iter().zip([
168+
("test", "my-cmd test", None),
169+
("hello", "my-cmd hello", None),
170+
("help", "my-cmd help", Some(HELP)),
171+
]) {
172+
assert_eq!(actual.0, expected.0);
173+
assert_eq!(actual.1, expected.1);
174+
assert_eq!(
175+
actual.2.as_ref().map(ToString::to_string).as_deref(),
176+
expected.2
177+
);
178+
}
182179
}
183180

184181
#[test]
185182
fn test_all_subcommands() {
186183
let cmd = built_with_version();
187184

188-
assert_eq!(
189-
all_subcommands(&cmd),
190-
vec![
191-
("test".to_string(), "my-cmd test".to_string()),
192-
("hello".to_string(), "my-cmd hello".to_string()),
193-
("help".to_string(), "my-cmd help".to_string()),
194-
("config".to_string(), "my-cmd test config".to_string()),
195-
("help".to_string(), "my-cmd test help".to_string()),
196-
("config".to_string(), "my-cmd test help config".to_string()),
197-
("help".to_string(), "my-cmd test help help".to_string()),
198-
("test".to_string(), "my-cmd help test".to_string()),
199-
("hello".to_string(), "my-cmd help hello".to_string()),
200-
("help".to_string(), "my-cmd help help".to_string()),
201-
("config".to_string(), "my-cmd help test config".to_string()),
202-
]
203-
);
185+
for (actual, expected) in subcommands(&cmd).into_iter().zip([
186+
("test", "my-cmd test", None),
187+
("hello", "my-cmd hello", None),
188+
("help", "my-cmd help", Some(HELP)),
189+
("config", "my-cmd test config", None),
190+
("help", "my-cmd test help", Some(HELP)),
191+
("config", "my-cmd test help config", None),
192+
("help", "my-cmd test help help", Some(HELP)),
193+
("test", "my-cmd help test", None),
194+
("hello", "my-cmd help hello", None),
195+
("help", "my-cmd help help", Some(HELP)),
196+
("config", "my-cmd help test config", None),
197+
]) {
198+
assert_eq!(actual.0, expected.0);
199+
assert_eq!(actual.1, expected.1);
200+
assert_eq!(
201+
actual.2.as_ref().map(ToString::to_string).as_deref(),
202+
expected.2
203+
);
204+
}
204205
}
205206

206207
#[test]
@@ -248,15 +249,15 @@ mod tests {
248249
let shorts = shorts_and_visible_aliases(&cmd);
249250

250251
assert_eq!(shorts.len(), 2);
251-
assert_eq!(shorts[0], 'h');
252-
assert_eq!(shorts[1], 'V');
252+
assert_option!(shorts[0], 'h', "Print help");
253+
assert_option!(shorts[1], 'V', "Print version");
253254

254255
let sc_shorts = shorts_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"]));
255256

256257
assert_eq!(sc_shorts.len(), 3);
257-
assert_eq!(sc_shorts[0], 'p');
258-
assert_eq!(sc_shorts[1], 'f');
259-
assert_eq!(sc_shorts[2], 'h');
258+
assert_option!(sc_shorts[0], 'f');
259+
assert_option!(sc_shorts[1], 'p');
260+
assert_option!(sc_shorts[2], 'h', "Print help");
260261
}
261262

262263
#[test]
@@ -265,14 +266,14 @@ mod tests {
265266
let longs = longs_and_visible_aliases(&cmd);
266267

267268
assert_eq!(longs.len(), 2);
268-
assert_eq!(longs[0], "help");
269-
assert_eq!(longs[1], "version");
269+
assert_option!(longs[0], "help", "Print help");
270+
assert_option!(longs[1], "version", "Print version");
270271

271272
let sc_longs = longs_and_visible_aliases(find_subcommand_with_path(&cmd, vec!["test"]));
272273

273274
assert_eq!(sc_longs.len(), 3);
274-
assert_eq!(sc_longs[0], "path");
275-
assert_eq!(sc_longs[1], "file");
276-
assert_eq!(sc_longs[2], "help");
275+
assert_option!(sc_longs[0], "file");
276+
assert_option!(sc_longs[1], "path");
277+
assert_option!(sc_longs[2], "help", "Print help");
277278
}
278279
}

0 commit comments

Comments
 (0)