Skip to main content

lattice_plugin_host/
events_host.rs

1//! The event/hook guest world (PH7.8b).
2//!
3//! An event-observing plugin implements the `events-plugin` world: it **imports**
4//! the `events` subscription API (host-provided) and `host-services`, and
5//! **exports** `register-events` (the host calls it once to drive subscription
6//! registration) and `on-event` (host→guest delivery). This module holds the
7//! **fifth `bindgen!`** (after `plugin`, `picker-source-plugin`,
8//! `completion-source-plugin`, `grammar-plugin`) for that world — the
9//! shared-types trick (`with:` points
10//! `types` + `host-services` at the `plugin` world's generated modules so a
11//! crossed `event` value is the SAME Rust type `WitBoundary` round-trips,
12//! `boundary_event.rs`; PH7.3d precedent).
13//!
14//! **Async (unlike grammar).** Event delivery is OFF the keystroke path: the host
15//! owns an mpsc and pushes each `event` to `on-event` on the plugin's own task
16//! (§5.10.4). So the `bindgen!` sets `exports: { default: async }` — an `on-event`
17//! call suspends the guest stack, never pins the caller's thread, and a slow
18//! handler can never freeze a keystroke or another subscriber (paramount #4). The
19//! per-plugin actor that drains the bus channel and drives `on-event` is the
20//! `event_task` bridge (PH7.8c).
21//!
22//! Registration flow (the grammar `register-grammar` precedent): the host calls
23//! the guest's `register-events` export; the guest calls the imported
24//! `events.subscribe(filter, handler)` host function; that records the
25//! declaration into the Store's [`EventContributions`] (via the `events::Host`
26//! impl on `PluginState`, `lib.rs`); after the export returns, the host drains the
27//! subscriptions and wires each to the native `EventBus` (PH7.8c).
28
29use crate::lattice::plugin_host::types::EventFilter as WitEventFilter;
30
31pub(crate) mod bindings {
32    wasmtime::component::bindgen!({
33        world: "events-plugin",
34        path: "../lattice-wit/wit",
35        // Event delivery (`on-event`) is async — off the keystroke path, a
36        // delivery suspends the guest, never pins the caller's thread.
37        exports: { default: async },
38        with: {
39            // Reuse the `plugin` world's generated mirrors so a crossed value is
40            // the same Rust type `WitBoundary` round-trips; `host-services` reuses
41            // the already-wired `Host` impl (the completion/picker precedent).
42            "lattice:plugin-host/types": crate::lattice::plugin_host::types,
43            "lattice:plugin-host/host-services": crate::lattice::plugin_host::host_services,
44            "lattice:plugin-host/logging": crate::lattice::plugin_host::logging,
45        },
46    });
47}
48
49/// One subscription a plugin declared through `events.subscribe`, recorded
50/// verbatim (the guest-chosen `handler` id + the WIT `filter`). The host drains
51/// these after `register-events` returns and wires each to the native
52/// `EventBus` (PH7.8c); the filter projects to native at wire time via
53/// [`boundary_event::project_event_filter`](crate::boundary_event::project_event_filter).
54pub struct RecordedSubscription {
55    /// The guest's own dispatch key — passed back to `on-event(handler, ev)`.
56    pub handler: u32,
57    /// The declarative filter the plugin subscribed with (WIT form; projected to
58    /// the native `EventFilter` when wired to the bus).
59    pub filter: WitEventFilter,
60}
61
62/// The per-plugin accumulator the `events::Host` impl records into during
63/// `register-events` (`lib.rs`). Held in `PluginState`; drained by the host
64/// after the registration export returns (PH7.8c). `record` is the sync
65/// host-func body (it only pushes — it cannot trap), factored here (the
66/// [`GrammarContributions`](crate::grammar_host::GrammarContributions) precedent)
67/// so recording is unit-testable without a `PluginState` / guest.
68#[derive(Default)]
69pub struct EventContributions {
70    recorded: Vec<RecordedSubscription>,
71}
72
73impl EventContributions {
74    /// Record a subscription (the `events.subscribe` host-func body).
75    pub fn record(&mut self, filter: WitEventFilter, handler: u32) {
76        self.recorded.push(RecordedSubscription { handler, filter });
77    }
78
79    /// How many subscriptions were recorded.
80    pub fn len(&self) -> usize {
81        self.recorded.len()
82    }
83
84    /// True when the plugin subscribed to nothing (the degenerate case — a
85    /// non-observing plugin, or one whose `register-events` is empty).
86    pub fn is_empty(&self) -> bool {
87        self.recorded.is_empty()
88    }
89
90    /// Drain the recorded subscriptions, leaving the accumulator empty. Called by
91    /// the host after `register-events` returns (PH7.8c) to wire the bus.
92    pub fn take(&mut self) -> Vec<RecordedSubscription> {
93        std::mem::take(&mut self.recorded)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    #![allow(clippy::unwrap_used, clippy::panic)]
100
101    use super::*;
102    use crate::lattice::plugin_host::types::EventKind as WitEventKind;
103
104    fn filter(kind: WitEventKind) -> WitEventFilter {
105        WitEventFilter {
106            kinds: Some(vec![kind]),
107            path_globs: None,
108            major_modes: None,
109            minor_modes: None,
110        }
111    }
112
113    #[test]
114    fn records_subscriptions_and_preserves_handler_ids() {
115        let mut e = EventContributions::default();
116        assert!(e.is_empty());
117
118        e.record(filter(WitEventKind::DocumentSaved), 1);
119        e.record(filter(WitEventKind::BeforeQuit), 7);
120        assert_eq!(e.len(), 2);
121
122        let drained = e.take();
123        assert!(e.is_empty(), "take() leaves the accumulator empty");
124        assert_eq!(drained.len(), 2);
125        assert_eq!(drained[0].handler, 1);
126        assert!(matches!(
127            drained[0].filter.kinds.as_deref(),
128            Some([WitEventKind::DocumentSaved])
129        ));
130        assert_eq!(drained[1].handler, 7);
131    }
132}