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:
Brandon Thomas
2026-10-25 10:37:54 -04:00
committed by GitHub
29 changed files with 101 additions and 5 deletions
+7 -1
View File
@@ -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");
+22 -3
View File
@@ -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