Skip to main content

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}