lattice_lsp/install.rs
1//! BC.8a (2026-06-24): the crate-owned `install(boot)` entry point.
2//!
3//! LSP is the last + largest boot-composition migration; it is sub-sliced
4//! (BC.8a–e). **BC.8a is the behaviour-preserving foundation:** the LSP modes
5//! and the four server-`*/refresh` off-keystroke wakes register through the
6//! generic [`SubsystemBoot`] surface, collapsing those host sites into the
7//! Phase-B install line. The inbound buses (apply-edit / show-document /
8//! configuration / show-message-request) reshape onto `boot.inbound::<T>` in
9//! the later sub-slices (BC.8b–e).
10//!
11//! ## What BC.8a collapses here
12//!
13//! - **Modes** — `register_lsp_log_modes` (the ~17 LSP modes: log/trace/
14//! diagnostics/hover/signature/format/rename/symbols/code-action/nav/
15//! progress/highlight/selection-range/folding/inlay/semantic-tokens) +
16//! `register_lsp_completion_mode`, which needs the `LspSupervisorHandle`.
17//! That handle is a **host-created** value (the supervisor + its four
18//! server-initiated buses live in `editor_boot::build_lsp_subsystem`, which
19//! produces `Editor` fields — the diff `DiffSubsystem`-bind residue class), so
20//! the host registers it as a **Phase-A service** and the install reads it
21//! back generically via `boot.service::<LspSupervisorHandle>()` — the trait
22//! names no host type, and mode-ownership is preserved (the *mode* owns its
23//! registration; the host owns the supervisor's lifecycle).
24//! - **Off-keystroke wakes** — the four `workspace/*/refresh` notifications
25//! (`LspInlayHintRefresh` / `LspSemanticTokensRefresh` / `LspDiagnosticRefresh`
26//! / `LspCodeLensRefresh`) become `boot.wake_on_event::<E>()`. Behaviour is
27//! byte-identical to the host's hand-rolled L1c `wake_on` forwarders
28//! (subscribe-typed + spawn a notify task), now baked into the primitive so
29//! the wake can't be forgotten (paramount #4). The per-type **drain** channels
30//! (`pending_*_refresh_rx` → `drain_*_refresh` Editor methods) keep doing the
31//! cache-eviction work host-side — they are `&mut Editor` residue, untouched.
32//!
33//! ## What stays host-side (residue, NOT mode-ownership violations)
34//!
35//! - **`build_lsp_subsystem`** — produces `Editor`-field values (the supervisor
36//! handle, the `DiagnosticsLayer` the renderer reads per-frame, the four
37//! `pending_*_rx` inbound receivers). `install(boot)` returns nothing and
38//! can't seat Editor fields — the diff `DiffSubsystem`-bind precedent.
39//! - **The host-created services** (`LspSupervisorHandle`, `LspLogger`,
40//! `DiagnosticsQueryHandle`) are registered host-side because the host owns
41//! the values (diff's `DiffSubsystemHandle` precedent). `install` only
42//! *reads* the supervisor handle for completion-mode registration.
43//! - **`lsp_diagnostics.set_wake`** arms the `DiagnosticsLayer`'s render wake —
44//! an Editor-field. The inbound drains + the modeline (`ModelineElementUpdate`,
45//! a generic event) wake stay host-side.
46
47use std::sync::Arc;
48
49use lattice_config::ConfigRegistry;
50use lattice_grammar::Effect;
51use lattice_grammar::app_effect::AppEffect;
52use lattice_mode::SubsystemBoot;
53use lattice_protocol::error_list::{ErrorEntry, ErrorSource, ErrorWrite};
54
55use crate::LspSupervisorHandle;
56use crate::completion::register_lsp_completion_mode;
57use crate::diagnostics_layer::DiagnosticsLayer;
58use crate::error_list_feed::ErrorListFeed;
59use crate::modes::register_lsp_log_modes;
60use crate::{
61 LspCodeLensRefresh, LspDiagnosticRefresh, LspInlayHintRefresh, LspSemanticTokensRefresh,
62};
63
64/// Wire the LSP subsystem's modes + off-keystroke refresh wakes into the editor
65/// at boot (BC.8a). One Phase-B line in `editor_boot.rs`.
66pub fn install(boot: &mut impl SubsystemBoot) {
67 // ── Modes ───────────────────────────────────────────────────────────────
68 register_lsp_log_modes(boot.modes_mut());
69 // `lsp-completion-mode` captures the supervisor handle. It is a host-created
70 // value registered as a Phase-A service (before this install line); read it
71 // back generically so the trait names no host type. Clone one Arc layer off
72 // (`boot.service::<T>()` yields `Arc<T>`; the registered `T` is the handle).
73 let lsp: LspSupervisorHandle = (*boot
74 .service::<LspSupervisorHandle>()
75 .expect("LspSupervisorHandle registered as a Phase-A service before lattice_lsp::install"))
76 .clone();
77 register_lsp_completion_mode(boot.modes_mut(), lsp);
78
79 // ── Off-keystroke wakes ─────────────────────────────────────────────────
80 // The four `workspace/*/refresh` notifications repaint without a keypress.
81 // The per-type drain channels (host-side `pending_*_refresh_rx`) still do
82 // the cache-eviction in `run_tick_pending`; these wakes only fire
83 // `async_landed`. Byte-identical to the retired L1c `wake_on` forwarders.
84 boot.wake_on_event::<LspInlayHintRefresh>();
85 boot.wake_on_event::<LspSemanticTokensRefresh>();
86 boot.wake_on_event::<LspDiagnosticRefresh>();
87 boot.wake_on_event::<LspCodeLensRefresh>();
88
89 // ── EP.3: the error-list feed ───────────────────────────────────────────
90 install_error_list_feed(boot);
91
92 // ── LR.1: the references multibuffer ────────────────────────────────────
93 install_references_provider(boot);
94}
95
96/// LR.1 (2026-08-11): register the references view's mode + per-view
97/// service, and wire its `DocumentClosed` cleanup.
98///
99/// The provider lives in this crate rather than `lattice-multibuffer`
100/// per the provider-home reversal: `gr`'s binding, the handler bodies
101/// and the view all belong to the LSP subsystem, so they sit together.
102fn install_references_provider(boot: &mut impl SubsystemBoot) {
103 use crate::providers::references::{
104 LspReferencesService, LspReferencesServiceHandle, register_references_mode,
105 };
106
107 register_references_mode(boot.modes_mut());
108 // The refresh action's command must resolve for the mode's
109 // `refresh_action()` target and its handler registration.
110 crate::providers::references::register_references_actions(boot.commands_mut());
111
112 let service: LspReferencesServiceHandle = Arc::new(LspReferencesService::new());
113 boot.register_service::<LspReferencesServiceHandle>(Arc::clone(&service));
114
115 // Drop a closed view's stored origin. Same shape as
116 // `register_multibuffer_modes`: only wire the subscriber when a
117 // runtime is in scope, so `Editor` constructed outside one (the
118 // host lib tests) skips it rather than panicking. A skipped
119 // subscriber leaks at most one small entry per view in a
120 // short-lived test process.
121 let Ok(handle) = tokio::runtime::Handle::try_current() else {
122 tracing::debug!(
123 "references: no tokio runtime in scope; skipping DocumentClosed cleanup \
124 (expected in test paths)"
125 );
126 return;
127 };
128 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<lattice_protocol::Event>();
129 boot.event_bus().subscribe(
130 lattice_runtime::EventFilter::kind(lattice_protocol::EventKind::DocumentClosed),
131 lattice_runtime::SubscriptionTarget::Channel(tx),
132 );
133 handle.spawn(async move {
134 while let Some(event) = rx.recv().await {
135 if let lattice_protocol::Event::DocumentClosed { id } = event {
136 service.forget_by_document_id(id);
137 }
138 }
139 });
140}
141
142/// EP.3 (2026-08-10): wire the language server up as a producer of the
143/// core error list.
144///
145/// `boot.inbound` is mandatory here, not stylistic: its `send` bakes in
146/// the `async_landed` wake, so a republish reaches `*problems*` and the
147/// `:next-error` family without waiting for a keystroke. A bare
148/// `tick_callback` would reproduce the "it only updates when I press
149/// something" bug class (`boot-composition.md` §3).
150///
151/// Every write is [`ErrorWrite::Refresh`] — a live feed must re-anchor
152/// the navigation index, or walking the list while typing snaps the
153/// user back to entry 1 on each keystroke (EP.2).
154///
155/// No-ops when the `DiagnosticsLayer` is absent (test harnesses that
156/// install a trimmed boot): the feed simply never starts, rather than
157/// panicking a subsystem installer.
158fn install_error_list_feed(boot: &mut impl SubsystemBoot) {
159 let Some(layer) = boot.service::<DiagnosticsLayer>() else {
160 tracing::debug!("lsp: DiagnosticsLayer service absent; error-list feed not started");
161 return;
162 };
163 let layer: DiagnosticsLayer = (*layer).clone();
164
165 let config = boot.service::<Arc<ConfigRegistry>>();
166
167 let bus = boot.inbound::<Vec<ErrorEntry>, _>(|entries| {
168 vec![Effect::AppAction(AppEffect::SetErrorList {
169 source: ErrorSource::Lsp,
170 write: ErrorWrite::Refresh,
171 entries,
172 })]
173 });
174
175 // Read the option every tick rather than capturing its value, so
176 // `:set lsp.diagnostics-to-error-list` takes effect immediately
177 // instead of at the next restart.
178 let enabled = move || match &config {
179 // `get_typed` returns None before the option registry is
180 // initialised; treat that as the option's default (on) rather
181 // than silently disabling a feature the user expects to have.
182 Some(cfg) => cfg
183 .get_typed::<lattice_config::core_options::LspDiagnosticsToErrorList>()
184 .map(|v| *v)
185 .unwrap_or(true),
186 None => true,
187 };
188
189 let feed = ErrorListFeed::spawn(layer, enabled, move |entries| {
190 // A send failure means the drain is gone — the editor is
191 // shutting down. Nothing to recover, and nothing worth
192 // surfacing to the user, but don't swallow it silently either.
193 if bus.send(entries).is_err() {
194 tracing::debug!("lsp: error-list drain closed; feed send dropped");
195 }
196 });
197 boot.register_service::<crate::error_list_feed::ErrorListFeedHandle>(Arc::new(feed));
198}