Skip to main content

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}