nu_cli/menus/
help_completions.rs1use nu_engine::documentation::{FormatterValue, 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| match v {
70 FormatterValue::DefaultValue(value) => {
71 value.to_parsable_string(", ", &self.config)
72 }
73 FormatterValue::CodeString(text) => text.to_string(),
74 }))
75 }
76
77 if !sig.required_positional.is_empty()
78 || !sig.optional_positional.is_empty()
79 || sig.rest_positional.is_some()
80 {
81 long_desc.push_str("\r\nParameters:\r\n");
82 for positional in &sig.required_positional {
83 let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc);
84 }
85 for positional in &sig.optional_positional {
86 let opt_suffix = if let Some(value) = &positional.default_value {
87 format!(
88 " (optional, default: {})",
89 &value.to_parsable_string(", ", &self.config),
90 )
91 } else {
92 (" (optional)").to_string()
93 };
94 let _ = write!(
95 long_desc,
96 " (optional) {}: {}{}\r\n",
97 positional.name, positional.desc, opt_suffix
98 );
99 }
100
101 if let Some(rest_positional) = &sig.rest_positional {
102 let _ = write!(
103 long_desc,
104 " ...{}: {}\r\n",
105 rest_positional.name, rest_positional.desc
106 );
107 }
108 }
109
110 let extra: Vec<String> = decl
111 .examples()
112 .iter()
113 .map(|example| example.example.replace('\n', "\r\n"))
114 .collect();
115
116 Suggestion {
117 value: decl.name().into(),
118 description: Some(long_desc),
119 extra: Some(extra),
120 span: reedline::Span {
121 start: pos - line.len(),
122 end: pos,
123 },
124 ..Suggestion::default()
125 }
126 })
127 .collect()
128 }
129}
130
131impl Completer for NuHelpCompleter {
132 fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
133 self.completion_helper(line, pos)
134 }
135}
136
137#[cfg(test)]
138mod test {
139 use super::*;
140 use rstest::rstest;
141
142 #[rstest]
143 #[case("who", 5, 8, &["whoami", "each"])]
144 #[case("hash", 1, 5, &["hash", "hash md5", "hash sha256"])]
145 #[case("into f", 0, 6, &["into float", "into filesize"])]
146 #[case("into nonexistent", 0, 16, &[])]
147 fn test_help_completer(
148 #[case] line: &str,
149 #[case] start: usize,
150 #[case] end: usize,
151 #[case] expected: &[&str],
152 ) {
153 let engine_state =
154 nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
155 let config = engine_state.get_config().clone();
156 let mut completer = NuHelpCompleter::new(engine_state.into(), config);
157 let suggestions = completer.complete(line, end);
158
159 assert_eq!(
160 expected.len(),
161 suggestions.len(),
162 "expected {:?}, got {:?}",
163 expected,
164 suggestions
165 .iter()
166 .map(|s| s.value.clone())
167 .collect::<Vec<_>>()
168 );
169
170 for (exp, actual) in expected.iter().zip(suggestions) {
171 assert_eq!(exp, &actual.value);
172 assert_eq!(reedline::Span::new(start, end), actual.span);
173 }
174 }
175}