lattice_plugin_host/picker_host.rs
1//! The picker-source guest world (PH7.4c.1a).
2//!
3//! A WASM picker source implements the `picker-source-plugin` world: it exports
4//! `register-picker-sources`/`init`/`accept` and imports the `host-services`
5//! `walk` seam (PH7.4b) plus the `picker-registry` seam it declares through.
6//! This module holds the **second `bindgen!`** for that world — the
7//! two-bindgen-with-shared-types trick (the `with:` map points `types` +
8//! `host-services` at the `plugin` world's generated modules so the crossed
9//! values are the SAME Rust types the boundary round-trips, PH7.3d precedent).
10//!
11//! The per-plugin actor task that *drives* these async exports lands at
12//! PH7.4c.1b; the `Arc<dyn PickerSourceGenerator>` adapter + registration at
13//! PH7.4c.2. This slice lands the world + its generated bindings.
14//!
15//! **Deferred — the `document` handle.** The active buffer's bulk text should
16//! ride a `borrow<document>` handle in `init` (PH7.3c `DocumentResource`, the
17//! §4.2 read-back model). Passing a *host-owned* resource into a guest **export**
18//! has a bindgen-modeling subtlety (a resource referenced only by an exported
19//! signature is not seen as a host `with`-mapped import), so it is carved into a
20//! focused follow-up. It does not block the ⭐ exit: the `fuzzy-finder`/`files`
21//! source reads no buffer text (it walks the fs via `host-services`); only a
22//! text-reading source (`:picker lines`) needs the handle.
23
24pub(crate) mod bindings {
25 wasmtime::component::bindgen!({
26 world: "picker-source-plugin",
27 path: "../lattice-wit/wit",
28 // The guest exports (`init`/`accept`/`spec`) are async — a picker source
29 // call suspends the guest stack, never pins the caller's thread.
30 exports: { default: async },
31 with: {
32 // Reuse the `plugin` world's generated mirrors so a value crossing
33 // here is the same Rust type `WitBoundary` round-trips (not a fresh,
34 // incompatible copy).
35 "lattice:plugin-host/types": crate::lattice::plugin_host::types,
36 "lattice:plugin-host/host-services": crate::lattice::plugin_host::host_services,
37 "lattice:plugin-host/logging": crate::lattice::plugin_host::logging,
38 },
39 });
40}
41
42/// OR.5b: the specs a guest declared during `register-picker-sources`.
43///
44/// Recorded on the plugin's `PluginState` and drained by the actor spawn after
45/// the export returns — the `GrammarContributions` / `ModeContributions` shape,
46/// and for the same reason: a guest registers by *calling*, so the host needs
47/// somewhere for those calls to land before it knows how many there were.
48#[derive(Debug, Default)]
49pub struct PickerContributions {
50 specs: Vec<lattice_picker::source::PickerSourceSpec>,
51}
52
53impl PickerContributions {
54 /// Record one declared source.
55 ///
56 /// A repeat of an id this plugin already declared REPLACES it rather than
57 /// appending: a guest that registers twice under one name means the second
58 /// one, and appending would register two generators the registry then
59 /// resolves by insertion order — which is a coin flip dressed as a rule.
60 pub fn push(&mut self, spec: lattice_picker::source::PickerSourceSpec) {
61 if let Some(existing) = self.specs.iter_mut().find(|s| s.id == spec.id) {
62 *existing = spec;
63 return;
64 }
65 self.specs.push(spec);
66 }
67
68 /// Take everything declared, leaving the store empty.
69 pub fn take(&mut self) -> Vec<lattice_picker::source::PickerSourceSpec> {
70 std::mem::take(&mut self.specs)
71 }
72
73 pub fn is_empty(&self) -> bool {
74 self.specs.is_empty()
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 #![allow(clippy::unwrap_used)]
81
82 use super::*;
83 use lattice_picker::source::PickerSourceSpec;
84
85 #[test]
86 fn declaring_several_sources_keeps_them_all() {
87 let mut c = PickerContributions::default();
88 c.push(PickerSourceSpec::no_args("a", "first"));
89 c.push(PickerSourceSpec::no_args("b", "second"));
90 let specs = c.take();
91 assert_eq!(specs.len(), 2, "the whole point of the slice");
92 assert!(c.is_empty(), "take leaves the store empty");
93 }
94
95 /// A guest that registers twice under one id means the second one. Appending
96 /// would register two generators the registry resolves by insertion order,
97 /// which is a coin flip dressed as a rule.
98 #[test]
99 fn re_declaring_an_id_replaces_rather_than_appends() {
100 let mut c = PickerContributions::default();
101 c.push(PickerSourceSpec::no_args("a", "first"));
102 c.push(PickerSourceSpec::no_args("a", "second"));
103 let specs = c.take();
104 assert_eq!(specs.len(), 1);
105 assert_eq!(specs[0].doc, "second");
106 }
107}