Skip to main content

lattice_plugin_host/
decoration_source.rs

1//! PH7.9c — the `WasmDecorationSource` adapter (the async-produce path).
2//!
3//! Wraps a decoration plugin's [`DecorationClient`] bridge and exposes a
4//! **native**-typed producer the host polls off the render path. Like
5//! `WasmCompletionSource` (PH7.6), this is NOT an `Arc<dyn>` impl of the sync
6//! `Mode::gutter_decorations` trait — that trait is read *per frame* by the
7//! renderer, and a WASM mode can't satisfy it inline (per-frame WASM =
8//! paramount-#1 violation). Instead the host calls this producer on a trigger
9//! (edit / scroll / diagnostic change), caches the returned
10//! `Vec<GutterDecoration>` per buffer, and the renderer reads the cache. This
11//! adapter is the host-facing "decoration source" a boot-wired `Editor` will poll
12//! (the renderer-reads-the-cache wiring is the Phase-8 boot-wiring step).
13
14use lattice_mode::GutterDecoration;
15
16use crate::boundary_decoration::project_decoration_context;
17use crate::{DecorationClient, PluginId};
18
19/// An async gutter-decoration producer over a plugin's [`DecorationClient`].
20/// Cheap to clone (the client is an mpsc `Sender` clone); every clone talks to
21/// the same actor / `Store`.
22#[derive(Clone, Debug)]
23pub struct WasmDecorationSource {
24    client: DecorationClient,
25    /// SG.3b: the sign registry, for resolving a placement's NAME to the
26    /// interned id the native placement carries.
27    ///
28    /// The HANDLE, not a loaded snapshot — a plugin loading later stores a new
29    /// inner `Arc`, and a captured snapshot would keep answering from the
30    /// registry as it was when this source was built, so every later plugin's
31    /// signs would silently skip.
32    ///
33    /// `Option` for harnesses that wire no registry: sign placements are then
34    /// skipped (logged) while diff and severity marks still cross, which is
35    /// the same degradation every other unwired seam here takes.
36    signs: Option<lattice_mode::SignRegistryHandle>,
37}
38
39/// PL8.E: expose the WASM producer as the native
40/// [`AsyncGutterDecorationSource`](lattice_mode::AsyncGutterDecorationSource) the
41/// host's decoration registry holds — the same trait-object indirection the
42/// completion (`AsyncCompletionSource`) and picker (`PickerSourceGenerator`)
43/// seams use, so `lattice-host` and the renderers never name this crate.
44impl lattice_mode::AsyncGutterDecorationSource for WasmDecorationSource {
45    fn source_id(&self) -> u64 {
46        self.plugin_id().0 as u64
47    }
48
49    fn produce(
50        &self,
51        buffer_id: u64,
52        path: Option<std::path::PathBuf>,
53        line_count: u32,
54    ) -> lattice_mode::DecorationFuture<'_> {
55        Box::pin(async move {
56            self.gutter_decorations(buffer_id, path.as_deref(), line_count)
57                .await
58        })
59    }
60}
61
62impl WasmDecorationSource {
63    /// Build the adapter over a client bridge. (No `connect`/`spec` round-trip
64    /// like completion — a decoration provider has no id/doc metadata; it is a
65    /// pure producer keyed by the mode that owns it.)
66    pub fn new(client: DecorationClient, signs: Option<lattice_mode::SignRegistryHandle>) -> Self {
67        Self { client, signs }
68    }
69
70    /// The host-issued id of the plugin behind this source.
71    pub fn plugin_id(&self) -> PluginId {
72        self.client.id()
73    }
74
75    /// Produce the per-line gutter decorations for a buffer — the async producer
76    /// the host calls OFF the render path. Projects the owned
77    /// [`decoration-context`](crate::decoration_task::DecorationContext) from the
78    /// buffer metadata, calls the guest, and converts the result to native
79    /// [`GutterDecoration`]s.
80    ///
81    /// Graceful (§8, no flicker): the outer host error (trap / plugin-gone) and
82    /// the inner guest WIT `err` both collapse to the `String` the caller logs —
83    /// on an `Err`, the caller keeps the buffer's *prior* cached snapshot rather
84    /// than clearing it, so decoration cues never blink mid-refresh. A candidate
85    /// that fails to cross (malformed record) fails the whole batch as a typed
86    /// error, never a silent drop.
87    pub async fn gutter_decorations(
88        &self,
89        buffer_id: u64,
90        path: Option<&std::path::Path>,
91        line_count: u32,
92    ) -> Result<Vec<GutterDecoration>, String> {
93        let ctx = project_decoration_context(buffer_id, path, line_count);
94        let wit = match self.client.produce(ctx).await {
95            Ok(inner) => inner?,
96            Err(host_err) => return Err(format!("decoration plugin: {host_err}")),
97        };
98        // SG.3b: resolve sign NAMES to interned ids here, at the boundary and
99        // off the render path — which is what lets a native placement stay
100        // `Copy` with no per-line `String`. A name nothing has defined yields
101        // `None` and is skipped rather than failing the batch, so one
102        // unregistered sign does not take the plugin's diff and severity marks
103        // down with it.
104        //
105        // Loaded per call, not captured at construction: a plugin loading
106        // later stores a NEW inner `Arc`, and a snapshot taken when this
107        // source was built would silently skip every sign defined after it.
108        let registry = self.signs.as_ref().map(|h| h.load_full());
109        let empty;
110        let registry = match registry.as_deref() {
111            Some(r) => r,
112            None => {
113                empty = lattice_mode::SignRegistry::new();
114                &empty
115            }
116        };
117        let mut out = Vec::with_capacity(wit.len());
118        for w in wit {
119            if let Some(deco) = crate::boundary_decoration::decoration_from_wit(w, registry)? {
120                out.push(deco);
121            }
122        }
123        Ok(out)
124    }
125}