Skip to main content

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