pub mod action;
pub mod builtin;
pub mod key_sequence;
pub mod preset;
pub mod profile;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
use crate::canvas::actions::CanvasAction;
use crate::canvas::modes::AppMode;
#[cfg(feature = "keybindings")]
use crate::editor::behavior::KeybindingParadigm;
use crate::keybindings::key_sequence::normalize_binding;
use crate::keybindings::preset::conflict_kind;
pub use action::CanvasKeyAction;
pub use builtin::{
BuiltinCanvasKeybindingPreset, ParseBuiltinCanvasKeybindingPresetError, builtin_emacs_preset,
builtin_helix_preset, builtin_vim_preset, builtin_vscode_preset,
default_builtin_action_bindings, default_emacs_action_bindings, default_helix_action_bindings,
default_vim_action_bindings, default_vscode_action_bindings, emacs_preset_toml,
helix_preset_toml, vim_preset_toml, vscode_preset_toml,
};
pub use key_sequence::{
KeyStroke, ParseKeyError, display_binding, parse_binding, try_parse_binding, try_parse_key,
};
pub use preset::{
CanvasKeybindingConflictKind, CanvasKeybindingPreset, CanvasKeybindingPresetBinding,
CanvasKeybindingPresetError, CanvasKeybindingPresetIssue, CanvasKeybindingPresetSection,
};
pub use profile::CanvasKeybindingProfile;
#[derive(Clone, Debug)]
struct Binding {
action: CanvasKeyAction,
sequence: Vec<KeyStroke>,
}
#[derive(Clone, Debug, Default)]
pub struct CanvasKeyBindings {
ro: Vec<Binding>,
edit: Vec<Binding>,
hl: Vec<Binding>,
pub(crate) paradigm: Option<KeybindingParadigm>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanvasActionKeyBinding {
pub mode: AppMode,
pub action: CanvasAction,
pub sequence: Vec<KeyStroke>,
}
pub type CanvasActionBinding = CanvasActionKeyBinding;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanvasKeyBindingEntry {
pub mode: AppMode,
pub action: CanvasKeyAction,
pub sequence: Vec<KeyStroke>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyEventOutcome {
Consumed(Option<String>),
Pending,
NotMatched,
ExitTop,
ExitBottom,
}
#[derive(Debug, Clone)]
pub struct KeySequenceTracker {
sequence: Vec<KeyStroke>,
last_key_time: Instant,
timeout: Duration,
}
impl KeySequenceTracker {
pub fn new(timeout_ms: u64) -> Self {
Self {
sequence: Vec::new(),
last_key_time: Instant::now(),
timeout: Duration::from_millis(timeout_ms),
}
}
pub fn reset(&mut self) {
self.sequence.clear();
self.last_key_time = Instant::now();
}
pub fn add_key(&mut self, stroke: KeyStroke) {
let now = Instant::now();
if now.duration_since(self.last_key_time) > self.timeout {
self.reset();
}
self.sequence.push(key_sequence::normalize_stroke(stroke));
self.last_key_time = now;
}
pub fn sequence(&self) -> &[KeyStroke] {
&self.sequence
}
}
impl CanvasKeyBindings {
pub fn vim_defaults() -> Self {
Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Vim)
}
pub fn helix_defaults() -> Self {
Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Helix)
}
pub fn emacs_defaults() -> Self {
Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Emacs)
}
pub fn vscode_defaults() -> Self {
Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Vscode)
}
pub fn from_builtin_preset(preset: BuiltinCanvasKeybindingPreset) -> Self {
let mut bindings = Self::from_preset(&preset.preset());
bindings.paradigm = Some(match preset {
BuiltinCanvasKeybindingPreset::Vim => KeybindingParadigm::Vim,
BuiltinCanvasKeybindingPreset::Helix => KeybindingParadigm::Helix,
BuiltinCanvasKeybindingPreset::Emacs => KeybindingParadigm::Emacs,
BuiltinCanvasKeybindingPreset::Vscode => KeybindingParadigm::Vscode,
});
bindings
}
pub fn from_preset(preset: &CanvasKeybindingPreset) -> Self {
Self::try_from_preset(preset).expect("canvas keybinding preset was validated")
}
pub fn try_from_preset(
preset: &CanvasKeybindingPreset,
) -> Result<Self, CanvasKeybindingPresetError> {
preset.validate()?;
let mut bindings = Self::default();
for section in preset.sections() {
let section_bindings = section
.bindings
.iter()
.flat_map(|binding| {
binding.keys.iter().map(|key| {
try_parse_binding(key).map(|sequence| Binding {
action: binding.action.clone(),
sequence,
})
})
})
.collect::<Result<Vec<_>, _>>()
.expect("canvas keybinding preset was validated");
match section.mode {
AppMode::Nor => bindings.ro.extend(section_bindings),
AppMode::Ins => bindings.edit.extend(section_bindings),
AppMode::Sel => bindings.hl.extend(section_bindings),
_ => {}
}
}
Ok(bindings)
}
pub fn from_mode_maps(
read_only: &HashMap<String, Vec<String>>,
edit: &HashMap<String, Vec<String>>,
highlight: &HashMap<String, Vec<String>>,
) -> Self {
let mut bindings = Self::default();
bindings.ro = collect_bindings(read_only);
bindings.edit = collect_bindings(edit);
bindings.hl = collect_bindings(highlight);
bindings
}
pub fn try_from_mode_maps(
read_only: &HashMap<String, Vec<String>>,
edit: &HashMap<String, Vec<String>>,
highlight: &HashMap<String, Vec<String>>,
) -> Result<Self, CanvasKeybindingPresetError> {
let preset = CanvasKeybindingPreset::from_mode_maps(read_only, edit, highlight)?;
Self::try_from_preset(&preset)
}
pub fn lookup_action(
&self,
mode: AppMode,
seq: &[KeyStroke],
) -> (Option<&CanvasKeyAction>, bool) {
let bindings = match mode {
AppMode::Nor => &self.ro,
AppMode::Ins => &self.edit,
AppMode::Sel => &self.hl,
_ => return (None, false),
};
if seq.is_empty() {
return (None, false);
}
for binding in bindings {
if binding.sequence == seq {
return (Some(&binding.action), false);
}
}
for binding in bindings {
if seq.len() < binding.sequence.len() && binding.sequence.starts_with(seq) {
return (None, true);
}
}
(None, false)
}
pub fn lookup(&self, mode: AppMode, seq: &[KeyStroke]) -> (Option<&str>, bool) {
let (action, is_prefix) = self.lookup_action(mode, seq);
(action.map(|action| action.as_str()), is_prefix)
}
pub fn entries(&self) -> Vec<CanvasKeyBindingEntry> {
let mut entries = Vec::new();
for (mode, bindings) in [
(AppMode::Nor, &self.ro),
(AppMode::Ins, &self.edit),
(AppMode::Sel, &self.hl),
] {
for binding in bindings {
entries.push(CanvasKeyBindingEntry {
mode,
action: binding.action.clone(),
sequence: binding.sequence.clone(),
});
}
}
entries
}
pub fn bindings_for(&self, mode: AppMode, action: &CanvasKeyAction) -> Vec<Vec<KeyStroke>> {
let Some(bindings) = self.bindings_for_mode(mode) else {
return Vec::new();
};
bindings
.iter()
.filter(|binding| &binding.action == action)
.map(|binding| binding.sequence.clone())
.collect()
}
pub fn bind(
&mut self,
mode: AppMode,
action: CanvasKeyAction,
binding: &str,
) -> Result<(), CanvasKeybindingPresetError> {
let sequence = parse_runtime_binding(mode, &action, binding)?;
self.ensure_available(mode, &action, &sequence)?;
self.bindings_for_mode_mut(mode)?
.push(Binding { action, sequence });
Ok(())
}
pub fn unbind(
&mut self,
mode: AppMode,
binding: &str,
) -> Result<Option<CanvasKeyAction>, CanvasKeybindingPresetError> {
let sequence = try_parse_binding(binding).map_err(|source| {
CanvasKeybindingPresetError::Issues(vec![CanvasKeybindingPresetIssue::InvalidBinding {
section: mode_section_name(mode).to_string(),
action: CanvasKeyAction::Unknown("<unbind>".to_string()),
binding: binding.to_string(),
source,
}])
})?;
let sequence = normalize_binding(&sequence);
let bindings = self.bindings_for_mode_mut(mode)?;
let Some(index) = bindings
.iter()
.position(|existing| existing.sequence == sequence)
else {
return Ok(None);
};
Ok(Some(bindings.remove(index).action))
}
pub fn unbind_action(
&mut self,
mode: AppMode,
action: &CanvasKeyAction,
) -> Result<usize, CanvasKeybindingPresetError> {
let bindings = self.bindings_for_mode_mut(mode)?;
let before = bindings.len();
bindings.retain(|binding| &binding.action != action);
Ok(before - bindings.len())
}
pub fn remap_action(
&mut self,
mode: AppMode,
action: CanvasKeyAction,
bindings: Vec<String>,
) -> Result<(), CanvasKeybindingPresetError> {
let mut sequences = Vec::new();
let mut seen = HashSet::new();
for binding in &bindings {
let sequence = parse_runtime_binding(mode, &action, binding)?;
if !seen.insert(sequence.clone()) {
return Err(CanvasKeybindingPresetError::Issues(vec![
CanvasKeybindingPresetIssue::BindingConflict {
section: mode_section_name(mode).to_string(),
mode,
binding: display_binding(&sequence),
action: action.clone(),
existing_binding: display_binding(&sequence),
existing_action: action.clone(),
kind: CanvasKeybindingConflictKind::Exact,
},
]));
}
sequences.push(sequence);
}
for left in 0..sequences.len() {
for right in (left + 1)..sequences.len() {
let Some(kind) = conflict_kind(&sequences[left], &sequences[right]) else {
continue;
};
return Err(CanvasKeybindingPresetError::Issues(vec![
CanvasKeybindingPresetIssue::BindingConflict {
section: mode_section_name(mode).to_string(),
mode,
binding: display_binding(&sequences[left]),
action: action.clone(),
existing_binding: display_binding(&sequences[right]),
existing_action: action.clone(),
kind,
},
]));
}
}
let mut candidate = self.clone();
candidate.unbind_action(mode, &action)?;
for sequence in &sequences {
candidate.ensure_available(mode, &action, sequence)?;
}
let target = self.bindings_for_mode_mut(mode)?;
target.retain(|binding| binding.action != action);
for sequence in sequences {
target.push(Binding {
action: action.clone(),
sequence,
});
}
Ok(())
}
fn bindings_for_mode(&self, mode: AppMode) -> Option<&Vec<Binding>> {
match mode {
AppMode::Nor => Some(&self.ro),
AppMode::Ins => Some(&self.edit),
AppMode::Sel => Some(&self.hl),
AppMode::General | AppMode::Command => None,
}
}
fn bindings_for_mode_mut(
&mut self,
mode: AppMode,
) -> Result<&mut Vec<Binding>, CanvasKeybindingPresetError> {
match mode {
AppMode::Nor => Ok(&mut self.ro),
AppMode::Ins => Ok(&mut self.edit),
AppMode::Sel => Ok(&mut self.hl),
AppMode::General | AppMode::Command => Err(unsupported_mode_error(mode)),
}
}
fn ensure_available(
&self,
mode: AppMode,
action: &CanvasKeyAction,
sequence: &[KeyStroke],
) -> Result<(), CanvasKeybindingPresetError> {
let Some(bindings) = self.bindings_for_mode(mode) else {
return Err(unsupported_mode_error(mode));
};
for existing in bindings {
let Some(kind) = conflict_kind(sequence, &existing.sequence) else {
continue;
};
return Err(CanvasKeybindingPresetError::Issues(vec![
CanvasKeybindingPresetIssue::BindingConflict {
section: mode_section_name(mode).to_string(),
mode,
binding: display_binding(sequence),
action: action.clone(),
existing_binding: display_binding(&existing.sequence),
existing_action: existing.action.clone(),
kind,
},
]));
}
Ok(())
}
}
fn collect_bindings(mode_map: &HashMap<String, Vec<String>>) -> Vec<Binding> {
let mut out = Vec::new();
for (action, list) in mode_map {
for binding_str in list {
if let Some(sequence) = parse_binding(binding_str) {
out.push(Binding {
action: CanvasKeyAction::from_name(action),
sequence,
});
}
}
}
out
}
fn parse_runtime_binding(
mode: AppMode,
action: &CanvasKeyAction,
binding: &str,
) -> Result<Vec<KeyStroke>, CanvasKeybindingPresetError> {
try_parse_binding(binding)
.map(|sequence| normalize_binding(&sequence))
.map_err(|source| {
CanvasKeybindingPresetError::Issues(vec![CanvasKeybindingPresetIssue::InvalidBinding {
section: mode_section_name(mode).to_string(),
action: action.clone(),
binding: binding.to_string(),
source,
}])
})
}
fn unsupported_mode_error(mode: AppMode) -> CanvasKeybindingPresetError {
CanvasKeybindingPresetError::Issues(vec![CanvasKeybindingPresetIssue::UnsupportedMode { mode }])
}
fn mode_section_name(mode: AppMode) -> &'static str {
mode.as_str()
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
#[test]
fn strict_mode_maps_support_named_undo_and_redo() {
let mut read_only = HashMap::new();
read_only.insert("undo".to_string(), vec!["u".to_string()]);
read_only.insert("redo".to_string(), vec!["ctrl+r".to_string()]);
let keybindings =
CanvasKeyBindings::try_from_mode_maps(&read_only, &HashMap::new(), &HashMap::new())
.unwrap();
let undo = [KeyStroke {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::empty(),
}];
let redo = [KeyStroke {
code: KeyCode::Char('r'),
modifiers: KeyModifiers::CONTROL,
}];
assert_eq!(
keybindings.lookup_action(AppMode::Nor, &undo).0,
Some(&CanvasKeyAction::Undo)
);
assert_eq!(
keybindings.lookup_action(AppMode::Nor, &redo).0,
Some(&CanvasKeyAction::Redo)
);
}
#[test]
fn profile_applies_partial_helix_overrides_and_keeps_unmentioned_defaults() {
let profile = BuiltinCanvasKeybindingPreset::Helix
.profile_with_overrides(
r#"
[nor]
undo = ["z u"]
redo = ["z r"]
"#,
)
.unwrap();
let current = profile.current();
let old_undo = try_parse_binding("u").unwrap();
let new_undo = try_parse_binding("z u").unwrap();
let redo = try_parse_binding("z r").unwrap();
let move_down = try_parse_binding("j").unwrap();
assert_eq!(current.lookup_action(AppMode::Nor, &old_undo).0, None);
assert_eq!(
current.lookup_action(AppMode::Nor, &new_undo).0,
Some(&CanvasKeyAction::Undo)
);
assert_eq!(
current.lookup_action(AppMode::Nor, &redo).0,
Some(&CanvasKeyAction::Redo)
);
assert_eq!(
current.lookup_action(AppMode::Nor, &move_down).0,
Some(&CanvasKeyAction::MoveDown)
);
}
#[test]
fn profile_serializes_only_normalized_differences() {
let mut profile = CanvasKeybindingProfile::new(BuiltinCanvasKeybindingPreset::Helix);
profile
.remap_action(
AppMode::Nor,
CanvasKeyAction::Undo,
vec!["Shift+z".to_string(), "z u".to_string()],
)
.unwrap();
profile
.remap_action(AppMode::Nor, CanvasKeyAction::Redo, Vec::new())
.unwrap();
let overrides = profile.overrides_toml();
assert!(overrides.contains("[nor]"));
assert!(overrides.contains("undo = [\"Z\", \"z u\"]"));
assert!(overrides.contains("redo = []"));
assert!(!overrides.contains("move_down"));
let round_tripped = BuiltinCanvasKeybindingPreset::Helix
.profile_with_overrides(&overrides)
.unwrap();
assert_eq!(round_tripped.overrides_toml(), overrides);
}
#[test]
fn runtime_remap_reports_prefix_conflicts() {
let mut bindings = CanvasKeyBindings::default();
bindings
.bind(AppMode::Nor, CanvasKeyAction::Undo, "z u")
.unwrap();
let err = bindings
.bind(AppMode::Nor, CanvasKeyAction::Redo, "z")
.unwrap_err();
let CanvasKeybindingPresetError::Issues(issues) = err else {
panic!("expected issues");
};
assert!(issues.iter().any(|issue| matches!(
issue,
CanvasKeybindingPresetIssue::BindingConflict {
kind: CanvasKeybindingConflictKind::PrefixOf,
..
}
)));
}
#[test]
fn runtime_remap_reports_extension_conflicts() {
let mut bindings = CanvasKeyBindings::default();
bindings
.bind(AppMode::Nor, CanvasKeyAction::Undo, "z")
.unwrap();
let err = bindings
.bind(AppMode::Nor, CanvasKeyAction::Redo, "z r")
.unwrap_err();
let CanvasKeybindingPresetError::Issues(issues) = err else {
panic!("expected issues");
};
assert!(issues.iter().any(|issue| matches!(
issue,
CanvasKeybindingPresetIssue::BindingConflict {
kind: CanvasKeybindingConflictKind::ExtensionOf,
..
}
)));
}
#[test]
fn runtime_mutation_rejects_unsupported_modes() {
let mut bindings = CanvasKeyBindings::default();
let err = bindings
.bind(AppMode::Command, CanvasKeyAction::Undo, "u")
.unwrap_err();
let CanvasKeybindingPresetError::Issues(issues) = err else {
panic!("expected issues");
};
assert!(issues.iter().any(|issue| matches!(
issue,
CanvasKeybindingPresetIssue::UnsupportedMode {
mode: AppMode::Command
}
)));
}
}