Skip to main content

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}