nu_engine/
documentation.rs

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