use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defkmacro")]
pub struct KmacroSpec {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub keys: String,
#[serde(default)]
pub mode: String,
#[serde(default)]
pub filetype: String,
#[serde(default)]
pub keybind: String,
#[serde(default)]
pub register: String,
}
pub use crate::mode::{KNOWN_MODES, is_known_mode};
impl KmacroSpec {
#[must_use]
pub fn has_valid_register(&self) -> bool {
if self.register.is_empty() {
return true;
}
let mut chars = self.register.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return false;
};
c.is_ascii_alphanumeric()
}
#[must_use]
pub fn named_key_count(&self) -> usize {
let bytes = self.keys.as_bytes();
let mut count = 0usize;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'<' {
if let Some(j) = bytes[i + 1..].iter().position(|&b| b == b'>') {
if j > 0 {
count += 1;
i += j + 2;
continue;
}
}
}
i += 1;
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bare(name: &str, keys: &str) -> KmacroSpec {
KmacroSpec {
name: name.into(),
keys: keys.into(),
..Default::default()
}
}
#[test]
fn known_mode_vocabulary_matches_defkeybind_vocabulary() {
assert!(is_known_mode("normal"));
assert!(is_known_mode("insert"));
assert!(is_known_mode("visual"));
assert!(is_known_mode("visual-line"));
assert!(is_known_mode("command"));
assert!(!is_known_mode("superman"));
assert!(!is_known_mode(""));
}
#[test]
fn register_empty_is_valid() {
let s = bare("m", "iX<Esc>");
assert!(s.has_valid_register());
}
#[test]
fn register_accepts_single_alphanumeric_char() {
for r in ["a", "z", "A", "Z", "0", "9"] {
let s = KmacroSpec {
name: "m".into(),
keys: "x".into(),
register: r.into(),
..Default::default()
};
assert!(s.has_valid_register(), "register {r:?} should be valid");
}
}
#[test]
fn register_rejects_multichar_or_symbols() {
for bad in ["ab", "!", "@@", "aa", " ", "-"] {
let s = KmacroSpec {
name: "m".into(),
keys: "x".into(),
register: bad.into(),
..Default::default()
};
assert!(
!s.has_valid_register(),
"register {bad:?} should be invalid",
);
}
}
#[test]
fn named_key_count_handles_canonical_sequences() {
assert_eq!(bare("m", "ihello").named_key_count(), 0);
assert_eq!(bare("m", "ihello<Esc>").named_key_count(), 1);
assert_eq!(bare("m", "<Esc>:wq<CR>").named_key_count(), 2);
assert_eq!(bare("m", "c`<C-r>\"`<Esc>").named_key_count(), 2);
}
#[test]
fn named_key_count_ignores_unclosed_or_empty_brackets() {
assert_eq!(bare("m", "iless <than").named_key_count(), 0);
assert_eq!(bare("m", "i<>x").named_key_count(), 0);
assert_eq!(bare("m", "i>x").named_key_count(), 0);
}
}