Skip to main content

lattice_host/
action.rs

1//! Action enum -- the dispatch language of the editor.
2//!
3//! Phase 5.2: extracted from `lattice-ui-tui::app` to its own
4//! module in `lattice-host`. Renderer-agnostic by construction
5//! (every variant references either `lattice-grammar` types,
6//! `lattice-protocol` types, `lattice-host::chord::KeyChord`,
7//! or std types). Both `lattice-ui-tui` and the future
8//! `lattice-ui-gpui` produce `Action` values via their own
9//! keystroke dispatch and feed them into the same host
10//! `App::apply` path.
11//!
12//! `lattice-ui-tui::app` re-exports `Action`, `FindKind`,
13//! `EchoMessage`, `EchoLevel` so existing `crate::app::Action`
14//! call sites continue to resolve unchanged across the move.
15
16use lattice_grammar::ModalState;
17use lattice_grammar::PaneDirection;
18use lattice_grammar::Register;
19use lattice_grammar::ScrollPos;
20use lattice_grammar::SearchDirection;
21use lattice_grammar::ViewportPos;
22use lattice_grammar::VisualKind;
23use lattice_grammar::command::CommandInvocation;
24
25/// Single-line message rendered in the echo area below the mode
26/// line (DESIGN.md §5.9.10). Replaced by the next call to
27/// `App::set_message` (no timeout-based fade yet).
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct EchoMessage {
30    pub text: String,
31    pub level: EchoLevel,
32}
33
34/// Renderer-side display level for echo messages. Mirrors
35/// `lattice_grammar::EchoLevel` (wire-typed) but kept separate so
36/// renderers can adopt their own display semantics around the
37/// shared wire levels.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum EchoLevel {
40    Trace,
41    Debug,
42    Info,
43    Warn,
44    Error,
45}
46
47/// `f` / `F` / `t` / `T` direction-and-stop discriminant for the inline find
48/// family.
49///
50/// VM.3c: the definition moved down to `lattice-grammar` so `;` and `,` could
51/// become motions — a motion's evaluator has to name the kind, and the grammar
52/// cannot depend upward on the host. Re-exported under the old path so the
53/// keymap and dispatch call sites did not have to move with it.
54pub use lattice_grammar::FindKind;
55
56#[derive(Debug, Clone)]
57pub enum Action {
58    None,
59    Quit,
60    /// CG.1: foreground cancellation — flip the armed token AND snap
61    /// back to Normal. Reached by `action:cancel`, bound to `<C-g>` at
62    /// `KeymapLayer::Builtin` (emacs `keyboard-quit`).
63    /// See [`Editor::cancel_foreground`](crate::Editor::cancel_foreground)
64    /// and `docs/dev/architecture/cancellation.md`.
65    ///
66    /// Deliberately NOT in [`action_is_document_mutation`]: cancel is
67    /// an escape hatch, so it must keep working on a read-only buffer.
68    ///
69    /// [`action_is_document_mutation`]: crate::dispatch::action_is_document_mutation
70    Cancel,
71    /// Run a CommandInvocation through `lattice_grammar::execute()`.
72    Invoke(CommandInvocation),
73    /// Slice 8.i.4.a -- absorb the captured chord into
74    /// `App::partial_chord`, marking that we're partway through
75    /// a multi-key sequence the trie hasn't fully resolved yet.
76    /// Replaces the `Action::SetPending(Pending::After*)` flow
77    /// for prefixes whose only role was "wait for the next key"
78    /// (`g`, `z`, `<C-w>`, `m`, `'`, `` ` ``, `"`, `q`, `@`).
79    /// `App::apply` appends the chord and otherwise no-ops; the
80    /// next keystroke runs through `dispatch_normal` with
81    /// `partial_chord` as the prefix, hitting the trie's
82    /// resolved binding (`gd`, `zo`, `<C-w>v`, ...). Parameterised
83    /// Pending variants (`AfterOperator(_)`,
84    /// `AfterTextObject{_}`, `AfterFindChar{_}`, `AfterCtrlX`)
85    /// stay on the `SetPending` flow for now -- 8.i.4.b retires
86    /// those.
87    AbsorbPartialChord(crate::chord::KeyChord),
88    /// Insert a string at the cursor (used by Insert mode).
89    Insert(String),
90    /// Delete the byte before the cursor (Insert-mode backspace).
91    DeleteCharBackward,
92    /// Insert-mode line editing (readline/vim `<C-a>`, `<C-e>`, `<C-w>`,
93    /// `<C-u>`, `<C-k>`, `<C-t>`, `<C-d>`, …) — general across all buffers.
94    InsertLineEdit(lattice_grammar::InsertLineEdit),
95    /// Move into a different modal state (Insert, Normal, ...).
96    EnterMode(ModalState),
97    /// Vim's `a`: move cursor one byte right (clamped) and enter Insert.
98    EnterAppend,
99    /// Vim's `I`: move to first non-blank of line and enter Insert.
100    EnterInsertFirstNonBlank,
101    /// Vim's `A`: move to end of line and enter Insert.
102    EnterAppendEndOfLine,
103    /// Vim's `gj`: move down one display line (wrap segment).
104    DisplayLineDown,
105    /// Vim's `gk`: move up one display line (wrap segment).
106    DisplayLineUp,
107    /// Vim's `g0`: move to the first byte of the current display segment.
108    DisplayLineStart,
109    /// Vim's `g$`: move to the last byte of the current display segment.
110    DisplayLineEnd,
111    /// Vim's blockwise-visual `I`: move cursor to the leftmost
112    /// column of the block on the top line, enter Insert, and on
113    /// Esc replicate the typed text to every other line in the
114    /// block at the same column. Issued only from Visual(Blockwise).
115    EnterBlockVisualInsert,
116    /// Vim's blockwise-visual `A`: same as [`Self::EnterBlockVisualInsert`]
117    /// but the cursor lands one byte past the rightmost column of
118    /// the block on each line.
119    EnterBlockVisualAppend,
120    /// Vim's `o`: open a new line below the current line and enter Insert.
121    OpenLineBelow,
122    /// Vim's `O`: open a new line above the current line and enter Insert.
123    OpenLineAbove,
124    Undo,
125    Redo,
126    /// Append a digit (0-9) to the in-progress count prefix.
127    PushDigit(u8),
128    /// Enter Visual modal state (`v` Charwise, `V` Linewise) anchored at
129    /// the current cursor.
130    EnterVisual(VisualKind),
131    /// Exit Visual to Normal, collapsing the selection.
132    ExitVisual,
133    /// Vim's `gv` -- re-enter Visual with the same anchor / head / kind
134    /// as the most recently exited Visual selection.
135    ReselectLastVisual,
136    /// SN.3d Select-mode entry (`gh` / `gH` / `g<C-h>`) — anchor a
137    /// zero-width Select selection of the named kind at the cursor,
138    /// mirroring [`Self::EnterVisual`]. Typing then overtypes it.
139    EnterSelect(VisualKind),
140    /// SN.3d Select mode — a bare printable key replaces the whole
141    /// selection with this char and drops into Insert (one undo step).
142    /// The load-bearing new behaviour: see
143    /// `docs/dev/architecture/select-mode.md` §3. Emitted only by
144    /// `translate_select`'s printable fallthrough; the host handler
145    /// (`do_select_overtype`) lands it as a single `Edit::replace`.
146    SelectOvertype(char),
147    /// SN.3d Select-mode `<Esc>` — collapse the selection to a cursor
148    /// and drop to Normal (the Select analogue of [`Self::ExitVisual`]).
149    ExitSelect,
150    /// SN.3d `<C-g>` — toggle between `Visual(k)` and `Select(k)`,
151    /// preserving the selection geometry (reserved in both modes for
152    /// this toggle). One handler flips whichever of the two is active.
153    ToggleVisualSelect,
154    /// Vim's `o` in Visual -- swap the cursor to the other end of the
155    /// selection so motions / text objects act on that end.
156    SwapVisualEnds,
157    /// Vim's `*` (Forward) and `#` (Backward) -- search for the word under
158    /// the cursor in the given direction.
159    SearchWordUnderCursor(SearchDirection),
160    /// Vim's `%` -- jump to the matching bracket. Looks at or beyond the
161    /// cursor on the current line for the first `()[]{}` and seeks its
162    /// pair using a depth-tracking scan.
163    MatchBracket,
164    /// Vim's `~` -- toggle the case of the char at the cursor and
165    /// advance by one byte.
166    ToggleCaseAtCursor,
167    /// Vim's `J` (with-space) and `gJ` (no-space): join the current line
168    /// with the next, replacing the joining newline with a single space
169    /// (or nothing for `gJ`).
170    JoinLines {
171        with_space: bool,
172    },
173    /// Vim's `;` (no-reverse) and `,` (reverse): repeat the last
174    /// f/F/t/T find on the current line.
175    FindRepeat {
176        reverse: bool,
177    },
178    /// Vim's `zf` -- create a fold from the current Visual selection.
179    CreateFoldFromVisual,
180    /// Vim's `zo` -- open the fold containing the cursor.
181    OpenFoldAtCursor,
182    /// Vim's `zc` -- close the fold containing the cursor.
183    CloseFoldAtCursor,
184    /// Vim's `za` -- toggle the fold containing the cursor.
185    ToggleFoldAtCursor,
186    /// Vim's `zR` -- open all folds.
187    OpenAllFolds,
188    /// Vim's `zM` -- close all folds.
189    CloseAllFolds,
190    /// org-cycle `z<Space>` / `:fold-cycle` -- cycle the fold under the
191    /// cursor through FOLDED → CHILDREN → SUBTREE.
192    CycleFoldAtCursor,
193    /// org-cycle `z<Tab>` / `:fold-cycle-global` -- cycle the whole buffer
194    /// through OVERVIEW → CONTENTS → SHOW-ALL.
195    CycleFoldsGlobal,
196    /// `zp` / `:fold-goto-parent` -- move the cursor to the parent heading
197    /// (one level up the fold hierarchy).
198    GotoParentFold,
199    /// Vim's `zd` -- delete the fold containing the cursor.
200    DeleteFoldAtCursor,
201    /// Vim's `zO` -- open the folds containing the cursor, recursively.
202    OpenFoldsRecursively,
203    /// Vim's `zC` -- close the folds containing the cursor.
204    CloseFoldsRecursively,
205    /// Vim's `zD` -- delete the innermost fold at the cursor and its nested folds.
206    DeleteFoldsRecursively,
207    /// Vim's `zj` -- move cursor to the start of the next fold.
208    GotoNextFold,
209    /// Vim's `zk` -- move cursor to the end of the previous fold.
210    GotoPrevFold,
211    /// MO.2: the wheel turned over `pane`.
212    ///
213    /// The pane under the POINTER scrolls, and it does not take focus —
214    /// the convention vim (`mousescroll`), Zed and Helix share, and the
215    /// one that makes a wheel over a reference split usable while you
216    /// are typing in another.
217    MouseScroll {
218        pane: lattice_core::ui::pane::PaneId,
219        down: bool,
220    },
221    /// MO.2: a press or drag resolved to a buffer position in `pane`.
222    ///
223    /// The renderer has already inverted its own geometry; this carries
224    /// a buffer coordinate and nothing about the screen. `extend` is
225    /// what separates the two gestures: a press moves the cursor and
226    /// leaves any Visual selection behind, a drag keeps the press's
227    /// anchor and extends charwise Visual to here. Unlike scroll, this
228    /// DOES focus the pane — clicking into a split is how you move to
229    /// it.
230    MouseGoto {
231        pane: lattice_core::ui::pane::PaneId,
232        line: u32,
233        byte: u32,
234        extend: bool,
235    },
236    /// Vim's `zi` -- toggle [`App::foldenable`]. With folds disabled
237    /// every line renders flat regardless of any closed flag.
238    ToggleFoldEnable,
239    // L7 (lsp-architecture.md §16): the 7 nav `Action::Lsp*Request`
240    // variants (`K` / `gd` / `gD` / `gy` / `gI` / `gr` / `gx`) removed.
241    // The nav surface is mode-owned now — `lsp-mode`'s `action_handlers()`
242    // closures emit `Effect::Lsp(LspRequest::…)`, dispatched host-side by
243    // `editor.lsp_request` onto the unchanged request substrate. The
244    // non-nav LSP actions below (signature-help / completion / symbols /
245    // on-type-formatting) are ex-command / insert-autopilot triggered,
246    // not chord-bound, and stay.
247    /// `:lsp-signature-help` (Phase 4.3). Sends
248    /// `textDocument/signatureHelp` to attached servers; the
249    /// first non-empty response renders into a popup near the
250    /// cursor. In Insert mode the same request fires
251    /// automatically when the user types a server-advertised
252    /// trigger character (commonly `(` and `,`).
253    LspSignatureHelpRequest,
254    /// `:complete` (Phase 4.2.g, picker-flavoured). Fires
255    /// `textDocument/completion` at the cursor; the merged item
256    /// list opens as a vertico picker (label + kind glyph +
257    /// detail). Accept replaces the prefix-under-cursor with
258    /// the item's insert text. Snippet expansion + lazy
259    /// `completionItem/resolve` are queued behind buffer-level
260    /// Insert-mode completion (which doesn't exist yet -- this
261    /// is the bridge until that lands).
262    LspCompletionRequest,
263    /// `<C-t>` -- pop the tag stack (vim's tag-stack
264    /// `:pop`). Walks back through the LIFO chain of `gd` /
265    /// `gD` / `gy` / `gI` drill-downs. Independent of the
266    /// jump-list `<C-o>` walk: the stack and the list have
267    /// different push semantics and can have different lengths.
268    TagStackPop,
269    /// **Insert-mode completion** (Phase 4.2.g.1). Manually
270    /// open the popup at the cursor or refresh an open one.
271    /// Bound by default to `<C-x><C-o>` / `<C-Space>` /
272    /// smart-tab.
273    CompletionTrigger,
274    /// Move the popup selection down (`<C-n>` / `<Down>` /
275    /// `<Tab>` cycle).
276    CompletionNext,
277    /// Move the popup selection up (`<C-p>` / `<Up>` /
278    /// `<S-Tab>` cycle).
279    CompletionPrev,
280    /// Accept the focused candidate (`<C-y>` / `<Tab>` /
281    /// `<CR>`). Splices the candidate's insert text into the
282    /// buffer at the popup's anchor and closes the popup.
283    CompletionAccept,
284    /// Close the popup, stay in Insert (`<C-e>`).
285    CompletionCancel,
286    /// Close the popup AND exit Insert mode (`<Esc>`). Mirrors
287    /// vim's `<Esc>` semantics with one extra step (drop the
288    /// popup before the modal switch).
289    CompletionCancelAndExitInsert,
290    /// Toggle the side documentation popup for the focused
291    /// candidate (`<C-d>`, only inside the completion-popup
292    /// minor mode).
293    CompletionToggleDocs,
294    /// Scroll the docs side popup forward (`<C-f>` inside
295    /// the completion-popup minor mode).
296    CompletionDocsScrollDown,
297    /// Scroll the docs side popup backward (`<C-b>` inside
298    /// the completion-popup minor mode).
299    CompletionDocsScrollUp,
300    /// Restrict the popup to a single completion source.
301    /// String is the `SourceId` (e.g. `"gen:buffer-words"`).
302    /// Bound to the popup-mode filter chords introduced in
303    /// CSM.K2 (`<C-b>` buffer, `<C-o>` lsp, `<C-f>` path,
304    /// `<C-t>` tree-sitter, ...).
305    CompletionFilterToSource(String),
306    /// Clear the active source filter (`<C-Space>`). Restores
307    /// the mixed merged candidate list.
308    CompletionFilterClear,
309    /// Insert-mode character key while the completion popup
310    /// is open (Phase 4.2.g.7 commit-char polish). The App's
311    /// handler decides at apply time:
312    ///
313    /// - If the typed `char` is in the focused candidate's
314    ///   effective commit-character set (LSP-supplied per-item
315    ///   list union'd with `completion.extra_commit_chars`),
316    ///   the popup accepts the candidate then inserts the
317    ///   typed `char` afterward (vim convention: a commit
318    ///   character behaves like "accept and continue typing").
319    /// - Otherwise the typed `char` flows through plain
320    ///   `do_insert_text`; the popup refilters against the
321    ///   updated query as if the layer had returned `None`.
322    ///
323    /// Routing every popup-time char through this single
324    /// action keeps the input layer ignorant of commit-char
325    /// state -- the App reads it once at apply time.
326    CompletionAcceptThenInsert(char),
327    /// YR.5: insert register `c`'s contents at the cursor.
328    InsertRegister(char),
329    /// YR.5: open the yank-ring picker over the current surface.
330    OpenYankPicker,
331    /// YR.6: open the picker registered for the `:`-line argument under
332    /// the cursor, filling the pick back into that argument.
333    OpenArgPicker,
334    // SN.3c.1 (2026-06-14): `Action::SnippetExpand` removed.
335    // `<C-x><C-s>` is mode-owned now (`snippet-mode`'s `keymap()` +
336    // `action_handlers()` emit `Effect::ExpandSnippet`); no host
337    // `Action` round-trip. (`feedback_mode_owns_its_surface`.)
338    // SN.2b (2026-06-12): `SnippetNextPlaceholder` /
339    // `SnippetPrevPlaceholder` removed — `<Tab>` / `<S-Tab>`
340    // placeholder navigation is mode-owned
341    // (`active-snippet-mode`'s `ActionHandlerRegistry` closures),
342    // not a host `Action`.
343    // SN.3c.2 (2026-06-14): `Action::SnippetLeave` removed.
344    // `<Esc>` while a snippet is active is mode-owned now
345    // (`active-snippet-mode`'s per-buffer handler clears the
346    // session + returns `Effect::EnterMode(Normal)`); no host
347    // `Action` round-trip. (`feedback_mode_owns_its_surface`.)
348    // CR.1 (2026-06-24): `Action::DiffGet` / `Action::DiffPut` deleted.
349    // The diff `do`/`dp` chords are mode-owned (`DiffMode::action_handlers()`
350    // → `Effect::ApplyEdit`), so they flow through the generic
351    // `Action::ApplyEdit` below instead of a host-side diff variant — the
352    // mode-ownership acid test (`feedback_mode_owns_its_surface`).
353    /// CR.0: host counterpart of [`lattice_grammar::Effect::ApplyEdit`].
354    /// Applies a mode-computed `edit` to `target` (routing through the
355    /// active-document pipeline when `target` is the focused buffer, or
356    /// the peer-buffer registry handle otherwise) and, when `cursor` is
357    /// `Some`, parks the active cursor at that row. `handle_effect`
358    /// translates the `Effect` into this `Action` and queues it on
359    /// `out.next_actions`; the applier arm in `handle_action` calls
360    /// `Editor::apply_targeted_edit`. The generic primitive the diff
361    /// (and future) modes drive instead of host `do_<x>` methods.
362    ApplyEdit {
363        target: lattice_core::BufferId,
364        edit: lattice_protocol::edit::Edit,
365        cursor: Option<lattice_protocol::position::Position>,
366    },
367    // M.10.7 (2026-06-03): four dead Action variants deleted —
368    // `MultibufferExpand`, `SearchTrigger`, `SearchJumpToSource`,
369    // `SearchRefresh`. All four are now mode-owned via the
370    // M.10.1.b ActionHandlerRegistry (the chord/ex-command
371    // routes through `run_invocation` → registry consultation →
372    // handler) OR work happens inline in the apply_effect arm
373    // (`:multibuffer-expand` + `:search`). No production path
374    // constructs these variants any longer.
375    /// `:lsp-symbols` (Phase 4.2.e). Send
376    /// `textDocument/documentSymbol` to every attached server;
377    /// render the merged outline as a vertico picker. Selecting
378    /// a row jumps to the symbol's location.
379    LspDocumentSymbolRequest,
380    /// `:lsp-workspace-symbol [query]` (Phase 4.2.f). Send
381    /// `workspace/symbol` to every attached server with the
382    /// user-supplied query string (server-side substring filter).
383    /// Empty query returns the server's idea of "everything"
384    /// (rust-analyzer streams all crate symbols). Picker UX
385    /// mirrors the document-symbol path.
386    LspWorkspaceSymbolRequest(String),
387    /// `"<reg>` prefix -- stash the named register for the next operator
388    /// / paste invocation.
389    SelectRegister(Register),
390    /// Vim's `Ctrl-O` -- step backward in the position history.
391    JumpHistoryBack,
392    /// Vim's `Ctrl-I` (Tab) -- step forward.
393    JumpHistoryForward,
394    /// PBH.3: `<C-6>` -- step back through the ACTIVE PANE's buffer
395    /// trail. Per-pane and buffer-granular, unlike the global,
396    /// position-granular `JumpHistoryBack`.
397    PaneHistoryBack,
398    /// PBH.3: `<C-7>` -- step forward through the active pane's
399    /// buffer trail.
400    PaneHistoryForward,
401    /// Vim's `Ctrl-L` -- force a full redraw. Reparses the syntax
402    /// tree, recomputes folds, clears the visible-highlight cache,
403    /// and tells the runtime to clear the terminal screen on the
404    /// next frame. Intended escape hatch for any visual glitch
405    /// (stale highlights, leftover ANSI escape sequences from a
406    /// crashed external program, terminal-resize race).
407    RedrawScreen,
408    /// Vim's `g;` -- step backward through `NamedMark` entries in the
409    /// unified position history.
410    WalkMarkHistoryBack,
411    /// Vim's `g,` -- step forward.
412    WalkMarkHistoryForward,
413    /// Vim's `q<reg>` to start recording into a register; `q` while
414    /// recording stops. App handles routing internally.
415    StartMacroRecord(char),
416    StopMacroRecord,
417    /// Vim's `@<reg>` to play. Replays the recorded Action stream.
418    PlayMacro(char),
419    /// Vim's `@@` to repeat the most recently played macro.
420    PlayLastMacro,
421    /// Vim's `.` -- re-dispatch the last buffer-mutating invocation from
422    /// the current cursor.
423    RepeatLastChange,
424    /// Replace mode: overwrite the char at the cursor with `c` and advance.
425    /// Beyond end-of-line, falls back to insert (vim behavior).
426    OverwriteChar(char),
427    /// Backspace within Replace -- pop the latest entry from
428    /// `replace_history` and restore the original byte (or delete if the
429    /// overwrite was a line extension).
430    ReplaceUndoLast,
431    /// Jump cursor to a viewport-relative line (vim's `H`, `M`, `L`).
432    JumpViewport(ViewportPos),
433    /// Adjust scroll so the cursor lands at the viewport top / center /
434    /// bottom (vim's `zt`, `zz`, `zb`).
435    ScrollCursorTo(ScrollPos),
436    /// HS.2: manual horizontal scroll (vim `z{l,h,L,H,s,e}`).
437    HorizontalScroll(lattice_grammar::HScroll),
438    /// Move cursor down / up by one viewport-page (vim's Ctrl-F / Ctrl-B).
439    PageDown,
440    PageUp,
441    /// VM.3j-2: vim's `<C-d>` / `<C-u>` — view and cursor together by
442    /// `scroll` lines. Scroll commands, so no operator composes with them.
443    HalfPageDown,
444    HalfPageUp,
445    /// Scroll the viewport one line up (Ctrl-Y) or down (Ctrl-E),
446    /// nudging the cursor to keep it on-screen.
447    ScrollLineUp,
448    ScrollLineDown,
449    /// `m<letter>` -- record the cursor at mark `<letter>`.
450    SetMark(char),
451    /// `'<letter>` -- jump to the line of mark `<letter>` (column = first
452    /// non-blank).
453    JumpToMarkLine(char),
454    /// `` `<letter> `` -- jump to the exact position of mark `<letter>`.
455    JumpToMarkExact(char),
456
457    // ---- Command-line minibuffer (Phase 2: simple, single-line) ----
458    /// Open the command picker (`:`/`M-x`). On accept: if the
459    /// chosen command needs a required arg, arm the cmdline;
460    /// otherwise execute immediately.
461    OpenCommandPicker,
462    /// MB.3: `q:` -- open the command-line *history* picker over
463    /// `command_history`; accept loads the picked command into the
464    /// `:` line without executing.
465    OpenHistoryPicker,
466    /// MB.5: `q/` / `q?` — open the search-line *history* picker
467    /// over `search_history`; accept loads the picked term into the
468    /// `/` search line without executing.
469    OpenSearchHistoryPicker,
470    /// Pressed `:` in Normal mode -- enter command modal with empty buffer.
471    EnterCommandLine,
472    /// Append a character to the in-progress command line.
473    CommandLineAppend(char),
474    /// Delete the last character. If the buffer is empty, leave Command mode.
475    CommandLineBackspace,
476    /// Submit the current command line: parse + execute, then leave Command.
477    CommandLineSubmit,
478    /// Drop the current command line and leave Command modal.
479    CommandLineCancel,
480    /// Walk to an older entry in the command history (`Up` arrow in
481    /// Command modal).
482    CommandLineHistoryPrev,
483    /// Walk to a newer entry, eventually returning to the user's
484    /// in-progress line.
485    CommandLineHistoryNext,
486    /// MB.2: toggle the `:` line's expanded tier-2 mini-buffer band
487    /// (`<C-x><C-e>`); collapse returns the edited text to the one-row
488    /// line for review.
489    CommandLineToggleExpand,
490    /// MB.5a: `<CR>` on the `/`·`?` search line — submit the search
491    /// pattern. Resolved from `search-line-mode`'s Insert keymap.
492    SearchLineSubmit,
493    /// MB.5a: `<Esc>` / `<C-c>` on the `/`·`?` search line — cancel
494    /// the search and restore the prior editing buffer.
495    SearchLineCancel,
496    /// `<BS>` on the `/`·`?` line: delete a char, or cancel when empty.
497    SearchLineBackspace,
498    /// MB.5b: `<C-p>` / `<Up>` on the `/`·`?` search line — walk to an
499    /// older entry in `search_history`.
500    SearchLineHistoryPrev,
501    /// MB.5b: `<C-n>` / `<Down>` on the `/`·`?` search line — walk to a
502    /// newer entry in `search_history`.
503    SearchLineHistoryNext,
504    /// `<CR>` in `prompt-line-mode` — submit the typed text to the
505    /// caller-named `on_submit_action` handler.
506    PromptLineSubmit,
507    /// `<Esc>` / `<C-c>` in `prompt-line-mode` — cancel, restore the
508    /// prior editing buffer.
509    PromptLineCancel,
510    /// Replace the echo area with a typed message.
511    Echo(EchoMessage),
512
513    // ---- Hover popup ----
514    /// Dismiss the hover popup. Mirrors the `:HoverClose` ex-command
515    /// for the keymap path. Once a hover is *promoted* to a help
516    /// buffer (via the second-K gesture), the standard
517    /// help-dismissal path (`HelpDismiss`) closes it instead.
518    CloseHover,
519
520    // ---- Picker (DESIGN.md §5.9.7) ----
521    /// Append a character to the picker's query and refilter.
522    PickerAppend(char),
523    /// Drop the last char from the picker's query and refilter.
524    PickerBackspace,
525    /// Move the selection cursor down one row (wraps).
526    PickerSelectNext,
527    /// Move the selection cursor up one row (wraps).
528    PickerSelectPrev,
529    /// Run the picker's accept action against the selected
530    /// candidate and dismiss.
531    PickerAccept,
532    /// Issue #32 (2026-05-22): `<C-s>` — accept candidate,
533    /// opening files in a horizontal split. Non-file outcomes
534    /// ignore the override.
535    PickerAcceptInSplit,
536    /// `<C-v>` — accept candidate in a vertical split.
537    PickerAcceptInVSplit,
538    /// `<C-t>` — accept candidate in a new tab.
539    PickerAcceptInTab,
540    /// PC.10: `<C-l>` — go INTO the selected candidate rather than
541    /// choosing it. The source answers through
542    /// `PickerSourceGenerator::descend`; a source with no notion of
543    /// going deeper (every one but `dir-pick` today) returns `None` and
544    /// this does nothing at all.
545    PickerDescend,
546    /// PC.10 / PH.1: `<C-w>` — the peer of [`Self::PickerDescend`], one
547    /// level up where the source has depth (`PickerSourceGenerator::ascend`),
548    /// and the command-line's delete-word everywhere else.
549    ///
550    /// The shape of [`Self::PickerDescendOrSelectNext`], for the same reason:
551    /// only the dispatcher can ask which source seated the picker. Ascend is
552    /// asked FIRST, so a source with depth gets its own notion of "up" rather
553    /// than a word boundary; a grep pattern, whose source declines, loses a
554    /// word rather than everything back to a `/`.
555    ///
556    /// Was `<C-h>` until PH.1 gave that key to picker help.
557    PickerAscendOrDeleteWord,
558    /// PH.1: `<C-h>` — close the picker and open its help page. See
559    /// `Editor::do_picker_help` for how the page is chosen.
560    PickerHelp,
561    /// PP.5: `<Tab>` — drill in where the source has depth, select the next
562    /// row everywhere else.
563    ///
564    /// One action rather than a `<Tab>` that translate resolves two ways,
565    /// because translate cannot see which source seated the picker — only the
566    /// dispatcher can ask it. The fallback is what keeps `<Tab>` meaning
567    /// select-next in the pickers that have no notion of depth, which is every
568    /// one but `dir-pick` today.
569    PickerDescendOrSelectNext,
570    /// PD.1: `<C-d>` — remove the selected row from whatever backs the list.
571    ///
572    /// The source names the verb through
573    /// `PickerSourceSpec::delete_command`; a source that names none leaves
574    /// this doing nothing at all. Never a filesystem delete — see that
575    /// field's doc for why that boundary is load-bearing.
576    PickerDelete,
577    /// LR.5 (2026-08-11): `<C-q>` — send every candidate that survived
578    /// the current query to the picker's declared bulk outcome, and
579    /// dismiss. Echoes when the opener declared none.
580    PickerBulkAccept,
581    /// Drop the picker without acting on any candidate.
582    PickerDismiss,
583
584    // ---- Transient (picker transient mode — PICK.1) ----
585    /// Fire the action for a transient item key press. The string
586    /// is the item's label (used as the lookup key in the current
587    /// transient spec's groups).
588    TransientTrigger(String),
589    /// Toggle a boolean flag in the transient state.
590    TransientToggleFlag(String),
591    /// Dismiss the transient (close the picker overlay).
592    TransientDismiss,
593
594    // ---- Paste (`p`, `P`) ----
595    /// Vim's `p` -- paste the unnamed register after the cursor (charwise)
596    /// or below the current line (linewise).
597    PasteAfter,
598    /// Vim's `P` -- paste before cursor / above current line.
599    PasteBefore,
600    /// A bracketed-paste burst from the terminal -- the user pressed
601    /// their terminal's paste shortcut (Ctrl-Shift-V, Cmd-V, mouse
602    /// middle-click, ...) and the terminal handed us the whole payload
603    /// in one event. Mode-dependent target: cursor in Insert/Normal/
604    /// Visual/Replace, command line in Command, search line in Search.
605    /// One undo unit, so a single `u` reverts the entire paste.
606    PasteText(String),
607
608    // ---- Command-line editing (DESIGN.md §5.11.3) ----
609    /// `<C-u>` -- clear the entire command line.
610    CommandLineClear,
611    /// `<C-w>` -- delete the word to the left of the cursor.
612    /// (v1: cursor is at end-of-line, so deletes the trailing word.)
613    CommandLineDeleteWordBackward,
614    /// `<C-h>` -- describe the command word / arg under cursor.
615    /// Hybrid resolution: word-at-cursor describes itself if it
616    /// resolves to a registered command; else describe the parent
617    /// command at the relevant `arg:<name>` anchor.
618    CommandLineDescribeUnderCursor,
619    /// Chord-capture overlay (`ArgKind::Chord` slot): append one
620    /// pre-formatted chord token (`<C-c>`, `<Esc>`, `gg`, ...) to
621    /// the cmdline. Translation from the raw `KeyEvent` happens
622    /// in `input::translate_command_chord_capture`.
623    CommandLineAppendChord(String),
624    /// A transient menu row's key, already in canonical chord spelling
625    /// (`<CR>`, `<Tab>`, `<Space>`, `g`). Emitted by
626    /// `Editor::retarget_claimed_transient_key` when the showing spec claims
627    /// a key the picker would otherwise spend on its own navigation, so a
628    /// menu can bind `<CR>` without losing it to "accept the selected row".
629    TransientKey(String),
630
631    // ---- Completion popup (DESIGN.md §5.11.3) ----
632    /// `<Tab>` -- open completion popup if closed; advance the
633    /// selected candidate if open.
634    CommandLineCompleteOrAdvance,
635    /// `<S-Tab>` -- previous candidate when popup is open.
636    CommandLineCompletePrev,
637    /// `<CR>` while popup open -- replace the prefix with the
638    /// selected candidate's `text` and close the popup.
639    CommandLineAcceptCompletion,
640    /// `<Esc>` while popup open -- close the popup without
641    /// touching the command line. (Two-stage Esc: a second Esc
642    /// then cancels the command line.)
643    CommandLineDismissCompletion,
644
645    // ---- Pane tree (DESIGN.md §5.9) ----
646    /// `<C-w>s` -- split the active pane horizontally (new pane below).
647    SplitPaneHorizontal,
648    /// `<C-w>v` -- split the active pane vertically (new pane right).
649    SplitPaneVertical,
650    /// `<C-w>c` / `<C-w>q` -- close the active pane.
651    ClosePane,
652    /// `<C-w>o` / `:only` / emacs `C-x 1` -- close every pane except
653    /// the active one. S3b (2026-06-22).
654    OnlyPane,
655    /// ZP.2: `<C-w>z` / `<C-w><C-z>` / `:zoom-pane` -- toggle
656    /// tmux-style zoom on the active pane. The non-destructive
657    /// counterpart of [`Self::OnlyPane`]: the split layout survives
658    /// and the second toggle restores it verbatim.
659    ToggleZoomPane,
660    /// `<C-w>{h,j,k,l}` -- move the active pane cardinally.
661    NavigatePane(PaneDirection),
662    /// `<C-w>w` -- cycle to the next pane in declaration order.
663    NextPane,
664    /// `<C-w>W` -- cycle to the previous pane.
665    PrevPane,
666    /// Issue #29 (2026-05-22): vim's `gt` — next tab.
667    NextTab,
668    /// Vim's `gT` — previous tab.
669    PrevTab,
670    /// Vim's `{N}gt` — switch to tab N (1-indexed; clamped).
671    GoToTab(u32),
672    /// `:tabnew` — new empty tab (scratch buffer).
673    NewTab,
674    /// `:tabnew <path>` — new tab opening `path`.
675    NewTabAt(String),
676    /// Issue #40 / Terminal-mode T1 (2026-05-22):
677    /// `:terminal [cmd]` — spawn a shell (or `cmd` if given)
678    /// under a fresh PTY and activate as a new terminal
679    /// buffer. None ⇒ spawn the user's shell from
680    /// `terminal.shell` (default `$SHELL` else `/bin/sh`).
681    TerminalSpawn(Option<String>),
682    /// Terminal-mode T2.a (2026-05-25): write encoded bytes to
683    /// the active Terminal buffer's PTY stdin. Emitted by the
684    /// translate layer when the user is in Terminal-Insert mode
685    /// on a Terminal buffer and the chord encodes to ANSI via
686    /// `keymap_terminal::key_to_ansi`. The host handler
687    /// (`Editor::do_terminal_input`) looks up the active
688    /// buffer's `PtyHandle` and forwards. No-op on non-Terminal
689    /// active buffers (defensive — translate never emits in
690    /// that state but the handler stays safe).
691    TerminalInput(Vec<u8>),
692    /// Terminal-mode T2.a: activate `terminal-insert-mode` on
693    /// the active Terminal buffer. Emitted by `i` (later `a`/
694    /// `I`/`A`) in Normal-in-terminal. No-op when the active
695    /// buffer is not a Terminal.
696    EnterTerminalInsert,
697    /// Terminal-mode T2.a: deactivate `terminal-insert-mode`
698    /// on the active Terminal buffer. Emitted by `<C-\><C-n>`
699    /// (and, T2.b, optionally `<Esc>` when `terminal.esc_exits`
700    /// is true).
701    ExitTerminalInsert,
702    /// Terminal-mode T3 (2026-05-25): re-position the active
703    /// terminal's scrollback viewport. Emitted by Normal-in-
704    /// terminal motions (`j`/`k`/`<C-d>`/`<C-u>`/`gg`/`G`).
705    /// No-op when the active buffer is not a Terminal.
706    TerminalScroll(lattice_terminal::TerminalScrollKind),
707    /// Terminal-mode T2.c (2026-05-25): user pressed `<C-\>` in
708    /// Terminal-Insert; arm the two-key exit chord on the
709    /// active terminal buffer. The next keystroke resolves it
710    /// (`<C-n>` exits; anything else sends `\x1c` + that key
711    /// to the PTY). Cleared automatically by the
712    /// `ExitTerminalInsert` / `TerminalInput` arms.
713    TerminalArmExitChord,
714    /// T4 (2026-05-25): `<C-w>T` — move the active pane (and
715    /// its buffer) to a fresh tab. Vim convention. The
716    /// previous tab loses the pane via the standard close-pane
717    /// path; the new tab opens with a single pane referencing
718    /// the same `BufferId`.
719    MovePaneToNewTab,
720    /// `:tabclose` — close active tab (no-op when only one tab).
721    CloseTab,
722    /// `:tabonly` — close every tab except the active one.
723    OnlyTab,
724    /// `:tabmove [N]` — move the active tab to position N
725    /// (1-indexed). Negative or omitted N is handled by the
726    /// ex-command parser; the runtime value here is the
727    /// resolved target index.
728    MoveTab(u32),
729    /// `<C-w>=` -- reset every split's ratio to 0.5 (equalize all
730    /// panes). Issue #28 (2026-05-22).
731    EqualizePanes,
732    /// `<C-w>+` -- grow the active pane vertically (nudge the
733    /// nearest HorizontalSplit ancestor's ratio).
734    GrowPaneHeight,
735    /// `<C-w>-` -- shrink the active pane vertically.
736    ShrinkPaneHeight,
737    /// `<C-w>>` -- grow the active pane horizontally (nudge the
738    /// nearest VerticalSplit ancestor's ratio).
739    GrowPaneWidth,
740    /// `<C-w><` -- shrink the active pane horizontally.
741    ShrinkPaneWidth,
742
743    // ---- Help buffer (DESIGN.md §5.11, §5.9) ----
744    //
745    // Help is a regular buffer routed through the same Normal-mode
746    // chord grammar as the document buffer (motions, page motions,
747    // viewport jumps, `<C-o>` / `<C-i>`, `gg` / `G`, etc.). The
748    // App's `active_buffer` field decides which cursor an action
749    // affects. Only two help-specific actions remain -- buffer-local
750    // bindings emitted by `translate()` when active_buffer == Help:
751    /// Close the active help overlay (`Esc` / `q`).
752    HelpDismiss,
753    /// Follow the link under the cursor (`<CR>`). Resolves the
754    /// link's URL scheme and dispatches: `command:NAME` re-runs
755    /// `:describe-command NAME`, `key:CHORD` re-runs
756    /// `:describe-key CHORD`, `file:PATH:LINE` opens the file at
757    /// the line. Cursor not on a link is a no-op.
758    FollowLink,
759    /// `-` in any normal-mode context — context-sensitive:
760    /// • Document / FileTree → open oil for parent dir of current file / hovered entry
761    /// • Oil buffer → `oil.navigate_up()`
762    OilNavigateUp,
763
764    // ---- 5.5.G.23.insert: host→App LSP autopilot follow-ups ----
765    /// 5.5.G.23.insert: emitted by host-side `Editor::do_insert_text`
766    /// after a typed character matches the active document's
767    /// `onTypeFormatting` trigger-char set. App-side handler fires
768    /// `textDocument/onTypeFormatting` against the highest-priority
769    /// server advertising the trigger and applies the returned edits
770    /// as one undo unit.
771    LspOnTypeFormattingRequest(char),
772    /// 5.5.G.23.insert: emitted by host-side
773    /// `Editor::maybe_refresh_insert_completion_after_edit` when the
774    /// last LSP completion response was `isIncomplete` and the popup
775    /// just refiltered against the new query. App-side handler
776    /// dispatches `textDocument/completion` through the
777    /// `LspCompletionSource`'s async fan-out.
778    LspInsertCompletionRequest,
779
780    // ---- Search (`/`, `?`, `n`, `N`) ----
781    /// Pressed `/` (Forward) or `?` (Backward) -- enter Search modal with
782    /// empty pattern, remembering origin so cancel restores cursor.
783    EnterSearch(SearchDirection),
784    SearchAppend(char),
785    /// Delete one char from the pattern. If pattern is empty, leave Search.
786    SearchBackspace,
787    /// Confirm the pattern: jump to current match (if any) and store it
788    /// as `last_search` for `n`/`N` repeat.
789    SearchSubmit,
790    /// Drop the in-progress pattern, restore cursor, leave Search.
791    SearchCancel,
792    /// MB.5c: toggle the `/`·`?` search line's expanded tier-2
793    /// mini-buffer band (`<C-x><C-e>`).
794    SearchLineToggleExpand,
795    /// Repeat the last search in its original direction.
796    SearchNext,
797    /// Repeat the last search in the opposite direction.
798    SearchPrevious,
799    // ---- Phase 5.8.AF.5 / Slice 3c.final.C ----
800    // Renderer-thread non-dispatch mutations lifted to Action
801    // variants so the renderer doesn't need `&mut Editor` to
802    // perform them. Each fires from the per-frame setup code in
803    // the TUI's `main_loop` / GPUI's `EditorView::render` and
804    // dispatches through the standard `apply` tail (which
805    // publishes RenderState).
806    /// Set the active pane's viewport-height (rows). Triggered by
807    /// the renderer when the window size changes. Mirrors the
808    /// pre-3c.final `App::set_viewport_height` shape — clamps to
809    /// `>= 1` and runs `ensure_cursor_visible` host-side.
810    SetViewportHeight(u32),
811    /// Auto-scroll so the cursor stays visible in the active
812    /// pane. Idempotent: no-op when the cursor is already on
813    /// screen.
814    EnsureCursorVisible,
815    /// Dismiss the active popup (closes `popup_buffer`, restores
816    /// the previous pane focus). No-op when no popup is open.
817    DismissPopup,
818    /// Mirror the TUI's terminal width into editor state so
819    /// status-line layout matches what crossterm reported.
820    SetTerminalWidth(u16),
821    /// IM.7a — the drawing peer's CELL geometry in pixels: one row's
822    /// height and one column's advance.
823    ///
824    /// The host sizes an inline media block (`block_geometry` wants a line
825    /// height and a pane width, both in pixels) and has no other way to
826    /// learn either — `terminal_width` is columns and `viewport_height` is
827    /// rows. Combined with the pane's existing column count this yields the
828    /// pane's pixel width, so this is two numbers rather than a per-pane
829    /// channel.
830    ///
831    /// A peer that cannot draw images never sends it, and the host then
832    /// reserves the provisional row count and reads no image file at all.
833    /// That is the TUI: it shows alt text, which needs no header read.
834    SetCellMetrics {
835        row_px: f32,
836        col_px: f32,
837    },
838    /// Clear `pending_redraw` after the renderer has cleared the
839    /// terminal buffer in response to `<C-l>` (`RedrawScreen`).
840    AcknowledgeRedraw,
841    /// SN.3c.2b: run a sequence of actions in order. Produced by
842    /// `dispatch_insert` for a `fall_through` binding —
843    /// `[mode_action, native_action]` — where the mode's chord
844    /// augments a native chord (`active-snippet-mode`'s `<Esc>` clears
845    /// the session, then continues to the builtin `<Esc>` → exit
846    /// insert). The renderer's `apply` applies each in order. General
847    /// (not snippet-specific): any future binding that wants to run +
848    /// continue resolves to a `Chain`.
849    Chain(Vec<Action>),
850}