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>>;