use crate::view::key_context::{ContextPredicate, KeyContext, PredicateError};
use crate::core::Action;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct Binding {
pub keystrokes: Vec<String>,
pub action: Option<Action>,
pub context: Option<ContextPredicate>,
pub context_source: Option<String>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum KeymapMatch {
Action(Action),
Pending,
None,
}
#[derive(Default)]
pub struct Keymap {
bindings: Vec<Binding>,
known_actions: HashMap<&'static str, Action>,
}
impl Keymap {
pub fn bind(
&mut self,
keystrokes: &str,
action: Option<Action>,
context: Option<&str>,
) -> Result<(), PredicateError> {
let context_source = context.map(str::to_owned);
let context = context.map(ContextPredicate::parse).transpose()?;
let keystrokes = keystrokes.split_whitespace().map(str::to_owned).collect();
self.bindings.push(Binding { keystrokes, action, context, context_source });
Ok(())
}
pub fn register_actions(&mut self, actions: &[Action]) {
for action in actions {
self.known_actions.insert(action.0, *action);
}
}
pub fn action_by_name(&self, name: &str) -> Option<Action> {
self.known_actions.get(name).copied()
}
pub fn known_actions(&self) -> Vec<Action> {
let mut actions: Vec<Action> = self.known_actions.values().copied().collect();
actions.sort_by_key(|action| action.0);
actions
}
pub fn bindings_for(&self, action: Action) -> Vec<(String, Option<String>)> {
let mut found: Vec<(String, Option<String>)> = Vec::new();
for (index, binding) in self.bindings.iter().enumerate() {
if binding.action != Some(action) {
continue;
}
let overridden = self.bindings[index + 1..].iter().any(|later| {
later.keystrokes == binding.keystrokes
&& later.context_source == binding.context_source
&& later.action != Some(action)
});
let entry = (binding.keystrokes.join(" "), binding.context_source.clone());
if !overridden && !found.contains(&entry) {
found.push(entry);
}
}
found
}
fn match_depth(binding: &Binding, contexts: &[KeyContext]) -> Option<usize> {
match &binding.context {
None => Some(0),
Some(predicate) => (0..contexts.len()).rev().find(|depth| predicate.matches(contexts, *depth)),
}
}
pub fn resolve(&self, sequence: &[String], contexts: &[KeyContext]) -> KeymapMatch {
let mut best: Option<(usize, usize)> = None;
let mut pending = false;
for (index, binding) in self.bindings.iter().enumerate() {
let is_prefix = binding.keystrokes.len() > sequence.len() && binding.keystrokes.starts_with(sequence);
if binding.keystrokes != sequence && !is_prefix {
continue;
}
let Some(depth) = Self::match_depth(binding, contexts) else { continue };
if is_prefix {
pending |= binding.action.is_some();
} else if best.is_none_or(|(best_depth, _)| depth >= best_depth) {
best = Some((depth, index));
}
}
if pending {
return KeymapMatch::Pending;
}
match best.and_then(|(_, index)| self.bindings[index].action) {
Some(action) => KeymapMatch::Action(action),
None => KeymapMatch::None,
}
}
pub fn continuations(&self, sequence: &[String], contexts: &[KeyContext]) -> Vec<(String, Action)> {
let mut best: Vec<(Vec<String>, usize, Option<Action>)> = Vec::new();
for binding in &self.bindings {
if binding.keystrokes.len() <= sequence.len() || !binding.keystrokes.starts_with(sequence) {
continue;
}
let Some(depth) = Self::match_depth(binding, contexts) else { continue };
let rest = binding.keystrokes[sequence.len()..].to_vec();
match best.iter_mut().find(|(keys, _, _)| *keys == rest) {
Some(entry) if depth >= entry.1 => *entry = (rest, depth, binding.action),
Some(_) => {}
None => best.push((rest, depth, binding.action)),
}
}
best.into_iter().filter_map(|(keys, _, action)| Some((keys.join(" "), action?))).collect()
}
pub fn keystrokes_for(&self, action: Action) -> Option<String> {
self.bindings
.iter()
.rev()
.find(|binding| binding.action == Some(action))
.map(|binding| binding.keystrokes.join(" "))
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAVE: Action = Action("workspace::Save");
const PICKER_CONFIRM: Action = Action("picker::Confirm");
const NEWLINE: Action = Action("editor::Newline");
fn keys(source: &str) -> Vec<String> {
source.split_whitespace().map(str::to_owned).collect()
}
fn contexts(levels: &[&str]) -> Vec<KeyContext> {
levels.iter().map(|level| KeyContext::parse(level)).collect()
}
#[test]
fn deeper_context_wins() {
let mut keymap = Keymap::default();
keymap.bind("enter", Some(NEWLINE), Some("Editor")).expect("válido");
keymap.bind("enter", Some(PICKER_CONFIRM), Some("Picker")).expect("válido");
let stack = contexts(&["Workspace", "Editor", "Picker"]);
assert_eq!(keymap.resolve(&keys("enter"), &stack), KeymapMatch::Action(PICKER_CONFIRM));
assert_eq!(keymap.resolve(&keys("enter"), &stack[..2]), KeymapMatch::Action(NEWLINE));
}
#[test]
fn sequences_wait_for_the_next_keystroke() {
let mut keymap = Keymap::default();
keymap.bind("mod-k mod-s", Some(SAVE), None).expect("válido");
let stack = contexts(&["Workspace"]);
assert_eq!(keymap.resolve(&keys("mod-k"), &stack), KeymapMatch::Pending);
let next = keymap.continuations(&keys("mod-k"), &stack);
assert!(!next.is_empty() && next.iter().all(|(rest, _)| !rest.is_empty()), "{next:?}");
assert_eq!(keymap.resolve(&keys("mod-k mod-s"), &stack), KeymapMatch::Action(SAVE));
}
#[test]
fn bindings_for_skips_overridden_ones() {
let mut keymap = Keymap::default();
keymap.bind("mod-s", Some(SAVE), Some("Workspace")).expect("válido");
keymap.bind("ctrl-s", Some(SAVE), Some("Editor")).expect("válido");
keymap.bind("mod-s", None, Some("Workspace")).expect("válido");
assert_eq!(keymap.bindings_for(SAVE), vec![("ctrl-s".to_owned(), Some("Editor".to_owned()))]);
}
#[test]
fn null_bindings_disable_inherited_ones() {
let mut keymap = Keymap::default();
keymap.bind("mod-s", Some(SAVE), None).expect("válido");
keymap.bind("mod-s", None, None).expect("válido");
assert_eq!(keymap.resolve(&keys("mod-s"), &contexts(&["Workspace"])), KeymapMatch::None);
}
}