Skip to main content

lattice_mode/
context_source.rs

1//! The native seam for an async producer of structural context scopes (TC.2).
2//!
3//! The tree-sitter-context analogue of [`AsyncGutterDecorationSource`]: the host
4//! drives it OFF the render path on a trigger (a completed reparse), caches the
5//! returned scopes per buffer, and every later read is native. The renderer
6//! never touches a producer.
7//!
8//! The trait lives here rather than in `lattice-plugin-host` for the reason the
9//! decoration one does: `lattice-mode` and the renderers must stay free of any
10//! wasmtime dependency, so the loader hands a trait object across the seam. The
11//! single implementor today is the plugin host's `WasmContextSource`.
12//!
13//! **Why scopes and not rows.** A [`ContextScope`] is a pure function of the
14//! parse tree — no viewport, no cursor, no options — so the host can cache the
15//! set per parse version and answer "which apply to THIS pane right now" itself
16//! with [`resolve_context`](lattice_cells::context::resolve_context). A producer
17//! that returned finished rows would have to be re-driven on every scroll and
18//! cursor move, which is a WASM call on the keystroke path (paramount #1) and a
19//! cache that thrashes by construction.
20//!
21//! Design anchor: `docs/dev/architecture/treesitter-context.md`.
22//!
23//! [`AsyncGutterDecorationSource`]: crate::AsyncGutterDecorationSource
24
25use std::any::Any;
26use std::future::Future;
27use std::path::PathBuf;
28use std::pin::Pin;
29use std::sync::Arc;
30
31use arc_swap::ArcSwap;
32use lattice_cells::context::ContextScope;
33
34/// The boxed future an [`AsyncContextSource::produce`] returns.
35///
36/// `Ok(scopes)` replaces the buffer's cached scope set; `Err(reason)` means
37/// **keep the prior cached set** — never "clear". A failed refresh must not
38/// blank the sticky strip: a transient error would otherwise read as the
39/// feature breaking rather than as one refresh that did not land. Same contract
40/// as [`DecorationFuture`](crate::DecorationFuture).
41pub type ContextFuture<'a> =
42    Pin<Box<dyn Future<Output = Result<Vec<ContextScope>, String>> + Send + 'a>>;
43
44/// An async, off-render-path producer of a buffer's structural context scopes.
45///
46/// The renderer NEVER calls this, and neither does anything on the keystroke
47/// path. The host drives it when a reparse completes, stamps the result with the
48/// parse version, and publishes it; resolution against a pane's anchor and
49/// viewport is native and happens per publish.
50pub trait AsyncContextSource: Send + Sync + std::fmt::Debug {
51    /// Stable id of the producing plugin — the teardown key. Two producers with
52    /// the same id are the same plugin (a reload replaces rather than
53    /// duplicates), mirroring
54    /// [`AsyncGutterDecorationSource::source_id`](crate::AsyncGutterDecorationSource::source_id).
55    fn source_id(&self) -> u64;
56
57    /// Produce this buffer's context scopes off the render path.
58    ///
59    /// `path` is the buffer's on-disk path when it has one; `line_count` bounds
60    /// the addressable lines.
61    ///
62    /// `syntax` is the buffer's parse snapshot, **type-erased** — the same
63    /// `Option<Arc<dyn Any + Send + Sync>>` shape `ActionContext::syntax` uses
64    /// (`lattice-grammar`), and for the same reason: `lattice-mode` must not
65    /// depend on `lattice-syntax`, so the implementor downcasts. `None` means
66    /// the buffer has no parse (plain text, or one still pending), and a
67    /// producer with nothing to work from returns an empty list rather than an
68    /// error — "no tree yet" is a normal state, not a failure.
69    ///
70    /// The caller acquires `syntax` at the same instant as `line_count` so the
71    /// tree and the text agree on version (the tree-sitter seam's §7 rule).
72    ///
73    /// See [`ContextFuture`] for the `Ok`/`Err` contract.
74    fn produce(
75        &self,
76        buffer_id: u64,
77        path: Option<PathBuf>,
78        line_count: u32,
79        syntax: Option<Arc<dyn Any + Send + Sync>>,
80    ) -> ContextFuture<'_>;
81}
82
83/// Runtime-mutable registry of [`AsyncContextSource`]s.
84///
85/// The plugin loader RCU-registers a loaded context plugin's producer here
86/// (`drain_context`); the host's reparse-driven refresh reads a wait-free
87/// snapshot to drive them. Named generically (not `Wasm…`) because it holds
88/// native trait objects — the WASM source is one implementor among potential
89/// natives, exactly as with [`GutterDecorationSourceRegistry`].
90///
91/// [`GutterDecorationSourceRegistry`]: crate::GutterDecorationSourceRegistry
92#[derive(Default, Clone)]
93pub struct ContextSourceRegistry {
94    sources: Vec<Arc<dyn AsyncContextSource>>,
95}
96
97impl std::fmt::Debug for ContextSourceRegistry {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        // Trait objects aren't usefully printable beyond their count; keep the
100        // Debug impl cheap (this rides `Editor: Debug` through the handle).
101        f.debug_struct("ContextSourceRegistry")
102            .field("sources", &self.sources.len())
103            .finish()
104    }
105}
106
107impl ContextSourceRegistry {
108    /// An empty registry.
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Register a producer. Idempotent per `source_id`: a re-register (reload)
114    /// replaces the prior producer for that id rather than accumulating a
115    /// duplicate.
116    pub fn register(&mut self, source: Arc<dyn AsyncContextSource>) {
117        let id = source.source_id();
118        self.sources.retain(|s| s.source_id() != id);
119        self.sources.push(source);
120    }
121
122    /// Unregister every producer for `source_id`; returns the count removed
123    /// (the teardown-report increment). No-op when absent — idempotent, per the
124    /// teardown contract.
125    pub fn unregister(&mut self, source_id: u64) -> usize {
126        let before = self.sources.len();
127        self.sources.retain(|s| s.source_id() != source_id);
128        before - self.sources.len()
129    }
130
131    /// A wait-free snapshot of the registered producers (cheap `Arc` clones) —
132    /// what the host's refresh iterates.
133    pub fn sources(&self) -> Vec<Arc<dyn AsyncContextSource>> {
134        self.sources.clone()
135    }
136
137    /// True when no producer is registered.
138    pub fn is_empty(&self) -> bool {
139        self.sources.is_empty()
140    }
141
142    /// Number of registered producers.
143    pub fn len(&self) -> usize {
144        self.sources.len()
145    }
146}
147
148/// Boot-service handle: `Arc<ArcSwap<…>>` so the loader RCU-registers producers
149/// at runtime while the host reads wait-free. Register **and** look up with this
150/// exact alias (the `ServiceRegistry` TypeId rule).
151pub type ContextSourceRegistryHandle = Arc<ArcSwap<ContextSourceRegistry>>;