nu_cli/menus/
help_completions.rs

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