use std::{fmt, str::FromStr};
use super::preset::CanvasKeybindingPreset;
use super::{CanvasActionKeyBinding, try_parse_binding};
use super::{CanvasKeyBindings, CanvasKeybindingPresetError, CanvasKeybindingProfile};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BuiltinCanvasKeybindingPreset {
Vim,
Helix,
Emacs,
Vscode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseBuiltinCanvasKeybindingPresetError {
name: String,
}
impl ParseBuiltinCanvasKeybindingPresetError {
pub fn name(&self) -> &str {
&self.name
}
}
impl fmt::Display for ParseBuiltinCanvasKeybindingPresetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"unknown built-in canvas keybinding preset {:?}",
self.name
)
}
}
impl std::error::Error for ParseBuiltinCanvasKeybindingPresetError {}
impl BuiltinCanvasKeybindingPreset {
fn as_name(&self) -> &'static str {
match self {
Self::Vim => "vim",
Self::Helix => "helix",
Self::Emacs => "emacs",
Self::Vscode => "vscode",
}
}
#[deprecated(
since = "0.8.5",
note = "use Display/to_string(); this compatibility shim will be removed in 1.0.0"
)]
pub fn name(&self) -> &'static str {
panic!(
"BuiltinCanvasKeybindingPreset::name() is deprecated; use Display/to_string() instead. It will be removed in 1.0.0."
)
}
pub fn toml(&self) -> &str {
match self {
Self::Vim => include_str!("presets/vim.toml"),
Self::Helix => include_str!("presets/helix.toml"),
Self::Emacs => include_str!("presets/emacs.toml"),
Self::Vscode => include_str!("presets/vscode.toml"),
}
}
pub fn preset(self) -> CanvasKeybindingPreset {
builtin_preset(self.as_name(), self.toml())
}
pub fn profile(self) -> CanvasKeybindingProfile {
CanvasKeybindingProfile::new(self)
}
pub fn profile_with_overrides(
self,
source: &str,
) -> Result<CanvasKeybindingProfile, CanvasKeybindingPresetError> {
CanvasKeybindingProfile::with_overrides_toml(self, source)
}
pub fn keybindings_with_overrides(
self,
source: &str,
) -> Result<CanvasKeyBindings, CanvasKeybindingPresetError> {
Ok(self.profile_with_overrides(source)?.current().clone())
}
}
impl FromStr for BuiltinCanvasKeybindingPreset {
type Err = ParseBuiltinCanvasKeybindingPresetError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
match name {
"vim" => Ok(Self::Vim),
"helix" => Ok(Self::Helix),
"emacs" => Ok(Self::Emacs),
"vscode" => Ok(Self::Vscode),
_ => Err(ParseBuiltinCanvasKeybindingPresetError {
name: name.to_string(),
}),
}
}
}
impl fmt::Display for BuiltinCanvasKeybindingPreset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_name())
}
}
pub fn vim_preset_toml() -> &'static str {
BuiltinCanvasKeybindingPreset::Vim.toml()
}
pub fn helix_preset_toml() -> &'static str {
BuiltinCanvasKeybindingPreset::Helix.toml()
}
pub fn emacs_preset_toml() -> &'static str {
BuiltinCanvasKeybindingPreset::Emacs.toml()
}
pub fn vscode_preset_toml() -> &'static str {
BuiltinCanvasKeybindingPreset::Vscode.toml()
}
pub fn builtin_vim_preset() -> CanvasKeybindingPreset {
BuiltinCanvasKeybindingPreset::Vim.preset()
}
pub fn builtin_helix_preset() -> CanvasKeybindingPreset {
BuiltinCanvasKeybindingPreset::Helix.preset()
}
pub fn builtin_emacs_preset() -> CanvasKeybindingPreset {
BuiltinCanvasKeybindingPreset::Emacs.preset()
}
pub fn builtin_vscode_preset() -> CanvasKeybindingPreset {
BuiltinCanvasKeybindingPreset::Vscode.preset()
}
pub fn default_builtin_action_bindings(
preset: BuiltinCanvasKeybindingPreset,
) -> Vec<CanvasActionKeyBinding> {
action_bindings_from_preset(preset.preset())
}
pub fn default_vim_action_bindings() -> Vec<CanvasActionKeyBinding> {
default_builtin_action_bindings(BuiltinCanvasKeybindingPreset::Vim)
}
pub fn default_helix_action_bindings() -> Vec<CanvasActionKeyBinding> {
default_builtin_action_bindings(BuiltinCanvasKeybindingPreset::Helix)
}
pub fn default_emacs_action_bindings() -> Vec<CanvasActionKeyBinding> {
default_builtin_action_bindings(BuiltinCanvasKeybindingPreset::Emacs)
}
pub fn default_vscode_action_bindings() -> Vec<CanvasActionKeyBinding> {
default_builtin_action_bindings(BuiltinCanvasKeybindingPreset::Vscode)
}
fn action_bindings_from_preset(preset: CanvasKeybindingPreset) -> Vec<CanvasActionKeyBinding> {
let mut bindings = Vec::new();
for section in preset.sections() {
for binding in §ion.bindings {
let Some(action) = binding.action.to_canvas_action() else {
continue;
};
for key in &binding.keys {
let sequence =
try_parse_binding(key).expect("built-in canvas vim keybinding was validated");
bindings.push(CanvasActionKeyBinding {
mode: section.mode,
action: action.clone(),
sequence,
});
}
}
}
bindings
}
fn builtin_preset(name: &str, source: &str) -> CanvasKeybindingPreset {
CanvasKeybindingPreset::from_toml(source)
.unwrap_or_else(|err| panic!("invalid built-in canvas {name} keybinding preset: {err}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canvas::actions::CanvasAction;
use crate::canvas::modes::AppMode;
use crate::keybindings::{CanvasKeyAction, CanvasKeyBindings, KeyStroke};
use crossterm::event::{KeyCode, KeyModifiers};
#[test]
fn parses_builtin_presets() {
for preset in [
BuiltinCanvasKeybindingPreset::Vim,
BuiltinCanvasKeybindingPreset::Helix,
BuiltinCanvasKeybindingPreset::Emacs,
BuiltinCanvasKeybindingPreset::Vscode,
] {
assert_eq!(
preset.to_string().parse::<BuiltinCanvasKeybindingPreset>(),
Ok(preset)
);
assert_eq!(preset.to_string(), preset.as_name());
let parsed = CanvasKeybindingPreset::from_toml(preset.toml()).unwrap();
assert_eq!(parsed.sections().len(), 3);
}
}
#[test]
fn serde_uses_config_names() {
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Config {
preset: BuiltinCanvasKeybindingPreset,
}
assert_eq!(
toml::from_str::<Config>("preset = \"helix\"")
.unwrap()
.preset,
BuiltinCanvasKeybindingPreset::Helix
);
assert_eq!(
toml::to_string(&Config {
preset: BuiltinCanvasKeybindingPreset::Vscode,
})
.unwrap(),
"preset = \"vscode\"\n"
);
}
#[test]
fn vscode_defaults_map_modeless_edit_bindings() {
let keybindings = CanvasKeyBindings::vscode_defaults();
let undo = [KeyStroke {
code: KeyCode::Char('z'),
modifiers: KeyModifiers::CONTROL,
}];
let redo = [KeyStroke {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::CONTROL,
}];
assert_eq!(
keybindings.lookup_action(AppMode::Ins, &undo).0,
Some(&CanvasKeyAction::Undo)
);
assert_eq!(
keybindings.lookup_action(AppMode::Ins, &redo).0,
Some(&CanvasKeyAction::Redo)
);
let del_word = [KeyStroke {
code: KeyCode::Backspace,
modifiers: KeyModifiers::CONTROL,
}];
let word_next = [KeyStroke {
code: KeyCode::Right,
modifiers: KeyModifiers::CONTROL,
}];
assert_eq!(
keybindings.lookup_action(AppMode::Ins, &del_word).0,
Some(&CanvasKeyAction::DeleteWordBackward)
);
assert_eq!(
keybindings.lookup_action(AppMode::Ins, &word_next).0,
Some(&CanvasKeyAction::MoveWordNext)
);
}
#[test]
fn vim_defaults_maps_undo_and_redo() {
let keybindings = CanvasKeyBindings::vim_defaults();
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 vim_defaults_include_visual_big_word_motions() {
let keybindings = CanvasKeyBindings::vim_defaults();
for (key, action) in [
('W', CanvasKeyAction::MoveBigWordNext),
('B', CanvasKeyAction::MoveBigWordPrev),
('E', CanvasKeyAction::MoveBigWordEnd),
] {
let stroke = [KeyStroke {
code: KeyCode::Char(key),
modifiers: KeyModifiers::empty(),
}];
assert_eq!(
keybindings.lookup_action(AppMode::Sel, &stroke).0,
Some(&action)
);
}
}
#[test]
fn helix_defaults_use_goto_mode_motions() {
let keybindings = CanvasKeyBindings::helix_defaults();
for (keys, action) in [
("gh", CanvasKeyAction::MoveLineStart),
("gl", CanvasKeyAction::MoveLineEnd),
("ge", CanvasKeyAction::MoveLastLine),
] {
let sequence = keys
.chars()
.map(|key| KeyStroke {
code: KeyCode::Char(key),
modifiers: KeyModifiers::empty(),
})
.collect::<Vec<_>>();
assert_eq!(
keybindings.lookup_action(AppMode::Nor, &sequence).0,
Some(&action)
);
assert_eq!(
keybindings.lookup_action(AppMode::Sel, &sequence).0,
Some(&action)
);
}
}
#[test]
fn action_bindings_are_derived_from_builtin_preset() {
let bindings = default_vim_action_bindings();
assert!(bindings.iter().any(|binding| {
binding.mode == AppMode::Nor
&& binding.action == CanvasAction::Undo
&& binding.sequence
== vec![KeyStroke {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::empty(),
}]
}));
assert!(bindings.iter().any(|binding| {
binding.mode == AppMode::Nor
&& binding.action == CanvasAction::Redo
&& binding.sequence
== vec![KeyStroke {
code: KeyCode::Char('r'),
modifiers: KeyModifiers::CONTROL,
}]
}));
}
#[test]
fn non_vim_defaults_are_available() {
let helix = default_helix_action_bindings();
let emacs = default_emacs_action_bindings();
assert!(helix
.iter()
.any(|binding| binding.mode == AppMode::Nor
&& binding.action == CanvasAction::Undo));
assert!(helix
.iter()
.any(|binding| binding.mode == AppMode::Nor
&& binding.action == CanvasAction::Redo));
let delete = [KeyStroke {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::empty(),
}];
assert_eq!(
CanvasKeyBindings::helix_defaults()
.lookup_action(AppMode::Nor, &delete)
.0,
Some(&CanvasKeyAction::DeleteSelection)
);
assert!(
emacs.iter().any(|binding| binding.mode == AppMode::Ins
&& binding.action == CanvasAction::DeleteForward)
);
}
#[test]
fn builtin_presets_include_edit_mode_suggestion_bindings() {
for bindings in [
default_vim_action_bindings(),
default_helix_action_bindings(),
default_emacs_action_bindings(),
] {
for action in [
CanvasAction::TriggerSuggestions,
CanvasAction::SuggestionDown,
CanvasAction::SuggestionUp,
CanvasAction::SelectSuggestion,
CanvasAction::ExitSuggestions,
] {
assert!(
bindings
.iter()
.any(|binding| binding.mode == AppMode::Ins && binding.action == action),
"missing edit mode binding for {action:?}"
);
}
}
}
#[test]
fn builtin_presets_include_undo_redo_in_command_modes() {
for bindings in [default_vim_action_bindings()] {
for mode in [AppMode::Nor] {
for action in [CanvasAction::Undo, CanvasAction::Redo] {
assert!(
bindings
.iter()
.any(|binding| binding.mode == mode && binding.action == action),
"missing {mode:?} binding for {action:?}"
);
}
}
}
for bindings in [
default_helix_action_bindings(),
default_emacs_action_bindings(),
] {
for mode in [AppMode::Nor, AppMode::Sel] {
for action in [CanvasAction::Undo, CanvasAction::Redo] {
assert!(
bindings
.iter()
.any(|binding| binding.mode == mode && binding.action == action),
"missing {mode:?} binding for {action:?}"
);
}
}
}
}
}