mirror of
https://github.com/nushell/nushell.git
synced 2025-08-01 12:30:33 +02:00
# Description Adds formatting for code in backticks in `help` output. If it's possible to highlight syntax (`nu-highlight` is available and there's no invalid syntax) then it's highlighted. If the syntax is invalid or not an internal command, then it's dimmed and italicized. like some of the output from `std/help`. If `use_ansi_coloring` is `false`, then we leave the backticks alone. Here's a couple examples:   (note on this one: usually we can highlight partial commands, like `get` in the `select` help page which is invalid according to `nu-check` but is still properly highlighted, however `where` is special cased and just typing `where` with no row condition is highlighted with the garbage style so `where` alone isn't highlighted here)  here's the `where` page with `$env.config.use_ansi_coloring = false`:  Technically, some syntax is valid but isn't really "Nushell code". For example, the `select` help page has a line that says "Select just the \`name\` column". If you just type `name` in the REPL, Nushell treats it as an external command, but for the purposes of highlighted we actually want this to fall back to the generic dimmed/italic style. This is accomplished by temporarily setting the `shape_external` and `shape_externalarg` color config to the generic/fallback style, and then restoring the color config after highlighting. This is a bit hack-ish but it seems to work pretty well. # User-Facing Changes - `help` command now supports code backtick formatting. Code will be highlighted using `nu-highlight` if possible, otherwise it will fall back to a generic format. - Adds `--reject-garbage` flag to `nu-highlight` which will return an error on invalid syntax (which would otherwise be highlighted with `$env.config.color_config.shape_garbage`) # Tests + Formatting Added tests for the regex. I don't think tests for the actual highlighting are very necessary since the failure mode is graceful and it would be difficult to meaningfully test. # After Submitting N/A --------- Co-authored-by: Piepmatz <git+github@cptpiepmatz.de>
176 lines
6.2 KiB
Rust
176 lines
6.2 KiB
Rust
use nu_engine::documentation::{FormatterValue, HelpStyle, get_flags_section};
|
|
use nu_protocol::{Config, engine::EngineState, levenshtein_distance};
|
|
use nu_utils::IgnoreCaseExt;
|
|
use reedline::{Completer, Suggestion};
|
|
use std::{fmt::Write, sync::Arc};
|
|
|
|
pub struct NuHelpCompleter {
|
|
engine_state: Arc<EngineState>,
|
|
config: Arc<Config>,
|
|
}
|
|
|
|
impl NuHelpCompleter {
|
|
pub fn new(engine_state: Arc<EngineState>, config: Arc<Config>) -> Self {
|
|
Self {
|
|
engine_state,
|
|
config,
|
|
}
|
|
}
|
|
|
|
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
|
let folded_line = line.to_folded_case();
|
|
|
|
let mut help_style = HelpStyle::default();
|
|
help_style.update_from_config(&self.engine_state, &self.config);
|
|
|
|
let mut commands = self
|
|
.engine_state
|
|
.get_decls_sorted(false)
|
|
.into_iter()
|
|
.filter_map(|(_, decl_id)| {
|
|
let decl = self.engine_state.get_decl(decl_id);
|
|
(decl.name().to_folded_case().contains(&folded_line)
|
|
|| decl.description().to_folded_case().contains(&folded_line)
|
|
|| decl
|
|
.search_terms()
|
|
.into_iter()
|
|
.any(|term| term.to_folded_case().contains(&folded_line))
|
|
|| decl
|
|
.extra_description()
|
|
.to_folded_case()
|
|
.contains(&folded_line))
|
|
.then_some(decl)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
commands.sort_by_cached_key(|decl| levenshtein_distance(line, decl.name()));
|
|
|
|
commands
|
|
.into_iter()
|
|
.map(|decl| {
|
|
let mut long_desc = String::new();
|
|
|
|
let description = decl.description();
|
|
if !description.is_empty() {
|
|
long_desc.push_str(description);
|
|
long_desc.push_str("\r\n\r\n");
|
|
}
|
|
|
|
let extra_desc = decl.extra_description();
|
|
if !extra_desc.is_empty() {
|
|
long_desc.push_str(extra_desc);
|
|
long_desc.push_str("\r\n\r\n");
|
|
}
|
|
|
|
let sig = decl.signature();
|
|
let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
|
|
|
|
if !sig.named.is_empty() {
|
|
long_desc.push_str(&get_flags_section(&sig, &help_style, |v| match v {
|
|
FormatterValue::DefaultValue(value) => {
|
|
value.to_parsable_string(", ", &self.config)
|
|
}
|
|
FormatterValue::CodeString(text) => text.to_string(),
|
|
}))
|
|
}
|
|
|
|
if !sig.required_positional.is_empty()
|
|
|| !sig.optional_positional.is_empty()
|
|
|| sig.rest_positional.is_some()
|
|
{
|
|
long_desc.push_str("\r\nParameters:\r\n");
|
|
for positional in &sig.required_positional {
|
|
let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc);
|
|
}
|
|
for positional in &sig.optional_positional {
|
|
let opt_suffix = if let Some(value) = &positional.default_value {
|
|
format!(
|
|
" (optional, default: {})",
|
|
&value.to_parsable_string(", ", &self.config),
|
|
)
|
|
} else {
|
|
(" (optional)").to_string()
|
|
};
|
|
let _ = write!(
|
|
long_desc,
|
|
" (optional) {}: {}{}\r\n",
|
|
positional.name, positional.desc, opt_suffix
|
|
);
|
|
}
|
|
|
|
if let Some(rest_positional) = &sig.rest_positional {
|
|
let _ = write!(
|
|
long_desc,
|
|
" ...{}: {}\r\n",
|
|
rest_positional.name, rest_positional.desc
|
|
);
|
|
}
|
|
}
|
|
|
|
let extra: Vec<String> = decl
|
|
.examples()
|
|
.iter()
|
|
.map(|example| example.example.replace('\n', "\r\n"))
|
|
.collect();
|
|
|
|
Suggestion {
|
|
value: decl.name().into(),
|
|
description: Some(long_desc),
|
|
extra: Some(extra),
|
|
span: reedline::Span {
|
|
start: pos - line.len(),
|
|
end: pos,
|
|
},
|
|
..Suggestion::default()
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl Completer for NuHelpCompleter {
|
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
|
self.completion_helper(line, pos)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use rstest::rstest;
|
|
|
|
#[rstest]
|
|
#[case("who", 5, 8, &["whoami"])]
|
|
#[case("hash", 1, 5, &["hash", "hash md5", "hash sha256"])]
|
|
#[case("into f", 0, 6, &["into float", "into filesize"])]
|
|
#[case("into nonexistent", 0, 16, &[])]
|
|
fn test_help_completer(
|
|
#[case] line: &str,
|
|
#[case] start: usize,
|
|
#[case] end: usize,
|
|
#[case] expected: &[&str],
|
|
) {
|
|
let engine_state =
|
|
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
|
|
let config = engine_state.get_config().clone();
|
|
let mut completer = NuHelpCompleter::new(engine_state.into(), config);
|
|
let suggestions = completer.complete(line, end);
|
|
|
|
assert_eq!(
|
|
expected.len(),
|
|
suggestions.len(),
|
|
"expected {:?}, got {:?}",
|
|
expected,
|
|
suggestions
|
|
.iter()
|
|
.map(|s| s.value.clone())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
|
|
for (exp, actual) in expected.iter().zip(suggestions) {
|
|
assert_eq!(exp, &actual.value);
|
|
assert_eq!(reedline::Span::new(start, end), actual.span);
|
|
}
|
|
}
|
|
}
|