diff --git a/Cargo.lock b/Cargo.lock index 5ca6d3985f..8fad2347e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,7 +605,7 @@ dependencies = [ "encoding_rs", "log", "once_cell", - "quick-xml", + "quick-xml 0.31.0", "serde", "zip", ] @@ -3094,7 +3094,7 @@ dependencies = [ "pretty_assertions", "print-positions", "procfs", - "quick-xml", + "quick-xml 0.31.0", "quickcheck", "quickcheck_macros", "rand", @@ -3476,12 +3476,14 @@ dependencies = [ name = "nu_plugin_formats" version = "0.96.2" dependencies = [ + "chrono", "eml-parser", "ical", "indexmap", "nu-plugin", "nu-plugin-test-support", "nu-protocol", + "plist", "rust-ini", ] @@ -4158,6 +4160,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.32.0", + "serde", + "time", +] + [[package]] name = "polars" version = "0.41.2" @@ -4826,6 +4841,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quickcheck" version = "1.0.3" @@ -5016,8 +5040,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f8c676a3f3814a23c6a0fc9dff6b6c35b2e04df8134aae6f3929cc34de21a53" +source = "git+https://github.com/nushell/reedline?branch=main#919292e40fd417e3da882692021961b444150c59" dependencies = [ "arboard", "chrono", @@ -6883,7 +6906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.31.0", "quote", ] diff --git a/Cargo.toml b/Cargo.toml index c9281927c2..17762b31fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -306,8 +306,8 @@ bench = false # To use a development version of a dependency please use a global override here # changing versions in each sub-crate of the workspace is tedious -# [patch.crates-io] -# reedline = { git = "https://github.com/nushell/reedline", branch = "main" } +[patch.crates-io] +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # Run all benchmarks with `cargo bench` diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs index f594e3d667..f8107f8ed7 100644 --- a/crates/nu-cli/src/completions/command_completions.rs +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -1,5 +1,5 @@ use crate::{ - completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy}, + completions::{Completer, CompletionOptions, MatchAlgorithm}, SuggestionKind, }; use nu_parser::FlatShape; @@ -193,11 +193,7 @@ impl Completer for CommandCompletion { }; if !subcommands.is_empty() { - return sort_suggestions( - &String::from_utf8_lossy(&prefix), - subcommands, - SortBy::LevenshteinDistance, - ); + return sort_suggestions(&String::from_utf8_lossy(&prefix), subcommands, options); } let config = working_set.get_config(); @@ -222,11 +218,7 @@ impl Completer for CommandCompletion { vec![] }; - sort_suggestions( - &String::from_utf8_lossy(&prefix), - commands, - SortBy::LevenshteinDistance, - ) + sort_suggestions(&String::from_utf8_lossy(&prefix), commands, options) } } diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index e794354f09..91fc1132a5 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -48,6 +48,7 @@ impl NuCompleter { let options = CompletionOptions { case_sensitive: config.case_sensitive_completions, match_algorithm: config.completion_algorithm.into(), + sort: config.completion_sort, ..Default::default() }; diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index 5469ba1326..a5d07be57e 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -2,17 +2,18 @@ use crate::{ completions::{matches, CompletionOptions}, SemanticSuggestion, }; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use nu_ansi_term::Style; use nu_engine::env_to_string; use nu_path::{expand_to_real_path, home_dir}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - levenshtein_distance, Span, + CompletionSort, Span, }; use nu_utils::get_ls_colors; use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; -use super::SortBy; +use super::MatchAlgorithm; #[derive(Clone, Default)] pub struct PathBuiltFromString { @@ -20,12 +21,21 @@ pub struct PathBuiltFromString { isdir: bool, } -fn complete_rec( +/// Recursively goes through paths that match a given `partial`. +/// built: State struct for a valid matching path built so far. +/// +/// `isdir`: whether the current partial path has a trailing slash. +/// Parsing a path string into a pathbuf loses that bit of information. +/// +/// want_directory: Whether we want only directories as completion matches. +/// Some commands like `cd` can only be run on directories whereas others +/// like `ls` can be run on regular files as well. +pub fn complete_rec( partial: &[&str], built: &PathBuiltFromString, cwd: &Path, options: &CompletionOptions, - dir: bool, + want_directory: bool, isdir: bool, ) -> Vec { let mut completions = vec![]; @@ -35,7 +45,7 @@ fn complete_rec( let mut built = built.clone(); built.parts.push(base.to_string()); built.isdir = true; - return complete_rec(rest, &built, cwd, options, dir, isdir); + return complete_rec(rest, &built, cwd, options, want_directory, isdir); } } @@ -56,24 +66,41 @@ fn complete_rec( built.parts.push(entry_name.clone()); built.isdir = entry_isdir; - if !dir || entry_isdir { + if !want_directory || entry_isdir { entries.push((entry_name, built)); } } let prefix = partial.first().unwrap_or(&""); - let sorted_entries = sort_completions(prefix, entries, SortBy::Ascending, |(entry, _)| entry); + let sorted_entries = sort_completions(prefix, entries, options, |(entry, _)| entry); for (entry_name, built) in sorted_entries { match partial.split_first() { Some((base, rest)) => { if matches(base, &entry_name, options) { + // We use `isdir` to confirm that the current component has + // at least one next component or a slash. + // Serves as confirmation to ignore longer completions for + // components in between. if !rest.is_empty() || isdir { - completions.extend(complete_rec(rest, &built, cwd, options, dir, isdir)); + completions.extend(complete_rec( + rest, + &built, + cwd, + options, + want_directory, + isdir, + )); } else { completions.push(built); } } + if entry_name.eq(base) + && matches!(options.match_algorithm, MatchAlgorithm::Prefix) + && isdir + { + break; + } } None => { completions.push(built); @@ -279,33 +306,37 @@ pub fn adjust_if_intermediate( pub fn sort_suggestions( prefix: &str, items: Vec, - sort_by: SortBy, + options: &CompletionOptions, ) -> Vec { - sort_completions(prefix, items, sort_by, |it| &it.suggestion.value) + sort_completions(prefix, items, options, |it| &it.suggestion.value) } /// # Arguments -/// * `prefix` - What the user's typed, for sorting by Levenshtein distance +/// * `prefix` - What the user's typed, for sorting by fuzzy matcher score pub fn sort_completions( prefix: &str, mut items: Vec, - sort_by: SortBy, + options: &CompletionOptions, get_value: fn(&T) -> &str, ) -> Vec { // Sort items - match sort_by { - SortBy::LevenshteinDistance => { - items.sort_by(|a, b| { - let a_distance = levenshtein_distance(prefix, get_value(a)); - let b_distance = levenshtein_distance(prefix, get_value(b)); - a_distance.cmp(&b_distance) - }); - } - SortBy::Ascending => { - items.sort_by(|a, b| get_value(a).cmp(get_value(b))); - } - SortBy::None => {} - }; + if options.sort == CompletionSort::Smart && options.match_algorithm == MatchAlgorithm::Fuzzy { + let mut matcher = SkimMatcherV2::default(); + if options.case_sensitive { + matcher = matcher.respect_case(); + } else { + matcher = matcher.ignore_case(); + }; + items.sort_by(|a, b| { + let a_str = get_value(a); + let b_str = get_value(b); + let a_score = matcher.fuzzy_match(a_str, prefix).unwrap_or_default(); + let b_score = matcher.fuzzy_match(b_str, prefix).unwrap_or_default(); + b_score.cmp(&a_score).then(a_str.cmp(b_str)) + }); + } else { + items.sort_by(|a, b| get_value(a).cmp(get_value(b))); + } items } diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index a414aafedf..880aa54fb1 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -1,17 +1,10 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use nu_parser::trim_quotes_str; -use nu_protocol::CompletionAlgorithm; +use nu_protocol::{CompletionAlgorithm, CompletionSort}; use std::fmt::Display; -#[derive(Copy, Clone)] -pub enum SortBy { - LevenshteinDistance, - Ascending, - None, -} - /// Describes how suggestions should be matched. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum MatchAlgorithm { /// Only show suggestions which begin with the given input /// @@ -96,6 +89,7 @@ pub struct CompletionOptions { pub case_sensitive: bool, pub positional: bool, pub match_algorithm: MatchAlgorithm, + pub sort: CompletionSort, } impl Default for CompletionOptions { @@ -104,6 +98,7 @@ impl Default for CompletionOptions { case_sensitive: true, positional: true, match_algorithm: MatchAlgorithm::Prefix, + sort: Default::default(), } } } diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index 0d2c674ef9..f21f942501 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -1,13 +1,13 @@ use crate::completions::{ completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm, - SemanticSuggestion, SortBy, + SemanticSuggestion, }; use nu_engine::eval_call; use nu_protocol::{ ast::{Argument, Call, Expr, Expression}, debugger::WithoutDebug, engine::{Stack, StateWorkingSet}, - PipelineData, Span, Type, Value, + CompletionSort, PipelineData, Span, Type, Value, }; use nu_utils::IgnoreCaseExt; use std::collections::HashMap; @@ -18,7 +18,6 @@ pub struct CustomCompletion { stack: Stack, decl_id: usize, line: String, - sort_by: SortBy, } impl CustomCompletion { @@ -27,7 +26,6 @@ impl CustomCompletion { stack, decl_id, line, - sort_by: SortBy::None, } } } @@ -93,10 +91,6 @@ impl Completer for CustomCompletion { .and_then(|val| val.as_bool().ok()) .unwrap_or(false); - if should_sort { - self.sort_by = SortBy::Ascending; - } - custom_completion_options = Some(CompletionOptions { case_sensitive: options .get("case_sensitive") @@ -114,6 +108,11 @@ impl Completer for CustomCompletion { .unwrap_or(MatchAlgorithm::Prefix), None => completion_options.match_algorithm, }, + sort: if should_sort { + CompletionSort::Alphabetical + } else { + CompletionSort::Smart + }, }); } @@ -124,12 +123,11 @@ impl Completer for CustomCompletion { }) .unwrap_or_default(); - let suggestions = if let Some(custom_completion_options) = custom_completion_options { - filter(&prefix, suggestions, &custom_completion_options) - } else { - filter(&prefix, suggestions, completion_options) - }; - sort_suggestions(&String::from_utf8_lossy(&prefix), suggestions, self.sort_by) + let options = custom_completion_options + .as_ref() + .unwrap_or(completion_options); + let suggestions = filter(&prefix, suggestions, completion_options); + sort_suggestions(&String::from_utf8_lossy(&prefix), suggestions, options) } } diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index 6b4be16769..4a241a57b9 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -6,7 +6,7 @@ use nu_protocol::{ use reedline::Suggestion; use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}; -use super::{completion_common::sort_suggestions, SemanticSuggestion, SortBy}; +use super::{completion_common::sort_suggestions, SemanticSuggestion}; #[derive(Clone, Default)] pub struct DotNuCompletion {} @@ -130,6 +130,6 @@ impl Completer for DotNuCompletion { }) .collect(); - sort_suggestions(&prefix_str, output, SortBy::Ascending) + sort_suggestions(&prefix_str, output, options) } } diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index c4ccfd6048..590929d5a0 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -1,6 +1,4 @@ -use crate::completions::{ - completion_common::sort_suggestions, Completer, CompletionOptions, SortBy, -}; +use crate::completions::{completion_common::sort_suggestions, Completer, CompletionOptions}; use nu_protocol::{ ast::{Expr, Expression}, engine::{Stack, StateWorkingSet}, @@ -90,7 +88,7 @@ impl Completer for FlagCompletion { } } - return sort_suggestions(&String::from_utf8_lossy(&prefix), output, SortBy::Ascending); + return sort_suggestions(&String::from_utf8_lossy(&prefix), output, options); } vec![] diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 20a61ee6d9..ed1510cdfb 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -13,7 +13,7 @@ mod variable_completions; pub use base::{Completer, SemanticSuggestion, SuggestionKind}; pub use command_completions::CommandCompletion; pub use completer::NuCompleter; -pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy}; +pub use completion_options::{CompletionOptions, MatchAlgorithm}; pub use custom_completions::CustomCompletion; pub use directory_completions::DirectoryCompletion; pub use dotnu_completions::DotNuCompletion; diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index 418dcec064..5e8f117810 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -9,7 +9,7 @@ use nu_protocol::{ use reedline::Suggestion; use std::str; -use super::{completion_common::sort_suggestions, SortBy}; +use super::completion_common::sort_suggestions; #[derive(Clone)] pub struct VariableCompletion { @@ -72,7 +72,7 @@ impl Completer for VariableCompletion { } } - return sort_suggestions(&prefix_str, output, SortBy::Ascending); + return sort_suggestions(&prefix_str, output, options); } } else { // No nesting provided, return all env vars @@ -93,7 +93,7 @@ impl Completer for VariableCompletion { } } - return sort_suggestions(&prefix_str, output, SortBy::Ascending); + return sort_suggestions(&prefix_str, output, options); } } @@ -117,7 +117,7 @@ impl Completer for VariableCompletion { } } - return sort_suggestions(&prefix_str, output, SortBy::Ascending); + return sort_suggestions(&prefix_str, output, options); } } @@ -139,7 +139,7 @@ impl Completer for VariableCompletion { } } - return sort_suggestions(&prefix_str, output, SortBy::Ascending); + return sort_suggestions(&prefix_str, output, options); } } } @@ -217,7 +217,7 @@ impl Completer for VariableCompletion { } } - output = sort_suggestions(&prefix_str, output, SortBy::Ascending); + output = sort_suggestions(&prefix_str, output, options); output.dedup(); // TODO: Removes only consecutive duplicates, is it intended? diff --git a/crates/nu-cli/src/menus/help_completions.rs b/crates/nu-cli/src/menus/help_completions.rs index 6d9cbb4cb5..7dc157748e 100644 --- a/crates/nu-cli/src/menus/help_completions.rs +++ b/crates/nu-cli/src/menus/help_completions.rs @@ -1,4 +1,4 @@ -use nu_engine::documentation::get_flags_section; +use nu_engine::documentation::{get_flags_section, HelpStyle}; use nu_protocol::{engine::EngineState, levenshtein_distance, Config}; use nu_utils::IgnoreCaseExt; use reedline::{Completer, Suggestion}; @@ -20,6 +20,9 @@ impl NuHelpCompleter { fn completion_helper(&self, line: &str, pos: usize) -> Vec { 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) @@ -60,12 +63,9 @@ impl NuHelpCompleter { let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature()); if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section( - Some(&self.engine_state), - Some(&self.config), - &sig, - |v| v.to_parsable_string(", ", &self.config), - )) + long_desc.push_str(&get_flags_section(&sig, &help_style, |v| { + v.to_parsable_string(", ", &self.config) + })) } if !sig.required_positional.is_empty() diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index ad5dcaa70e..e1714f78e2 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -89,14 +89,12 @@ fn subcommand_completer() -> NuCompleter { // Create a new engine let (dir, _, mut engine, mut stack) = new_engine(); - // Use fuzzy matching, because subcommands are sorted by Levenshtein distance, - // and that's not very useful with prefix matching let commands = r#" $env.config.completions.algorithm = "fuzzy" def foo [] {} def "foo bar" [] {} def "foo abaz" [] {} - def "foo aabrr" [] {} + def "foo aabcrr" [] {} def food [] {} "#; assert!(support::merge_input(commands.as_bytes(), &mut engine, &mut stack, dir).is_ok()); @@ -105,6 +103,22 @@ fn subcommand_completer() -> NuCompleter { NuCompleter::new(Arc::new(engine), Arc::new(stack)) } +/// Use fuzzy completions but sort in alphabetical order +#[fixture] +fn fuzzy_alpha_sort_completer() -> NuCompleter { + // Create a new engine + let (dir, _, mut engine, mut stack) = new_engine(); + + let config = r#" + $env.config.completions.algorithm = "fuzzy" + $env.config.completions.sort = "alphabetical" + "#; + assert!(support::merge_input(config.as_bytes(), &mut engine, &mut stack, dir).is_ok()); + + // Instantiate a new completer + NuCompleter::new(Arc::new(engine), Arc::new(stack)) +} + #[test] fn variables_dollar_sign_with_variablecompletion() { let (_, _, engine, stack) = new_engine(); @@ -774,7 +788,7 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) { let prefix = "foo br"; let suggestions = subcommand_completer.complete(prefix, prefix.len()); match_suggestions( - &vec!["foo bar".to_string(), "foo aabrr".to_string()], + &vec!["foo bar".to_string(), "foo aabcrr".to_string()], &suggestions, ); @@ -783,8 +797,8 @@ fn subcommand_completions(mut subcommand_completer: NuCompleter) { match_suggestions( &vec![ "foo bar".to_string(), + "foo aabcrr".to_string(), "foo abaz".to_string(), - "foo aabrr".to_string(), ], &suggestions, ); @@ -1270,6 +1284,17 @@ fn custom_completer_triggers_cursor_after_word(mut custom_completer: NuCompleter match_suggestions(&expected, &suggestions); } +#[rstest] +fn sort_fuzzy_completions_in_alphabetical_order(mut fuzzy_alpha_sort_completer: NuCompleter) { + let suggestions = fuzzy_alpha_sort_completer.complete("ls nu", 5); + // Even though "nushell" is a better match, it should come second because + // the completions should be sorted in alphabetical order + match_suggestions( + &vec!["custom_completion.nu".into(), "nushell".into()], + &suggestions, + ); +} + #[ignore = "was reverted, still needs fixing"] #[rstest] fn alias_offset_bug_7648() { diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 8dc0340ba1..9dd6ad9335 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -379,42 +379,47 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { // If input is not a timestamp, try parsing it as a string let span = input.span(); - match input { - Value::String { val, .. } => { - match dateformat { - Some(dt) => match DateTime::parse_from_str(val, &dt.0) { - Ok(d) => Value::date ( d, head ), - Err(reason) => { - match NaiveDateTime::parse_from_str(val, &dt.0) { - Ok(d) => Value::date ( - DateTime::from_naive_utc_and_offset( - d, - *Local::now().offset(), - ), - head, + + let parse_as_string = |val: &str| { + match dateformat { + Some(dt) => match DateTime::parse_from_str(val, &dt.0) { + Ok(d) => Value::date ( d, head ), + Err(reason) => { + match NaiveDateTime::parse_from_str(val, &dt.0) { + Ok(d) => Value::date ( + DateTime::from_naive_utc_and_offset( + d, + *Local::now().offset(), ), - Err(_) => { - Value::error ( - ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, - head, - ) - } + head, + ), + Err(_) => { + Value::error ( + ShellError::CantConvert { to_type: format!("could not parse as datetime using format '{}'", dt.0), from_type: reason.to_string(), span: head, help: Some("you can use `into datetime` without a format string to enable flexible parsing".to_string()) }, + head, + ) } } - }, + } + }, - // Tries to automatically parse the date - // (i.e. without a format string) - // and assumes the system's local timezone if none is specified - None => match parse_date_from_string(val, span) { - Ok(date) => Value::date ( - date, - span, - ), - Err(err) => err, - }, - } + // Tries to automatically parse the date + // (i.e. without a format string) + // and assumes the system's local timezone if none is specified + None => match parse_date_from_string(val, span) { + Ok(date) => Value::date ( + date, + span, + ), + Err(err) => err, + }, } + }; + + match input { + Value::String { val, .. } => parse_as_string(val), + Value::Int { val, .. } => parse_as_string(&val.to_string()), + // Propagate errors by explicitly matching them before the final case. Value::Error { .. } => input.clone(), other => Value::error( @@ -575,6 +580,24 @@ mod tests { assert_eq!(actual, expected) } + #[test] + fn takes_int_with_formatstring() { + let date_int = Value::test_int(1_614_434_140); + let fmt_options = Some(DatetimeFormat("%s".to_string())); + let args = Arguments { + zone_options: None, + format_options: fmt_options, + cell_paths: None, + }; + let actual = action(&date_int, &args, Span::test_data()); + let expected = Value::date( + DateTime::parse_from_str("2021-02-27 21:55:40 +08:00", "%Y-%m-%d %H:%M:%S %z").unwrap(), + Span::test_data(), + ); + + assert_eq!(actual, expected) + } + #[test] fn takes_timestamp() { let date_str = Value::test_string("1614434140000000000"); diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4adcfe9214..6ef188d571 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -397,6 +397,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { RandomFloat, RandomInt, RandomUuid, + RandomBinary }; // Generators diff --git a/crates/nu-command/src/random/binary.rs b/crates/nu-command/src/random/binary.rs new file mode 100644 index 0000000000..57fd8b01a7 --- /dev/null +++ b/crates/nu-command/src/random/binary.rs @@ -0,0 +1,64 @@ +use nu_engine::command_prelude::*; + +use rand::{thread_rng, RngCore}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "random binary" + } + + fn signature(&self) -> Signature { + Signature::build("random binary") + .input_output_types(vec![(Type::Nothing, Type::Binary)]) + .allow_variants_without_examples(true) + .required("length", SyntaxShape::Int, "Length of the output binary.") + .category(Category::Random) + } + + fn usage(&self) -> &str { + "Generate random bytes." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["generate", "bytes"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let length = call.req(engine_state, stack, 0)?; + let mut rng = thread_rng(); + + let mut out = vec![0u8; length]; + rng.fill_bytes(&mut out); + + Ok(Value::binary(out, call.head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Generate 16 random bytes", + example: "random bytes 16", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/random/mod.rs b/crates/nu-command/src/random/mod.rs index 5a1fb06cf8..4c05376c88 100644 --- a/crates/nu-command/src/random/mod.rs +++ b/crates/nu-command/src/random/mod.rs @@ -1,3 +1,4 @@ +mod binary; mod bool; mod chars; mod dice; @@ -6,6 +7,7 @@ mod int; mod random_; mod uuid; +pub use self::binary::SubCommand as RandomBinary; pub use self::bool::SubCommand as RandomBool; pub use self::chars::SubCommand as RandomChars; pub use self::dice::SubCommand as RandomDice; diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index ed432d848f..3d5381dd9d 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -3,25 +3,28 @@ use nu_protocol::{ ast::{Argument, Call, Expr, Expression, RecordItem}, debugger::WithoutDebug, engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID}, - record, Category, Config, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, - Spanned, SyntaxShape, Type, Value, + record, Category, Config, Example, IntoPipelineData, PipelineData, PositionalArg, Signature, + Span, SpanId, Spanned, SyntaxShape, Type, Value, }; use std::{collections::HashMap, fmt::Write}; use terminal_size::{Height, Width}; +/// ANSI style reset +const RESET: &str = "\x1b[0m"; +/// ANSI set default color (as set in the terminal) +const DEFAULT_COLOR: &str = "\x1b[39m"; + pub fn get_full_help( command: &dyn Command, engine_state: &EngineState, stack: &mut Stack, ) -> String { - let config = stack.get_config(engine_state); - let doc_config = DocumentationConfig { - no_subcommands: false, - no_color: !config.use_ansi_coloring, - brief: false, - }; - + // Precautionary step to capture any command output generated during this operation. We + // internally call several commands (`table`, `ansi`, `nu-highlight`) and get their + // `PipelineData` using this `Stack`, any other output should not be redirected like the main + // execution. let stack = &mut stack.start_capture(); + let signature = command.signature().update_from_command(command); get_documentation( @@ -29,19 +32,11 @@ pub fn get_full_help( &command.examples(), engine_state, stack, - &doc_config, command.is_keyword(), ) } -#[derive(Default)] -struct DocumentationConfig { - no_subcommands: bool, - no_color: bool, - brief: bool, -} - -// Utility returns nu-highlighted string +/// Syntax highlight code using the `nu-highlight` command if available fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mut Stack) -> String { if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { let decl = engine_state.get_decl(highlighter); @@ -68,35 +63,15 @@ fn get_documentation( examples: &[Example], engine_state: &EngineState, stack: &mut Stack, - config: &DocumentationConfig, is_parser_keyword: bool, ) -> String { let nu_config = stack.get_config(engine_state); // Create ansi colors - //todo make these configurable -- pull from enginestate.config - let help_section_name: String = get_ansi_color_for_component_or_default( - engine_state, - &nu_config, - "shape_string", - "\x1b[32m", - ); // default: green - - let help_subcolor_one: String = get_ansi_color_for_component_or_default( - engine_state, - &nu_config, - "shape_external", - "\x1b[36m", - ); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - let help_subcolor_two: String = get_ansi_color_for_component_or_default( - engine_state, - &nu_config, - "shape_block", - "\x1b[94m", - ); // default: light blue (nobold, should be bolding the *names*) - - const RESET: &str = "\x1b[0m"; // reset + let mut help_style = HelpStyle::default(); + help_style.update_from_config(engine_state, &nu_config); + let help_section_name = &help_style.section_name; + let help_subcolor_one = &help_style.subcolor_one; let cmd_name = &sig.name; let mut long_desc = String::new(); @@ -107,44 +82,46 @@ fn get_documentation( long_desc.push_str("\n\n"); } - let extra_usage = if config.brief { "" } else { &sig.extra_usage }; + let extra_usage = &sig.extra_usage; if !extra_usage.is_empty() { long_desc.push_str(extra_usage); long_desc.push_str("\n\n"); } - let mut subcommands = vec![]; - if !config.no_subcommands { - let signatures = engine_state.get_signatures(true); - for sig in signatures { - if sig.name.starts_with(&format!("{cmd_name} ")) - // Don't display removed/deprecated commands in the Subcommands list - && !matches!(sig.category, Category::Removed) - { - subcommands.push(format!( - " {help_subcolor_one}{}{RESET} - {}", - sig.name, sig.usage - )); - } - } - } - if !sig.search_terms.is_empty() { - let text = format!( - "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{}\n\n", + let _ = write!( + long_desc, + "{help_section_name}Search terms{RESET}: {help_subcolor_one}{}{RESET}\n\n", sig.search_terms.join(", "), - RESET ); - let _ = write!(long_desc, "{text}"); } - let text = format!( - "{}Usage{}:\n > {}\n", - help_section_name, - RESET, + let _ = write!( + long_desc, + "{help_section_name}Usage{RESET}:\n > {}\n", sig.call_signature() ); - let _ = write!(long_desc, "{text}"); + + // TODO: improve the subcommand name resolution + // issues: + // - Aliases are included + // - https://github.com/nushell/nushell/issues/11657 + // - Subcommands are included violating module scoping + // - https://github.com/nushell/nushell/issues/11447 + // - https://github.com/nushell/nushell/issues/11625 + let mut subcommands = vec![]; + let signatures = engine_state.get_signatures(true); + for sig in signatures { + // Don't display removed/deprecated commands in the Subcommands list + if sig.name.starts_with(&format!("{cmd_name} ")) + && !matches!(sig.category, Category::Removed) + { + subcommands.push(format!( + " {help_subcolor_one}{}{RESET} - {}", + sig.name, sig.usage + )); + } + } if !subcommands.is_empty() { let _ = write!(long_desc, "\n{help_section_name}Subcommands{RESET}:\n"); @@ -154,12 +131,9 @@ fn get_documentation( } if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section( - Some(engine_state), - Some(&nu_config), - sig, - |v| nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack), - )) + long_desc.push_str(&get_flags_section(sig, &help_style, |v| { + nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack) + })) } if !sig.required_positional.is_empty() @@ -168,70 +142,38 @@ fn get_documentation( { let _ = write!(long_desc, "\n{help_section_name}Parameters{RESET}:\n"); for positional in &sig.required_positional { - let text = match &positional.shape { - SyntaxShape::Keyword(kw, shape) => { - format!( - " {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {}", - String::from_utf8_lossy(kw), - document_shape(*shape.clone()), - positional.desc - ) - } - _ => { - format!( - " {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}", - positional.name, - document_shape(positional.shape.clone()), - positional.desc - ) - } - }; - let _ = writeln!(long_desc, "{text}"); + write_positional( + &mut long_desc, + positional, + PositionalKind::Required, + &help_style, + &nu_config, + engine_state, + stack, + ); } for positional in &sig.optional_positional { - let text = match &positional.shape { - SyntaxShape::Keyword(kw, shape) => { - format!( - " {help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>: {} (optional)", - String::from_utf8_lossy(kw), - document_shape(*shape.clone()), - positional.desc - ) - } - _ => { - let opt_suffix = if let Some(value) = &positional.default_value { - format!( - " (optional, default: {})", - nu_highlight_string( - &value.to_parsable_string(", ", &nu_config), - engine_state, - stack - ) - ) - } else { - (" (optional)").to_string() - }; - - format!( - " {help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}{}", - positional.name, - document_shape(positional.shape.clone()), - positional.desc, - opt_suffix, - ) - } - }; - let _ = writeln!(long_desc, "{text}"); + write_positional( + &mut long_desc, + positional, + PositionalKind::Optional, + &help_style, + &nu_config, + engine_state, + stack, + ); } if let Some(rest_positional) = &sig.rest_positional { - let text = format!( - " ...{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>: {}", - rest_positional.name, - document_shape(rest_positional.shape.clone()), - rest_positional.desc + write_positional( + &mut long_desc, + rest_positional, + PositionalKind::Rest, + &help_style, + &nu_config, + engine_state, + stack, ); - let _ = writeln!(long_desc, "{text}"); } } @@ -300,36 +242,12 @@ fn get_documentation( long_desc.push_str(" "); long_desc.push_str(example.description); - if config.no_color { + if !nu_config.use_ansi_coloring { let _ = write!(long_desc, "\n > {}\n", example.example); - } else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { - let decl = engine_state.get_decl(highlighter); - let call = Call::new(Span::unknown()); - - match decl.run( - engine_state, - stack, - &(&call).into(), - Value::string(example.example, Span::unknown()).into_pipeline_data(), - ) { - Ok(output) => { - let result = output.into_value(Span::unknown()); - match result.and_then(Value::coerce_into_string) { - Ok(s) => { - let _ = write!(long_desc, "\n > {s}\n"); - } - _ => { - let _ = write!(long_desc, "\n > {}\n", example.example); - } - } - } - Err(_) => { - let _ = write!(long_desc, "\n > {}\n", example.example); - } - } } else { - let _ = write!(long_desc, "\n > {}\n", example.example); - } + let code_string = nu_highlight_string(example.example, engine_state, stack); + let _ = write!(long_desc, "\n > {code_string}\n"); + }; if let Some(result) = &example.result { let mut table_call = Call::new(Span::unknown()); @@ -395,19 +313,19 @@ fn get_documentation( long_desc.push('\n'); - if config.no_color { + if !nu_config.use_ansi_coloring { nu_utils::strip_ansi_string_likely(long_desc) } else { long_desc } } -fn get_ansi_color_for_component_or_default( +fn update_ansi_from_config( + ansi_code: &mut String, engine_state: &EngineState, nu_config: &Config, theme_component: &str, - default: &str, -) -> String { +) { if let Some(color) = &nu_config.color_config.get(theme_component) { let caller_stack = &mut Stack::new().capture(); let span = Span::unknown(); @@ -430,14 +348,12 @@ fn get_ansi_color_for_component_or_default( PipelineData::Empty, ) { if let Ok((str, ..)) = result.collect_string_strict(span) { - return str; + *ansi_code = str; } } } } } - - default.to_string() } fn get_argument_for_color_value( @@ -491,151 +407,174 @@ fn get_argument_for_color_value( } } -// document shape helps showing more useful information -pub fn document_shape(shape: SyntaxShape) -> SyntaxShape { +/// Contains the settings for ANSI colors in help output +/// +/// By default contains a fixed set of (4-bit) colors +/// +/// Can reflect configuration using [`HelpStyle::update_from_config`] +pub struct HelpStyle { + section_name: String, + subcolor_one: String, + subcolor_two: String, +} + +impl Default for HelpStyle { + fn default() -> Self { + HelpStyle { + // default: green + section_name: "\x1b[32m".to_string(), + // default: cyan + subcolor_one: "\x1b[36m".to_string(), + // default: light blue + subcolor_two: "\x1b[94m".to_string(), + } + } +} + +impl HelpStyle { + /// Pull colors from the [`Config`] + /// + /// Uses some arbitrary `shape_*` settings, assuming they are well visible in the terminal theme. + /// + /// Implementation detail: currently executes `ansi` command internally thus requiring the + /// [`EngineState`] for execution. + /// See for details + pub fn update_from_config(&mut self, engine_state: &EngineState, nu_config: &Config) { + update_ansi_from_config( + &mut self.section_name, + engine_state, + nu_config, + "shape_string", + ); + update_ansi_from_config( + &mut self.subcolor_one, + engine_state, + nu_config, + "shape_external", + ); + update_ansi_from_config( + &mut self.subcolor_two, + engine_state, + nu_config, + "shape_block", + ); + } +} + +/// Make syntax shape presentable by stripping custom completer info +fn document_shape(shape: &SyntaxShape) -> &SyntaxShape { match shape { - SyntaxShape::CompleterWrapper(inner_shape, _) => *inner_shape, + SyntaxShape::CompleterWrapper(inner_shape, _) => inner_shape, _ => shape, } } +#[derive(PartialEq)] +enum PositionalKind { + Required, + Optional, + Rest, +} + +fn write_positional( + long_desc: &mut String, + positional: &PositionalArg, + arg_kind: PositionalKind, + help_style: &HelpStyle, + nu_config: &Config, + engine_state: &EngineState, + stack: &mut Stack, +) { + let help_subcolor_one = &help_style.subcolor_one; + let help_subcolor_two = &help_style.subcolor_two; + + // Indentation + long_desc.push_str(" "); + if arg_kind == PositionalKind::Rest { + long_desc.push_str("..."); + } + match &positional.shape { + SyntaxShape::Keyword(kw, shape) => { + let _ = write!( + long_desc, + "{help_subcolor_one}\"{}\" + {RESET}<{help_subcolor_two}{}{RESET}>", + String::from_utf8_lossy(kw), + document_shape(shape), + ); + } + _ => { + let _ = write!( + long_desc, + "{help_subcolor_one}{}{RESET} <{help_subcolor_two}{}{RESET}>", + positional.name, + document_shape(&positional.shape), + ); + } + }; + if !positional.desc.is_empty() || arg_kind == PositionalKind::Optional { + let _ = write!(long_desc, ": {}", positional.desc); + } + if arg_kind == PositionalKind::Optional { + if let Some(value) = &positional.default_value { + let _ = write!( + long_desc, + " (optional, default: {})", + nu_highlight_string( + &value.to_parsable_string(", ", nu_config), + engine_state, + stack + ) + ); + } else { + long_desc.push_str(" (optional)"); + }; + } + long_desc.push('\n'); +} + pub fn get_flags_section( - engine_state_opt: Option<&EngineState>, - nu_config_opt: Option<&Config>, signature: &Signature, + help_style: &HelpStyle, mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight) ) -> String where F: FnMut(&nu_protocol::Value) -> String, { - //todo make these configurable -- pull from enginestate.config - let help_section_name: String; - let help_subcolor_one: String; - let help_subcolor_two: String; - - // Sometimes we want to get the flags without engine_state - // For example, in nu-plugin. In that case, we fall back on default values - if let Some(engine_state) = engine_state_opt { - let nu_config = nu_config_opt.unwrap_or_else(|| engine_state.get_config()); - help_section_name = get_ansi_color_for_component_or_default( - engine_state, - nu_config, - "shape_string", - "\x1b[32m", - ); // default: green - help_subcolor_one = get_ansi_color_for_component_or_default( - engine_state, - nu_config, - "shape_external", - "\x1b[36m", - ); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - help_subcolor_two = get_ansi_color_for_component_or_default( - engine_state, - nu_config, - "shape_block", - "\x1b[94m", - ); - // default: light blue (nobold, should be bolding the *names*) - } else { - help_section_name = "\x1b[32m".to_string(); - help_subcolor_one = "\x1b[36m".to_string(); - help_subcolor_two = "\x1b[94m".to_string(); - } - - const RESET: &str = "\x1b[0m"; // reset - const D: &str = "\x1b[39m"; // default + let help_section_name = &help_style.section_name; + let help_subcolor_one = &help_style.subcolor_one; + let help_subcolor_two = &help_style.subcolor_two; let mut long_desc = String::new(); let _ = write!(long_desc, "\n{help_section_name}Flags{RESET}:\n"); for flag in &signature.named { - let default_str = if let Some(value) = &flag.default_value { - format!( - " (default: {help_subcolor_two}{}{RESET})", - &value_formatter(value) - ) - } else { - "".to_string() - }; - - let msg = if let Some(arg) = &flag.arg { - if let Some(short) = flag.short { - if flag.required { - format!( - " {help_subcolor_one}-{}{}{RESET} (required parameter) {:?} - {}{}\n", - short, - if !flag.long.is_empty() { - format!("{D},{RESET} {help_subcolor_one}--{}", flag.long) - } else { - "".into() - }, - arg, - flag.desc, - default_str, - ) - } else { - format!( - " {help_subcolor_one}-{}{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n", - short, - if !flag.long.is_empty() { - format!("{D},{RESET} {help_subcolor_one}--{}", flag.long) - } else { - "".into() - }, - arg, - flag.desc, - default_str, - ) - } - } else if flag.required { - format!( - " {help_subcolor_one}--{}{RESET} (required parameter) <{help_subcolor_two}{:?}{RESET}> - {}{}\n", - flag.long, arg, flag.desc, default_str, - ) - } else { - format!( - " {help_subcolor_one}--{}{RESET} <{help_subcolor_two}{:?}{RESET}> - {}{}\n", - flag.long, arg, flag.desc, default_str, - ) + // Indentation + long_desc.push_str(" "); + // Short flag shown before long flag + if let Some(short) = flag.short { + let _ = write!(long_desc, "{help_subcolor_one}-{}{RESET}", short); + if !flag.long.is_empty() { + let _ = write!(long_desc, "{DEFAULT_COLOR},{RESET} "); } - } else if let Some(short) = flag.short { - if flag.required { - format!( - " {help_subcolor_one}-{}{}{RESET} (required parameter) - {}{}\n", - short, - if !flag.long.is_empty() { - format!("{D},{RESET} {help_subcolor_one}--{}", flag.long) - } else { - "".into() - }, - flag.desc, - default_str, - ) - } else { - format!( - " {help_subcolor_one}-{}{}{RESET} - {}{}\n", - short, - if !flag.long.is_empty() { - format!("{D},{RESET} {help_subcolor_one}--{}", flag.long) - } else { - "".into() - }, - flag.desc, - default_str - ) - } - } else if flag.required { - format!( - " {help_subcolor_one}--{}{RESET} (required parameter) - {}{}\n", - flag.long, flag.desc, default_str, - ) - } else { - format!( - " {help_subcolor_one}--{}{RESET} - {}\n", - flag.long, flag.desc - ) - }; - long_desc.push_str(&msg); + } + if !flag.long.is_empty() { + let _ = write!(long_desc, "{help_subcolor_one}--{}{RESET}", flag.long); + } + if flag.required { + long_desc.push_str(" (required parameter)") + } + // Type/Syntax shape info + if let Some(arg) = &flag.arg { + let _ = write!( + long_desc, + " <{help_subcolor_two}{}{RESET}>", + document_shape(arg) + ); + } + let _ = write!(long_desc, " - {}", flag.desc); + if let Some(value) = &flag.default_value { + let _ = write!(long_desc, " (default: {})", &value_formatter(value)); + } + long_desc.push('\n'); } long_desc } diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index aee2ab240d..6841749fc9 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1195,17 +1195,17 @@ mod tests { assert_json_include!( actual: result, expected: serde_json::json!([ - { - "label": "def", - "textEdit": { - "newText": "def", - "range": { - "start": { "character": 0, "line": 0 }, - "end": { "character": 2, "line": 0 } - } - }, - "kind": 14 - } + { + "label": "overlay", + "textEdit": { + "newText": "overlay", + "range": { + "start": { "character": 0, "line": 0 }, + "end": { "character": 2, "line": 0 } + } + }, + "kind": 14 + }, ]) ); } diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index d809318d68..87f95cf7d4 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -9,7 +9,7 @@ use std::{ thread, }; -use nu_engine::documentation::get_flags_section; +use nu_engine::documentation::{get_flags_section, HelpStyle}; use nu_plugin_core::{ ClientCommunicationIo, CommunicationMode, InterfaceManager, PluginEncoder, PluginRead, PluginWrite, @@ -657,6 +657,7 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) { println!("Encoder: {}", encoder.name()); let mut help = String::new(); + let help_style = HelpStyle::default(); plugin.commands().into_iter().for_each(|command| { let signature = command.signature(); @@ -670,7 +671,7 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) { } }) .and_then(|_| { - let flags = get_flags_section(None, None, &signature, |v| format!("{:#?}", v)); + let flags = get_flags_section(&signature, &help_style, |v| format!("{:#?}", v)); write!(help, "{flags}") }) .and_then(|_| writeln!(help, "\nParameters:")) diff --git a/crates/nu-protocol/src/config/completer.rs b/crates/nu-protocol/src/config/completer.rs index 67bde52e27..921057c082 100644 --- a/crates/nu-protocol/src/config/completer.rs +++ b/crates/nu-protocol/src/config/completer.rs @@ -35,6 +35,35 @@ impl ReconstructVal for CompletionAlgorithm { } } +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)] +pub enum CompletionSort { + #[default] + Smart, + Alphabetical, +} + +impl FromStr for CompletionSort { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "smart" => Ok(Self::Smart), + "alphabetical" => Ok(Self::Alphabetical), + _ => Err("expected either 'smart' or 'alphabetical'"), + } + } +} + +impl ReconstructVal for CompletionSort { + fn reconstruct_value(&self, span: Span) -> Value { + let str = match self { + Self::Smart => "smart", + Self::Alphabetical => "alphabetical", + }; + Value::string(str, span) + } +} + pub(super) fn reconstruct_external_completer(config: &Config, span: Span) -> Value { if let Some(closure) = config.external_completer.as_ref() { Value::closure(closure.clone(), span) diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index eda3d9a15e..59cc70fb79 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -11,7 +11,7 @@ use crate::{record, ShellError, Span, Value}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -pub use self::completer::CompletionAlgorithm; +pub use self::completer::{CompletionAlgorithm, CompletionSort}; pub use self::helper::extract_value; pub use self::hooks::Hooks; pub use self::output::ErrorStyle; @@ -69,6 +69,7 @@ pub struct Config { pub quick_completions: bool, pub partial_completions: bool, pub completion_algorithm: CompletionAlgorithm, + pub completion_sort: CompletionSort, pub edit_mode: EditBindings, pub history: HistoryConfig, pub keybindings: Vec, @@ -141,6 +142,7 @@ impl Default for Config { quick_completions: true, partial_completions: true, completion_algorithm: CompletionAlgorithm::default(), + completion_sort: CompletionSort::default(), enable_external_completion: true, max_external_completion_results: 100, recursion_limit: 50, @@ -341,6 +343,13 @@ impl Value { "case_sensitive" => { process_bool_config(value, &mut errors, &mut config.case_sensitive_completions); } + "sort" => { + process_string_enum( + &mut config.completion_sort, + &[key, key2], + value, + &mut errors); + } "external" => { if let Value::Record { val, .. } = value { val.to_mut().retain_mut(|key3, value| @@ -401,6 +410,7 @@ impl Value { "partial" => Value::bool(config.partial_completions, span), "algorithm" => config.completion_algorithm.reconstruct_value(span), "case_sensitive" => Value::bool(config.case_sensitive_completions, span), + "sort" => config.completion_sort.reconstruct_value(span), "external" => reconstruct_external(&config, span), "use_ls_colors" => Value::bool(config.use_ls_colors_completions, span), }, diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 0c78cf9ca1..b807244cee 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -206,6 +206,7 @@ $env.config = { quick: true # set this to false to prevent auto-selecting completions when only one remains partial: true # set this to false to prevent partial filling of the prompt algorithm: "prefix" # prefix or fuzzy + sort: "smart" # "smart" (alphabetical for prefix matching, fuzzy score for fuzzy matching) or "alphabetical" external: { enable: true # set to false to prevent nushell looking into $env.PATH to find more suggestions, `false` recommended for WSL users as this look up may be very slow max_results: 100 # setting it lower can improve completion performance at the cost of omitting some options diff --git a/crates/nu_plugin_formats/Cargo.toml b/crates/nu_plugin_formats/Cargo.toml index dbf1b43657..f6ad84758f 100644 --- a/crates/nu_plugin_formats/Cargo.toml +++ b/crates/nu_plugin_formats/Cargo.toml @@ -16,6 +16,8 @@ indexmap = { workspace = true } eml-parser = "0.1" ical = "0.11" rust-ini = "0.21.0" +plist = "1.7" +chrono = "0.4" [dev-dependencies] -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.96.2" } \ No newline at end of file +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.96.2" } diff --git a/crates/nu_plugin_formats/src/from/eml.rs b/crates/nu_plugin_formats/src/from/eml.rs index 2630e3b1c2..2b4c33a814 100644 --- a/crates/nu_plugin_formats/src/from/eml.rs +++ b/crates/nu_plugin_formats/src/from/eml.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use eml_parser::eml::*; use eml_parser::EmlParser; use indexmap::IndexMap; @@ -12,7 +12,7 @@ const DEFAULT_BODY_PREVIEW: usize = 50; pub struct FromEml; impl SimplePluginCommand for FromEml { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from eml" @@ -40,7 +40,7 @@ impl SimplePluginCommand for FromEml { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -176,5 +176,5 @@ fn from_eml(input: &Value, body_preview: usize, head: Span) -> Result Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromEml) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromEml) } diff --git a/crates/nu_plugin_formats/src/from/ics.rs b/crates/nu_plugin_formats/src/from/ics.rs index 099b3431fe..bcd7311f8a 100644 --- a/crates/nu_plugin_formats/src/from/ics.rs +++ b/crates/nu_plugin_formats/src/from/ics.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use ical::{parser::ical::component::*, property::Property}; use indexmap::IndexMap; @@ -11,7 +11,7 @@ use std::io::BufReader; pub struct FromIcs; impl SimplePluginCommand for FromIcs { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from ics" @@ -33,7 +33,7 @@ impl SimplePluginCommand for FromIcs { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -274,5 +274,5 @@ fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIcs) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIcs) } diff --git a/crates/nu_plugin_formats/src/from/ini.rs b/crates/nu_plugin_formats/src/from/ini.rs index cf37ffc3d7..bb44ce1398 100644 --- a/crates/nu_plugin_formats/src/from/ini.rs +++ b/crates/nu_plugin_formats/src/from/ini.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; use nu_protocol::{ @@ -8,7 +8,7 @@ use nu_protocol::{ pub struct FromIni; impl SimplePluginCommand for FromIni { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from ini" @@ -30,7 +30,7 @@ impl SimplePluginCommand for FromIni { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -101,5 +101,5 @@ b=2' | from ini", fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIni) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIni) } diff --git a/crates/nu_plugin_formats/src/from/mod.rs b/crates/nu_plugin_formats/src/from/mod.rs index 4b4976f391..232dde7d6d 100644 --- a/crates/nu_plugin_formats/src/from/mod.rs +++ b/crates/nu_plugin_formats/src/from/mod.rs @@ -1,4 +1,5 @@ -pub mod eml; -pub mod ics; -pub mod ini; -pub mod vcf; +pub(crate) mod eml; +pub(crate) mod ics; +pub(crate) mod ini; +pub(crate) mod plist; +pub(crate) mod vcf; diff --git a/crates/nu_plugin_formats/src/from/plist.rs b/crates/nu_plugin_formats/src/from/plist.rs new file mode 100644 index 0000000000..9894879955 --- /dev/null +++ b/crates/nu_plugin_formats/src/from/plist.rs @@ -0,0 +1,240 @@ +use std::time::SystemTime; + +use chrono::{DateTime, FixedOffset, Offset, Utc}; +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; +use nu_protocol::{ + record, Category, Example, LabeledError, Record, Signature, Span, Value as NuValue, +}; +use plist::{Date as PlistDate, Dictionary, Value as PlistValue}; + +use crate::FormatCmdsPlugin; + +pub struct FromPlist; + +impl SimplePluginCommand for FromPlist { + type Plugin = FormatCmdsPlugin; + + fn name(&self) -> &str { + "from plist" + } + + fn usage(&self) -> &str { + "Convert plist to Nushell values" + } + + fn examples(&self) -> Vec { + vec![Example { + example: r#"' + + + + a + 3 + +' | from plist"#, + description: "Convert a table into a plist file", + result: Some(NuValue::test_record(record!( "a" => NuValue::test_int(3)))), + }] + } + + fn signature(&self) -> Signature { + Signature::build(PluginCommand::name(self)).category(Category::Formats) + } + + fn run( + &self, + _plugin: &FormatCmdsPlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &NuValue, + ) -> Result { + match input { + NuValue::String { val, .. } => { + let plist = plist::from_bytes(val.as_bytes()) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + let converted = convert_plist_value(&plist, call.head)?; + Ok(converted) + } + NuValue::Binary { val, .. } => { + let plist = plist::from_bytes(val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + let converted = convert_plist_value(&plist, call.head)?; + Ok(converted) + } + _ => Err(build_label_error( + format!("Invalid input, must be string not: {:?}", input), + call.head, + )), + } + } +} + +fn build_label_error(msg: impl Into, span: Span) -> LabeledError { + LabeledError::new("Could not load plist").with_label(msg, span) +} + +fn convert_plist_value(plist_val: &PlistValue, span: Span) -> Result { + match plist_val { + PlistValue::String(s) => Ok(NuValue::string(s.to_owned(), span)), + PlistValue::Boolean(b) => Ok(NuValue::bool(*b, span)), + PlistValue::Real(r) => Ok(NuValue::float(*r, span)), + PlistValue::Date(d) => Ok(NuValue::date(convert_date(d), span)), + PlistValue::Integer(i) => { + let signed = i + .as_signed() + .ok_or_else(|| build_label_error(format!("Cannot convert {i} to i64"), span))?; + Ok(NuValue::int(signed, span)) + } + PlistValue::Uid(uid) => Ok(NuValue::float(uid.get() as f64, span)), + PlistValue::Data(data) => Ok(NuValue::binary(data.to_owned(), span)), + PlistValue::Array(arr) => Ok(NuValue::list(convert_array(arr, span)?, span)), + PlistValue::Dictionary(dict) => Ok(convert_dict(dict, span)?), + _ => Ok(NuValue::nothing(span)), + } +} + +fn convert_dict(dict: &Dictionary, span: Span) -> Result { + let cols: Vec = dict.keys().cloned().collect(); + let vals: Result, LabeledError> = dict + .values() + .map(|v| convert_plist_value(v, span)) + .collect(); + Ok(NuValue::record( + Record::from_raw_cols_vals(cols, vals?, span, span)?, + span, + )) +} + +fn convert_array(plist_array: &[PlistValue], span: Span) -> Result, LabeledError> { + plist_array + .iter() + .map(|v| convert_plist_value(v, span)) + .collect() +} + +pub fn convert_date(plist_date: &PlistDate) -> DateTime { + // In the docs the plist date object is listed as a utc timestamp, so this + // conversion should be fine + let plist_sys_time: SystemTime = plist_date.to_owned().into(); + let utc_date: DateTime = plist_sys_time.into(); + let utc_offset = utc_date.offset().fix(); + utc_date.with_timezone(&utc_offset) +} + +#[cfg(test)] +mod test { + use super::*; + use chrono::Datelike; + use plist::Uid; + use std::time::SystemTime; + + use nu_plugin_test_support::PluginTest; + use nu_protocol::ShellError; + + #[test] + fn test_convert_string() { + let plist_val = PlistValue::String("hello".to_owned()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!( + result, + Ok(NuValue::string("hello".to_owned(), Span::test_data())) + ); + } + + #[test] + fn test_convert_boolean() { + let plist_val = PlistValue::Boolean(true); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::bool(true, Span::test_data()))); + } + + #[test] + fn test_convert_real() { + let plist_val = PlistValue::Real(3.14); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::float(3.14, Span::test_data()))); + } + + #[test] + fn test_convert_integer() { + let plist_val = PlistValue::Integer(42.into()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::int(42, Span::test_data()))); + } + + #[test] + fn test_convert_uid() { + let v = 12345678_u64; + let uid = Uid::new(v); + let plist_val = PlistValue::Uid(uid); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::float(v as f64, Span::test_data()))); + } + + #[test] + fn test_convert_data() { + let data = vec![0x41, 0x42, 0x43]; + let plist_val = PlistValue::Data(data.clone()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::binary(data, Span::test_data()))); + } + + #[test] + fn test_convert_date() { + let epoch = SystemTime::UNIX_EPOCH; + let plist_date = epoch.into(); + + let datetime = convert_date(&plist_date); + assert_eq!(1970, datetime.year()); + assert_eq!(1, datetime.month()); + assert_eq!(1, datetime.day()); + } + + #[test] + fn test_convert_dict() { + let mut dict = Dictionary::new(); + dict.insert("a".to_string(), PlistValue::String("c".to_string())); + dict.insert("b".to_string(), PlistValue::String("d".to_string())); + let nu_dict = convert_dict(&dict, Span::test_data()).unwrap(); + assert_eq!( + nu_dict, + NuValue::record( + Record::from_raw_cols_vals( + vec!["a".to_string(), "b".to_string()], + vec![ + NuValue::string("c".to_string(), Span::test_data()), + NuValue::string("d".to_string(), Span::test_data()) + ], + Span::test_data(), + Span::test_data(), + ) + .expect("failed to create record"), + Span::test_data(), + ) + ); + } + + #[test] + fn test_convert_array() { + let mut arr = Vec::new(); + arr.push(PlistValue::String("a".to_string())); + arr.push(PlistValue::String("b".to_string())); + let nu_arr = convert_array(&arr, Span::test_data()).unwrap(); + assert_eq!( + nu_arr, + vec![ + NuValue::string("a".to_string(), Span::test_data()), + NuValue::string("b".to_string(), Span::test_data()) + ] + ); + } + + #[test] + fn test_examples() -> Result<(), ShellError> { + let plugin = FormatCmdsPlugin {}; + let cmd = FromPlist {}; + + let mut plugin_test = PluginTest::new("polars", plugin.into())?; + plugin_test.test_command_examples(&cmd) + } +} diff --git a/crates/nu_plugin_formats/src/from/vcf.rs b/crates/nu_plugin_formats/src/from/vcf.rs index 4de20154d7..a751a774d3 100644 --- a/crates/nu_plugin_formats/src/from/vcf.rs +++ b/crates/nu_plugin_formats/src/from/vcf.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use ical::{parser::vcard::component::*, property::Property}; use indexmap::IndexMap; @@ -10,7 +10,7 @@ use nu_protocol::{ pub struct FromVcf; impl SimplePluginCommand for FromVcf { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from vcf" @@ -32,7 +32,7 @@ impl SimplePluginCommand for FromVcf { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -164,5 +164,5 @@ fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromVcf) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromVcf) } diff --git a/crates/nu_plugin_formats/src/lib.rs b/crates/nu_plugin_formats/src/lib.rs index 2ae24a4971..ce95fcf8c9 100644 --- a/crates/nu_plugin_formats/src/lib.rs +++ b/crates/nu_plugin_formats/src/lib.rs @@ -1,15 +1,18 @@ mod from; +mod to; use nu_plugin::{Plugin, PluginCommand}; -pub use from::eml::FromEml; -pub use from::ics::FromIcs; -pub use from::ini::FromIni; -pub use from::vcf::FromVcf; +use from::eml::FromEml; +use from::ics::FromIcs; +use from::ini::FromIni; +use from::plist::FromPlist; +use from::vcf::FromVcf; +use to::plist::IntoPlist; -pub struct FromCmds; +pub struct FormatCmdsPlugin; -impl Plugin for FromCmds { +impl Plugin for FormatCmdsPlugin { fn version(&self) -> String { env!("CARGO_PKG_VERSION").into() } @@ -20,6 +23,8 @@ impl Plugin for FromCmds { Box::new(FromIcs), Box::new(FromIni), Box::new(FromVcf), + Box::new(FromPlist), + Box::new(IntoPlist), ] } } diff --git a/crates/nu_plugin_formats/src/main.rs b/crates/nu_plugin_formats/src/main.rs index e6c7179781..f36ba3364a 100644 --- a/crates/nu_plugin_formats/src/main.rs +++ b/crates/nu_plugin_formats/src/main.rs @@ -1,6 +1,6 @@ use nu_plugin::{serve_plugin, MsgPackSerializer}; -use nu_plugin_formats::FromCmds; +use nu_plugin_formats::FormatCmdsPlugin; fn main() { - serve_plugin(&FromCmds, MsgPackSerializer {}) + serve_plugin(&FormatCmdsPlugin, MsgPackSerializer {}) } diff --git a/crates/nu_plugin_formats/src/to/mod.rs b/crates/nu_plugin_formats/src/to/mod.rs new file mode 100644 index 0000000000..2f804de5d6 --- /dev/null +++ b/crates/nu_plugin_formats/src/to/mod.rs @@ -0,0 +1 @@ +pub(crate) mod plist; diff --git a/crates/nu_plugin_formats/src/to/plist.rs b/crates/nu_plugin_formats/src/to/plist.rs new file mode 100644 index 0000000000..94368c184a --- /dev/null +++ b/crates/nu_plugin_formats/src/to/plist.rs @@ -0,0 +1,113 @@ +use std::time::SystemTime; + +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; +use nu_protocol::{Category, Example, LabeledError, Record, Signature, Span, Value as NuValue}; +use plist::{Integer, Value as PlistValue}; + +use crate::FormatCmdsPlugin; + +pub(crate) struct IntoPlist; + +impl SimplePluginCommand for IntoPlist { + type Plugin = FormatCmdsPlugin; + + fn name(&self) -> &str { + "to plist" + } + + fn usage(&self) -> &str { + "Convert Nu values into plist" + } + + fn examples(&self) -> Vec { + vec![Example { + example: "{ a: 3 } | to plist", + description: "Convert a table into a plist file", + result: None, + }] + } + + fn signature(&self) -> Signature { + Signature::build(PluginCommand::name(self)) + .switch("binary", "Output plist in binary format", Some('b')) + .category(Category::Formats) + } + + fn run( + &self, + _plugin: &FormatCmdsPlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &NuValue, + ) -> Result { + let plist_val = convert_nu_value(input)?; + let mut out = Vec::new(); + if call.has_flag("binary")? { + plist::to_writer_binary(&mut out, &plist_val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + Ok(NuValue::binary(out, input.span())) + } else { + plist::to_writer_xml(&mut out, &plist_val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + Ok(NuValue::string( + String::from_utf8(out) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?, + input.span(), + )) + } + } +} + +fn build_label_error(msg: String, span: Span) -> LabeledError { + LabeledError::new("Cannot convert plist").with_label(msg, span) +} + +fn convert_nu_value(nu_val: &NuValue) -> Result { + let span = Span::test_data(); + match nu_val { + NuValue::String { val, .. } => Ok(PlistValue::String(val.to_owned())), + NuValue::Bool { val, .. } => Ok(PlistValue::Boolean(*val)), + NuValue::Float { val, .. } => Ok(PlistValue::Real(*val)), + NuValue::Int { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + NuValue::Binary { val, .. } => Ok(PlistValue::Data(val.to_owned())), + NuValue::Record { val, .. } => convert_nu_dict(val), + NuValue::List { vals, .. } => Ok(PlistValue::Array( + vals.iter() + .map(convert_nu_value) + .collect::>()?, + )), + NuValue::Date { val, .. } => Ok(PlistValue::Date(SystemTime::from(val.to_owned()).into())), + NuValue::Filesize { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + _ => Err(build_label_error( + format!("{:?} is not convertible", nu_val), + span, + )), + } +} + +fn convert_nu_dict(record: &Record) -> Result { + Ok(PlistValue::Dictionary( + record + .iter() + .map(|(k, v)| convert_nu_value(v).map(|v| (k.to_owned(), v))) + .collect::>()?, + )) +} + +#[cfg(test)] +mod test { + + use nu_plugin_test_support::PluginTest; + use nu_protocol::ShellError; + + use super::*; + + #[test] + fn test_examples() -> Result<(), ShellError> { + let plugin = FormatCmdsPlugin {}; + let cmd = IntoPlist {}; + + let mut plugin_test = PluginTest::new("polars", plugin.into())?; + plugin_test.test_command_examples(&cmd) + } +} diff --git a/crates/nu_plugin_query/src/web_tables.rs b/crates/nu_plugin_query/src/web_tables.rs index 1f69342a3e..fef8f6c37e 100644 --- a/crates/nu_plugin_query/src/web_tables.rs +++ b/crates/nu_plugin_query/src/web_tables.rs @@ -281,6 +281,10 @@ fn select_cells( let scraped = element.select(selector).map(cell_content); let mut dehtmlized: Vec = Vec::new(); for item in scraped { + if item.is_empty() { + dehtmlized.push(item); + continue; + } let frag = Html::parse_fragment(&item); for node in frag.tree { if let scraper::node::Node::Text(text) = node { @@ -411,6 +415,7 @@ mod tests { John20 May30foo + abcd "#; @@ -425,6 +430,7 @@ mod tests { John20 May30foo + abcd @@ -432,6 +438,7 @@ mod tests { +
CarpenterSingle
MechanicMarriedbar
efgh
@@ -808,7 +815,7 @@ mod tests { assert_eq!(2, WebTable::find_first(TABLE_TD_TD).unwrap().iter().count()); assert_eq!(1, WebTable::find_first(TABLE_TH_TH).unwrap().iter().count()); assert_eq!( - 4, + 5, WebTable::find_first(TABLE_COMPLEX).unwrap().iter().count() ); } @@ -823,7 +830,7 @@ mod tests { let table = WebTable::find_first(TABLE_COMPLEX).unwrap(); assert_eq!( - vec![false, false, true, false], + vec![false, false, true, false, false], table.iter().map(|r| r.is_empty()).collect::>() ); } @@ -835,7 +842,7 @@ mod tests { let table = WebTable::find_first(TABLE_COMPLEX).unwrap(); assert_eq!( - vec![2, 3, 0, 4], + vec![2, 3, 0, 3, 4], table.iter().map(|r| r.len()).collect::>() ); } @@ -854,11 +861,11 @@ mod tests { let table_1 = tables_iter.next().unwrap(); let table_2 = tables_iter.next().unwrap(); assert_eq!( - vec![2, 3, 0, 4], + vec![2, 3, 0, 3, 4], table_1.iter().map(|r| r.len()).collect::>() ); assert_eq!( - vec![2, 3, 0, 4], + vec![2, 3, 0, 3, 4], table_2.iter().map(|r| r.len()).collect::>() ); } @@ -911,6 +918,11 @@ mod tests { assert_eq!(None, row.get("Age")); assert_eq!(None, row.get("Extra")); + let row = iter.next().unwrap(); + assert_eq!(Some(""), row.get("Name")); + assert_eq!(Some(""), row.get("Age")); + assert_eq!(Some(""), row.get("Extra")); + let row = iter.next().unwrap(); assert_eq!(Some("a"), row.get("Name")); assert_eq!(Some("b"), row.get("Age")); @@ -955,6 +967,15 @@ mod tests { assert_eq!(None, row_table_2.get("Age")); assert_eq!(None, row_table_2.get("Extra")); + let row_table_1 = iter_1.next().unwrap(); + let row_table_2 = iter_2.next().unwrap(); + assert_eq!(Some(""), row_table_1.get("Name")); + assert_eq!(Some(""), row_table_1.get("Age")); + assert_eq!(Some(""), row_table_1.get("Extra")); + assert_eq!(Some(""), row_table_2.get("Profession")); + assert_eq!(Some(""), row_table_2.get("Civil State")); + assert_eq!(Some(""), row_table_2.get("Extra")); + let row_table_1 = iter_1.next().unwrap(); let row_table_2 = iter_2.next().unwrap(); assert_eq!(Some("a"), row_table_1.get("Name")); @@ -1028,6 +1049,7 @@ mod tests { assert_eq!(&["John", "20"], iter.next().unwrap().as_slice()); assert_eq!(&["May", "30", "foo"], iter.next().unwrap().as_slice()); assert_eq!(&empty, iter.next().unwrap().as_slice()); + assert_eq!(&["", "", ""], iter.next().unwrap().as_slice()); assert_eq!(&["a", "b", "c", "d"], iter.next().unwrap().as_slice()); assert_eq!(None, iter.next()); } @@ -1045,6 +1067,7 @@ mod tests { assert_eq!(&["John", "20"], iter_1.next().unwrap().as_slice()); assert_eq!(&["May", "30", "foo"], iter_1.next().unwrap().as_slice()); assert_eq!(&empty, iter_1.next().unwrap().as_slice()); + assert_eq!(&["", "", ""], iter_1.next().unwrap().as_slice()); assert_eq!(&["a", "b", "c", "d"], iter_1.next().unwrap().as_slice()); assert_eq!(None, iter_1.next()); assert_eq!(&["Carpenter", "Single"], iter_2.next().unwrap().as_slice()); @@ -1053,6 +1076,7 @@ mod tests { iter_2.next().unwrap().as_slice() ); assert_eq!(&empty, iter_2.next().unwrap().as_slice()); + assert_eq!(&["", "", ""], iter_2.next().unwrap().as_slice()); assert_eq!(&["e", "f", "g", "h"], iter_2.next().unwrap().as_slice()); assert_eq!(None, iter_2.next()); } @@ -1109,6 +1133,13 @@ mod tests { let mut iter = row.iter(); assert_eq!(None, iter.next()); + let row = table_iter.next().unwrap(); + let mut iter = row.iter(); + assert_eq!(Some(""), iter.next().map(String::as_str)); + assert_eq!(Some(""), iter.next().map(String::as_str)); + assert_eq!(Some(""), iter.next().map(String::as_str)); + assert_eq!(None, iter.next()); + let row = table_iter.next().unwrap(); let mut iter = row.iter(); assert_eq!(Some("a"), iter.next().map(String::as_str)); @@ -1156,6 +1187,19 @@ mod tests { assert_eq!(None, iter_1.next()); assert_eq!(None, iter_2.next()); + let row_1 = table_1.next().unwrap(); + let row_2 = table_2.next().unwrap(); + let mut iter_1 = row_1.iter(); + let mut iter_2 = row_2.iter(); + assert_eq!(Some(""), iter_1.next().map(String::as_str)); + assert_eq!(Some(""), iter_1.next().map(String::as_str)); + assert_eq!(Some(""), iter_1.next().map(String::as_str)); + assert_eq!(None, iter_1.next()); + assert_eq!(Some(""), iter_2.next().map(String::as_str)); + assert_eq!(Some(""), iter_2.next().map(String::as_str)); + assert_eq!(Some(""), iter_2.next().map(String::as_str)); + assert_eq!(None, iter_2.next()); + let row_1 = table_1.next().unwrap(); let row_2 = table_2.next().unwrap(); let mut iter_1 = row_1.iter(); diff --git a/tests/fixtures/lsp/completion/keyword.nu b/tests/fixtures/lsp/completion/keyword.nu index 7673daa944..b4226322e1 100644 --- a/tests/fixtures/lsp/completion/keyword.nu +++ b/tests/fixtures/lsp/completion/keyword.nu @@ -1 +1 @@ -de +ov