Skip to main content

lattice_plugin_host/
boundary_app_effect.rs

1//! `WitBoundary` mirror for `AppEffect` (plugin-host.md §4.4, PH7.3b2).
2//!
3//! `AppEffect` is the App-side typed effect carried by `Effect::AppAction` —
4//! chord-bound work with no grammar concept attached (`<Esc>` exits Visual,
5//! `<C-w>v` splits a pane, `o` opens a line below). Mirroring it unblocks the
6//! `effect::app-action` arm (which crossed as a typed error at PH7.3b1b).
7//!
8//! Every arm is pure, flat, non-recursive data and reuses the shared payload
9//! mirrors (`ModalState`/`VisualKind`/`SearchDirection`/`Register`, PH7.3b1a)
10//! plus four `app-effect`-only helper enums (`ViewportPos`/`ScrollPos`/
11//! `PaneDirection`/`HScroll`). One arm is absent by design:
12//! `AppEffect::NarrowTrigger { range: Option<Range> }` carries the recursive
13//! ex-command `Range` (`RangeBound::Offset { base: Box<RangeBound> }`) + a
14//! plugin `RangeId`, which WIT cannot express — it crosses as a typed
15//! `WitBoundary` error until a range mirror lands (the `Effect::Global`
16//! precedent). `NarrowLines` (a pre-resolved line span) crosses fine.
17
18use crate::WitBoundary;
19use crate::lattice::plugin_host::types::{
20    AppEffect as WitAppEffect, FormatIntent as WitFormatIntent,
21    FormatRangePayload as WitFormatRangePayload, Hscroll as WitHscroll,
22    InsertLineEdit as WitInsertLineEdit, NarrowLinesPayload as WitNarrowLinesPayload,
23    OpenProviderViewPayload as WitOpenProviderViewPayload, PaneDirection as WitPaneDirection,
24    ScrollPos as WitScrollPos, ViewportPos as WitViewportPos,
25};
26use lattice_grammar::app_effect::{
27    AppEffect as NativeAppEffect, HScroll as NativeHScroll, InsertLineEdit as NativeInsertLineEdit,
28    PaneDirection as NativePaneDirection, ScrollPos as NativeScrollPos,
29    ViewportPos as NativeViewportPos,
30};
31use lattice_grammar::modal::{
32    ModalState as NativeModalState, SearchDirection as NativeSearchDirection,
33    VisualKind as NativeVisualKind,
34};
35use lattice_grammar::register::Register as NativeRegister;
36use lattice_grammar::registry::OperatorId;
37use lattice_protocol::ids::CommandId;
38
39/// RF.5b: `FormatIntent` ↔ its WIT mirror.
40///
41/// Two total functions rather than a derive: the WIT enum is generated,
42/// so an exhaustive `match` here is what makes adding an intent on either
43/// side a compile error instead of a silent mistranslation.
44fn wit_format_intent(intent: lattice_core::FormatIntent) -> WitFormatIntent {
45    match intent {
46        lattice_core::FormatIntent::Indent => WitFormatIntent::Indent,
47        lattice_core::FormatIntent::Reflow => WitFormatIntent::Reflow,
48        lattice_core::FormatIntent::Reformat => WitFormatIntent::Reformat,
49    }
50}
51
52fn native_format_intent(intent: WitFormatIntent) -> lattice_core::FormatIntent {
53    match intent {
54        WitFormatIntent::Indent => lattice_core::FormatIntent::Indent,
55        WitFormatIntent::Reflow => lattice_core::FormatIntent::Reflow,
56        WitFormatIntent::Reformat => lattice_core::FormatIntent::Reformat,
57    }
58}
59
60impl WitBoundary for NativeViewportPos {
61    type Wit = WitViewportPos;
62    fn to_wit(&self) -> Result<WitViewportPos, String> {
63        Ok(match self {
64            NativeViewportPos::Top => WitViewportPos::Top,
65            NativeViewportPos::Middle => WitViewportPos::Middle,
66            NativeViewportPos::Bottom => WitViewportPos::Bottom,
67        })
68    }
69    fn from_wit(w: WitViewportPos) -> Result<Self, String> {
70        Ok(match w {
71            WitViewportPos::Top => NativeViewportPos::Top,
72            WitViewportPos::Middle => NativeViewportPos::Middle,
73            WitViewportPos::Bottom => NativeViewportPos::Bottom,
74        })
75    }
76}
77
78impl WitBoundary for NativeScrollPos {
79    type Wit = WitScrollPos;
80    fn to_wit(&self) -> Result<WitScrollPos, String> {
81        Ok(match self {
82            NativeScrollPos::Top => WitScrollPos::Top,
83            NativeScrollPos::Center => WitScrollPos::Center,
84            NativeScrollPos::Bottom => WitScrollPos::Bottom,
85        })
86    }
87    fn from_wit(w: WitScrollPos) -> Result<Self, String> {
88        Ok(match w {
89            WitScrollPos::Top => NativeScrollPos::Top,
90            WitScrollPos::Center => NativeScrollPos::Center,
91            WitScrollPos::Bottom => NativeScrollPos::Bottom,
92        })
93    }
94}
95
96impl WitBoundary for NativePaneDirection {
97    type Wit = WitPaneDirection;
98    fn to_wit(&self) -> Result<WitPaneDirection, String> {
99        Ok(match self {
100            NativePaneDirection::Left => WitPaneDirection::Left,
101            NativePaneDirection::Down => WitPaneDirection::Down,
102            NativePaneDirection::Up => WitPaneDirection::Up,
103            NativePaneDirection::Right => WitPaneDirection::Right,
104        })
105    }
106    fn from_wit(w: WitPaneDirection) -> Result<Self, String> {
107        Ok(match w {
108            WitPaneDirection::Left => NativePaneDirection::Left,
109            WitPaneDirection::Down => NativePaneDirection::Down,
110            WitPaneDirection::Up => NativePaneDirection::Up,
111            WitPaneDirection::Right => NativePaneDirection::Right,
112        })
113    }
114}
115
116impl WitBoundary for NativeInsertLineEdit {
117    type Wit = WitInsertLineEdit;
118    fn to_wit(&self) -> Result<WitInsertLineEdit, String> {
119        Ok(match self {
120            NativeInsertLineEdit::CursorLineStart => WitInsertLineEdit::CursorLineStart,
121            NativeInsertLineEdit::CursorLineEnd => WitInsertLineEdit::CursorLineEnd,
122            NativeInsertLineEdit::CursorCharLeft => WitInsertLineEdit::CursorCharLeft,
123            NativeInsertLineEdit::CursorCharRight => WitInsertLineEdit::CursorCharRight,
124            NativeInsertLineEdit::DeleteWordBackward => WitInsertLineEdit::DeleteWordBackward,
125            NativeInsertLineEdit::DeleteToLineStart => WitInsertLineEdit::DeleteToLineStart,
126            NativeInsertLineEdit::KillToLineEnd => WitInsertLineEdit::KillToLineEnd,
127            NativeInsertLineEdit::IndentLine => WitInsertLineEdit::IndentLine,
128            NativeInsertLineEdit::DedentLine => WitInsertLineEdit::DedentLine,
129        })
130    }
131    fn from_wit(w: WitInsertLineEdit) -> Result<Self, String> {
132        Ok(match w {
133            WitInsertLineEdit::CursorLineStart => NativeInsertLineEdit::CursorLineStart,
134            WitInsertLineEdit::CursorLineEnd => NativeInsertLineEdit::CursorLineEnd,
135            WitInsertLineEdit::CursorCharLeft => NativeInsertLineEdit::CursorCharLeft,
136            WitInsertLineEdit::CursorCharRight => NativeInsertLineEdit::CursorCharRight,
137            WitInsertLineEdit::DeleteWordBackward => NativeInsertLineEdit::DeleteWordBackward,
138            WitInsertLineEdit::DeleteToLineStart => NativeInsertLineEdit::DeleteToLineStart,
139            WitInsertLineEdit::KillToLineEnd => NativeInsertLineEdit::KillToLineEnd,
140            WitInsertLineEdit::IndentLine => NativeInsertLineEdit::IndentLine,
141            WitInsertLineEdit::DedentLine => NativeInsertLineEdit::DedentLine,
142        })
143    }
144}
145
146impl WitBoundary for NativeHScroll {
147    type Wit = WitHscroll;
148    fn to_wit(&self) -> Result<WitHscroll, String> {
149        Ok(match self {
150            NativeHScroll::Columns { right } => WitHscroll::Columns(*right),
151            NativeHScroll::HalfScreen { right } => WitHscroll::HalfScreen(*right),
152            NativeHScroll::CursorToEdge { end } => WitHscroll::CursorToEdge(*end),
153        })
154    }
155    fn from_wit(w: WitHscroll) -> Result<Self, String> {
156        Ok(match w {
157            WitHscroll::Columns(right) => NativeHScroll::Columns { right },
158            WitHscroll::HalfScreen(right) => NativeHScroll::HalfScreen { right },
159            WitHscroll::CursorToEdge(end) => NativeHScroll::CursorToEdge { end },
160        })
161    }
162}
163
164impl WitBoundary for NativeAppEffect {
165    type Wit = WitAppEffect;
166
167    fn to_wit(&self) -> Result<WitAppEffect, String> {
168        Ok(match self {
169            NativeAppEffect::Quit => WitAppEffect::Quit,
170            // CG.1: foreground cancellation is the *user's* escape hatch —
171            // it flips the token of whatever the user triggered and snaps
172            // the editor back to Normal. A plugin emitting it would be
173            // yanking the user out of their mode from a background task,
174            // which is the opposite of the contract. Deliberately no WIT
175            // surface; typed error, never lossy (the `*Line*` precedent
176            // below). CG.4 threads the token INTO plugin calls so the host
177            // can cancel a plugin — the direction that does make sense.
178            NativeAppEffect::Cancel => {
179                return Err(
180                    "AppEffect::Cancel is the user's foreground-cancel hatch (`<C-c>`, \
181                     cancellation.md CG.1); no plugin (WIT) surface — a plugin must not \
182                     reset the user's mode"
183                        .to_string(),
184                );
185            }
186            NativeAppEffect::MatchBracket => WitAppEffect::MatchBracket,
187            NativeAppEffect::ToggleCaseAtCursor => WitAppEffect::ToggleCaseAtCursor,
188            NativeAppEffect::OpenLineBelow => WitAppEffect::OpenLineBelow,
189            NativeAppEffect::OpenLineAbove => WitAppEffect::OpenLineAbove,
190            NativeAppEffect::SearchNext => WitAppEffect::SearchNext,
191            NativeAppEffect::SearchPrevious => WitAppEffect::SearchPrevious,
192            NativeAppEffect::JumpHistoryBack => WitAppEffect::JumpHistoryBack,
193            NativeAppEffect::JumpHistoryForward => WitAppEffect::JumpHistoryForward,
194            NativeAppEffect::PaneHistoryBack => WitAppEffect::PaneHistoryBack,
195            NativeAppEffect::PaneHistoryForward => WitAppEffect::PaneHistoryForward,
196            NativeAppEffect::WalkMarkHistoryBack => WitAppEffect::WalkMarkHistoryBack,
197            NativeAppEffect::WalkMarkHistoryForward => WitAppEffect::WalkMarkHistoryForward,
198            NativeAppEffect::TagStackPop => WitAppEffect::TagStackPop,
199            NativeAppEffect::OpenFoldAtCursor => WitAppEffect::OpenFoldAtCursor,
200            NativeAppEffect::CloseFoldAtCursor => WitAppEffect::CloseFoldAtCursor,
201            NativeAppEffect::ToggleFoldAtCursor => WitAppEffect::ToggleFoldAtCursor,
202            NativeAppEffect::OpenAllFolds => WitAppEffect::OpenAllFolds,
203            NativeAppEffect::CloseAllFolds => WitAppEffect::CloseAllFolds,
204            NativeAppEffect::CycleFoldAtCursor => WitAppEffect::CycleFoldAtCursor,
205            NativeAppEffect::CycleFoldsGlobal => WitAppEffect::CycleFoldsGlobal,
206            NativeAppEffect::GotoParentFold => WitAppEffect::GotoParentFold,
207            NativeAppEffect::DeleteFoldAtCursor => WitAppEffect::DeleteFoldAtCursor,
208            NativeAppEffect::OpenFoldsRecursively => WitAppEffect::OpenFoldsRecursively,
209            NativeAppEffect::CloseFoldsRecursively => WitAppEffect::CloseFoldsRecursively,
210            NativeAppEffect::DeleteFoldsRecursively => WitAppEffect::DeleteFoldsRecursively,
211            NativeAppEffect::GotoNextFold => WitAppEffect::GotoNextFold,
212            NativeAppEffect::GotoPrevFold => WitAppEffect::GotoPrevFold,
213            NativeAppEffect::ToggleFoldEnable => WitAppEffect::ToggleFoldEnable,
214            NativeAppEffect::Undo => WitAppEffect::Undo,
215            NativeAppEffect::Redo => WitAppEffect::Redo,
216            NativeAppEffect::RepeatLastChange => WitAppEffect::RepeatLastChange,
217            NativeAppEffect::PageDown => WitAppEffect::PageDown,
218            NativeAppEffect::HalfPageDown => WitAppEffect::HalfPageDown,
219            NativeAppEffect::HalfPageUp => WitAppEffect::HalfPageUp,
220            NativeAppEffect::PageUp => WitAppEffect::PageUp,
221            NativeAppEffect::ScrollLineUp => WitAppEffect::ScrollLineUp,
222            NativeAppEffect::ScrollLineDown => WitAppEffect::ScrollLineDown,
223            NativeAppEffect::RedrawScreen => WitAppEffect::RedrawScreen,
224            NativeAppEffect::OpenCommandPicker => WitAppEffect::OpenCommandPicker,
225            // MB.3: `q:` opens the command-line *history* picker — a
226            // host-internal command-line affordance (its accept seeds the
227            // `:` line). No plugin surface; plugins reach the same picker
228            // via the `:history` ex-command. Typed error, never lossy
229            // (mirrors the `CommandLine*` host-internal group below).
230            NativeAppEffect::OpenHistoryPicker => {
231                return Err(
232                    "AppEffect::OpenHistoryPicker is a host-internal command-line-history \
233                     affordance (rich-minibuffer MB.3); no plugin (WIT) surface — use the \
234                     `:history` ex-command"
235                        .to_string(),
236                );
237            }
238            NativeAppEffect::OpenSearchHistoryPicker => {
239                return Err(
240                    "AppEffect::OpenSearchHistoryPicker is a host-internal search-line-history \
241                     affordance (rich-minibuffer MB.5); no plugin (WIT) surface — use the \
242                     `:history search` ex-command"
243                        .to_string(),
244                );
245            }
246            NativeAppEffect::EnterCommandLine => WitAppEffect::EnterCommandLine,
247            // MB.1: the `command-line-mode` chord effects (submit / cancel
248            // / history / completion / describe) are host-internal — the
249            // `:` line's readline machinery lives entirely host-side and
250            // plugins never bind these. No WIT variant; typed error, never
251            // lossy (NarrowTrigger / CompileRun precedent).
252            NativeAppEffect::CommandLineSubmit
253            | NativeAppEffect::CommandLineCancel
254            | NativeAppEffect::CommandLineHistoryPrev
255            | NativeAppEffect::CommandLineHistoryNext
256            | NativeAppEffect::CommandLineComplete
257            | NativeAppEffect::CommandLineCompletePrev
258            | NativeAppEffect::CommandLineDescribeUnderCursor
259            | NativeAppEffect::CommandLineToggleExpand
260            | NativeAppEffect::SearchLineSubmit
261            | NativeAppEffect::SearchLineCancel
262            | NativeAppEffect::SearchLineBackspace
263            | NativeAppEffect::SearchLineHistoryPrev
264            | NativeAppEffect::SearchLineHistoryNext
265            | NativeAppEffect::SearchLineToggleExpand
266            | NativeAppEffect::PromptLineSubmit
267            | NativeAppEffect::PromptLineCancel => {
268                return Err(
269                    "AppEffect::*Line* are host-internal minibuffer-prompt effects \
270                     (rich-minibuffer MB.1–MB.5); no plugin (WIT) surface"
271                        .to_string(),
272                );
273            }
274            NativeAppEffect::OilNavigateUp => WitAppEffect::OilNavigateUp,
275            NativeAppEffect::ReselectLastVisual => WitAppEffect::ReselectLastVisual,
276            NativeAppEffect::SwapVisualEnds => WitAppEffect::SwapVisualEnds,
277            NativeAppEffect::PasteAfter => WitAppEffect::PasteAfter,
278            NativeAppEffect::PasteBefore => WitAppEffect::PasteBefore,
279            NativeAppEffect::EnterAppend => WitAppEffect::EnterAppend,
280            NativeAppEffect::EnterInsertFirstNonBlank => WitAppEffect::EnterInsertFirstNonBlank,
281            NativeAppEffect::EnterAppendEndOfLine => WitAppEffect::EnterAppendEndOfLine,
282            NativeAppEffect::DisplayLineDown => WitAppEffect::DisplayLineDown,
283            NativeAppEffect::DisplayLineUp => WitAppEffect::DisplayLineUp,
284            NativeAppEffect::DisplayLineStart => WitAppEffect::DisplayLineStart,
285            NativeAppEffect::DisplayLineEnd => WitAppEffect::DisplayLineEnd,
286            NativeAppEffect::CreateFoldFromVisual => WitAppEffect::CreateFoldFromVisual,
287            NativeAppEffect::DeleteCharBackward => WitAppEffect::DeleteCharBackward,
288            NativeAppEffect::CompletionTrigger => WitAppEffect::CompletionTrigger,
289            NativeAppEffect::ExitVisual => WitAppEffect::ExitVisual,
290            NativeAppEffect::ReplaceUndoLast => WitAppEffect::ReplaceUndoLast,
291            NativeAppEffect::EnterMode(state) => WitAppEffect::EnterMode(state.to_wit()?),
292            NativeAppEffect::EnterVisual(k) => WitAppEffect::EnterVisual(k.to_wit()?),
293            NativeAppEffect::EnterSelect(k) => WitAppEffect::EnterSelect(k.to_wit()?),
294            NativeAppEffect::EnterSearch(d) => WitAppEffect::EnterSearch(d.to_wit()?),
295            NativeAppEffect::SearchWordUnderCursor(d) => {
296                WitAppEffect::SearchWordUnderCursor(d.to_wit()?)
297            }
298            NativeAppEffect::JumpViewport(p) => WitAppEffect::JumpViewport(p.to_wit()?),
299            NativeAppEffect::ScrollCursorTo(p) => WitAppEffect::ScrollCursorTo(p.to_wit()?),
300            NativeAppEffect::HorizontalScroll(h) => WitAppEffect::HorizontalScroll(h.to_wit()?),
301            NativeAppEffect::JoinLines { with_space } => WitAppEffect::JoinLines(*with_space),
302            NativeAppEffect::FindRepeat { reverse } => WitAppEffect::FindRepeat(*reverse),
303            NativeAppEffect::InsertNewline => WitAppEffect::InsertNewline,
304            NativeAppEffect::InsertTab => WitAppEffect::InsertTab,
305            NativeAppEffect::OverwriteChar(c) => WitAppEffect::OverwriteChar(*c),
306            NativeAppEffect::SetMark(c) => WitAppEffect::SetMark(*c),
307            NativeAppEffect::JumpToMarkLine(c) => WitAppEffect::JumpToMarkLine(*c),
308            NativeAppEffect::JumpToMarkExact(c) => WitAppEffect::JumpToMarkExact(*c),
309            NativeAppEffect::SelectRegister(r) => WitAppEffect::SelectRegister(r.to_wit()?),
310            NativeAppEffect::StartMacroRecord(c) => WitAppEffect::StartMacroRecord(*c),
311            NativeAppEffect::PlayMacro(c) => WitAppEffect::PlayMacro(*c),
312            NativeAppEffect::PlayLastMacro => WitAppEffect::PlayLastMacro,
313            NativeAppEffect::AbsorbOperatorPrefix(op) => {
314                WitAppEffect::AbsorbOperatorPrefix(op.0.raw())
315            }
316            NativeAppEffect::SplitPaneHorizontal => WitAppEffect::SplitPaneHorizontal,
317            NativeAppEffect::SplitPaneVertical => WitAppEffect::SplitPaneVertical,
318            NativeAppEffect::ClosePane => WitAppEffect::ClosePane,
319            NativeAppEffect::OnlyPane => WitAppEffect::OnlyPane,
320            NativeAppEffect::ToggleZoomPane => WitAppEffect::ToggleZoomPane,
321            NativeAppEffect::NavigatePane(d) => WitAppEffect::NavigatePane(d.to_wit()?),
322            NativeAppEffect::NextPane => WitAppEffect::NextPane,
323            NativeAppEffect::PrevPane => WitAppEffect::PrevPane,
324            NativeAppEffect::NextTab => WitAppEffect::NextTab,
325            NativeAppEffect::PrevTab => WitAppEffect::PrevTab,
326            NativeAppEffect::GoToTab(n) => WitAppEffect::GoToTab(*n),
327            NativeAppEffect::NewTab => WitAppEffect::NewTab,
328            NativeAppEffect::NewTabAt(path) => WitAppEffect::NewTabAt(path.clone()),
329            NativeAppEffect::TerminalSpawn(cmd) => WitAppEffect::TerminalSpawn(cmd.clone()),
330            NativeAppEffect::TerminalSpawnInNewTab(cmd) => {
331                WitAppEffect::TerminalSpawnInNewTab(cmd.clone())
332            }
333            NativeAppEffect::MovePaneToNewTab => WitAppEffect::MovePaneToNewTab,
334            NativeAppEffect::CloseTab => WitAppEffect::CloseTab,
335            NativeAppEffect::OnlyTab => WitAppEffect::OnlyTab,
336            NativeAppEffect::MoveTab(n) => WitAppEffect::MoveTab(*n),
337            NativeAppEffect::PickerAcceptInSplit => WitAppEffect::PickerAcceptInSplit,
338            NativeAppEffect::PickerAcceptInVSplit => WitAppEffect::PickerAcceptInVsplit,
339            NativeAppEffect::PickerAcceptInTab => WitAppEffect::PickerAcceptInTab,
340            NativeAppEffect::EqualizePanes => WitAppEffect::EqualizePanes,
341            NativeAppEffect::GrowPaneHeight => WitAppEffect::GrowPaneHeight,
342            NativeAppEffect::ShrinkPaneHeight => WitAppEffect::ShrinkPaneHeight,
343            NativeAppEffect::GrowPaneWidth => WitAppEffect::GrowPaneWidth,
344            NativeAppEffect::ShrinkPaneWidth => WitAppEffect::ShrinkPaneWidth,
345            NativeAppEffect::CompletionNext => WitAppEffect::CompletionNext,
346            NativeAppEffect::CompletionPrev => WitAppEffect::CompletionPrev,
347            NativeAppEffect::CompletionAccept => WitAppEffect::CompletionAccept,
348            NativeAppEffect::CompletionCancel => WitAppEffect::CompletionCancel,
349            NativeAppEffect::CompletionCancelAndExitInsert => {
350                WitAppEffect::CompletionCancelAndExitInsert
351            }
352            NativeAppEffect::CompletionToggleDocs => WitAppEffect::CompletionToggleDocs,
353            NativeAppEffect::CompletionDocsScrollDown => WitAppEffect::CompletionDocsScrollDown,
354            NativeAppEffect::CompletionDocsScrollUp => WitAppEffect::CompletionDocsScrollUp,
355            NativeAppEffect::CompletionAcceptThenInsert(c) => {
356                WitAppEffect::CompletionAcceptThenInsert(*c)
357            }
358            // YR.5: both put text into, or open a picker over, whichever
359            // HOST surface is focused — a place a plugin cannot name.
360            // Representable once the fill targets themselves cross.
361            // YR.6 joins them: the argument picker names a `:`-line slot,
362            // which is the same unnameable-host-surface problem. A plugin
363            // that wants an argument picker declares one in its
364            // `args_schema` (`ArgSpec.picker`, which DOES cross) rather
365            // than emitting this effect.
366            NativeAppEffect::InsertRegister(_)
367            | NativeAppEffect::OpenYankPicker
368            | NativeAppEffect::OpenArgPicker => {
369                return Err(
370                    "insert-register / open-yank-picker / open-arg-picker are host-internal, \
371                     not representable over WIT"
372                        .into(),
373                );
374            }
375            NativeAppEffect::SnippetNextPlaceholder => WitAppEffect::SnippetNextPlaceholder,
376            NativeAppEffect::SnippetPrevPlaceholder => WitAppEffect::SnippetPrevPlaceholder,
377            NativeAppEffect::CompletionFilterToSource(s) => {
378                WitAppEffect::CompletionFilterToSource(s.clone())
379            }
380            NativeAppEffect::CompletionFilterClear => WitAppEffect::CompletionFilterClear,
381            NativeAppEffect::DiffGet => WitAppEffect::DiffGet,
382            NativeAppEffect::DiffPut => WitAppEffect::DiffPut,
383            NativeAppEffect::TutorAdvance => WitAppEffect::TutorAdvance,
384            NativeAppEffect::TutorRetreat => WitAppEffect::TutorRetreat,
385            NativeAppEffect::MultibufferExpand { delta } => WitAppEffect::MultibufferExpand(*delta),
386            NativeAppEffect::NarrowWiden => WitAppEffect::NarrowWiden,
387            NativeAppEffect::NarrowLines {
388                start_line,
389                end_line,
390            } => WitAppEffect::NarrowLines(WitNarrowLinesPayload {
391                start_line: *start_line,
392                end_line: *end_line,
393            }),
394            // VM.3h: the same pre-resolved span shape as `NarrowLines`.
395            NativeAppEffect::CreateFold {
396                start_line,
397                end_line,
398            } => WitAppEffect::CreateFold(WitNarrowLinesPayload {
399                start_line: *start_line,
400                end_line: *end_line,
401            }),
402            // RF.5b: a resolved line range plus which chain owns it.
403            // Mirrors `NarrowLines` above — same shape, same reason it
404            // crosses cleanly: the range is already resolved to absolute
405            // 0-based lines, so nothing recursive rides along.
406            NativeAppEffect::FormatRange {
407                intent,
408                start_line,
409                end_line,
410            } => WitAppEffect::FormatRange(WitFormatRangePayload {
411                intent: wit_format_intent(*intent),
412                start_line: *start_line,
413                end_line: *end_line,
414            }),
415            NativeAppEffect::SearchTrigger { query } => WitAppEffect::SearchTrigger(query.clone()),
416            NativeAppEffect::SearchRefresh => WitAppEffect::SearchRefresh,
417            // Carries the recursive ex-command `Range` (§4.4); crosses with the
418            // range mirror. Typed error until then, never lossy.
419            NativeAppEffect::NarrowTrigger { .. } => {
420                return Err(
421                    "AppEffect::NarrowTrigger carries a recursive ex-command Range; it crosses \
422                     with the range mirror (fragment §4.4)"
423                        .to_string(),
424                );
425            }
426            NativeAppEffect::InsertLineEdit(edit) => WitAppEffect::InsertLineEdit(edit.to_wit()?),
427            // CM.1: `:compile`/`:recompile`/`:make` are a native
428            // built-in; the compilation WIT surface is deferred with
429            // the plugin host (compilation-mode.md §8 #2). No WIT
430            // variant yet — typed error, never lossy (NarrowTrigger
431            // precedent).
432            NativeAppEffect::CompileRun { .. } => {
433                return Err(
434                    "AppEffect::CompileRun is a native built-in; its plugin (WIT) surface is \
435                     deferred with the plugin host (compilation-mode.md §8)"
436                        .to_string(),
437                );
438            }
439            // CM.3b: the `<CR>`-jump from a `*compilation*` location line
440            // jumps + syncs core error list index; like CompileRun /
441            // ErrorNav a native built-in, WIT surface deferred with
442            // the plugin host. Typed error, never lossy.
443            NativeAppEffect::CompileJumpToLocation { .. } => {
444                return Err(
445                    "AppEffect::CompileJumpToLocation jumps to a source location + syncs core \
446                     error state; its plugin (WIT) surface is deferred with the plugin host \
447                     (compilation-mode.md §5)"
448                        .to_string(),
449                );
450            }
451            // CM.2: error navigation walks core/host state; like
452            // CompileRun, its plugin (WIT) surface is deferred with
453            // the plugin host. Typed error, never lossy.
454            NativeAppEffect::ErrorNav { .. } => {
455                return Err(
456                    "AppEffect::ErrorNav walks core error state; its plugin (WIT) surface \
457                     is deferred with the plugin host (compilation-mode.md §3)"
458                        .to_string(),
459                );
460            }
461            // CM.3a: parsed error entries feed core error state
462            // from the native compilation parser; like CompileRun /
463            // ErrorNav a native built-in, WIT surface deferred with
464            // the plugin host. Typed error, never lossy.
465            NativeAppEffect::SetErrorList { .. } => {
466                return Err(
467                    "AppEffect::SetErrorList feeds core error state from the native compilation \
468                     parser; its plugin (WIT) surface is deferred with the plugin host \
469                     (compilation-mode.md §5)"
470                        .to_string(),
471                );
472            }
473            // CM.3c: the per-buffer severity gutter index feeds the
474            // `*compilation*` buffer's in-buffer severity marks from the
475            // native compilation parser; like SetErrorList a native built-in,
476            // WIT surface deferred with the plugin host. Typed error, never
477            // lossy.
478            NativeAppEffect::CompilationGutterSet { .. } => {
479                return Err(
480                    "AppEffect::CompilationGutterSet feeds the *compilation* buffer's severity \
481                     gutter marks from the native compilation parser; its plugin (WIT) surface is \
482                     deferred with the plugin host (compilation-mode.md §5)"
483                        .to_string(),
484                );
485            }
486            // CM.3c: location-line index for theme-based highlighting of
487            // navigable file-location lines in the *compilation* buffer.
488            // Same reasoning as CompilationGutterSet — native built-in,
489            // WIT surface deferred.
490            NativeAppEffect::CompilationLocationLines { .. } => {
491                return Err(
492                    "AppEffect::CompilationLocationLines marks navigable file-location lines in the \
493                     *compilation* buffer for theme-based highlighting; its plugin (WIT) surface is \
494                     deferred with the plugin host"
495                        .to_string(),
496                );
497            }
498            // CM.3d: resolved theme colours for compilation
499            // location lines — same as above, native built-in.
500            NativeAppEffect::CompilationThemeColors { .. } => {
501                return Err(
502                    "AppEffect::CompilationThemeColors feeds theme-resolved compilation location \
503                     colours to the renderer; its plugin (WIT) surface is deferred with the \
504                     plugin host"
505                        .to_string(),
506                );
507            }
508            NativeAppEffect::CompilationKill => {
509                return Err(
510                    "AppEffect::CompilationKill kills the running compilation child process; \
511                     its plugin (WIT) surface is deferred with the plugin host"
512                        .to_string(),
513                );
514            }
515            // CM.4 / RV.3: `:copen` / `:cclose` / `gr` open, close and
516            // rebuild the `*problems*` multibuffer over core error
517            // state; like CompileRun / ErrorNav a native built-in, WIT
518            // surface deferred with the plugin host. Typed error,
519            // never lossy.
520            NativeAppEffect::ProblemsOpen
521            | NativeAppEffect::ProblemsClose
522            | NativeAppEffect::ProblemsRefresh => {
523                return Err(
524                    "AppEffect::Problems{Open,Close,Refresh} open, close and rebuild the \
525                     *problems* view over core error state; their plugin (WIT) surface is \
526                     deferred with the plugin host (compilation-mode.md §4)"
527                        .to_string(),
528                );
529            }
530            // AG.1: the generic provider-view seam, no longer deferred.
531            //
532            // PV.1 withheld it as a capability question — which providers may a
533            // plugin trigger? — to be answered with the host's capability model
534            // rather than ahead of it. The precedent had already answered it:
535            // `Effect::OpenPicker` and `Effect::OpenTransient` both cross
536            // UNGATED and let a guest open any registered source by name. This
537            // is the same authority in the same shape, so withholding it never
538            // withheld the capability; it only stopped the one seam that needed
539            // it from naming its own trigger.
540            //
541            // That cost was concrete: the agenda's ex-command lived in the host
542            // because the plugin had no way to open the view, so a feature every
543            // user calls `org-agenda` shipped as `:agenda` and the plugin could
544            // not fix it from its own side.
545            //
546            // The argument crosses as `option<string>` — the host-interpreted
547            // parameter, of which every provider view takes at most one (a
548            // root, a query). Mirroring the whole recursive `Args` enum would
549            // add a second args encoding to the boundary for cases no provider
550            // has, so anything it cannot express is a typed error rather than a
551            // silent flattening — the `NarrowTrigger` precedent two arms up.
552            //
553            // OA.11a: `Args::List` is now expressible, as `argument` plus
554            // `scan-args`. That is not the recursive mirror this comment used
555            // to refuse; it is the flat positional case, and it exists because
556            // a scan source needs a channel the host does NOT read. Position 0
557            // is the host's argument, the rest are the guest's. An empty
558            // position 0 means "no root override", which is how a caller sends
559            // scan args without one.
560            NativeAppEffect::OpenProviderView { provider, args } => {
561                let (argument, scan_args) = match args {
562                    lattice_grammar::args::Args::None => (None, Vec::new()),
563                    lattice_grammar::args::Args::String(s) => (Some(s.clone()), Vec::new()),
564                    lattice_grammar::args::Args::List(values) => {
565                        let mut strings = Vec::with_capacity(values.len());
566                        for v in values {
567                            match v {
568                                lattice_grammar::args::ArgValue::String(s) => {
569                                    strings.push(s.clone())
570                                }
571                                // Only the string form crosses. A provider
572                                // view's arguments are free text on both sides
573                                // of the boundary, and quietly stringifying an
574                                // `Int` or an `Invocation` here would invent a
575                                // spelling the guest never agreed to.
576                                other => {
577                                    return Err(format!(
578                                        "AppEffect::OpenProviderView carries a non-string list \
579                                         argument the provider-view boundary does not mirror \
580                                         ({other:?}); provider-view arguments are free text"
581                                    ));
582                                }
583                            }
584                        }
585                        let mut it = strings.into_iter();
586                        let first = it.next().unwrap_or_default();
587                        // An empty first element is "no root override" rather
588                        // than a root of "", which is not a path anyone means.
589                        let argument = (!first.is_empty()).then_some(first);
590                        (argument, it.collect())
591                    }
592                    other => {
593                        return Err(format!(
594                            "AppEffect::OpenProviderView carries args the provider-view boundary \
595                             does not mirror ({other:?}); a provider view takes at most one \
596                             free-text argument"
597                        ));
598                    }
599                };
600                WitAppEffect::OpenProviderView(WitOpenProviderViewPayload {
601                    provider: provider.clone(),
602                    argument,
603                    scan_args,
604                })
605            }
606        })
607    }
608
609    fn from_wit(w: WitAppEffect) -> Result<Self, String> {
610        Ok(match w {
611            WitAppEffect::Quit => NativeAppEffect::Quit,
612            WitAppEffect::MatchBracket => NativeAppEffect::MatchBracket,
613            WitAppEffect::ToggleCaseAtCursor => NativeAppEffect::ToggleCaseAtCursor,
614            WitAppEffect::OpenLineBelow => NativeAppEffect::OpenLineBelow,
615            WitAppEffect::OpenLineAbove => NativeAppEffect::OpenLineAbove,
616            WitAppEffect::SearchNext => NativeAppEffect::SearchNext,
617            WitAppEffect::SearchPrevious => NativeAppEffect::SearchPrevious,
618            WitAppEffect::JumpHistoryBack => NativeAppEffect::JumpHistoryBack,
619            WitAppEffect::JumpHistoryForward => NativeAppEffect::JumpHistoryForward,
620            WitAppEffect::PaneHistoryBack => NativeAppEffect::PaneHistoryBack,
621            WitAppEffect::PaneHistoryForward => NativeAppEffect::PaneHistoryForward,
622            WitAppEffect::WalkMarkHistoryBack => NativeAppEffect::WalkMarkHistoryBack,
623            WitAppEffect::WalkMarkHistoryForward => NativeAppEffect::WalkMarkHistoryForward,
624            WitAppEffect::TagStackPop => NativeAppEffect::TagStackPop,
625            WitAppEffect::OpenFoldAtCursor => NativeAppEffect::OpenFoldAtCursor,
626            WitAppEffect::CloseFoldAtCursor => NativeAppEffect::CloseFoldAtCursor,
627            WitAppEffect::ToggleFoldAtCursor => NativeAppEffect::ToggleFoldAtCursor,
628            WitAppEffect::OpenAllFolds => NativeAppEffect::OpenAllFolds,
629            WitAppEffect::CloseAllFolds => NativeAppEffect::CloseAllFolds,
630            WitAppEffect::CycleFoldAtCursor => NativeAppEffect::CycleFoldAtCursor,
631            WitAppEffect::CycleFoldsGlobal => NativeAppEffect::CycleFoldsGlobal,
632            WitAppEffect::GotoParentFold => NativeAppEffect::GotoParentFold,
633            WitAppEffect::DeleteFoldAtCursor => NativeAppEffect::DeleteFoldAtCursor,
634            WitAppEffect::OpenFoldsRecursively => NativeAppEffect::OpenFoldsRecursively,
635            WitAppEffect::CloseFoldsRecursively => NativeAppEffect::CloseFoldsRecursively,
636            WitAppEffect::DeleteFoldsRecursively => NativeAppEffect::DeleteFoldsRecursively,
637            WitAppEffect::GotoNextFold => NativeAppEffect::GotoNextFold,
638            WitAppEffect::GotoPrevFold => NativeAppEffect::GotoPrevFold,
639            WitAppEffect::ToggleFoldEnable => NativeAppEffect::ToggleFoldEnable,
640            WitAppEffect::Undo => NativeAppEffect::Undo,
641            WitAppEffect::Redo => NativeAppEffect::Redo,
642            WitAppEffect::RepeatLastChange => NativeAppEffect::RepeatLastChange,
643            WitAppEffect::PageDown => NativeAppEffect::PageDown,
644            WitAppEffect::HalfPageDown => NativeAppEffect::HalfPageDown,
645            WitAppEffect::HalfPageUp => NativeAppEffect::HalfPageUp,
646            WitAppEffect::PageUp => NativeAppEffect::PageUp,
647            WitAppEffect::ScrollLineUp => NativeAppEffect::ScrollLineUp,
648            WitAppEffect::ScrollLineDown => NativeAppEffect::ScrollLineDown,
649            WitAppEffect::RedrawScreen => NativeAppEffect::RedrawScreen,
650            WitAppEffect::OpenCommandPicker => NativeAppEffect::OpenCommandPicker,
651            WitAppEffect::EnterCommandLine => NativeAppEffect::EnterCommandLine,
652            WitAppEffect::OilNavigateUp => NativeAppEffect::OilNavigateUp,
653            WitAppEffect::ReselectLastVisual => NativeAppEffect::ReselectLastVisual,
654            WitAppEffect::SwapVisualEnds => NativeAppEffect::SwapVisualEnds,
655            WitAppEffect::PasteAfter => NativeAppEffect::PasteAfter,
656            WitAppEffect::PasteBefore => NativeAppEffect::PasteBefore,
657            WitAppEffect::EnterAppend => NativeAppEffect::EnterAppend,
658            WitAppEffect::EnterInsertFirstNonBlank => NativeAppEffect::EnterInsertFirstNonBlank,
659            WitAppEffect::EnterAppendEndOfLine => NativeAppEffect::EnterAppendEndOfLine,
660            WitAppEffect::DisplayLineDown => NativeAppEffect::DisplayLineDown,
661            WitAppEffect::DisplayLineUp => NativeAppEffect::DisplayLineUp,
662            WitAppEffect::DisplayLineStart => NativeAppEffect::DisplayLineStart,
663            WitAppEffect::DisplayLineEnd => NativeAppEffect::DisplayLineEnd,
664            WitAppEffect::CreateFoldFromVisual => NativeAppEffect::CreateFoldFromVisual,
665            WitAppEffect::DeleteCharBackward => NativeAppEffect::DeleteCharBackward,
666            WitAppEffect::CompletionTrigger => NativeAppEffect::CompletionTrigger,
667            WitAppEffect::ExitVisual => NativeAppEffect::ExitVisual,
668            WitAppEffect::ReplaceUndoLast => NativeAppEffect::ReplaceUndoLast,
669            WitAppEffect::EnterMode(state) => {
670                NativeAppEffect::EnterMode(NativeModalState::from_wit(state)?)
671            }
672            WitAppEffect::EnterVisual(k) => {
673                NativeAppEffect::EnterVisual(NativeVisualKind::from_wit(k)?)
674            }
675            WitAppEffect::EnterSelect(k) => {
676                NativeAppEffect::EnterSelect(NativeVisualKind::from_wit(k)?)
677            }
678            WitAppEffect::EnterSearch(d) => {
679                NativeAppEffect::EnterSearch(NativeSearchDirection::from_wit(d)?)
680            }
681            WitAppEffect::SearchWordUnderCursor(d) => {
682                NativeAppEffect::SearchWordUnderCursor(NativeSearchDirection::from_wit(d)?)
683            }
684            WitAppEffect::JumpViewport(p) => {
685                NativeAppEffect::JumpViewport(NativeViewportPos::from_wit(p)?)
686            }
687            WitAppEffect::ScrollCursorTo(p) => {
688                NativeAppEffect::ScrollCursorTo(NativeScrollPos::from_wit(p)?)
689            }
690            WitAppEffect::HorizontalScroll(h) => {
691                NativeAppEffect::HorizontalScroll(NativeHScroll::from_wit(h)?)
692            }
693            WitAppEffect::JoinLines(with_space) => NativeAppEffect::JoinLines { with_space },
694            WitAppEffect::FindRepeat(reverse) => NativeAppEffect::FindRepeat { reverse },
695            WitAppEffect::InsertNewline => NativeAppEffect::InsertNewline,
696            WitAppEffect::InsertTab => NativeAppEffect::InsertTab,
697            WitAppEffect::OverwriteChar(c) => NativeAppEffect::OverwriteChar(c),
698            WitAppEffect::SetMark(c) => NativeAppEffect::SetMark(c),
699            WitAppEffect::JumpToMarkLine(c) => NativeAppEffect::JumpToMarkLine(c),
700            WitAppEffect::JumpToMarkExact(c) => NativeAppEffect::JumpToMarkExact(c),
701            WitAppEffect::SelectRegister(r) => {
702                NativeAppEffect::SelectRegister(NativeRegister::from_wit(r)?)
703            }
704            WitAppEffect::StartMacroRecord(c) => NativeAppEffect::StartMacroRecord(c),
705            WitAppEffect::PlayMacro(c) => NativeAppEffect::PlayMacro(c),
706            WitAppEffect::PlayLastMacro => NativeAppEffect::PlayLastMacro,
707            WitAppEffect::AbsorbOperatorPrefix(raw) => {
708                NativeAppEffect::AbsorbOperatorPrefix(OperatorId(CommandId::new(raw)))
709            }
710            WitAppEffect::SplitPaneHorizontal => NativeAppEffect::SplitPaneHorizontal,
711            WitAppEffect::SplitPaneVertical => NativeAppEffect::SplitPaneVertical,
712            WitAppEffect::ClosePane => NativeAppEffect::ClosePane,
713            WitAppEffect::OnlyPane => NativeAppEffect::OnlyPane,
714            WitAppEffect::ToggleZoomPane => NativeAppEffect::ToggleZoomPane,
715            WitAppEffect::NavigatePane(d) => {
716                NativeAppEffect::NavigatePane(NativePaneDirection::from_wit(d)?)
717            }
718            WitAppEffect::NextPane => NativeAppEffect::NextPane,
719            WitAppEffect::PrevPane => NativeAppEffect::PrevPane,
720            WitAppEffect::NextTab => NativeAppEffect::NextTab,
721            WitAppEffect::PrevTab => NativeAppEffect::PrevTab,
722            WitAppEffect::GoToTab(n) => NativeAppEffect::GoToTab(n),
723            WitAppEffect::NewTab => NativeAppEffect::NewTab,
724            WitAppEffect::NewTabAt(path) => NativeAppEffect::NewTabAt(path),
725            WitAppEffect::TerminalSpawn(cmd) => NativeAppEffect::TerminalSpawn(cmd),
726            WitAppEffect::TerminalSpawnInNewTab(cmd) => NativeAppEffect::TerminalSpawnInNewTab(cmd),
727            WitAppEffect::MovePaneToNewTab => NativeAppEffect::MovePaneToNewTab,
728            WitAppEffect::CloseTab => NativeAppEffect::CloseTab,
729            WitAppEffect::OnlyTab => NativeAppEffect::OnlyTab,
730            WitAppEffect::MoveTab(n) => NativeAppEffect::MoveTab(n),
731            WitAppEffect::PickerAcceptInSplit => NativeAppEffect::PickerAcceptInSplit,
732            WitAppEffect::PickerAcceptInVsplit => NativeAppEffect::PickerAcceptInVSplit,
733            WitAppEffect::PickerAcceptInTab => NativeAppEffect::PickerAcceptInTab,
734            WitAppEffect::EqualizePanes => NativeAppEffect::EqualizePanes,
735            WitAppEffect::GrowPaneHeight => NativeAppEffect::GrowPaneHeight,
736            WitAppEffect::ShrinkPaneHeight => NativeAppEffect::ShrinkPaneHeight,
737            WitAppEffect::GrowPaneWidth => NativeAppEffect::GrowPaneWidth,
738            WitAppEffect::ShrinkPaneWidth => NativeAppEffect::ShrinkPaneWidth,
739            WitAppEffect::CompletionNext => NativeAppEffect::CompletionNext,
740            WitAppEffect::CompletionPrev => NativeAppEffect::CompletionPrev,
741            WitAppEffect::CompletionAccept => NativeAppEffect::CompletionAccept,
742            WitAppEffect::CompletionCancel => NativeAppEffect::CompletionCancel,
743            WitAppEffect::CompletionCancelAndExitInsert => {
744                NativeAppEffect::CompletionCancelAndExitInsert
745            }
746            WitAppEffect::CompletionToggleDocs => NativeAppEffect::CompletionToggleDocs,
747            WitAppEffect::CompletionDocsScrollDown => NativeAppEffect::CompletionDocsScrollDown,
748            WitAppEffect::CompletionDocsScrollUp => NativeAppEffect::CompletionDocsScrollUp,
749            WitAppEffect::CompletionAcceptThenInsert(c) => {
750                NativeAppEffect::CompletionAcceptThenInsert(c)
751            }
752            WitAppEffect::SnippetNextPlaceholder => NativeAppEffect::SnippetNextPlaceholder,
753            WitAppEffect::SnippetPrevPlaceholder => NativeAppEffect::SnippetPrevPlaceholder,
754            WitAppEffect::CompletionFilterToSource(s) => {
755                NativeAppEffect::CompletionFilterToSource(s)
756            }
757            WitAppEffect::CompletionFilterClear => NativeAppEffect::CompletionFilterClear,
758            WitAppEffect::DiffGet => NativeAppEffect::DiffGet,
759            WitAppEffect::DiffPut => NativeAppEffect::DiffPut,
760            WitAppEffect::TutorAdvance => NativeAppEffect::TutorAdvance,
761            WitAppEffect::TutorRetreat => NativeAppEffect::TutorRetreat,
762            WitAppEffect::MultibufferExpand(delta) => NativeAppEffect::MultibufferExpand { delta },
763            WitAppEffect::NarrowWiden => NativeAppEffect::NarrowWiden,
764            WitAppEffect::NarrowLines(p) => NativeAppEffect::NarrowLines {
765                start_line: p.start_line,
766                end_line: p.end_line,
767            },
768            WitAppEffect::CreateFold(p) => NativeAppEffect::CreateFold {
769                start_line: p.start_line,
770                end_line: p.end_line,
771            },
772            WitAppEffect::FormatRange(p) => NativeAppEffect::FormatRange {
773                intent: native_format_intent(p.intent),
774                start_line: p.start_line,
775                end_line: p.end_line,
776            },
777            WitAppEffect::SearchTrigger(query) => NativeAppEffect::SearchTrigger { query },
778            WitAppEffect::SearchRefresh => NativeAppEffect::SearchRefresh,
779            // AG.1: the direction that matters — a plugin's ex-command opening
780            // the view. `none` becomes `Args::None`, which is what a bare
781            // `:org-agenda` with no root means.
782            // OA.11a: with no scan args this is byte-for-byte the old mapping,
783            // which is what keeps every existing trigger — `:org-agenda`,
784            // `:org-agenda ~/notes` — unchanged. Scan args promote it to the
785            // positional list form, with the host's argument still at 0 so the
786            // two never contend for one slot.
787            WitAppEffect::OpenProviderView(p) => NativeAppEffect::OpenProviderView {
788                provider: p.provider,
789                args: match (p.argument, p.scan_args.is_empty()) {
790                    (Some(s), true) => lattice_grammar::args::Args::String(s),
791                    (None, true) => lattice_grammar::args::Args::None,
792                    (argument, false) => lattice_grammar::args::Args::List(
793                        std::iter::once(argument.unwrap_or_default())
794                            .chain(p.scan_args)
795                            .map(lattice_grammar::args::ArgValue::String)
796                            .collect(),
797                    ),
798                },
799            },
800            WitAppEffect::InsertLineEdit(edit) => {
801                NativeAppEffect::InsertLineEdit(NativeInsertLineEdit::from_wit(edit)?)
802            }
803        })
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    #![allow(clippy::unwrap_used, clippy::panic)]
810
811    use super::*;
812
813    /// `AppEffect` derives `PartialEq`/`Eq`, so round-trip is a direct equality.
814    fn assert_round_trips(native: NativeAppEffect) {
815        let wit = native.to_wit().expect("to_wit");
816        let back = NativeAppEffect::from_wit(wit).expect("from_wit");
817        assert_eq!(native, back);
818    }
819
820    /// Every payload-bearing arm (exercises the 4 helper enums + the shared
821    /// ModalState/VisualKind/SearchDirection/Register mirrors + the primitives).
822    /// The unit arms are covered separately; `to_wit`/`from_wit` are both
823    /// compiler-exhaustive, so a new `AppEffect` arm forces a mapping here.
824    #[test]
825    fn app_effect_payload_arms_round_trip() {
826        for e in [
827            NativeAppEffect::EnterMode(NativeModalState::Insert),
828            NativeAppEffect::EnterVisual(NativeVisualKind::Linewise),
829            NativeAppEffect::EnterSelect(NativeVisualKind::Blockwise),
830            NativeAppEffect::EnterSearch(NativeSearchDirection::Backward),
831            NativeAppEffect::SearchWordUnderCursor(NativeSearchDirection::Forward),
832            NativeAppEffect::JumpViewport(NativeViewportPos::Middle),
833            NativeAppEffect::ScrollCursorTo(NativeScrollPos::Center),
834            NativeAppEffect::HorizontalScroll(NativeHScroll::Columns { right: true }),
835            NativeAppEffect::HorizontalScroll(NativeHScroll::HalfScreen { right: false }),
836            NativeAppEffect::HorizontalScroll(NativeHScroll::CursorToEdge { end: true }),
837            NativeAppEffect::JoinLines { with_space: true },
838            NativeAppEffect::FindRepeat { reverse: false },
839            NativeAppEffect::OverwriteChar('z'),
840            NativeAppEffect::SetMark('a'),
841            NativeAppEffect::JumpToMarkLine('b'),
842            NativeAppEffect::JumpToMarkExact('c'),
843            NativeAppEffect::SelectRegister(NativeRegister::Named('q')),
844            NativeAppEffect::StartMacroRecord('m'),
845            NativeAppEffect::PlayMacro('m'),
846            NativeAppEffect::AbsorbOperatorPrefix(OperatorId(CommandId::new(17))),
847            NativeAppEffect::NavigatePane(NativePaneDirection::Right),
848            NativeAppEffect::GoToTab(3),
849            NativeAppEffect::MoveTab(2),
850            NativeAppEffect::NewTabAt("/a/b.rs".into()),
851            NativeAppEffect::TerminalSpawn(Some("bash".into())),
852            NativeAppEffect::TerminalSpawnInNewTab(None),
853            NativeAppEffect::CompletionAcceptThenInsert('x'),
854            NativeAppEffect::CompletionFilterToSource("gen:lsp-completion".into()),
855            NativeAppEffect::MultibufferExpand { delta: -2 },
856            NativeAppEffect::NarrowLines {
857                start_line: 3,
858                end_line: 9,
859            },
860            // Different values from `NarrowLines` above, so a decode that
861            // crossed the two arms can't pass.
862            NativeAppEffect::CreateFold {
863                start_line: 4,
864                end_line: 11,
865            },
866            NativeAppEffect::SearchTrigger {
867                query: "TODO".into(),
868            },
869        ] {
870            assert_round_trips(e);
871        }
872    }
873
874    /// A representative sample of the unit arms.
875    #[test]
876    fn app_effect_unit_arms_round_trip() {
877        for e in [
878            NativeAppEffect::Quit,
879            NativeAppEffect::MatchBracket,
880            NativeAppEffect::OpenLineBelow,
881            NativeAppEffect::Undo,
882            NativeAppEffect::Redo,
883            NativeAppEffect::PageDown,
884            NativeAppEffect::HalfPageDown,
885            NativeAppEffect::HalfPageUp,
886            NativeAppEffect::ExitVisual,
887            NativeAppEffect::SplitPaneVertical,
888            NativeAppEffect::OnlyPane,
889            NativeAppEffect::NextTab,
890            NativeAppEffect::CompletionAccept,
891            NativeAppEffect::DiffGet,
892            NativeAppEffect::DiffPut,
893            NativeAppEffect::TutorAdvance,
894            NativeAppEffect::NarrowWiden,
895            NativeAppEffect::SearchRefresh,
896            NativeAppEffect::PlayLastMacro,
897            NativeAppEffect::OpenFoldsRecursively,
898            NativeAppEffect::CloseFoldsRecursively,
899            NativeAppEffect::DeleteFoldsRecursively,
900        ] {
901            assert_round_trips(e);
902        }
903    }
904
905    /// The 4 `app-effect`-only helper enums round-trip every variant.
906    #[test]
907    fn app_effect_helper_enums_round_trip() {
908        for p in [
909            NativeViewportPos::Top,
910            NativeViewportPos::Middle,
911            NativeViewportPos::Bottom,
912        ] {
913            assert_eq!(p, NativeViewportPos::from_wit(p.to_wit().unwrap()).unwrap());
914        }
915        for p in [
916            NativeScrollPos::Top,
917            NativeScrollPos::Center,
918            NativeScrollPos::Bottom,
919        ] {
920            assert_eq!(p, NativeScrollPos::from_wit(p.to_wit().unwrap()).unwrap());
921        }
922        for d in [
923            NativePaneDirection::Left,
924            NativePaneDirection::Down,
925            NativePaneDirection::Up,
926            NativePaneDirection::Right,
927        ] {
928            assert_eq!(
929                d,
930                NativePaneDirection::from_wit(d.to_wit().unwrap()).unwrap()
931            );
932        }
933        for h in [
934            NativeHScroll::Columns { right: true },
935            NativeHScroll::HalfScreen { right: false },
936            NativeHScroll::CursorToEdge { end: true },
937        ] {
938            assert_eq!(h, NativeHScroll::from_wit(h.to_wit().unwrap()).unwrap());
939        }
940    }
941
942    /// OA.11a: a provider view's two argument slots have two owners, and the
943    /// boundary keeps them apart.
944    ///
945    /// `argument` is the root the HOST interprets; `scan-args` are the guest's
946    /// own vocabulary, which it does not. Round-tripping both together is what
947    /// pins that neither consumes the other — the failure this shape exists to
948    /// prevent is a command key landing in the root slot, where the opener
949    /// would turn it into a directory that does not exist and the scan would
950    /// silently cover nothing.
951    #[test]
952    fn a_provider_views_root_and_scan_args_do_not_consume_each_other() {
953        use lattice_grammar::args::{ArgValue, Args};
954
955        let native = NativeAppEffect::OpenProviderView {
956            provider: "agenda".to_string(),
957            args: Args::List(vec![
958                ArgValue::String("~/notes".to_string()),
959                ArgValue::String("waiting".to_string()),
960            ]),
961        };
962        let wit = native.to_wit().expect("to_wit");
963        let WitAppEffect::OpenProviderView(p) = &wit else {
964            panic!("wrong arm");
965        };
966        assert_eq!(p.argument.as_deref(), Some("~/notes"), "the host's slot");
967        assert_eq!(p.scan_args, vec!["waiting".to_string()], "the guest's");
968        assert_round_trips(native);
969
970        // Scan args with NO root: position 0 is empty rather than absent, so
971        // the list form stays positional and `~/notes` cannot be mistaken for
972        // a command key by arriving first.
973        let native = NativeAppEffect::OpenProviderView {
974            provider: "agenda".to_string(),
975            args: Args::List(vec![
976                ArgValue::String(String::new()),
977                ArgValue::String("waiting".to_string()),
978            ]),
979        };
980        let wit = native.to_wit().expect("to_wit");
981        let WitAppEffect::OpenProviderView(p) = &wit else {
982            panic!("wrong arm");
983        };
984        assert_eq!(
985            p.argument, None,
986            "an empty root is no root, not a root of \"\""
987        );
988        assert_eq!(p.scan_args, vec!["waiting".to_string()]);
989    }
990
991    /// OA.11a must not have moved the triggers that already existed. With no
992    /// scan args the mapping is what it was: `None` ⇄ `Args::None`, one string
993    /// ⇄ `Args::String`. `:org-agenda` and `:org-agenda ~/notes` are those two
994    /// cases, so this is the regression guard for every pre-existing caller.
995    #[test]
996    fn a_provider_view_with_no_scan_args_maps_exactly_as_before() {
997        use lattice_grammar::args::Args;
998
999        for (args, expect) in [
1000            (Args::None, None),
1001            (Args::String("~/notes".to_string()), Some("~/notes")),
1002        ] {
1003            let native = NativeAppEffect::OpenProviderView {
1004                provider: "agenda".to_string(),
1005                args,
1006            };
1007            let wit = native.to_wit().expect("to_wit");
1008            let WitAppEffect::OpenProviderView(p) = &wit else {
1009                panic!("wrong arm");
1010            };
1011            assert_eq!(p.argument.as_deref(), expect);
1012            assert!(
1013                p.scan_args.is_empty(),
1014                "no scan args means no scan args — not an empty string in a list"
1015            );
1016            assert_round_trips(native);
1017        }
1018    }
1019
1020    /// Only free text crosses. Stringifying an `Int` or an `Invocation` here
1021    /// would invent a spelling the guest never agreed to, so it is a typed
1022    /// error — the `NarrowTrigger` precedent, applied to the list form.
1023    #[test]
1024    fn a_non_string_provider_view_argument_is_a_typed_error() {
1025        use lattice_grammar::args::{ArgValue, Args};
1026
1027        let e = NativeAppEffect::OpenProviderView {
1028            provider: "agenda".to_string(),
1029            args: Args::List(vec![ArgValue::Int(7)]),
1030        };
1031        let err = e
1032            .to_wit()
1033            .expect_err("a non-string list argument must not cross");
1034        assert!(err.contains("free text"), "error says why: {err}");
1035    }
1036
1037    /// `NarrowTrigger` carries the recursive ex-command `Range`; it cannot cross
1038    /// yet and surfaces as a typed error, never a panic or lossy encoding.
1039    #[test]
1040    fn narrow_trigger_is_a_typed_error() {
1041        let e = NativeAppEffect::NarrowTrigger { range: None };
1042        let err = e.to_wit().expect_err("NarrowTrigger must not cross yet");
1043        assert!(
1044            err.contains("NarrowTrigger"),
1045            "error names the culprit: {err}"
1046        );
1047    }
1048}