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}