Skip to main content

lattice_mode/
activator.rs

1//! `ModeActivator`: synchronous activation surface for extension
2//! crates that create buffers and need their major / minor modes
3//! activated on the host side.
4//!
5//! M.2.b.2 (2026-06-01) introduces this trait so `lattice-multibuffer`
6//! (and every future in-tree provider that ships within it) can
7//! atomically insert + activate-major a buffer without depending on
8//! `lattice-host` directly. The activation cascade requires `&mut Editor`
9//! (it mutates per-buffer mode state, options cache, completion sources,
10//! LSP wiring) — but extension crates can't import `lattice-host`'s
11//! `Editor` type. This trait, living in `lattice-mode`, is the seam
12//! both crates depend on.
13//!
14//! See `docs/dev/architecture/multibuffer-views.md` §3.7 + the H-series
15//! deferral note in `docs/dev/architecture/kind-agnostic-buffers.md`
16//! §10 for the design.
17
18use std::sync::Arc;
19
20use lattice_core::{BufferFlags, BufferId, BufferKind};
21
22use crate::mode::ModeId;
23use crate::services::ServiceRegistry;
24
25/// Activation surface implemented by `lattice-host::Editor` and
26/// consumed by extension crates that need to drive activation
27/// without holding a typed `&mut Editor`.
28///
29/// All three methods run synchronously on the App thread (caller
30/// owns `&mut Self`). The implementor is responsible for routing
31/// the cascade's renderer signals into its own pending-signals
32/// queue — extension crates never see `RendererSignal`, keeping
33/// host-layer types out of `lattice-mode`.
34///
35/// Failures (mode not registered, missing capability, conflict)
36/// are logged + swallowed by the impl (matching the existing
37/// `Editor::activate_*` helpers' shape). Callers that need to
38/// observe activation outcome subscribe to
39/// [`crate::ModeEvent`] on the event bus.
40pub trait ModeActivator {
41    /// Activate the major mode bound to `kind` on `buffer`. Uses
42    /// the [`crate::ModeRegistry::find_major_for_kind`] lookup
43    /// (populated by `Mode::target_buffer_kind` declarations)
44    /// to resolve the mode id. No-ops if a major is already
45    /// active on the buffer (idempotency / preserve-intent).
46    fn activate_major_for_kind(&mut self, buffer: BufferId, kind: BufferKind);
47
48    /// Activate a minor mode by id on `buffer`. The mode must
49    /// be registered. Idempotent: re-activating an already-active
50    /// minor is a no-op.
51    fn activate_minor_by_id(&mut self, buffer: BufferId, mode: ModeId);
52
53    /// Deactivate a minor mode by id on `buffer`. Idempotent: a mode
54    /// that is not active is a no-op.
55    ///
56    /// PD.4: the symmetric half of [`Self::activate_minor_by_id`], and
57    /// it exists because a re-triggered provider view reuses its buffer.
58    /// A view that was read-only for one query and is editable for the
59    /// next has to be able to say so — without this, "open the staged
60    /// diff, then open the working-tree diff" would leave the second one
61    /// unwritable, and the cause would be invisible because the buffer
62    /// looks identical.
63    ///
64    /// Default impl is a no-op so test activators that do not model
65    /// mode state stay valid; the host impl forwards to
66    /// `Editor::deactivate_mode_by_id`.
67    fn deactivate_minor_by_id(&mut self, buffer: BufferId, mode: ModeId) {
68        let _ = (buffer, mode);
69    }
70
71    /// Find-or-create a synthetic **named `Document`** buffer and
72    /// activate `major` on it *by id* (no on-disk language detection),
73    /// returning its [`BufferId`]. This is the reliable, `&mut`-backed
74    /// creation seam a mode / provider uses to **provision its own
75    /// buffer** — the creation of a mode-owned buffer is the mode's
76    /// responsibility, triggered through here.
77    ///
78    /// Unlike [`crate::BufferStore`] (whose `&self` handle can only
79    /// *find* an existing buffer — activating a mode mutates the mode
80    /// registry / active-modes / options cache and needs `&mut`), this
81    /// method actually creates: the host impl mints the `BufferId`,
82    /// spawns an empty `Document`, registers it with `flags`, seeds
83    /// mode-locals, and runs `major`'s `on_activate` — so any drain /
84    /// subscription the mode establishes there is live by the time this
85    /// returns. Idempotent: a second call with the same `name` returns
86    /// the existing id without re-activating (the drain from the first
87    /// activation stays put).
88    fn ensure_named_document(&mut self, name: &str, major: ModeId, flags: BufferFlags) -> BufferId;
89
90    /// The buffer that is active right now (MR.6).
91    ///
92    /// A provider view is opened *over* something — the file you were
93    /// reading, the magit buffer you pressed a chord in — and what it
94    /// should show usually depends on which. `ProviderViewOpener`
95    /// received the services and the trigger's arguments but no way to
96    /// name that buffer, so magit's project diff had to fall back to the
97    /// process's working directory and showed the wrong repository's
98    /// changes for anyone with two checkouts open.
99    ///
100    /// The peer of `TransientContext::buffer` and
101    /// `ExCommandContext::buffer_id`, and generic for the same reason:
102    /// "which buffer did this come from" is a question every
103    /// context-varying surface asks, not a magit one.
104    ///
105    /// Default `None` so test activators that model no pane tree stay
106    /// valid; the host impl returns its active document buffer.
107    fn active_buffer(&self) -> Option<BufferId> {
108        None
109    }
110
111    /// Cheap-clone handle to the App's [`ServiceRegistry`]. Used
112    /// by extension-crate trigger functions that need to look up
113    /// service handles (`BufferStoreHandle`, per-provider
114    /// services, the per-extension-crate registries) without
115    /// fighting the borrow checker against the `&mut Self`
116    /// activator borrow.
117    fn services(&self) -> Arc<ServiceRegistry>;
118
119    /// Register a virtual-row provider against
120    /// `buffer`. Used by extension crates that contribute virtual
121    /// rows for their own buffer kinds (multibuffer excerpt
122    /// headers — `MultibufferHeaderProvider`; future fold-range
123    /// providers, diff-hunk overlays, LSP code-lens, ...). The
124    /// host-side impl forwards to
125    /// `Editor::virtual_row_providers.register(buffer, provider)`;
126    /// the worker picks the provider up on its next wake (K.4.6, 2026-06-02).
127    ///
128    /// Returns `true` on registration, `false` if a provider with
129    /// the same `ProviderId` was already registered in the same
130    /// buffer scope (no replacement — caller `unregister`s first
131    /// via the existing registry handle). The default impl
132    /// returns `false` so test activators that don't wire the
133    /// virtual-row pipeline behave as no-ops; production impls
134    /// (Editor) override.
135    ///
136    /// Paramount-#2 anchor: every mode contributing to a buffer
137    /// registers its own virtual rows via this seam, matching
138    /// the mode-owns-its-surface principle (keymaps via
139    /// `register_<mode>_keymap`, virtual rows via this method,
140    /// future status-line items via similar). WIT plugin path
141    /// inherits the trait surface.
142    fn register_virtual_row_provider(
143        &mut self,
144        buffer: lattice_core::BufferId,
145        provider: Arc<dyn lattice_cells::VirtualRowProvider>,
146    ) -> bool {
147        let _ = (buffer, provider);
148        false
149    }
150
151    /// Record the directory `buffer` is *about*
152    /// ([`BufferScopeDir`](crate::BufferScopeDir)).
153    ///
154    /// A provider calls this from its trigger, where it has already resolved
155    /// the directory — a magit repository's workdir, an oil listing's dir, a
156    /// scan root. Project resolution (`:files`, `:search`) then answers for
157    /// that buffer instead of falling through to the working directory.
158    ///
159    /// Pass the directory, not a project root: the host resolves the project
160    /// from it through the ordinary resolver, so a provider never has to
161    /// learn what a project is and the two cannot be recorded inconsistently.
162    ///
163    /// Default impl is a no-op so test activators stay valid; the host
164    /// forwards to `Editor::set_buffer_scope_dir`.
165    fn set_buffer_scope_dir(&mut self, buffer: BufferId, dir: std::path::PathBuf) {
166        let _ = (buffer, dir);
167    }
168}
169
170/// Service-accessible interface for registering virtual row providers
171/// on a buffer. The host registers an `Arc<dyn VirtualRowRegistrar>` during boot
172/// so subsystems (e.g. `lattice-ai`) can register headerlines without depending
173/// on `lattice-host`'s concrete `VirtualRowProviderRegistry` (AUX‑2).
174pub trait VirtualRowRegistrar: Send + Sync {
175    /// Register `provider` against `buffer`. Returns `false` if a provider with
176    /// the same `ProviderId` is already registered in the same buffer scope.
177    fn register(
178        &self,
179        buffer: BufferId,
180        provider: Arc<dyn lattice_cells::VirtualRowProvider>,
181    ) -> bool;
182    /// Remove the provider identified by `id` from `buffer`'s scope.
183    fn unregister(&self, buffer: BufferId, id: lattice_cells::ProviderId) -> bool;
184}