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::TextObject;
8use crate::motion::{Motion, Operator};
9
10/// WHERE the caret lands when Insert mode is entered — vim's insert-entry
11/// family (`i` `I` `a` `A` `o` `O`) as ONE typed surface.
12///
13/// Until 2026-08-12 the family was a single key. `i` was bound straight to
14/// `Action::ChangeMode(Mode::Insert)` and the other five did not exist —
15/// pressing `A` in escriba 0.1.71 moved nothing, changed no mode, and reported
16/// nothing, because an unbound Normal-mode key resolves to `Action::Pending`.
17/// The absence was invisible from inside: `escriba --keymap` lists what IS
18/// bound, so nothing named the four keys a vim user reaches for first.
19///
20/// Modelling the ENTRY POINT rather than the destination mode is what makes
21/// that class of omission unrepresentable. A new entry is a variant here, and
22/// [`Action::text_effect`], [`Action::highlight_effect`],
23/// [`Action::edits_prompt`] and the runtime's damage classifier are all total
24/// over `Action` — so adding one cannot compile until every consequence has
25/// been decided.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27pub enum InsertAt {
28    /// `i` — insert BEFORE the caret. The caret does not move.
29    Caret,
30    /// `I` — the first non-blank character of the line.
31    FirstNonBlank,
32    /// `a` — one column right of the caret ("append"), clamped to one past the
33    /// last character so `a` on the final character still appends.
34    AfterCaret,
35    /// `A` — one column past the last character of the line.
36    LineEnd,
37    /// `o` — open a fresh line BELOW the caret's and land on it.
38    OpenBelow,
39    /// `O` — open a fresh line ABOVE the caret's and land on it.
40    OpenAbove,
41}
42
43/// Where `zt` / `zz` / `zb` put the cursor's line on screen.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45pub enum ViewAlign {
46    /// `zt` — the cursor's line becomes the top visible line.
47    Top,
48    /// `zz` — the cursor's line is centred.
49    Center,
50    /// `zb` — the cursor's line becomes the bottom visible line.
51    Bottom,
52}
53
54impl ViewAlign {
55    /// The stable label — `escriba --keymap`, the rc's `:action` names.
56    #[must_use]
57    pub const fn label(self) -> &'static str {
58        match self {
59            Self::Top => "scroll-top",
60            Self::Center => "scroll-center",
61            Self::Bottom => "scroll-bottom",
62        }
63    }
64}
65
66impl InsertAt {
67    /// Does this entry ADD a line to the buffer?
68    ///
69    /// `o` and `O` are the only two members of the family that change text;
70    /// the other four move the caret and nothing else. Read by
71    /// [`Action::text_effect`], so the answer lives here beside the variants
72    /// rather than being re-derived by each classifier that needs it.
73    #[must_use]
74    pub const fn opens_a_line(self) -> bool {
75        matches!(self, Self::OpenBelow | Self::OpenAbove)
76    }
77
78    /// The stable label — `escriba --keymap`, the command palette, the rc's
79    /// `:action` names.
80    #[must_use]
81    pub const fn as_str(self) -> &'static str {
82        match self {
83            Self::Caret => "caret",
84            Self::FirstNonBlank => "first-non-blank",
85            Self::AfterCaret => "after-caret",
86            Self::LineEnd => "line-end",
87            Self::OpenBelow => "open-below",
88            Self::OpenAbove => "open-above",
89        }
90    }
91
92    /// Every entry, so a matrix test cannot silently miss one.
93    pub const ALL: [Self; 6] = [
94        Self::Caret,
95        Self::FirstNonBlank,
96        Self::AfterCaret,
97        Self::LineEnd,
98        Self::OpenBelow,
99        Self::OpenAbove,
100    ];
101}
102
103/// A fully-resolved editor action — what the keymap emits, what the buffer
104/// consumes.
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
106pub enum Action {
107    /// Move every cursor by `motion`.
108    Move(Motion),
109    /// Begin an operator (the `d`/`c`/`y` key). The editor enters
110    /// operator-pending: the next motion composes into an [`Action::ApplyOperator`].
111    /// Resolved by the operator-pending FSM, never executed directly.
112    Operator(Operator),
113    /// Apply a pending operator over a motion (delete-word, yank-line, etc.).
114    ApplyOperator {
115        op: Operator,
116        motion: Motion,
117    },
118    /// Apply a primitive edit at each cursor.
119    Edit(Edit),
120    /// Enter the given mode.
121    ChangeMode(Mode),
122    /// Enter Insert mode AT a named place — vim's `i` `I` `a` `A` `o` `O`.
123    ///
124    /// Distinct from `ChangeMode(Mode::Insert)`, which is still what the
125    /// *runtime* emits when an operator leaves you inserting (`ciw`) and what
126    /// `<Esc>`'s counterpart looks like. This variant is the KEYBOARD's entry
127    /// point, and it carries the one thing `ChangeMode` cannot: where the caret
128    /// goes. See [`InsertAt`].
129    EnterInsert(InsertAt),
130    /// Run a named command (via the command registry).
131    Command {
132        name: String,
133        args: Vec<String>,
134    },
135    /// Insert a character at each caret. Separate from Edit so the keymap
136    /// can stay ignorant of rope details.
137    InsertChar(char),
138    /// Submit a minibuffer / command-mode line (e.g. `:w`, `:q`).
139    SubmitCommand,
140    /// Undo / redo one change.
141    Undo,
142    Redo,
143    /// Save the current buffer.
144    Save,
145    /// Quit the editor.
146    Quit,
147    // ── search (vim `/`, `?`, `n`, `N`, `*`, `#`) ──────────────────────
148    /// Open the search prompt in `direction` (the `/` and `?` keys).
149    ///
150    /// The prompt reuses `Mode::Command` rather than adding a mode variant:
151    /// vim's `/` IS the command-line with a different prompt character, and
152    /// this module's own doc states new modes are layered through pending
153    /// state, not new variants. `SearchState`'s typed `Option<Prompt>` is what
154    /// disambiguates a `<CR>` that submits a search from one that submits an
155    /// ex-command — a discriminator that cannot be forgotten, unlike a bool.
156    SearchOpen(SearchDirection),
157    /// `n` (`reverse = false`) / `N` (`reverse = true`) — jump to the next
158    /// match, relative to the direction the search was committed with, so `N`
159    /// after a `?` search moves forward.
160    SearchRepeat {
161        reverse: bool,
162    },
163    /// `*` (`reverse = false`) / `#` (`reverse = true`) — search the whole word
164    /// under the cursor. Literal, not regex: the word may contain `.` or `[`
165    /// and the user means those characters.
166    SearchWord {
167        reverse: bool,
168    },
169    /// `:noh` — stop highlighting matches while keeping the pattern, so `n`
170    /// still works. Distinct from cancelling a search.
171    ClearSearchHighlight,
172
173    /// `d/foo<CR>` — commit the open search prompt and apply `op` from the
174    /// prompt's ORIGIN to wherever the search lands.
175    ///
176    /// Emitted only by the operator-pending machine; no keymap produces it.
177    /// It exists because committing a search MOVES the cursor, and the
178    /// operator needs the pre-move position as its start point. Carrying the
179    /// operator through the commit makes "operate over a search" one atomic
180    /// action instead of two steps racing to own the cursor.
181    SearchSubmitOperated {
182        op: Operator,
183    },
184
185    /// `gn` / `gN` — the next/previous match AS AN OBJECT.
186    ///
187    /// Not a motion. A motion resolves to a POINT and an operator acts over
188    /// `[cursor, point)`; `gn` names an EXTENT that need not start at the
189    /// cursor, so `dgn` deletes the whole match wherever it is. That
190    /// distinction is why this is its own action rather than a `Motion`
191    /// variant — folding it into `Motion` would silently give
192    /// `[cursor, match.start)`, which deletes the text BEFORE the match.
193    TextObject(TextObject),
194
195    /// `{operator}gn` — apply `op` over a text object's extent.
196    ///
197    /// Emitted only by the operator-pending machine.
198    ApplyOperatorObject {
199        op: Operator,
200        object: TextObject,
201    },
202
203    /// `p` / `P` — put the register back into the buffer.
204    ///
205    /// Named `Put` after vim's own verb, not `Paste`, and the distinction is
206    /// load-bearing rather than pedantic: a put replays escriba's REGISTER
207    /// (whatever `d`/`y` last captured, with its [`crate::RegisterKind`]),
208    /// while a paste will replay the SYSTEM CLIPBOARD via `hasami` — a
209    /// different source, arriving as bracketed-paste bytes rather than a
210    /// keypress, with no linewise/charwise distinction to honour. Reusing one
211    /// name for both is how they would end up sharing an executor that is
212    /// wrong for one of them.
213    ///
214    /// `before` is vim's `P`: a charwise put lands at the cursor column
215    /// instead of after it, a linewise put opens above the line instead of
216    /// below. Every other rule is shared, which is why this is one variant
217    /// with a flag rather than two.
218    Put {
219        before: bool,
220    },
221
222    /// `r{char}` — overwrite the character(s) under the cursor with `char`.
223    ///
224    /// Carries the replacement rather than reading it from pending state, so
225    /// the action is self-contained and `.` can replay it. The KEY that
226    /// supplies it is captured at the key layer (`consume_replace_key`),
227    /// the same place `f`'s operand and `` ` ``'s mark letter are: `rw` must
228    /// not read as `r` then *move a word*.
229    ///
230    /// Not a `Change` operator over one character — `r` does not enter Insert,
231    /// does not touch the register, and refuses at end of line rather than
232    /// appending, all three of which an operator composition would get wrong.
233    ReplaceChar(char),
234
235    /// `J` / `gJ` — join the following line onto this one.
236    ///
237    /// `space: true` is `J`: the next line's leading whitespace is dropped and
238    /// a single space takes the newline's place. `space: false` is `gJ`, which
239    /// splices the lines exactly as they are — the reason to reach for it is
240    /// that `J` is lossy, and a `gJ` spelled as "`J` without the fixup" would
241    /// still have stripped the indent.
242    JoinLines {
243        space: bool,
244    },
245
246    /// `.` — repeat the last text change.
247    ///
248    /// Vim's most-used key, and the half that makes `cgn` a workflow rather
249    /// than a curiosity: `cgn` changes the next match, then `.` changes the
250    /// one after it, giving a per-instance confirmable rename with no
251    /// multi-cursor machinery.
252    RepeatLastChange,
253
254    /// `<C-o>` — walk back to where the last far jump was taken from.
255    ///
256    /// Lives beside the search actions because search is what made it
257    /// necessary — committing a `/` used to be a one-way door — but it is not
258    /// a search action: `G`, `gg`, `%` and tag jumps are the other consumers.
259    JumpBack,
260    /// `<C-i>` — walk forward again after [`Action::JumpBack`].
261    JumpForward,
262
263    /// `m{a-z}` — name the cursor's position so `` `{a-z} `` can return to it.
264    ///
265    /// The letter is carried IN the action for the same reason
266    /// [`Motion::FindChar`] carries its character: the second keystroke is an
267    /// OPERAND, and an action that had to be paired with separate pending
268    /// state is an action a face could dispatch half of.
269    SetMark(char),
270
271    /// `zt` / `zz` / `zb` — move the VIEWPORT so the cursor's line sits at a
272    /// named place on screen, without moving the cursor.
273    ///
274    /// Not a [`Motion`], and the distinction is the whole point: a motion
275    /// changes where you are, and this changes only what you can see. Folding
276    /// it into `Motion` would make it composable with an operator, and `dzz`
277    /// is not a thing.
278    ScrollView(ViewAlign),
279    /// `<BS>` — delete the character BEFORE the caret, wherever the caret is.
280    ///
281    /// ONE action, three targets, routed by the runtime: the search prompt,
282    /// the ex command-line, or the buffer in Insert mode. It is deliberately
283    /// not three actions — a face binding `<BS>` should not have to know which
284    /// of the three the operator is currently typing into, and the routing
285    /// question ("is a prompt open?") is already answered by typed state the
286    /// runtime owns.
287    ///
288    /// Named `PromptBackspace` until 2026-08-09, when the Insert-mode target
289    /// landed. The old name was the honest one while the buffer arm did not
290    /// exist — `text_effect` below already described the buffer arm as though
291    /// it did, which is how it went unnoticed that Insert mode had NO way to
292    /// erase a character.
293    Backspace,
294
295    /// Move the caret inside an open prompt (`←` `→` `Home` `End`).
296    ///
297    /// The prompt was append-only until this existed, so a typo in the middle
298    /// of a pattern could only be fixed by deleting back to it.
299    PromptCaret {
300        to: escriba_search::CaretMove,
301    },
302    /// `<C-g>` / `<C-t>` — step the search PREVIEW to the next/previous
303    /// match without committing.
304    ///
305    /// Distinct from `n` in the one way that matters: this is still
306    /// cancellable. Escape returns to where the search started, which `n`
307    /// after a commit cannot do.
308    SearchPreviewStep {
309        forward: bool,
310    },
311    /// `<Del>` — delete the character AT the caret, wherever the caret is.
312    ///
313    /// The forward-delete sibling of [`Action::Backspace`], routed the same
314    /// way. Never closes a prompt: emptying the text by deleting rightwards is
315    /// not the "backspaced past the `/`" gesture that means "I changed my
316    /// mind".
317    DeleteForward,
318    /// `<C-w>` — delete the word before the caret, wherever the caret is.
319    ///
320    /// The word-sized member of the same erase family as [`Action::Backspace`]:
321    /// ONE action, three targets (search prompt / ex line / buffer), routed by
322    /// the runtime on typed state it already owns.
323    ///
324    /// Named `PromptDeleteWord` until 2026-08-09, when the Insert-mode target
325    /// landed. `<BS>` and `<Del>` had been given their buffer arm that morning
326    /// and these two were left behind, so Insert mode could erase one character
327    /// at a time and nothing larger — the half-migration is exactly what the
328    /// `Prompt` prefix was hiding.
329    DeleteWordBefore,
330    /// `<C-u>` — delete from the caret back to the start of the line.
331    ///
332    /// The line-sized member of the erase family; routed exactly like
333    /// [`Action::DeleteWordBefore`]. In the buffer it stops at the first
334    /// non-blank before falling through to column 0, so the first press on an
335    /// indented line clears what was typed and the second clears the indent —
336    /// vim's two-step, which is what keeps `<C-u>` from eating alignment you
337    /// wanted to keep.
338    DeleteToLineStart,
339    /// Up/Down inside a prompt — walk search history.
340    ///
341    /// `back = true` is older. Stepping forward past the newest entry restores
342    /// the text that was being typed when browsing began, so arrowing through
343    /// history and back never destroys a half-typed pattern.
344    PromptHistory {
345        back: bool,
346    },
347    /// No-op — used when a key sequence is pending but not yet complete.
348    Pending,
349}
350
351/// An [`Action`] with an optional repetition count (vim's `5dd`, `10k`).
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
353pub struct CountedAction {
354    pub count: u32,
355    pub action: Action,
356}
357
358impl CountedAction {
359    #[must_use]
360    pub fn once(action: Action) -> Self {
361        Self { count: 1, action }
362    }
363
364    #[must_use]
365    pub fn repeated(count: u32, action: Action) -> Self {
366        Self {
367            count: count.max(1),
368            action,
369        }
370    }
371}
372
373/// Whether an action can change buffer TEXT.
374///
375/// This exists so that anything cached against the buffer's contents — today
376/// the search-match set, tomorrow anything else derived from it — is
377/// invalidated by construction rather than by remembering to. Search
378/// highlights were stale after every edit precisely because that invalidation
379/// was a thing to remember: `SearchState::refresh` existed and had zero
380/// callers, so inserting four characters repainted the highlight four columns
381/// off.
382#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub enum TextEffect {
384    /// May edit the active buffer. Derived state must be recomputed.
385    Mutates,
386    /// Cannot edit the active buffer.
387    Preserves,
388}
389
390impl Action {
391    /// Classify this action's effect on buffer text.
392    ///
393    /// **Total over `Action` — no wildcard arm.** A new variant fails to
394    /// compile here rather than silently defaulting to "preserves", which is
395    /// the direction that produces a stale-cache bug rather than a slow one.
396    ///
397    /// Deliberately CONSERVATIVE where a variant's reach is open-ended:
398    /// `Command`/`SubmitCommand` can run an ex-command that edits. It is not
399    /// conservative for `Backspace`/`DeleteForward` — those genuinely edit the
400    /// buffer outside a prompt. This paragraph claimed they did for months
401    /// before the Insert-mode arm was written; the classifier was right about
402    /// the design and the executor had simply never implemented it.
403    /// Over-reporting costs one extra scan; under-reporting paints the wrong
404    /// columns, so the asymmetry decides the genuinely doubtful cases.
405    #[must_use]
406    pub const fn text_effect(&self) -> TextEffect {
407        match self {
408            Self::Edit(_)
409            | Self::InsertChar(_)
410            | Self::ApplyOperator { .. }
411            | Self::ApplyOperatorObject { .. }
412            | Self::Undo
413            | Self::Redo
414            | Self::Backspace
415            | Self::DeleteForward
416            | Self::DeleteWordBefore
417            | Self::DeleteToLineStart
418            | Self::TextObject(_)
419            | Self::Command { .. }
420            | Self::SearchSubmitOperated { .. }
421            | Self::RepeatLastChange
422            // A put with an EMPTY register mutates nothing, but the classifier
423            // cannot see the register — and over-reporting costs one re-scan
424            // while under-reporting paints stale columns over freshly pasted
425            // text. The asymmetry decides it, exactly as for `Command`.
426            | Self::Put { .. }
427            | Self::ReplaceChar(_)
428            | Self::JoinLines { .. }
429            | Self::SubmitCommand => TextEffect::Mutates,
430
431            // `o`/`O` add a line; `i`/`I`/`a`/`A` move the caret and nothing
432            // else. Classified from the payload rather than reported as
433            // "Mutates" wholesale: over-reporting here costs a re-scan of the
434            // match set on every `i`, which is the most-pressed key in the
435            // editor.
436            Self::EnterInsert(at) => {
437                if at.opens_a_line() {
438                    TextEffect::Mutates
439                } else {
440                    TextEffect::Preserves
441                }
442            }
443
444            Self::Move(_)
445            | Self::Operator(_)
446            | Self::ChangeMode(_)
447            | Self::Save
448            | Self::Quit
449            | Self::SearchOpen(_)
450            | Self::SearchRepeat { .. }
451            | Self::SearchWord { .. }
452            | Self::ClearSearchHighlight
453            | Self::JumpBack
454            | Self::JumpForward
455            | Self::PromptCaret { .. }
456            | Self::PromptHistory { .. }
457            | Self::SearchPreviewStep { .. }
458            // A mark names a position and a scroll moves the window; neither
459            // touches a byte.
460            | Self::SetMark(_)
461            | Self::ScrollView(_)
462            | Self::Pending => TextEffect::Preserves,
463        }
464    }
465}
466
467/// What an action does to search HIGHLIGHTING.
468///
469/// vim leaves `hlsearch` lit until `:nohlsearch`, which is why nearly every
470/// published vimrc remaps something to `:noh` — the highlight has done its job
471/// the moment you start editing, and leaving it on turns the buffer into
472/// confetti. escriba clears it on the first action that is plainly not part of
473/// searching.
474///
475/// Clearing SUPPRESSES without forgetting: the pattern survives, so `n` still
476/// works and re-lights.
477#[derive(Clone, Copy, Debug, PartialEq, Eq)]
478pub enum HighlightEffect {
479    /// Search highlighting stays as it is.
480    Keep,
481    /// Stop drawing highlights (the pattern is retained).
482    Clear,
483}
484
485impl Action {
486    /// Classify this action's effect on search highlighting.
487    ///
488    /// **Total over `Action` — no wildcard arm.** "We forgot to clear on the
489    /// new command" becomes unconstructible rather than remembered: adding a
490    /// variant forces the decision here.
491    #[must_use]
492    pub const fn highlight_effect(&self) -> HighlightEffect {
493        match self {
494            // Everything that IS searching, or that is operating the prompt.
495            Self::SearchOpen(_)
496            | Self::SearchRepeat { .. }
497            | Self::SearchWord { .. }
498            | Self::SearchSubmitOperated { .. }
499            | Self::ClearSearchHighlight
500            | Self::TextObject(_)
501            | Self::SearchPreviewStep { .. }
502            | Self::PromptHistory { .. }
503            | Self::Backspace
504            | Self::PromptCaret { .. }
505            | Self::DeleteForward
506            | Self::DeleteWordBefore
507            | Self::DeleteToLineStart
508            | Self::InsertChar(_)
509            | Self::SubmitCommand
510            | Self::Pending
511            // A jump is how you USE the matches; extinguishing them mid-walk
512            // would defeat the purpose.
513            | Self::JumpBack
514            | Self::JumpForward
515            // Arming an operator is not yet a move — `d` then `n` must still
516            // see its matches.
517            | Self::Operator(_)
518            // Neither moves the cursor off a match: `zz` re-frames the same
519            // line and `ma` names it. Clearing here would make "centre the
520            // view so I can see the other hits" the gesture that removes them.
521            | Self::SetMark(_)
522            | Self::ScrollView(_)
523            | Self::Save
524            | Self::Quit => HighlightEffect::Keep,
525
526            // A search MOTION is searching, not moving on — `n` must not
527            // extinguish the matches it is walking. Every other motion is a
528            // departure.
529            //
530            // The `_` here is deliberate and is the SAFE direction, unlike
531            // `text_effect`'s: a motion nobody has classified yet is "moving
532            // on", which at worst clears a highlight early. The opposite
533            // default would leave stale confetti on screen.
534            // Entering Insert begins editing, so the search is over. Every
535            // OTHER mode change is navigation or a CANCEL — and a cancel must
536            // not erase the committed pattern's highlights. Both
537            // `SearchState::cancel` and the runtime's own `ChangeMode` arm
538            // promise that in writing ("cancelling a new search must not erase
539            // the old highlights"), and a blanket `Clear` here landed on top of
540            // the cancel it had just performed: `/foo<CR>` then `/bar<Esc>`
541            // silently extinguished `foo`.
542            //
543            // Total over `Mode`, so a new mode must decide.
544            Self::ChangeMode(m) => match m {
545                Mode::Insert => HighlightEffect::Clear,
546                Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::Command => {
547                    HighlightEffect::Keep
548                }
549            },
550
551            // Every member of the family begins editing, so the search is
552            // over — the same reading as `ChangeMode(Insert)` above, and it
553            // must not drift from it.
554            Self::EnterInsert(_) => HighlightEffect::Clear,
555
556            Self::Move(m) => match m {
557                Motion::SearchNext | Motion::SearchPrev => HighlightEffect::Keep,
558                _ => HighlightEffect::Clear,
559            },
560
561            // Moving on, or changing the text: the search is over.
562            Self::ApplyOperator { .. }
563            | Self::ApplyOperatorObject { .. }
564            | Self::RepeatLastChange
565            | Self::Edit(_)
566            | Self::Command { .. }
567            | Self::Put { .. }
568            | Self::ReplaceChar(_)
569            | Self::JoinLines { .. }
570            | Self::Undo
571            | Self::Redo => HighlightEffect::Clear,
572        }
573    }
574}
575
576impl Action {
577    /// Does this action edit or navigate an OPEN PROMPT, rather than doing
578    /// something to the buffer?
579    ///
580    /// The operator-pending machine needs this: during `d/foo` the operator
581    /// must survive every keystroke that is part of composing the pattern, and
582    /// disarm on anything that is not.
583    ///
584    /// **Total over `Action` — no wildcard arm**, and that totality is the
585    /// whole point. The machine originally listed the prompt actions inline;
586    /// when `PromptCaret`, `DeleteForward`, `DeleteWordBefore`,
587    /// `DeleteToLineStart` and `SearchPreviewStep` were added later, none was
588    /// added to that list, so pressing `←` or `<C-g>` midway through `d/foo`
589    /// silently disarmed the operator — reintroducing exactly the defect the
590    /// `AwaitingSearch` state had been created to fix. A new prompt action now
591    /// cannot be added without deciding here.
592    #[must_use]
593    pub const fn edits_prompt(&self) -> bool {
594        match self {
595            Self::InsertChar(_)
596            | Self::Backspace
597            | Self::PromptHistory { .. }
598            | Self::PromptCaret { .. }
599            | Self::DeleteForward
600            | Self::DeleteWordBefore
601            | Self::DeleteToLineStart
602            | Self::SearchPreviewStep { .. } => true,
603
604            Self::Move(_)
605            | Self::Operator(_)
606            | Self::ApplyOperator { .. }
607            | Self::ApplyOperatorObject { .. }
608            | Self::TextObject(_)
609            | Self::Edit(_)
610            | Self::ChangeMode(_)
611            // Insert-entry acts on the BUFFER, never on an open prompt — and
612            // it is unreachable while one is open anyway, because Command mode
613            // preempts every printable key before the table is consulted.
614            | Self::EnterInsert(_)
615            | Self::Command { .. }
616            | Self::SubmitCommand
617            // These edit the BUFFER. They are also unreachable while a prompt
618            // is open, for the same reason `EnterInsert` is.
619            | Self::Put { .. }
620            | Self::ReplaceChar(_)
621            | Self::JoinLines { .. }
622            | Self::Undo
623            | Self::Redo
624            | Self::Save
625            | Self::Quit
626            | Self::SearchOpen(_)
627            | Self::SearchRepeat { .. }
628            | Self::SearchWord { .. }
629            | Self::SearchSubmitOperated { .. }
630            | Self::ClearSearchHighlight
631            | Self::RepeatLastChange
632            | Self::JumpBack
633            | Self::JumpForward
634            | Self::SetMark(_)
635            | Self::ScrollView(_)
636            | Self::Pending => false,
637        }
638    }
639}