escriba_core/action.rs
1use escriba_search::Direction as SearchDirection;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5use crate::edit::Edit;
6use crate::mode::Mode;
7use crate::motion::{Motion, Operator};
8
9/// A fully-resolved editor action — what the keymap emits, what the buffer
10/// consumes.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12pub enum Action {
13 /// Move every cursor by `motion`.
14 Move(Motion),
15 /// Begin an operator (the `d`/`c`/`y` key). The editor enters
16 /// operator-pending: the next motion composes into an [`Action::ApplyOperator`].
17 /// Resolved by the operator-pending FSM, never executed directly.
18 Operator(Operator),
19 /// Apply a pending operator over a motion (delete-word, yank-line, etc.).
20 ApplyOperator {
21 op: Operator,
22 motion: Motion,
23 },
24 /// Apply a primitive edit at each cursor.
25 Edit(Edit),
26 /// Enter the given mode.
27 ChangeMode(Mode),
28 /// Run a named command (via the command registry).
29 Command {
30 name: String,
31 args: Vec<String>,
32 },
33 /// Insert a character at each caret. Separate from Edit so the keymap
34 /// can stay ignorant of rope details.
35 InsertChar(char),
36 /// Submit a minibuffer / command-mode line (e.g. `:w`, `:q`).
37 SubmitCommand,
38 /// Undo / redo one change.
39 Undo,
40 Redo,
41 /// Save the current buffer.
42 Save,
43 /// Quit the editor.
44 Quit,
45 // ── search (vim `/`, `?`, `n`, `N`, `*`, `#`) ──────────────────────
46 /// Open the search prompt in `direction` (the `/` and `?` keys).
47 ///
48 /// The prompt reuses `Mode::Command` rather than adding a mode variant:
49 /// vim's `/` IS the command-line with a different prompt character, and
50 /// this module's own doc states new modes are layered through pending
51 /// state, not new variants. `SearchState`'s typed `Option<Prompt>` is what
52 /// disambiguates a `<CR>` that submits a search from one that submits an
53 /// ex-command — a discriminator that cannot be forgotten, unlike a bool.
54 SearchOpen(SearchDirection),
55 /// `n` (`reverse = false`) / `N` (`reverse = true`) — jump to the next
56 /// match, relative to the direction the search was committed with, so `N`
57 /// after a `?` search moves forward.
58 SearchRepeat {
59 reverse: bool,
60 },
61 /// `*` (`reverse = false`) / `#` (`reverse = true`) — search the whole word
62 /// under the cursor. Literal, not regex: the word may contain `.` or `[`
63 /// and the user means those characters.
64 SearchWord {
65 reverse: bool,
66 },
67 /// `:noh` — stop highlighting matches while keeping the pattern, so `n`
68 /// still works. Distinct from cancelling a search.
69 ClearSearchHighlight,
70 /// Backspace inside a command-line or search prompt.
71 ///
72 /// Key::Backspace was previously bound in NO mode, so the minibuffer could
73 /// be typed into but never corrected — a typo meant Esc and start again.
74 /// One action serves both prompts; the runtime routes it by whether a
75 /// search prompt is open.
76 PromptBackspace,
77 /// No-op — used when a key sequence is pending but not yet complete.
78 Pending,
79}
80
81/// An [`Action`] with an optional repetition count (vim's `5dd`, `10k`).
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83pub struct CountedAction {
84 pub count: u32,
85 pub action: Action,
86}
87
88impl CountedAction {
89 #[must_use]
90 pub fn once(action: Action) -> Self {
91 Self { count: 1, action }
92 }
93
94 #[must_use]
95 pub fn repeated(count: u32, action: Action) -> Self {
96 Self {
97 count: count.max(1),
98 action,
99 }
100 }
101}