1use std::path::{Path, PathBuf};
25
26use crate::lattice::plugin_host::types::{
30 ArgValue as WitArgValue, Args as WitArgs, CandidateChord as WitCandidateChord,
31 CandidateData as WitCandidateData, CandidateExtension as WitCandidateExtension,
32 CandidateFile as WitCandidateFile, CandidateKind as WitCandidateKind,
33 CandidateMark as WitCandidateMark, CandidateOption as WitCandidateOption,
34 CandidateOptionValue as WitCandidateOptionValue, CandidateRegister as WitCandidateRegister,
35 CommandRef as WitCommandRef, JumpTarget as WitJumpTarget, Location as WitLocation,
36 LspCodeActionRef as WitLspCodeActionRef, PickerAcceptOutcome as WitPickerAcceptOutcome,
37 RawCandidate as WitRawCandidate,
38};
39use lattice_completion::candidate::{
40 CandidateData as NativeCandidateData, CandidateKind as NativeCandidateKind,
41 RawCandidate as NativeRawCandidate,
42};
43use lattice_completion::insert::SourceId;
44use lattice_grammar::args::{ArgValue as NativeArgValue, Args as NativeArgs};
45use lattice_picker::outcome::PickerAcceptOutcome as NativePickerAcceptOutcome;
46
47pub(crate) fn path_to_wit(path: &Path) -> Result<String, String> {
51 path.to_str().map(str::to_string).ok_or_else(|| {
52 format!(
53 "path is not valid UTF-8 and cannot cross the boundary: {}",
54 path.display()
55 )
56 })
57}
58
59pub trait WitBoundary: Sized {
67 type Wit;
69
70 fn to_wit(&self) -> Result<Self::Wit, String>;
72
73 fn from_wit(wit: Self::Wit) -> Result<Self, String>;
75}
76
77impl WitBoundary for NativeArgValue {
78 type Wit = WitArgValue;
79
80 fn to_wit(&self) -> Result<WitArgValue, String> {
81 Ok(match self {
82 NativeArgValue::String(s) => WitArgValue::String(s.clone()),
83 NativeArgValue::Char(c) => WitArgValue::Char(*c),
84 NativeArgValue::Bool(b) => WitArgValue::Bool(*b),
85 NativeArgValue::Int(i) => WitArgValue::Int(*i),
86 NativeArgValue::Pattern(s) => WitArgValue::Pattern(s.clone()),
87 NativeArgValue::Chord(s) => WitArgValue::Chord(s.clone()),
88 NativeArgValue::Raw(s) => WitArgValue::Raw(s.clone()),
89 NativeArgValue::Invocation(_) => {
92 return Err(
93 "ArgValue::Invocation crosses the boundary with the command mirror \
94 (PH7.3b, fragment §4.1)"
95 .to_string(),
96 );
97 }
98 })
99 }
100
101 fn from_wit(wit: WitArgValue) -> Result<Self, String> {
102 Ok(match wit {
103 WitArgValue::String(s) => NativeArgValue::String(s),
104 WitArgValue::Char(c) => NativeArgValue::Char(c),
105 WitArgValue::Bool(b) => NativeArgValue::Bool(b),
106 WitArgValue::Int(i) => NativeArgValue::Int(i),
107 WitArgValue::Pattern(s) => NativeArgValue::Pattern(s),
108 WitArgValue::Chord(s) => NativeArgValue::Chord(s),
109 WitArgValue::Raw(s) => NativeArgValue::Raw(s),
110 })
111 }
112}
113
114impl WitBoundary for NativeArgs {
115 type Wit = WitArgs;
116
117 fn to_wit(&self) -> Result<WitArgs, String> {
118 Ok(match self {
119 NativeArgs::None => WitArgs::None,
120 NativeArgs::Char(c) => WitArgs::Char(*c),
121 NativeArgs::String(s) => WitArgs::String(s.clone()),
122 NativeArgs::Bytes(b) => WitArgs::Bytes(b.clone()),
123 NativeArgs::List(values) => WitArgs::List(
124 values
125 .iter()
126 .map(WitBoundary::to_wit)
127 .collect::<Result<Vec<_>, _>>()?,
128 ),
129 })
130 }
131
132 fn from_wit(wit: WitArgs) -> Result<Self, String> {
133 Ok(match wit {
134 WitArgs::None => NativeArgs::None,
135 WitArgs::Char(c) => NativeArgs::Char(c),
136 WitArgs::String(s) => NativeArgs::String(s),
137 WitArgs::Bytes(b) => NativeArgs::Bytes(b),
138 WitArgs::List(values) => NativeArgs::List(
139 values
140 .into_iter()
141 .map(NativeArgValue::from_wit)
142 .collect::<Result<Vec<_>, _>>()?,
143 ),
144 })
145 }
146}
147
148impl WitBoundary for NativeCandidateKind {
149 type Wit = WitCandidateKind;
150
151 fn to_wit(&self) -> Result<WitCandidateKind, String> {
152 Ok(match self {
153 NativeCandidateKind::Command => WitCandidateKind::Command,
154 NativeCandidateKind::Option => WitCandidateKind::Option,
155 NativeCandidateKind::File => WitCandidateKind::File,
156 NativeCandidateKind::Directory => WitCandidateKind::Directory,
157 NativeCandidateKind::Pattern => WitCandidateKind::Pattern,
158 NativeCandidateKind::Buffer => WitCandidateKind::Buffer,
159 NativeCandidateKind::Register => WitCandidateKind::Register,
160 NativeCandidateKind::Mark => WitCandidateKind::Mark,
161 NativeCandidateKind::Chord => WitCandidateKind::Chord,
162 NativeCandidateKind::Plain => WitCandidateKind::Plain,
163 NativeCandidateKind::Extension(tag) => WitCandidateKind::Extension(*tag),
164 })
165 }
166
167 fn from_wit(wit: WitCandidateKind) -> Result<Self, String> {
168 Ok(match wit {
169 WitCandidateKind::Command => NativeCandidateKind::Command,
170 WitCandidateKind::Option => NativeCandidateKind::Option,
171 WitCandidateKind::File => NativeCandidateKind::File,
172 WitCandidateKind::Directory => NativeCandidateKind::Directory,
173 WitCandidateKind::Pattern => NativeCandidateKind::Pattern,
174 WitCandidateKind::Buffer => NativeCandidateKind::Buffer,
175 WitCandidateKind::Register => NativeCandidateKind::Register,
176 WitCandidateKind::Mark => NativeCandidateKind::Mark,
177 WitCandidateKind::Chord => NativeCandidateKind::Chord,
178 WitCandidateKind::Plain => NativeCandidateKind::Plain,
179 WitCandidateKind::Extension(tag) => NativeCandidateKind::Extension(tag),
180 })
181 }
182}
183
184impl WitBoundary for NativeCandidateData {
185 type Wit = WitCandidateData;
186
187 fn to_wit(&self) -> Result<WitCandidateData, String> {
188 Ok(match self {
189 NativeCandidateData::Command { .. } => {
193 return Err(
194 "CandidateData::Command carries a recursive SourceLocation; it crosses \
195 with the provenance mirror (fragment §4.4)"
196 .to_string(),
197 );
198 }
199 NativeCandidateData::File { path, is_dir, size } => {
200 WitCandidateData::File(WitCandidateFile {
201 path: path_to_wit(path)?,
202 is_dir: *is_dir,
203 size: *size,
204 })
205 }
206 NativeCandidateData::Option {
207 name,
208 current_value,
209 doc,
210 } => WitCandidateData::Option(WitCandidateOption {
211 name: name.clone(),
212 current_value: current_value.clone(),
213 doc: doc.clone(),
214 }),
215 NativeCandidateData::OptionValue {
216 option_name,
217 value,
218 doc,
219 } => WitCandidateData::OptionValue(WitCandidateOptionValue {
220 option_name: option_name.clone(),
221 value: value.clone(),
222 doc: doc.clone(),
223 }),
224 NativeCandidateData::Chord {
225 chord,
226 mode_label,
227 doc,
228 } => WitCandidateData::Chord(WitCandidateChord {
229 chord: chord.clone(),
230 mode_label: mode_label.clone(),
231 doc: doc.clone(),
232 }),
233 NativeCandidateData::Register { name, preview } => {
234 WitCandidateData::Register(WitCandidateRegister {
235 name: *name,
236 preview: preview.clone(),
237 })
238 }
239 NativeCandidateData::Mark { name, position } => {
240 WitCandidateData::Mark(WitCandidateMark {
241 name: *name,
242 position: position.clone(),
243 })
244 }
245 NativeCandidateData::Plain => WitCandidateData::Plain,
246 NativeCandidateData::Extension { kind_id, payload } => {
247 WitCandidateData::Extension(WitCandidateExtension {
248 kind_id: *kind_id,
249 payload: payload.clone(),
250 })
251 }
252 })
253 }
254
255 fn from_wit(wit: WitCandidateData) -> Result<Self, String> {
256 Ok(match wit {
257 WitCandidateData::File(f) => NativeCandidateData::File {
258 path: PathBuf::from(f.path),
259 is_dir: f.is_dir,
260 size: f.size,
261 },
262 WitCandidateData::Option(o) => NativeCandidateData::Option {
263 name: o.name,
264 current_value: o.current_value,
265 doc: o.doc,
266 },
267 WitCandidateData::OptionValue(o) => NativeCandidateData::OptionValue {
268 option_name: o.option_name,
269 value: o.value,
270 doc: o.doc,
271 },
272 WitCandidateData::Chord(c) => NativeCandidateData::Chord {
273 chord: c.chord,
274 mode_label: c.mode_label,
275 doc: c.doc,
276 },
277 WitCandidateData::Register(r) => NativeCandidateData::Register {
278 name: r.name,
279 preview: r.preview,
280 },
281 WitCandidateData::Mark(m) => NativeCandidateData::Mark {
282 name: m.name,
283 position: m.position,
284 },
285 WitCandidateData::Plain => NativeCandidateData::Plain,
286 WitCandidateData::Extension(e) => NativeCandidateData::Extension {
287 kind_id: e.kind_id,
288 payload: e.payload,
289 },
290 })
291 }
292}
293
294impl WitBoundary for NativeRawCandidate {
295 type Wit = WitRawCandidate;
296
297 fn to_wit(&self) -> Result<WitRawCandidate, String> {
298 Ok(WitRawCandidate {
299 text: self.text.clone(),
300 insert_text: self.insert_text.clone(),
301 display: self.display.clone(),
302 source: self.source.as_ref().map(|s| s.0.clone()),
303 kind: self.kind.to_wit()?,
304 data: self.data.to_wit()?,
305 annotations: self
308 .annotations
309 .iter()
310 .map(WitBoundary::to_wit)
311 .collect::<Result<Vec<_>, String>>()?,
312 display_spans: Vec::new(),
319 })
320 }
321
322 fn from_wit(wit: WitRawCandidate) -> Result<Self, String> {
323 let spans = display_spans_from_wit(&wit.display, wit.display_spans);
325 Ok(NativeRawCandidate {
326 text: wit.text,
327 insert_text: wit.insert_text,
328 display: wit.display,
329 source: wit.source.map(SourceId),
330 kind: NativeCandidateKind::from_wit(wit.kind)?,
331 data: NativeCandidateData::from_wit(wit.data)?,
332 annotations: wit
333 .annotations
334 .into_iter()
335 .map(lattice_completion::candidate::Annotation::from_wit)
336 .collect::<Result<Vec<_>, String>>()?,
337 accept_action: None,
340 display_spans: spans,
344 })
345 }
346}
347
348fn display_spans_from_wit(
367 text: &str,
371 spans: Vec<crate::lattice::plugin_host::types::DisplaySpan>,
372) -> Vec<lattice_completion::candidate::DisplaySpan> {
373 spans
374 .into_iter()
375 .filter_map(|s| {
376 let (start, end) = (s.start as usize, s.end as usize);
377 if start >= end || end > text.len() {
378 tracing::warn!(
379 start,
380 end,
381 len = text.len(),
382 slot = %s.slot,
383 "picker candidate: display span out of range; dropped"
384 );
385 return None;
386 }
387 if !text.is_char_boundary(start) || !text.is_char_boundary(end) {
388 tracing::warn!(
389 start,
390 end,
391 slot = %s.slot,
392 "picker candidate: display span is not on a UTF-8 boundary \
393 (offsets are BYTES, not chars); dropped"
394 );
395 return None;
396 }
397 Some(lattice_completion::candidate::DisplaySpan {
398 range: start..end,
399 style: resolve_slot_style(&s.slot),
400 })
401 })
402 .collect()
403}
404
405fn resolve_slot_style(slot: &str) -> lattice_cells::style::Style {
416 lattice_syntax::style::name_to_style_with_theme(slot, None)
417}
418
419impl WitBoundary for NativePickerAcceptOutcome {
420 type Wit = WitPickerAcceptOutcome;
421
422 fn to_wit(&self) -> Result<WitPickerAcceptOutcome, String> {
423 Ok(match self {
424 NativePickerAcceptOutcome::OpenFile { path } => {
425 WitPickerAcceptOutcome::OpenFile(path_to_wit(path)?)
426 }
427 NativePickerAcceptOutcome::SwitchBuffer { buffer_id } => {
428 WitPickerAcceptOutcome::SwitchBuffer(*buffer_id)
429 }
430 NativePickerAcceptOutcome::JumpInBuffer {
431 buffer_id,
432 line,
433 col,
434 } => WitPickerAcceptOutcome::JumpInBuffer(WitJumpTarget {
435 buffer_id: *buffer_id,
436 line: *line,
437 col: *col,
438 }),
439 NativePickerAcceptOutcome::JumpToMark { name } => {
440 WitPickerAcceptOutcome::JumpToMark(*name)
441 }
442 NativePickerAcceptOutcome::JumpToLocation { path, line, col } => {
443 WitPickerAcceptOutcome::JumpToLocation(WitLocation {
444 path: path_to_wit(path)?,
445 line: *line,
446 col: *col,
447 })
448 }
449 NativePickerAcceptOutcome::InvokeCommand { id, args } => {
450 WitPickerAcceptOutcome::InvokeCommand(WitCommandRef {
451 id: id.clone(),
452 args: args.to_wit()?,
453 })
454 }
455 NativePickerAcceptOutcome::PasteRegister { name } => {
456 WitPickerAcceptOutcome::PasteRegister(*name)
457 }
458 NativePickerAcceptOutcome::ExpandSnippet { id } => {
459 WitPickerAcceptOutcome::ExpandSnippet(id.clone())
460 }
461 NativePickerAcceptOutcome::OpenLspLog { server_id } => {
462 WitPickerAcceptOutcome::OpenLspLog(server_id.clone())
463 }
464 NativePickerAcceptOutcome::OpenLspTraceLog { server_id } => {
465 WitPickerAcceptOutcome::OpenLspTraceLog(server_id.clone())
466 }
467 NativePickerAcceptOutcome::ApplyLspCodeAction { handle, index } => {
468 WitPickerAcceptOutcome::ApplyLspCodeAction(WitLspCodeActionRef {
469 handle: *handle,
470 index: *index,
471 })
472 }
473 NativePickerAcceptOutcome::ApplyLspCompletion { index } => {
474 WitPickerAcceptOutcome::ApplyLspCompletion(*index)
475 }
476 NativePickerAcceptOutcome::ApplyColorscheme { name } => {
477 WitPickerAcceptOutcome::ApplyColorscheme(name.clone())
478 }
479 NativePickerAcceptOutcome::LoadCommandLine { .. }
484 | NativePickerAcceptOutcome::LoadSearchLine { .. } => {
485 return Err(
486 "load-command/load-search are host-internal picker outcomes, not representable over WIT"
487 .into(),
488 );
489 }
490 NativePickerAcceptOutcome::WalkPaneHistory { .. } => {
491 return Err(
492 "walk-pane-history is a host-internal picker outcome, not representable over WIT"
493 .into(),
494 );
495 }
496 NativePickerAcceptOutcome::OpenPrompt { .. } => {
497 return Err(
498 "open-prompt is a host-internal picker outcome, not representable over WIT"
499 .into(),
500 );
501 }
502 NativePickerAcceptOutcome::FillCaller { .. } => {
508 return Err(
509 "fill-caller is a host-internal picker outcome, not representable over WIT"
510 .into(),
511 );
512 }
513 NativePickerAcceptOutcome::NoOp => WitPickerAcceptOutcome::NoOp,
514 })
515 }
516
517 fn from_wit(wit: WitPickerAcceptOutcome) -> Result<Self, String> {
518 Ok(match wit {
519 WitPickerAcceptOutcome::OpenFile(path) => NativePickerAcceptOutcome::OpenFile {
520 path: PathBuf::from(path),
521 },
522 WitPickerAcceptOutcome::SwitchBuffer(buffer_id) => {
523 NativePickerAcceptOutcome::SwitchBuffer { buffer_id }
524 }
525 WitPickerAcceptOutcome::JumpInBuffer(t) => NativePickerAcceptOutcome::JumpInBuffer {
526 buffer_id: t.buffer_id,
527 line: t.line,
528 col: t.col,
529 },
530 WitPickerAcceptOutcome::JumpToMark(name) => {
531 NativePickerAcceptOutcome::JumpToMark { name }
532 }
533 WitPickerAcceptOutcome::JumpToLocation(l) => {
534 NativePickerAcceptOutcome::JumpToLocation {
535 path: PathBuf::from(l.path),
536 line: l.line,
537 col: l.col,
538 }
539 }
540 WitPickerAcceptOutcome::InvokeCommand(c) => NativePickerAcceptOutcome::InvokeCommand {
541 id: c.id,
542 args: NativeArgs::from_wit(c.args)?,
543 },
544 WitPickerAcceptOutcome::PasteRegister(name) => {
545 NativePickerAcceptOutcome::PasteRegister { name }
546 }
547 WitPickerAcceptOutcome::ExpandSnippet(id) => {
548 NativePickerAcceptOutcome::ExpandSnippet { id }
549 }
550 WitPickerAcceptOutcome::OpenLspLog(server_id) => {
551 NativePickerAcceptOutcome::OpenLspLog { server_id }
552 }
553 WitPickerAcceptOutcome::OpenLspTraceLog(server_id) => {
554 NativePickerAcceptOutcome::OpenLspTraceLog { server_id }
555 }
556 WitPickerAcceptOutcome::ApplyLspCodeAction(r) => {
557 NativePickerAcceptOutcome::ApplyLspCodeAction {
558 handle: r.handle,
559 index: r.index,
560 }
561 }
562 WitPickerAcceptOutcome::ApplyLspCompletion(index) => {
563 NativePickerAcceptOutcome::ApplyLspCompletion { index }
564 }
565 WitPickerAcceptOutcome::ApplyColorscheme(name) => {
566 NativePickerAcceptOutcome::ApplyColorscheme { name }
567 }
568 WitPickerAcceptOutcome::NoOp => NativePickerAcceptOutcome::NoOp,
569 })
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 #![allow(clippy::unwrap_used, clippy::panic)]
576
577 use super::*;
578
579 fn assert_args_round_trips(native: NativeArgs) {
581 let wit = native.to_wit().expect("to_wit");
582 let back = NativeArgs::from_wit(wit).expect("from_wit");
583 assert_eq!(native, back);
584 }
585
586 #[test]
587 fn args_round_trip_covers_every_representable_variant() {
588 assert_args_round_trips(NativeArgs::None);
589 assert_args_round_trips(NativeArgs::Char('x'));
590 assert_args_round_trips(NativeArgs::String("hello".into()));
591 assert_args_round_trips(NativeArgs::Bytes(vec![0, 1, 2, 255]));
592 assert_args_round_trips(NativeArgs::List(vec![
593 NativeArgValue::String("s".into()),
594 NativeArgValue::Char('c'),
595 NativeArgValue::Bool(true),
596 NativeArgValue::Int(-42),
597 NativeArgValue::Pattern("re".into()),
598 NativeArgValue::Chord("<C-c>".into()),
599 NativeArgValue::Raw("body".into()),
600 ]));
601 }
602
603 #[test]
604 fn arg_value_int_survives_full_i64_range() {
605 for i in [i64::MIN, -1, 0, 1, i64::MAX] {
606 let v = NativeArgValue::Int(i);
607 let back = NativeArgValue::from_wit(v.to_wit().unwrap()).unwrap();
608 assert_eq!(v, back);
609 }
610 }
611
612 #[test]
613 fn nested_invocation_is_a_typed_error_not_a_panic() {
614 use lattice_grammar::CommandId;
615 use lattice_grammar::command::CommandInvocation;
616 let invocation = CommandInvocation::of(CommandId::new(0));
619 let native = NativeArgs::List(vec![NativeArgValue::Invocation(Box::new(invocation))]);
620 let err = native
621 .to_wit()
622 .expect_err("nested invocation must not cross yet");
623 assert!(err.contains("Invocation"), "error names the culprit: {err}");
624 }
625
626 fn raw(kind: NativeCandidateKind, data: NativeCandidateData) -> NativeRawCandidate {
629 NativeRawCandidate {
630 insert_text: None,
631 text: "text".into(),
632 display: "display".into(),
633 source: Some(SourceId("gen:files".into())),
634 kind,
635 data,
636 accept_action: None,
639 annotations: Vec::new(),
640 display_spans: Vec::new(),
641 }
642 }
643
644 fn assert_candidate_round_trips(native: NativeRawCandidate) {
645 let wit = native.to_wit().expect("to_wit");
646 let back = NativeRawCandidate::from_wit(wit).expect("from_wit");
647 assert_eq!(native, back);
648 }
649
650 #[test]
651 fn raw_candidate_round_trips_across_data_and_kind_variants() {
652 assert_candidate_round_trips(raw(NativeCandidateKind::Plain, NativeCandidateData::Plain));
653 assert_candidate_round_trips(raw(
654 NativeCandidateKind::File,
655 NativeCandidateData::File {
656 path: PathBuf::from("/home/alice/x.rs"),
657 is_dir: false,
658 size: Some(4096),
659 },
660 ));
661 assert_candidate_round_trips(raw(
662 NativeCandidateKind::Option,
663 NativeCandidateData::Option {
664 name: "wrap".into(),
665 current_value: "on".into(),
666 doc: "soft wrap".into(),
667 },
668 ));
669 assert_candidate_round_trips(raw(
670 NativeCandidateKind::Register,
671 NativeCandidateData::Register {
672 name: 'a',
673 preview: "yanked".into(),
674 },
675 ));
676 assert_candidate_round_trips(raw(
677 NativeCandidateKind::Extension(7),
678 NativeCandidateData::Extension {
679 kind_id: 7,
680 payload: vec![1, 2, 3],
681 },
682 ));
683 }
684
685 #[test]
686 fn candidate_data_command_is_a_typed_error() {
687 use lattice_grammar::source::{SourceKind, SourceLayer, SourceLocation};
688 let data = NativeCandidateData::Command {
689 name: "quit".into(),
690 doc: "quit".into(),
691 kind_label: "ex-command".into(),
692 source: SourceLocation {
693 layer: SourceLayer::Builtin,
694 kind: SourceKind::Synthetic("<builtin>".into()),
695 },
696 };
697 let err = data.to_wit().expect_err("Command must not cross yet");
698 assert!(err.contains("Command"), "error names the culprit: {err}");
699 }
700
701 fn assert_outcome_round_trips(native: NativePickerAcceptOutcome) {
704 let wit = native.to_wit().expect("to_wit");
705 let back = NativePickerAcceptOutcome::from_wit(wit).expect("from_wit");
706 assert_eq!(format!("{native:?}"), format!("{back:?}"));
708 }
709
710 #[test]
711 fn picker_accept_outcome_round_trips_every_variant() {
712 for outcome in [
713 NativePickerAcceptOutcome::OpenFile {
714 path: PathBuf::from("/a/b.rs"),
715 },
716 NativePickerAcceptOutcome::SwitchBuffer { buffer_id: 3 },
717 NativePickerAcceptOutcome::JumpInBuffer {
718 buffer_id: 3,
719 line: 10,
720 col: 4,
721 },
722 NativePickerAcceptOutcome::JumpToMark { name: 'a' },
723 NativePickerAcceptOutcome::JumpToLocation {
724 path: PathBuf::from("/a/b.rs"),
725 line: 1,
726 col: 0,
727 },
728 NativePickerAcceptOutcome::InvokeCommand {
729 id: "write".into(),
730 args: NativeArgs::List(vec![NativeArgValue::Bool(true)]),
731 },
732 NativePickerAcceptOutcome::PasteRegister { name: '+' },
733 NativePickerAcceptOutcome::ExpandSnippet { id: "fn".into() },
734 NativePickerAcceptOutcome::OpenLspLog {
735 server_id: "rust-analyzer".into(),
736 },
737 NativePickerAcceptOutcome::OpenLspTraceLog {
738 server_id: "rust-analyzer".into(),
739 },
740 NativePickerAcceptOutcome::ApplyLspCodeAction {
741 handle: 42,
742 index: 1,
743 },
744 NativePickerAcceptOutcome::ApplyLspCompletion { index: 2 },
745 NativePickerAcceptOutcome::ApplyColorscheme {
746 name: "nord".into(),
747 },
748 NativePickerAcceptOutcome::NoOp,
749 ] {
750 assert_outcome_round_trips(outcome);
751 }
752 }
753
754 #[cfg(unix)]
755 #[test]
756 fn non_utf8_path_is_a_typed_error_not_a_lossy_cross() {
757 use std::os::unix::ffi::OsStrExt;
758 let bad = PathBuf::from(std::ffi::OsStr::from_bytes(b"/inv\xff/path"));
759 let outcome = NativePickerAcceptOutcome::OpenFile { path: bad };
760 let err = outcome.to_wit().expect_err("non-UTF-8 path must not cross");
761 assert!(err.contains("UTF-8"), "error explains why: {err}");
762 }
763
764 use crate::lattice::plugin_host::types::DisplaySpan as WitDisplaySpan;
767
768 fn wit_span(start: u32, end: u32, slot: &str) -> WitDisplaySpan {
769 WitDisplaySpan {
770 start,
771 end,
772 slot: slot.to_string(),
773 }
774 }
775
776 fn candidate_with_spans(display: &str, spans: Vec<WitDisplaySpan>) -> NativeRawCandidate {
777 let wit = WitRawCandidate {
778 text: display.to_string(),
779 insert_text: None,
780 display: display.to_string(),
781 source: None,
782 kind: WitCandidateKind::Plain,
783 data: WitCandidateData::Plain,
784 annotations: Vec::new(),
785 display_spans: spans,
786 };
787 NativeRawCandidate::from_wit(wit).expect("from_wit")
788 }
789
790 #[test]
794 fn guest_display_spans_cross_and_resolve_their_slot() {
795 let c = candidate_with_spans(
796 "Reading list (books)",
797 vec![wit_span(0, 12, "text.title.1"), wit_span(12, 21, "comment")],
798 );
799 assert_eq!(c.display_spans.len(), 2, "got {:?}", c.display_spans);
800 assert_eq!(c.display_spans[0].range, 0..12);
801 assert_eq!(
802 c.display_spans[0].style,
803 lattice_syntax::style::name_to_style_pub("text.title.1"),
804 "the slot must resolve exactly as the capture name does"
805 );
806 assert_eq!(
807 c.display_spans[1].style,
808 lattice_syntax::style::name_to_style_pub("comment")
809 );
810 }
811
812 #[test]
820 fn a_span_off_a_utf8_boundary_is_dropped() {
821 let c = candidate_with_spans("Café notes", vec![wit_span(0, 4, "text.title.1")]);
822 assert!(
823 c.display_spans.is_empty(),
824 "a mid-codepoint span must be dropped, got {:?}",
825 c.display_spans
826 );
827 let ok = candidate_with_spans("Café notes", vec![wit_span(0, 5, "text.title.1")]);
830 assert_eq!(ok.display_spans.len(), 1);
831 }
832
833 #[test]
837 fn malformed_spans_are_dropped_without_taking_the_row_with_them() {
838 let c = candidate_with_spans(
839 "abcdef",
840 vec![
841 wit_span(0, 3, "keyword"), wit_span(4, 99, "comment"), wit_span(5, 2, "comment"), wit_span(3, 3, "comment"), ],
846 );
847 assert_eq!(c.display_spans.len(), 1, "got {:?}", c.display_spans);
848 assert_eq!(c.display_spans[0].range, 0..3);
849 }
850
851 #[test]
855 fn an_unknown_slot_keeps_the_run_unstyled() {
856 let c = candidate_with_spans("abcdef", vec![wit_span(0, 3, "not.a.real.capture")]);
857 assert_eq!(c.display_spans.len(), 1);
858 assert_eq!(
859 c.display_spans[0].style,
860 lattice_cells::style::Style::Default
861 );
862 }
863
864 #[test]
867 fn a_candidate_with_no_spans_is_unchanged() {
868 let c = candidate_with_spans("plain row", Vec::new());
869 assert!(c.display_spans.is_empty());
870 }
871}