mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Merge pull request #4060 from dotCooCoo/feat/unknown-command-suggestion
Commands: an unknown command id is answered with the nearest command id
This commit is contained in:
@@ -56,7 +56,13 @@ pub(super) fn action_batch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, "batches cannot nest"));
|
||||
}
|
||||
if find_command(id).is_none() {
|
||||
return Err(bad(C, format!("commands[{i}]: unknown command `{id}`")));
|
||||
return Err(bad(
|
||||
C,
|
||||
match super::super::params::nearest_command(id) {
|
||||
Some(near) => format!("commands[{i}]: unknown command `{id}`; did you mean `{near}`?"),
|
||||
None => format!("commands[{i}]: unknown command `{id}`"),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
let source = str_param(p, "source").unwrap_or("folder");
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{bad, find_command};
|
||||
use super::{bad, command_specs, find_command};
|
||||
use crate::Result;
|
||||
|
||||
/// What a params description says about the keys of the params object.
|
||||
@@ -208,8 +208,12 @@ fn distance(a: &str, b: &str) -> usize {
|
||||
|
||||
/// The key of `keys` a misspelt `key` most likely meant: the nearest one, when it is near.
|
||||
fn nearest<'a>(key: &str, keys: &[&'a str]) -> Option<&'a str> {
|
||||
let near = keys.iter().map(|k| (distance(key, k), *k)).min()?;
|
||||
(near.0 <= (key.chars().count() / 3).max(1)).then_some(near.1)
|
||||
let len = key.chars().count();
|
||||
let most = (len / 3).max(1);
|
||||
// A key whose length differs by more than `most` is more than `most` edits away and isn't
|
||||
// measured.
|
||||
let near = keys.iter().filter(|k| k.chars().count().abs_diff(len) <= most).map(|k| (distance(key, k), *k)).min()?;
|
||||
(near.0 <= most).then_some(near.1)
|
||||
}
|
||||
|
||||
/// `params` has only keys the description `doc` of command `id` names, or the parameter error
|
||||
@@ -480,6 +484,21 @@ pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The command id a misspelled `id`, which names no command, most likely meant: the nearest one,
|
||||
/// when it is near (as [`check_keys`] matches a misspelled key).
|
||||
pub(crate) fn nearest_command(id: &str) -> Option<&'static str> {
|
||||
let ids: Vec<&'static str> = command_specs().iter().map(|c| c.id).collect();
|
||||
nearest(id, &ids)
|
||||
}
|
||||
|
||||
/// The error for `id`, which names no command: with the nearest command id, when one is near.
|
||||
pub(crate) fn unknown_command(id: &str) -> crate::EngineError {
|
||||
match nearest_command(id) {
|
||||
Some(near) => crate::EngineError::UnknownCommandNear(id.to_string(), near.to_string()),
|
||||
None => crate::EngineError::UnknownCommand(id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// [`check_params`], and [`check_effect_params`] for the effects `effect.setParams` edits in
|
||||
/// this session's document (not as a step of a `command.batch`: what it edits is only there
|
||||
|
||||
@@ -44,6 +44,9 @@ pub use vectorcraft_tools as tools;
|
||||
pub enum EngineError {
|
||||
#[error("unknown command `{0}`")]
|
||||
UnknownCommand(String),
|
||||
/// An unknown command id, with the command id it most likely misspells.
|
||||
#[error("unknown command `{0}`; did you mean `{1}`?")]
|
||||
UnknownCommandNear(String, String),
|
||||
#[error("command `{0}` is not available right now: {1}")]
|
||||
Disabled(String, String),
|
||||
#[error("invalid parameters for `{cmd}`: {msg}")]
|
||||
@@ -1784,7 +1787,7 @@ impl Session {
|
||||
|
||||
/// Execute a command by id. This is THE entry point for every frontend.
|
||||
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))?;
|
||||
let spec = find_command(id).ok_or_else(|| cmd::params::unknown_command(id))?;
|
||||
// The crate's own tests are the audit of the params descriptions (#1442): a key a test
|
||||
// passes that its command's description doesn't name fails the test.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -90,6 +90,42 @@ fn unknown_and_disabled() {
|
||||
assert!(matches!(s.execute("object.group", &json!({})), Err(EngineError::Disabled(..))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_command_names_the_nearest_id() {
|
||||
let mut s = session();
|
||||
for typo in ["select.al", "Select.All", "selcet.all"] {
|
||||
let e = s.execute(typo, &json!({})).unwrap_err();
|
||||
assert!(matches!(&e, EngineError::UnknownCommandNear(id, near) if id == typo && near == "select.all"), "{typo}: {e:?}");
|
||||
assert_eq!(e.to_string(), format!("unknown command `{typo}`; did you mean `select.all`?"));
|
||||
}
|
||||
let e = s.execute("nope", &json!({})).unwrap_err();
|
||||
assert!(matches!(&e, EngineError::UnknownCommand(id) if id == "nope"), "{e:?}");
|
||||
assert_eq!(e.to_string(), "unknown command `nope`");
|
||||
let e = s.execute("command.batch", &json!({"commands": [{"command": "select.al", "params": {}}]})).unwrap_err();
|
||||
assert!(e.to_string().contains("did you mean `select.all`?"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_unknown_command_id_is_answered_at_once() {
|
||||
let mut s = session();
|
||||
let long = "x".repeat(200_000);
|
||||
let start = std::time::Instant::now();
|
||||
assert!(matches!(s.execute(&long, &json!({})), Err(EngineError::UnknownCommand(_))));
|
||||
let e = crate::check_params("file.new", &json!({ long.as_str(): 1 })).unwrap_err();
|
||||
assert!(e.to_string().contains("the command takes:"), "{e}");
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(2), "{:?}", start.elapsed());
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn action_batch_names_the_nearest_command_id() {
|
||||
let mut s = session();
|
||||
let e = s.execute("action.batch", &json!({"source": "dataSets", "commands": [{"command": "select.al"}]})).unwrap_err();
|
||||
assert_eq!(e.to_string(), "invalid parameters for `action.batch`: commands[0]: unknown command `select.al`; did you mean `select.all`?");
|
||||
let e = s.execute("action.batch", &json!({"source": "dataSets", "commands": [{"command": "nope"}]})).unwrap_err();
|
||||
assert_eq!(e.to_string(), "invalid parameters for `action.batch`: commands[0]: unknown command `nope`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_ungroup() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user