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