feat(command): implement command aliasing

Implement command aliasing in CmdRegistry and update command parsing to
resolve aliases.

- Added `aliases` field to `CmdRegistry` .
- Added `alias()` method to register short names.
- Added `resolve()` method to map aliases to canonical names.
- Updated `parse()` and `interactive()` to use `resolve()` .
- Added unit tests for alias resolution in `src/command/parse.rs` .

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-08 22:27:36 -07:00
parent fb85e98abe
commit 1813d2a662
2 changed files with 77 additions and 0 deletions

View File

@ -113,15 +113,32 @@ struct CmdEntry {
#[derive(Default)]
pub struct CmdRegistry {
entries: Vec<CmdEntry>,
aliases: Vec<(&'static str, &'static str)>,
}
impl CmdRegistry {
pub fn new() -> Self {
Self {
entries: Vec::new(),
aliases: Vec::new(),
}
}
/// Register a short name that resolves to a canonical command name.
pub fn alias(&mut self, short: &'static str, canonical: &'static str) {
self.aliases.push((short, canonical));
}
/// Resolve a command name through the alias table.
fn resolve<'a>(&'a self, name: &'a str) -> &'a str {
for (alias, canonical) in &self.aliases {
if *alias == name {
return canonical;
}
}
name
}
/// Register a command with both a text parser and an interactive constructor.
/// The name is derived from a prototype command instance.
pub fn register(&mut self, prototype: &dyn Cmd, parse: ParseFn, interactive: InteractiveFn) {
@ -162,6 +179,7 @@ impl CmdRegistry {
/// Construct a command from text arguments (script/headless).
pub fn parse(&self, name: &str, args: &[String]) -> Result<Box<dyn Cmd>, String> {
let name = self.resolve(name);
for e in &self.entries {
if e.name == name {
return (e.parse)(args);
@ -179,6 +197,7 @@ impl CmdRegistry {
args: &[String],
ctx: &CmdContext,
) -> Result<Box<dyn Cmd>, String> {
let name = self.resolve(name);
for e in &self.entries {
if e.name == name {
return (e.interactive)(args, ctx);
@ -2781,6 +2800,12 @@ pub fn default_registry() -> CmdRegistry {
// ── Wizard ───────────────────────────────────────────────────────────
r.register_nullary(|| Box::new(HandleWizardKey));
// ── Aliases (short names for common commands) ────────────────────────
r.alias("add-cat", "add-category");
r.alias("formula", "add-formula");
r.alias("add-view", "create-view");
r.alias("q!", "force-quit");
r
}

View File

@ -181,4 +181,56 @@ mod tests {
assert!(parse_line("add-category").is_err());
assert!(parse_line("set-cell 100").is_err());
}
// ── Alias resolution ────────────────────────────────────────────────
#[test]
fn alias_add_cat_resolves_to_add_category() {
let cmds = parse_line("add-cat Region").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "add-category");
}
#[test]
fn alias_formula_resolves_to_add_formula() {
let cmds = parse_line(r#"formula Product "Total = A + B""#).unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "add-formula");
}
#[test]
fn alias_add_view_resolves_to_create_view() {
let cmds = parse_line("add-view MyView").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "create-view");
}
#[test]
fn alias_q_bang_resolves_to_force_quit() {
let cmds = parse_line("q!").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "force-quit");
}
#[test]
fn alias_does_not_interfere_with_canonical_q() {
let cmds = parse_line("q").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "q");
}
// ── add-items command ───────────────────────────────────────────────
#[test]
fn parse_add_items_multiple() {
let cmds = parse_line("add-items Region North South East").unwrap();
assert_eq!(cmds.len(), 1);
assert_eq!(cmds[0].name(), "add-items");
}
#[test]
fn add_items_requires_at_least_two_args() {
assert!(parse_line("add-items").is_err());
assert!(parse_line("add-items Region").is_err());
}
}