feat(commands): show command suggestions if a command was not found

This commit is contained in:
dusk 2025-01-11 22:38:29 +09:00
parent ee45fca6ab
commit f0d287b873
No known key found for this signature in database
7 changed files with 58 additions and 23 deletions

View file

@ -26,10 +26,10 @@ public class Parameters
// just used for errors, temporarily // just used for errors, temporarily
public string FullCommand { get; init; } public string FullCommand { get; init; }
public Parameters(string cmd) public Parameters(string prefix, string cmd)
{ {
FullCommand = cmd; FullCommand = cmd;
var result = CommandsMethods.ParseCommand(cmd); var result = CommandsMethods.ParseCommand(prefix, cmd);
if (result is CommandResult.Ok) if (result is CommandResult.Ok)
{ {
var command = ((CommandResult.Ok)result).@command; var command = ((CommandResult.Ok)result).@command;

View file

@ -142,7 +142,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
Parameters parameters; Parameters parameters;
try try
{ {
parameters = new Parameters(evt.Content?.Substring(cmdStart)); parameters = new Parameters(evt.Content?.Substring(0, cmdStart), evt.Content?.Substring(cmdStart));
} }
catch (PKError e) catch (PKError e)
{ {

View file

@ -57,7 +57,7 @@ impl Display for Command {
write!(f, " ")?; write!(f, " ")?;
} }
} }
write!(f, " - {}", self.help) Ok(())
} }
} }

View file

@ -1,5 +1,5 @@
namespace commands { namespace commands {
CommandResult parse_command(string input); CommandResult parse_command(string prefix, string input);
}; };
[Enum] [Enum]
interface CommandResult { interface CommandResult {

View file

@ -9,6 +9,7 @@ uniffi::include_scaffolding!("commands");
use core::panic; use core::panic;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Write;
use smol_str::{format_smolstr, SmolStr}; use smol_str::{format_smolstr, SmolStr};
use tree::TreeBranch; use tree::TreeBranch;
@ -51,7 +52,7 @@ pub struct ParsedCommand {
pub flags: HashMap<String, Option<String>>, pub flags: HashMap<String, Option<String>>,
} }
pub fn parse_command(input: String) -> CommandResult { pub fn parse_command(prefix: String, input: String) -> CommandResult {
let input: SmolStr = input.into(); let input: SmolStr = input.into();
let mut local_tree: TreeBranch = COMMAND_TREE.clone(); let mut local_tree: TreeBranch = COMMAND_TREE.clone();
@ -91,11 +92,11 @@ pub fn parse_command(input: String) -> CommandResult {
} }
} }
Err(None) => { Err(None) => {
if let Some(command_ref) = local_tree.callback() { if let Some(command) = local_tree.command() {
println!("{command_ref} {params:?}"); println!("{} {params:?}", command.cb);
return CommandResult::Ok { return CommandResult::Ok {
command: ParsedCommand { command: ParsedCommand {
command_ref: command_ref.into(), command_ref: command.cb.into(),
params, params,
args, args,
flags, flags,
@ -103,13 +104,26 @@ pub fn parse_command(input: String) -> CommandResult {
}; };
} }
println!("{possible_tokens:?}"); let mut error = format!("Unknown command `{prefix}{input}`.");
let possible_commands = local_tree.possible_commands(2);
if !possible_commands.is_empty() {
error.push_str(" Perhaps you meant to use one of the commands below:\n");
for command in possible_commands {
writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help)
.expect("oom");
}
} else {
error.push_str("\n");
}
error.push_str(
"For a list of possible commands, see <https://pluralkit.me/commands>.",
);
// todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if last token is a common incorrect unquote (multi-member names etc)
// todo: check if this is a system name in pk;s command // todo: check if this is a system name in pk;s command
return CommandResult::Err { return CommandResult::Err { error };
error: format!("Unknown command `{input}`. For a list of possible commands, see <https://pluralkit.me/commands>."),
};
} }
Err(Some(short_circuit)) => { Err(Some(short_circuit)) => {
return CommandResult::Err { return CommandResult::Err {

View file

@ -8,8 +8,12 @@ fn main() {
.intersperse(" ".to_string()) .intersperse(" ".to_string())
.collect::<String>(); .collect::<String>();
if !cmd.is_empty() { if !cmd.is_empty() {
let parsed = commands::parse_command(cmd); use commands::CommandResult;
println!("{:#?}", parsed); let parsed = commands::parse_command("pk;".to_string(), cmd);
match parsed {
CommandResult::Ok { command } => println!("{command:#?}"),
CommandResult::Err { error } => println!("{error}"),
}
} else { } else {
for command in cmds::all() { for command in cmds::all() {
println!("{}", command); println!("{}", command);

View file

@ -1,18 +1,17 @@
use ordermap::OrderMap; use ordermap::OrderMap;
use smol_str::SmolStr;
use crate::{commands::Command, Token}; use crate::{commands::Command, Token};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TreeBranch { pub struct TreeBranch {
current_command_key: Option<SmolStr>, current_command: Option<Command>,
branches: OrderMap<Token, TreeBranch>, branches: OrderMap<Token, TreeBranch>,
} }
impl TreeBranch { impl TreeBranch {
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self {
current_command_key: None, current_command: None,
branches: OrderMap::new(), branches: OrderMap::new(),
} }
} }
@ -20,10 +19,10 @@ impl TreeBranch {
pub fn register_command(&mut self, command: Command) { pub fn register_command(&mut self, command: Command) {
let mut current_branch = self; let mut current_branch = self;
// iterate over tokens in command // iterate over tokens in command
for token in command.tokens { for token in command.tokens.clone() {
// recursively get or create a sub-branch for each token // recursively get or create a sub-branch for each token
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
current_command_key: None, current_command: None,
branches: OrderMap::new(), branches: OrderMap::new(),
}) })
} }
@ -31,20 +30,38 @@ impl TreeBranch {
current_branch.branches.insert( current_branch.branches.insert(
Token::Empty, Token::Empty,
TreeBranch { TreeBranch {
current_command_key: Some(command.cb), current_command: Some(command),
branches: OrderMap::new(), branches: OrderMap::new(),
}, },
); );
} }
pub fn callback(&self) -> Option<SmolStr> { pub fn command(&self) -> Option<Command> {
self.current_command_key.clone() self.current_command.clone()
} }
pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> { pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
self.branches.keys() self.branches.keys()
} }
pub fn possible_commands(&self, max_depth: usize) -> Vec<Command> {
if max_depth == 0 {
return Vec::new();
}
let mut commands = Vec::new();
for token in self.possible_tokens() {
if let Some(tree) = self.get_branch(token) {
if let Some(command) = tree.command() {
commands.push(command);
// we dont need to look further if we found a command
continue;
}
commands.append(&mut tree.possible_commands(max_depth - 1));
}
}
commands
}
pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> {
self.branches.get(token) self.branches.get(token)
} }