From 19645575d60a52d832cf126ba97b779f4b191617 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Mon, 8 Nov 2021 10:48:50 +1300 Subject: [PATCH] Add 'did you mean' error (#305) --- crates/nu-protocol/src/shell_error.rs | 83 +++++++++++++++++++++++++++ crates/nu-protocol/src/value/mod.rs | 19 +++--- src/tests.rs | 4 +- 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 4c55d8610b..033d720a31 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -182,6 +182,10 @@ pub enum ShellError { #[error("Plugin error")] PluginError(String), + + #[error("Name not found")] + #[diagnostic(code(nu::shell::name_not_found), url(docsrs))] + DidYouMean(String, #[label("did you mean '{0}'?")] Span), } impl From for ShellError { @@ -201,3 +205,82 @@ impl From> for ShellError { ShellError::InternalError(format!("{:?}", input)) } } + +pub fn did_you_mean(possibilities: &[String], tried: &str) -> Option { + let mut possible_matches: Vec<_> = possibilities + .iter() + .map(|word| { + let edit_distance = levenshtein_distance(word, tried); + (edit_distance, word.to_owned()) + }) + .collect(); + + possible_matches.sort(); + + if let Some((_, first)) = possible_matches.into_iter().next() { + Some(first) + } else { + None + } +} + +// Borrowed from here https://github.com/wooorm/levenshtein-rs +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let mut result = 0; + + /* Shortcut optimizations / degenerate cases. */ + if a == b { + return result; + } + + let length_a = a.chars().count(); + let length_b = b.chars().count(); + + if length_a == 0 { + return length_b; + } + + if length_b == 0 { + return length_a; + } + + /* Initialize the vector. + * + * This is why it’s fast, normally a matrix is used, + * here we use a single vector. */ + let mut cache: Vec = (1..).take(length_a).collect(); + let mut distance_a; + let mut distance_b; + + /* Loop. */ + for (index_b, code_b) in b.chars().enumerate() { + result = index_b; + distance_a = index_b; + + for (index_a, code_a) in a.chars().enumerate() { + distance_b = if code_a == code_b { + distance_a + } else { + distance_a + 1 + }; + + distance_a = cache[index_a]; + + result = if distance_a > result { + if distance_b > result { + result + 1 + } else { + distance_b + } + } else if distance_b > distance_a { + distance_a + 1 + } else { + distance_b + }; + + cache[index_a] = result; + } + } + + result +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 39df86cf7d..516cd31dbf 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; use std::{cmp::Ordering, fmt::Debug}; use crate::ast::{CellPath, PathMember}; -use crate::{span, BlockId, Span, Spanned, Type}; +use crate::{did_you_mean, span, BlockId, Span, Spanned, Type}; use crate::ShellError; @@ -269,17 +269,16 @@ impl Value { span: origin_span, } => match &mut current { Value::Record { cols, vals, span } => { + let cols = cols.clone(); let span = *span; - let mut found = false; - for col in cols.iter().zip(vals.iter()) { - if col.0 == column_name { - current = col.1.clone(); - found = true; - break; - } - } - if !found { + if let Some(found) = + cols.iter().zip(vals.iter()).find(|x| x.0 == column_name) + { + current = found.1.clone(); + } else if let Some(suggestion) = did_you_mean(&cols, column_name) { + return Err(ShellError::DidYouMean(suggestion, *origin_span)); + } else { return Err(ShellError::CantFindColumn(*origin_span, span)); } } diff --git a/src/tests.rs b/src/tests.rs index 36b7b36d52..dc5e40326f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -730,7 +730,7 @@ fn earlier_errors() -> TestResult { fn missing_column_error() -> TestResult { fail_test( r#"([([[name, size]; [ABC, 10], [DEF, 20]]).1, ([[name]; [HIJ]]).0]).size | table"#, - "cannot find column", + "did you mean 'name'?", ) } @@ -829,7 +829,7 @@ fn shorthand_env_3() -> TestResult { #[test] fn shorthand_env_4() -> TestResult { - fail_test(r#"FOO=BAZ FOO= $nu.env.FOO"#, "cannot find column") + fail_test(r#"FOO=BAZ FOO= $nu.env.FOO"#, "did you mean") } #[test]