nu_cli/menus/
help_completions.rs

1use 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}