1use std::path::PathBuf;
15
16use crate::WitBoundary;
17use crate::boundary::path_to_wit;
18use crate::lattice::plugin_host::types::{
19 AppliedEdit as WitAppliedEdit, ApplyEditPayload as WitApplyEditPayload,
20 CloseSessionDiffsPayload as WitCloseSessionDiffsPayload, ConfirmPayload as WitConfirmPayload,
21 DescribeCommandPayload as WitDescribeCommandPayload, DiffsplitPayload as WitDiffsplitPayload,
22 EchoLevel as WitEchoLevel, EchoPayload as WitEchoPayload, Edit as WitEdit,
23 EditDelta as WitEditDelta, EditKind as WitEditKind, Effect as WitEffect,
24 FileAnchor as WitFileAnchor, LspRequest as WitLspRequest, ModalState as WitModalState,
25 OpenBufferAtColumnPayload as WitOpenBufferAtColumnPayload,
26 OpenBufferAtPayload as WitOpenBufferAtPayload, OpenBufferPayload as WitOpenBufferPayload,
27 OpenPickerPayload as WitOpenPickerPayload, OpenPopupPayload as WitOpenPopupPayload,
28 OpenPromptPayload as WitOpenPromptPayload,
29 OpenSyntheticBufferPayload as WitOpenSyntheticBufferPayload,
30 OpenTransientPayload as WitOpenTransientPayload, PopupFocus as WitPopupFocus,
31 PopupPlacement as WitPopupPlacement, Position as WitPosition, QuitPayload as WitQuitPayload,
32 QuitScope as WitQuitScope, Range as WitRange, Register as WitRegister,
33 SearchDirection as WitSearchDirection, Selection as WitSelection,
34 SelectionSet as WitSelectionSet, SetLspLogLevelPayload as WitSetLspLogLevelPayload,
35 SpawnTerminalPayload as WitSpawnTerminalPayload, SubstitutePayload as WitSubstitutePayload,
36 SubstituteScope as WitSubstituteScope, Utf16Pos as WitUtf16Pos, VisualKind as WitVisualKind,
37 VisualMode as WitVisualMode, WriteToFilePayload as WitWriteToFilePayload,
38 YankKind as WitYankKind, YankPayload as WitYankPayload,
39};
40use lattice_core::BufferId;
41use lattice_core::buffer::AppliedEdit as NativeAppliedEdit;
42use lattice_core::ui::popup::{
43 PopupFocus as NativePopupFocus, PopupPlacement as NativePopupPlacement,
44};
45use lattice_grammar::app_effect::AppEffect as NativeAppEffect;
46use lattice_grammar::args::Args as NativeArgs;
47use lattice_grammar::effect::{
48 EchoLevel as NativeEchoLevel, Effect as NativeEffect, LspRequest as NativeLspRequest,
49 QuitScope as NativeQuitScope, SubstituteScope as NativeSubstituteScope,
50 Utf16Pos as NativeUtf16Pos, YankKind as NativeYankKind,
51};
52use lattice_grammar::modal::{
53 ModalState as NativeModalState, SearchDirection as NativeSearchDirection,
54 VisualKind as NativeVisualKind,
55};
56use lattice_grammar::register::Register as NativeRegister;
57use lattice_protocol::edit::{
58 Edit as NativeEdit, EditDelta as NativeEditDelta, EditKind as NativeEditKind,
59};
60use lattice_protocol::position::{Position as NativePosition, Range as NativeRange};
61use lattice_protocol::selection::{
62 Selection as NativeSelection, SelectionSet as NativeSelectionSet,
63 VisualMode as NativeVisualMode,
64};
65
66impl WitBoundary for NativePosition {
67 type Wit = WitPosition;
68 fn to_wit(&self) -> Result<WitPosition, String> {
69 Ok(WitPosition {
70 line: self.line,
71 byte: self.byte,
72 })
73 }
74 fn from_wit(w: WitPosition) -> Result<Self, String> {
75 Ok(NativePosition {
76 line: w.line,
77 byte: w.byte,
78 })
79 }
80}
81
82impl WitBoundary for NativeRange {
83 type Wit = WitRange;
84 fn to_wit(&self) -> Result<WitRange, String> {
85 Ok(WitRange {
86 start: self.start.to_wit()?,
87 end: self.end.to_wit()?,
88 })
89 }
90 fn from_wit(w: WitRange) -> Result<Self, String> {
91 Ok(NativeRange {
92 start: NativePosition::from_wit(w.start)?,
93 end: NativePosition::from_wit(w.end)?,
94 })
95 }
96}
97
98impl WitBoundary for NativeEditKind {
99 type Wit = WitEditKind;
100 fn to_wit(&self) -> Result<WitEditKind, String> {
101 Ok(match self {
102 NativeEditKind::Replace { text } => WitEditKind::Replace(text.clone()),
103 })
104 }
105 fn from_wit(w: WitEditKind) -> Result<Self, String> {
106 Ok(match w {
107 WitEditKind::Replace(text) => NativeEditKind::Replace { text },
108 })
109 }
110}
111
112impl WitBoundary for NativeEdit {
113 type Wit = WitEdit;
114 fn to_wit(&self) -> Result<WitEdit, String> {
115 Ok(WitEdit {
116 range: self.range.to_wit()?,
117 kind: self.kind.to_wit()?,
118 })
119 }
120 fn from_wit(w: WitEdit) -> Result<Self, String> {
121 Ok(NativeEdit {
122 range: NativeRange::from_wit(w.range)?,
123 kind: NativeEditKind::from_wit(w.kind)?,
124 })
125 }
126}
127
128impl WitBoundary for NativeEditDelta {
129 type Wit = WitEditDelta;
130 fn to_wit(&self) -> Result<WitEditDelta, String> {
131 Ok(WitEditDelta {
132 start_byte: self.start_byte,
133 old_end_byte: self.old_end_byte,
134 new_end_byte: self.new_end_byte,
135 start_position: self.start_position.to_wit()?,
136 old_end_position: self.old_end_position.to_wit()?,
137 new_end_position: self.new_end_position.to_wit()?,
138 })
139 }
140 fn from_wit(w: WitEditDelta) -> Result<Self, String> {
141 Ok(NativeEditDelta {
142 start_byte: w.start_byte,
143 old_end_byte: w.old_end_byte,
144 new_end_byte: w.new_end_byte,
145 start_position: NativePosition::from_wit(w.start_position)?,
146 old_end_position: NativePosition::from_wit(w.old_end_position)?,
147 new_end_position: NativePosition::from_wit(w.new_end_position)?,
148 })
149 }
150}
151
152impl WitBoundary for NativeAppliedEdit {
153 type Wit = WitAppliedEdit;
154 fn to_wit(&self) -> Result<WitAppliedEdit, String> {
155 Ok(WitAppliedEdit {
156 original_range: self.original_range.to_wit()?,
157 inserted_range: self.inserted_range.to_wit()?,
158 replaced_text: self.replaced_text.clone(),
159 inserted_text: self.inserted_text.clone(),
160 delta: self.delta.to_wit()?,
161 })
162 }
163 fn from_wit(w: WitAppliedEdit) -> Result<Self, String> {
164 Ok(NativeAppliedEdit {
165 original_range: NativeRange::from_wit(w.original_range)?,
166 inserted_range: NativeRange::from_wit(w.inserted_range)?,
167 replaced_text: w.replaced_text,
168 inserted_text: w.inserted_text,
169 delta: NativeEditDelta::from_wit(w.delta)?,
170 })
171 }
172}
173
174impl WitBoundary for NativeVisualMode {
175 type Wit = WitVisualMode;
176 fn to_wit(&self) -> Result<WitVisualMode, String> {
177 Ok(match self {
178 NativeVisualMode::Charwise => WitVisualMode::Charwise,
179 NativeVisualMode::Linewise => WitVisualMode::Linewise,
180 NativeVisualMode::Blockwise => WitVisualMode::Blockwise,
181 })
182 }
183 fn from_wit(w: WitVisualMode) -> Result<Self, String> {
184 Ok(match w {
185 WitVisualMode::Charwise => NativeVisualMode::Charwise,
186 WitVisualMode::Linewise => NativeVisualMode::Linewise,
187 WitVisualMode::Blockwise => NativeVisualMode::Blockwise,
188 })
189 }
190}
191
192impl WitBoundary for NativeSelection {
193 type Wit = WitSelection;
194 fn to_wit(&self) -> Result<WitSelection, String> {
195 Ok(WitSelection {
196 anchor: self.anchor.to_wit()?,
197 head: self.head.to_wit()?,
198 visual: self.visual.map(|v| v.to_wit()).transpose()?,
199 })
200 }
201 fn from_wit(w: WitSelection) -> Result<Self, String> {
202 Ok(NativeSelection {
203 anchor: NativePosition::from_wit(w.anchor)?,
204 head: NativePosition::from_wit(w.head)?,
205 visual: w.visual.map(NativeVisualMode::from_wit).transpose()?,
206 })
207 }
208}
209
210impl WitBoundary for NativeSelectionSet {
211 type Wit = WitSelectionSet;
212 fn to_wit(&self) -> Result<WitSelectionSet, String> {
213 Ok(WitSelectionSet {
214 selections: self
215 .all()
216 .iter()
217 .map(WitBoundary::to_wit)
218 .collect::<Result<Vec<_>, _>>()?,
219 primary: self.primary_index() as u32,
220 })
221 }
222 fn from_wit(w: WitSelectionSet) -> Result<Self, String> {
223 let selections = w
224 .selections
225 .into_iter()
226 .map(NativeSelection::from_wit)
227 .collect::<Result<Vec<_>, _>>()?;
228 Ok(NativeSelectionSet::from_parts(
229 selections,
230 w.primary as usize,
231 ))
232 }
233}
234
235impl WitBoundary for NativeVisualKind {
236 type Wit = WitVisualKind;
237 fn to_wit(&self) -> Result<WitVisualKind, String> {
238 Ok(match self {
239 NativeVisualKind::Charwise => WitVisualKind::Charwise,
240 NativeVisualKind::Linewise => WitVisualKind::Linewise,
241 NativeVisualKind::Blockwise => WitVisualKind::Blockwise,
242 })
243 }
244 fn from_wit(w: WitVisualKind) -> Result<Self, String> {
245 Ok(match w {
246 WitVisualKind::Charwise => NativeVisualKind::Charwise,
247 WitVisualKind::Linewise => NativeVisualKind::Linewise,
248 WitVisualKind::Blockwise => NativeVisualKind::Blockwise,
249 })
250 }
251}
252
253impl WitBoundary for NativeSearchDirection {
254 type Wit = WitSearchDirection;
255 fn to_wit(&self) -> Result<WitSearchDirection, String> {
256 Ok(match self {
257 NativeSearchDirection::Forward => WitSearchDirection::Forward,
258 NativeSearchDirection::Backward => WitSearchDirection::Backward,
259 })
260 }
261 fn from_wit(w: WitSearchDirection) -> Result<Self, String> {
262 Ok(match w {
263 WitSearchDirection::Forward => NativeSearchDirection::Forward,
264 WitSearchDirection::Backward => NativeSearchDirection::Backward,
265 })
266 }
267}
268
269impl WitBoundary for NativeModalState {
270 type Wit = WitModalState;
271 fn to_wit(&self) -> Result<WitModalState, String> {
272 Ok(match self {
273 NativeModalState::Normal => WitModalState::Normal,
274 NativeModalState::Insert => WitModalState::Insert,
275 NativeModalState::Visual(k) => WitModalState::Visual(k.to_wit()?),
276 NativeModalState::Select(k) => WitModalState::Select(k.to_wit()?),
277 NativeModalState::OperatorPending => WitModalState::OperatorPending,
278 NativeModalState::Command => WitModalState::Command,
279 NativeModalState::Search(d) => WitModalState::Search(d.to_wit()?),
280 NativeModalState::Replace => WitModalState::Replace,
281 NativeModalState::Prompt => WitModalState::Prompt,
282 })
283 }
284 fn from_wit(w: WitModalState) -> Result<Self, String> {
285 Ok(match w {
286 WitModalState::Normal => NativeModalState::Normal,
287 WitModalState::Insert => NativeModalState::Insert,
288 WitModalState::Visual(k) => NativeModalState::Visual(NativeVisualKind::from_wit(k)?),
289 WitModalState::Select(k) => NativeModalState::Select(NativeVisualKind::from_wit(k)?),
290 WitModalState::OperatorPending => NativeModalState::OperatorPending,
291 WitModalState::Command => NativeModalState::Command,
292 WitModalState::Search(d) => {
293 NativeModalState::Search(NativeSearchDirection::from_wit(d)?)
294 }
295 WitModalState::Replace => NativeModalState::Replace,
296 WitModalState::Prompt => NativeModalState::Prompt,
297 })
298 }
299}
300
301impl WitBoundary for NativeRegister {
302 type Wit = WitRegister;
303 fn to_wit(&self) -> Result<WitRegister, String> {
304 Ok(match self {
305 NativeRegister::Unnamed => WitRegister::Unnamed,
306 NativeRegister::Named(c) => WitRegister::Named(*c),
307 NativeRegister::System => WitRegister::System,
308 NativeRegister::BlackHole => WitRegister::BlackHole,
309 NativeRegister::Expression => WitRegister::Expression,
310 NativeRegister::ReadOnly(c) => WitRegister::ReadOnly(*c),
311 NativeRegister::Numbered(n) => WitRegister::Numbered(*n),
312 })
313 }
314 fn from_wit(w: WitRegister) -> Result<Self, String> {
315 Ok(match w {
316 WitRegister::Unnamed => NativeRegister::Unnamed,
317 WitRegister::Named(c) => NativeRegister::Named(c),
318 WitRegister::System => NativeRegister::System,
319 WitRegister::BlackHole => NativeRegister::BlackHole,
320 WitRegister::Expression => NativeRegister::Expression,
321 WitRegister::ReadOnly(c) => NativeRegister::ReadOnly(c),
322 WitRegister::Numbered(n) => NativeRegister::Numbered(n),
323 })
324 }
325}
326
327impl WitBoundary for NativeYankKind {
328 type Wit = WitYankKind;
329 fn to_wit(&self) -> Result<WitYankKind, String> {
330 Ok(match self {
331 NativeYankKind::Charwise => WitYankKind::Charwise,
332 NativeYankKind::Linewise => WitYankKind::Linewise,
333 NativeYankKind::Blockwise => WitYankKind::Blockwise,
334 })
335 }
336 fn from_wit(w: WitYankKind) -> Result<Self, String> {
337 Ok(match w {
338 WitYankKind::Charwise => NativeYankKind::Charwise,
339 WitYankKind::Linewise => NativeYankKind::Linewise,
340 WitYankKind::Blockwise => NativeYankKind::Blockwise,
341 })
342 }
343}
344
345impl WitBoundary for NativeQuitScope {
348 type Wit = WitQuitScope;
349 fn to_wit(&self) -> Result<WitQuitScope, String> {
350 Ok(match self {
351 NativeQuitScope::Pane => WitQuitScope::Pane,
352 NativeQuitScope::All => WitQuitScope::All,
353 })
354 }
355 fn from_wit(w: WitQuitScope) -> Result<Self, String> {
356 Ok(match w {
357 WitQuitScope::Pane => NativeQuitScope::Pane,
358 WitQuitScope::All => NativeQuitScope::All,
359 })
360 }
361}
362
363impl WitBoundary for NativeEchoLevel {
364 type Wit = WitEchoLevel;
365 fn to_wit(&self) -> Result<WitEchoLevel, String> {
366 Ok(match self {
367 NativeEchoLevel::Trace => WitEchoLevel::Trace,
368 NativeEchoLevel::Debug => WitEchoLevel::Debug,
369 NativeEchoLevel::Info => WitEchoLevel::Info,
370 NativeEchoLevel::Warn => WitEchoLevel::Warn,
371 NativeEchoLevel::Error => WitEchoLevel::Error,
372 })
373 }
374 fn from_wit(w: WitEchoLevel) -> Result<Self, String> {
375 Ok(match w {
376 WitEchoLevel::Trace => NativeEchoLevel::Trace,
377 WitEchoLevel::Debug => NativeEchoLevel::Debug,
378 WitEchoLevel::Info => NativeEchoLevel::Info,
379 WitEchoLevel::Warn => NativeEchoLevel::Warn,
380 WitEchoLevel::Error => NativeEchoLevel::Error,
381 })
382 }
383}
384
385impl WitBoundary for NativeSubstituteScope {
386 type Wit = WitSubstituteScope;
387 fn to_wit(&self) -> Result<WitSubstituteScope, String> {
388 Ok(match self {
389 NativeSubstituteScope::CurrentLine => WitSubstituteScope::CurrentLine,
390 NativeSubstituteScope::Whole => WitSubstituteScope::Whole,
391 })
392 }
393 fn from_wit(w: WitSubstituteScope) -> Result<Self, String> {
394 Ok(match w {
395 WitSubstituteScope::CurrentLine => NativeSubstituteScope::CurrentLine,
396 WitSubstituteScope::Whole => NativeSubstituteScope::Whole,
397 })
398 }
399}
400
401impl WitBoundary for NativeUtf16Pos {
402 type Wit = WitUtf16Pos;
403 fn to_wit(&self) -> Result<WitUtf16Pos, String> {
404 Ok(WitUtf16Pos {
405 line: self.line,
406 col: self.col,
407 })
408 }
409 fn from_wit(w: WitUtf16Pos) -> Result<Self, String> {
410 Ok(NativeUtf16Pos {
411 line: w.line,
412 col: w.col,
413 })
414 }
415}
416
417impl WitBoundary for NativeLspRequest {
418 type Wit = WitLspRequest;
419 fn to_wit(&self) -> Result<WitLspRequest, String> {
420 Ok(match self {
421 NativeLspRequest::Hover => WitLspRequest::Hover,
422 NativeLspRequest::Definition => WitLspRequest::Definition,
423 NativeLspRequest::Declaration => WitLspRequest::Declaration,
424 NativeLspRequest::TypeDefinition => WitLspRequest::TypeDefinition,
425 NativeLspRequest::Implementation => WitLspRequest::Implementation,
426 NativeLspRequest::References => WitLspRequest::References,
427 NativeLspRequest::FollowLink => WitLspRequest::FollowLink,
428 NativeLspRequest::ReferencesView
435 | NativeLspRequest::ReferencesViewRefresh
436 | NativeLspRequest::ReferencesToErrorList => {
437 return Err(
438 "LspRequest::ReferencesView (`:lsp-references`) has no WIT mirror yet; \
439 adding one is a versioned plugin-API change (lsp-architecture.md §17)"
440 .to_string(),
441 );
442 }
443 })
444 }
445 fn from_wit(w: WitLspRequest) -> Result<Self, String> {
446 Ok(match w {
447 WitLspRequest::Hover => NativeLspRequest::Hover,
448 WitLspRequest::Definition => NativeLspRequest::Definition,
449 WitLspRequest::Declaration => NativeLspRequest::Declaration,
450 WitLspRequest::TypeDefinition => NativeLspRequest::TypeDefinition,
451 WitLspRequest::Implementation => NativeLspRequest::Implementation,
452 WitLspRequest::References => NativeLspRequest::References,
453 WitLspRequest::FollowLink => NativeLspRequest::FollowLink,
454 })
455 }
456}
457
458fn opt_path_to_wit(path: &Option<PathBuf>) -> Result<Option<String>, String> {
461 path.as_ref().map(|p| path_to_wit(p)).transpose()
462}
463
464fn opt_path_from_wit(path: Option<String>) -> Option<PathBuf> {
465 path.map(PathBuf::from)
466}
467
468impl WitBoundary for NativeEffect {
471 type Wit = Vec<WitEffect>;
479
480 fn to_wit(&self) -> Result<Vec<WitEffect>, String> {
481 let mut out = Vec::new();
482 flatten_effect(self, &mut out)?;
483 Ok(out)
484 }
485
486 fn from_wit(wit: Vec<WitEffect>) -> Result<Self, String> {
487 let mut effects = wit
488 .into_iter()
489 .map(effect_from_wit)
490 .collect::<Result<Vec<_>, _>>()?;
491 Ok(if effects.is_empty() {
492 NativeEffect::None
493 } else if effects.len() == 1 {
494 effects.remove(0)
495 } else {
496 NativeEffect::Many(effects)
497 })
498 }
499}
500
501fn flatten_effect(e: &NativeEffect, out: &mut Vec<WitEffect>) -> Result<(), String> {
505 match e {
506 NativeEffect::Many(list) => {
507 for sub in list {
508 flatten_effect(sub, out)?;
509 }
510 Ok(())
511 }
512 atom => {
513 out.push(effect_to_wit(atom)?);
514 Ok(())
515 }
516 }
517}
518
519fn effect_to_wit(e: &NativeEffect) -> Result<WitEffect, String> {
523 Ok(match e {
524 NativeEffect::None => WitEffect::None,
525 NativeEffect::Declined => WitEffect::Declined,
526 NativeEffect::Edits(edits) => WitEffect::Edits(
527 edits
528 .iter()
529 .map(WitBoundary::to_wit)
530 .collect::<Result<Vec<_>, _>>()?,
531 ),
532 NativeEffect::ApplyEdit {
533 target,
534 edit,
535 cursor,
536 } => WitEffect::ApplyEdit(WitApplyEditPayload {
537 target: target.0,
538 edit: edit.to_wit()?,
539 cursor: cursor.as_ref().map(|p| p.to_wit()).transpose()?,
540 }),
541 NativeEffect::CursorMove(pos) => {
542 let p = *pos;
543 WitEffect::SelectionChange(
544 NativeSelectionSet::single(NativeSelection {
545 anchor: p,
546 head: p,
547 visual: None,
548 })
549 .to_wit()?,
550 )
551 }
552 NativeEffect::CursorMoveIn { .. } => {
559 return Err(
560 "Effect::CursorMoveIn addresses a BufferId; it crosses with the buffer-handle \
561 mirror, and must not degrade to a target-less cursor move"
562 .to_string(),
563 );
564 }
565 NativeEffect::WriteToFile {
566 path,
567 anchor,
568 text,
569 cut,
570 create_parents,
571 save,
572 } => WitEffect::WriteToFile(WitWriteToFilePayload {
573 path: path
577 .to_str()
578 .ok_or_else(|| {
579 format!("Effect::WriteToFile path is not UTF-8: {}", path.display())
580 })?
581 .to_string(),
582 anchor: anchor_to_wit(*anchor),
583 text: text.clone(),
584 cut: cut.as_ref().map(|r| r.to_wit()).transpose()?,
585 create_parents: *create_parents,
586 save: *save,
587 }),
588 NativeEffect::SelectionChange(set) => WitEffect::SelectionChange(set.to_wit()?),
589 NativeEffect::Yank {
590 register,
591 content,
592 kind,
593 explicit_yank,
594 } => WitEffect::Yank(WitYankPayload {
595 register: register.to_wit()?,
596 content: content.clone(),
597 kind: kind.to_wit()?,
598 explicit_yank: *explicit_yank,
599 }),
600 NativeEffect::EnterMode(state) => WitEffect::EnterMode(state.to_wit()?),
601 NativeEffect::SaveBuffer { path } => WitEffect::SaveBuffer(opt_path_to_wit(path)?),
602 NativeEffect::QuitEditor { force, scope } => WitEffect::QuitEditor(WitQuitPayload {
603 force: *force,
604 scope: scope.to_wit()?,
605 }),
606 NativeEffect::OpenBuffer { path, force } => WitEffect::OpenBuffer(WitOpenBufferPayload {
607 path: opt_path_to_wit(path)?,
608 force: *force,
609 }),
610 NativeEffect::OpenBufferAt {
611 path,
612 position,
613 force,
614 content,
615 activate_minor,
616 } => WitEffect::OpenBufferAt(WitOpenBufferAtPayload {
617 path: opt_path_to_wit(path)?,
618 position: position.to_wit()?,
619 force: *force,
620 content: content.clone(),
621 activate_minor: activate_minor.clone(),
622 }),
623 NativeEffect::OpenExternalUri { uri } => WitEffect::OpenExternalUri(uri.clone()),
624 NativeEffect::OpenBufferAtColumn {
625 path,
626 column,
627 force,
628 } => WitEffect::OpenBufferAtColumn(WitOpenBufferAtColumnPayload {
629 path: opt_path_to_wit(path)?,
630 column: column.map(|c| c.to_wit()).transpose()?,
631 force: *force,
632 }),
633 NativeEffect::SpawnTerminal {
634 cwd,
635 cmd_line,
636 env,
637 activate_minor,
638 } => WitEffect::SpawnTerminal(WitSpawnTerminalPayload {
639 cwd: opt_path_to_wit(cwd)?,
640 cmd_line: cmd_line.clone(),
641 env: env.clone(),
642 activate_minor: activate_minor.clone(),
643 }),
644 NativeEffect::TerminalInput(bytes) => WitEffect::TerminalInput(bytes.clone()),
645 NativeEffect::SetOption { spec } => WitEffect::SetOption(spec.clone()),
646 NativeEffect::SetLocalOption { spec } => WitEffect::SetLocalOption(spec.clone()),
647 NativeEffect::SetGlobalOption { spec } => WitEffect::SetGlobalOption(spec.clone()),
648 NativeEffect::ClearSearchHighlight => WitEffect::ClearSearchHighlight,
649 NativeEffect::SetColorscheme(name) => WitEffect::SetColorscheme(name.clone()),
650 NativeEffect::Echo { level, text } => WitEffect::Echo(WitEchoPayload {
651 level: level.to_wit()?,
652 text: text.clone(),
653 }),
654 NativeEffect::ShowDiagnosticsPopup { lines } => {
655 WitEffect::ShowDiagnosticsPopup(lines.clone())
656 }
657 NativeEffect::Lsp(req) => WitEffect::Lsp(req.to_wit()?),
658 NativeEffect::EchoRegisters => WitEffect::EchoRegisters,
659 NativeEffect::EchoMarks => WitEffect::EchoMarks,
660 NativeEffect::Substitute {
661 scope,
662 pattern,
663 replacement,
664 global,
665 } => WitEffect::Substitute(WitSubstitutePayload {
666 scope: scope.to_wit()?,
667 pattern: pattern.clone(),
668 replacement: replacement.clone(),
669 global: *global,
670 }),
671 NativeEffect::Global { .. } => {
672 return Err(
673 "Effect::Global carries a Box<CommandInvocation>; it crosses with the \
674 command mirror (fragment §4.1)"
675 .to_string(),
676 );
677 }
678 NativeEffect::DeleteCurrentLine => WitEffect::DeleteCurrentLine,
679 NativeEffect::DescribeCommand { name, anchor } => {
680 WitEffect::DescribeCommand(WitDescribeCommandPayload {
681 name: name.clone(),
682 anchor: anchor.clone(),
683 })
684 }
685 NativeEffect::DescribeBuffer => WitEffect::DescribeBuffer,
686 NativeEffect::Apropos { pattern } => WitEffect::Apropos(pattern.clone()),
687 NativeEffect::DescribeKey { chord } => WitEffect::DescribeKey(chord.clone()),
688 NativeEffect::ListKeymap => WitEffect::ListKeymap,
689 NativeEffect::BufferNext => WitEffect::BufferNext,
690 NativeEffect::BufferPrev => WitEffect::BufferPrev,
691 NativeEffect::ListBuffers => WitEffect::ListBuffers,
692 NativeEffect::OpenBufferPicker => WitEffect::OpenBufferPicker,
693 NativeEffect::OpenPicker {
694 source,
695 args,
696 root,
697 fill_action,
698 query,
699 } => WitEffect::OpenPicker(WitOpenPickerPayload {
700 source: source.clone(),
701 args: args.clone(),
702 root: opt_path_to_wit(root)?,
703 fill_action: fill_action.clone(),
704 query: query.clone(),
705 }),
706 NativeEffect::BufferDelete { force } => WitEffect::BufferDelete(*force),
707 NativeEffect::FocusBuffer(id) => WitEffect::FocusBuffer(*id),
708 NativeEffect::InvokeCommand { id, args } => {
709 WitEffect::InvokeCommand(crate::lattice::plugin_host::types::CommandRef {
710 id: id.clone(),
711 args: args.to_wit()?,
712 })
713 }
714 NativeEffect::OpenFileTree { root } => WitEffect::OpenFileTree(opt_path_to_wit(root)?),
715 NativeEffect::CloseFileTree => WitEffect::CloseFileTree,
716 NativeEffect::OpenOil { dir } => WitEffect::OpenOil(opt_path_to_wit(dir)?),
717 NativeEffect::DescribeOption { name } => WitEffect::DescribeOption(name.clone()),
718 NativeEffect::DescribeElement { name } => WitEffect::DescribeElement(name.clone()),
719 NativeEffect::ListOptions => WitEffect::ListOptions,
720 NativeEffect::DescribePluginApi { seam } => WitEffect::DescribePluginApi(seam.clone()),
721 NativeEffect::ListPluginApis => WitEffect::ListPluginApis,
722 NativeEffect::ExportPluginApi { format } => WitEffect::ExportPluginApi(format.clone()),
723 NativeEffect::ListCommands => WitEffect::ListCommands,
724 NativeEffect::DescribePlugin { name } => WitEffect::DescribePlugin(name.clone()),
725 NativeEffect::ListPlugins => WitEffect::ListPlugins,
726 NativeEffect::OpenHover { markdown } => WitEffect::OpenHover(markdown.clone()),
727 NativeEffect::DismissPopup => WitEffect::DismissPopup,
728 NativeEffect::DismissPopupNamed { name } => WitEffect::DismissPopupNamed(name.clone()),
729 NativeEffect::KillBuffer => {
732 return Err(
733 "Effect::KillBuffer has no WIT mirror yet — native-only until the \
734 plugin-API slice adds it"
735 .to_string(),
736 );
737 }
738 NativeEffect::BuryBuffer => {
739 return Err(
745 "Effect::BuryBuffer has no WIT mirror yet — native-only until the \
746 plugin-API slice adds it"
747 .to_string(),
748 );
749 }
750 NativeEffect::OpenPopup {
751 name,
752 mode_id,
753 placement,
754 focus,
755 } => WitEffect::OpenPopup(WitOpenPopupPayload {
756 name: name.clone(),
757 mode_id: mode_id.clone(),
758 placement: match placement {
759 NativePopupPlacement::Centered => WitPopupPlacement::Centered,
760 NativePopupPlacement::CursorAnchored => WitPopupPlacement::CursorAnchored,
761 NativePopupPlacement::MinibufferBand => WitPopupPlacement::MinibufferBand,
762 },
763 focus: match focus {
764 NativePopupFocus::Steal => WitPopupFocus::Steal,
765 NativePopupFocus::Passive => WitPopupFocus::Passive,
766 },
767 }),
768 NativeEffect::OpenHelpTopic { topic } => WitEffect::OpenHelpTopic(topic.clone()),
769 NativeEffect::ListDiagnostics => WitEffect::ListDiagnostics,
770 NativeEffect::NextDiagnostic => WitEffect::NextDiagnostic,
771 NativeEffect::PrevDiagnostic => WitEffect::PrevDiagnostic,
772 NativeEffect::OpenLspLog { server_id } => WitEffect::OpenLspLog(server_id.clone()),
773 NativeEffect::OpenMessages => WitEffect::OpenMessages,
774 NativeEffect::OpenDashboard => WitEffect::OpenDashboard,
775 NativeEffect::ToggleLspTrace { server_id } => WitEffect::ToggleLspTrace(server_id.clone()),
776 NativeEffect::OpenLspTraceLog { server_id } => {
777 WitEffect::OpenLspTraceLog(server_id.clone())
778 }
779 NativeEffect::LspStatus => WitEffect::LspStatus,
780 NativeEffect::LspDiagnosticsToErrorList => {
786 return Err(
787 "Effect::LspDiagnosticsToErrorList feeds core error state from the native LSP \
788 subsystem; its plugin (WIT) surface is deferred with AppEffect::SetErrorList \
789 (error-list.md §3.2)"
790 .to_string(),
791 );
792 }
793 NativeEffect::LspServerLogListing => WitEffect::LspServerLogListing,
794 NativeEffect::LspRestart { server_id } => WitEffect::LspRestart(server_id.clone()),
795 NativeEffect::LspProgressCancel { server_id } => {
796 WitEffect::LspProgressCancel(server_id.clone())
797 }
798 NativeEffect::LspExpandRegion => WitEffect::LspExpandRegion,
799 NativeEffect::LspShrinkRegion => WitEffect::LspShrinkRegion,
800 NativeEffect::SetLspLogLevel { server_id, level } => {
801 WitEffect::SetLspLogLevel(WitSetLspLogLevelPayload {
802 server_id: server_id.clone(),
803 level: level.clone(),
804 })
805 }
806 NativeEffect::LspLogClear { server_id } => WitEffect::LspLogClear(server_id.clone()),
807 NativeEffect::LspDocumentSymbol => WitEffect::LspDocumentSymbol,
808 NativeEffect::LspWorkspaceSymbol { query } => WitEffect::LspWorkspaceSymbol(query.clone()),
809 NativeEffect::LspIncomingCalls => WitEffect::LspIncomingCalls,
810 NativeEffect::LspOutgoingCalls => WitEffect::LspOutgoingCalls,
811 NativeEffect::LspSupertypes => WitEffect::LspSupertypes,
812 NativeEffect::LspSubtypes => WitEffect::LspSubtypes,
813 NativeEffect::LspMoniker => WitEffect::LspMoniker,
814 NativeEffect::LspCodeLens => WitEffect::LspCodeLens,
815 NativeEffect::LspColorPresentation => WitEffect::LspColorPresentation,
816 NativeEffect::Format => {
822 return Err("Effect::Format has no WIT mirror yet (IN.8b)".to_string());
823 }
824 NativeEffect::LspFormat => WitEffect::LspFormat,
825 NativeEffect::LspFormatRange => WitEffect::LspFormatRange,
826 NativeEffect::LspSignatureHelp => WitEffect::LspSignatureHelp,
827 NativeEffect::LspComplete => WitEffect::LspComplete,
828 NativeEffect::LspRename { new_name } => WitEffect::LspRename(new_name.clone()),
829 NativeEffect::LspCodeAction => WitEffect::LspCodeAction,
830 NativeEffect::ExpandSnippet { replace_range } => {
831 WitEffect::ExpandSnippet(replace_range.to_wit()?)
832 }
833 NativeEffect::ReloadSnippets => WitEffect::ReloadSnippets,
834 NativeEffect::DescribeEvents => WitEffect::DescribeEvents,
835 NativeEffect::DescribeDiff => WitEffect::DescribeDiff,
836 NativeEffect::DiffOpen => WitEffect::DiffOpen,
837 NativeEffect::DiffOff { force } => WitEffect::DiffOff(*force),
838 NativeEffect::Diffthis => WitEffect::Diffthis,
839 NativeEffect::Diffsplit { path, remote } => WitEffect::Diffsplit(WitDiffsplitPayload {
840 path: path_to_wit(path)?,
841 remote: opt_path_to_wit(remote)?,
842 }),
843 NativeEffect::DiffGetCmd { target } => WitEffect::DiffGetCmd(*target),
844 NativeEffect::DiffPutCmd { target } => WitEffect::DiffPutCmd(*target),
845 NativeEffect::DiffAccept => WitEffect::DiffAccept,
846 NativeEffect::DiffReject => WitEffect::DiffReject,
847 NativeEffect::DiffAcceptAll => WitEffect::DiffAcceptAll,
848 NativeEffect::DiffRejectAll => WitEffect::DiffRejectAll,
849 NativeEffect::CloseSessionDiffs {
850 origin_session,
851 tab_name,
852 } => WitEffect::CloseSessionDiffs(WitCloseSessionDiffsPayload {
853 origin_session: *origin_session,
854 tab_name: tab_name.clone(),
855 }),
856 NativeEffect::CloseAllSessionDiffs { origin_session } => {
857 WitEffect::CloseAllSessionDiffs(*origin_session)
858 }
859 NativeEffect::NextHunk => WitEffect::NextHunk,
860 NativeEffect::PrevHunk => WitEffect::PrevHunk,
861 NativeEffect::DescribeEvent { name } => WitEffect::DescribeEvent(name.clone()),
862 NativeEffect::ListModes => WitEffect::ListModes,
863 NativeEffect::DescribeMode { name } => WitEffect::DescribeMode(name.clone()),
864 NativeEffect::DescribeActiveModes => WitEffect::DescribeActiveModes,
865 NativeEffect::DescribeActiveBindings => WitEffect::DescribeActiveBindings,
866 NativeEffect::DescribeOptionResolution { name } => {
867 WitEffect::DescribeOptionResolution(name.clone())
868 }
869 NativeEffect::Customize { name } => WitEffect::Customize(name.clone()),
870 NativeEffect::Tutor { lesson } => WitEffect::Tutor(*lesson),
871 NativeEffect::ToggleMode { mode_name } => WitEffect::ToggleMode(mode_name.clone()),
872 NativeEffect::AppAction(app) => WitEffect::AppAction(app.to_wit()?),
875 NativeEffect::RecordJump => WitEffect::RecordJump,
876 NativeEffect::Confirm {
881 prompt,
882 yes_action,
883 args,
884 } => WitEffect::Confirm(WitConfirmPayload {
885 prompt: prompt.clone(),
886 yes_action: yes_action.clone(),
887 args: args.to_wit()?,
888 }),
889 NativeEffect::OpenPrompt {
898 prompt,
899 initial,
900 on_submit_action,
901 buffer_name,
902 } => WitEffect::OpenPrompt(WitOpenPromptPayload {
903 prompt: prompt.clone(),
904 initial: initial.clone(),
905 on_submit_action: on_submit_action.clone(),
906 buffer_name: buffer_name.clone(),
907 }),
908 NativeEffect::OpenTransient { source, args } => {
909 WitEffect::OpenTransient(WitOpenTransientPayload {
910 source: source.clone(),
911 args: args.to_wit()?,
912 })
913 }
914 NativeEffect::ChangeDir(_) => {
919 return Err(
920 "Effect::ChangeDir is host-only (`:cd` acts on the editor process)".to_string(),
921 );
922 }
923 NativeEffect::PrintWorkingDir => {
924 return Err("Effect::PrintWorkingDir is host-only (`:pwd`)".to_string());
925 }
926 NativeEffect::PrintProjectRoot => {
931 return Err("Effect::PrintProjectRoot is host-only (`:project-root`)".to_string());
932 }
933 NativeEffect::ListErrors => {
934 return Err(
935 "Effect::ListErrors is host-only (`:clist` opens a host-owned picker)".to_string(),
936 );
937 }
938 NativeEffect::OpenAiLog { session } => WitEffect::OpenAiLog(session.clone()),
939 NativeEffect::OpenSyntheticBuffer { name, mode_id, .. } => {
940 WitEffect::OpenSyntheticBuffer(WitOpenSyntheticBufferPayload {
941 name: name.clone(),
942 mode_id: mode_id.clone(),
943 content: None,
948 cursor: None,
949 activate_minor: None,
950 })
951 }
952 NativeEffect::OpenSyntheticBufferAt { .. } => {
959 return Err("Effect::OpenSyntheticBufferAt has no WIT mirror yet \
960 (native emitters only; add a payload to wit/types.wit \
961 when a plugin needs it)"
962 .to_string());
963 }
964 NativeEffect::OpenInTarget { .. } => {
969 return Err("Effect::OpenInTarget has no WIT mirror yet \
970 (host-only pane targeting; add a payload to wit/types.wit \
971 when a plugin needs to open in a split)"
972 .to_string());
973 }
974 NativeEffect::OilNavigate { .. } => {
978 return Err("Effect::OilNavigate has no WIT mirror yet \
979 (host-only oil re-list)"
980 .to_string());
981 }
982 NativeEffect::FileTreeToggle { .. } => {
983 return Err("Effect::FileTreeToggle has no WIT mirror yet \
984 (host-only file-tree expand/collapse)"
985 .to_string());
986 }
987 NativeEffect::Many(_) => {
988 return Err(
989 "Effect::Many is flattened to list<effect> at the boundary and must not \
990 reach effect_to_wit"
991 .to_string(),
992 );
993 }
994 })
995}
996
997fn anchor_to_wit(a: lattice_grammar::FileAnchor) -> WitFileAnchor {
1002 match a {
1003 lattice_grammar::FileAnchor::End => WitFileAnchor::End,
1004 lattice_grammar::FileAnchor::Start => WitFileAnchor::Start,
1005 lattice_grammar::FileAnchor::Line(n) => WitFileAnchor::Line(n),
1006 }
1007}
1008
1009fn anchor_from_wit(a: WitFileAnchor) -> lattice_grammar::FileAnchor {
1010 match a {
1011 WitFileAnchor::End => lattice_grammar::FileAnchor::End,
1012 WitFileAnchor::Start => lattice_grammar::FileAnchor::Start,
1013 WitFileAnchor::Line(n) => lattice_grammar::FileAnchor::Line(n),
1014 }
1015}
1016
1017fn effect_from_wit(w: WitEffect) -> Result<NativeEffect, String> {
1018 Ok(match w {
1019 WitEffect::None => NativeEffect::None,
1020 WitEffect::Declined => NativeEffect::Declined,
1021 WitEffect::Edits(edits) => NativeEffect::Edits(
1022 edits
1023 .into_iter()
1024 .map(NativeAppliedEdit::from_wit)
1025 .collect::<Result<Vec<_>, _>>()?,
1026 ),
1027 WitEffect::WriteToFile(p) => NativeEffect::WriteToFile {
1031 path: std::path::PathBuf::from(p.path),
1032 anchor: anchor_from_wit(p.anchor),
1033 text: p.text,
1034 cut: p.cut.map(NativeRange::from_wit).transpose()?,
1035 create_parents: p.create_parents,
1036 save: p.save,
1037 },
1038 WitEffect::ApplyEdit(p) => NativeEffect::ApplyEdit {
1039 target: BufferId(p.target),
1040 edit: NativeEdit::from_wit(p.edit)?,
1041 cursor: p.cursor.map(NativePosition::from_wit).transpose()?,
1042 },
1043 WitEffect::SelectionChange(set) => {
1044 NativeEffect::SelectionChange(NativeSelectionSet::from_wit(set)?)
1045 }
1046 WitEffect::CursorMove(pos) => NativeEffect::CursorMove(NativePosition::from_wit(pos)?),
1047 WitEffect::Confirm(p) => NativeEffect::Confirm {
1052 prompt: p.prompt,
1053 yes_action: p.yes_action,
1054 args: NativeArgs::from_wit(p.args)?,
1055 },
1056 WitEffect::OpenTransient(p) => NativeEffect::OpenTransient {
1060 source: p.source,
1061 args: lattice_grammar::Args::from_wit(p.args)?,
1062 },
1063 WitEffect::OpenPrompt(p) => NativeEffect::OpenPrompt {
1064 prompt: p.prompt,
1065 initial: p.initial,
1066 on_submit_action: p.on_submit_action,
1067 buffer_name: p.buffer_name,
1068 },
1069 WitEffect::Yank(p) => NativeEffect::Yank {
1070 register: NativeRegister::from_wit(p.register)?,
1071 content: p.content,
1072 kind: NativeYankKind::from_wit(p.kind)?,
1073 explicit_yank: p.explicit_yank,
1074 },
1075 WitEffect::EnterMode(state) => NativeEffect::EnterMode(NativeModalState::from_wit(state)?),
1076 WitEffect::SaveBuffer(path) => NativeEffect::SaveBuffer {
1077 path: opt_path_from_wit(path),
1078 },
1079 WitEffect::QuitEditor(p) => NativeEffect::QuitEditor {
1080 force: p.force,
1081 scope: NativeQuitScope::from_wit(p.scope)?,
1082 },
1083 WitEffect::OpenBuffer(p) => NativeEffect::OpenBuffer {
1084 path: opt_path_from_wit(p.path),
1085 force: p.force,
1086 },
1087 WitEffect::OpenBufferAt(p) => NativeEffect::OpenBufferAt {
1088 path: opt_path_from_wit(p.path),
1089 position: NativePosition::from_wit(p.position)?,
1090 force: p.force,
1091 content: p.content,
1094 activate_minor: p.activate_minor,
1095 },
1096 WitEffect::OpenExternalUri(uri) => NativeEffect::OpenExternalUri { uri },
1097 WitEffect::OpenBufferAtColumn(p) => NativeEffect::OpenBufferAtColumn {
1098 path: opt_path_from_wit(p.path),
1099 column: p.column.map(NativeUtf16Pos::from_wit).transpose()?,
1100 force: p.force,
1101 },
1102 WitEffect::SpawnTerminal(p) => NativeEffect::SpawnTerminal {
1103 cmd_line: p.cmd_line,
1104 env: p.env,
1105 activate_minor: p.activate_minor,
1106 cwd: opt_path_from_wit(p.cwd),
1107 },
1108 WitEffect::TerminalInput(bytes) => NativeEffect::TerminalInput(bytes),
1109 WitEffect::SetOption(spec) => NativeEffect::SetOption { spec },
1110 WitEffect::SetLocalOption(spec) => NativeEffect::SetLocalOption { spec },
1111 WitEffect::SetGlobalOption(spec) => NativeEffect::SetGlobalOption { spec },
1112 WitEffect::ClearSearchHighlight => NativeEffect::ClearSearchHighlight,
1113 WitEffect::SetColorscheme(name) => NativeEffect::SetColorscheme(name),
1114 WitEffect::Echo(p) => NativeEffect::Echo {
1115 level: NativeEchoLevel::from_wit(p.level)?,
1116 text: p.text,
1117 },
1118 WitEffect::ShowDiagnosticsPopup(lines) => NativeEffect::ShowDiagnosticsPopup { lines },
1119 WitEffect::Lsp(req) => NativeEffect::Lsp(NativeLspRequest::from_wit(req)?),
1120 WitEffect::EchoRegisters => NativeEffect::EchoRegisters,
1121 WitEffect::EchoMarks => NativeEffect::EchoMarks,
1122 WitEffect::Substitute(p) => NativeEffect::Substitute {
1123 scope: NativeSubstituteScope::from_wit(p.scope)?,
1124 pattern: p.pattern,
1125 replacement: p.replacement,
1126 global: p.global,
1127 },
1128 WitEffect::DeleteCurrentLine => NativeEffect::DeleteCurrentLine,
1129 WitEffect::DescribeCommand(p) => NativeEffect::DescribeCommand {
1130 name: p.name,
1131 anchor: p.anchor,
1132 },
1133 WitEffect::DescribeBuffer => NativeEffect::DescribeBuffer,
1134 WitEffect::Apropos(pattern) => NativeEffect::Apropos { pattern },
1135 WitEffect::DescribeKey(chord) => NativeEffect::DescribeKey { chord },
1136 WitEffect::ListKeymap => NativeEffect::ListKeymap,
1137 WitEffect::BufferNext => NativeEffect::BufferNext,
1138 WitEffect::BufferPrev => NativeEffect::BufferPrev,
1139 WitEffect::ListBuffers => NativeEffect::ListBuffers,
1140 WitEffect::OpenBufferPicker => NativeEffect::OpenBufferPicker,
1141 WitEffect::OpenPicker(p) => NativeEffect::OpenPicker {
1142 source: p.source,
1143 args: p.args,
1144 root: opt_path_from_wit(p.root),
1145 fill_action: p.fill_action,
1152 query: p.query,
1153 },
1154 WitEffect::BufferDelete(force) => NativeEffect::BufferDelete { force },
1155 WitEffect::FocusBuffer(id) => NativeEffect::FocusBuffer(id),
1156 WitEffect::InvokeCommand(c) => NativeEffect::InvokeCommand {
1157 id: c.id,
1158 args: NativeArgs::from_wit(c.args)?,
1159 },
1160 WitEffect::OpenFileTree(root) => NativeEffect::OpenFileTree {
1161 root: opt_path_from_wit(root),
1162 },
1163 WitEffect::CloseFileTree => NativeEffect::CloseFileTree,
1164 WitEffect::OpenOil(dir) => NativeEffect::OpenOil {
1165 dir: opt_path_from_wit(dir),
1166 },
1167 WitEffect::DescribeOption(name) => NativeEffect::DescribeOption { name },
1168 WitEffect::DescribeElement(name) => NativeEffect::DescribeElement { name },
1169 WitEffect::ListOptions => NativeEffect::ListOptions,
1170 WitEffect::DescribePluginApi(seam) => NativeEffect::DescribePluginApi { seam },
1171 WitEffect::ListPluginApis => NativeEffect::ListPluginApis,
1172 WitEffect::ExportPluginApi(format) => NativeEffect::ExportPluginApi { format },
1173 WitEffect::ListCommands => NativeEffect::ListCommands,
1174 WitEffect::DescribePlugin(name) => NativeEffect::DescribePlugin { name },
1175 WitEffect::ListPlugins => NativeEffect::ListPlugins,
1176 WitEffect::OpenHover(markdown) => NativeEffect::OpenHover { markdown },
1177 WitEffect::DismissPopup => NativeEffect::DismissPopup,
1178 WitEffect::DismissPopupNamed(name) => NativeEffect::DismissPopupNamed { name },
1179 WitEffect::OpenPopup(p) => NativeEffect::OpenPopup {
1180 name: p.name,
1181 mode_id: p.mode_id,
1182 placement: match p.placement {
1183 WitPopupPlacement::Centered => NativePopupPlacement::Centered,
1184 WitPopupPlacement::CursorAnchored => NativePopupPlacement::CursorAnchored,
1185 WitPopupPlacement::MinibufferBand => NativePopupPlacement::MinibufferBand,
1186 },
1187 focus: match p.focus {
1188 WitPopupFocus::Steal => NativePopupFocus::Steal,
1189 WitPopupFocus::Passive => NativePopupFocus::Passive,
1190 },
1191 },
1192 WitEffect::OpenHelpTopic(topic) => NativeEffect::OpenHelpTopic { topic },
1193 WitEffect::ListDiagnostics => NativeEffect::ListDiagnostics,
1194 WitEffect::NextDiagnostic => NativeEffect::NextDiagnostic,
1195 WitEffect::PrevDiagnostic => NativeEffect::PrevDiagnostic,
1196 WitEffect::OpenLspLog(server_id) => NativeEffect::OpenLspLog { server_id },
1197 WitEffect::OpenMessages => NativeEffect::OpenMessages,
1198 WitEffect::OpenDashboard => NativeEffect::OpenDashboard,
1199 WitEffect::ToggleLspTrace(server_id) => NativeEffect::ToggleLspTrace { server_id },
1200 WitEffect::OpenLspTraceLog(server_id) => NativeEffect::OpenLspTraceLog { server_id },
1201 WitEffect::LspStatus => NativeEffect::LspStatus,
1202 WitEffect::LspServerLogListing => NativeEffect::LspServerLogListing,
1203 WitEffect::LspRestart(server_id) => NativeEffect::LspRestart { server_id },
1204 WitEffect::LspProgressCancel(server_id) => NativeEffect::LspProgressCancel { server_id },
1205 WitEffect::LspExpandRegion => NativeEffect::LspExpandRegion,
1206 WitEffect::LspShrinkRegion => NativeEffect::LspShrinkRegion,
1207 WitEffect::SetLspLogLevel(p) => NativeEffect::SetLspLogLevel {
1208 server_id: p.server_id,
1209 level: p.level,
1210 },
1211 WitEffect::LspLogClear(server_id) => NativeEffect::LspLogClear { server_id },
1212 WitEffect::LspDocumentSymbol => NativeEffect::LspDocumentSymbol,
1213 WitEffect::LspWorkspaceSymbol(query) => NativeEffect::LspWorkspaceSymbol { query },
1214 WitEffect::LspIncomingCalls => NativeEffect::LspIncomingCalls,
1215 WitEffect::LspOutgoingCalls => NativeEffect::LspOutgoingCalls,
1216 WitEffect::LspSupertypes => NativeEffect::LspSupertypes,
1217 WitEffect::LspSubtypes => NativeEffect::LspSubtypes,
1218 WitEffect::LspMoniker => NativeEffect::LspMoniker,
1219 WitEffect::LspCodeLens => NativeEffect::LspCodeLens,
1220 WitEffect::LspColorPresentation => NativeEffect::LspColorPresentation,
1221 WitEffect::LspFormat => NativeEffect::LspFormat,
1222 WitEffect::LspFormatRange => NativeEffect::LspFormatRange,
1223 WitEffect::LspSignatureHelp => NativeEffect::LspSignatureHelp,
1224 WitEffect::LspComplete => NativeEffect::LspComplete,
1225 WitEffect::LspRename(new_name) => NativeEffect::LspRename { new_name },
1226 WitEffect::LspCodeAction => NativeEffect::LspCodeAction,
1227 WitEffect::ExpandSnippet(range) => NativeEffect::ExpandSnippet {
1228 replace_range: NativeRange::from_wit(range)?,
1229 },
1230 WitEffect::ReloadSnippets => NativeEffect::ReloadSnippets,
1231 WitEffect::DescribeEvents => NativeEffect::DescribeEvents,
1232 WitEffect::DescribeDiff => NativeEffect::DescribeDiff,
1233 WitEffect::DiffOpen => NativeEffect::DiffOpen,
1234 WitEffect::DiffOff(force) => NativeEffect::DiffOff { force },
1235 WitEffect::Diffthis => NativeEffect::Diffthis,
1236 WitEffect::Diffsplit(p) => NativeEffect::Diffsplit {
1237 path: PathBuf::from(p.path),
1238 remote: opt_path_from_wit(p.remote),
1239 },
1240 WitEffect::DiffGetCmd(target) => NativeEffect::DiffGetCmd { target },
1241 WitEffect::DiffPutCmd(target) => NativeEffect::DiffPutCmd { target },
1242 WitEffect::DiffAccept => NativeEffect::DiffAccept,
1243 WitEffect::DiffReject => NativeEffect::DiffReject,
1244 WitEffect::DiffAcceptAll => NativeEffect::DiffAcceptAll,
1245 WitEffect::DiffRejectAll => NativeEffect::DiffRejectAll,
1246 WitEffect::CloseSessionDiffs(p) => NativeEffect::CloseSessionDiffs {
1247 origin_session: p.origin_session,
1248 tab_name: p.tab_name,
1249 },
1250 WitEffect::CloseAllSessionDiffs(origin_session) => {
1251 NativeEffect::CloseAllSessionDiffs { origin_session }
1252 }
1253 WitEffect::NextHunk => NativeEffect::NextHunk,
1254 WitEffect::PrevHunk => NativeEffect::PrevHunk,
1255 WitEffect::DescribeEvent(name) => NativeEffect::DescribeEvent { name },
1256 WitEffect::ListModes => NativeEffect::ListModes,
1257 WitEffect::DescribeMode(name) => NativeEffect::DescribeMode { name },
1258 WitEffect::DescribeActiveModes => NativeEffect::DescribeActiveModes,
1259 WitEffect::DescribeActiveBindings => NativeEffect::DescribeActiveBindings,
1260 WitEffect::DescribeOptionResolution(name) => {
1261 NativeEffect::DescribeOptionResolution { name }
1262 }
1263 WitEffect::Customize(name) => NativeEffect::Customize { name },
1264 WitEffect::Tutor(lesson) => NativeEffect::Tutor { lesson },
1265 WitEffect::ToggleMode(mode_name) => NativeEffect::ToggleMode { mode_name },
1266 WitEffect::AppAction(app) => NativeEffect::AppAction(NativeAppEffect::from_wit(app)?),
1267 WitEffect::RecordJump => NativeEffect::RecordJump,
1268 WitEffect::OpenAiLog(session) => NativeEffect::OpenAiLog { session },
1269 WitEffect::OpenSyntheticBuffer(p) => NativeEffect::OpenSyntheticBuffer {
1275 name: p.name,
1276 mode_id: p.mode_id,
1277 content: p.content,
1278 cursor: p.cursor.map(NativePosition::from_wit).transpose()?,
1279 activate_minor: p.activate_minor,
1280 },
1281 })
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 #![allow(clippy::unwrap_used, clippy::panic)]
1287
1288 use super::*;
1289
1290 fn pos(line: u32, byte: u32) -> NativePosition {
1291 NativePosition { line, byte }
1292 }
1293
1294 #[test]
1302 fn an_effect_with_no_mirror_fails_loudly_instead_of_becoming_none() {
1303 let unmirrored = vec![
1307 (NativeEffect::PrintWorkingDir, "PrintWorkingDir"),
1308 (NativeEffect::PrintProjectRoot, "PrintProjectRoot"),
1309 (NativeEffect::ListErrors, "ListErrors"),
1310 (
1311 NativeEffect::ChangeDir(Some("/tmp".to_string())),
1312 "ChangeDir",
1313 ),
1314 ];
1315 for (effect, name) in unmirrored {
1316 let err = effect_to_wit(&effect)
1317 .expect_err("an unmirrored effect must not silently become `none`");
1318 assert!(
1319 err.contains(name),
1320 "the error must name the culprit for a plugin author who \
1321 cannot see host internals: got {err:?}"
1322 );
1323 }
1324 }
1325
1326 #[test]
1333 fn confirm_crosses_the_boundary_in_both_directions() {
1334 let native = NativeEffect::Confirm {
1335 prompt: "Delete src/main.rs?".to_string(),
1336 yes_action: "action:my-plugin-delete".to_string(),
1337 args: NativeArgs::List(vec![lattice_grammar::args::ArgValue::String(
1338 "src/main.rs".to_string(),
1339 )]),
1340 };
1341 let back = effect_from_wit(effect_to_wit(&native).unwrap()).unwrap();
1342 match back {
1343 NativeEffect::Confirm {
1344 prompt,
1345 yes_action,
1346 args,
1347 } => {
1348 assert_eq!(prompt, "Delete src/main.rs?");
1349 assert_eq!(yes_action, "action:my-plugin-delete");
1350 let list = args.as_list().expect("the target survived");
1354 assert!(matches!(
1355 &list[0],
1356 lattice_grammar::args::ArgValue::String(p) if p == "src/main.rs"
1357 ));
1358 }
1359 other => panic!("expected Confirm, got {other:?}"),
1360 }
1361 }
1362
1363 #[test]
1366 fn a_confirm_with_no_args_crosses_too() {
1367 let native = NativeEffect::Confirm {
1368 prompt: "Proceed?".to_string(),
1369 yes_action: "action:my-plugin-go".to_string(),
1370 args: NativeArgs::None,
1371 };
1372 let back = effect_from_wit(effect_to_wit(&native).unwrap()).unwrap();
1373 assert!(matches!(
1374 back,
1375 NativeEffect::Confirm {
1376 args: NativeArgs::None,
1377 ..
1378 }
1379 ));
1380 }
1381
1382 fn write_effect(cut: Option<NativeRange>) -> NativeEffect {
1385 NativeEffect::WriteToFile {
1386 path: std::path::PathBuf::from("/tmp/archive.org"),
1387 anchor: lattice_grammar::FileAnchor::End,
1388 text: "* Archived\n".to_string(),
1389 cut,
1390 create_parents: false,
1391 save: false,
1392 }
1393 }
1394
1395 #[test]
1398 fn focus_buffer_round_trips() {
1399 let wit = effect_to_wit(&NativeEffect::FocusBuffer(42)).unwrap();
1400 assert!(matches!(wit, WitEffect::FocusBuffer(42)));
1401 let back = effect_from_wit(wit).unwrap();
1402 assert!(matches!(back, NativeEffect::FocusBuffer(42)));
1403 }
1404
1405 #[test]
1407 fn invoke_command_round_trips() {
1408 let native = NativeEffect::InvokeCommand {
1409 id: "org-capture-cleanup".to_string(),
1410 args: NativeArgs::String("a3f9c1".to_string()),
1411 };
1412 match effect_from_wit(effect_to_wit(&native).unwrap()).unwrap() {
1413 NativeEffect::InvokeCommand { id, args } => {
1414 assert_eq!(id, "org-capture-cleanup");
1415 assert_eq!(args, NativeArgs::String("a3f9c1".to_string()));
1416 }
1417 other => panic!("expected InvokeCommand, got {other:?}"),
1418 }
1419 }
1420
1421 #[test]
1422 fn write_to_file_round_trips() {
1423 let back = effect_from_wit(effect_to_wit(&write_effect(None)).unwrap()).unwrap();
1424 match back {
1425 NativeEffect::WriteToFile {
1426 path,
1427 anchor,
1428 text,
1429 cut,
1430 ..
1431 } => {
1432 assert_eq!(path, std::path::PathBuf::from("/tmp/archive.org"));
1433 assert_eq!(anchor, lattice_grammar::FileAnchor::End);
1434 assert_eq!(text, "* Archived\n");
1435 assert!(cut.is_none());
1436 }
1437 other => panic!("expected WriteToFile, got {other:?}"),
1438 }
1439 }
1440
1441 #[test]
1445 fn the_cut_range_survives_the_crossing() {
1446 let range = NativeRange {
1447 start: pos(2, 0),
1448 end: pos(5, 0),
1449 };
1450 let back = effect_from_wit(effect_to_wit(&write_effect(Some(range))).unwrap()).unwrap();
1451 match back {
1452 NativeEffect::WriteToFile { cut: Some(got), .. } => assert_eq!(got, range),
1453 other => panic!("expected a cut, got {other:?}"),
1454 }
1455 }
1456
1457 #[test]
1460 fn every_anchor_round_trips() {
1461 for anchor in [
1462 lattice_grammar::FileAnchor::End,
1463 lattice_grammar::FileAnchor::Start,
1464 lattice_grammar::FileAnchor::Line(0),
1465 lattice_grammar::FileAnchor::Line(42),
1466 ] {
1467 let native = NativeEffect::WriteToFile {
1468 path: std::path::PathBuf::from("/tmp/a.org"),
1469 anchor,
1470 text: "x".to_string(),
1471 cut: None,
1472 create_parents: false,
1473 save: false,
1474 };
1475 let back = effect_from_wit(effect_to_wit(&native).unwrap()).unwrap();
1476 match back {
1477 NativeEffect::WriteToFile { anchor: got, .. } => assert_eq!(got, anchor),
1478 other => panic!("expected WriteToFile, got {other:?}"),
1479 }
1480 }
1481 }
1482
1483 #[test]
1487 fn a_non_utf8_path_is_refused_rather_than_mangled() {
1488 #[cfg(unix)]
1489 {
1490 use std::os::unix::ffi::OsStrExt;
1491 let bad = std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b"/tmp/\xff.org"));
1492 let native = NativeEffect::WriteToFile {
1493 path: bad,
1494 anchor: lattice_grammar::FileAnchor::End,
1495 text: "x".to_string(),
1496 cut: None,
1497 create_parents: false,
1498 save: false,
1499 };
1500 let err = effect_to_wit(&native).expect_err("must refuse");
1501 assert!(err.contains("not UTF-8"), "got {err}");
1502 }
1503 }
1504
1505 #[test]
1506 fn position_and_range_round_trip() {
1507 let p = pos(3, 7);
1508 assert_eq!(p, NativePosition::from_wit(p.to_wit().unwrap()).unwrap());
1509 let r = NativeRange {
1510 start: pos(1, 0),
1511 end: pos(2, 5),
1512 };
1513 assert_eq!(r, NativeRange::from_wit(r.to_wit().unwrap()).unwrap());
1514 }
1515
1516 #[test]
1517 fn plugin_api_help_effects_round_trip() {
1518 let rt = |e: &NativeEffect| effect_from_wit(effect_to_wit(e).unwrap()).unwrap();
1522 match rt(&NativeEffect::DescribePluginApi {
1523 seam: Some("host-services".into()),
1524 }) {
1525 NativeEffect::DescribePluginApi { seam } => {
1526 assert_eq!(seam.as_deref(), Some("host-services"))
1527 }
1528 other => panic!("unexpected: {other:?}"),
1529 }
1530 match rt(&NativeEffect::DescribePluginApi { seam: None }) {
1531 NativeEffect::DescribePluginApi { seam } => assert!(seam.is_none()),
1532 other => panic!("unexpected: {other:?}"),
1533 }
1534 assert!(matches!(
1535 rt(&NativeEffect::ListPluginApis),
1536 NativeEffect::ListPluginApis
1537 ));
1538 match rt(&NativeEffect::ExportPluginApi {
1539 format: Some("json".into()),
1540 }) {
1541 NativeEffect::ExportPluginApi { format } => assert_eq!(format.as_deref(), Some("json")),
1542 other => panic!("unexpected: {other:?}"),
1543 }
1544 match rt(&NativeEffect::ExportPluginApi { format: None }) {
1545 NativeEffect::ExportPluginApi { format } => assert!(format.is_none()),
1546 other => panic!("unexpected: {other:?}"),
1547 }
1548 assert!(matches!(
1549 rt(&NativeEffect::ListCommands),
1550 NativeEffect::ListCommands
1551 ));
1552 match rt(&NativeEffect::DescribePlugin {
1553 name: "git-gutter".into(),
1554 }) {
1555 NativeEffect::DescribePlugin { name } => assert_eq!(name, "git-gutter"),
1556 other => panic!("unexpected: {other:?}"),
1557 }
1558 assert!(matches!(
1559 rt(&NativeEffect::ListPlugins),
1560 NativeEffect::ListPlugins
1561 ));
1562 }
1563
1564 #[test]
1565 fn edit_and_applied_edit_round_trip() {
1566 let edit = NativeEdit {
1567 range: NativeRange {
1568 start: pos(0, 0),
1569 end: pos(0, 3),
1570 },
1571 kind: NativeEditKind::Replace { text: "abc".into() },
1572 };
1573 assert_eq!(edit, NativeEdit::from_wit(edit.to_wit().unwrap()).unwrap());
1574
1575 let applied = NativeAppliedEdit {
1576 original_range: NativeRange {
1577 start: pos(0, 0),
1578 end: pos(0, 1),
1579 },
1580 inserted_range: NativeRange {
1581 start: pos(0, 0),
1582 end: pos(0, 3),
1583 },
1584 replaced_text: "x".into(),
1585 inserted_text: "abc".into(),
1586 delta: NativeEditDelta {
1587 start_byte: 0,
1588 old_end_byte: 1,
1589 new_end_byte: 3,
1590 start_position: pos(0, 0),
1591 old_end_position: pos(0, 1),
1592 new_end_position: pos(0, 3),
1593 },
1594 };
1595 let wit = applied.to_wit().unwrap();
1599 let back = NativeAppliedEdit::from_wit(wit.clone()).unwrap();
1600 assert_eq!(format!("{:?}", back.to_wit().unwrap()), format!("{wit:?}"));
1601 }
1602
1603 #[test]
1604 fn selection_set_round_trips_single_and_multi() {
1605 let single = NativeSelectionSet::single(NativeSelection {
1606 anchor: pos(0, 0),
1607 head: pos(0, 4),
1608 visual: Some(NativeVisualMode::Charwise),
1609 });
1610 assert_eq!(
1611 single,
1612 NativeSelectionSet::from_wit(single.to_wit().unwrap()).unwrap()
1613 );
1614
1615 let multi = NativeSelectionSet::from_parts(
1616 vec![
1617 NativeSelection {
1618 anchor: pos(0, 0),
1619 head: pos(0, 1),
1620 visual: None,
1621 },
1622 NativeSelection {
1623 anchor: pos(2, 0),
1624 head: pos(2, 3),
1625 visual: Some(NativeVisualMode::Linewise),
1626 },
1627 ],
1628 1,
1629 );
1630 let back = NativeSelectionSet::from_wit(multi.to_wit().unwrap()).unwrap();
1631 assert_eq!(multi, back);
1632 assert_eq!(back.primary_index(), 1);
1633 }
1634
1635 #[test]
1636 fn modal_state_round_trips_every_variant() {
1637 for s in [
1638 NativeModalState::Normal,
1639 NativeModalState::Insert,
1640 NativeModalState::Visual(NativeVisualKind::Blockwise),
1641 NativeModalState::Select(NativeVisualKind::Charwise),
1642 NativeModalState::OperatorPending,
1643 NativeModalState::Command,
1644 NativeModalState::Search(NativeSearchDirection::Backward),
1645 NativeModalState::Replace,
1646 ] {
1647 assert_eq!(s, NativeModalState::from_wit(s.to_wit().unwrap()).unwrap());
1648 }
1649 }
1650
1651 #[test]
1652 fn register_round_trips_every_variant() {
1653 for r in [
1654 NativeRegister::Unnamed,
1655 NativeRegister::Named('a'),
1656 NativeRegister::System,
1657 NativeRegister::BlackHole,
1658 NativeRegister::Expression,
1659 NativeRegister::ReadOnly('%'),
1660 NativeRegister::Numbered(3),
1661 ] {
1662 assert_eq!(r, NativeRegister::from_wit(r.to_wit().unwrap()).unwrap());
1663 }
1664 }
1665
1666 #[test]
1667 fn yank_kind_round_trips() {
1668 for k in [
1669 NativeYankKind::Charwise,
1670 NativeYankKind::Linewise,
1671 NativeYankKind::Blockwise,
1672 ] {
1673 assert_eq!(k, NativeYankKind::from_wit(k.to_wit().unwrap()).unwrap());
1674 }
1675 }
1676
1677 fn assert_effect_round_trips(native: NativeEffect) {
1682 let wit = native.to_wit().expect("to_wit");
1683 let back = NativeEffect::from_wit(wit).expect("from_wit");
1684 assert_eq!(format!("{native:?}"), format!("{back:?}"));
1685 }
1686
1687 fn sample_applied_edit() -> NativeAppliedEdit {
1688 NativeAppliedEdit {
1689 original_range: NativeRange {
1690 start: pos(0, 0),
1691 end: pos(0, 1),
1692 },
1693 inserted_range: NativeRange {
1694 start: pos(0, 0),
1695 end: pos(0, 3),
1696 },
1697 replaced_text: "x".into(),
1698 inserted_text: "abc".into(),
1699 delta: NativeEditDelta {
1700 start_byte: 0,
1701 old_end_byte: 1,
1702 new_end_byte: 3,
1703 start_position: pos(0, 0),
1704 old_end_position: pos(0, 1),
1705 new_end_position: pos(0, 3),
1706 },
1707 }
1708 }
1709
1710 fn sample_edit() -> NativeEdit {
1711 NativeEdit {
1712 range: NativeRange {
1713 start: pos(1, 2),
1714 end: pos(1, 5),
1715 },
1716 kind: NativeEditKind::Replace { text: "yo".into() },
1717 }
1718 }
1719
1720 #[test]
1727 fn effect_payload_arms_round_trip() {
1728 let cases = vec![
1729 NativeEffect::None,
1730 NativeEffect::Edits(vec![sample_applied_edit()]),
1731 NativeEffect::ApplyEdit {
1732 target: BufferId(2),
1733 edit: sample_edit(),
1734 cursor: Some(NativePosition::new(3, 0)),
1735 },
1736 NativeEffect::ApplyEdit {
1737 target: BufferId(0),
1738 edit: sample_edit(),
1739 cursor: None,
1740 },
1741 NativeEffect::SelectionChange(NativeSelectionSet::single(NativeSelection {
1742 anchor: pos(0, 0),
1743 head: pos(0, 4),
1744 visual: Some(NativeVisualMode::Charwise),
1745 })),
1746 NativeEffect::Yank {
1747 register: NativeRegister::Named('a'),
1748 content: "text".into(),
1749 kind: NativeYankKind::Linewise,
1750 explicit_yank: true,
1751 },
1752 NativeEffect::EnterMode(NativeModalState::Visual(NativeVisualKind::Blockwise)),
1753 NativeEffect::SaveBuffer {
1754 path: Some(PathBuf::from("/a/b.rs")),
1755 },
1756 NativeEffect::SaveBuffer { path: None },
1757 NativeEffect::QuitEditor {
1758 force: true,
1759 scope: NativeQuitScope::All,
1760 },
1761 NativeEffect::OpenBuffer {
1762 path: Some(PathBuf::from("/a/b.rs")),
1763 force: false,
1764 },
1765 NativeEffect::OpenBufferAt {
1766 path: None,
1767 position: pos(10, 4),
1768 force: true,
1769 content: None,
1770 activate_minor: None,
1771 },
1772 NativeEffect::OpenExternalUri {
1773 uri: "https://example.com".into(),
1774 },
1775 NativeEffect::OpenBufferAtColumn {
1776 path: Some(PathBuf::from("/a/b.rs")),
1777 column: Some(NativeUtf16Pos { line: 2, col: 8 }),
1778 force: false,
1779 },
1780 NativeEffect::OpenBufferAtColumn {
1781 path: None,
1782 column: None,
1783 force: false,
1784 },
1785 NativeEffect::SpawnTerminal {
1786 cmd_line: Some("claude".into()),
1787 env: vec![("CLAUDE_CODE_SSE_PORT".into(), "9000".into())],
1788 activate_minor: Some("claude-code-mode".into()),
1789 cwd: None,
1790 },
1791 NativeEffect::SpawnTerminal {
1795 cmd_line: None,
1796 env: Vec::new(),
1797 activate_minor: None,
1798 cwd: Some("/work/api".into()),
1799 },
1800 NativeEffect::TerminalInput(vec![0x1b]),
1801 NativeEffect::SetOption {
1802 spec: "wrap".into(),
1803 },
1804 NativeEffect::SetLocalOption {
1805 spec: "number".into(),
1806 },
1807 NativeEffect::SetGlobalOption {
1808 spec: "hlsearch".into(),
1809 },
1810 NativeEffect::SetColorscheme("nord".into()),
1811 NativeEffect::Echo {
1812 level: NativeEchoLevel::Warn,
1813 text: "careful".into(),
1814 },
1815 NativeEffect::ShowDiagnosticsPopup {
1816 lines: vec![("E0308: mismatched types".into(), 0), ("hint".into(), 3)],
1817 },
1818 NativeEffect::Lsp(NativeLspRequest::Definition),
1819 NativeEffect::Lsp(NativeLspRequest::FollowLink),
1820 NativeEffect::Substitute {
1821 scope: NativeSubstituteScope::Whole,
1822 pattern: "foo".into(),
1823 replacement: "bar".into(),
1824 global: true,
1825 },
1826 NativeEffect::DescribeCommand {
1827 name: "write".into(),
1828 anchor: Some("arg:path".into()),
1829 },
1830 NativeEffect::Apropos {
1831 pattern: "buf".into(),
1832 },
1833 NativeEffect::DescribeKey { chord: "gd".into() },
1834 NativeEffect::OpenPicker {
1835 source: "files".into(),
1836 args: vec!["src".into(), "*.rs".into()],
1837 root: None,
1838 fill_action: None,
1839 query: None,
1840 },
1841 NativeEffect::OpenPicker {
1844 source: "grep".into(),
1845 args: vec!["needle".into()],
1846 root: Some("/work/api".into()),
1847 fill_action: None,
1848 query: None,
1849 },
1850 NativeEffect::OpenPicker {
1860 source: "dir-pick".into(),
1861 args: vec!["~".into()],
1862 root: None,
1863 fill_action: Some("project-remember-and-switch".into()),
1864 query: None,
1865 },
1866 NativeEffect::OpenPicker {
1868 source: "org-roam-insert".into(),
1869 args: Vec::new(),
1870 root: None,
1871 fill_action: None,
1872 query: Some("rust async".into()),
1873 },
1874 NativeEffect::BufferDelete { force: true },
1875 NativeEffect::OpenFileTree {
1876 root: Some(PathBuf::from("/proj")),
1877 },
1878 NativeEffect::OpenOil { dir: None },
1879 NativeEffect::DescribeOption {
1880 name: "wrap".into(),
1881 },
1882 NativeEffect::DescribeElement {
1883 name: "modeline.normal".into(),
1884 },
1885 NativeEffect::OpenHover {
1886 markdown: "**hi**".into(),
1887 },
1888 NativeEffect::OpenHelpTopic {
1889 topic: Some("motions".into()),
1890 },
1891 NativeEffect::OpenLspLog {
1892 server_id: Some("rust-analyzer".into()),
1893 },
1894 NativeEffect::ToggleLspTrace {
1895 server_id: "rust-analyzer".into(),
1896 },
1897 NativeEffect::OpenLspTraceLog { server_id: None },
1898 NativeEffect::LspRestart {
1899 server_id: "rust-analyzer".into(),
1900 },
1901 NativeEffect::LspProgressCancel { server_id: None },
1902 NativeEffect::SetLspLogLevel {
1903 server_id: Some("rust-analyzer".into()),
1904 level: "debug".into(),
1905 },
1906 NativeEffect::LspLogClear { server_id: None },
1907 NativeEffect::LspWorkspaceSymbol {
1908 query: "Foo".into(),
1909 },
1910 NativeEffect::LspRename {
1911 new_name: "renamed".into(),
1912 },
1913 NativeEffect::ExpandSnippet {
1914 replace_range: NativeRange {
1915 start: pos(0, 0),
1916 end: pos(0, 2),
1917 },
1918 },
1919 NativeEffect::DiffOff { force: true },
1920 NativeEffect::Diffsplit {
1921 path: PathBuf::from("/a/base.rs"),
1922 remote: Some(PathBuf::from("/a/remote.rs")),
1923 },
1924 NativeEffect::DiffGetCmd { target: Some(2) },
1925 NativeEffect::DiffPutCmd { target: None },
1926 NativeEffect::CloseSessionDiffs {
1927 origin_session: 7,
1928 tab_name: "/a/b.rs".into(),
1929 },
1930 NativeEffect::CloseAllSessionDiffs { origin_session: 7 },
1931 NativeEffect::DescribeEvent {
1932 name: "document-changed".into(),
1933 },
1934 NativeEffect::DescribeMode {
1935 name: "lsp-mode".into(),
1936 },
1937 NativeEffect::DescribeOptionResolution {
1938 name: "wrap".into(),
1939 },
1940 NativeEffect::Customize { name: None },
1941 NativeEffect::Tutor { lesson: Some(3) },
1942 NativeEffect::ToggleMode {
1943 mode_name: "diff-mode".into(),
1944 },
1945 ];
1946 for case in cases {
1947 assert_effect_round_trips(case);
1948 }
1949 }
1950
1951 #[test]
1953 fn effect_unit_arms_round_trip() {
1954 for e in [
1955 NativeEffect::ClearSearchHighlight,
1956 NativeEffect::EchoRegisters,
1957 NativeEffect::EchoMarks,
1958 NativeEffect::DeleteCurrentLine,
1959 NativeEffect::DescribeBuffer,
1960 NativeEffect::ListKeymap,
1961 NativeEffect::BufferNext,
1962 NativeEffect::BufferPrev,
1963 NativeEffect::ListBuffers,
1964 NativeEffect::OpenBufferPicker,
1965 NativeEffect::CloseFileTree,
1966 NativeEffect::ListOptions,
1967 NativeEffect::DismissPopup,
1968 NativeEffect::DismissPopupNamed {
1972 name: "*which-key*".to_string(),
1973 },
1974 NativeEffect::OpenPopup {
1975 name: "*ai-permission*".to_string(),
1976 mode_id: "ai-permission-mode".to_string(),
1977 placement: NativePopupPlacement::Centered,
1978 focus: NativePopupFocus::Steal,
1979 },
1980 NativeEffect::OpenPopup {
1981 name: "hover".to_string(),
1982 mode_id: "hover-mode".to_string(),
1983 placement: NativePopupPlacement::CursorAnchored,
1984 focus: NativePopupFocus::Passive,
1985 },
1986 NativeEffect::ListDiagnostics,
1987 NativeEffect::NextDiagnostic,
1988 NativeEffect::PrevDiagnostic,
1989 NativeEffect::OpenMessages,
1990 NativeEffect::OpenDashboard,
1991 NativeEffect::LspStatus,
1992 NativeEffect::LspServerLogListing,
1993 NativeEffect::LspExpandRegion,
1994 NativeEffect::LspShrinkRegion,
1995 NativeEffect::LspDocumentSymbol,
1996 NativeEffect::LspIncomingCalls,
1997 NativeEffect::LspOutgoingCalls,
1998 NativeEffect::LspSupertypes,
1999 NativeEffect::LspSubtypes,
2000 NativeEffect::LspMoniker,
2001 NativeEffect::LspCodeLens,
2002 NativeEffect::LspColorPresentation,
2003 NativeEffect::LspFormat,
2004 NativeEffect::LspFormatRange,
2005 NativeEffect::LspSignatureHelp,
2006 NativeEffect::LspComplete,
2007 NativeEffect::LspCodeAction,
2008 NativeEffect::ReloadSnippets,
2009 NativeEffect::DescribeEvents,
2010 NativeEffect::DescribeDiff,
2011 NativeEffect::DiffOpen,
2012 NativeEffect::Diffthis,
2013 NativeEffect::DiffAccept,
2014 NativeEffect::DiffReject,
2015 NativeEffect::DiffAcceptAll,
2016 NativeEffect::DiffRejectAll,
2017 NativeEffect::NextHunk,
2018 NativeEffect::PrevHunk,
2019 NativeEffect::ListModes,
2020 NativeEffect::RecordJump,
2021 ] {
2022 assert_effect_round_trips(e);
2023 }
2024 }
2025
2026 #[test]
2028 fn effect_helper_enums_round_trip() {
2029 for s in [NativeQuitScope::Pane, NativeQuitScope::All] {
2030 assert_eq!(s, NativeQuitScope::from_wit(s.to_wit().unwrap()).unwrap());
2031 }
2032 for l in [
2033 NativeEchoLevel::Trace,
2034 NativeEchoLevel::Debug,
2035 NativeEchoLevel::Info,
2036 NativeEchoLevel::Warn,
2037 NativeEchoLevel::Error,
2038 ] {
2039 assert_eq!(l, NativeEchoLevel::from_wit(l.to_wit().unwrap()).unwrap());
2040 }
2041 for s in [
2042 NativeSubstituteScope::CurrentLine,
2043 NativeSubstituteScope::Whole,
2044 ] {
2045 assert_eq!(
2046 s,
2047 NativeSubstituteScope::from_wit(s.to_wit().unwrap()).unwrap()
2048 );
2049 }
2050 let u = NativeUtf16Pos { line: 4, col: 9 };
2051 assert_eq!(u, NativeUtf16Pos::from_wit(u.to_wit().unwrap()).unwrap());
2052 for r in [
2053 NativeLspRequest::Hover,
2054 NativeLspRequest::Definition,
2055 NativeLspRequest::Declaration,
2056 NativeLspRequest::TypeDefinition,
2057 NativeLspRequest::Implementation,
2058 NativeLspRequest::References,
2059 NativeLspRequest::FollowLink,
2060 ] {
2061 assert_eq!(r, NativeLspRequest::from_wit(r.to_wit().unwrap()).unwrap());
2062 }
2063 }
2064
2065 #[test]
2070 fn many_flattens_and_rebuilds() {
2071 let many = NativeEffect::Many(vec![
2072 NativeEffect::RecordJump,
2073 NativeEffect::Many(vec![
2074 NativeEffect::BufferNext,
2075 NativeEffect::ClearSearchHighlight,
2076 ]),
2077 ]);
2078 let wit = many.to_wit().unwrap();
2079 assert_eq!(wit.len(), 3, "nested Many flattens to 3 atoms");
2080 let back = NativeEffect::from_wit(wit).unwrap();
2081 match back {
2082 NativeEffect::Many(list) => {
2083 assert_eq!(list.len(), 3);
2084 assert!(matches!(list[0], NativeEffect::RecordJump));
2085 assert!(matches!(list[1], NativeEffect::BufferNext));
2086 assert!(matches!(list[2], NativeEffect::ClearSearchHighlight));
2087 }
2088 other => panic!("expected Many, got {other:?}"),
2089 }
2090 }
2091
2092 #[test]
2095 fn single_element_and_empty_list_normalise() {
2096 let one = NativeEffect::from_wit(vec![WitEffect::BufferNext]).unwrap();
2097 assert!(matches!(one, NativeEffect::BufferNext));
2098 let empty = NativeEffect::from_wit(Vec::new()).unwrap();
2099 assert!(matches!(empty, NativeEffect::None));
2100 }
2101
2102 #[test]
2106 fn global_is_a_typed_error() {
2107 use lattice_grammar::CommandId;
2108 use lattice_grammar::command::CommandInvocation;
2109
2110 let global = NativeEffect::Global {
2111 pattern: "TODO".into(),
2112 inverted: false,
2113 body: Box::new(CommandInvocation::of(CommandId::new(0))),
2114 };
2115 let err = global.to_wit().expect_err("Global must not cross yet");
2116 assert!(err.contains("Global"), "error names the culprit: {err}");
2117
2118 let nested = NativeEffect::Many(vec![NativeEffect::RecordJump, global]);
2120 assert!(
2121 nested.to_wit().is_err(),
2122 "nested Global fails the whole list"
2123 );
2124 }
2125
2126 #[test]
2131 fn app_action_crosses_and_narrow_trigger_still_errors() {
2132 use lattice_grammar::app_effect::AppEffect;
2133
2134 let app = NativeEffect::AppAction(AppEffect::SplitPaneVertical);
2135 assert_effect_round_trips(app);
2136
2137 assert_effect_round_trips(NativeEffect::Many(vec![
2139 NativeEffect::AppAction(AppEffect::Quit),
2140 NativeEffect::RecordJump,
2141 NativeEffect::AppAction(AppEffect::NextTab),
2142 ]));
2143
2144 let narrow = NativeEffect::AppAction(AppEffect::NarrowTrigger { range: None });
2145 let err = narrow
2146 .to_wit()
2147 .expect_err("NarrowTrigger-carrying AppAction must not cross yet");
2148 assert!(
2149 err.contains("NarrowTrigger"),
2150 "error names the culprit: {err}"
2151 );
2152 }
2153
2154 #[test]
2167 fn oc7a_guest_seed_reaches_the_native_open() {
2168 let wit = WitEffect::OpenSyntheticBuffer(WitOpenSyntheticBufferPayload {
2169 name: "*org-capture*".to_string(),
2170 mode_id: "org-mode".to_string(),
2171 content: Some("* TODO \n body".to_string()),
2172 cursor: Some(WitPosition { line: 0, byte: 7 }),
2173 activate_minor: Some("org-capture-mode".to_string()),
2174 });
2175 match effect_from_wit(wit).expect("from_wit") {
2176 NativeEffect::OpenSyntheticBuffer {
2177 name,
2178 mode_id,
2179 content,
2180 cursor,
2181 activate_minor,
2182 } => {
2183 assert_eq!(name, "*org-capture*");
2184 assert_eq!(mode_id, "org-mode", "the capture buffer IS an org buffer");
2185 assert_eq!(content.as_deref(), Some("* TODO \n body"));
2186 assert_eq!(cursor, Some(pos(0, 7)), "the `%?` point");
2187 assert_eq!(
2188 activate_minor.as_deref(),
2189 Some("org-capture-mode"),
2190 "the minor carries C-c C-c / C-c C-k without putting them \
2191 on every org buffer"
2192 );
2193 }
2194 other => panic!("wrong variant: {other:?}"),
2195 }
2196 }
2197
2198 #[test]
2201 fn oc7a_an_unseeded_open_is_unchanged() {
2202 let wit = WitEffect::OpenSyntheticBuffer(WitOpenSyntheticBufferPayload {
2203 name: "*plugins*".to_string(),
2204 mode_id: "plugin-manager-mode".to_string(),
2205 content: None,
2206 cursor: None,
2207 activate_minor: None,
2208 });
2209 match effect_from_wit(wit).expect("from_wit") {
2210 NativeEffect::OpenSyntheticBuffer {
2211 content,
2212 cursor,
2213 activate_minor,
2214 ..
2215 } => {
2216 assert!(content.is_none() && cursor.is_none() && activate_minor.is_none());
2217 }
2218 other => panic!("wrong variant: {other:?}"),
2219 }
2220 }
2221}