#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExVerb {
pub full: &'static str,
pub min: usize,
pub plain: &'static str,
pub forced: &'static str,
}
impl ExVerb {
#[must_use]
pub fn spelled_by(&self, word: &str) -> bool {
word.len() >= self.min && self.full.len() >= word.len() && self.full.starts_with(word)
}
#[must_use]
pub const fn command(&self, bang: bool) -> &'static str {
if bang { self.forced } else { self.plain }
}
}
pub const VERBS: &[ExVerb] = &[
ExVerb { full: "write", min: 1, plain: "save", forced: "save" },
ExVerb { full: "wall", min: 2, plain: "buffer.write-all", forced: "buffer.write-all" },
ExVerb { full: "wq", min: 2, plain: "write-quit", forced: "write-quit" },
ExVerb { full: "wqall", min: 3, plain: "write-quit-all", forced: "write-quit-all" },
ExVerb { full: "xit", min: 1, plain: "exit-write", forced: "exit-write" },
ExVerb { full: "xall", min: 2, plain: "write-quit-all", forced: "write-quit-all" },
ExVerb { full: "exit", min: 3, plain: "exit-write", forced: "exit-write" },
ExVerb { full: "quit", min: 1, plain: "quit", forced: "quit!" },
ExVerb { full: "qall", min: 2, plain: "quit-all", forced: "quit-all!" },
ExVerb { full: "quitall", min: 5, plain: "quit-all", forced: "quit-all!" },
ExVerb { full: "undo", min: 1, plain: "undo", forced: "undo" },
ExVerb { full: "redo", min: 3, plain: "redo", forced: "redo" },
];
#[must_use]
pub fn resolve(word: &str) -> Option<&'static ExVerb> {
VERBS.iter().find(|v| v.spelled_by(word))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Invocation {
pub command: String,
pub args: Vec<String>,
}
#[must_use]
pub fn parse(line: &str) -> Option<Invocation> {
let line = line.trim();
let line = line.strip_prefix(':').unwrap_or(line);
let mut parts = line.split_whitespace();
let word = parts.next()?;
let args: Vec<String> = parts.map(str::to_string).collect();
let (head, bang) = word
.strip_suffix('!')
.map_or((word, false), |stripped| (stripped, true));
let command = resolve(head).map_or_else(|| word.to_string(), |v| v.command(bang).to_string());
Some(Invocation { command, args })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn abbreviations_all_resolve() {
for v in VERBS {
for len in v.min..=v.full.len() {
let word = &v.full[..len];
assert_eq!(
resolve(word),
Some(v),
"`:{word}` must resolve to `{}`",
v.full,
);
}
}
}
#[test]
fn no_abbreviation_is_ambiguous() {
for v in VERBS {
for len in v.min..=v.full.len() {
let word = &v.full[..len];
let hits: Vec<&str> = VERBS
.iter()
.filter(|c| c.spelled_by(word))
.map(|c| c.full)
.collect();
assert_eq!(hits.len(), 1, "`:{word}` is ambiguous: {hits:?}");
}
}
}
#[test]
fn below_the_minimum_selects_nothing() {
for v in VERBS {
for len in 1..v.min {
let word = &v.full[..len];
let hit = resolve(word);
assert!(
hit.is_none_or(|h| h.full != v.full),
"`:{word}` is below `{}`'s minimum and must not select it",
v.full,
);
}
}
}
#[test]
fn the_write_quit_family_reaches_its_commands() {
for (typed, expect) in [
("w", "save"),
("write", "save"),
("wq", "write-quit"),
("wq!", "write-quit"),
("wqa", "write-quit-all"),
("wqall", "write-quit-all"),
("x", "exit-write"),
("xit", "exit-write"),
("xa", "write-quit-all"),
("exi", "exit-write"),
("exit", "exit-write"),
("wa", "buffer.write-all"),
("q", "quit"),
("q!", "quit!"),
("quit", "quit"),
("qa", "quit-all"),
("qa!", "quit-all!"),
("quita", "quit-all"),
("quitall", "quit-all"),
("u", "undo"),
("red", "redo"),
] {
assert_eq!(
parse(typed).map(|i| i.command),
Some(expect.to_string()),
"`:{typed}`",
);
}
}
#[test]
fn a_leading_colon_and_surrounding_space_are_not_part_of_the_name() {
assert_eq!(parse(":wq").map(|i| i.command), Some("write-quit".into()));
assert_eq!(parse(" wq ").map(|i| i.command), Some("write-quit".into()));
}
#[test]
fn an_empty_line_dispatches_nothing() {
assert_eq!(parse(""), None);
assert_eq!(parse(" "), None);
assert_eq!(parse(":"), None);
}
#[test]
fn an_unknown_word_passes_through_with_its_bang() {
assert_eq!(parse("noh").map(|i| i.command), Some("noh".into()));
assert_eq!(
parse("picker.files").map(|i| i.command),
Some("picker.files".into()),
);
assert_eq!(parse("Ghost!").map(|i| i.command), Some("Ghost!".into()));
}
#[test]
fn arguments_survive_the_verb() {
let i = parse("w foo.txt bar").expect("a verb with arguments parses");
assert_eq!(i.command, "save");
assert_eq!(i.args, vec!["foo.txt".to_string(), "bar".to_string()]);
}
}