Skip to main content

lattice_protocol/
event.rs

1//! Events published by the core to subscribed clients.
2//!
3//! Per DESIGN.md §5.10: every meaningful editor state transition
4//! publishes a typed event. Vim's `autocmd` and emacs's hooks both
5//! desugar to the same `EventBus::subscribe` call (filter +
6//! sink). The `Event` enum is the catalog; `EventKind` is the
7//! discriminator used by filter dispatch.
8//!
9//! This is the *closed* catalogue: editor-core transitions the host owns.
10//! Feature crates declare their own events as types through
11//! [`crate::event_registry`] instead of growing this enum, and plugins
12//! publish theirs through the one open arm, [`Event::Plugin`].
13//!
14//! The bus itself (`lattice_runtime::EventBus`) is not here — this crate has
15//! no runtime. Publishing is fire-and-forget unless a variant says otherwise.
16//! Most variants are also delivered to WASM plugins (mirrored in WIT by
17//! `lattice-plugin-host`); the ones marked *host-internal* below are refused
18//! at that boundary.
19//!
20//! Versions: `version` fields carry `lattice_core::Document`'s counters.
21//! [`Event::DocumentOpened`] carries the *text* version (bumps on text
22//! changes only); [`Event::DocumentChanged`] and [`Event::SelectionsChanged`]
23//! carry the whole-document version, which also bumps on selection changes.
24//!
25//! # Examples
26//!
27//! ```
28//! use lattice_protocol::{DocumentId, Event, EventKind};
29//! use std::path::PathBuf;
30//!
31//! let saved = Event::DocumentSaved {
32//!     id: DocumentId::new(3),
33//!     path: PathBuf::from("src/lib.rs"),
34//! };
35//! // Filters bucket on the payload-free discriminator.
36//! assert_eq!(saved.kind(), EventKind::DocumentSaved);
37//! assert_eq!(Event::BeforeQuit.kind(), EventKind::BeforeQuit);
38//! ```
39
40use std::path::PathBuf;
41
42use serde::{Deserialize, Serialize};
43
44use crate::ids::{BufferId, DocumentId};
45use crate::position::Range;
46use crate::selection::SelectionSet;
47
48/// One editor-core state transition, as published on the event bus.
49///
50/// See the [module docs](crate::event) for how this relates to typed events and the
51/// plugin boundary; each variant says when it fires, who publishes it, and
52/// what its fields carry.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum Event {
55    /// Fired when a document buffer opens. Subscribers (the
56    /// LSP attach driver, future plugin hooks, project-watcher,
57    /// completion warmer) react asynchronously; the publisher
58    /// (`Editor::publish_document_opened_for_active`, run at boot for
59    /// the initial document and after each `:e <path>` open) returns
60    /// immediately. The
61    /// event-driven design keeps the UI thread off the LSP
62    /// `initialize` round-trip -- aligned with paramount goal
63    /// #4 (asynchronicity).
64    ///
65    /// `path` is `None` for unsaved scratch buffers (no LSP
66    /// attach work to drive). `text` carries the buffer's
67    /// initial content so subscribers don't have to reach back
68    /// through a document handle on the publish path -- LSP
69    /// hands it straight to `didOpen`.
70    ///
71    /// Caveat: the current publisher builds `id` from the raw value of the
72    /// buffer-registry id (`DocumentId::new(buffer_id.0)`), whereas every
73    /// other document event carries the document's own [`DocumentId`]. The
74    /// two number spaces are not guaranteed to agree.
75    DocumentOpened {
76        /// The opened document (see the caveat above).
77        id: DocumentId,
78        /// Its file path; `None` for a scratch buffer.
79        path: Option<PathBuf>,
80        /// The document's *text* version at open — the version LSP's
81        /// `didOpen` starts from.
82        version: u64,
83        /// The full initial content.
84        text: String,
85    },
86    /// A document was closed. Subscribers drop per-document state keyed by
87    /// `id` (the LSP references provider, multibuffer excerpts, diff and VCS
88    /// caches all do).
89    ///
90    /// Note: the variant is subscribed to and mirrored in WIT, but no
91    /// production code path publishes it yet.
92    DocumentClosed {
93        /// The closed document.
94        id: DocumentId,
95    },
96    /// Fired before [`Self::DocumentSaved`]. Observation-only in
97    /// v1; future revisions may carry a payload that handlers can
98    /// mutate (formatters rewriting buffer content) or veto
99    /// (return Err to abort the save).
100    ///
101    /// Published by the host's save paths (`:w` and background saves)
102    /// immediately before the write.
103    BeforeSave {
104        /// The document about to be written.
105        id: DocumentId,
106        /// Where it is about to be written.
107        path: PathBuf,
108    },
109    /// A document was written to disk successfully (after
110    /// [`Self::BeforeSave`]; a failed write publishes nothing further).
111    /// Published by the host's save paths, including background saves of
112    /// buffers the user is not looking at.
113    DocumentSaved {
114        /// The saved document.
115        id: DocumentId,
116        /// The path actually written.
117        path: PathBuf,
118    },
119    /// A document's text changed. Published by the host after each applied
120    /// edit (and by `lattice-multibuffer` when an edit through a multibuffer
121    /// lands in its source document). LSP's `didChange`, the multibuffer's
122    /// excerpt refresh and the diff subsystem feed on it.
123    DocumentChanged {
124        /// The changed document.
125        id: DocumentId,
126        /// The buffer's filesystem path, if it has one. Carried so
127        /// subscribers can resolve URIs without holding their own
128        /// DocumentId -> path map. `None` for scratch / unsaved
129        /// buffers.
130        path: Option<PathBuf>,
131        /// The document's whole version after the change (see the module
132        /// docs on versions).
133        version: u64,
134        /// The edits, in the order they were applied; each one's ranges are
135        /// in the coordinates of the buffer as the previous one left it.
136        /// Today's publishers send one edit per event.
137        edits: Vec<AppliedEdit>,
138    },
139    /// The selection set of a document changed — visual extension, a
140    /// selection-changing effect, `gv`. Published by
141    /// `Editor::publish_selections_changed`. Carries the complete new set,
142    /// not a delta.
143    SelectionsChanged {
144        /// The document whose selections changed.
145        id: DocumentId,
146        /// The document's whole version after the change.
147        version: u64,
148        /// The full new selection set.
149        selections: SelectionSet,
150    },
151    /// Fired when the modal state transitions
152    /// (Normal -> Insert, Insert -> Normal, ...). Carries the
153    /// previous and next state as opaque labels; the App owns the
154    /// `ModalState` type so the protocol layer keeps it as String.
155    ///
156    /// Published by the host's modal-state setter, only on a real
157    /// transition. The labels are the `Debug` rendering of the host's
158    /// `ModalState` (`"Normal"`, `"Insert"`, ...).
159    ModalModeChanged {
160        /// The state being left.
161        from: String,
162        /// The state being entered.
163        to: String,
164    },
165    /// Fired before the editor exits. Observation-only in v1; the
166    /// veto path (a handler returning Err to abort quit) layers on
167    /// once the bus grows the Before-event mutation semantics.
168    BeforeQuit,
169    /// Fired after a typed-options registry value changes
170    /// (DESIGN.md §5.12). Carries the option's canonical name plus
171    /// the formatted old / new value strings -- string-formatted
172    /// (rather than `Box<dyn Any>`) because most subscribers just
173    /// react to the change signal and don't need the typed value.
174    /// Subscribers that need the typed value re-read through the
175    /// registry (`config.with(handle, |v| ...)`).
176    ///
177    /// `old` is `None` for the very first publish after registration
178    /// (when the option is initialised to its default and no prior
179    /// value exists); subsequent edits always carry both sides.
180    ///
181    /// Published by `lattice_config::ConfigRegistry` through its injected
182    /// publisher, after the write and outside the registry lock (so a
183    /// handler may read other options).
184    OptionChanged {
185        /// The option's canonical name (`tabstop`, not `ts`).
186        name: String,
187        /// The previous value, formatted; `None` on the first publish.
188        old: Option<String>,
189        /// The new value, formatted as `:set` would print it.
190        new: String,
191    },
192    /// A major mode became the active major on `buffer` (published
193    /// *after* the mode's `on_activate` resolved, so subscribers see
194    /// a consistent state). `major` is the major mode's canonical
195    /// name (e.g. `rust-mode`) -- carried as a `String` so the
196    /// protocol layer stays free of the `ModeId` type, mirroring
197    /// [`Self::ModalModeChanged`].
198    ///
199    /// This is the event minor-mode activation triggers filter on:
200    /// `EventFilter.major_modes` matches against `major`
201    /// (mode-architecture.md §7.4). Published by the mode
202    /// dispatcher's cascade task (MA.1); supersedes the prior typed
203    /// `ModeEvent::MajorEntered` so the EF.1 filter machinery applies.
204    MajorEntered {
205        /// The buffer the major mode is now active on.
206        buffer: BufferId,
207        /// The major mode's canonical name.
208        major: String,
209    },
210    /// The active major mode on `buffer` is about to be deactivated
211    /// (published *before* the mode's Guard drops, so subscribers can
212    /// inspect what's being torn down). Pairs with
213    /// [`Self::MajorEntered`] for minor-mode teardown. `major` is the
214    /// canonical name of the major being torn down.
215    MajorExiting {
216        /// The buffer the major mode is leaving.
217        buffer: BufferId,
218        /// The major mode's canonical name.
219        major: String,
220    },
221    /// A minor mode was activated on `buffer` (published *after* its
222    /// `on_activate` resolved). `minor` is the minor mode's canonical
223    /// name. The full observable mode-lifecycle quartet
224    /// (`MajorEntered`/`MajorExiting`/`MinorActivated`/`MinorDeactivated`)
225    /// lives on this `Event` enum (design.md §5.10.1) so hooks /
226    /// `EventFilter` apply uniformly; only the internal
227    /// `ModeActivationFailed` / `OptionConflict` cascade signals stay
228    /// on the typed `lattice_mode::ModeEvent` bus.
229    MinorActivated {
230        /// The buffer the minor mode is now active on.
231        buffer: BufferId,
232        /// The minor mode's canonical name.
233        minor: String,
234    },
235    /// A minor mode was deactivated on `buffer` (published *before*
236    /// its Guard drops). `minor` is the minor mode's canonical name.
237    MinorDeactivated {
238        /// The buffer the minor mode is leaving.
239        buffer: BufferId,
240        /// The minor mode's canonical name.
241        minor: String,
242    },
243    /// A plugin-DEFINED event (PH7.8b). Unlike every arm above -- each a
244    /// closed, host-owned editor-core transition -- this arm is the OPEN
245    /// escape hatch a runtime-loaded plugin publishes through
246    /// (`host-services emit-event`). The host is a thin router: `name` is
247    /// the plugin's event identifier (declared via `register-event`, surfaced
248    /// in the runtime event registry, `event_registry`); `payload` is opaque
249    /// MessagePack the *plugin* owns and the host NEVER interprets -- the
250    /// boundary discipline the plugin host rests on. Every plugin event shares
251    /// this one variant + [`EventKind::Plugin`]; subscribers filter by `name`
252    /// inside their handler (the bus discriminates only to `Plugin`, not
253    /// per-name), so a new plugin event needs no enum/WIT change.
254    Plugin {
255        /// The plugin-declared event name (`my-plugin.file-indexed`).
256        name: String,
257        /// Opaque MessagePack bytes, owned and interpreted only by plugins.
258        payload: Vec<u8>,
259    },
260    /// A plugin instance crashed (a lifecycle / callback export trapped: fuel
261    /// exhaustion, epoch deadline, a guest panic, or any wasm trap) and was
262    /// quarantined by the host (PH7.12). Unlike [`Self::Plugin`] -- the OPEN
263    /// escape hatch a *live* plugin publishes through -- this is a CLOSED,
264    /// host-owned lifecycle transition the host itself originates, mirroring the
265    /// mode-lifecycle quartet ([`Self::MinorDeactivated`] et al.): the host is
266    /// the sole publisher, and subscribers (a future crash-notification surface,
267    /// the Phase-8 plugin manager's reload/health UI) filter it by *kind*, not by
268    /// string-matching a name.
269    ///
270    /// Fired exactly once per instance -- on the first trap that trips
271    /// quarantine. A component trap taints its instance irrecoverably (wasmtime
272    /// offers no rollback), so the instance is dead-until-reinstantiation: every
273    /// later call short-circuits without re-entering the dead `Store`, and no
274    /// further `PluginCrashed` fires until a reload (PH7.12b) mints a fresh
275    /// instance. The guarantee is **isolation**: the editor, actor, other
276    /// plugins, LSP, and UI are untouched.
277    ///
278    /// `plugin` is the host-issued numeric plugin id (the same id inside
279    /// `SourceLayer::Plugin(id)`); `func` is the export that trapped
280    /// (`"on-event"` / `"spec"` / `"generate"` / `"apply-motion"` / ...); `kind`
281    /// is a stable machine label (`"fuel"` / `"epoch"` / `"trap"`) -- a `String`
282    /// (not the host's `TrapKind`) so the protocol layer stays free of the
283    /// plugin-host type, mirroring [`Self::ModalModeChanged`].
284    ///
285    /// Host-internal: refused at the plugin boundary, never delivered to a
286    /// guest.
287    PluginCrashed {
288        /// The host-issued numeric id of the quarantined plugin instance.
289        plugin: u32,
290        /// The export that trapped (`"on-event"`, `"spec"`, ...).
291        func: String,
292        /// Why: `"fuel"`, `"epoch"` or `"trap"`.
293        kind: String,
294    },
295    /// A named plugin is ABOUT to run the load-time exports that read its own
296    /// options (OA.14d). Published by the loader mid-load: after the plugin's
297    /// `config` seam drained — so every option it declares EXISTS and can be
298    /// set — and before every seam that consumes one.
299    ///
300    /// It exists because [`Self::PluginLoaded`] is too late for a value the
301    /// plugin reads at load. org derives its per-keyword theme elements and its
302    /// generated highlight-query rules from `org.todo-keywords` inside
303    /// `register-theme-elements`; a handler that runs after the load sets an
304    /// option nothing will read again until a restart.
305    ///
306    /// `name` is the manifest id, and it is carried rather than left implicit
307    /// so a handler discriminates: config for a plugin you know is legible,
308    /// config invited to run for every plugin on the disk is not.
309    ///
310    /// **Delivery is awaited.** The loader publishes this via
311    /// `EventBus::publish_awaited` and does not continue the load until every
312    /// guest handler has returned — an unawaited publish would leave the
313    /// handler racing the very export it exists to precede. It is the one event
314    /// with that property; everything else on the bus is fire-and-forget.
315    PrePluginLoaded {
316        /// The loading plugin's manifest id. (No numeric id: guests receive
317        /// the name only.)
318        name: String,
319    },
320    /// A plugin finished loading (CI.1): every seam drained, its modes /
321    /// options / commands all registered. Published by the loader at
322    /// `load_discovered` completion. UNLIKE [`Self::PluginCrashed`], this IS
323    /// delivered to guests — an `init.rs` subscribes (filtered by `name`) to run
324    /// deferred config against a now-present plugin (`with-eval-after-load`;
325    /// config-and-init.md). `name` is the manifest id; `id` the host-issued
326    /// numeric plugin id.
327    PluginLoaded {
328        /// The plugin's manifest id.
329        name: String,
330        /// The host-issued numeric plugin id.
331        id: u32,
332    },
333    /// A plugin was unloaded (CI.1): teardown reversed its contributions
334    /// (`:plugin-unload` / crash-teardown). Delivered to guests so a handler can
335    /// tear down its own dependent setup. Fields mirror [`Self::PluginLoaded`].
336    PluginUnloaded {
337        /// The plugin's manifest id.
338        name: String,
339        /// The host-issued numeric plugin id it had while loaded.
340        id: u32,
341    },
342    /// A request to enable/disable a minor mode globally (CI.4) — the
343    /// guest-to-Editor bridge for `enable-mode` / `disable-mode`. A plugin
344    /// (init.rs) calls the modes-seam `enable-mode` from an `on-plugin-loaded`
345    /// handler; the host publishes THIS, and the Editor (which owns the mode
346    /// registry + the open-buffer set + the activator) flips the enablement and
347    /// re-activates open buffers. Host-internal — NOT delivered back to guests
348    /// (like [`Self::PluginCrashed`]). `mode` is the mode id.
349    ModeEnablementRequested {
350        /// The minor mode's id.
351        mode: String,
352        /// `true` to enable globally, `false` to disable.
353        enabled: bool,
354    },
355    /// A request to set an option for ONE buffer — the guest-to-Editor bridge
356    /// for `set-option-in-buffer`, and the peer of
357    /// [`Self::ModeEnablementRequested`] in both shape and reason.
358    ///
359    /// The config seam's `set-option` writes the GLOBAL layer (it is the
360    /// `:set` path). A handler that wants "wrap in org buffers" cannot use it:
361    /// it would wrap everything, and nothing would unwrap on leaving org. The
362    /// buffer-local layer is what expresses that, and it lives on the Editor
363    /// (`buffer_local_overrides`) rather than in the `ConfigRegistry` the
364    /// plugin host holds — hence the bridge.
365    ///
366    /// Host-internal, NOT delivered back to guests: a plugin observing every
367    /// other plugin's option writes is a surveillance seam nobody asked for,
368    /// and `option-changed` already reports the outcome.
369    BufferOptionOverrideRequested {
370        /// The buffer the override applies to.
371        buffer: BufferId,
372        /// `name=value` in `:set` syntax, parsed by the same
373        /// `parse_for_buffer_local` the `:setlocal` path uses — so a guest
374        /// cannot express anything `:setlocal` could not, and a bad value is
375        /// rejected with the same message.
376        option: String,
377    },
378    /// MG.41g: a long-running background operation finished.
379    ///
380    /// The decoupling seam between *producers* of async work (magit's
381    /// git invocations, LSP requests, a plugin's task) and whatever
382    /// *reports* completion. Producers publish this and never mention
383    /// notifications; the notification layer is one subscriber, so the
384    /// policy — which levels surface, whether to notify at all, rate
385    /// limiting — lives in one place and is configurable later without
386    /// touching a single producer.
387    ///
388    /// Replaces threading a `NotificationStoreHandle` into every
389    /// spawner, which was opt-in and therefore already forgotten in
390    /// five of magit's ten (`spawn_git`, the generic one, among them).
391    ///
392    /// Published today by magit's git spawners; `lattice-notify` is the
393    /// subscriber that turns it into a notification. Not yet mirrored in WIT,
394    /// so it is not delivered to guests.
395    BackgroundTaskFinished {
396        /// Subsystem that ran it — `"magit"`, `"lsp"`, a plugin id.
397        /// Lets a subscriber filter without parsing `label`.
398        source: String,
399        /// NC.2: what the work was done *in* — a repository name, a
400        /// project, a server. `None` when the work has no such place.
401        ///
402        /// A field, not part of `label`, so every producer's scope
403        /// lands in the same place on screen: several notifications at
404        /// once are told apart by reading one column, not by parsing
405        /// each producer's own phrasing.
406        scope: Option<String>,
407        /// What was done, as an imperative phrase naming its object:
408        /// `"push main → origin/main"`, `"drop stash@{2}"`. The outcome
409        /// is appended by whoever reports it, so the label must read
410        /// correctly before "failed" as well as before a summary.
411        label: String,
412        /// How it ended.
413        outcome: TaskOutcome,
414    },
415    /// OR.2: files under a directory a plugin asked the host to watch
416    /// (`host-services.watch`) changed on disk.
417    ///
418    /// **Addressed, not broadcast.** `plugin` is the host-issued numeric id of
419    /// the plugin whose watch produced this, and the event-delivery actor drops
420    /// any delivery whose `plugin` is not its own. The bus is a broadcast and a
421    /// watch is a capability: a plugin granted `fs:read` over one directory
422    /// must not learn which files under *another* plugin's watched directory
423    /// changed, and it would if this rode [`Self::Plugin`] — every
424    /// `EventKind::Plugin` subscriber sees every plugin event. The id never
425    /// crosses to a guest, because by the time it does it is always the guest's
426    /// own.
427    ///
428    /// **Many paths, one event.** A `git pull` that rewrites two hundred files
429    /// produces one delivery carrying two hundred paths, not two hundred
430    /// deliveries: the host coalesces a burst behind a quiet window before
431    /// publishing. A consumer re-reads what changed, so ordering within the
432    /// batch carries no meaning and the paths are deduplicated and sorted.
433    ///
434    /// Published by `lattice-plugin-host`'s watch host.
435    FilesChanged {
436        /// The host-issued numeric plugin id that armed the watch.
437        plugin: u32,
438        /// Absolute paths that changed, created or were removed. A removal is
439        /// reported as a change — the consumer stats the path — because an
440        /// index that cannot see deletions offers destinations that no longer
441        /// exist.
442        paths: Vec<std::path::PathBuf>,
443    },
444}
445
446/// MG.41g: how a [`Event::BackgroundTaskFinished`] ended.
447///
448/// Deliberately two variants rather than a `Result<String, String>`:
449/// the payload crosses the plugin boundary, where a typed variant
450/// mirrors cleanly and a `Result` does not.
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
452pub enum TaskOutcome {
453    /// Finished cleanly. `summary` is a short human line — the full
454    /// output belongs in a log, not a notification.
455    Succeeded {
456        /// A short human-readable result line.
457        summary: String,
458    },
459    /// NC.2: ended cleanly but **not done** — a rebase paused on an
460    /// `edit`, a merge left uncommitted, a conflict waiting for the
461    /// user. Reporting these as success says "finished" about work the
462    /// user still has to finish. `message` says what is waiting.
463    Stopped {
464        /// What is waiting on the user.
465        message: String,
466    },
467    /// Failed. `message` is the reason, already truncated for display.
468    Failed {
469        /// Why it failed, display-ready.
470        message: String,
471    },
472}
473
474// M.5.3.b: `LspLogPushed`, `LspBufferAttached`, and
475// `LspBufferDetached` moved out of this enum and into
476// `lattice-lsp::events` as concrete types implementing
477// [`crate::event_registry::Event`]. They publish via the
478// typed-bus path (`EventBus::publish_typed`); subscribers
479// use `EventBus::subscribe_typed::<T>`. Future cleanup will
480// migrate the rest of the enum the same way.
481
482impl Event {
483    /// Project the event to its [`EventKind`] discriminator. Used
484    /// by the runtime event bus's filter dispatch to bucket
485    /// subscriptions without string-matching variant names.
486    pub fn kind(&self) -> EventKind {
487        match self {
488            Event::DocumentOpened { .. } => EventKind::DocumentOpened,
489            Event::DocumentClosed { .. } => EventKind::DocumentClosed,
490            Event::BeforeSave { .. } => EventKind::BeforeSave,
491            Event::DocumentSaved { .. } => EventKind::DocumentSaved,
492            Event::DocumentChanged { .. } => EventKind::DocumentChanged,
493            Event::SelectionsChanged { .. } => EventKind::SelectionsChanged,
494            Event::ModalModeChanged { .. } => EventKind::ModalModeChanged,
495            Event::BeforeQuit => EventKind::BeforeQuit,
496            Event::OptionChanged { .. } => EventKind::OptionChanged,
497            Event::MajorEntered { .. } => EventKind::MajorEntered,
498            Event::MajorExiting { .. } => EventKind::MajorExiting,
499            Event::MinorActivated { .. } => EventKind::MinorActivated,
500            Event::MinorDeactivated { .. } => EventKind::MinorDeactivated,
501            Event::Plugin { .. } => EventKind::Plugin,
502            Event::PluginCrashed { .. } => EventKind::PluginCrashed,
503            Event::PrePluginLoaded { .. } => EventKind::PrePluginLoaded,
504            Event::PluginLoaded { .. } => EventKind::PluginLoaded,
505            Event::PluginUnloaded { .. } => EventKind::PluginUnloaded,
506            Event::ModeEnablementRequested { .. } => EventKind::ModeEnablementRequested,
507            Event::BufferOptionOverrideRequested { .. } => EventKind::BufferOptionOverrideRequested,
508            Event::BackgroundTaskFinished { .. } => EventKind::BackgroundTaskFinished,
509            Event::FilesChanged { .. } => EventKind::FilesChanged,
510        }
511    }
512}
513
514/// Discriminator for [`Event`] variants. Stored in
515/// `EventFilter::kinds` and used by the bus to bucket
516/// subscriptions per kind so publish does no global iteration.
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
518pub enum EventKind {
519    /// Discriminator for [`Event::DocumentOpened`].
520    DocumentOpened,
521    /// Discriminator for [`Event::DocumentClosed`].
522    DocumentClosed,
523    /// Discriminator for [`Event::BeforeSave`].
524    BeforeSave,
525    /// Discriminator for [`Event::DocumentSaved`].
526    DocumentSaved,
527    /// Discriminator for [`Event::DocumentChanged`].
528    DocumentChanged,
529    /// Discriminator for [`Event::SelectionsChanged`].
530    SelectionsChanged,
531    /// Discriminator for [`Event::ModalModeChanged`].
532    ModalModeChanged,
533    /// Discriminator for [`Event::BeforeQuit`].
534    BeforeQuit,
535    /// Discriminator for [`Event::OptionChanged`].
536    OptionChanged,
537    /// Discriminator for [`Event::MajorEntered`].
538    MajorEntered,
539    /// Discriminator for [`Event::MajorExiting`].
540    MajorExiting,
541    /// Discriminator for [`Event::MinorActivated`].
542    MinorActivated,
543    /// Discriminator for [`Event::MinorDeactivated`].
544    MinorDeactivated,
545    /// Discriminator for every plugin-defined event ([`Event::Plugin`]). All
546    /// plugin events share this one kind; the per-event `name` is NOT a bus
547    /// discriminator (subscribers filter by name in their handler, PH7.8b).
548    Plugin,
549    /// Discriminator for [`Event::PluginCrashed`] -- the host-originated
550    /// crash/quarantine lifecycle transition (PH7.12). A single kind so a
551    /// crash-notification surface or the Phase-8 plugin manager subscribes to
552    /// every plugin crash with one filter.
553    PluginCrashed,
554    /// Discriminator for [`Event::PrePluginLoaded`] (OA.14d) — the awaited
555    /// signal an `init.rs` subscribes to for config a plugin reads at LOAD.
556    PrePluginLoaded,
557    /// Discriminator for [`Event::PluginLoaded`] (CI.1) — the plugin-load
558    /// lifecycle signal an `init.rs` subscribes to for deferred config.
559    PluginLoaded,
560    /// Discriminator for [`Event::PluginUnloaded`] (CI.1).
561    PluginUnloaded,
562    /// Discriminator for [`Event::ModeEnablementRequested`] (CI.4) — the
563    /// host-internal enable/disable-minor-mode bridge the Editor handles.
564    ModeEnablementRequested,
565    /// Discriminator for [`Event::BufferOptionOverrideRequested`] — like its
566    /// neighbour above, host-internal and never deliverable to a guest.
567    BufferOptionOverrideRequested,
568    /// Discriminator for [`Event::BackgroundTaskFinished`] (MG.41g).
569    BackgroundTaskFinished,
570    /// Discriminator for [`Event::FilesChanged`] (OR.2) — a plugin's
571    /// `host-services.watch` fired. One kind for every watch; the delivery
572    /// actor, not the filter, is what scopes a batch to the plugin that armed
573    /// it (see the variant's doc).
574    FilesChanged,
575}
576
577/// An edit as actually applied to the buffer (the original `Edit` plus the
578/// resulting range, useful for clients that want to know what changed).
579///
580/// The bus-level copy of `lattice_core::AppliedEdit`, without its
581/// tree-sitter [`EditDelta`](crate::EditDelta). Positions are
582/// (line, UTF-8 byte), as everywhere in this crate.
583///
584/// `inserted_text` carries the text that was placed into `inserted_range`.
585/// Together with `original_range` this is exactly what an LSP
586/// `textDocument/didChange` payload needs, which lets the
587/// `lattice-lsp` fan-in synthesise a `lattice_protocol::edit::Edit`
588/// from this event without re-reading the buffer.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct AppliedEdit {
591    /// The range the edit targeted, in pre-edit coordinates.
592    pub original_range: Range,
593    /// Where the inserted text now sits, in post-edit coordinates: starts at
594    /// `original_range.start`; empty for a pure delete.
595    pub inserted_range: Range,
596    /// The text removed from `original_range` (empty for a pure insert).
597    pub replaced_text: String,
598    /// The text placed at `inserted_range` (empty for a pure delete).
599    pub inserted_text: String,
600}