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}