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
public string FullCommand { get; init; }
public Parameters(string cmd)
public Parameters(string prefix, string cmd)
{
FullCommand = cmd;
var result = CommandsMethods.ParseCommand(cmd);
var result = CommandsMethods.ParseCommand(prefix, cmd);
if (result is CommandResult.Ok)
{
var command = ((CommandResult.Ok)result).@command;

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ uniffi::include_scaffolding!("commands");
use core::panic;
use std::collections::HashMap;
use std::fmt::Write;
use smol_str::{format_smolstr, SmolStr};
use tree::TreeBranch;
@ -51,7 +52,7 @@ pub struct ParsedCommand {
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 mut local_tree: TreeBranch = COMMAND_TREE.clone();
@ -91,11 +92,11 @@ pub fn parse_command(input: String) -> CommandResult {
}
}
Err(None) => {
if let Some(command_ref) = local_tree.callback() {
println!("{command_ref} {params:?}");
if let Some(command) = local_tree.command() {
println!("{} {params:?}", command.cb);
return CommandResult::Ok {
command: ParsedCommand {
command_ref: command_ref.into(),
command_ref: command.cb.into(),
params,
args,
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 this is a system name in pk;s command
return CommandResult::Err {
error: format!("Unknown command `{input}`. For a list of possible commands, see <https://pluralkit.me/commands>."),
};
return CommandResult::Err { error };
}
Err(Some(short_circuit)) => {
return CommandResult::Err {

View file

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

View file

@ -1,18 +1,17 @@
use ordermap::OrderMap;
use smol_str::SmolStr;
use crate::{commands::Command, Token};
#[derive(Debug, Clone)]
pub struct TreeBranch {
current_command_key: Option<SmolStr>,
current_command: Option<Command>,
branches: OrderMap<Token, TreeBranch>,
}
impl TreeBranch {
pub fn empty() -> Self {
Self {
current_command_key: None,
current_command: None,
branches: OrderMap::new(),
}
}
@ -20,10 +19,10 @@ impl TreeBranch {
pub fn register_command(&mut self, command: Command) {
let mut current_branch = self;
// 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
current_branch = current_branch.branches.entry(token).or_insert(TreeBranch {
current_command_key: None,
current_command: None,
branches: OrderMap::new(),
})
}
@ -31,20 +30,38 @@ impl TreeBranch {
current_branch.branches.insert(
Token::Empty,
TreeBranch {
current_command_key: Some(command.cb),
current_command: Some(command),
branches: OrderMap::new(),
},
);
}
pub fn callback(&self) -> Option<SmolStr> {
self.current_command_key.clone()
pub fn command(&self) -> Option<Command> {
self.current_command.clone()
}
pub fn possible_tokens(&self) -> impl Iterator<Item = &Token> {
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> {
self.branches.get(token)
}