lattice_mode/decoration_source.rs
1//! The async gutter-decoration producer seam (PL8.E).
2//!
3//! `Mode::gutter_decorations` (see [`crate::contributions`]) is the **sync**,
4//! read-per-frame decoration trait native modes (diff, LSP severity) satisfy
5//! inline. A WASM plugin can NOT satisfy it that way — running the guest per
6//! frame from the renderer would violate paramount goal #1 (no UI-thread WASM).
7//!
8//! Instead a plugin decoration provider is an [`AsyncGutterDecorationSource`]:
9//! the host polls it OFF the render path on a trigger (edit / scroll /
10//! producer (de)registration), caches the returned `Vec<GutterDecoration>` per
11//! buffer, and the renderer reads only that native cache — never the producer.
12//!
13//! The trait + registry live here (not in `lattice-plugin-host`) so the
14//! substrate-neutral `lattice-mode` and the renderers (which read only the
15//! cached `GutterDecoration`s) stay free of any wasmtime dependency. The single
16//! implementor today is the plugin host's `WasmDecorationSource`; the
17//! indirection mirrors `AsyncCompletionSource` (lattice-completion) and
18//! `PickerSourceGenerator` (lattice-picker) — a generic native trait a WASM
19//! source implements, so the loader hands a trait object across the seam.
20
21use std::future::Future;
22use std::path::PathBuf;
23use std::pin::Pin;
24use std::sync::Arc;
25
26use arc_swap::ArcSwap;
27
28use crate::GutterDecoration;
29
30/// The boxed future an [`AsyncGutterDecorationSource::produce`] returns.
31///
32/// `Ok(marks)` replaces the buffer's cached decorations; `Err(reason)` means
33/// **keep the prior cached snapshot** (no flicker, §8) — never "clear".
34pub type DecorationFuture<'a> =
35 Pin<Box<dyn Future<Output = Result<Vec<GutterDecoration>, String>> + Send + 'a>>;
36
37/// An async, off-render-path producer of a buffer's gutter decorations.
38///
39/// The renderer NEVER calls this. The host's per-tick decoration pump drives it
40/// on a trigger, writes the result into the per-buffer cache, and the renderer
41/// merges the cached marks into the same gutter partition it walks for
42/// [`Mode::gutter_decorations`](crate::Mode::gutter_decorations).
43pub trait AsyncGutterDecorationSource: Send + Sync + std::fmt::Debug {
44 /// Stable id of the producing plugin — the teardown key
45 /// ([`GutterDecorationSourceRegistry::unregister`]). Two producers with the
46 /// same id are the same plugin (reload replaces rather than duplicates).
47 fn source_id(&self) -> u64;
48
49 /// Produce this buffer's gutter decorations off the render path. `path` is
50 /// the buffer's on-disk path when it has one (a provider may key marks off
51 /// it); `line_count` bounds the addressable lines. See [`DecorationFuture`]
52 /// for the `Ok`/`Err` contract.
53 fn produce(
54 &self,
55 buffer_id: u64,
56 path: Option<PathBuf>,
57 line_count: u32,
58 ) -> DecorationFuture<'_>;
59}
60
61/// Runtime-mutable registry of [`AsyncGutterDecorationSource`]s.
62///
63/// The plugin loader RCU-registers a loaded decoration plugin's producer here
64/// (`drain_decorations`); the host's per-tick refresh reads a wait-free
65/// snapshot to drive them. Named generically (not `Wasm…`) because it holds
66/// native trait objects — mirrors `PickerRegistry`, whose WASM source is one
67/// implementor among potential natives.
68#[derive(Default, Clone)]
69pub struct GutterDecorationSourceRegistry {
70 sources: Vec<Arc<dyn AsyncGutterDecorationSource>>,
71}
72
73impl std::fmt::Debug for GutterDecorationSourceRegistry {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 // Trait objects aren't usefully printable beyond their count; keep the
76 // Debug impl cheap (this rides `Editor: Debug` through the handle).
77 f.debug_struct("GutterDecorationSourceRegistry")
78 .field("sources", &self.sources.len())
79 .finish()
80 }
81}
82
83impl GutterDecorationSourceRegistry {
84 /// An empty registry.
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 /// Register a producer. Idempotent per `source_id`: a re-register (reload)
90 /// replaces the prior producer for that id rather than accumulating a
91 /// duplicate.
92 pub fn register(&mut self, source: Arc<dyn AsyncGutterDecorationSource>) {
93 let id = source.source_id();
94 self.sources.retain(|s| s.source_id() != id);
95 self.sources.push(source);
96 }
97
98 /// Unregister every producer for `source_id`; returns the count removed
99 /// (the teardown-report increment). No-op when absent — idempotent, per the
100 /// teardown contract.
101 pub fn unregister(&mut self, source_id: u64) -> usize {
102 let before = self.sources.len();
103 self.sources.retain(|s| s.source_id() != source_id);
104 before - self.sources.len()
105 }
106
107 /// A wait-free snapshot of the registered producers (cheap `Arc` clones) —
108 /// what the host's refresh iterates.
109 pub fn sources(&self) -> Vec<Arc<dyn AsyncGutterDecorationSource>> {
110 self.sources.clone()
111 }
112
113 /// True when no producer is registered.
114 pub fn is_empty(&self) -> bool {
115 self.sources.is_empty()
116 }
117
118 /// Number of registered producers.
119 pub fn len(&self) -> usize {
120 self.sources.len()
121 }
122}
123
124/// Boot-service handle: `Arc<ArcSwap<…>>` so the loader RCU-registers producers
125/// at runtime while the host reads wait-free. Register **and** look up with this
126/// exact alias (the `ServiceRegistry` TypeId rule).
127pub type GutterDecorationSourceRegistryHandle = Arc<ArcSwap<GutterDecorationSourceRegistry>>;
128
129/// The counter a guest bumps to say "my decorations changed, though the
130/// document did not" (OA.30).
131///
132/// ## The hole this fills
133///
134/// `maybe_refresh_wasm_decorations` re-runs producers on exactly two triggers:
135/// the producer registry changed, or this buffer's cached `document_version`
136/// moved. Both are about things the HOST can see. A producer whose output
137/// depends on its own view-local state — the agenda's bulk marks are the case
138/// that found this — changes neither, so its first answer is cached forever and
139/// every later toggle paints nothing.
140///
141/// That is the same shape `refresh-view` (OA.15a) closed one level up: a guest
142/// could compute a new answer and had no way to say so. This is the gutter's
143/// version of it.
144///
145/// ## Why a bare counter, and not a per-buffer map
146///
147/// The refresh pump only ever refetches the ACTIVE buffer, so a bump means "the
148/// next tick should refetch" and nothing finer would be read. A per-buffer key
149/// would be a more precise answer to a question nobody asks, and the cost of
150/// being coarse is one extra producer call — off the actor thread, on a tick,
151/// for a buffer that is on screen anyway.
152#[derive(Debug, Default)]
153pub struct DecorationEpoch(std::sync::atomic::AtomicU64);
154
155impl DecorationEpoch {
156 /// Say that a producer's answer has changed. Cheap enough to call per
157 /// keystroke: one relaxed increment.
158 pub fn bump(&self) {
159 self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
160 }
161
162 /// The current value, for the refresh gate to compare against its last.
163 pub fn get(&self) -> u64 {
164 self.0.load(std::sync::atomic::Ordering::Relaxed)
165 }
166}
167
168/// Register **and** look up with this exact alias (the `ServiceRegistry` TypeId
169/// rule).
170pub type DecorationEpochHandle = Arc<DecorationEpoch>;