diff options
| author | rottedfm <rottedfm@proton.me> | 2026-08-19 11:21:47 -0400 |
|---|---|---|
| committer | rottedfm <rottedfm@proton.me> | 2026-08-19 11:21:47 -0400 |
| commit | ea0bd36167b684c0accdb5ce2b2e21b8d84aeb25 (patch) | |
| tree | be1267972b5de2f1ae592577dfabce67f1fe6e87 /src/buffer/command.rs | |
| parent | 8c0b4c53b130555f040884c1f52b90f16b23e241 (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.rs | 147 |
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 + } +} |
