aboutsummaryrefslogtreecommitdiff
path: root/src/buffer/command.rs
diff options
context:
space:
mode:
authorrottedfm <rottedfm@proton.me>2026-08-19 11:21:47 -0400
committerrottedfm <rottedfm@proton.me>2026-08-19 11:21:47 -0400
commitea0bd36167b684c0accdb5ce2b2e21b8d84aeb25 (patch)
treebe1267972b5de2f1ae592577dfabce67f1fe6e87 /src/buffer/command.rs
parent8c0b4c53b130555f040884c1f52b90f16b23e241 (diff)
feat: implement Helix-style modal buffer under DO-178C DAL-C
The repository was an unmodified ratatui component template: no editor code, JSON5 config, and placeholder widgets. This establishes the first working baseline — `moji <file>` opens a file into a ropey rope and edits it with Helix selection-first semantics. Requirements, implementation and tests land together because they must: the traceability check rejects requirements with no implementation and tests naming requirements that do not exist, so neither half is a valid commit on its own. Package renamed to mojibake-editor (mojibake was taken on crates.io); binary is moji, library target stays mojibake. Class: New behaviour Requirements: MJB-HLR-001..019, MJB-LLR-001..205 Derived: MJB-DR-001..007 (DR-001 resolved, six open for review) Verified: cargo build; clippy --all-targets -D warnings clean; cargo test 294 passing; ./scripts/check-trace.sh 98/98/98; cargo package clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/buffer/command.rs')
-rw-r--r--src/buffer/command.rs147
1 files changed, 147 insertions, 0 deletions
diff --git a/src/buffer/command.rs b/src/buffer/command.rs
new file mode 100644
index 0000000..c5a2aef
--- /dev/null
+++ b/src/buffer/command.rs
@@ -0,0 +1,147 @@
+//! 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::<Wrapper>("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::<Wrapper>(&doc)
+ .unwrap_or_else(|e| panic!("{name} must parse: {e}"))
+ .cmd
+ }
+}