From bcfe2ca53a720bf4d7161e7d800baf993d7d20b4 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Sun, 6 Sep 2026 19:32:19 -0400 Subject: [PATCH] Add Kakoune bindings --- Cargo.lock | 23 + Cargo.toml | 1 + crates/modalkit-ratatui/examples/editor.rs | 1 + crates/modalkit/Cargo.toml | 3 +- crates/modalkit/src/env/kak/keybindings.rs | 1209 ++++++++++++++++++++ crates/modalkit/src/env/kak/mod.rs | 513 +++++++++ crates/modalkit/src/env/mixed.rs | 32 + crates/modalkit/src/env/mod.rs | 1 + crates/modalkit/src/util.rs | 14 +- 9 files changed, 1794 insertions(+), 3 deletions(-) create mode 100644 crates/modalkit/src/env/kak/keybindings.rs create mode 100644 crates/modalkit/src/env/kak/mod.rs diff --git a/Cargo.lock b/Cargo.lock index ad768fd..2e69cc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,6 +374,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -991,6 +997,7 @@ dependencies = [ "intervaltree", "keybindings", "nom 8.0.0", + "pretty_assertions", "radix_trie", "rand 0.10.2", "regex", @@ -1412,6 +1419,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2543,6 +2560,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.56" diff --git a/Cargo.toml b/Cargo.toml index 822019e..a09fa75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ unicode-width = "0.2.0" [workspace.lints.clippy] bool_assert_comparison = "allow" bool_to_int_with_if = "allow" +enum_variant_names = "allow" field_reassign_with_default = "allow" len_without_is_empty = "allow" manual_range_contains = "allow" diff --git a/crates/modalkit-ratatui/examples/editor.rs b/crates/modalkit-ratatui/examples/editor.rs index ff28a36..07add1d 100644 --- a/crates/modalkit-ratatui/examples/editor.rs +++ b/crates/modalkit-ratatui/examples/editor.rs @@ -765,6 +765,7 @@ fn main() -> Result<(), std::io::Error> { Some(arg) => { match arg.as_str().trim() { "e" | "emacs" => MixedChoice::Emacs, + "k" | "kak" | "kakoune" => MixedChoice::Kakoune, "v" | "vim" => MixedChoice::Vim, m => panic!("Unknown environment: {m:?}"), } diff --git a/crates/modalkit/Cargo.toml b/crates/modalkit/Cargo.toml index f23a6f3..c7bb9ab 100644 --- a/crates/modalkit/Cargo.toml +++ b/crates/modalkit/Cargo.toml @@ -5,7 +5,7 @@ homepage = "https://github.com/ulyssa/modalkit/tree/main/crates/modalkit" readme = "README.md" description = "A library for building applications that use modal editing" exclude = [".github", "CONTRIBUTING.md"] -keywords = ["modal", "vim", "emacs"] +keywords = ["modal", "vim", "emacs", "kakoune"] categories = ["command-line-interface", "text-editors"] edition.workspace = true @@ -45,6 +45,7 @@ features = ["wayland-data-control"] [dev-dependencies] rand = { workspace = true } +pretty_assertions = "^1.4.1" temp-dir = { workspace = true } [lints] diff --git a/crates/modalkit/src/env/kak/keybindings.rs b/crates/modalkit/src/env/kak/keybindings.rs new file mode 100644 index 0000000..3bcbeba --- /dev/null +++ b/crates/modalkit/src/env/kak/keybindings.rs @@ -0,0 +1,1209 @@ +//! # Kakoune Keybindings (WIP) +//! +//! ## Overview +//! +//! This module handles mapping the keybindings used in Kakoune onto the +//! [Action] type. +//! +//! NOTE: A lot of this is still a work in progress, and some things +//! might not work the way you expect yet! +//! +//! ## Example +//! +//! ``` +//! use modalkit::env::kak::KakouneMode; +//! use modalkit::env::kak::keybindings::{default_kakoune_keys, KakouneMachine}; +//! +//! use modalkit::actions::{Action, EditAction, EditorAction, SelectionAction}; +//! use modalkit::keybindings::BindingMachine; +//! use modalkit::key::TerminalKey; +//! use modalkit::prelude::*; +//! +//! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +//! +//! const fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { +//! KeyEvent::new(code, modifiers) +//! } +//! +//! fn main() { +//! let mut keybindings: KakouneMachine = default_kakoune_keys(); +//! +//! // Begins in Normal mode. +//! assert_eq!(keybindings.mode(), KakouneMode::Normal); +//! +//! // Typing "i" enters Insert mode: +//! keybindings.input_key(key(KeyCode::Char('i'), KeyModifiers::NONE).into()); +//! assert_eq!(keybindings.mode(), KakouneMode::Insert); +//! +//! // Pop action produced by typing "i" to set cursor position for insert. +//! let (act, _) = keybindings.pop().unwrap(); +//! let exp = SelectionAction::CursorSet(SelectionCursorChange::Beginning); +//! assert_eq!(act, EditorAction::Selection(exp).into()); +//! +//! // End of available actions. +//! assert_eq!(keybindings.pop(), None); +//! } +//! ``` +use bitflags::bitflags; + +use editor_types::{ + action, + application::{ApplicationInfo, EmptyInfo}, + Action, + EditAction, + EditorAction, + SelectionAction, +}; + +use crate::{ + env::{keyparse::parse, CommonKeyClass, ShellBindings}, + key::TerminalKey, + keybindings::{InputBindings, ModalMachine, Step}, + prelude::*, +}; + +use super::{KakouneMode, KakouneState, ObjectPosition}; + +bitflags! { + #[derive(Debug, Clone, Copy)] + struct MappedModes: u32 { + const N = 0b0000000000000001; + const I = 0b0000000000000010; + const P = 0b0000000000000100; + const V = 0b0000000000001000; + const O = 0b0000000000010000; + const G = 0b0000000000100000; + + const NIP = Self::N.bits() | Self::I.bits() | Self::P.bits(); + const IP = Self::I.bits() | Self::P.bits(); + } +} + +const MAP: MappedModes = MappedModes::NIP; +const NMAP: MappedModes = MappedModes::N; +const IMAP: MappedModes = MappedModes::I; +const PMAP: MappedModes = MappedModes::P; +const VMAP: MappedModes = MappedModes::V; +const OMAP: MappedModes = MappedModes::O; +const GMAP: MappedModes = MappedModes::G; +const IPMAP: MappedModes = MappedModes::IP; + +impl MappedModes { + pub fn split(&self) -> Vec { + let mut modes = Vec::new(); + + if self.contains(MappedModes::N) { + modes.push(KakouneMode::Normal); + } + + if self.contains(MappedModes::I) { + modes.push(KakouneMode::Insert); + } + + if self.contains(MappedModes::P) { + modes.push(KakouneMode::Prompt); + } + + if self.contains(MappedModes::V) { + modes.push(KakouneMode::View); + } + + if self.contains(MappedModes::O) { + modes.push(KakouneMode::ObjectSelect); + } + + if self.contains(MappedModes::G) { + modes.push(KakouneMode::Goto); + } + + return modes; + } +} + +#[derive(Clone, Debug)] +enum InternalAction { + SetCursorChar(char), + SetCursorEnd(CursorEnd), + SetObjectSelect(SelectionResizeStyle, ObjectPosition, bool), + SetRegister(Register), + SetSearchChar, + SetSearchCharParams(MoveDir1D, bool), + SetTargetShape(TargetShape), +} + +impl InternalAction { + pub fn run(&self, ctx: &mut KakouneState) { + match self { + InternalAction::SetCursorChar(c) => { + ctx.action.cursor = Some(*c); + }, + InternalAction::SetCursorEnd(end) => { + ctx.action.cursor_end = *end; + }, + InternalAction::SetObjectSelect(style, pos, inc) => { + ctx.action.objsel = Some((*style, *pos, *inc)); + }, + InternalAction::SetRegister(reg) => { + ctx.action.register = Some(reg.clone()); + }, + InternalAction::SetSearchChar => { + ctx.persist.charsearch = ctx.ch.get_typed(); + }, + InternalAction::SetSearchCharParams(dir, inclusive) => { + ctx.persist.charsearch_params = (*dir, *inclusive); + }, + InternalAction::SetTargetShape(shape) => { + ctx.action.shape = Some(*shape); + }, + } + } +} + +#[derive(Debug)] +enum ExternalAction { + CountOnly(Action), + ObjectSelect(RangeType, Option), + Something(Action), +} + +impl ExternalAction { + fn resolve(&self, ctx: &mut KakouneState) -> Vec> { + match self { + ExternalAction::CountOnly(act) => { + if ctx.action.count.is_some() { + vec![act.clone()] + } else { + vec![] + } + }, + ExternalAction::ObjectSelect(rt1, rt2) => { + if let Some((style, pos, inclusive)) = ctx.action.objsel { + let rt = match (rt2, inclusive) { + (Some(rt), true) => rt.clone(), + (_, _) => rt1.clone(), + }; + + let target = match pos { + ObjectPosition::Whole => { + EditTarget::Range(rt, inclusive, Count::Contextual) + }, + ObjectPosition::Beginning => { + EditTarget::Boundary( + rt, + inclusive, + MoveTerminus::Beginning, + Count::Contextual, + ) + }, + ObjectPosition::End => { + EditTarget::Boundary( + rt, + inclusive, + MoveTerminus::End, + Count::Contextual, + ) + }, + }; + + vec![action!("selection resize -s {style} -t {target}")] + } else { + vec![] + } + }, + ExternalAction::Something(act) => vec![act.clone()], + } + } +} + +impl Clone for ExternalAction { + fn clone(&self) -> Self { + match self { + ExternalAction::CountOnly(act) => ExternalAction::CountOnly(act.clone()), + ExternalAction::ObjectSelect(range1, range2) => { + ExternalAction::ObjectSelect(range1.clone(), range2.clone()) + }, + ExternalAction::Something(act) => ExternalAction::Something(act.clone()), + } + } +} + +impl From> for ExternalAction { + fn from(act: Action) -> Self { + ExternalAction::Something(act) + } +} + +/// Description of actions to take after an input sequence. +#[derive(Debug)] +pub struct InputStep { + internal: Vec, + external: Vec>, + fallthrough_mode: Option, + nextm: Option, +} + +impl InputStep { + /// Create a new step that input keys can map to. + pub fn new() -> Self { + InputStep { + internal: vec![], + external: vec![], + fallthrough_mode: None, + nextm: None, + } + } + + /// Set the [actions](Action) that this step produces. + pub fn actions(mut self, acts: Vec>) -> Self { + self.external = acts.into_iter().map(ExternalAction::Something).collect(); + self + } +} + +impl Default for InputStep { + fn default() -> Self { + Self::new() + } +} + +impl Clone for InputStep { + fn clone(&self) -> Self { + Self { + internal: self.internal.clone(), + external: self.external.clone(), + fallthrough_mode: self.fallthrough_mode, + nextm: self.nextm, + } + } +} + +impl Step for InputStep { + type A = Action; + type State = KakouneState; + type M = KakouneMode; + type Class = CommonKeyClass; + type Sequence = RepeatType; + + fn is_unmapped(&self) -> bool { + match self { + InputStep { + internal, + external, + fallthrough_mode: None, + nextm: None, + } => internal.is_empty() && external.is_empty(), + _ => false, + } + } + + fn fallthrough(&self) -> Option { + self.fallthrough_mode + } + + fn step(&self, ctx: &mut KakouneState) -> (Vec>, Option) { + for iact in self.internal.iter() { + iact.run(ctx); + } + + let external: Vec> = + self.external.iter().flat_map(|act| act.resolve(ctx)).collect(); + + return (external, self.nextm); + } +} + +macro_rules! act { + ($ext: expr) => { + isv!(vec![], vec![ExternalAction::Something($ext)]) + }; + ($ext: expr, $ns: expr) => { + isv!(vec![], vec![ExternalAction::Something($ext)], $ns) + }; +} + +macro_rules! action_step { + ($cmd: expr) => { + act!(action!($cmd)) + }; + ($cmd: expr, $ns: expr) => { + act!(action!($cmd), $ns) + }; +} + +macro_rules! iact { + ($int: expr) => { + isv!(vec![$int], vec![]) + }; + ($int: expr, $ns: expr) => { + isv!(vec![$int], vec![], $ns) + }; +} + +macro_rules! isv { + () => { + InputStep { + internal: vec![], + external: vec![], + fallthrough_mode: None, + nextm: None, + } + }; + ($ints: expr, $exts: expr) => { + InputStep { + internal: $ints, + external: $exts, + fallthrough_mode: None, + nextm: None, + } + }; + ($ints: expr, $exts: expr, $ns: expr) => { + InputStep { + internal: $ints, + external: $exts, + fallthrough_mode: None, + nextm: Some($ns), + } + }; +} + +macro_rules! is { + ($int: expr, $ext: expr) => { + isv!(vec![$int], vec![ExternalAction::Something($ext.into())]) + }; + ($int: expr, $ext: expr, $ns: expr) => { + isv!(vec![$int], vec![ExternalAction::Something($ext.into())], $ns) + }; +} + +macro_rules! fallthrough { + ($mode: expr) => { + InputStep { + internal: vec![], + external: vec![], + fallthrough_mode: Some($mode), + + nextm: None, + } + }; + ($mode: expr, $iacts: expr) => { + InputStep { + internal: $iacts, + external: vec![], + fallthrough_mode: Some($mode), + + nextm: None, + } + }; + ($mode: expr, $iacts: expr, $eacts: expr) => { + InputStep { + internal: $iacts, + external: $eacts, + fallthrough_mode: Some($mode), + + nextm: None, + } + }; +} + +macro_rules! goto_goto { + ($iacts: expr) => { + fallthrough!(KakouneMode::Goto, $iacts, vec![ExternalAction::CountOnly( + EditorAction::Edit( + Specifier::Exact(EditAction::Motion), + MoveType::BufferLineOffset.into() + ) + .into() + )]) + }; +} + +macro_rules! shaped { + ($shape: expr, $act: expr) => { + is!(InternalAction::SetTargetShape($shape), $act) + }; + ($shape: expr, $act: expr, $nm: expr) => { + is!(InternalAction::SetTargetShape($shape), $act, $nm) + }; +} + +macro_rules! blackhole { + ($act: expr) => { + is!(InternalAction::SetRegister(Register::Blackhole), $act) + }; + ($act: expr, $nm: expr) => { + is!(InternalAction::SetRegister(Register::Blackhole), $act, $nm) + }; +} + +macro_rules! insert { + ($mt: expr, $c: literal) => { + edit!(EditAction::Motion, $mt, Count::Exact(0), KakouneMode::Insert) + }; + ($mt: expr, $c: expr) => { + edit!(EditAction::Motion, $mt, $c, KakouneMode::Insert) + }; +} + +macro_rules! edit_target_cursor_end { + ($ea: expr, $et: expr, $end: expr) => { + is!(InternalAction::SetCursorEnd($end), EditorAction::Edit(Specifier::Exact($ea), $et)) + }; + ($ea: expr, $et: expr, $end: expr, $mode: expr) => { + is!( + InternalAction::SetCursorEnd($end), + EditorAction::Edit(Specifier::Exact($ea), $et), + $mode + ) + }; +} + +macro_rules! edit_target_cursor_keep { + ($ea: expr, $et: expr) => { + edit_target_cursor_end!($ea, $et, CursorEnd::Keep) + }; + ($ea: expr, $et: expr, $mode: expr) => { + edit_target_cursor_end!($ea, $et, CursorEnd::Keep, $mode) + }; +} + +macro_rules! edit_selection_cursor_keep { + ($ea: expr) => { + edit_target_cursor_keep!($ea, EditTarget::Selection) + }; +} + +macro_rules! action_selection { + ($cmd: expr) => { + is!(InternalAction::SetCursorEnd(CursorEnd::Selection), action!($cmd)) + }; +} + +macro_rules! action_keep { + ($cmd: expr) => { + is!(InternalAction::SetCursorEnd(CursorEnd::Keep), action!($cmd)) + }; + ($cmd: expr, $nm: expr) => { + is!(InternalAction::SetCursorEnd(CursorEnd::Keep), action!($cmd), $nm) + }; +} + +macro_rules! open_lines { + ($dir: expr) => { + isv!( + vec![], + vec![ + ExternalAction::Something(action!("cursor split -c ctx-sub-one")), + ExternalAction::Something(action!("insert open-line -S line -d {} -c 1", $dir)) + ], + KakouneMode::Insert + ) + }; +} + +macro_rules! extend_target { + ($et: expr) => { + selection_resize!(SelectionResizeStyle::Extend, $et) + }; +} + +macro_rules! extend { + ($mt: expr) => { + extend_target!(EditTarget::Motion($mt, Count::Contextual)) + }; + ($mt: expr, $c: literal) => { + extend_target!(EditTarget::Motion($mt, Count::Exact($c))) + }; + ($mt: expr, $c: expr) => { + extend_target!(EditTarget::Motion($mt, $c)) + }; +} + +macro_rules! extend_search { + ($st: expr, $mod: expr) => { + is!( + InternalAction::SetSearchChar, + EditorAction::Selection(SelectionAction::Resize( + SelectionResizeStyle::Extend, + EditTarget::Search($st, $mod, Count::Contextual) + )) + ) + }; + ($st: expr, $mod: expr, $c: literal) => { + is!( + InternalAction::SetSearchChar, + EditorAction::Selection(SelectionAction::Resize( + SelectionResizeStyle::Extend, + EditTarget::Search($st, $mod, Count::Exact($c)) + )) + ) + }; + ($st: expr, $mod: expr, $c: expr) => { + is!( + InternalAction::SetSearchChar, + EditorAction::Selection(SelectionAction::Resize( + SelectionResizeStyle::Extend, + EditTarget::Search($st, $mod, $c) + )) + ) + }; +} + +macro_rules! selection_resize { + ($style: expr, $et: expr) => { + shaped!( + TargetShape::CharWise, + EditorAction::Selection(SelectionAction::Resize($style, $et)) + ) + }; +} + +macro_rules! selection_restart_target { + ($et: expr) => { + selection_resize!(SelectionResizeStyle::Restart, $et) + }; +} + +macro_rules! selection_restart { + ($mt: expr) => { + selection_restart_target!(EditTarget::Motion($mt, Count::Contextual)) + }; + ($mt: expr, $c: literal) => { + selection_restart_target!(EditTarget::Motion($mt, Count::Exact($c))) + }; +} + +macro_rules! selection_restart_search { + ($st: expr, $mod: expr) => { + is!( + InternalAction::SetSearchChar, + EditorAction::Selection(SelectionAction::Resize( + SelectionResizeStyle::Restart, + EditTarget::Search($st, $mod, Count::Contextual) + )) + ) + }; +} + +macro_rules! selection_object_search { + ($dir: expr) => { + selection_resize!( + SelectionResizeStyle::Object, + EditTarget::Search(SearchType::Regex, MoveDirMod::Exact($dir), Count::Contextual) + ) + }; +} + +macro_rules! object_select { + ($style: expr, $pos: expr, $inc: expr) => { + fallthrough!(KakouneMode::ObjectSelect, vec![ + InternalAction::SetTargetShape(TargetShape::CharWise), + InternalAction::SetObjectSelect($style, $pos, $inc) + ]) + }; +} + +macro_rules! object_end { + ($rt: expr) => { + isv!(vec![], vec![ExternalAction::ObjectSelect($rt, None)], KakouneMode::Normal) + }; + ($rt1: expr, $rt2: expr) => { + isv!(vec![], vec![ExternalAction::ObjectSelect($rt1, Some($rt2))], KakouneMode::Normal) + }; +} + +macro_rules! object_whitespace_end { + () => { + object_end!( + RangeType::Word(WordStyle::Whitespace(false)), + RangeType::Word(WordStyle::Whitespace(true)) + ) + }; +} + +macro_rules! delete_selection { + () => { + editor!(EditorAction::Edit(Specifier::Exact(EditAction::Delete), EditTarget::Selection)) + }; + ($nm: expr) => { + editor!( + EditorAction::Edit(Specifier::Exact(EditAction::Delete), EditTarget::Selection), + $nm + ) + }; + ($nm: expr, $register: expr) => { + is!( + InternalAction::SetRegister($register), + EditorAction::Edit(Specifier::Exact(EditAction::Delete), EditTarget::Selection), + $nm + ) + }; +} + +#[rustfmt::skip] +fn default_keys() -> Vec<(MappedModes, &'static str, InputStep)> { + [ + // Normal, Insert, and Command mode keys. + ( MAP, "", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Previous, true)) ), + ( MAP, "", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Next, true)) ), + + // Normal mode keys + ( NMAP, "", object_select!(SelectionResizeStyle::Object, ObjectPosition::Whole, true) ), + ( NMAP, "", selection_restart!(MoveType::WordBegin(WordStyle::Big, MoveDir1D::Previous)) ), + ( NMAP, "", extend!(MoveType::WordBegin(WordStyle::Big, MoveDir1D::Previous)) ), + ( NMAP, "", delete_selection!(KakouneMode::Insert, Register::Blackhole) ), + ( NMAP, "", action_step!("selection duplicate -d previous -c ctx") ), + ( NMAP, "", delete_selection!(KakouneMode::Normal, Register::Blackhole) ), + ( NMAP, "", selection_restart!(MoveType::WordEnd(WordStyle::Big, MoveDir1D::Next)) ), + ( NMAP, "", extend!(MoveType::WordEnd(WordStyle::Big, MoveDir1D::Next)) ), + ( NMAP, "", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Previous, true)) ), + ( NMAP, "{any}", selection_restart_search!(SearchType::Char(true), MoveDirMod::Same) ), + ( NMAP, "", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Previous, true)) ), + ( NMAP, "{any}", extend_search!(SearchType::Char(true), MoveDirMod::Same, Count::Contextual) ), + ( NMAP, "", selection_restart!(MoveType::LinePos(MovePosition::Beginning), 0) ), + ( NMAP, "", extend!(MoveType::LinePos(MovePosition::Beginning), 0) ), + ( NMAP, "", object_select!(SelectionResizeStyle::Object, ObjectPosition::Whole, false) ), + ( NMAP, "", edit_selection_cursor_keep!(EditAction::Join(JoinStyle::NewSpace)) ), + ( NMAP, "", edit_selection!(EditAction::Join(JoinStyle::NewSpace)) ), + ( NMAP, "", selection_restart!(MoveType::LinePos(MovePosition::End), 0) ), + ( NMAP, "", extend!(MoveType::LinePos(MovePosition::End), 0) ), + ( NMAP, "", action_step!(r#"cmdbar focus -p "keep matching:" -s search -a (selection filter -F keep)"#, KakouneMode::Prompt) ), + ( NMAP, "", action_step!(r#"cmdbar focus -p "keep not matching:" -s search -a (selection filter -F drop)"#, KakouneMode::Prompt) ), + ( NMAP, "", action_keep!("insert open-line -S line -d next -c ctx", KakouneMode::Normal) ), + ( NMAP, "", action_keep!("insert open-line -S line -d prev -c ctx", KakouneMode::Normal) ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", action_step!("selection split -s lines") ), + ( NMAP, "", action_step!("selection split -s anchor") ), + ( NMAP, "", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Previous, false)) ), + ( NMAP, "{any}", selection_restart_search!(SearchType::Char(true), MoveDirMod::Same) ), + ( NMAP, "", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Previous, false)) ), + ( NMAP, "{any}", extend_search!(SearchType::Char(true), MoveDirMod::Same, Count::Contextual) ), + ( NMAP, "", action_step!("history undo -c ctx", Default::default()) ), + ( NMAP, "", action_step!("history redo -c ctx", Default::default()) ), + ( NMAP, "", selection_restart!(MoveType::WordBegin(WordStyle::Big, MoveDir1D::Next)) ), + ( NMAP, "", extend!(MoveType::WordBegin(WordStyle::Big, MoveDir1D::Next)) ), + ( NMAP, "", action_step!("selection trim -b line -t all") ), + ( NMAP, "", action_step!("selection expand -b line -t all") ), + ( NMAP, "a", action_step!("cursor save -s append") ), + ( NMAP, "u", action_step!("cursor save -s (merge union)") ), + ( NMAP, "i", action_step!("cursor save -s (merge intersect)") ), + ( NMAP, "<", action_step!("cursor save -s (merge select-cursor -d previous)") ), + ( NMAP, ">", action_step!("cursor save -s (merge select-cursor -d next)") ), + ( NMAP, "+", action_step!("cursor save -s (merge select-long)") ), + ( NMAP, "-", action_step!("cursor save -s (merge select-short)") ), + ( NMAP, "a", action_step!("cursor restore -s append") ), + ( NMAP, "u", action_step!("cursor restore -s (merge union)") ), + ( NMAP, "i", action_step!("cursor restore -s (merge intersect)") ), + ( NMAP, "<", action_step!("cursor restore -s (merge select-cursor -d previous)") ), + ( NMAP, ">", action_step!("cursor restore -s (merge select-cursor -d next)") ), + ( NMAP, "+", action_step!("cursor restore -s (merge select-long)") ), + ( NMAP, "-", action_step!("cursor restore -s (merge select-short)") ), + ( NMAP, "", object_select!(SelectionResizeStyle::Restart, ObjectPosition::Beginning, false) ), + ( NMAP, "", object_select!(SelectionResizeStyle::Restart, ObjectPosition::End, false) ), + ( NMAP, "", object_select!(SelectionResizeStyle::Extend, ObjectPosition::Beginning, false) ), + ( NMAP, "", object_select!(SelectionResizeStyle::Extend, ObjectPosition::End, false) ), + ( NMAP, "", action_step!("selection join") ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", edit_selection!(EditAction::ChangeCase(Case::Toggle)) ), + ( NMAP, "", action_step!(r#"cmdbar focus -p "reverse search:" -s search -a (search -d (exact previous))"#, KakouneMode::Prompt) ), + ( NMAP, "", action_step!(r#"cmdbar focus -p "reverse search (extend):" -s search -a (search -d (exact previous))"#, KakouneMode::Prompt) ), + ( NMAP, "", unmapped!() ), + ( NMAP, "", action_step!("repeat -s last-selection") ), + ( NMAP, "", action_step!("selection cursor-set -f swap-anchor") ), + ( NMAP, "", action_step!("selection cursor-set -f end") ), + ( NMAP, "", action_step!("cursor close -t leader") ), + ( NMAP, "", action_step!("scroll -s (dir2d -d up -z half-page)") ), + ( NMAP, "", action_step!("scroll -s (dir2d -d down -z page)") ), + ( NMAP, "", action_step!("scroll -s (dir2d -d down -z half-page)") ), + ( NMAP, "", action_step!("scroll -s (dir2d -d up -z page)") ), + ( NMAP, "", action_step!("jump -t jump-list -d next -c ctx") ), + ( NMAP, "", action_step!("jump -t jump-list -d prev -c ctx") ), + ( NMAP, "", unmapped!() ), + ( NMAP, "a", action_step!("selection cursor-set -f end", KakouneMode::Insert) ), + ( NMAP, "A", insert!(MoveType::LinePos(MovePosition::End), 0) ), + ( NMAP, "b", selection_restart!(MoveType::WordBegin(WordStyle::Little, MoveDir1D::Previous)) ), + ( NMAP, "B", extend!(MoveType::WordBegin(WordStyle::Little, MoveDir1D::Previous)) ), + ( NMAP, "c", delete_selection!(KakouneMode::Insert) ), + ( NMAP, "C", action_step!("selection duplicate -d next -c ctx") ), + ( NMAP, "d", delete_selection!() ), + ( NMAP, "e", selection_restart!(MoveType::WordEnd(WordStyle::Little, MoveDir1D::Next)) ), + ( NMAP, "E", extend!(MoveType::WordEnd(WordStyle::Little, MoveDir1D::Next)) ), + ( NMAP, "f", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Next, true)) ), + ( NMAP, "f{any}", selection_restart_search!(SearchType::Char(true), MoveDirMod::Same) ), + ( NMAP, "F", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Next, true)) ), + ( NMAP, "F{any}", extend_search!(SearchType::Char(true), MoveDirMod::Same, Count::Contextual) ), + ( NMAP, "g", goto_goto!(vec![]) ), + ( NMAP, "G", goto_goto!(vec![InternalAction::SetTargetShape(TargetShape::CharWise)]) ), + ( NMAP, "h", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Previous, true)) ), + ( NMAP, "H", extend!(MoveType::Column(MoveDir1D::Previous, true)) ), + ( NMAP, "i", action_step!("selection cursor-set -f beginning", KakouneMode::Insert) ), + ( NMAP, "I", insert!(MoveType::FirstWord(MoveDir1D::Next), 0) ), + ( NMAP, "j", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Next)) ), + ( NMAP, "J", extend!(MoveType::Line(MoveDir1D::Next)) ), + ( NMAP, "k", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Previous)) ), + ( NMAP, "K", extend!(MoveType::Line(MoveDir1D::Previous)) ), + ( NMAP, "l", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Next, true)) ), + ( NMAP, "L", extend!(MoveType::Column(MoveDir1D::Next, true)) ), + ( NMAP, "m", selection_resize!(SelectionResizeStyle::Object, RangeType::Item.into()) ), + ( NMAP, "M", extend_target!(RangeType::Item.into()) ), + ( NMAP, "o", open_lines!(MoveDir1D::Next) ), + ( NMAP, "O", open_lines!(MoveDir1D::Previous) ), + ( NMAP, "p", action_selection!("insert paste -s (side -d next)") ), + ( NMAP, "P", action_selection!("insert paste -s (side -d previous)") ), + ( NMAP, "q", action_step!("macro execute -c ctx") ), + ( NMAP, "Q", action_step!("macro toggle-recording") ), + ( NMAP, "r{any}", edit_selection!(EditAction::Replace(false)) ), + ( NMAP, "R", action_selection!("insert paste -s replace") ), + ( NMAP, "s", action_step!(r#"cmdbar focus -p "select:" -s search -a (selection split -s (regex keep) -F all)"#, KakouneMode::Prompt) ), + ( NMAP, "S", action_step!(r#"cmdbar focus -p "split:" -s search -a (selection split -s (regex drop) -F all)"#, KakouneMode::Prompt) ), + ( NMAP, "t", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Next, false)) ), + ( NMAP, "t{any}", selection_restart_search!(SearchType::Char(true), MoveDirMod::Same) ), + ( NMAP, "T", iact!(InternalAction::SetSearchCharParams(MoveDir1D::Next, false)) ), + ( NMAP, "T{any}", extend_search!(SearchType::Char(true), MoveDirMod::Same, Count::Contextual) ), + ( NMAP, "u", action_step!("history undo -c ctx", Default::default()) ), + ( NMAP, "U", action_step!("history redo -c ctx", Default::default()) ), + ( NMAP, "v", fallthrough!(KakouneMode::View) ), + ( NMAP, "V", goto!(KakouneMode::View) ), + ( NMAP, "w", selection_restart!(MoveType::WordBegin(WordStyle::Little, MoveDir1D::Next)) ), + ( NMAP, "W", extend!(MoveType::WordBegin(WordStyle::Little, MoveDir1D::Next)) ), + ( NMAP, "x", selection_resize!(SelectionResizeStyle::Object, RangeType::Line.into()) ), + ( NMAP, "X", extend_target!(RangeType::Line.into()) ), + ( NMAP, "y", edit_selection!(EditAction::Yank) ), + ( NMAP, "z", action_step!("cursor save -s replace") ), + ( NMAP, "Z", action_step!("cursor restore -s replace") ), + ( NMAP, "<", edit_selection!(EditAction::Indent(IndentChange::Decrease(Count::Contextual))) ), + ( NMAP, ">", edit_selection!(EditAction::Indent(IndentChange::Increase(Count::Contextual))) ), + ( NMAP, "[", object_select!(SelectionResizeStyle::Restart, ObjectPosition::Beginning, true) ), + ( NMAP, "]", object_select!(SelectionResizeStyle::Restart, ObjectPosition::End, true) ), + ( NMAP, "{", object_select!(SelectionResizeStyle::Extend, ObjectPosition::Beginning, true) ), + ( NMAP, "}", object_select!(SelectionResizeStyle::Extend, ObjectPosition::End, true) ), + ( NMAP, ",", action_step!("cursor close -t followers") ), + ( NMAP, ".", action_step!("repeat -s edit-sequence") ), + ( NMAP, "%", selection_restart_target!(RangeType::Buffer.into()) ), + ( NMAP, "&", unmapped!() ), + ( NMAP, "|", unmapped!() ), + ( NMAP, "!", unmapped!() ), + ( NMAP, "`", edit_selection!(EditAction::ChangeCase(Case::Lower)) ), + ( NMAP, "~", edit_selection!(EditAction::ChangeCase(Case::Upper)) ), + ( NMAP, ";", selection_restart_target!(EditTarget::CurrentPosition) ), + ( NMAP, "@", unmapped!() ), + ( NMAP, "_", action_step!("selection trim -b non-whitespace -t all") ), + ( NMAP, "/", action_step!(r#"cmdbar focus -p "search:" -s search -a (search -d (exact next))"#, KakouneMode::Prompt) ), + ( NMAP, "?", action_step!(r#"cmdbar focus -p "search (extend):" -s search -a (search -d (exact next))"#, KakouneMode::Prompt) ), + ( NMAP, "*", unmapped!() ), + ( NMAP, "$", unmapped!() ), + ( NMAP, ":", action_step!(r#"cmdbar focus -p ":" -s command -a (command execute -c 1)"#, KakouneMode::Prompt) ), + ( NMAP, ")", action_step!("cursor rotate -d next -c ctx") ), + ( NMAP, "(", action_step!("cursor rotate -d previous -c ctx") ), + ( NMAP, "", fallthrough!(KakouneMode::User) ), + ( NMAP, "", selection_restart!(MoveType::LinePos(MovePosition::Beginning), 0) ), + ( NMAP, "", selection_restart!(MoveType::LinePos(MovePosition::End), 0) ), + ( NMAP, "", action_step!("scroll -s (dir2d -d down -z half-page)") ), + ( NMAP, "", action_step!("scroll -s (dir2d -d up -z half-page)") ), + ( NMAP, "", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Next)) ), + ( NMAP, "", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Previous)) ), + ( NMAP, "", extend!(MoveType::Column(MoveDir1D::Previous, true)) ), + ( NMAP, "", extend!(MoveType::Column(MoveDir1D::Next, true)) ), + ( NMAP, "", extend!(MoveType::Line(MoveDir1D::Next)) ), + ( NMAP, "", extend!(MoveType::Line(MoveDir1D::Previous)) ), + ( NMAP, "", extend!(MoveType::LinePos(MovePosition::Beginning), 0) ), + ( NMAP, "", extend!(MoveType::LinePos(MovePosition::End), 0) ), + + // Insert and Command mode keys. + ( IPMAP, "", iact!(InternalAction::SetCursorChar('"')) ), + ( IPMAP, "{register}", action_step!("insert paste -s cursor") ), + ( IPMAP, "", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::Beginning), 0) ), + ( IPMAP, "", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::End), 0) ), + ( IPMAP, "", erase!(MoveType::Column(MoveDir1D::Previous, true)) ), + ( IPMAP, "", erase!(MoveType::Column(MoveDir1D::Next, true)) ), + + // Insert mode keys + ( IMAP, "", fallthrough!(KakouneMode::Normal) ), + ( IMAP, "", action_step!("complete -s (list -d next --toggle true) -T auto -D list") ), + ( IMAP, "", unmapped!() ), + ( IMAP, "", action_step!("complete -s (list -d prev --toggle true) -T auto -D list") ), + ( IMAP, "", action_step!("history checkpoint", Default::default()) ), + ( IMAP, "", iact!(InternalAction::SetCursorChar('^')) ), + ( IMAP, "{any}", action_step!("insert type -i ctx -d prev -c 1") ), + ( IMAP, "f", action_step!("complete -s none -T file -D list") ), + ( IMAP, "w", action_step!("complete -s none -T (word buffer) -D list") ), + ( IMAP, "W", action_step!("complete -s none -T (word global) -D list") ), + ( IMAP, "l", action_step!("complete -s none -T (line buffer) -D list") ), + ( IMAP, "L", action_step!("complete -s none -T (line global) -D list") ), + ( IMAP, "", goto!(KakouneMode::Normal) ), + ( IMAP, "", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Previous)) ), + ( IMAP, "", edit!(EditAction::Motion, MoveType::Line(MoveDir1D::Next)) ), + + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordBegin(WordStyle::Little, MoveDir1D::Previous)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordBegin(WordStyle::Big, MoveDir1D::Previous)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordEnd(WordStyle::Little, MoveDir1D::Next)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordEnd(WordStyle::Big, MoveDir1D::Next)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordBegin(WordStyle::Little, MoveDir1D::Next)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::WordBegin(WordStyle::Big, MoveDir1D::Next)) ), + ( PMAP, "", fallthrough!(KakouneMode::Normal) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::Beginning), 0) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Previous, false)) ), + ( PMAP, "", erase!(MoveType::Column(MoveDir1D::Next, true)) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::End), 0) ), + ( PMAP, "", edit!(EditAction::Motion, MoveType::Column(MoveDir1D::Next, false)) ), + ( PMAP, "", erase!(MoveType::Column(MoveDir1D::Previous, true)) ), + ( PMAP, "", erase!(MoveType::LinePos(MovePosition::End), 0) ), + ( PMAP, "", action_step!("prompt recall -d next -c ctx -F all") ), + ( PMAP, "", action_step!("prompt recall -d previous -c ctx -F all") ), + ( PMAP, "", erase!(MoveType::LinePos(MovePosition::Beginning), 0) ), + ( PMAP, "{any}", action_step!("insert type -i ctx -d prev -c 1") ), + ( PMAP, "", erase!(MoveType::WordBegin(WordStyle::Little, MoveDir1D::Previous)) ), + ( PMAP, "", action_step!("insert paste -s cursor") ), + ( PMAP, "", action_step!("prompt abort", KakouneMode::Normal) ), + ( PMAP, "", action_step!("prompt recall -d previous -c ctx -F all") ), + ( PMAP, "", action_step!("prompt recall -d next -c ctx -F all") ), + ( PMAP, "", action_step!("complete -s (list -d next --toggle true) -T auto -D none") ), + ( PMAP, "", action_step!("complete -s (list -d previous --toggle true) -T auto -D none") ), + + // View mode keys + ( VMAP, "", goto!(KakouneMode::Normal) ), + ( VMAP, "b", action_step!("scroll -s (cursor-pos -p end -x vertical)") ), + ( VMAP, "c", action_step!("scroll -s (cursor-pos -p middle -x vertical)") ), + ( VMAP, "h", action_step!("scroll -s (dir2d -d left -z cell)") ), + ( VMAP, "j", action_step!("scroll -s (dir2d -d down -z cell)") ), + ( VMAP, "k", action_step!("scroll -s (dir2d -d up -z cell)") ), + ( VMAP, "l", action_step!("scroll -s (dir2d -d right -z cell)") ), + ( VMAP, "m", action_step!("scroll -s (cursor-pos -p middle -x horizontal)") ), + ( VMAP, "t", action_step!("scroll -s (cursor-pos -p beginning -x vertical)") ), + ( VMAP, "v", action_step!("scroll -s (cursor-pos -p middle -x vertical)") ), + + // Goto mode keys + ( GMAP, "a", action_step!("window switch -t alternate") ), + ( GMAP, "b", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::End)) ), + ( GMAP, "B", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::End)) ), + ( GMAP, "c", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::Middle)) ), + ( GMAP, "C", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::Middle)) ), + ( GMAP, "e", edit_buffer!(EditAction::Motion, MoveTerminus::End) ), + ( GMAP, "E", edit_buffer!(EditAction::Motion, MoveTerminus::End) ), + ( GMAP, "f", action_step!("window switch -t selection") ), + ( GMAP, "g", edit!(EditAction::Motion, MoveType::BufferPos(MovePosition::Beginning)) ), + ( GMAP, "G", edit!(EditAction::Motion, MoveType::BufferPos(MovePosition::Beginning)) ), + ( GMAP, "h", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::Beginning), 0) ), + ( GMAP, "H", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::Beginning), 0) ), + ( GMAP, "i", edit!(EditAction::Motion, MoveType::FirstWord(MoveDir1D::Next), 0)), + ( GMAP, "I", edit!(EditAction::Motion, MoveType::FirstWord(MoveDir1D::Next), 0)), + ( GMAP, "j", edit!(EditAction::Motion, MoveType::BufferPos(MovePosition::End)) ), + ( GMAP, "J", edit!(EditAction::Motion, MoveType::BufferPos(MovePosition::End)) ), + ( GMAP, "k", edit_buffer!(EditAction::Motion, MoveTerminus::Beginning) ), + ( GMAP, "K", edit_buffer!(EditAction::Motion, MoveTerminus::Beginning) ), + ( GMAP, "l", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::End), 0) ), + ( GMAP, "L", edit!(EditAction::Motion, MoveType::LinePos(MovePosition::End), 0) ), + ( GMAP, "t", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::Beginning)) ), + ( GMAP, "T", edit!(EditAction::Motion, MoveType::ViewportPos(MovePosition::Beginning)) ), + ( GMAP, ".", edit_target!(EditAction::Motion, EditTarget::CharJump(Mark::LastInserted.into())) ), + + // Object keys + ( OMAP, "", object_end!(RangeType::Word(WordStyle::Big)) ), + ( OMAP, "", unmapped!() ), + ( OMAP, "", object_whitespace_end!() ), + ( OMAP, "a", object_end!(RangeType::Bracketed('<', '>')) ), + ( OMAP, "b", object_end!(RangeType::Bracketed('(', ')')) ), + ( OMAP, "c", unmapped!() ), + ( OMAP, "B", object_end!(RangeType::Bracketed('{', '}')) ), + ( OMAP, "i", unmapped!() ), + ( OMAP, "g", object_end!(RangeType::Quote('`')) ), + ( OMAP, "n", object_end!(RangeType::Word(WordStyle::Number(Radix::Decimal))) ), + ( OMAP, "p", object_end!(RangeType::Paragraph) ), + ( OMAP, "q", object_end!(RangeType::Quote('\'')) ), + ( OMAP, "Q", object_end!(RangeType::Quote('"')) ), + ( OMAP, "r", object_end!(RangeType::Bracketed('[', ']')) ), + ( OMAP, "s", object_end!(RangeType::Sentence) ), + ( OMAP, "u", unmapped!() ), + ( OMAP, "w", object_end!(RangeType::Word(WordStyle::Little)) ), + ( OMAP, "(", object_end!(RangeType::Bracketed('(', ')')) ), + ( OMAP, ")", object_end!(RangeType::Bracketed('(', ')')) ), + ( OMAP, "<", object_end!(RangeType::Bracketed('<', '>')) ), + ( OMAP, ">", object_end!(RangeType::Bracketed('<', '>')) ), + ( OMAP, "[", object_end!(RangeType::Bracketed('[', ']')) ), + ( OMAP, "]", object_end!(RangeType::Bracketed('[', ']')) ), + ( OMAP, "{", object_end!(RangeType::Bracketed('{', '}')) ), + ( OMAP, "}", object_end!(RangeType::Bracketed('{', '}')) ), + ( OMAP, "'", object_end!(RangeType::Quote('\'')) ), + ( OMAP, "\"", object_end!(RangeType::Quote('"')) ), + ( OMAP, "`", object_end!(RangeType::Quote('`')) ), + ].to_vec() +} + +#[rustfmt::skip] +fn default_pfxs() -> Vec<(MappedModes, &'static str, Option>)> { + [ + // Normal mode commands can be prefixed w/ a count. + ( NMAP, "{count}", None ), + ( NMAP, "\"{register}", None ), + ].to_vec() +} + +#[rustfmt::skip] +fn default_enter() -> Vec<(MappedModes, &'static str, InputStep)> { + [ + // in Insert mode types a newline character. + ( IMAP, "", action_step!("insert type -i (exact '\\n') -d prev -c 1") ), + + // in Command mode submits the command. + ( PMAP, "", action_step!("prompt submit", KakouneMode::Normal) ), + ].to_vec() +} + +#[rustfmt::skip] +fn default_search() -> Vec<(MappedModes, &'static str, InputStep)> { + [ + // Visually select searches in Normal mode. + ( NMAP, "", selection_object_search!(MoveDir1D::Previous) ), + ( NMAP, "", extend_search!(SearchType::Regex, MoveDirMod::Exact(MoveDir1D::Previous)) ), + ( NMAP, "n", selection_object_search!(MoveDir1D::Next) ), + ( NMAP, "N", extend_search!(SearchType::Regex, MoveDirMod::Exact(MoveDir1D::Next)) ), + ].to_vec() +} + +#[rustfmt::skip] +fn submit_on_enter() -> Vec<(MappedModes, &'static str, InputStep)> { + [ + // in Normal, Insert and Command modes submits the command. + ( MAP, "", action_step!("prompt submit") ), + ].to_vec() +} + +#[rustfmt::skip] +fn search_is_action() -> Vec<(MappedModes, &'static str, InputStep)> { + [ + // Perform an application-level search in Normal mode. + ( NMAP, "", action_step!("search -d (exact previous) -c ctx") ), + ( NMAP, "", action_step!("search -d (exact previous) -c ctx") ), + ( NMAP, "n", action_step!("search -d (exact next) -c ctx") ), + ( NMAP, "N", action_step!("search -d (exact next) -c ctx") ), + ].to_vec() +} + +#[inline] +fn add_prefix( + machine: &mut KakouneMachine, + modes: &MappedModes, + keys: &str, + action: &Option>, +) { + let (_, evs) = parse(keys).unwrap_or_else(|_| panic!("invalid kakoune keybinding: {keys}")); + let modes = modes.split(); + + for mode in modes { + machine.add_prefix(mode, &evs, action); + } +} + +#[inline] +fn add_mapping( + machine: &mut KakouneMachine, + modes: &MappedModes, + keys: &str, + action: &InputStep, +) { + let (_, evs) = parse(keys).unwrap_or_else(|_| panic!("invalid kakoune keybinding: {keys}")); + let modes = modes.split(); + + for mode in modes { + machine.add_mapping(mode, &evs, action); + } +} + +/// A configurable collection of Kakoune bindings that can be added to a [ModalMachine]. +#[derive(Debug)] +pub struct KakouneBindings { + prefixes: Vec<(MappedModes, &'static str, Option>)>, + mappings: Vec<(MappedModes, &'static str, InputStep)>, + enter: Vec<(MappedModes, &'static str, InputStep)>, + search: Vec<(MappedModes, &'static str, InputStep)>, +} + +impl KakouneBindings { + /// Map the Enter key to [submit](PromptAction::Submit) in all modes. + /// + /// Normally, Enter is unmapped in Kakoune's Normal mode. + pub fn submit_on_enter(mut self) -> Self { + self.enter = submit_on_enter(); + self + } + + /// Remap `n`, `N`, `` and `` in Normal mode to perform [Action::Search] instead. + pub fn search_is_action(mut self) -> Self { + self.search = search_is_action(); + self + } +} + +impl ShellBindings for KakouneBindings { + fn shell(self) -> Self { + self.submit_on_enter().search_is_action() + } +} + +impl Default for KakouneBindings { + fn default() -> Self { + KakouneBindings { + prefixes: default_pfxs(), + mappings: default_keys(), + enter: default_enter(), + search: default_search(), + } + } +} + +impl InputBindings> for KakouneBindings { + fn setup(&self, machine: &mut KakouneMachine) { + for (modes, keys, action) in self.prefixes.iter() { + add_prefix(machine, modes, keys, action); + } + + for (modes, keys, action) in self.mappings.iter() { + add_mapping(machine, modes, keys, action); + } + + for (modes, keys, action) in self.enter.iter() { + add_mapping(machine, modes, keys, action); + } + } +} + +/// Manage Kakoune keybindings and modes. +pub type KakouneMachine = ModalMachine>; + +/// Create a new [KakouneMachine] populated with standard Kakoune keys. +pub fn default_kakoune_keys() -> KakouneMachine { + ModalMachine::from_bindings::>() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::editing::context::EditContext; + use crate::keybindings::BindingMachine; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use editor_types::HistoryAction; + + macro_rules! action_reset { + ($ctx: expr) => { + $ctx.action.count = None; + $ctx.action.cursor_end = CursorEnd::Auto; + $ctx.action.register = None; + $ctx.action.register_append = false; + $ctx.action.shape = None; + $ctx.ch = Default::default(); + }; + } + + macro_rules! assert_normal { + ($mm: expr, $ctx: expr) => { + let mut keep = $ctx.clone(); + action_reset!($ctx); + $ctx.persist.insert = None; + assert_pop2!($mm, CHECKPOINT, $ctx); + assert_eq!($mm.mode(), KakouneMode::Normal); + std::mem::swap(&mut keep, &mut $ctx); + }; + } + + const CHECKPOINT: Action = Action::Editor(EditorAction::History(HistoryAction::Checkpoint)); + + fn mkctx() -> KakouneState { + KakouneState::default() + } + + #[test] + fn test_mode_transitions() { + let mut km: KakouneMachine = default_kakoune_keys(); + let mut ctx = mkctx(); + + // Begin in Normal mode: + assert_eq!(km.mode(), KakouneMode::Normal); + + // Move to View mode: + km.input_key(key!('V')); + assert_pop2!(km, Action::NoOp, ctx); + assert_eq!(km.mode(), KakouneMode::View); + + // And then back to Normal mode: + km.input_key(key!(KeyCode::Esc)); + assert_pop1!(km, Action::NoOp, ctx); + assert_normal!(km, ctx); + } + + #[test] + fn test_charsearch_params_and_char() { + let mut km: KakouneMachine = default_kakoune_keys(); + let mut ctx = mkctx(); + + // Search for 'a': + let search = + EditTarget::Search(SearchType::Char(true), MoveDirMod::Same, Count::Contextual); + let resize = SelectionAction::Resize(SelectionResizeStyle::Restart, search); + let resize = Action::from(EditorAction::Selection(resize)); + + km.input_key(key!('f')); + km.input_key(key!('a')); + ctx.persist.charsearch_params = (MoveDir1D::Next, true); + ctx.persist.charsearch = Some(Char::Single('a')); + ctx.ch.any = Some(key!('a')); + assert_pop1!(km, resize, ctx); + assert_normal!(km, ctx); + + // Repeat the selection: + let repeat = Action::Repeat(RepeatType::LastSelection); + + km.input_key(alt!('.')); + ctx.ch.any = None; + assert_pop1!(km, repeat, ctx); + assert_normal!(km, ctx); + + // Verify that it repeats the right actions and editing context: + km.repeat(RepeatType::LastSelection, None); + + // Original charsearch context: + ctx.ch.any = Some(key!('a')); + assert_pop1!(km, resize, ctx); + assert_eq!(km.pop(), None); + } + + #[test] + fn test_line_selection() { + let mut km: KakouneMachine = default_kakoune_keys(); + let mut ctx = mkctx(); + + let obj = SelectionResizeStyle::Object; + let sel = SelectionAction::Resize(obj, RangeType::Line.into()); + let sel = EditorAction::Selection(sel); + + km.input_key(key!('x')); + ctx.action.shape = Some(TargetShape::CharWise); + assert_pop1!(km, Action::from(sel), ctx); + assert_normal!(km, ctx); + } + + #[test] + fn test_alt_a_object_select() { + let mut km: KakouneMachine = default_kakoune_keys(); + let mut ctx = mkctx(); + + let word = EditTarget::Range(RangeType::Word(WordStyle::Little), true, Count::Contextual); + let resize = SelectionAction::Resize(SelectionResizeStyle::Object, word); + let resize = Action::from(EditorAction::Selection(resize)); + + // Begin the object selection: + km.input_key(alt!('a')); + assert_eq!(km.pop(), None); + + // Select a word: + km.input_key(key!('w')); + ctx.action.shape = Some(TargetShape::CharWise); + assert_pop1!(km, resize, ctx); + assert_normal!(km, ctx); + } +} diff --git a/crates/modalkit/src/env/kak/mod.rs b/crates/modalkit/src/env/kak/mod.rs new file mode 100644 index 0000000..d4e22c7 --- /dev/null +++ b/crates/modalkit/src/env/kak/mod.rs @@ -0,0 +1,513 @@ +//! # Kakoune-like User Interfaces (WIP) +//! +//! This module contains components to help with building applications that mimic Kakoune's user +//! interfaces. +//! +//! This is still a work in progress and you may encounter bugs, missing keybindings, +//! and differences in editing behaviour while using this. If you do, please open +//! an issue with a description of the problem. +//! +use std::marker::PhantomData; + +use crate::{ + actions::{Action, EditAction, EditorAction, HistoryAction, InsertTextAction}, + editing::{ + application::{ApplicationInfo, EmptyInfo}, + context::{EditContext, EditContextBuilder}, + cursor::CursorStyle, + }, + env::{CharacterContext, CommonKeyClass}, + key::TerminalKey, + keybindings::{ + EdgeEvent, + InputKey, + InputKeyState, + InputState, + Mode, + ModeKeys, + ModeSequence, + SequenceStatus, + }, + prelude::*, + util::{keycode_to_num, option_muladd_u32, option_muladd_usize}, +}; + +pub mod keybindings; + +/// Kakoune's input modes +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum KakouneMode { + /// Normal mode keypresses. + #[default] + Normal, + + /// Insert mode keypresses. + Insert, + + /// Prompt mode keypresses. + Prompt, + + /// User mode keypresses. + User, + + /// Goto mode keypresses. + Goto, + + /// View mode keypresses. + View, + + /// Object selection keypresses. + ObjectSelect, +} + +impl Mode, KakouneState> for KakouneMode { + fn enter(&self, prev: Self, ctx: &mut KakouneState) -> Vec> { + match self { + KakouneMode::Normal => { + ctx.persist.insert = None; + + return vec![HistoryAction::Checkpoint.into()]; + }, + KakouneMode::Insert => { + ctx.persist.insert = Some(InsertStyle::Insert); + + match prev { + KakouneMode::Normal | KakouneMode::Insert => { + return vec![]; + }, + _ => { + let action = EditAction::Motion.into(); + let target = EditTarget::CurrentPosition; + let act = EditorAction::Edit(action, target); + + return vec![act.into()]; + }, + } + }, + KakouneMode::Prompt => { + ctx.persist.insert = Some(InsertStyle::Insert); + + return vec![]; + }, + KakouneMode::User => { + ctx.persist.insert = Some(InsertStyle::Insert); + + return vec![]; + }, + KakouneMode::View | KakouneMode::ObjectSelect | KakouneMode::Goto => { + ctx.persist.insert = None; + + return vec![]; + }, + } + } + + fn show(&self, ctx: &KakouneState) -> Option { + let msg = match self { + KakouneMode::Normal | KakouneMode::Prompt | KakouneMode::ObjectSelect => "", + KakouneMode::Goto => "goto", + KakouneMode::View => "view", + KakouneMode::User => "user", + KakouneMode::Insert => "insert", + }; + + let mut res = String::from(msg); + + fn push(s: &mut String, suffix: String) { + if !s.is_empty() { + s.push(' '); + } + + s.push_str(suffix.as_str()); + } + + if let Some(n) = ctx.action.count { + push(&mut res, format!("param={n}")); + } + + if let Some(r) = ctx.action.register.as_ref().and_then(register_to_char) { + push(&mut res, format!("reg={r}")); + } + + if !res.is_empty() { + return Some(res); + } else { + return None; + } + } +} + +impl ModeSequence, KakouneState> for KakouneMode { + fn sequences( + &self, + action: &Action, + ctx: &EditContext, + ) -> Vec<(RepeatType, SequenceStatus)> { + match self { + KakouneMode::Normal | + KakouneMode::Insert | + KakouneMode::ObjectSelect | + KakouneMode::User | + KakouneMode::View | + KakouneMode::Goto => { + vec![ + (RepeatType::EditSequence, action.is_edit_sequence(SequenceStatus::Break, ctx)), + (RepeatType::LastAction, action.is_last_action(ctx)), + (RepeatType::LastSelection, action.is_last_selection(ctx)), + ] + }, + KakouneMode::Prompt => { + vec![] + }, + } + } +} + +impl ModeKeys, KakouneState> for KakouneMode { + fn unmapped( + &self, + ke: &TerminalKey, + _: &mut KakouneState, + ) -> (Vec>, Option) { + match self { + KakouneMode::Normal | KakouneMode::View => { + return (vec![], None); + }, + KakouneMode::Insert => { + if let Some(c) = ke.get_char() { + let ch = Char::Single(c).into(); + let it = InsertTextAction::Type(ch, MoveDir1D::Previous, 1.into()); + + (vec![it.into()], None) + } else { + (vec![], None) + } + }, + KakouneMode::Prompt => { + if let Some(c) = ke.get_char() { + let ch = Char::Single(c).into(); + let it = InsertTextAction::Type(ch, MoveDir1D::Previous, 1.into()); + + (vec![it.into()], None) + } else { + (vec![], None) + } + }, + KakouneMode::User | KakouneMode::ObjectSelect | KakouneMode::Goto => { + (vec![], Some(KakouneMode::Normal)) + }, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ObjectPosition { + Beginning, + End, + Whole, +} + +/// This is the context specific to an action, and gets reset every time a full sequence of +/// keybindings is pressed. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct ActionContext { + // Fields for managing entered counts. + pub(crate) count: Option, + + // Other arguments to key sequences. + pub(crate) register: Option, + pub(crate) register_append: bool, + + // Where to place the cursor after performing an operation. + pub(crate) cursor_end: CursorEnd, + + // Control object selection. + pub(self) objsel: Option<(SelectionResizeStyle, ObjectPosition, bool)>, + + // Control text selection. + pub(crate) shape: Option, + + // Cursor indicator to show on-screen. + pub(crate) cursor: Option, +} + +impl Default for ActionContext { + fn default() -> Self { + Self { + count: None, + + register: None, + register_append: false, + + cursor_end: CursorEnd::Auto, + + objsel: None, + + shape: None, + + cursor: None, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct PersistentContext { + pub(crate) regexsearch_dir: MoveDir1D, + pub(crate) regexsearch_inc: bool, + pub(crate) charsearch_params: (MoveDir1D, bool), + pub(crate) charsearch: Option, + pub(crate) insert: Option, +} + +impl Default for PersistentContext { + fn default() -> Self { + Self { + regexsearch_dir: MoveDir1D::Next, + regexsearch_inc: true, + charsearch_params: (MoveDir1D::Next, false), + charsearch: None, + insert: None, + } + } +} + +/// This wraps both action specific context, and persistent context. +#[derive(Debug, Eq, PartialEq)] +pub struct KakouneState { + pub(crate) action: ActionContext, + pub(crate) persist: PersistentContext, + pub(self) ch: CharacterContext, + + _p: PhantomData, +} + +impl Clone for KakouneState { + fn clone(&self) -> Self { + Self { + action: self.action.clone(), + persist: self.persist.clone(), + ch: self.ch.clone(), + + _p: PhantomData, + } + } +} + +impl Default for KakouneState { + fn default() -> Self { + KakouneState { + action: ActionContext::default(), + persist: PersistentContext::default(), + ch: CharacterContext::default(), + + _p: PhantomData, + } + } +} + +impl InputState for KakouneState { + type CursorHint = CursorStyle; + type Output = EditContext; + + fn merge(original: EditContext, _: &EditContext) -> EditContext { + // Don't allow any overrides for now. + original + } + + fn reset(&mut self) { + self.action = ActionContext::default(); + } + + fn take(&mut self) -> Self::Output { + let ctx = Self { + persist: self.persist.clone(), + action: std::mem::take(&mut self.action), + ch: std::mem::take(&mut self.ch), + + _p: PhantomData, + }; + + EditContext::from(ctx) + } + + fn get_cursor_hint(&self) -> Self::CursorHint { + CursorStyle { + indicator: self.action.cursor, + insert: self.persist.insert, + } + } +} + +impl InputKeyState for KakouneState { + fn event(&mut self, ev: &EdgeEvent, ke: &TerminalKey) { + match ev { + EdgeEvent::Key(_) | EdgeEvent::Fallthrough => { + // Do nothing. + }, + EdgeEvent::Class(CommonKeyClass::Mark) => { + // Do nothing for now. + }, + + EdgeEvent::Class(CommonKeyClass::Count) => { + if let Some(n) = keycode_to_num(ke, 10) { + let new = option_muladd_usize(&self.action.count, 10, n as usize); + + self.action.count = Some(new); + } + }, + EdgeEvent::Class(CommonKeyClass::Register) => { + if let Some((reg, append)) = key_to_register(ke) { + self.action.register = Some(reg); + self.action.register_append = append; + } + }, + + // Track literals, codepoints, etc. + EdgeEvent::Any => { + self.ch.any = Some(*ke); + }, + EdgeEvent::Class(CommonKeyClass::Octal) => { + if let Some(n) = keycode_to_num(ke, 8) { + let new = option_muladd_u32(&self.ch.oct, 8, n); + + self.ch.oct = Some(new); + } + }, + EdgeEvent::Class(CommonKeyClass::Decimal) => { + if let Some(n) = keycode_to_num(ke, 10) { + let new = option_muladd_u32(&self.ch.dec, 10, n); + + self.ch.dec = Some(new); + } + }, + EdgeEvent::Class(CommonKeyClass::Hexadecimal) => { + if let Some(n) = keycode_to_num(ke, 16) { + let new = option_muladd_u32(&self.ch.hex, 16, n); + + self.ch.hex = Some(new); + } + }, + EdgeEvent::Class(CommonKeyClass::Digraph1) => { + if let Some(c) = ke.get_char() { + self.ch.digraph1 = Some(c); + } + }, + EdgeEvent::Class(CommonKeyClass::Digraph2) => { + if let Some(c) = ke.get_char() { + self.ch.digraph2 = Some(c); + } + }, + } + } +} + +impl From> for EditContext { + fn from(ctx: KakouneState) -> Self { + let search_char = if let Some(c) = &ctx.persist.charsearch { + let (dir, inc) = ctx.persist.charsearch_params; + + Some((dir, inc, c.clone())) + } else { + None + }; + + let typed = ctx.ch.get_typed(); + + EditContextBuilder::default() + .count(ctx.action.count) + .typed_char(typed.clone()) + .cursor_end(ctx.action.cursor_end) + .replace_char(typed) + .search_char(search_char) + .search_regex_dir(ctx.persist.regexsearch_dir) + .target_shape(ctx.action.shape) + .insert_style(ctx.persist.insert) + .last_column(true) + .register(ctx.action.register.clone()) + .register_append(ctx.action.register_append) + .search_incremental(ctx.persist.regexsearch_inc) + .build() + } +} + +fn register_to_char(reg: &Register) -> Option { + match reg { + Register::Named(c) => (*c).into(), + Register::Unnamed => '"'.into(), + Register::UnnamedMacro => '@'.into(), + Register::UnnamedCursorGroup => '^'.into(), + Register::Blackhole => '_'.into(), + Register::CurBufName => '%'.into(), + Register::LastCommand(CommandType::Command) => ':'.into(), + Register::LastCommand(CommandType::Search) => '/'.into(), + + Register::RecentlyDeleted(_) => None, + Register::SmallDelete => None, + Register::LastYanked => None, + Register::LastInserted => None, + Register::AltBufName => None, + Register::SelectionPrimary => None, + Register::SelectionClipboard => None, + + // Catch non-exhaustive pattern: + _ => None, + } +} + +fn char_to_register(c: char) -> Option<(Register, bool)> { + let r = match c { + // Lowercase letters + c @ 'a'..='z' => Register::Named(c), + + // Uppercase letters + c @ 'A'..='Z' => Register::Named(c.to_ascii_lowercase()), + + // Special Characters + '"' => Register::Unnamed, + '@' => Register::UnnamedMacro, + '^' => Register::UnnamedCursorGroup, + + '_' => Register::Blackhole, + '%' => Register::CurBufName, + ':' => Register::LastCommand(CommandType::Command), + '/' => Register::LastCommand(CommandType::Search), + + // XXX: implement + '1'..='9' => return None, + '#' => return None, + '.' => return None, + '|' => return None, + + _ => return None, + }; + + return Some((r, false)); +} + +fn key_to_register(ke: &TerminalKey) -> Option<(Register, bool)> { + char_to_register(ke.get_char()?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mode_show() { + let mut ctx: KakouneState = KakouneState::default(); + + // No count has no prompt. + assert_eq!(KakouneMode::Normal.show(&ctx), None); + + // Count is displayed. + ctx.action.count = Some(5); + assert_eq!(KakouneMode::Normal.show(&ctx), Some("param=5".into())); + + // Register is also displayed. + ctx.action.register = Some(Register::Named('a')); + assert_eq!(KakouneMode::Normal.show(&ctx), Some("param=5 reg=a".into())); + + // Move to Insert mode. + assert_eq!(KakouneMode::Insert.show(&ctx), Some("insert param=5 reg=a".into())); + } +} diff --git a/crates/modalkit/src/env/mixed.rs b/crates/modalkit/src/env/mixed.rs index e3179b7..ae9881b 100644 --- a/crates/modalkit/src/env/mixed.rs +++ b/crates/modalkit/src/env/mixed.rs @@ -21,6 +21,15 @@ use super::{ keybindings::{default_emacs_keys, EmacsBindings, EmacsMachine, InputStep as EmacsStep}, EmacsState, }, + kak::{ + keybindings::{ + default_kakoune_keys, + InputStep as KakouneStep, + KakouneBindings, + KakouneMachine, + }, + KakouneState, + }, vim::{ keybindings::{default_vim_keys, InputStep as VimStep, VimBindings, VimMachine}, VimState, @@ -35,6 +44,9 @@ pub enum MixedChoice { /// Choose Emacs keybindings. Emacs, + /// Choose Kakoune keybindings. + Kakoune, + /// Choose Vim keybindings. Vim, } @@ -43,18 +55,21 @@ macro_rules! delegate_bindings { ($s: expr, $invoke: expr) => { match $s { MixedMachine::Emacs(c) => $invoke(c), + MixedMachine::Kakoune(c) => $invoke(c), MixedMachine::Vim(c) => $invoke(c), } }; ($s: expr, $invoke: expr, $arg: expr) => { match $s { MixedMachine::Emacs(c) => $invoke(c, $arg), + MixedMachine::Kakoune(c) => $invoke(c, $arg), MixedMachine::Vim(c) => $invoke(c, $arg), } }; ($s: expr, $invoke: expr, $arg1: expr, $arg2: expr) => { match $s { MixedMachine::Emacs(c) => $invoke(c, $arg1, $arg2), + MixedMachine::Kakoune(c) => $invoke(c, $arg1, $arg2), MixedMachine::Vim(c) => $invoke(c, $arg1, $arg2), } }; @@ -70,6 +85,9 @@ where /// Wrap Emacs bindings. Emacs(EmacsBindings), + /// Wrap Kakoune bindings. + Kakoune(KakouneBindings), + /// Wrap Vim bindings. Vim(VimBindings), } @@ -81,6 +99,7 @@ where fn shell(self) -> Self { match self { MixedBindings::Emacs(b) => MixedBindings::Emacs(b.shell()), + MixedBindings::Kakoune(b) => MixedBindings::Kakoune(b.shell()), MixedBindings::Vim(b) => MixedBindings::Vim(b.shell()), } } @@ -94,6 +113,7 @@ where match choice { MixedChoice::Emacs => MixedBindings::Emacs(EmacsBindings::default()), MixedChoice::Vim => MixedBindings::Vim(VimBindings::default()), + MixedChoice::Kakoune => MixedBindings::Kakoune(KakouneBindings::default()), } } } @@ -106,11 +126,15 @@ where K: InputKey, I: ApplicationInfo, EmacsStep: Step, + KakouneStep: Step, VimStep: Step, { /// Wrap Emacs bindings. Emacs(EmacsMachine), + /// Wrap Kakoune bindings. + Kakoune(KakouneMachine), + /// Wrap Vim bindings. Vim(VimMachine), } @@ -122,6 +146,7 @@ where fn from(choice: MixedChoice) -> Self { match choice { MixedChoice::Emacs => MixedMachine::Emacs(default_emacs_keys()), + MixedChoice::Kakoune => MixedMachine::Kakoune(default_kakoune_keys()), MixedChoice::Vim => MixedMachine::Vim(default_vim_keys()), } } @@ -139,6 +164,12 @@ where MixedMachine::Emacs(machine) }, + MixedBindings::Kakoune(b) => { + let mut machine = KakouneMachine::empty(); + b.setup(&mut machine); + + MixedMachine::Kakoune(machine) + }, MixedBindings::Vim(b) => { let mut machine = VimMachine::empty(); b.setup(&mut machine); @@ -154,6 +185,7 @@ where K: InputKey, I: ApplicationInfo, EmacsStep: Step, Sequence = RepeatType, State = EmacsState>, + KakouneStep: Step, Sequence = RepeatType, State = KakouneState>, VimStep: Step, Sequence = RepeatType, State = VimState>, { fn input_key(&mut self, key: K) { diff --git a/crates/modalkit/src/env/mod.rs b/crates/modalkit/src/env/mod.rs index d5f476a..47fccfe 100644 --- a/crates/modalkit/src/env/mod.rs +++ b/crates/modalkit/src/env/mod.rs @@ -18,6 +18,7 @@ mod macros; mod keyparse; pub mod emacs; +pub mod kak; pub mod mixed; pub mod vim; diff --git a/crates/modalkit/src/util.rs b/crates/modalkit/src/util.rs index a88b9d9..394621f 100644 --- a/crates/modalkit/src/util.rs +++ b/crates/modalkit/src/util.rs @@ -45,6 +45,13 @@ macro_rules! key { }; } +#[allow(unused_macros)] +macro_rules! alt { + ($ch: literal) => { + key!(KeyCode::Char($ch.to_ascii_lowercase()), KeyModifiers::ALT) + }; +} + #[allow(unused_macros)] macro_rules! ctl { ($ch: literal) => { @@ -55,7 +62,10 @@ macro_rules! ctl { #[allow(unused_macros)] macro_rules! assert_pop1 { ($mm: expr, $act: expr, $ctx: expr) => { - assert_eq!($mm.pop(), Some(($act.clone(), EditContext::from($ctx.clone())))); + pretty_assertions::assert_eq!( + $mm.pop(), + Some(($act.clone(), EditContext::from($ctx.clone()))) + ); }; } @@ -63,7 +73,7 @@ macro_rules! assert_pop1 { macro_rules! assert_pop2 { ($mm: expr, $act: expr, $ctx: expr) => { assert_pop1!($mm, $act, $ctx); - assert_eq!($mm.pop(), None); + pretty_assertions::assert_eq!($mm.pop(), None); }; }