//! Editor commands — the values key bindings map to. //! //! Kept distinct from [`crate::action::Action`], which is application-level //! (tick, render, resize). A binding names a `Command`; the buffer executes it. //! `Quit` and `Suspend` appear here because the `Global` keymap is expressed in //! the same table and must be able to name them. use serde::{Deserialize, Serialize}; use strum::Display; /// MJB-LLR-157: one unit variant per bound editor command, deserialized from /// the variant name so a TOML value like `"MoveCharLeft"` resolves directly. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, Serialize, Deserialize)] pub enum Command { // --- Application (reachable from the `global` keymap) --- Quit, Suspend, // --- Mode switching --- NormalMode, InsertMode, SelectMode, CommandMode, // --- Character and line motion (MJB-HLR-006) --- MoveCharLeft, MoveCharRight, MoveLineUp, MoveLineDown, // --- Word motion; these produce selections (MJB-HLR-007) --- MoveNextWordStart, MovePrevWordStart, MoveNextWordEnd, MoveNextLongWordStart, MovePrevLongWordStart, MoveNextLongWordEnd, // --- Extending variants, used by select mode --- ExtendCharLeft, ExtendCharRight, ExtendLineUp, ExtendLineDown, ExtendNextWordStart, ExtendPrevWordStart, ExtendNextWordEnd, // --- Goto (MJB-HLR-008) --- GotoFileStart, GotoLastLine, GotoLineStart, GotoLineEnd, GotoFirstNonWhitespace, // --- Selection manipulation --- ExtendLineBelow, CollapseSelection, FlipSelections, SelectAll, // --- Entering insert mode (MJB-HLR-009) --- AppendMode, InsertAtLineStart, InsertAtLineEnd, OpenBelow, OpenAbove, // --- Modification (MJB-HLR-010) --- DeleteSelection, ChangeSelection, InsertNewline, InsertTab, DeleteCharBackward, DeleteCharForward, DeleteWordBackward, KillToLineStart, // --- Undo / redo (MJB-HLR-011) --- Undo, Redo, // --- Scrolling and paging (MJB-HLR-013) --- PageCursorHalfUp, PageCursorHalfDown, PageUp, PageDown, // --- Command line (MJB-HLR-016) --- CommandSubmit, CommandBackspace, } #[cfg(test)] mod tests { use super::*; /// MJB-LLR-157: a TOML value naming a variant deserializes to it, which is /// what makes the keymap config-driven. #[test] fn mjb_llr_157_deserializes_from_the_variant_name() { let cmd: Command = serde_json_free_parse("MoveCharLeft"); assert_eq!(cmd, Command::MoveCharLeft); assert_eq!(serde_json_free_parse("Undo"), Command::Undo); assert_eq!( serde_json_free_parse("PageCursorHalfDown"), Command::PageCursorHalfDown ); } #[test] fn mjb_llr_157_unknown_command_name_is_an_error_not_a_panic() { let err = toml::from_str::("cmd = \"NoSuchCommand\"").unwrap_err(); assert!( err.to_string().contains("NoSuchCommand"), "error must name the offending value, got: {err}" ); } #[test] fn mjb_llr_157_display_round_trips_through_deserialization() { for cmd in [ Command::Quit, Command::GotoFileStart, Command::DeleteSelection, Command::CommandSubmit, ] { assert_eq!( serde_json_free_parse(&cmd.to_string()), cmd, "{cmd} must round-trip" ); } } #[derive(Debug, serde::Deserialize)] struct Wrapper { cmd: Command, } /// Parse a bare command name the way the keymap table does. fn serde_json_free_parse(name: &str) -> Command { let doc = format!("cmd = \"{name}\""); toml::from_str::(&doc) .unwrap_or_else(|e| panic!("{name} must parse: {e}")) .cmd } }