nu_cli/menus/
help_completions.rs1use nu_engine::documentation::{HelpStyle, get_flags_section};
2use nu_protocol::{Config, engine::EngineState, levenshtein_distance};
3use nu_utils::IgnoreCaseExt;
4use reedline::{Completer, Suggestion};
5use std::{fmt::Write, sync::Arc};
6
7pub struct NuHelpCompleter {
8 engine_state: Arc<EngineState>,
9 config: Arc<Config>,
10}
11
12impl NuHelpCompleter {
13 pub fn new(engine_state: Arc<EngineState>, config: Arc<Config>) -> Self {
14 Self {
15 engine_state,
16 config,
17 }
18 }
19
20 fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
21 let folded_line = line.to_folded_case();
22
23 let mut help_style = HelpStyle::default();
24 help_style.update_from_config(&self.engine_state, &self.config);
25
26 let mut commands = self
27 .engine_state
28 .get_decls_sorted(false)
29 .into_iter()
30 .filter_map(|(_, decl_id)| {
31 let decl = self.engine_state.get_decl(decl_id);
32 (decl.name().to_folded_case().contains(&folded_line)
33 || decl.description().to_folded_case().contains(&folded_line)
34 || decl
35 .search_terms()
36 .into_iter()
37 .any(|term| term.to_folded_case().contains(&folded_line))
38 || decl
39 .extra_description()
40 .to_folded_case()
41 .contains(&folded_line))
42 .then_some(decl)
43 })
44 .collect::<Vec<_>>();
45
46 commands.sort_by_cached_key(|decl| levenshtein_distance(line, decl.name()));
47
48 commands
49 .into_iter()
50 .map(|decl| {
51 let mut long_desc = String::new();
52
53 let description = decl.description();
54 if !description.is_empty() {
55 long_desc.push_str(description);
56 long_desc.push_str("\r\n\r\n");
57 }
58
59 let extra_desc = decl.extra_description();
60 if !extra_desc.is_empty() {
61 long_desc.push_str(extra_desc);
62 long_desc.push_str("\r\n\r\n");
63 }
64
65 let sig = decl.signature();
66 let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
67
68 if !sig.named.is_empty() {
69 long_desc.push_str(&get_flags_section(&sig, &help_style, |v| {
70 v.to_parsable_string(", ", &self.config)
71 }))
72 }
73
74 if !sig.required_positional.is_empty()
75 || !sig.optional_positional.is_empty()
76 || sig.rest_positional.is_some()
77 {
78 long_desc.push_str("\r\nParameters:\r\n");
79 for positional in &sig.required_positional {
80 let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc);
81 }
82 for positional in &sig.optional_positional {
83 let opt_suffix = if let Some(value) = &positional.default_value {
84 format!(
85 " (optional, default: {})",
86 &value.to_parsable_string(", ", &self.config),
87 )
88 } else {
89 (" (optional)").to_string()
90 };
91 let _ = write!(
92 long_desc,
93 " (optional) {}: {}{}\r\n",
94 positional.name, positional.desc, opt_suffix
95 );
96 }
97
98 if let Some(rest_positional) = &sig.rest_positional {
99 let _ = write!(
100 long_desc,
101 " ...{}: {}\r\n",
102 rest_positional.name, rest_positional.desc
103 );
104 }
105 }
106
107 let extra: Vec<String> = decl
108 .examples()
109 .iter()
110 .map(|example| example.example.replace('\n', "\r\n"))
111 .collect();
112
113 Suggestion {
114 value: decl.name().into(),
115 description: Some(long_desc),
116 extra: Some(extra),
117 span: reedline::Span {
118 start: pos - line.len(),
119 end: pos,
120 },
121 ..Suggestion::default()
122 }
123 })
124 .collect()
125 }
126}
127
128impl Completer for NuHelpCompleter {
129 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
130 self.completion_helper(line, pos)
131 }
132}
133
134#[cfg(test)]
135mod test {
136 use super::*;
137 use rstest::rstest;
138
139 #[rstest]
140 #[case("who", 5, 8, &["whoami"])]
141 #[case("hash", 1, 5, &["hash", "hash md5", "hash sha256"])]
142 #[case("into f", 0, 6, &["into float", "into filesize"])]
143 #[case("into nonexistent", 0, 16, &[])]
144 fn test_help_completer(
145 #[case] line: &str,
146 #[case] start: usize,
147 #[case] end: usize,
148 #[case] expected: &[&str],
149 ) {
150 let engine_state =
151 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
152 let config = engine_state.get_config().clone();
153 let mut completer = NuHelpCompleter::new(engine_state.into(), config);
154 let suggestions = completer.complete(line, end);
155
156 assert_eq!(
157 expected.len(),
158 suggestions.len(),
159 "expected {:?}, got {:?}",
160 expected,
161 suggestions
162 .iter()
163 .map(|s| s.value.clone())
164 .collect::<Vec<_>>()
165 );
166
167 for (exp, actual) in expected.iter().zip(suggestions) {
168 assert_eq!(exp, &actual.value);
169 assert_eq!(reedline::Span::new(start, end), actual.span);
170 }
171 }
172}