nu_engine/
documentation.rs

1use crate::eval_call;
2use fancy_regex::{Captures, Regex};
3use nu_protocol::{
4    Category, Config, IntoPipelineData, PipelineData, PositionalArg, Signature, Span, SpanId,
5    Spanned, SyntaxShape, Type, Value,
6    ast::{Argument, Call, Expr, Expression, RecordItem},
7    debugger::WithoutDebug,
8    engine::CommandType,
9    engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID},
10    record,
11};
12use nu_utils::terminal_size;
13use std::{
14    borrow::Cow,
15    collections::HashMap,
16    fmt::Write,
17    sync::{Arc, LazyLock},
18};
19
20/// ANSI style reset
21const RESET: &str = "\x1b[0m";
22/// ANSI set default color (as set in the terminal)
23const DEFAULT_COLOR: &str = "\x1b[39m";
24/// ANSI set default dimmed
25const DEFAULT_DIMMED: &str = "\x1b[2;39m";
26/// ANSI set default italic
27const DEFAULT_ITALIC: &str = "\x1b[3;39m";
28
29pub fn get_full_help(
30    command: &dyn Command,
31    engine_state: &EngineState,
32    stack: &mut Stack,
33) -> String {
34    // Precautionary step to capture any command output generated during this operation. We
35    // internally call several commands (`table`, `ansi`, `nu-highlight`) and get their
36    // `PipelineData` using this `Stack`, any other output should not be redirected like the main
37    // execution.
38    let stack = &mut stack.start_collect_value();
39
40    let nu_config = stack.get_config(engine_state);
41
42    let sig = engine_state
43        .get_signature(command)
44        .update_from_command(command);
45
46    // Create ansi colors
47    let mut help_style = HelpStyle::default();
48    help_style.update_from_config(engine_state, &nu_config);
49
50    let mut long_desc = String::new();
51
52    let desc = &sig.description;
53    if !desc.is_empty() {
54        long_desc.push_str(&highlight_code(desc, engine_state, stack));
55        long_desc.push_str("\n\n");
56    }
57
58    let extra_desc = &sig.extra_description;
59    if !extra_desc.is_empty() {
60        long_desc.push_str(&highlight_code(extra_desc, engine_state, stack));
61        long_desc.push_str("\n\n");
62    }
63
64    match command.command_type() {
65        CommandType::Alias => get_alias_documentation(
66            &mut long_desc,
67            command,
68            &sig,
69            &help_style,
70            engine_state,
71            stack,
72        ),
73        _ => get_command_documentation(
74            &mut long_desc,
75            command,
76            &sig,
77            &nu_config,
78            &help_style,
79            engine_state,
80            stack,
81        ),
82    };
83
84    if !nu_config.use_ansi_coloring.get(engine_state) {
85        nu_utils::strip_ansi_string_likely(long_desc)
86    } else {
87        long_desc
88    }
89}
90
91/// Syntax highlight code using the `nu-highlight` command if available
92fn try_nu_highlight(
93    code_string: &str,
94    reject_garbage: bool,
95    engine_state: &EngineState,
96    stack: &mut Stack,
97) -> Option<String> {
98    let highlighter = engine_state.find_decl(b"nu-highlight", &[])?;
99
100    let decl = engine_state.get_decl(highlighter);
101    let mut call = Call::new(Span::unknown());
102    if reject_garbage {
103        call.add_named((
104            Spanned {
105                item: "reject-garbage".into(),
106                span: Span::unknown(),
107            },
108            None,
109            None,
110        ));
111    }
112
113    decl.run(
114        engine_state,
115        stack,
116        &(&call).into(),
117        Value::string(code_string, Span::unknown()).into_pipeline_data(),
118    )
119    .and_then(|pipe| pipe.into_value(Span::unknown()))
120    .and_then(|val| val.coerce_into_string())
121    .ok()
122}
123
124/// Syntax highlight code using the `nu-highlight` command if available, falling back to the given string
125fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String {
126    try_nu_highlight(code_string, false, engine_state, stack)
127        .unwrap_or_else(|| code_string.to_string())
128}
129
130/// Apply code highlighting to code in a capture group
131fn highlight_capture_group(
132    captures: &Captures,
133    engine_state: &EngineState,
134    stack: &mut Stack,
135) -> String {
136    let Some(content) = captures.get(1) else {
137        // this shouldn't happen
138        return String::new();
139    };
140
141    // Save current color config
142    let config_old = stack.get_config(engine_state);
143    let mut config = (*config_old).clone();
144
145    // Style externals and external arguments with fallback style,
146    // so nu-highlight styles code which is technically valid syntax,
147    // but not an internal command is highlighted with the fallback style
148    let code_style = Value::record(
149        record! {
150            "attr" => Value::string("di", Span::unknown()),
151        },
152        Span::unknown(),
153    );
154    let color_config = &mut config.color_config;
155    color_config.insert("shape_external".into(), code_style.clone());
156    color_config.insert("shape_external_resolved".into(), code_style.clone());
157    color_config.insert("shape_externalarg".into(), code_style);
158
159    // Apply config with external argument style
160    stack.config = Some(Arc::new(config));
161
162    // Highlight and reject invalid syntax
163    let highlighted = try_nu_highlight(content.into(), true, engine_state, stack)
164        // // Make highlighted string italic
165        .map(|text| {
166            let resets = text.match_indices(RESET).count();
167            // replace resets with reset + italic, so the whole string is italicized, excluding the final reset
168            let text = text.replacen(RESET, &format!("{RESET}{DEFAULT_ITALIC}"), resets - 1);
169            // start italicized
170            format!("{DEFAULT_ITALIC}{text}")
171        });
172
173    // Restore original config
174    stack.config = Some(config_old);
175
176    // Use fallback style if highlight failed/syntax was invalid
177    highlighted.unwrap_or_else(|| highlight_fallback(content.into()))
178}
179
180/// Apply fallback code style
181fn highlight_fallback(text: &str) -> String {
182    format!("{DEFAULT_DIMMED}{DEFAULT_ITALIC}{text}{RESET}")
183}
184
185/// Highlight code within backticks
186///
187/// Will attempt to use nu-highlight, falling back to dimmed and italic on invalid syntax
188fn highlight_code<'a>(
189    text: &'a str,
190    engine_state: &EngineState,
191    stack: &mut Stack,
192) -> Cow<'a, str> {
193    let config = stack.get_config(engine_state);
194    if !config.use_ansi_coloring.get(engine_state) {
195        return Cow::Borrowed(text);
196    }
197
198    // See [`tests::test_code_formatting`] for examples
199    static PATTERN: &str = r"(?x)     # verbose mode
200        (?<![\p{Letter}\d])    # negative look-behind for alphanumeric: ensure backticks are not directly preceded by letter/number.
201        `
202        ([^`\n]+?)           # capture characters inside backticks, excluding backticks and newlines. ungreedy.
203        `
204        (?![\p{Letter}\d])     # negative look-ahead for alphanumeric: ensure backticks are not directly followed by letter/number.
205    ";
206    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(PATTERN).expect("valid regex"));
207
208    let do_try_highlight =
209        |captures: &Captures| highlight_capture_group(captures, engine_state, stack);
210    RE.replace_all(text, do_try_highlight)
211}
212
213fn get_alias_documentation(
214    long_desc: &mut String,
215    command: &dyn Command,
216    sig: &Signature,
217    help_style: &HelpStyle,
218    engine_state: &EngineState,
219    stack: &mut Stack,
220) {
221    let help_section_name = &help_style.section_name;
222    let help_subcolor_one = &help_style.subcolor_one;
223
224    let alias_name = &sig.name;
225
226    long_desc.push_str(&format!(
227        "{help_section_name}Alias{RESET}: {help_subcolor_one}{alias_name}{RESET}"
228    ));
229    long_desc.push_str("\n\n");
230
231    let Some(alias) = command.as_alias() else {
232        // this is already checked in `help alias`, but just omit the expansion if this is somehow not actually an alias
233        return;
234    };
235
236    let alias_expansion =
237        String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span));
238
239    long_desc.push_str(&format!(
240        "{help_section_name}Expansion{RESET}:\n  {}",
241        nu_highlight_string(&alias_expansion, engine_state, stack)
242    ));
243}
244
245fn get_command_documentation(
246    long_desc: &mut String,
247    command: &dyn Command,
248    sig: &Signature,
249    nu_config: &Config,
250    help_style: &HelpStyle,
251    engine_state: &EngineState,
252    stack: &mut Stack,
253) {
254    let help_section_name = &help_style.section_name;
255    let help_subcolor_one = &help_style.subcolor_one;
256
257    let cmd_name = &sig.name;
258
259    if !sig.search_terms.is_empty() {
260        let _ = write!(
261            long_desc,
262            "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n",
263            sig.search_terms.join(", "),
264        );
265    }
266
267    let _ = write!(
268        long_desc,
269        "{help_section_name}Usage{RESET}:\n  > {}\n",
270        sig.call_signature()
271    );
272
273    // TODO: improve the subcommand name resolution
274    // issues:
275    // - Aliases are included
276    //   - https://github.com/nushell/nushell/issues/11657
277    // - Subcommands are included violating module scoping
278    //   - https://github.com/nushell/nushell/issues/11447
279    //   - https://github.com/nushell/nushell/issues/11625
280    let mut subcommands = vec![];
281    let signatures = engine_state.get_signatures_and_declids(true);
282    for (sig, decl_id) in signatures {
283        let command_type = engine_state.get_decl(decl_id).command_type();
284
285        // Don't display removed/deprecated commands in the Subcommands list
286        if sig.name.starts_with(&format!("{cmd_name} "))
287            && !matches!(sig.category, Category::Removed)
288        {
289            // If it's a plugin, alias, or custom command, display that information in the help
290            if command_type == CommandType::Plugin
291                || command_type == CommandType::Alias
292                || command_type == CommandType::Custom
293            {
294                subcommands.push(format!(
295                    "  {help_subcolor_one}{} {help_section_name}({}){RESET} - {}",
296                    sig.name,
297                    command_type,
298                    highlight_code(&sig.description, engine_state, stack)
299                ));
300            } else {
301                subcommands.push(format!(
302                    "  {help_subcolor_one}{}{RESET} - {}",
303                    sig.name,
304                    highlight_code(&sig.description, engine_state, stack)
305                ));
306            }
307        }
308    }
309
310    if !subcommands.is_empty() {
311        let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n");
312        subcommands.sort();
313        long_desc.push_str(&subcommands.join("\n"));
314        long_desc.push('\n');
315    }
316
317    if !sig.named.is_empty() {
318        long_desc.push_str(&get_flags_section(sig, help_style, |v| match v {
319            FormatterValue::DefaultValue(value) => nu_highlight_string(
320                &value.to_parsable_string(", ", nu_config),
321                engine_state,
322                stack,
323            ),
324            FormatterValue::CodeString(text) => {
325                highlight_code(text, engine_state, stack).to_string()
326            }
327        }))
328    }
329
330    if !sig.required_positional.is_empty()
331        || !sig.optional_positional.is_empty()
332        || sig.rest_positional.is_some()
333    {
334        let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n");
335        for positional in &sig.required_positional {
336            write_positional(
337                long_desc,
338                positional,
339                PositionalKind::Required,
340                help_style,
341                nu_config,
342                engine_state,
343                stack,
344            );
345        }
346        for positional in &sig.optional_positional {
347            write_positional(
348                long_desc,
349                positional,
350                PositionalKind::Optional,
351                help_style,
352                nu_config,
353                engine_state,
354                stack,
355            );
356        }
357
358        if let Some(rest_positional) = &sig.rest_positional {
359            write_positional(
360                long_desc,
361                rest_positional,
362                PositionalKind::Rest,
363                help_style,
364                nu_config,
365                engine_state,
366                stack,
367            );
368        }
369    }
370
371    fn get_term_width() -> usize {
372        if let Ok((w, _h)) = terminal_size() {
373            w as usize
374        } else {
375            80
376        }
377    }
378
379    if !command.is_keyword() && !sig.input_output_types.is_empty() {
380        if let Some(decl_id) = engine_state.find_decl(b"table", &[]) {
381            // FIXME: we may want to make this the span of the help command in the future
382            let span = Span::unknown();
383            let mut vals = vec![];
384            for (input, output) in &sig.input_output_types {
385                vals.push(Value::record(
386                    record! {
387                        "input" => Value::string(input.to_string(), span),
388                        "output" => Value::string(output.to_string(), span),
389                    },
390                    span,
391                ));
392            }
393
394            let caller_stack = &mut Stack::new().collect_value();
395            if let Ok(result) = eval_call::<WithoutDebug>(
396                engine_state,
397                caller_stack,
398                &Call {
399                    decl_id,
400                    head: span,
401                    arguments: vec![Argument::Named((
402                        Spanned {
403                            item: "width".to_string(),
404                            span: Span::unknown(),
405                        },
406                        None,
407                        Some(Expression::new_unknown(
408                            Expr::Int(get_term_width() as i64 - 2), // padding, see below
409                            Span::unknown(),
410                            Type::Int,
411                        )),
412                    ))],
413                    parser_info: HashMap::new(),
414                },
415                PipelineData::value(Value::list(vals, span), None),
416            ) {
417                if let Ok((str, ..)) = result.collect_string_strict(span) {
418                    let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
419                    for line in str.lines() {
420                        let _ = writeln!(long_desc, "  {line}");
421                    }
422                }
423            }
424        }
425    }
426
427    let examples = command.examples();
428
429    if !examples.is_empty() {
430        let _ = write!(long_desc, "\n{help_section_name}Examples{RESET}:");
431    }
432
433    for example in examples {
434        long_desc.push('\n');
435        long_desc.push_str("  ");
436        long_desc.push_str(&highlight_code(example.description, engine_state, stack));
437
438        if !nu_config.use_ansi_coloring.get(engine_state) {
439            let _ = write!(long_desc, "\n  > {}\n", example.example);
440        } else {
441            let code_string = nu_highlight_string(example.example, engine_state, stack);
442            let _ = write!(long_desc, "\n  > {code_string}\n");
443        };
444
445        if let Some(result) = &example.result {
446            let mut table_call = Call::new(Span::unknown());
447            if example.example.ends_with("--collapse") {
448                // collapse the result
449                table_call.add_named((
450                    Spanned {
451                        item: "collapse".to_string(),
452                        span: Span::unknown(),
453                    },
454                    None,
455                    None,
456                ))
457            } else {
458                // expand the result
459                table_call.add_named((
460                    Spanned {
461                        item: "expand".to_string(),
462                        span: Span::unknown(),
463                    },
464                    None,
465                    None,
466                ))
467            }
468            table_call.add_named((
469                Spanned {
470                    item: "width".to_string(),
471                    span: Span::unknown(),
472                },
473                None,
474                Some(Expression::new_unknown(
475                    Expr::Int(get_term_width() as i64 - 2),
476                    Span::unknown(),
477                    Type::Int,
478                )),
479            ));
480
481            let table = engine_state
482                .find_decl("table".as_bytes(), &[])
483                .and_then(|decl_id| {
484                    engine_state
485                        .get_decl(decl_id)
486                        .run(
487                            engine_state,
488                            stack,
489                            &(&table_call).into(),
490                            PipelineData::value(result.clone(), None),
491                        )
492                        .ok()
493                });
494
495            for item in table.into_iter().flatten() {
496                let _ = writeln!(
497                    long_desc,
498                    "  {}",
499                    item.to_expanded_string("", nu_config)
500                        .trim_end()
501                        .trim_start_matches(|c: char| c.is_whitespace() && c != ' ')
502                        .replace('\n', "\n  ")
503                );
504            }
505        }
506    }
507
508    long_desc.push('\n');
509}
510
511fn update_ansi_from_config(
512    ansi_code: &mut String,
513    engine_state: &EngineState,
514    nu_config: &Config,
515    theme_component: &str,
516) {
517    if let Some(color) = &nu_config.color_config.get(theme_component) {
518        let caller_stack = &mut Stack::new().collect_value();
519        let span = Span::unknown();
520        let span_id = UNKNOWN_SPAN_ID;
521
522        let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id);
523
524        // Call ansi command using argument
525        if let Some(argument) = argument_opt {
526            if let Some(decl_id) = engine_state.find_decl(b"ansi", &[]) {
527                if let Ok(result) = eval_call::<WithoutDebug>(
528                    engine_state,
529                    caller_stack,
530                    &Call {
531                        decl_id,
532                        head: span,
533                        arguments: vec![argument],
534                        parser_info: HashMap::new(),
535                    },
536                    PipelineData::empty(),
537                ) {
538                    if let Ok((str, ..)) = result.collect_string_strict(span) {
539                        *ansi_code = str;
540                    }
541                }
542            }
543        }
544    }
545}
546
547fn get_argument_for_color_value(
548    nu_config: &Config,
549    color: &Value,
550    span: Span,
551    span_id: SpanId,
552) -> Option<Argument> {
553    match color {
554        Value::Record { val, .. } => {
555            let record_exp: Vec<RecordItem> = (**val)
556                .iter()
557                .map(|(k, v)| {
558                    RecordItem::Pair(
559                        Expression::new_existing(
560                            Expr::String(k.clone()),
561                            span,
562                            span_id,
563                            Type::String,
564                        ),
565                        Expression::new_existing(
566                            Expr::String(v.clone().to_expanded_string("", nu_config)),
567                            span,
568                            span_id,
569                            Type::String,
570                        ),
571                    )
572                })
573                .collect();
574
575            Some(Argument::Positional(Expression::new_existing(
576                Expr::Record(record_exp),
577                Span::unknown(),
578                UNKNOWN_SPAN_ID,
579                Type::Record(
580                    [
581                        ("fg".to_string(), Type::String),
582                        ("attr".to_string(), Type::String),
583                    ]
584                    .into(),
585                ),
586            )))
587        }
588        Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
589            Expr::String(val.clone()),
590            Span::unknown(),
591            UNKNOWN_SPAN_ID,
592            Type::String,
593        ))),
594        _ => None,
595    }
596}
597
598/// Contains the settings for ANSI colors in help output
599///
600/// By default contains a fixed set of (4-bit) colors
601///
602/// Can reflect configuration using [`HelpStyle::update_from_config`]
603pub struct HelpStyle {
604    section_name: String,
605    subcolor_one: String,
606    subcolor_two: String,
607}
608
609impl Default for HelpStyle {
610    fn default() -> Self {
611        HelpStyle {
612            // default: green
613            section_name: "\x1b[32m".to_string(),
614            // default: cyan
615            subcolor_one: "\x1b[36m".to_string(),
616            // default: light blue
617            subcolor_two: "\x1b[94m".to_string(),
618        }
619    }
620}
621
622impl HelpStyle {
623    /// Pull colors from the [`Config`]
624    ///
625    /// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme.
626    ///
627    /// Implementation detail: currently executes `ansi` command internally thus requiring the
628    /// [`EngineState`] for execution.
629    /// See <https://github.com/nushell/nushell/pull/10623> for details
630    pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
631        update_ansi_from_config(
632            &mut self.section_name,
633            engine_state,
634            nu_config,
635            "shape_string",
636        );
637        update_ansi_from_config(
638            &mut self.subcolor_one,
639            engine_state,
640            nu_config,
641            "shape_external",
642        );
643        update_ansi_from_config(
644            &mut self.subcolor_two,
645            engine_state,
646            nu_config,
647            "shape_block",
648        );
649    }
650}
651
652#[derive(PartialEq)]
653enum PositionalKind {
654    Required,
655    Optional,
656    Rest,
657}
658
659fn write_positional(
660    long_desc: &mut String,
661    positional: &PositionalArg,
662    arg_kind: PositionalKind,
663    help_style: &HelpStyle,
664    nu_config: &Config,
665    engine_state: &EngineState,
666    stack: &mut Stack,
667) {
668    let help_subcolor_one = &help_style.subcolor_one;
669    let help_subcolor_two = &help_style.subcolor_two;
670
671    // Indentation
672    long_desc.push_str("  ");
673    if arg_kind == PositionalKind::Rest {
674        long_desc.push_str("...");
675    }
676    match &positional.shape {
677        SyntaxShape::Keyword(kw, shape) => {
678            let _ = write!(
679                long_desc,
680                "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
681                String::from_utf8_lossy(kw),
682                shape,
683            );
684        }
685        _ => {
686            let _ = write!(
687                long_desc,
688                "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
689                positional.name, &positional.shape,
690            );
691        }
692    };
693    if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
694        let _ = write!(
695            long_desc,
696            ": {}",
697            highlight_code(&positional.desc, engine_state, stack)
698        );
699    }
700    if arg_kind == PositionalKind::Optional {
701        if let Some(value) = &positional.default_value {
702            let _ = write!(
703                long_desc,
704                " (optional, default: {})",
705                nu_highlight_string(
706                    &value.to_parsable_string(", ", nu_config),
707                    engine_state,
708                    stack
709                )
710            );
711        } else {
712            long_desc.push_str(" (optional)");
713        };
714    }
715    long_desc.push('\n');
716}
717
718/// Helper for `get_flags_section`
719///
720/// The formatter with access to nu-highlight must be passed to `get_flags_section`, but it's not possible
721/// to pass separate closures since they both need `&mut Stack`, so this enum lets us differentiate between
722/// default values to be formatted and strings which might contain code in backticks to be highlighted.
723pub enum FormatterValue<'a> {
724    /// Default value to be styled
725    DefaultValue(&'a Value),
726    /// String which might have code in backticks to be highlighted
727    CodeString(&'a str),
728}
729
730fn write_flag_to_long_desc<F>(
731    flag: &nu_protocol::Flag,
732    long_desc: &mut String,
733    help_subcolor_one: &str,
734    help_subcolor_two: &str,
735    formatter: &mut F,
736) where
737    F: FnMut(FormatterValue) -> String,
738{
739    // Indentation
740    long_desc.push_str("  ");
741    // Short flag shown before long flag
742    if let Some(short) = flag.short {
743        let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
744        if !flag.long.is_empty() {
745            let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
746        }
747    }
748    if !flag.long.is_empty() {
749        let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
750    }
751    if flag.required {
752        long_desc.push_str(" (required parameter)")
753    }
754    // Type/Syntax shape info
755    if let Some(arg) = &flag.arg {
756        let _ = write!(long_desc, " <{help_subcolor_two}{arg}{RESET}>");
757    }
758    if !flag.desc.is_empty() {
759        let _ = write!(
760            long_desc,
761            ": {}",
762            &formatter(FormatterValue::CodeString(&flag.desc))
763        );
764    }
765    if let Some(value) = &flag.default_value {
766        let _ = write!(
767            long_desc,
768            " (default: {})",
769            &formatter(FormatterValue::DefaultValue(value))
770        );
771    }
772    long_desc.push('\n');
773}
774
775pub fn get_flags_section<F>(
776    signature: &Signature,
777    help_style: &HelpStyle,
778    mut formatter: F, // format default Value or text with code (because some calls cant access config or nu-highlight)
779) -> String
780where
781    F: FnMut(FormatterValue) -> String,
782{
783    let help_section_name = &help_style.section_name;
784    let help_subcolor_one = &help_style.subcolor_one;
785    let help_subcolor_two = &help_style.subcolor_two;
786
787    let mut long_desc = String::new();
788    let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
789
790    let help = signature.named.iter().find(|flag| flag.long == "help");
791    let required = signature.named.iter().filter(|flag| flag.required);
792    let optional = signature
793        .named
794        .iter()
795        .filter(|flag| !flag.required && flag.long != "help");
796
797    let flags = required.chain(help).chain(optional);
798
799    for flag in flags {
800        write_flag_to_long_desc(
801            flag,
802            &mut long_desc,
803            help_subcolor_one,
804            help_subcolor_two,
805            &mut formatter,
806        );
807    }
808
809    long_desc
810}
811
812#[cfg(test)]
813mod tests {
814    use nu_protocol::UseAnsiColoring;
815
816    use super::*;
817
818    #[test]
819    fn test_code_formatting() {
820        let mut engine_state = EngineState::new();
821        let mut stack = Stack::new();
822
823        // force coloring on for test
824        let mut config = (*engine_state.config).clone();
825        config.use_ansi_coloring = UseAnsiColoring::True;
826        engine_state.config = Arc::new(config);
827
828        // using Cow::Owned here to mean a match, since the content changed,
829        // and borrowed to mean not a match, since the content didn't change
830
831        // match: typical example
832        let haystack = "Run the `foo` command";
833        assert!(matches!(
834            highlight_code(haystack, &engine_state, &mut stack),
835            Cow::Owned(_)
836        ));
837
838        // no match: backticks preceded by alphanum
839        let haystack = "foo`bar`";
840        assert!(matches!(
841            highlight_code(haystack, &engine_state, &mut stack),
842            Cow::Borrowed(_)
843        ));
844
845        // match: command at beginning of string is ok
846        let haystack = "`my-command` is cool";
847        assert!(matches!(
848            highlight_code(haystack, &engine_state, &mut stack),
849            Cow::Owned(_)
850        ));
851
852        // match: preceded and followed by newline is ok
853        let haystack = r"
854        `command`
855        ";
856        assert!(matches!(
857            highlight_code(haystack, &engine_state, &mut stack),
858            Cow::Owned(_)
859        ));
860
861        // no match: newline between backticks
862        let haystack = "// hello `beautiful \n world`";
863        assert!(matches!(
864            highlight_code(haystack, &engine_state, &mut stack),
865            Cow::Borrowed(_)
866        ));
867
868        // match: backticks followed by period, not letter/number
869        let haystack = "try running `my cool command`.";
870        assert!(matches!(
871            highlight_code(haystack, &engine_state, &mut stack),
872            Cow::Owned(_)
873        ));
874
875        // match: backticks enclosed by parenthesis, not letter/number
876        let haystack = "a command (`my cool command`).";
877        assert!(matches!(
878            highlight_code(haystack, &engine_state, &mut stack),
879            Cow::Owned(_)
880        ));
881
882        // no match: only characters inside backticks are backticks
883        // (the regex sees two backtick pairs with a single backtick inside, which doesn't qualify)
884        let haystack = "```\ncode block\n```";
885        assert!(matches!(
886            highlight_code(haystack, &engine_state, &mut stack),
887            Cow::Borrowed(_)
888        ));
889    }
890}