lattice_grammar/app_effect.rs
1//! `AppEffect` -- the typed App-side effects produced by free-form
2//! `CommandKind::Action` registrations.
3//!
4//! Background: the keymap-bound `Action` enum in `lattice-ui-tui`
5//! historically encoded the App's response to non-grammar chords
6//! (`<Esc>`, `o`, `<C-w>v`, ...). Slice 8.i (see
7//! [`docs/dev/notes/8i-approach.md`](../../../docs/dev/notes/8i-approach.md)) retires
8//! the per-binding `Action` bridge and routes those chords through
9//! the unified dispatcher's `CommandKind::Action` branch instead.
10//! Action-kind registry entries return `Effect::AppAction(AppEffect)`;
11//! the App's `apply_effect` then matches on the inner `AppEffect`
12//! variant.
13//!
14//! Why a sibling type to `Effect` rather than a flat extension:
15//!
16//! - `Effect`'s existing variants describe *core / dispatcher-native*
17//! work (`Edits`, `SelectionChange`, `Yank`, `EnterMode`,
18//! `Substitute`, ...) and *ex-command-emitted* App work
19//! (`SaveBuffer`, `OpenBuffer`, `LspRestart`, ...). Both are
20//! produced by code paths that *resolve* a typed grammar concern
21//! into a typed effect.
22//! - `AppEffect` is for chord bindings that historically had no
23//! grammar concept attached -- `<Esc>` exits Visual, `<C-w>v`
24//! splits a pane, `o` opens a line below. Treating those as
25//! first-class "free-form actions" keeps the dispatcher contract
26//! ("everything returns Effect") honest without fusing two
27//! conceptually different surfaces into one giant enum.
28//!
29//! Applied host-side by `Editor::apply_app_effect` (reached from the
30//! [`crate::Effect::AppAction`] arm of `handle_effect`), so every variant
31//! acts on the focused buffer / active pane at apply time unless it names a
32//! buffer. Several variants are now *fallback shells*: the chord is owned by
33//! a mode whose `ActionHandlerRegistry` closure intercepts it before the
34//! arm runs, and the arm is a no-op kept so the registered action id
35//! resolves. Those variants say so.
36
37use serde::{Deserialize, Serialize};
38
39use crate::modal::{ModalState, SearchDirection, VisualKind};
40use crate::register::Register;
41use crate::registry::OperatorId;
42
43// M.4 follow-up: `PaneDirection` moved to `lattice-core::ui::pane`
44// (the same crate as the pane geometry). lattice-grammar
45// re-exports it so `AppEffect::NavigatePane(PaneDirection)`
46// remains ergonomic.
47pub use lattice_core::ui::pane::PaneDirection;
48
49/// Vim's `H` / `M` / `L` target positions: where in the visible
50/// viewport the cursor lands. App-side concept hosted here so
51/// `AppEffect::JumpViewport(ViewportPos)` can carry the typed
52/// payload without `lattice-ui-tui` having to dance through a
53/// dependency cycle. Slice 8.i.2.c hoist; the App's previous
54/// `crate::app::ViewportPos` becomes a `pub use` re-export of
55/// this type.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum ViewportPos {
58 /// `H` -- first visible line of the viewport.
59 Top,
60 /// `M` -- middle visible line of the viewport.
61 Middle,
62 /// `L` -- last visible line of the viewport.
63 Bottom,
64}
65
66/// Vim's `zz` / `zt` / `zb` target positions: where in the
67/// viewport the cursor's current line should sit after the
68/// scroll. App-side concept hosted alongside [`ViewportPos`] for
69/// the same dependency reason. Slice 8.i.2.c hoist.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub enum ScrollPos {
72 /// `zt` -- cursor's line lands at the top of the viewport.
73 Top,
74 /// `zz` -- cursor's line lands at the vertical centre.
75 Center,
76 /// `zb` -- cursor's line lands at the bottom of the viewport.
77 Bottom,
78}
79
80/// Manual horizontal-scroll commands (HS.2), the vim `z{l,h,L,H,s,e}`
81/// family. Carried by [`AppEffect::HorizontalScroll`]; the host
82/// handler mutates `leftcol` and keeps the cursor inside the new
83/// window. `wrap`-off only (no-op under wrap, like the cursor-follow
84/// clamp). See `docs/dev/architecture/horizontal-scroll.md`.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86pub enum HScroll {
87 /// `zl` / `zh`: scroll `count` columns right / left.
88 Columns {
89 /// `true` = `zl` (view moves right), `false` = `zh`.
90 right: bool,
91 },
92 /// `zL` / `zH`: scroll half the body width right / left.
93 HalfScreen {
94 /// `true` = `zL`, `false` = `zH`.
95 right: bool,
96 },
97 /// `zs` (cursor to left edge) / `ze` (cursor to right edge).
98 CursorToEdge {
99 /// `true` = `ze` (cursor column at the right edge), `false` = `zs`.
100 end: bool,
101 },
102}
103
104/// CM.2 (2026-07-22): which error entry a [`AppEffect::ErrorNav`]
105/// should resolve to. Carried in the AppEffect so the whole
106/// `:cnext` / `:cprev` / `:cc` / `:cfirst` / `:clast` / `]q` / `[q`
107/// family shares a single host handler (`Editor::do_error_nav`).
108/// The error list is core substrate (like the jump ring), so a
109/// host AppEffect variant is the right carrier — not a
110/// provider-specific one.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112pub enum ErrorTarget {
113 /// `:cnext` / `]q` — next entry (wraps to first past the end).
114 Next,
115 /// `:cprev` / `[q` — previous entry (wraps to last past the start).
116 Prev,
117 /// `:cc [N]` — jump to the Nth entry (1-based). `None` (bare
118 /// `:cc`) re-visits the current entry.
119 Jump(Option<usize>),
120 /// `:cfirst` / `[Q` — jump to the first entry.
121 First,
122 /// `:clast` / `]Q` — jump to the last entry.
123 Last,
124 /// `:cnextfile` / `]qf` — first entry of the next file (wraps).
125 NextFile,
126 /// `:cprevfile` / `[qf` — first entry of the previous file (wraps).
127 PrevFile,
128}
129
130/// App-side typed effect produced by a `CommandKind::Action`
131/// dispatch (DESIGN.md §5.2.1, see also `docs/dev/notes/8i-approach.md`).
132///
133/// Insert-mode line-editing operations — the general readline/vim chords
134/// available in **every** buffer (part of the built-in grammar keymap, not a
135/// mode). Distinct from Normal-mode motions/operators because Insert cursor
136/// semantics differ (the caret sits *between* bytes and may rest past the last
137/// char), so `<C-e>` lands after the last byte where `$` would land on it.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139pub enum InsertLineEdit {
140 /// `<C-a>`: caret to the start of the line (byte 0).
141 CursorLineStart,
142 /// `<C-e>`: caret to the end of the line (past the last byte).
143 CursorLineEnd,
144 /// `<C-b>`: caret one byte left (stops at line start).
145 CursorCharLeft,
146 /// `<C-f>`: caret one byte right (stops past the last byte).
147 CursorCharRight,
148 /// `<C-w>`: delete the word before the caret.
149 DeleteWordBackward,
150 /// `<C-u>`: delete from the line start to the caret.
151 DeleteToLineStart,
152 /// `<C-k>`: delete from the caret to the line end.
153 KillToLineEnd,
154 /// `<C-t>`: indent the current line by one shiftwidth.
155 IndentLine,
156 /// `<C-d>`: dedent the current line by one shiftwidth.
157 DedentLine,
158}
159
160/// Variants are added incrementally during slice 8.i as each
161/// historical `Action` variant is promoted from the legacy
162/// `bind_legacy` bridge to a typed `CommandInvocation`.
163///
164/// **`Eq` was dropped in PV.1 (2026-08-12)**, deliberately.
165/// [`Self::OpenProviderView`] carries an [`Args`](crate::args::Args) so a
166/// provider's trigger keeps the typed multi-argument shape the `:` line
167/// and transient rows already produce — and `Args` reaches
168/// `ArgValue::Invocation(Box<CommandInvocation>)`, which is `PartialEq`
169/// but not `Eq`. The alternative (flattening the payload to a
170/// `Option<String>`) would force every provider to re-encode structured
171/// arguments as strings, which is the stringly-typed weakness the seam
172/// exists to avoid. Nothing keys a map or set on `AppEffect`; `PartialEq`
173/// is what the round-trip tests use.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub enum AppEffect {
176 /// Graceful editor shutdown. The App's `apply_effect` arm
177 /// publishes `Event::BeforeQuit` and sets the `should_quit`
178 /// flag the runtime polls between frames; matches today's
179 /// `Action::Quit` semantics exactly. (8.i.0 smoke variant.)
180 ///
181 /// CM.3d (2026-07-22) unbound the `<C-c>` → quit hatch that
182 /// used to be hardcoded in `input.rs`; `action:quit` is now
183 /// reachable only through `:q` and user bindings.
184 Quit,
185 /// CG.1: **foreground cancellation**. Flips the token of the
186 /// in-flight user-initiated async op (search scan, LSP command,
187 /// plugin call) and snaps the editor back to a stable Normal
188 /// state. Bound to `<C-c>` at the Builtin layer in every mode,
189 /// and to `<C-g>` by `emacs-keys-mode`.
190 ///
191 /// Idempotent: with no op armed it degrades to the mode reset
192 /// alone, which is why it is safe as a universal binding.
193 /// See `docs/dev/architecture/cancellation.md`.
194 Cancel,
195 /// RF.5b: an operator resolved its range and the buffer's chain for
196 /// that intent says a **non-native** provider owns it.
197 ///
198 /// The operator cannot run one itself — an LSP round-trip and a
199 /// process spawn are both asynchronous, and the grammar layer has
200 /// neither client nor runtime. So it returns the range and the host
201 /// resolves the chain, exactly as `SearchTrigger` hands back a query
202 /// rather than running a search.
203 ///
204 /// Emitted only when the winning rung is not `native`, so the
205 /// default configuration never produces one and the common path is
206 /// unchanged.
207 FormatRange {
208 /// Which kind of formatting the operator asked for (`=` indent,
209 /// `gq` reflow, `g=` reformat); selects the provider chain.
210 intent: lattice_core::FormatIntent,
211 /// Inclusive 0-based line span.
212 start_line: u32,
213 /// Last line of the span (inclusive, 0-based).
214 end_line: u32,
215 },
216 /// Vim's `%`. Jumps to the bracket / brace / paren matching
217 /// the one at-or-after the cursor on the current line.
218 /// Promoted from `Action::MatchBracket` in slice 8.i.1.a.
219 MatchBracket,
220 /// Vim's `~`. Toggles the case of the char at the cursor and
221 /// advances by one byte. Promoted from
222 /// `Action::ToggleCaseAtCursor` in slice 8.i.1.a.
223 ToggleCaseAtCursor,
224 /// Vim's `o`. Opens a new line below the current line and
225 /// enters Insert. Promoted from `Action::OpenLineBelow` in
226 /// slice 8.i.1.a.
227 OpenLineBelow,
228 /// Vim's `O`. Opens a new line above the current line and
229 /// enters Insert. Promoted from `Action::OpenLineAbove` in
230 /// slice 8.i.1.a.
231 OpenLineAbove,
232 // L7: `AppEffect::LspHoverRequest` removed — `K` is mode-owned now
233 // (`lsp-mode`'s `action_handlers()` emits `Effect::Lsp(LspRequest::Hover)`).
234 /// Vim's `n`. Re-runs the last search forward. Promoted from
235 /// `Action::SearchNext` in slice 8.i.1.b.
236 SearchNext,
237 /// Vim's `N`. Re-runs the last search in the reverse
238 /// direction. Promoted from `Action::SearchPrevious` in
239 /// slice 8.i.1.b.
240 SearchPrevious,
241 /// Vim's `<C-o>`. Walk one step backward through the position
242 /// history (DESIGN.md §5.1.1). Promoted from
243 /// `Action::JumpHistoryBack` in slice 8.i.1.b.
244 JumpHistoryBack,
245 /// Vim's `<C-i>` / `<Tab>`. Walk one step forward through the
246 /// position history. Promoted from `Action::JumpHistoryForward`
247 /// in slice 8.i.1.b.
248 JumpHistoryForward,
249 /// PBH.3: `<C-6>`. Walk one step **back** through the *active
250 /// pane's* buffer trail — the buffers this pane has shown.
251 ///
252 /// Distinct from [`Self::JumpHistoryBack`] (`<C-o>`), which walks
253 /// the global position ring at *position* granularity. This one is
254 /// per-pane and moves whole buffers. See
255 /// `docs/dev/architecture/pane-buffer-history.md`.
256 PaneHistoryBack,
257 /// PBH.3: `<C-7>`. Walk one step **forward** through the active
258 /// pane's buffer trail.
259 PaneHistoryForward,
260 /// Vim's `g;`. Walk one step backward through the
261 /// mark history (oldest -> newest cursor positions in this
262 /// buffer). Promoted from `Action::WalkMarkHistoryBack` in
263 /// slice 8.i.1.b.
264 WalkMarkHistoryBack,
265 /// Vim's `g,`. Walk one step forward through the mark
266 /// history. Promoted from `Action::WalkMarkHistoryForward` in
267 /// slice 8.i.1.b.
268 WalkMarkHistoryForward,
269 /// Vim's `<C-t>`. Pop one entry off the tag stack and jump
270 /// back to where the previous `gd` / `<C-]>` originated.
271 /// Promoted from `Action::TagStackPop` in slice 8.i.1.b.
272 TagStackPop,
273 /// Vim's `zo`. Open the fold containing the cursor.
274 /// Promoted from `Action::OpenFoldAtCursor` in slice 8.i.1.c.
275 OpenFoldAtCursor,
276 /// Vim's `zc`. Close the fold containing the cursor.
277 /// Promoted from `Action::CloseFoldAtCursor` in slice 8.i.1.c.
278 CloseFoldAtCursor,
279 /// Vim's `za`. Toggle the fold containing the cursor.
280 /// Promoted from `Action::ToggleFoldAtCursor` in slice 8.i.1.c.
281 ToggleFoldAtCursor,
282 /// Vim's `zR`. Open every fold in the buffer.
283 /// Promoted from `Action::OpenAllFolds` in slice 8.i.1.c.
284 OpenAllFolds,
285 /// Vim's `zM`. Close every fold in the buffer.
286 /// Promoted from `Action::CloseAllFolds` in slice 8.i.1.c.
287 CloseAllFolds,
288 /// org-cycle `z<Space>` / `:fold-cycle`. Cycle the heading/fold under
289 /// the cursor through emacs org-mode's local states
290 /// FOLDED → CHILDREN → SUBTREE.
291 CycleFoldAtCursor,
292 /// org-cycle `z<Tab>` / `:fold-cycle-global`. Cycle the WHOLE buffer
293 /// through OVERVIEW → CONTENTS → SHOW-ALL.
294 CycleFoldsGlobal,
295 /// `zp` / `:fold-goto-parent`. Move the cursor to the parent heading
296 /// (one level up the fold hierarchy) — emacs `outline-up-heading`.
297 GotoParentFold,
298 /// Vim's `zd`. Delete the fold containing the cursor (drop
299 /// it from the manual fold table; structure-driven folds
300 /// reappear on the next reparse). Promoted from
301 /// `Action::DeleteFoldAtCursor` in slice 8.i.1.c.
302 DeleteFoldAtCursor,
303 /// VM.3h: vim's `zO`. Open every fold containing the cursor line and every
304 /// fold nested inside those. In Visual, the same over each selected line.
305 OpenFoldsRecursively,
306 /// VM.3h: vim's `zC`. Close every fold containing the cursor line, and
307 /// nothing nested below the cursor. In Visual, every fold containing a
308 /// selected line, enclosing ones included.
309 CloseFoldsRecursively,
310 /// VM.3h: vim's `zD`. Delete the innermost fold containing the cursor line
311 /// and every fold nested inside it. In Visual, the folds inside the
312 /// selection, not the ones that merely enclose it.
313 DeleteFoldsRecursively,
314 /// Vim's `zj`. Move cursor to the start of the next fold.
315 /// Promoted from `Action::GotoNextFold` in slice 8.i.1.c.
316 GotoNextFold,
317 /// Vim's `zk`. Move cursor to the end of the previous fold.
318 /// Promoted from `Action::GotoPrevFold` in slice 8.i.1.c.
319 GotoPrevFold,
320 /// Vim's `zi`. Toggle the `foldenable` option (when off,
321 /// every line renders flat regardless of any closed flag).
322 /// Promoted from `Action::ToggleFoldEnable` in slice 8.i.1.c.
323 ToggleFoldEnable,
324 /// Vim's `u`. Undo the last buffer change. Promoted from
325 /// `Action::Undo` in slice 8.i.1.d.
326 Undo,
327 /// Vim's `<C-r>`. Redo the last undone change. Promoted from
328 /// `Action::Redo` in slice 8.i.1.d.
329 Redo,
330 /// Vim's `.`. Repeat the last change (operator + motion +
331 /// register + count). Promoted from `Action::RepeatLastChange`
332 /// in slice 8.i.1.d.
333 RepeatLastChange,
334 /// Vim's `<C-f>`. Page-down: scroll the viewport down one
335 /// page. Promoted from `Action::PageDown` in slice 8.i.1.d.
336 PageDown,
337 /// VM.3j-2: vim's `<C-d>` / `<C-u>` — scroll the view and the cursor
338 /// together by `scroll` lines (half the window by default). SCROLL
339 /// commands, not motions: vim composes no operator with them.
340 HalfPageDown,
341 /// Vim's `<C-u>`; the upward twin of [`Self::HalfPageDown`].
342 HalfPageUp,
343 /// Vim's `<C-b>`. Page-up: scroll the viewport up one page.
344 /// Promoted from `Action::PageUp` in slice 8.i.1.d.
345 PageUp,
346 /// Vim's `<C-y>`. Scroll viewport up one line (cursor
347 /// stays at the same screen position when possible).
348 /// Promoted from `Action::ScrollLineUp` in slice 8.i.1.d.
349 ScrollLineUp,
350 /// Vim's `<C-e>`. Scroll viewport down one line. Promoted
351 /// from `Action::ScrollLineDown` in slice 8.i.1.d.
352 ScrollLineDown,
353 /// Vim's `<C-l>`. Force a full screen redraw. Promoted from
354 /// `Action::RedrawScreen` in slice 8.i.1.e.
355 RedrawScreen,
356 /// Vim's `:` / Emacs' `M-x`. Open the command picker over
357 /// all registered ex-commands. If the chosen command has a
358 /// required first argument the picker arms the cmdline so
359 /// the user can supply it; otherwise executes immediately.
360 OpenCommandPicker,
361 /// MB.3: Vim's `q:`. Open the command-line *history* picker
362 /// over `command_history`. `<CR>` loads the chosen command
363 /// into the `:` line WITHOUT executing (the user tweaks /
364 /// `<C-x><C-e>` expands, then `<CR>`s). Fired from an ordinary
365 /// buffer's Normal mode or the expanded tier-2 band's Normal
366 /// mode (where it seeds the picker filter with the in-progress
367 /// command-line text).
368 OpenHistoryPicker,
369 /// MB.5: `q/` / `q?` — open the search-line history picker.
370 /// Accept loads the chosen term into the `/` search line.
371 OpenSearchHistoryPicker,
372 /// Vim's `:`. Enter the command-line minibuffer. Promoted
373 /// from `Action::EnterCommandLine` in slice 8.i.1.e.
374 EnterCommandLine,
375 /// MB.1: `<CR>` in `command-line-mode` — submit (or accept the
376 /// open completion candidate). Drives the rewired
377 /// `Editor::do_command_line_submit`.
378 CommandLineSubmit,
379 /// MB.1: `<Esc>` / `<C-c>` in `command-line-mode` — cancel (or
380 /// dismiss the open completion popup first).
381 CommandLineCancel,
382 /// MB.1: `<C-p>` / `<Up>` in `command-line-mode` — walk history
383 /// backward (or previous completion candidate when the popup is open).
384 CommandLineHistoryPrev,
385 /// MB.1: `<C-n>` / `<Down>` in `command-line-mode` — walk history
386 /// forward (or next completion candidate when the popup is open).
387 CommandLineHistoryNext,
388 /// MB.1: `<Tab>` in `command-line-mode` — open the completion popup
389 /// or advance the selection.
390 CommandLineComplete,
391 /// MB.1: `<S-Tab>` in `command-line-mode` — previous completion
392 /// candidate.
393 CommandLineCompletePrev,
394 /// MB.1: `<C-h>` in `command-line-mode` — describe the command / arg
395 /// under the cursor.
396 CommandLineDescribeUnderCursor,
397 /// MB.2: `<C-x><C-e>` in `command-line-mode` — toggle the `:` line's
398 /// **expanded** tier-2 mini-buffer band (full modal editing in place),
399 /// or collapse it back to the one-row readline line for review.
400 CommandLineToggleExpand,
401 /// MB.5a: `<CR>` on the `/`·`?` search line — submit the search
402 /// pattern. Resolved from `search-line-mode`'s Insert keymap.
403 SearchLineSubmit,
404 /// MB.5a: `<Esc>` / `<C-c>` on the `/`·`?` search line — cancel
405 /// the search and restore the prior editing buffer.
406 SearchLineCancel,
407 /// `<BS>` on the `/`·`?` line: delete a char, or cancel when empty.
408 SearchLineBackspace,
409 /// MB.5b: `<C-p>` / `<Up>` on the `/`·`?` search line — walk to an
410 /// older entry in `search_history`.
411 SearchLineHistoryPrev,
412 /// MB.5b: `<C-n>` / `<Down>` on the `/`·`?` search line — walk to a
413 /// newer entry in `search_history`.
414 SearchLineHistoryNext,
415 /// MB.5c: `<C-x><C-e>` on the `/`·`?` search line — toggle the
416 /// expanded tier-2 mini-buffer band.
417 SearchLineToggleExpand,
418 /// `<CR>` in `prompt-line-mode` (`Effect::OpenPrompt`'s generic
419 /// one-line text prompt) — submit the typed text to whichever
420 /// `action:*` handler the caller named as `on_submit_action`.
421 /// Drives `Editor::do_prompt_line_submit`.
422 PromptLineSubmit,
423 /// `<Esc>` / `<C-c>` in `prompt-line-mode` — cancel without
424 /// firing anything, restore the prior editing buffer.
425 PromptLineCancel,
426 /// Lattice's `-`. Open / step up in the oil-style directory
427 /// view (DESIGN.md §5.9.4). Promoted from
428 /// `Action::OilNavigateUp` in slice 8.i.1.e.
429 OilNavigateUp,
430 /// Vim's `gv`. Reselect the last Visual selection (same
431 /// kind, anchor, head). Promoted from
432 /// `Action::ReselectLastVisual` in slice 8.i.1.e.
433 ReselectLastVisual,
434 /// Vim's `o` in Visual mode -- swap the cursor (head) to the
435 /// other end of the selection (and back). Anchor and head trade
436 /// places so subsequent motions / text objects grow or shrink the
437 /// selection at the end the cursor now sits on.
438 SwapVisualEnds,
439 /// Vim's `p`. Paste the unnamed register's contents after
440 /// the cursor. Promoted from `Action::PasteAfter` in slice
441 /// 8.i.1.e.
442 PasteAfter,
443 /// Vim's `P`. Paste the unnamed register's contents before
444 /// the cursor. Promoted from `Action::PasteBefore` in slice
445 /// 8.i.1.e.
446 PasteBefore,
447 // L7: the 6 nav `AppEffect::Lsp*Request` variants (`gd` / `gD` / `gy`
448 // / `gI` / `gr` / `gx`) removed — they are mode-owned now. `lsp-mode`'s
449 // `action_handlers()` closures emit `Effect::Lsp(LspRequest::{Definition,
450 // Declaration, TypeDefinition, Implementation, References, FollowLink})`,
451 // dispatched host-side by `editor.lsp_request`.
452 /// Vim's `a`. Move cursor one byte right (clamped) and enter
453 /// Insert. Promoted from `Action::EnterAppend` in slice
454 /// 8.i.1.g.
455 EnterAppend,
456 /// Vim's `I`: move cursor to first non-blank column of the
457 /// current line and enter Insert.
458 EnterInsertFirstNonBlank,
459 /// Vim's `A`: move cursor to end of the current line and enter
460 /// Insert.
461 EnterAppendEndOfLine,
462 /// Vim's `gj`: move down one display line (wrap segment).
463 /// Degrades to `j` when wrapping is off.
464 DisplayLineDown,
465 /// Vim's `gk`: move up one display line (wrap segment).
466 /// Degrades to `k` when wrapping is off.
467 DisplayLineUp,
468 /// Vim's `g0`: move to the first byte of the current display segment.
469 /// Degrades to `0` when wrapping is off.
470 DisplayLineStart,
471 /// Vim's `g$`: move to the last byte of the current display segment.
472 /// Degrades to `$` when wrapping is off.
473 DisplayLineEnd,
474 /// Vim's `zf`. Create a fold from the most recent Visual
475 /// selection. Promoted from `Action::CreateFoldFromVisual`
476 /// in slice 8.i.1.g.
477 CreateFoldFromVisual,
478 /// VM.3h: vim's `zf` operator (`zf{motion}`, `{Visual}zf`). A CLOSED fold
479 /// over the pre-resolved inclusive 0-based lines. Carries the span, so it
480 /// crosses WIT like `NarrowLines`.
481 CreateFold {
482 /// First line of the fold (0-based, inclusive).
483 start_line: u32,
484 /// Last line of the fold (0-based, inclusive).
485 end_line: u32,
486 },
487 /// Insert mode's `<BS>`. Delete the byte before the cursor.
488 /// Promoted from `Action::DeleteCharBackward` in slice
489 /// 8.i.1.g.
490 DeleteCharBackward,
491 /// Insert-mode line editing — the readline/vim chords (`<C-a>`, `<C-e>`,
492 /// `<C-w>`, `<C-u>`, `<C-k>`, `<C-t>`, `<C-d>`, …) available in every
493 /// buffer. One grouped effect keyed by [`InsertLineEdit`] so the whole
494 /// family shares a single host handler.
495 InsertLineEdit(InsertLineEdit),
496 /// Insert mode's `<C-Space>` and `<C-x><C-o>`. Trigger the
497 /// completion popup (omni-completion alias). Promoted from
498 /// `Action::CompletionTrigger` in slice 8.i.1.g. The
499 /// completion-popup minor-mode layer's `<C-Space>` binding
500 /// keeps its legacy `bind_action` registration until that
501 /// helper picks up `CommandInvocation` (separate scope).
502 CompletionTrigger,
503 // SN.3c.1 (2026-06-14): `AppEffect::SnippetExpand` removed.
504 // `<C-x><C-s>` is now mode-owned (`snippet-mode`'s `keymap()` +
505 // `action_handlers()`): the handler scans the word prefix and
506 // emits `Effect::ExpandSnippet { replace_range }`, which the host
507 // resolves + expands. No host `Action` / `AppEffect` round-trip.
508 /// Visual mode's `<Esc>` / `v` / `V`. Exit Visual to Normal,
509 /// collapsing the selection. Promoted from `Action::ExitVisual`
510 /// in slice 8.i.1.h.
511 ExitVisual,
512 /// Replace mode's `<BS>`. Undo the last overwritten char.
513 /// Promoted from `Action::ReplaceUndoLast` in slice 8.i.1.h.
514 ReplaceUndoLast,
515 /// Vim's `i` / `R` / `<Esc>` (from Insert / Replace) -- enter
516 /// the named modal state. Promoted from
517 /// `Action::EnterMode(_)` in slice 8.i.2.a. Each chord in the
518 /// keymap binds a *distinct* `CommandId` whose `ActionSpec`
519 /// returns the right `AppEffect::EnterMode(state)` constant
520 /// -- the `ModalState` rides in the AppEffect rather than in
521 /// `CommandInvocation::args` so the App's `apply_app_effect`
522 /// matches a single `EnterMode(state)` arm instead of N
523 /// param-flat variants.
524 EnterMode(ModalState),
525 /// Vim's `v` / `V` / `<C-v>` -- enter Visual with the named
526 /// kind anchored at the current cursor. Promoted from
527 /// `Action::EnterVisual(_)` in slice 8.i.2.a. Same encoding
528 /// as [`Self::EnterMode`]: distinct `CommandId` per kind,
529 /// payload rides in the AppEffect.
530 EnterVisual(VisualKind),
531 /// Vim's `gh` / `gH` / `g<C-h>` -- enter Select mode (SN.3d) with
532 /// the named kind, anchored at the current cursor. Same encoding as
533 /// [`Self::EnterVisual`]: distinct `CommandId` per kind, payload
534 /// rides in the AppEffect. The host handler (`do_enter_select`)
535 /// anchors a zero-width selection like `do_enter_visual` does —
536 /// typing then overtypes it. Programmatic entry (snippets) instead
537 /// uses `EnterMode(Select(k))` with an explicit selection already
538 /// set; see `docs/dev/architecture/select-mode.md` §3.
539 EnterSelect(VisualKind),
540 /// Vim's `/` / `?` -- enter the Search minibuffer in the
541 /// named direction. Promoted from `Action::EnterSearch(_)`
542 /// in slice 8.i.2.b. Same encoding as [`Self::EnterMode`]:
543 /// distinct `CommandId` per direction.
544 EnterSearch(SearchDirection),
545 /// Vim's `*` / `#` -- search for the word under the cursor
546 /// in the named direction. Promoted from
547 /// `Action::SearchWordUnderCursor(_)` in slice 8.i.2.b.
548 SearchWordUnderCursor(SearchDirection),
549 /// Vim's `H` / `M` / `L` -- jump cursor to the named
550 /// position within the visible viewport. Promoted from
551 /// `Action::JumpViewport(_)` in slice 8.i.2.c.
552 JumpViewport(ViewportPos),
553 /// Vim's `zz` / `zt` / `zb` -- scroll the viewport so the
554 /// cursor's current line sits at the named position.
555 /// Promoted from `Action::ScrollCursorTo(_)` in slice 8.i.2.c.
556 ScrollCursorTo(ScrollPos),
557 /// HS.2: vim `z{l,h,L,H,s,e}` manual horizontal scroll.
558 HorizontalScroll(HScroll),
559 /// Vim's `J` (with-space) / `gJ` (no-space). Joins the
560 /// current line with the next, replacing the joining
561 /// newline with a single space (`with_space: true`) or
562 /// nothing (`false`). Promoted from `Action::JoinLines` in
563 /// slice 8.i.2.d. Bool payload rides in the AppEffect:
564 /// distinct `CommandId` per binding (`J` -> with-space=true,
565 /// `gJ` -> with-space=false).
566 JoinLines {
567 /// `true` for `J` (joined with a space), `false` for `gJ` (no
568 /// space inserted).
569 with_space: bool,
570 },
571 /// Vim's `;` (forward) / `,` (reverse). Repeat the most
572 /// recent `f` / `F` / `t` / `T` find on the current line
573 /// in the originally-typed direction (`reverse: false`) or
574 /// the opposite direction (`reverse: true`). Promoted from
575 /// `Action::FindRepeat` in slice 8.i.2.d.
576 FindRepeat {
577 /// `false` for `;` (same direction as the original find), `true`
578 /// for `,` (opposite direction).
579 reverse: bool,
580 },
581 /// Insert / Replace mode's `<CR>`. Inserts a literal newline
582 /// at the cursor. Promoted from
583 /// `Action::Insert("\n".into())` in slice 8.i.2.e. Distinct
584 /// flat variant rather than a `String`-payload `Insert(_)`
585 /// because the keymap-bound forms always pin a fixed
586 /// literal; the wildcard "type any printable char" path
587 /// stays on `Action::Insert(c.to_string())` since it isn't
588 /// keymap-bound.
589 InsertNewline,
590 /// Insert mode's `<Tab>`. Inserts a literal tab at the
591 /// cursor. Promoted from `Action::Insert("\t".into())` in
592 /// slice 8.i.2.e.
593 InsertTab,
594 /// Replace mode's bare-printable wildcard. Overwrites the
595 /// byte at the cursor with the captured char. Promoted from
596 /// `Action::OverwriteChar(c)` in slice 8.i.3.
597 OverwriteChar(char),
598 /// Vim's `m<X>`. Sets mark `<X>` at the cursor's current
599 /// position. Promoted from `Action::SetMark(c)` in slice
600 /// 8.i.3. The bound `ActionSpec` validates that `<X>` is
601 /// `[a-zA-Z0-9]` -- invalid chars dispatch to `Effect::None`
602 /// (effectively a no-op; `App::apply` clears the pending
603 /// state on every Invoke).
604 SetMark(char),
605 /// Vim's `'<X>`. Jumps the cursor to the line of mark `<X>`.
606 /// Promoted from `Action::JumpToMarkLine(c)` in slice 8.i.3.
607 JumpToMarkLine(char),
608 /// Vim's `` `<X> ``. Jumps the cursor to the exact position
609 /// (line + byte) of mark `<X>`. Promoted from
610 /// `Action::JumpToMarkExact(c)` in slice 8.i.3.
611 JumpToMarkExact(char),
612 /// Vim's `"<X>`. Selects the named register for the next
613 /// yank / paste / delete. Promoted from
614 /// `Action::SelectRegister(_)` in slice 8.i.3. The bound
615 /// `ActionSpec` validates the captured char via
616 /// [`Register::from_input_char`]; chars that don't name a
617 /// register dispatch to `Effect::None`.
618 SelectRegister(Register),
619 /// Vim's `q<X>`. Starts recording a macro into register
620 /// `<X>`. Promoted from `Action::StartMacroRecord(c)` in
621 /// slice 8.i.3.
622 StartMacroRecord(char),
623 /// Vim's `@<X>` for `<X>` alphanumeric. Plays the macro
624 /// stored in register `<X>`. Promoted from
625 /// `Action::PlayMacro(c)` in slice 8.i.3. The `@@` chord is
626 /// dispatched to [`Self::PlayLastMacro`] from the same
627 /// `play-macro` action (the spec branches on the captured
628 /// char).
629 PlayMacro(char),
630 /// Vim's `@@`. Replays the most recently played macro.
631 /// Promoted from `Action::PlayLastMacro` in slice 8.i.3.
632 /// Shares its bind site (`@<CharLiteral>`) with
633 /// [`Self::PlayMacro`]; the `play-macro` action's apply
634 /// closure picks one or the other based on the captured
635 /// char.
636 PlayLastMacro,
637 /// Slice 8.i.4.c: arm an operator-pending state via the
638 /// `partial_chord` mechanism. The App handler does two
639 /// things atomically:
640 ///
641 /// 1. Latch `pending_count` into `op_count` (vim's
642 /// `<count>op<motion>` count multiplication; without
643 /// this `2dd` would get a count of 1).
644 /// 2. Push the operator's chord prefix into
645 /// `App::partial_chord` so the next keystroke routes
646 /// through `lookup_normal_with_prefix` and resolves
647 /// `[op, motion]` / `[op, i/a, text-object]` /
648 /// `[op, f/F/t/T, char]` to the bound `Invoke`.
649 ///
650 /// Replaces the `Action::SetPending(Pending::AfterOperator(_))`
651 /// flow that did the same two things split across the
652 /// keymap (which fired `SetPending`) and `App::apply`
653 /// (which latched `op_count` inside the SetPending arm).
654 /// The 8 operator prefixes -- `d`, `c`, `y`, `>`, `<`,
655 /// `gU`, `gu`, `g~` -- bind to typed actions whose
656 /// `ApplySpec` returns this variant.
657 AbsorbOperatorPrefix(OperatorId),
658 /// Vim's `<C-w>s`: split the active pane horizontally.
659 /// Promoted from `Action::SplitPaneHorizontal` in slice 8.i.4.d.
660 SplitPaneHorizontal,
661 /// Vim's `<C-w>v`: split the active pane vertically.
662 /// Promoted from `Action::SplitPaneVertical` in slice 8.i.4.d.
663 SplitPaneVertical,
664 /// Vim's `<C-w>c` / `<C-w>q`: close the active pane.
665 /// Promoted from `Action::ClosePane` in slice 8.i.4.d.
666 ClosePane,
667 /// Vim's `<C-w>o` / `:only` / emacs `C-x 1`: close every pane
668 /// except the active one (collapse the tree to the active leaf).
669 /// No-op when only one pane is open. S3b (2026-06-22).
670 OnlyPane,
671 /// ZP.2: `<C-w>z` / `<C-w><C-z>` / `:zoom-pane` — toggle
672 /// tmux-style zoom on the active pane. Non-destructive, unlike
673 /// [`Self::OnlyPane`]: the split layout is preserved and the
674 /// second toggle restores it verbatim. No-op on a single-pane
675 /// tab. See `docs/dev/architecture/pane-zoom.md`.
676 ToggleZoomPane,
677 /// Vim's `<C-w>h/j/k/l` (and arrow / `<BS>` aliases): move
678 /// focus to the pane in the named direction. Promoted from
679 /// `Action::NavigatePane(_)` in slice 8.i.4.d.
680 NavigatePane(PaneDirection),
681 /// Vim's `<C-w>w` / `<C-w><Tab>`: cycle focus to the next
682 /// pane. Promoted from `Action::NextPane` in slice 8.i.4.d.
683 NextPane,
684 /// Vim's `<C-w>W` / `<C-w><S-Tab>`: cycle focus to the
685 /// previous pane. Promoted from `Action::PrevPane` in
686 /// slice 8.i.4.d.
687 PrevPane,
688 /// Issue #29 (2026-05-22): vim's `gt` — next tab.
689 NextTab,
690 /// Vim's `gT` — previous tab.
691 PrevTab,
692 /// Vim's `{N}gt` — switch to tab N (1-indexed; clamped).
693 GoToTab(u32),
694 /// `:tabnew` — new empty tab.
695 NewTab,
696 /// `:tabnew <path>` — new tab opening `path`.
697 NewTabAt(String),
698 /// Issue #40 / Terminal-mode T1 (2026-05-22):
699 /// `:terminal [cmd]` — spawn a PTY-backed shell.
700 TerminalSpawn(Option<String>),
701 /// T4 (2026-05-25): `:tabterminal [cmd]` — open a fresh
702 /// tab and spawn a PTY-backed shell in it. Handler does
703 /// `do_new_tab` then `do_terminal_spawn(cmd)`.
704 TerminalSpawnInNewTab(Option<String>),
705 /// T4 (2026-05-25): `<C-w>T` — move the active pane to a
706 /// fresh tab. Handler in `Editor::do_move_pane_to_new_tab`.
707 MovePaneToNewTab,
708 /// `:tabclose` — close active tab.
709 CloseTab,
710 /// `:tabonly` — close every tab except the active one.
711 OnlyTab,
712 /// `:tabmove [N]` — move active tab to position N (1-indexed).
713 MoveTab(u32),
714 /// Issue #32 (2026-05-22): picker open-target overrides.
715 /// `<C-s>` — accept selected candidate in horizontal split.
716 PickerAcceptInSplit,
717 /// `<C-v>` — accept selected candidate in vertical split.
718 PickerAcceptInVSplit,
719 /// `<C-t>` — accept selected candidate in new tab.
720 PickerAcceptInTab,
721 /// Issue #28 (2026-05-22): `<C-w>=` — reset every split's
722 /// ratio to 0.5.
723 EqualizePanes,
724 /// `<C-w>+` — grow the active pane vertically.
725 GrowPaneHeight,
726 /// `<C-w>-` — shrink the active pane vertically.
727 ShrinkPaneHeight,
728 /// `<C-w>>` — grow the active pane horizontally.
729 GrowPaneWidth,
730 /// `<C-w><` — shrink the active pane horizontally.
731 ShrinkPaneWidth,
732 /// Completion-popup overlay: focus the next entry. Promoted
733 /// from `Action::CompletionNext` in slice 8.i.4.e.
734 CompletionNext,
735 /// Completion-popup overlay: focus the previous entry.
736 /// Promoted from `Action::CompletionPrev` in slice 8.i.4.e.
737 CompletionPrev,
738 /// Completion-popup overlay: accept the focused candidate.
739 /// Promoted from `Action::CompletionAccept` in slice
740 /// 8.i.4.e.
741 CompletionAccept,
742 /// Completion-popup overlay: cancel the popup, stay in
743 /// Insert. Promoted from `Action::CompletionCancel` in
744 /// slice 8.i.4.e.
745 CompletionCancel,
746 /// Completion-popup overlay: cancel the popup and exit
747 /// Insert. Promoted from
748 /// `Action::CompletionCancelAndExitInsert` in slice 8.i.4.e.
749 CompletionCancelAndExitInsert,
750 /// Completion-popup overlay: toggle the doc-popup
751 /// (`<C-d>`). Promoted from `Action::CompletionToggleDocs`
752 /// in slice 8.i.4.e.
753 CompletionToggleDocs,
754 /// Completion-popup overlay: scroll the doc-popup down
755 /// (`<C-f>`). Promoted from
756 /// `Action::CompletionDocsScrollDown` in slice 8.i.4.e.
757 CompletionDocsScrollDown,
758 /// Completion-popup overlay: scroll the doc-popup up
759 /// (`<C-b>`). Promoted from
760 /// `Action::CompletionDocsScrollUp` in slice 8.i.4.e.
761 CompletionDocsScrollUp,
762 /// Completion-popup overlay: bare-printable wildcard.
763 /// Accept the focused candidate, then insert the captured
764 /// char (so the user can finish typing through a
765 /// confirmed prefix). Promoted from
766 /// `Action::CompletionAcceptThenInsert(c)` in slice
767 /// 8.i.4.e.
768 CompletionAcceptThenInsert(char),
769 /// YR.5: vim's insert-register — `<C-r>` then a register char,
770 /// inserting that register's contents at the cursor.
771 InsertRegister(char),
772 /// YR.5: `<C-r><C-r>` — open the yank-ring picker, filling whichever
773 /// surface it was opened from.
774 OpenYankPicker,
775 /// YR.6: open the picker registered for the `:`-line argument under
776 /// the cursor (`ArgSpec.picker`), filling the pick back into it.
777 OpenArgPicker,
778 /// Active-snippet overlay: jump to the next placeholder
779 /// (`<Tab>`). Promoted from
780 /// `Action::SnippetNextPlaceholder` in slice 8.i.4.e.
781 SnippetNextPlaceholder,
782 /// Active-snippet overlay: jump to the previous placeholder
783 /// (`<S-Tab>`). Promoted from
784 /// `Action::SnippetPrevPlaceholder` in slice 8.i.4.e.
785 SnippetPrevPlaceholder,
786 // SN.3c.2 (2026-06-14): `AppEffect::SnippetLeave` removed.
787 // `<Esc>` is mode-owned now (`active-snippet-mode`'s
788 // `keymap()` binds it + a per-buffer closure in `on_activate`
789 // clears the session + returns `Effect::EnterMode(Normal)`);
790 // no host `Action` / `AppEffect` round-trip. (Unlike the nav
791 // placeholders, which keep their `register_simple`-produced
792 // AppEffect variants as no-ops, leave switched to
793 // `register_action`, so this variant had no producer left.)
794 /// Completion-popup overlay: restrict candidates to a single
795 /// source. The string is the `SourceId` (e.g.
796 /// `"gen:buffer-words"`, `"gen:lsp-completion"`). Bound to
797 /// the popup-mode filter chords (`<C-b>`, `<C-o>`, `<C-f>`,
798 /// `<C-t>`, ...) introduced in CSM.K2.
799 CompletionFilterToSource(String),
800 /// Completion-popup overlay: clear the active source filter
801 /// (`<C-Space>`). Restores the mixed merged candidate list.
802 CompletionFilterClear,
803 /// D.5.b (2026-05-30): diff-mode `do` (diff-get) operator.
804 /// CR.1 (2026-06-24): `do` is now mode-owned
805 /// (`DiffMode::action_handlers()` → `Effect::ApplyEdit`); this
806 /// `AppEffect` is retained only as the FALLBACK the `action:diff-get`
807 /// CommandSpec emits when no handler is registered. The host's
808 /// `apply_app_effect` arm is emptied to a silent no-op (the
809 /// `Action::DiffGet` variant it used to push is deleted).
810 DiffGet,
811 /// D.5.c (2026-05-30): diff-mode `dp` (diff-put) operator.
812 /// CR.1: mirror of [`Self::DiffGet`] — mode-owned now; retained as
813 /// the emptied `action:diff-put` fallback shell.
814 DiffPut,
815 /// T.3: tutor-mode `<CR>` / `:tutor-next`. Advance to the
816 /// next exercise; advance to the next lesson when the
817 /// current one is complete. No-op when tutor-mode is not
818 /// active on the current buffer.
819 TutorAdvance,
820 /// T.3: tutor-mode `:tutor-prev`. Retreat to the previous
821 /// exercise. No-op at exercise 0.
822 TutorRetreat,
823 /// M.5 (2026-06-01): `:multibuffer-expand [n]` /
824 /// `:multibuffer-contract [n]` ex-commands. `delta` is the
825 /// signed row count (positive expands, negative contracts).
826 /// Routed to the active multibuffer view's `expand_excerpt_at`
827 /// from the dispatch path. No-op when the active buffer
828 /// isn't a multibuffer.
829 MultibufferExpand {
830 /// Context lines to add (positive) or remove (negative) around the
831 /// excerpt under the cursor.
832 delta: i32,
833 },
834 /// N.1.1 (2026-06-10): `:narrow [{range}]` ex-command. The host
835 /// arm resolves `range` against the active document to a line
836 /// span, fetches the active buffer's `Arc<dyn Document>`, and
837 /// calls `lattice_multibuffer::providers::narrow::create_narrow_view`
838 /// — opening a one-excerpt multibuffer focused on that region.
839 /// `range == None` (bare `:narrow`) narrows the current paragraph
840 /// (blank-line delimited, vim's `ip`).
841 NarrowTrigger {
842 /// The unresolved `:` range, resolved by the host against cursor,
843 /// last Visual selection and marks (see [`crate::Range`] for which
844 /// forms resolve); `None` = current paragraph.
845 range: Option<crate::range::Range>,
846 },
847 /// N.1.1 (2026-06-10): `:widen` ex-command. The host arm closes
848 /// the active narrow view (an editable one-excerpt multibuffer),
849 /// restoring the full source buffer. No-op + echo when the
850 /// active buffer isn't a narrow view.
851 NarrowWiden,
852 /// N.1.3 (2026-06-10): the `zn` narrow operator emits this once
853 /// the operator-pending machinery has resolved its motion / text
854 /// object to a line span. Carries pre-resolved inclusive 0-based
855 /// `[start_line, end_line]` (unlike `NarrowTrigger`, which carries
856 /// an unresolved `Range`); the host arm narrows the active buffer
857 /// to that span via the same `create_narrow_view` sink.
858 NarrowLines {
859 /// First line of the region (0-based, inclusive).
860 start_line: u32,
861 /// Last line of the region (0-based, inclusive).
862 end_line: u32,
863 },
864 /// M.6 (2026-06-01): `:search <query>` ex-command. M.10.6
865 /// (2026-06-03) inlined the work into the host's
866 /// apply_effect arm — it calls
867 /// `lattice_multibuffer::providers::search::project_search`
868 /// against the active editor as the activator. No longer
869 /// trampolines through `Action::SearchTrigger` /
870 /// `Editor::do_search` (both deleted).
871 SearchTrigger {
872 /// The search query, passed to the project-search provider as
873 /// typed.
874 query: String,
875 },
876 /// M.6.1 (2026-06-01): `gr` chord in project-search-mode.
877 /// Re-runs the scan with the view's current query. M.10.5
878 /// (2026-06-03) made this mode-owned: the search mode's
879 /// `on_activate` registers a closure that intercepts via
880 /// `ActionHandlerRegistry` before this AppEffect arm runs.
881 /// The arm is a no-op marker; the work happens in the
882 /// mode's closure (reading state, clearing excerpts,
883 /// spawning a fresh scan task).
884 SearchRefresh,
885 /// CM.1 (2026-07-21): `:compile <cmd>` / `:recompile` / `:make`.
886 /// The host arm creates the read-only synthetic `*compilation*`
887 /// buffer host-side (`Editor::ensure_named_synthetic_document`,
888 /// activating `compilation-mode`) and kicks off the pipe-captured
889 /// off-thread run via the `CompilationServiceHandle`, whose output
890 /// streams into that buffer. Same
891 /// shape as `SearchTrigger`: the substrate crate owns the work;
892 /// the host arm is generic apply-effect routing. `cmdline`:
893 /// `Some(cmd)` for `:compile`; `None` for `:recompile` / bare
894 /// `:make` (reuse the last command).
895 CompileRun {
896 /// Shell command line to run; `None` re-runs the last one.
897 cmdline: Option<String>,
898 },
899 /// CM.3b (2026-07-22): `<CR>` on a location line in the
900 /// `*compilation*` buffer. The `compilation-mode` action handler
901 /// parses the cursor line's text (`parse_location_line`) into a
902 /// source location and emits this; the host arm calls
903 /// `Editor::jump_to_file_line_col(&path, line, col)` (records the
904 /// hop in position history) and syncs the error list index to the
905 /// matching entry. `line` / `col` are 0-based. The location rides
906 /// in the AppEffect because the jump target is computed off the
907 /// buffer text in the mode's closure, but the error list index is
908 /// core/host state — so the host owns the apply.
909 CompileJumpToLocation {
910 /// File to open (activated if already open).
911 path: std::path::PathBuf,
912 /// 0-based line; clamped to the file's last line.
913 line: u32,
914 /// 0-based **byte** column; clamped to the line's length.
915 col: u32,
916 },
917 /// CM.2 (2026-07-22): `:cnext`/`:cprev`/`:cc [N]`/`:cfirst`/
918 /// `:clast` and the Builtin `]q`/`[q` chords. The host arm calls
919 /// `Editor::do_error_nav`, which walks the core error list
920 /// (recording each hop in position history via
921 /// `jump_to_file_line_col`). On an empty list every target echoes
922 /// "no error list" and moves nothing -- there is no fallback to
923 /// diagnostic hopping (`]d` / `[d` own that).
924 ErrorNav {
925 /// Which entry to jump to.
926 target: ErrorTarget,
927 },
928 /// CM.3a (2026-07-22): parsed error entries from the compilation
929 /// stderr reader — the off-thread → host-state seam. The reader
930 /// accumulates entries and sends the FULL list through the
931 /// compilation inbound bus; this handler maps each send here, and
932 /// the host arm calls `Editor::set_error_list(entries)`
933 /// (replace-semantics — the growing list stays visible). An empty
934 /// vec (sent on a new run / `:recompile`) clears the stale list.
935 /// The parser (below-host `lattice-compilation`) and this payload
936 /// share the `lattice_protocol::error_list::ErrorEntry` type.
937 SetErrorList {
938 /// EP.1 (2026-08-10): which producer this run came from. The
939 /// host arm replaces only this source's slice, so a language
940 /// server republishing on edit-debounce cannot wipe a compile
941 /// run's entries while the user walks them.
942 source: lattice_protocol::error_list::ErrorSource,
943 /// EP.2: new run (reset the index) vs live refresh (re-anchor
944 /// it). Declared by the producer — not inferrable from the
945 /// entries.
946 write: lattice_protocol::error_list::ErrorWrite,
947 /// The source's complete entry list (replace, not append). Empty
948 /// clears this source's slice.
949 entries: Vec<lattice_protocol::error_list::ErrorEntry>,
950 },
951 /// CM.3c (2026-07-22): the per-buffer severity gutter index for the
952 /// `*compilation*` buffer — the off-thread → host-state seam for
953 /// in-buffer severity marks (twin of `SetErrorList`, which feeds the
954 /// cross-file error list). The compilation drain scans each
955 /// streamed line for a severity keyword, accumulates the FULL
956 /// per-buffer index of `(line, severity)`, and sends it through the
957 /// compilation inbound bus; this handler maps each send here, and the
958 /// host arm converts the severities to `GutterSeverityLevel` and writes
959 /// the `render_state` compilation-severity slot for `buffer`. The
960 /// renderer reads that slot and injects it into the mode's
961 /// `gutter_decorations` via `CompilationSeverityData`. An empty vec
962 /// (sent on `Reset` / a new run) clears the buffer's marks.
963 ///
964 /// `buffer` is the raw `BufferId.0` (`BufferId` is process-local and
965 /// not `Serialize`, so the wire form is the `u32`; the host arm
966 /// reconstructs `BufferId`). Severity rides as the parser-native
967 /// `ErrorSeverity` (already shared with `SetErrorList`) — the single
968 /// map to the renderer-facing `GutterSeverityLevel` happens host-side,
969 /// avoiding a lossy `GutterSeverityLevel`↔`ErrorSeverity` round-trip
970 /// and keeping `lattice-grammar` free of a `lattice-mode` dependency.
971 CompilationGutterSet {
972 /// `BufferId.0` of the compilation buffer.
973 buffer: u32,
974 /// `(0-based line, severity)` for every marked line; the full
975 /// index, replacing the previous one.
976 entries: Vec<(u32, lattice_protocol::error_list::ErrorSeverity)>,
977 },
978 /// CM.3c (2026-07-22): per-buffer compilation location-line
979 /// index for theme-based highlighting of navigable lines in
980 /// the `*compilation*` buffer. Twin of `CompilationGutterSet`:
981 /// the off-thread compilation drain scans each chunk for
982 /// location-bearing lines (via `parse_location_line`) and
983 /// ships the full list through an inbound bus; this effect
984 /// writes the `render_state` compilation-location slot for
985 /// `buffer`. An empty vec (sent on `Reset` / a new run)
986 /// clears the buffer's location-line set.
987 CompilationLocationLines {
988 /// `BufferId.0` of the compilation buffer.
989 buffer: u32,
990 /// (line, path_byte_start, path_byte_end) for each location line.
991 /// byte_start/end are the byte offsets of the file-path portion
992 /// within the line text, for link-like fg highlighting.
993 lines: Vec<(u32, u32, u32)>,
994 },
995 /// CM.3d (2026-07-22): resolved compilation location theme colours
996 /// — published by the mode during activation so the renderer
997 /// reads `compilation.location` bg/fg from the theme rather than
998 /// hardcoding RGB values.
999 ///
1000 /// Stored editor-wide (not per buffer); the latest send wins.
1001 CompilationThemeColors {
1002 /// Background of a location line, packed `0xRRGGBB`.
1003 bg: u32,
1004 /// Foreground of a location line's path, packed `0xRRGGBB`.
1005 fg: u32,
1006 },
1007 /// CM.3d (2026-07-22): kill the running compilation child
1008 /// process. The host arm calls `CompilationService::kill()`.
1009 CompilationKill,
1010 /// CM.4 (2026-07-22): `:copen`. The host arm reads the core
1011 /// error list and calls
1012 /// `lattice_multibuffer::providers::problems::create_problems_view`,
1013 /// opening the `*problems*` multibuffer — the error entries
1014 /// grouped as editable source excerpts by file. Same shape as
1015 /// `SearchTrigger`: the substrate crate owns view-creation; the
1016 /// host arm is generic apply-effect routing. Echoes "no error
1017 /// list" when the list is empty.
1018 ProblemsOpen,
1019 /// CM.4 (2026-07-22): `:cclose`. The host arm closes the active
1020 /// `*problems*` view (an editable multibuffer with
1021 /// `ProblemsMinorMode`), leaving the source buffers open — the
1022 /// `NarrowWiden` close shape, guarded to problems views. No-op +
1023 /// echo when the active buffer isn't a problems view.
1024 ProblemsClose,
1025 /// RV.3 (2026-08-10): `gr` in a `*problems*` view — rebuild its
1026 /// excerpts from the *current* error list, in place.
1027 ///
1028 /// Deliberately NOT a re-fire of [`Self::ProblemsOpen`]:
1029 /// `create_multibuffer_view` mints a fresh `BufferId` on every
1030 /// call, so re-opening would leave the old view behind and add a
1031 /// second `*problems*` buffer rather than refreshing the one the
1032 /// user is looking at. The host arm instead re-reads the error
1033 /// list and hands it to
1034 /// `lattice_multibuffer::providers::problems::refresh_problems_view`,
1035 /// which swaps sources + excerpts atomically via
1036 /// `replace_excerpts`. Same division as `ProblemsOpen`: the
1037 /// substrate crate owns the rebuild, the host arm is generic glue.
1038 ProblemsRefresh,
1039 /// PV.1 (2026-08-12): **open the multibuffer view a provider owns**
1040 /// — the generic replacement for the per-provider trigger variant.
1041 ///
1042 /// A multibuffer view can only be created through `ModeActivator`,
1043 /// which is `&mut`-backed and therefore host-only, so every provider
1044 /// trigger has to round-trip through an effect. `SearchTrigger` /
1045 /// `ProblemsOpen` each spent a variant here plus a host match arm
1046 /// plus a plugin-boundary arm on exactly that — three crates touched
1047 /// per provider, against a design whose acid test is that a provider
1048 /// crate touches none.
1049 ///
1050 /// Here the host arm is provider-agnostic: look `provider` up in the
1051 /// `ProviderViewRegistry`, call the registered opener with the host
1052 /// as the activator and `args` verbatim, then apply the generic
1053 /// outcome (activate + echo, or echo the refusal). What the opener
1054 /// computes, and how it words its own success and refusal, stays in
1055 /// the provider's crate.
1056 ///
1057 /// `args` is the trigger's parameters as the `:` line or transient
1058 /// row produced them — the same `Args` shape a command handler
1059 /// receives, so one opener serves both front-ends.
1060 ///
1061 /// `:narrow` / `zn` deliberately do NOT route through here; see
1062 /// [`Self::NarrowTrigger`] and `lattice_mode::provider_view`'s module
1063 /// docs for why a range resolved against cursor / Visual / marks is
1064 /// not a provider parameter.
1065 OpenProviderView {
1066 /// Name the provider registered its opener under in the
1067 /// `ProviderViewRegistry`. An unknown name echoes a warning naming
1068 /// it.
1069 provider: String,
1070 /// The trigger's parameters, passed to the opener verbatim.
1071 args: crate::args::Args,
1072 },
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 #![allow(clippy::unwrap_used, clippy::panic)]
1078 use super::*;
1079
1080 #[test]
1081 fn quit_round_trips_through_serde() {
1082 let q = AppEffect::Quit;
1083 let s = serde_json::to_string(&q).unwrap();
1084 let back: AppEffect = serde_json::from_str(&s).unwrap();
1085 assert_eq!(q, back);
1086 }
1087}