use hjkl_engine::{Input, Key, VimMode};
use hjkl_keymap::{Chord, KeyCode as KmKeyCode, KeyModifiers, key::KeyEvent};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum MapMode {
Normal,
Visual,
Insert,
OperatorPending,
CommandLine,
Terminal,
}
#[derive(Debug, Clone)]
pub(crate) struct UserKeymapRecord {
pub mode: MapMode,
pub lhs: Vec<Input>,
pub rhs: Vec<Input>,
pub recursive: bool,
}
pub(crate) fn format_user_map_list(records: &[UserKeymapRecord], modes: &[MapMode]) -> String {
let mut lines = Vec::new();
for &mode in modes {
let label = match mode {
MapMode::Normal => "normal",
MapMode::Visual => "visual",
MapMode::Insert => "insert",
MapMode::OperatorPending => "operator-pending",
MapMode::CommandLine => "command-line",
MapMode::Terminal => "terminal",
};
lines.push(format!("[{label}]"));
let mut entries: Vec<&UserKeymapRecord> =
records.iter().filter(|r| r.mode == mode).collect();
entries.sort_by(|a, b| {
a.lhs
.len()
.cmp(&b.lhs.len())
.then_with(|| a.rhs.len().cmp(&b.rhs.len()))
});
if entries.is_empty() {
lines.push(" (none)".into());
continue;
}
for r in entries {
lines.push(format!(
" {} {} {}",
if r.recursive { "map" } else { "noremap" },
display_keys(&r.lhs),
display_keys(&r.rhs)
));
}
}
lines.join("\n")
}
pub(crate) fn input_to_km_event(input: Input) -> hjkl_keymap::KeyEvent {
use hjkl_keymap::{KeyCode as KmCode, KeyEvent as KmEvent, KeyModifiers as KmMods};
let code = match input.key {
Key::Char(c) => KmCode::Char(c),
Key::Backspace => KmCode::Backspace,
Key::Enter => KmCode::Enter,
Key::Left => KmCode::Left,
Key::Right => KmCode::Right,
Key::Up => KmCode::Up,
Key::Down => KmCode::Down,
Key::Tab => KmCode::Tab,
Key::Delete => KmCode::Delete,
Key::Home => KmCode::Home,
Key::End => KmCode::End,
Key::PageUp => KmCode::PageUp,
Key::PageDown => KmCode::PageDown,
Key::Esc => KmCode::Esc,
Key::Null => KmCode::Char('\0'),
};
let mut modifiers = KmMods::NONE;
if input.ctrl {
modifiers |= KmMods::CTRL;
}
if input.alt {
modifiers |= KmMods::ALT;
}
if input.shift {
modifiers |= KmMods::SHIFT;
}
KmEvent::new(code, modifiers)
}
pub(crate) fn map_mode_to_km_mode(mode: MapMode) -> Option<hjkl_vim::Mode> {
match mode {
MapMode::Normal => Some(hjkl_vim::Mode::Normal),
MapMode::Visual => Some(hjkl_vim::Mode::Visual),
MapMode::Insert => Some(hjkl_vim::Mode::Insert),
MapMode::OperatorPending => Some(hjkl_vim::Mode::OpPending),
MapMode::CommandLine => Some(hjkl_vim::Mode::CommandLine),
MapMode::Terminal => None,
}
}
pub(crate) fn map_mode_for_vim(mode: VimMode) -> Option<MapMode> {
match mode {
VimMode::Normal => Some(MapMode::Normal),
VimMode::Insert => Some(MapMode::Insert),
VimMode::Visual | VimMode::VisualLine | VimMode::VisualBlock => Some(MapMode::Visual),
}
}
pub(crate) fn parse_key_sequence(text: &str, leader: char) -> Result<Vec<Input>, String> {
let mut result = Vec::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '<' {
result.push(Input {
key: Key::Char(ch),
..Input::default()
});
continue;
}
let mut tag = String::new();
let mut closed = false;
for next in chars.by_ref() {
if next == '>' {
closed = true;
break;
}
tag.push(next);
}
if !closed {
result.push(Input {
key: Key::Char('<'),
..Input::default()
});
for c in tag.chars() {
result.push(Input {
key: Key::Char(c),
..Input::default()
});
}
break;
}
let chord = Chord::parse(&format!("<{tag}>"), leader).map_err(|e| e.to_string())?;
for ev in &chord.0 {
match chord_event_to_input(ev) {
Some(input) => result.push(input),
None => return Err(format!("cannot map <{tag}> to an engine input")),
}
}
}
Ok(result)
}
fn chord_event_to_input(ev: &KeyEvent) -> Option<Input> {
let ctrl = ev.modifiers.contains(KeyModifiers::CTRL);
let alt = ev.modifiers.contains(KeyModifiers::ALT);
let mut shift = ev.modifiers.contains(KeyModifiers::SHIFT);
let key = match ev.code {
KmKeyCode::Char(c) => {
if c.is_ascii_alphabetic() && shift {
shift = false;
Key::Char(c.to_ascii_uppercase())
} else {
Key::Char(c)
}
}
KmKeyCode::Enter => Key::Enter,
KmKeyCode::Esc => Key::Esc,
KmKeyCode::Tab => Key::Tab,
KmKeyCode::Backspace => Key::Backspace,
KmKeyCode::Delete => Key::Delete,
KmKeyCode::Insert => return None,
KmKeyCode::Up => Key::Up,
KmKeyCode::Down => Key::Down,
KmKeyCode::Left => Key::Left,
KmKeyCode::Right => Key::Right,
KmKeyCode::Home => Key::Home,
KmKeyCode::End => Key::End,
KmKeyCode::PageUp => Key::PageUp,
KmKeyCode::PageDown => Key::PageDown,
KmKeyCode::F(_) => return None,
};
Some(Input {
key,
ctrl,
alt,
shift,
})
}
pub(crate) fn parse_mode_groups(cmd: &str) -> Option<Vec<MapMode>> {
match cmd {
"map" | "noremap" | "nm" => Some(vec![
MapMode::Normal,
MapMode::Visual,
MapMode::OperatorPending,
]),
"nmap" | "nnoremap" | "nunmap" | "nmapclear" => Some(vec![MapMode::Normal]),
"vmap" | "vnoremap" | "vunmap" | "vmapclear" => Some(vec![MapMode::Visual]),
"xmap" | "xnoremap" | "xunmap" | "xmapclear" => Some(vec![MapMode::Visual]),
"imap" | "inoremap" | "iunmap" | "imapclear" => Some(vec![MapMode::Insert]),
"omap" | "onoremap" | "ounmap" | "omapclear" => Some(vec![MapMode::OperatorPending]),
"cmap" | "cnoremap" | "cunmap" | "cmapclear" => Some(vec![MapMode::CommandLine]),
"tmap" | "tnoremap" | "tunmap" | "tmapclear" => Some(vec![MapMode::Terminal]),
"unmap" | "mapclear" => Some(vec![
MapMode::Normal,
MapMode::Visual,
MapMode::OperatorPending,
MapMode::Insert,
MapMode::CommandLine,
MapMode::Terminal,
]),
_ => None,
}
}
pub(crate) enum RuntimeMapCommand {
Add {
modes: Vec<MapMode>,
recursive: bool,
lhs: Vec<Input>,
rhs: Vec<Input>,
},
Remove {
modes: Vec<MapMode>,
lhs: Vec<Input>,
},
Clear {
modes: Vec<MapMode>,
},
List {
modes: Vec<MapMode>,
},
}
pub(crate) fn parse_runtime_map_command(
cmd: &str,
leader: char,
) -> Option<Result<RuntimeMapCommand, String>> {
let cmd = cmd.trim();
let split = cmd
.char_indices()
.find(|(_, c)| c.is_whitespace())
.map(|(i, _)| i)
.unwrap_or(cmd.len());
let (name, rest) = cmd.split_at(split);
let rest = rest.trim();
let modes = parse_mode_groups(name)?;
if name.ends_with("clear") {
return Some(Ok(RuntimeMapCommand::Clear { modes }));
}
let is_remove = name.ends_with("unmap");
if rest.is_empty() {
return Some(Ok(RuntimeMapCommand::List { modes }));
}
let split = rest
.char_indices()
.find(|(_, c)| c.is_whitespace())
.map(|(i, _)| i)
.unwrap_or(rest.len());
let (lhs_text, rhs_text) = rest.split_at(split);
let lhs_text = lhs_text.trim();
let rhs_text = rhs_text.trim();
let lhs = match parse_key_sequence(lhs_text, leader) {
Ok(keys) => keys,
Err(reason) => return Some(Err(format!("{name}: invalid lhs `{lhs_text}`: {reason}"))),
};
if is_remove {
return Some(Ok(RuntimeMapCommand::Remove { modes, lhs }));
}
let recursive = !matches!(
name,
"noremap"
| "nnoremap"
| "vnoremap"
| "xnoremap"
| "inoremap"
| "onoremap"
| "cnoremap"
| "tnoremap"
| "nm"
);
let rhs = match parse_key_sequence(rhs_text, leader) {
Ok(keys) => keys,
Err(reason) => return Some(Err(format!("{name}: invalid rhs `{rhs_text}`: {reason}"))),
};
Some(Ok(RuntimeMapCommand::Add {
modes,
recursive,
lhs,
rhs,
}))
}
fn display_keys(keys: &[Input]) -> String {
let mut out = String::new();
for input in keys {
match input.key {
Key::Char(c) if input.ctrl => {
out.push_str("<C-");
out.push(c);
out.push('>');
}
Key::Char(c) if input.alt => {
out.push_str("<M-");
out.push(c);
out.push('>');
}
Key::Char('<') => out.push_str("<lt>"),
Key::Char(c) => out.push(c),
Key::Esc => out.push_str("<Esc>"),
Key::Enter => out.push_str("<CR>"),
Key::Backspace => out.push_str("<BS>"),
Key::Tab => out.push_str("<Tab>"),
Key::Up => out.push_str("<Up>"),
Key::Down => out.push_str("<Down>"),
Key::Left => out.push_str("<Left>"),
Key::Right => out.push_str("<Right>"),
Key::Delete => out.push_str("<Del>"),
Key::Home => out.push_str("<Home>"),
Key::End => out.push_str("<End>"),
Key::PageUp => out.push_str("<PageUp>"),
Key::PageDown => out.push_str("<PageDown>"),
Key::Null => {}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn ch(c: char) -> Input {
Input {
key: Key::Char(c),
..Input::default()
}
}
#[test]
fn parse_leader_and_cr_tags() {
let keys = parse_key_sequence("<leader>w<CR>", '\\').unwrap();
assert_eq!(
keys,
vec![
ch('\\'),
ch('w'),
Input {
key: Key::Enter,
..Input::default()
}
]
);
}
#[test]
fn input_to_km_event_char() {
use hjkl_keymap::{KeyCode, KeyModifiers};
let ev = input_to_km_event(ch('x'));
assert_eq!(ev.code, KeyCode::Char('x'));
assert_eq!(ev.modifiers, KeyModifiers::NONE);
}
#[test]
fn input_to_km_event_ctrl() {
use hjkl_keymap::{KeyCode, KeyModifiers};
let input = Input {
key: Key::Char('w'),
ctrl: true,
..Input::default()
};
let ev = input_to_km_event(input);
assert_eq!(ev.code, KeyCode::Char('w'));
assert!(ev.modifiers.contains(KeyModifiers::CTRL));
}
#[test]
fn map_mode_to_km_mode_terminal_is_none() {
assert!(map_mode_to_km_mode(MapMode::Terminal).is_none());
}
#[test]
fn runtime_map_bad_lhs_notation_is_some_err() {
let out = parse_runtime_map_command("nmap <NoSuchKey> x", '\\');
let Some(Err(msg)) = out else {
panic!("bad lhs notation must be Some(Err(_))");
};
assert!(
msg.contains("NoSuchKey"),
"message must name the offending tag, got {msg:?}"
);
assert!(
msg.contains("nmap"),
"message must name the map verb, got {msg:?}"
);
}
#[test]
fn runtime_map_unrepresentable_lhs_is_some_err() {
let out = parse_runtime_map_command("nmap <F5> x", '\\');
let Some(Err(msg)) = out else {
panic!("unrepresentable lhs must be Some(Err(_))");
};
assert!(
msg.contains("F5"),
"message must name the offending tag, got {msg:?}"
);
}
#[test]
fn runtime_map_bad_rhs_notation_is_some_err() {
let out = parse_runtime_map_command("nmap x <F5>", '\\');
let Some(Err(msg)) = out else {
panic!("bad rhs notation must be Some(Err(_))");
};
assert!(
msg.contains("rhs") && msg.contains("F5"),
"message must flag the rhs and the tag, got {msg:?}"
);
}
#[test]
fn non_map_command_is_none() {
assert!(parse_runtime_map_command("nonsense stuff", '\\').is_none());
}
#[test]
fn valid_runtime_map_is_some_ok() {
let out = parse_runtime_map_command("nmap <CR> x", '\\');
let Some(Ok(RuntimeMapCommand::Add { lhs, rhs, .. })) = out else {
panic!("valid nmap must be Some(Ok(Add {{ .. }}))");
};
assert_eq!(
lhs,
vec![Input {
key: Key::Enter,
..Input::default()
}]
);
assert_eq!(rhs, vec![ch('x')]);
}
}