From 768ff47d28ec587b689a39e5278c3f3c44fb3940 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 6 May 2022 07:58:32 -0500 Subject: [PATCH] enable cd to work with directory abbreviations (#5452) * enable cd to work with abbreviations * add abbreviation example * fix tests * make it configurable --- Cargo.lock | 14 + crates/nu-command/Cargo.toml | 2 + crates/nu-command/src/filesystem/cd.rs | 84 ++- crates/nu-command/src/filesystem/cd_query.rs | 549 ++++++++++++++++++ crates/nu-command/src/filesystem/mod.rs | 2 + .../tests/format_conversions/html.rs | 19 +- crates/nu-protocol/src/config.rs | 10 +- crates/nu-protocol/src/shell_error.rs | 21 + docs/sample_config/default_config.nu | 1 + 9 files changed, 666 insertions(+), 36 deletions(-) create mode 100644 crates/nu-command/src/filesystem/cd_query.rs diff --git a/Cargo.lock b/Cargo.lock index 0dabf4bb69..44661b3678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "alphanumeric-sort" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0" + [[package]] name = "ansi-parser" version = "0.8.0" @@ -2486,6 +2492,7 @@ name = "nu-command" version = "0.62.0" dependencies = [ "Inflector", + "alphanumeric-sort", "base64", "bytesize", "calamine", @@ -2532,6 +2539,7 @@ dependencies = [ "num 0.4.0", "pathdiff", "polars", + "powierza-coefficient", "quick-xml 0.22.0", "quickcheck", "quickcheck_macros", @@ -3360,6 +3368,12 @@ dependencies = [ "nom 7.1.1", ] +[[package]] +name = "powierza-coefficient" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9caa43783252cf8c4c66dd1cc381a5929cc95f6530da7abd1f9cdb97e2065842" + [[package]] name = "ppv-lite86" version = "0.2.16" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 86e9ef77d0..d664f07185 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -26,6 +26,7 @@ nu-utils = { path = "../nu-utils", version = "0.62.0" } nu-ansi-term = "0.45.1" # Potential dependencies for extras +alphanumeric-sort = "1.4.4" base64 = "0.13.0" bytesize = "1.1.0" calamine = "0.18.0" @@ -56,6 +57,7 @@ mime = "0.3.16" notify = "4.0.17" num = { version = "0.4.0", optional = true } pathdiff = "0.2.1" +powierza-coefficient = "1.0" quick-xml = "0.22" rand = "0.8" rayon = "1.5.1" diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index d223b30ba4..fc6ddb1242 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -1,3 +1,4 @@ +use crate::filesystem::cd_query::query; use nu_engine::{current_dir, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; @@ -36,6 +37,8 @@ impl Command for Cd { ) -> Result { let path_val: Option> = call.opt(engine_state, stack, 0)?; let cwd = current_dir(engine_state, stack)?; + let config = engine_state.get_config(); + let use_abbrev = config.cd_with_abbreviations; let (path, span) = match path_val { Some(v) => { @@ -44,13 +47,25 @@ impl Command for Cd { if let Some(oldpwd) = oldpwd { let path = oldpwd.as_path()?; - let path = match nu_path::canonicalize_with(path, &cwd) { + let path = match nu_path::canonicalize_with(path.clone(), &cwd) { Ok(p) => p, - Err(e) => { - return Err(ShellError::DirectoryNotFound( - v.span, - Some(format!("IO Error: {:?}", e)), - )) + Err(e1) => { + if use_abbrev { + match query(&path, None, v.span) { + Ok(p) => p, + Err(e) => { + return Err(ShellError::DirectoryNotFound( + v.span, + Some(format!("IO Error: {:?}", e)), + )) + } + } + } else { + return Err(ShellError::DirectoryNotFound( + v.span, + Some(format!("IO Error: {:?}", e1)), + )); + } } }; (path.to_string_lossy().to_string(), v.span) @@ -64,16 +79,42 @@ impl Command for Cd { let path = match nu_path::canonicalize_with(path_no_whitespace, &cwd) { Ok(p) => { if !p.is_dir() { - return Err(ShellError::NotADirectory(v.span)); - } + if use_abbrev { + // if it's not a dir, let's check to see if it's something abbreviated + match query(&p, None, v.span) { + Ok(path) => path, + Err(e) => { + return Err(ShellError::DirectoryNotFound( + v.span, + Some(format!("IO Error: {:?}", e)), + )) + } + }; + } else { + return Err(ShellError::NotADirectory(v.span)); + } + }; p } - Err(e) => { - return Err(ShellError::DirectoryNotFound( - v.span, - Some(format!("IO Error: {:?}", e)), - )) + // if canonicalize failed, let's check to see if it's abbreviated + Err(e1) => { + if use_abbrev { + match query(&path_no_whitespace, None, v.span) { + Ok(path) => path, + Err(e) => { + return Err(ShellError::DirectoryNotFound( + v.span, + Some(format!("IO Error: {:?}", e)), + )) + } + } + } else { + return Err(ShellError::DirectoryNotFound( + v.span, + Some(format!("IO Error: {:?}", e1)), + )); + } } }; (path.to_string_lossy().to_string(), v.span) @@ -136,10 +177,17 @@ impl Command for Cd { } fn examples(&self) -> Vec { - vec![Example { - description: "Change to your home directory", - example: r#"cd ~"#, - result: None, - }] + vec![ + Example { + description: "Change to your home directory", + example: r#"cd ~"#, + result: None, + }, + Example { + description: "Change to a directory via abbreviations", + example: r#"cd d/s/9"#, + result: None, + }, + ] } } diff --git a/crates/nu-command/src/filesystem/cd_query.rs b/crates/nu-command/src/filesystem/cd_query.rs new file mode 100644 index 0000000000..6cdaa6537c --- /dev/null +++ b/crates/nu-command/src/filesystem/cd_query.rs @@ -0,0 +1,549 @@ +// Attribution: +// Thanks kn team https://github.com/micouy/kn + +use alphanumeric_sort::compare_os_str; +use nu_protocol::ShellError; +use nu_protocol::Span; +use powierza_coefficient::powierża_coefficient; +use std::cmp::{Ord, Ordering}; +use std::{ + convert::AsRef, + ffi::{OsStr, OsString}, + fs::DirEntry, + mem, + path::{Component, Path, PathBuf}, +}; + +/// A path matching an abbreviation. +/// +/// Stores [`Congruence`](Congruence)'s of its ancestors, with that of the +/// closest ancestors first (so that it can be compared +/// [lexicographically](std::cmp::Ord#lexicographical-comparison). +struct Finding { + file_name: OsString, + path: PathBuf, + congruence: Vec, +} + +/// Returns an interator over directory's children matching the abbreviation. +fn get_matching_children<'a, P>( + path: &'a P, + abbr: &'a Abbr, + parent_congruence: &'a [Congruence], +) -> impl Iterator + 'a +where + P: AsRef, +{ + let filter_map_entry = move |entry: DirEntry| { + let file_type = entry.file_type().ok()?; + + if file_type.is_dir() || file_type.is_symlink() { + let file_name: String = entry.file_name().into_string().ok()?; + + if let Some(congruence) = abbr.compare(&file_name) { + let mut entry_congruence = parent_congruence.to_vec(); + entry_congruence.insert(0, congruence); + + return Some(Finding { + file_name: entry.file_name(), + congruence: entry_congruence, + path: entry.path(), + }); + } + } + + None + }; + + path.as_ref() + .read_dir() + .ok() + .map(|reader| { + reader + .filter_map(|entry| entry.ok()) + .filter_map(filter_map_entry) + }) + .into_iter() + .flatten() +} + +/// The `query` subcommand. +/// +/// It takes two args — `--abbr` and `--exclude` (optionally). The value of +/// `--abbr` gets split into a prefix containing components like `c:/`, `/`, +/// `~/`, and dots, and [`Abbr`](Abbr)'s. If there is more than one dir matching +/// the query, the value of `--exclude` is excluded from the search. +pub fn query

(arg: &P, excluded: Option, span: Span) -> Result +where + P: AsRef, +{ + // If the arg is a real path and not an abbreviation, return it. It + // prevents potential unexpected behavior due to abbreviation expansion. + // For example, `kn` doesn't allow for any component other than `Normal` in + // the abbreviation but the arg itself may be a valid path. `kn` should only + // behave differently from `cd` in situations where `cd` would fail. + if arg.as_ref().is_dir() { + return Ok(arg.as_ref().into()); + } + + let (prefix, abbrs) = parse_arg(&arg)?; + let start_dir = match prefix { + Some(start_dir) => start_dir, + None => std::env::current_dir()?, + }; + + match abbrs.as_slice() { + [] => Ok(start_dir), + [first_abbr, abbrs @ ..] => { + let mut current_level = + get_matching_children(&start_dir, first_abbr, &[]).collect::>(); + let mut next_level = vec![]; + + for abbr in abbrs { + let children = current_level.iter().flat_map(|parent| { + get_matching_children(&parent.path, abbr, &parent.congruence) + }); + + next_level.clear(); + next_level.extend(children); + + mem::swap(&mut next_level, &mut current_level); + } + + let cmp_findings = |finding_a: &Finding, finding_b: &Finding| { + finding_a + .congruence + .cmp(&finding_b.congruence) + .then(compare_os_str(&finding_a.file_name, &finding_b.file_name)) + }; + + let found_path = match excluded { + Some(excluded) if current_level.len() > 1 => current_level + .into_iter() + .filter(|finding| finding.path != excluded) + .min_by(cmp_findings) + .map(|Finding { path, .. }| path), + _ => current_level + .into_iter() + .min_by(cmp_findings) + .map(|Finding { path, .. }| path), + }; + + found_path.ok_or(ShellError::NotADirectory(span)) + } + } +} + +/// Checks if the component contains only dots and returns the equivalent number +/// of [`ParentDir`](Component::ParentDir) components if it does. +/// +/// It is the number of dots, less one. For example, `...` is converted to +/// `../..`, `....` to `../../..` etc. +fn parse_dots(component: &str) -> Option { + component + .chars() + .try_fold( + 0, + |n_dots, c| if c == '.' { Some(n_dots + 1) } else { None }, + ) + .and_then(|n_dots| if n_dots > 1 { Some(n_dots - 1) } else { None }) +} + +/// Extracts leading components of the path that are not parts of the +/// abbreviation. +/// +/// The prefix is the path where the search starts. If there is no prefix (when +/// the path consists only of normal components), the search starts in the +/// current directory, just as you'd expect. The function collects each +/// [`Prefix`](Component::Prefix), [`RootDir`](Component::RootDir), +/// [`CurDir`](Component::CurDir), and [`ParentDir`](Component::ParentDir) +/// components and stops at the first [`Normal`](Component::Normal) component +/// **unless** it only contains dots. In this case, it converts it to as many +/// [`ParentDir`](Component::ParentDir)'s as there are dots in this component, +/// less one. For example, `...` is converted to `../..`, `....` to `../../..` +/// etc. +fn extract_prefix<'a, P>( + arg: &'a P, +) -> Result<(Option, impl Iterator> + 'a), ShellError> +where + P: AsRef + ?Sized + 'a, +{ + use Component::*; + + let mut components = arg.as_ref().components().peekable(); + let mut prefix: Option = None; + let mut push_to_prefix = |component: Component| match &mut prefix { + None => prefix = Some(PathBuf::from(&component)), + Some(prefix) => prefix.push(component), + }; + let parse_dots_os = |component_os: &OsStr| { + component_os + .to_os_string() + .into_string() + .map_err(|_| ShellError::NonUnicodeInput) + .map(|component| parse_dots(&component)) + }; + + while let Some(component) = components.peek() { + match component { + Prefix(_) | RootDir | CurDir | ParentDir => push_to_prefix(*component), + Normal(component_os) => { + if let Some(n_dots) = parse_dots_os(component_os)? { + (0..n_dots).for_each(|_| push_to_prefix(ParentDir)); + } else { + break; + } + } + } + + let _consumed = components.next(); + } + + Ok((prefix, components)) +} + +/// Converts each component into [`Abbr`](Abbr) without checking +/// the component's type. +/// +/// This may change in the future. +fn parse_abbrs<'a, I>(components: I) -> Result, ShellError> +where + I: Iterator> + 'a, +{ + use Component::*; + + let abbrs = components + .into_iter() + .map(|component| match component { + Prefix(_) | RootDir | CurDir | ParentDir => { + let component_string = component + .as_os_str() + .to_os_string() + .to_string_lossy() + .to_string(); + + Err(ShellError::UnexpectedAbbrComponent(component_string)) + } + Normal(component_os) => component_os + .to_os_string() + .into_string() + .map_err(|_| ShellError::NonUnicodeInput) + .map(|string| Abbr::new_sanitized(&string)), + }) + .collect::, _>>()?; + + Ok(abbrs) +} + +/// Parses the provided argument into a prefix and [`Abbr`](Abbr)'s. +fn parse_arg

(arg: &P) -> Result<(Option, Vec), ShellError> +where + P: AsRef, +{ + let (prefix, suffix) = extract_prefix(arg)?; + let abbrs = parse_abbrs(suffix)?; + + Ok((prefix, abbrs)) +} + +#[cfg(test)] +mod test { + use super::*; + + // // #[cfg(any(test, doc))] + // // #[macro_export] + // // macro_rules! assert_variant { + // // ($expression_in:expr , $( pat )|+ $( if $guard: expr )? $( => $expression_out:expr )? ) => { + // // match $expression_in { + // // $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, + // // variant => panic!("{:?}", variant), + // // } + // // }; + + // // ($expression_in:expr , $( pat )|+ $( if $guard: expr )? $( => $expression_out:expr)? , $panic:expr) => { + // // match $expression_in { + // // $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, + // // _ => panic!($panic), + // // } + // // }; + // // } + + // /// Asserts that the expression matches the variant. Optionally returns a value. + // /// + // /// Inspired by [`std::matches`](https://doc.rust-lang.org/stable/std/macro.matches.html). + // /// + // /// # Examples + // /// + // /// ``` + // /// # fn main() -> Option<()> { + // /// use kn::Congruence::*; + // /// + // /// let abbr = Abbr::new_sanitized("abcjkl"); + // /// let coeff_1 = assert_variant!(abbr.compare("abc_jkl"), Some(Subsequence(coeff)) => coeff); + // /// let coeff_2 = assert_variant!(abbr.compare("ab_cj_kl"), Some(Subsequence(coeff)) => coeff); + // /// assert!(coeff_1 < coeff_2); + // /// # Ok(()) + // /// # } + // /// ``` + // #[cfg(any(test, doc))] + // #[macro_export] + // macro_rules! assert_variant { + // ($expression_in:expr , $( $pattern:pat )+ $( if $guard: expr )? $( => $expression_out:expr )? ) => { + // match $expression_in { + // $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, + // variant => panic!("{:?}", variant), + // } + // }; + + // ($expression_in:expr , $( $pattern:pat )+ $( if $guard: expr )? $( => $expression_out:expr)? , $panic:expr) => { + // match $expression_in { + // $( $pattern )|+ $( if $guard )? => { $( $expression_out )? }, + // _ => panic!($panic), + // } + // }; + // } + + // #[test] + // fn test_parse_dots() { + // assert_variant!(parse_dots(""), None); + // assert_variant!(parse_dots("."), None); + // assert_variant!(parse_dots(".."), Some(1)); + // assert_variant!(parse_dots("..."), Some(2)); + // assert_variant!(parse_dots("...."), Some(3)); + // assert_variant!(parse_dots("xyz"), None); + // assert_variant!(parse_dots("...dot"), None); + // } + + #[test] + fn test_extract_prefix() { + { + let (prefix, suffix) = extract_prefix("suf/fix").unwrap(); + let suffix = suffix.collect::(); + + assert_eq!(prefix, None); + assert_eq!(as_path(&suffix), as_path("suf/fix")); + } + + { + let (prefix, suffix) = extract_prefix("./.././suf/fix").unwrap(); + let suffix = suffix.collect::(); + + assert_eq!(prefix.unwrap(), as_path("./..")); + assert_eq!(as_path(&suffix), as_path("suf/fix")); + } + + { + let (prefix, suffix) = extract_prefix(".../.../suf/fix").unwrap(); + let suffix = suffix.collect::(); + + assert_eq!(prefix.unwrap(), as_path("../../../..")); + assert_eq!(as_path(&suffix), as_path("suf/fix")); + } + } + + #[test] + fn test_parse_arg_invalid_unicode() { + #[cfg(unix)] + { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let source = [0x66, 0x6f, 0x80, 0x6f]; + let non_unicode_input = OsStr::from_bytes(&source[..]).to_os_string(); + let result = parse_arg(&non_unicode_input); + + assert!(result.is_err()); + } + + #[cfg(windows)] + { + use std::os::windows::prelude::*; + + let source = [0x0066, 0x006f, 0xd800, 0x006f]; + let os_string = OsString::from_wide(&source[..]); + let result = parse_arg(&os_string); + + assert!(result.is_err()); + } + } + + #[test] + fn test_congruence_ordering() { + assert!(Complete < Prefix); + assert!(Complete < Subsequence(1)); + assert!(Prefix < Subsequence(1)); + assert!(Subsequence(1) < Subsequence(1000)); + } + + // #[test] + // fn test_compare_abbr() { + // let abbr = Abbr::new_sanitized("abcjkl"); + + // assert_variant!(abbr.compare("abcjkl"), Some(Complete)); + // assert_variant!(abbr.compare("abcjkl_"), Some(Prefix)); + // assert_variant!(abbr.compare("_abcjkl"), Some(Subsequence(0))); + // assert_variant!(abbr.compare("abc_jkl"), Some(Subsequence(1))); + + // assert_variant!(abbr.compare("xyz"), None); + // assert_variant!(abbr.compare(""), None); + // } + + // #[test] + // fn test_compare_abbr_different_cases() { + // let abbr = Abbr::new_sanitized("AbCjKl"); + + // assert_variant!(abbr.compare("aBcJkL"), Some(Complete)); + // assert_variant!(abbr.compare("AbcJkl_"), Some(Prefix)); + // assert_variant!(abbr.compare("_aBcjKl"), Some(Subsequence(0))); + // assert_variant!(abbr.compare("abC_jkL"), Some(Subsequence(1))); + // } + + // #[test] + // fn test_empty_abbr_empty_component() { + // let empty = ""; + + // let abbr = Abbr::new_sanitized(empty); + // assert_variant!(abbr.compare("non empty component"), None); + + // let abbr = Abbr::new_sanitized("non empty abbr"); + // assert_variant!(abbr.compare(empty), None); + // } + + #[test] + fn test_order_paths() { + fn sort<'a>(paths: &'a Vec<&'a str>, abbr: &str) -> Vec<&'a str> { + let abbr = Abbr::new_sanitized(abbr); + let mut paths = paths.clone(); + paths.sort_by_key(|path| abbr.compare(path).unwrap()); + + paths + } + + let paths = vec!["playground", "plotka"]; + assert_eq!(paths, sort(&paths, "pla")); + + let paths = vec!["veccentric", "vehiccles"]; + assert_eq!(paths, sort(&paths, "vecc")); + } +} + +/// Shorthand for `AsRef::as_ref(&x)`. +#[cfg(any(test, doc))] +pub fn as_path

(path: &P) -> &Path +where + P: AsRef + ?Sized, +{ + path.as_ref() +} + +/// A component of the user's query. +/// +/// It is used in comparing and ordering of found paths. Read more in +/// [`Congruence`'s docs](Congruence). +#[derive(Debug, Clone)] +pub enum Abbr { + /// Wildcard matches every component with congruence + /// [`Complete`](Congruence::Complete). + Wildcard, + + /// Literal abbreviation. + Literal(String), +} + +impl Abbr { + /// Constructs [`Abbr::Wildcard`](Abbr::Wildcard) if the + /// string slice is '-', otherwise constructs + /// wrapped [`Abbr::Literal`](Abbr::Literal) with the abbreviation + /// mapped to its ASCII lowercase equivalent. + pub fn new_sanitized(abbr: &str) -> Self { + if abbr == "-" { + Self::Wildcard + } else { + Self::Literal(abbr.to_ascii_lowercase()) + } + } + + /// Compares a component against the abbreviation. + pub fn compare(&self, component: &str) -> Option { + // What about characters with accents? [https://eev.ee/blog/2015/09/12/dark-corners-of-unicode/] + let component = component.to_ascii_lowercase(); + + match self { + Self::Wildcard => Some(Congruence::Complete), + Self::Literal(literal) => { + if literal.is_empty() || component.is_empty() { + None + } else if *literal == component { + Some(Congruence::Complete) + } else if component.starts_with(literal) { + Some(Congruence::Prefix) + } else { + powierża_coefficient(literal, &component).map(Congruence::Subsequence) + } + } + } + } +} + +/// The strength of the match between an abbreviation and a component. +/// +/// [`Congruence`](Congruence) is used to order path components in the following +/// way: +/// +/// 1. Components are first ordered based on how well they match the +/// abbreviation — first [`Complete`](Congruence::Complete), then +/// [`Prefix`](Congruence::Prefix), then +/// [`Subsequence`](Congruence::Subsequence). +/// 2. Components with congruence [`Subsequence`](Congruence::Subsequence) are +/// ordered by their [Powierża coefficient](https://github.com/micouy/powierza-coefficient). +/// 3. If the order of two components cannot be determined based on the above, [`alphanumeric_sort`](https://docs.rs/alphanumeric-sort) is used. +/// +/// Below are the results of matching components against abbreviation `abc`: +/// +/// | Component | Match strength | +/// |-------------|------------------------------------------| +/// | `abc` | [`Complete`](Congruence::Complete) | +/// | `abc___` | [`Prefix`](Congruence::Prefix) | +/// | `_a_b_c_` | [`Subsequence`](Congruence::Subsequence) | +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Congruence { + /// Either the abbreviation and the component are the same or the + /// abbreviation is a wildcard. + Complete, + + /// The abbreviation is a prefix of the component. + Prefix, + + /// The abbreviation's characters form a subsequence of the component's + /// characters. The field contains the Powierża coefficient of the pair of + /// strings. + Subsequence(u32), +} + +use Congruence::*; + +impl PartialOrd for Congruence { + fn partial_cmp(&self, other: &Self) -> Option { + Some(Ord::cmp(self, other)) + } +} + +impl Ord for Congruence { + fn cmp(&self, other: &Self) -> Ordering { + use Ordering::*; + + match (self, other) { + (Complete, Complete) => Equal, + (Complete, Prefix) => Less, + (Complete, Subsequence(_)) => Less, + + (Prefix, Complete) => Greater, + (Prefix, Prefix) => Equal, + (Prefix, Subsequence(_)) => Less, + + (Subsequence(_), Complete) => Greater, + (Subsequence(_), Prefix) => Greater, + (Subsequence(dist_a), Subsequence(dist_b)) => dist_a.cmp(dist_b), + } + } +} diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 7d6b702fe5..17ef8f54ee 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -1,4 +1,5 @@ mod cd; +mod cd_query; mod cp; mod glob; mod ls; @@ -12,6 +13,7 @@ mod util; mod watch; pub use cd::Cd; +pub use cd_query::query; pub use cp::Cp; pub use glob::Glob; pub use ls::Ls; diff --git a/crates/nu-command/tests/format_conversions/html.rs b/crates/nu-command/tests/format_conversions/html.rs index a5a71427d9..f705bd8260 100644 --- a/crates/nu-command/tests/format_conversions/html.rs +++ b/crates/nu-command/tests/format_conversions/html.rs @@ -56,7 +56,7 @@ fn test_cd_html_color_flag_dark_false() { ); assert_eq!( actual.out, - r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

Examples:
Change to your home directory
> cd ~

" + r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

Examples:
Change to your home directory
> cd ~

Change to a directory via abbreviations
>
cd
d/s/9

" ); } @@ -71,21 +71,6 @@ fn test_no_color_flag() { ); assert_eq!( actual.out, - r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

Examples:
Change to your home directory
> cd ~

" - ); -} - -#[test] -fn test_html_color_cd_flag_dark_false() { - let actual = nu!( - cwd: ".", pipeline( - r#" - cd --help | to html --html-color - "# - ) - ); - assert_eq!( - actual.out, - r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

Examples:
Change to your home directory
> cd ~

" + r"Change directory.

Usage:
> cd (path)

Flags:
-h, --help
Display this help message

Parameters:
(optional) path: the path to change to

Examples:
Change to your home directory
> cd ~

Change to a directory via abbreviations
> cd d/s/9

" ); } diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 2147d92049..50c58ed0c3 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -49,6 +49,7 @@ pub struct Config { pub shell_integration: bool, pub buffer_editor: String, pub disable_table_indexes: bool, + pub cd_with_abbreviations: bool, } impl Default for Config { @@ -77,6 +78,7 @@ impl Default for Config { shell_integration: false, buffer_editor: String::new(), disable_table_indexes: false, + cd_with_abbreviations: false, } } } @@ -265,7 +267,6 @@ impl Value { eprintln!("$config.buffer_editor is not a string") } } - "disable_table_indexes" => { if let Ok(b) = value.as_bool() { config.disable_table_indexes = b; @@ -273,6 +274,13 @@ impl Value { eprintln!("$config.disable_table_indexes is not a bool") } } + "cd_with_abbreviations" => { + if let Ok(b) = value.as_bool() { + config.cd_with_abbreviations = b; + } else { + eprintln!("$config.disable_table_indexes is not a bool") + } + } x => { eprintln!("$config.{} is an unknown config setting", x) } diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 97fb9bb46a..08d61bb6f0 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -636,6 +636,27 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE String, #[label = "'{0}' is deprecated. Please use '{1}' instead."] Span, ), + + /// Non-Unicode input received. + /// + /// ## Resolution + /// + /// Check that your path is UTF-8 compatible. + #[error("Non-Unicode input received.")] + #[diagnostic(code(nu::shell::non_unicode_input), url(docsrs))] + NonUnicodeInput, + + // /// Path not found. + // #[error("Path not found.")] + // PathNotFound, + /// Unexpected abbr component. + /// + /// ## Resolution + /// + /// Check the path abbreviation to ensure that it is valid. + #[error("Unexpected abbr component `{0}`.")] + #[diagnostic(code(nu::shell::unexpected_path_abbreviateion), url(docsrs))] + UnexpectedAbbrComponent(String), } impl From for ShellError { diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index 73bda6f701..6e680a5e8c 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -198,6 +198,7 @@ let-env config = { sync_history_on_enter: true # Enable to share the history between multiple sessions, else you have to close the session to persist history to file shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue disable_table_indexes: false # set to true to remove the index column from tables + cd_with_abbreviations: false # set to true to allow you to do things like cd s/o/f and nushell expand it to cd some/other/folder menus: [ # Configuration for default nushell menus # Note the lack of souce parameter