use crate::action::DirectKind;
pub struct Command {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub description: &'static str,
pub args: Args,
pub kind: Kind,
}
pub enum Args {
None,
Path,
Sub(&'static [Subcommand]),
}
pub struct Subcommand {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub description: &'static str,
}
pub enum Kind {
Direct(DirectKind),
Inline(Inline),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Inline {
Copilot,
Grammar,
Agent,
Theme,
Bookmarks,
Conflict,
}
pub const COPILOT_SUBCOMMANDS: &[Subcommand] = &[
Subcommand {
name: "status",
aliases: &[],
description: "show Copilot auth state",
},
Subcommand {
name: "signin",
aliases: &["login"],
description: "start device-flow sign-in",
},
Subcommand {
name: "signout",
aliases: &["logout"],
description: "sign out of Copilot",
},
Subcommand {
name: "code",
aliases: &[],
description: "re-show signin modal + re-copy code",
},
];
pub const AGENT_SUBCOMMANDS: &[Subcommand] = &[
Subcommand {
name: "explain",
aliases: &[],
description: "explain @file / @selection with the agent",
},
Subcommand {
name: "chat",
aliases: &[],
description: "discuss @file / @selection with the agent",
},
];
pub const GRAMMAR_SUBCOMMANDS: &[Subcommand] = &[
Subcommand {
name: "list",
aliases: &["ls"],
description: "browse, install & remove (modal)",
},
Subcommand {
name: "install",
aliases: &["add"],
description: "install <name>... | --all",
},
Subcommand {
name: "remove",
aliases: &["rm", "uninstall"],
description: "remove <name>...",
},
];
pub const CONFLICT_SUBCOMMANDS: &[Subcommand] = &[
Subcommand {
name: "ours",
aliases: &["local", "top"],
description: "keep the top side (ours / local)",
},
Subcommand {
name: "theirs",
aliases: &["disk", "bottom"],
description: "keep the bottom side (theirs / disk)",
},
Subcommand {
name: "both",
aliases: &["all"],
description: "keep both sides, drop the markers",
},
Subcommand {
name: "none",
aliases: &["remove"],
description: "delete the whole conflict",
},
];
pub const BOOKMARK_SUBCOMMANDS: &[Subcommand] = &[
Subcommand {
name: "list",
aliases: &["ls"],
description: "open the bookmark picker (modal)",
},
Subcommand {
name: "add",
aliases: &[],
description: "bookmark the current buffer at the cursor",
},
Subcommand {
name: "delete",
aliases: &["del", "rm"],
description: "remove the current buffer's bookmark",
},
];
impl Command {
pub fn find(name: &str) -> Option<&'static Command> {
COMMANDS
.iter()
.find(|c| c.name == name || c.aliases.contains(&name))
}
pub fn all_names(&self) -> impl Iterator<Item = &'static str> {
std::iter::once(self.name).chain(self.aliases.iter().copied())
}
}
pub fn resolve_subcommand(subs: &'static [Subcommand], token: &str) -> Option<&'static str> {
subs.iter()
.find(|s| s.name == token || s.aliases.contains(&token))
.map(|s| s.name)
}
pub const COMMANDS: &[Command] = &[
Command {
name: "q",
aliases: &["quit"],
description: "quit",
args: Args::None,
kind: Kind::Direct(DirectKind::Quit),
},
Command {
name: "q!",
aliases: &["quit!"],
description: "force quit",
args: Args::None,
kind: Kind::Direct(DirectKind::QuitForce),
},
Command {
name: "w",
aliases: &["write"],
description: "save (or :w <path>)",
args: Args::Path,
kind: Kind::Direct(DirectKind::Save),
},
Command {
name: "w!",
aliases: &["write!"],
description: "save, creating dirs",
args: Args::Path,
kind: Kind::Direct(DirectKind::SaveForce),
},
Command {
name: "wq",
aliases: &["x"],
description: "save & quit",
args: Args::Path,
kind: Kind::Direct(DirectKind::SaveAndQuit),
},
Command {
name: "e",
aliases: &["edit"],
description: "open <path>",
args: Args::Path,
kind: Kind::Direct(DirectKind::Open),
},
Command {
name: "bn",
aliases: &["bnext"],
description: "next buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferNext),
},
Command {
name: "bp",
aliases: &["bprev"],
description: "previous buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferPrev),
},
Command {
name: "bd",
aliases: &["bdelete"],
description: "delete buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferDelete),
},
Command {
name: "bd!",
aliases: &["bdelete!", "bc", "bc!"],
description: "force delete buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferDeleteForce),
},
Command {
name: "bca",
aliases: &["bca!"],
description: "force delete all buffers",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferDeleteAll),
},
Command {
name: "bls",
aliases: &["buffers"],
description: "buffer picker",
args: Args::None,
kind: Kind::Direct(DirectKind::BufferList),
},
Command {
name: "new",
aliases: &["enew"],
description: "new scratch buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::NewScratchBuffer),
},
Command {
name: "goto",
aliases: &[],
description: "go to line <n>",
args: Args::None,
kind: Kind::Direct(DirectKind::GotoLine),
},
Command {
name: "jumps",
aliases: &[],
description: "jump history picker",
args: Args::None,
kind: Kind::Direct(DirectKind::JumpList),
},
Command {
name: "bookmarks",
aliases: &["bm"],
description: "list / add / delete bookmarks",
args: Args::Sub(BOOKMARK_SUBCOMMANDS),
kind: Kind::Inline(Inline::Bookmarks),
},
Command {
name: "conflict",
aliases: &[],
description: "resolve the conflict at the cursor (]c / [c to navigate)",
args: Args::Sub(CONFLICT_SUBCOMMANDS),
kind: Kind::Inline(Inline::Conflict),
},
Command {
name: "log",
aliases: &[],
description: "open debug log file",
args: Args::None,
kind: Kind::Direct(DirectKind::OpenLog),
},
Command {
name: "lsp",
aliases: &[],
description: "show LSP for current buffer (:lsp all for every language)",
args: Args::None,
kind: Kind::Direct(DirectKind::LspStatus),
},
Command {
name: "reload",
aliases: &["e!"],
description: "reload buffer from disk (undo restores)",
args: Args::None,
kind: Kind::Direct(DirectKind::Reload),
},
Command {
name: "reload-all",
aliases: &[],
description: "reload every file-backed buffer",
args: Args::None,
kind: Kind::Direct(DirectKind::ReloadAll),
},
Command {
name: "noh",
aliases: &["nohl", "nohlsearch"],
description: "clear search highlight",
args: Args::None,
kind: Kind::Direct(DirectKind::ClearSearch),
},
Command {
name: "split",
aliases: &["sp", "nh"],
description: "split pane below",
args: Args::None,
kind: Kind::Direct(DirectKind::SplitWindowHorizontal),
},
Command {
name: "vsplit",
aliases: &["vsp", "vs", "nv"],
description: "split pane right",
args: Args::None,
kind: Kind::Direct(DirectKind::SplitWindowVertical),
},
Command {
name: "close",
aliases: &["clo"],
description: "close active pane",
args: Args::None,
kind: Kind::Direct(DirectKind::CloseWindow),
},
Command {
name: "only",
aliases: &["on"],
description: "(future) close all but active pane",
args: Args::None,
kind: Kind::Direct(DirectKind::CloseWindow),
},
Command {
name: "copilot",
aliases: &[],
description: "Copilot status / signin / signout",
args: Args::Sub(COPILOT_SUBCOMMANDS),
kind: Kind::Inline(Inline::Copilot),
},
Command {
name: "grammar",
aliases: &[],
description: "install / remove tree-sitter grammars",
args: Args::Sub(GRAMMAR_SUBCOMMANDS),
kind: Kind::Inline(Inline::Grammar),
},
Command {
name: "agent",
aliases: &[],
description: "launch an AI agent (or :agent explain/chat @file)",
args: Args::Sub(AGENT_SUBCOMMANDS),
kind: Kind::Inline(Inline::Agent),
},
Command {
name: "theme",
aliases: &["colorscheme", "colo"],
description: "pick a color theme (live preview)",
args: Args::None,
kind: Kind::Inline(Inline::Theme),
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_resolves_name_and_alias() {
assert!(matches!(
Command::find("w").map(|c| &c.kind),
Some(Kind::Direct(DirectKind::Save))
));
assert!(matches!(
Command::find("write").map(|c| &c.kind),
Some(Kind::Direct(DirectKind::Save))
));
assert!(Command::find("nope").is_none());
}
#[test]
fn inline_commands_are_in_the_table() {
for (name, inline) in [
("copilot", Inline::Copilot),
("grammar", Inline::Grammar),
("agent", Inline::Agent),
("bookmarks", Inline::Bookmarks),
("bm", Inline::Bookmarks),
] {
match Command::find(name).map(|c| &c.kind) {
Some(Kind::Inline(i)) => assert_eq!(*i, inline),
other => panic!("{name} should be Inline, got {:?}", other.is_some()),
}
}
}
#[test]
fn path_commands_take_a_path() {
assert!(matches!(
Command::find("e").map(|c| &c.args),
Some(Args::Path)
));
assert!(matches!(
Command::find("q").map(|c| &c.args),
Some(Args::None)
));
}
#[test]
fn resolve_subcommand_handles_aliases() {
assert_eq!(
resolve_subcommand(COPILOT_SUBCOMMANDS, "login"),
Some("signin")
);
assert_eq!(resolve_subcommand(GRAMMAR_SUBCOMMANDS, "ls"), Some("list"));
assert_eq!(resolve_subcommand(GRAMMAR_SUBCOMMANDS, "nope"), None);
assert_eq!(
resolve_subcommand(BOOKMARK_SUBCOMMANDS, "rm"),
Some("delete")
);
assert_eq!(resolve_subcommand(BOOKMARK_SUBCOMMANDS, "ls"), Some("list"));
}
}