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()
380        && !sig.input_output_types.is_empty()
381        && let Some(decl_id) = engine_state.find_decl(b"table", &[])
382    {
383        // FIXME: we may want to make this the span of the help command in the future
384        let span = Span::unknown();
385        let mut vals = vec![];
386        for (input, output) in &sig.input_output_types {
387            vals.push(Value::record(
388                record! {
389                    "input" => Value::string(input.to_string(), span),
390                    "output" => Value::string(output.to_string(), span),
391                },
392                span,
393            ));
394        }
395
396        let caller_stack = &mut Stack::new().collect_value();
397        if let Ok(result) = eval_call::<WithoutDebug>(
398            engine_state,
399            caller_stack,
400            &Call {
401                decl_id,
402                head: span,
403                arguments: vec![Argument::Named((
404                    Spanned {
405                        item: "width".to_string(),
406                        span: Span::unknown(),
407                    },
408                    None,
409                    Some(Expression::new_unknown(
410                        Expr::Int(get_term_width() as i64 - 2), // padding, see below
411                        Span::unknown(),
412                        Type::Int,
413                    )),
414                ))],
415                parser_info: HashMap::new(),
416            },
417            PipelineData::value(Value::list(vals, span), None),
418        ) && let Ok((str, ..)) = result.collect_string_strict(span)
419        {
420            let _ = writeln!(long_desc, "\n{help_section_name}Input/output types{RESET}:");
421            for line in str.lines() {
422                let _ = writeln!(long_desc, "  {line}");
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            && let Some(decl_id) = engine_state.find_decl(b"ansi", &[])
527            && 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            && let Ok((str, ..)) = result.collect_string_strict(span)
539        {
540            *ansi_code = str;
541        }
542    }
543}
544
545fn get_argument_for_color_value(
546    nu_config: &Config,
547    color: &Value,
548    span: Span,
549    span_id: SpanId,
550) -> Option<Argument> {
551    match color {
552        Value::Record { val, .. } => {
553            let record_exp: Vec<RecordItem> = (**val)
554                .iter()
555                .map(|(k, v)| {
556                    RecordItem::Pair(
557                        Expression::new_existing(
558                            Expr::String(k.clone()),
559                            span,
560                            span_id,
561                            Type::String,
562                        ),
563                        Expression::new_existing(
564                            Expr::String(v.clone().to_expanded_string("", nu_config)),
565                            span,
566                            span_id,
567                            Type::String,
568                        ),
569                    )
570                })
571                .collect();
572
573            Some(Argument::Positional(Expression::new_existing(
574                Expr::Record(record_exp),
575                Span::unknown(),
576                UNKNOWN_SPAN_ID,
577                Type::Record(
578                    [
579                        ("fg".to_string(), Type::String),
580                        ("attr".to_string(), Type::String),
581                    ]
582                    .into(),
583                ),
584            )))
585        }
586        Value::String { val, .. } => Some(Argument::Positional(Expression::new_existing(
587            Expr::String(val.clone()),
588            Span::unknown(),
589            UNKNOWN_SPAN_ID,
590            Type::String,
591        ))),
592        _ => None,
593    }
594}
595
596/// Contains the settings for ANSI colors in help output
597///
598/// By default contains a fixed set of (4-bit) colors
599///
600/// Can reflect configuration using [`HelpStyle::update_from_config`]
601pub struct HelpStyle {
602    section_name: String,
603    subcolor_one: String,
604    subcolor_two: String,
605}
606
607impl Default for HelpStyle {
608    fn default() -> Self {
609        HelpStyle {
610            // default: green
611            section_name: "\x1b[32m".to_string(),
612            // default: cyan
613            subcolor_one: "\x1b[36m".to_string(),
614            // default: light blue
615            subcolor_two: "\x1b[94m".to_string(),
616        }
617    }
618}
619
620impl HelpStyle {
621    /// Pull colors from the [`Config`]
622    ///
623    /// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme.
624    ///
625    /// Implementation detail: currently executes `ansi` command internally thus requiring the
626    /// [`EngineState`] for execution.
627    /// See <https://github.com/nushell/nushell/pull/10623> for details
628    pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) {
629        update_ansi_from_config(
630            &mut self.section_name,
631            engine_state,
632            nu_config,
633            "shape_string",
634        );
635        update_ansi_from_config(
636            &mut self.subcolor_one,
637            engine_state,
638            nu_config,
639            "shape_external",
640        );
641        update_ansi_from_config(
642            &mut self.subcolor_two,
643            engine_state,
644            nu_config,
645            "shape_block",
646        );
647    }
648}
649
650#[derive(PartialEq)]
651enum PositionalKind {
652    Required,
653    Optional,
654    Rest,
655}
656
657fn write_positional(
658    long_desc: &mut String,
659    positional: &PositionalArg,
660    arg_kind: PositionalKind,
661    help_style: &HelpStyle,
662    nu_config: &Config,
663    engine_state: &EngineState,
664    stack: &mut Stack,
665) {
666    let help_subcolor_one = &help_style.subcolor_one;
667    let help_subcolor_two = &help_style.subcolor_two;
668
669    // Indentation
670    long_desc.push_str("  ");
671    if arg_kind == PositionalKind::Rest {
672        long_desc.push_str("...");
673    }
674    match &positional.shape {
675        SyntaxShape::Keyword(kw, shape) => {
676            let _ = write!(
677                long_desc,
678                "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>",
679                String::from_utf8_lossy(kw),
680                shape,
681            );
682        }
683        _ => {
684            let _ = write!(
685                long_desc,
686                "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>",
687                positional.name, &positional.shape,
688            );
689        }
690    };
691    if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional {
692        let _ = write!(
693            long_desc,
694            ": {}",
695            highlight_code(&positional.desc, engine_state, stack)
696        );
697    }
698    if arg_kind == PositionalKind::Optional {
699        if let Some(value) = &positional.default_value {
700            let _ = write!(
701                long_desc,
702                " (optional, default: {})",
703                nu_highlight_string(
704                    &value.to_parsable_string(", ", nu_config),
705                    engine_state,
706                    stack
707                )
708            );
709        } else {
710            long_desc.push_str(" (optional)");
711        };
712    }
713    long_desc.push('\n');
714}
715
716/// Helper for `get_flags_section`
717///
718/// The formatter with access to nu-highlight must be passed to `get_flags_section`, but it's not possible
719/// to pass separate closures since they both need `&mut Stack`, so this enum lets us differentiate between
720/// default values to be formatted and strings which might contain code in backticks to be highlighted.
721pub enum FormatterValue<'a> {
722    /// Default value to be styled
723    DefaultValue(&'a Value),
724    /// String which might have code in backticks to be highlighted
725    CodeString(&'a str),
726}
727
728fn write_flag_to_long_desc<F>(
729    flag: &nu_protocol::Flag,
730    long_desc: &mut String,
731    help_subcolor_one: &str,
732    help_subcolor_two: &str,
733    formatter: &mut F,
734) where
735    F: FnMut(FormatterValue) -> String,
736{
737    // Indentation
738    long_desc.push_str("  ");
739    // Short flag shown before long flag
740    if let Some(short) = flag.short {
741        let _ = write!(long_desc, "{help_subcolor_one}-{short}{RESET}");
742        if !flag.long.is_empty() {
743            let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} ");
744        }
745    }
746    if !flag.long.is_empty() {
747        let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long);
748    }
749    if flag.required {
750        long_desc.push_str(" (required parameter)")
751    }
752    // Type/Syntax shape info
753    if let Some(arg) = &flag.arg {
754        let _ = write!(long_desc, " <{help_subcolor_two}{arg}{RESET}>");
755    }
756    if !flag.desc.is_empty() {
757        let _ = write!(
758            long_desc,
759            ": {}",
760            &formatter(FormatterValue::CodeString(&flag.desc))
761        );
762    }
763    if let Some(value) = &flag.default_value {
764        let _ = write!(
765            long_desc,
766            " (default: {})",
767            &formatter(FormatterValue::DefaultValue(value))
768        );
769    }
770    long_desc.push('\n');
771}
772
773pub fn get_flags_section<F>(
774    signature: &Signature,
775    help_style: &HelpStyle,
776    mut formatter: F, // format default Value or text with code (because some calls cant access config or nu-highlight)
777) -> String
778where
779    F: FnMut(FormatterValue) -> String,
780{
781    let help_section_name = &help_style.section_name;
782    let help_subcolor_one = &help_style.subcolor_one;
783    let help_subcolor_two = &help_style.subcolor_two;
784
785    let mut long_desc = String::new();
786    let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n");
787
788    let help = signature.named.iter().find(|flag| flag.long == "help");
789    let required = signature.named.iter().filter(|flag| flag.required);
790    let optional = signature
791        .named
792        .iter()
793        .filter(|flag| !flag.required && flag.long != "help");
794
795    let flags = required.chain(help).chain(optional);
796
797    for flag in flags {
798        write_flag_to_long_desc(
799            flag,
800            &mut long_desc,
801            help_subcolor_one,
802            help_subcolor_two,
803            &mut formatter,
804        );
805    }
806
807    long_desc
808}
809
810#[cfg(test)]
811mod tests {
812    use nu_protocol::UseAnsiColoring;
813
814    use super::*;
815
816    #[test]
817    fn test_code_formatting() {
818        let mut engine_state = EngineState::new();
819        let mut stack = Stack::new();
820
821        // force coloring on for test
822        let mut config = (*engine_state.config).clone();
823        config.use_ansi_coloring = UseAnsiColoring::True;
824        engine_state.config = Arc::new(config);
825
826        // using Cow::Owned here to mean a match, since the content changed,
827        // and borrowed to mean not a match, since the content didn't change
828
829        // match: typical example
830        let haystack = "Run the `foo` command";
831        assert!(matches!(
832            highlight_code(haystack, &engine_state, &mut stack),
833            Cow::Owned(_)
834        ));
835
836        // no match: backticks preceded by alphanum
837        let haystack = "foo`bar`";
838        assert!(matches!(
839            highlight_code(haystack, &engine_state, &mut stack),
840            Cow::Borrowed(_)
841        ));
842
843        // match: command at beginning of string is ok
844        let haystack = "`my-command` is cool";
845        assert!(matches!(
846            highlight_code(haystack, &engine_state, &mut stack),
847            Cow::Owned(_)
848        ));
849
850        // match: preceded and followed by newline is ok
851        let haystack = r"
852        `command`
853        ";
854        assert!(matches!(
855            highlight_code(haystack, &engine_state, &mut stack),
856            Cow::Owned(_)
857        ));
858
859        // no match: newline between backticks
860        let haystack = "// hello `beautiful \n world`";
861        assert!(matches!(
862            highlight_code(haystack, &engine_state, &mut stack),
863            Cow::Borrowed(_)
864        ));
865
866        // match: backticks followed by period, not letter/number
867        let haystack = "try running `my cool command`.";
868        assert!(matches!(
869            highlight_code(haystack, &engine_state, &mut stack),
870            Cow::Owned(_)
871        ));
872
873        // match: backticks enclosed by parenthesis, not letter/number
874        let haystack = "a command (`my cool command`).";
875        assert!(matches!(
876            highlight_code(haystack, &engine_state, &mut stack),
877            Cow::Owned(_)
878        ));
879
880        // no match: only characters inside backticks are backticks
881        // (the regex sees two backtick pairs with a single backtick inside, which doesn't qualify)
882        let haystack = "```\ncode block\n```";
883        assert!(matches!(
884            highlight_code(haystack, &engine_state, &mut stack),
885            Cow::Borrowed(_)
886        ));
887    }
888}