lattice_plugin_host/context_source.rs
1//! TC.2 — the `WasmContextSource` adapter (the async-produce path).
2//!
3//! Wraps a context plugin's [`ContextClient`] bridge and exposes the **native**
4//! [`AsyncContextSource`](lattice_mode::AsyncContextSource) the host's registry
5//! holds — the same trait-object indirection the completion / picker /
6//! decoration seams use, so `lattice-host` and the renderers never name this
7//! crate.
8//!
9//! The host drives this when a reparse completes, stamps the result with the
10//! parse version, and caches it. Nothing on the keystroke path calls it: the
11//! per-pane resolution that runs at cursor rate reads the *cache*
12//! (`lattice_cells::context::resolve_context`), never a producer.
13
14use std::sync::Arc;
15
16use lattice_cells::context::ContextScope;
17use lattice_syntax::SyntaxSnapshot;
18
19use crate::WitBoundary;
20use crate::boundary_context::project_context_request;
21use crate::{ContextClient, PluginId};
22
23/// An async context-scope producer over a plugin's [`ContextClient`]. Cheap to
24/// clone (the client is an mpsc `Sender` clone); every clone talks to the same
25/// actor / `Store`.
26#[derive(Clone, Debug)]
27pub struct WasmContextSource {
28 client: ContextClient,
29}
30
31impl lattice_mode::AsyncContextSource for WasmContextSource {
32 fn source_id(&self) -> u64 {
33 self.plugin_id().0 as u64
34 }
35
36 fn produce(
37 &self,
38 buffer_id: u64,
39 path: Option<std::path::PathBuf>,
40 line_count: u32,
41 syntax: Option<Arc<dyn std::any::Any + Send + Sync>>,
42 ) -> lattice_mode::ContextFuture<'_> {
43 // Downcast the type-erased snapshot here, at the one place that knows
44 // both sides — `lattice-mode` must not depend on `lattice-syntax` (the
45 // `ActionContext::syntax` precedent). A snapshot of some other type is
46 // indistinguishable from "no parse" and degrades to an empty result
47 // rather than an error: it is a host wiring mistake, not a plugin
48 // failure, and blanking the strip would misattribute it.
49 let tree = syntax.and_then(|any| any.downcast::<SyntaxSnapshot>().ok());
50 Box::pin(async move {
51 self.context_scopes(buffer_id, path.as_deref(), line_count, tree)
52 .await
53 })
54 }
55}
56
57impl WasmContextSource {
58 /// Build the adapter over a client bridge. (No `connect`/`spec` round-trip
59 /// like completion — a context provider has no id/doc metadata; it is a pure
60 /// producer keyed by the plugin that owns it.)
61 pub fn new(client: ContextClient) -> Self {
62 Self { client }
63 }
64
65 /// The host-issued id of the plugin behind this source.
66 pub fn plugin_id(&self) -> PluginId {
67 self.client.id()
68 }
69
70 /// Produce the structural context scopes for a buffer — the async producer
71 /// the host calls OFF the render path when a reparse lands.
72 ///
73 /// Graceful (§8, no blanking): the outer host error (trap / plugin-gone) and
74 /// the inner guest WIT `err` both collapse to the `String` the caller logs —
75 /// on an `Err` the caller keeps the buffer's *prior* cached scopes rather
76 /// than clearing them, so a failed refresh never reads as the feature
77 /// breaking. A scope that fails to cross fails the whole batch as a typed
78 /// error, never a silent drop.
79 pub async fn context_scopes(
80 &self,
81 buffer_id: u64,
82 path: Option<&std::path::Path>,
83 line_count: u32,
84 tree: Option<Arc<SyntaxSnapshot>>,
85 ) -> Result<Vec<ContextScope>, String> {
86 let req = project_context_request(buffer_id, path, line_count);
87 let wit = match self.client.produce(req, tree).await {
88 Ok(inner) => inner?,
89 Err(host_err) => return Err(format!("context plugin: {host_err}")),
90 };
91 wit.into_iter().map(ContextScope::from_wit).collect()
92 }
93}