lattice_compilation/lib.rs
1//! Native compilation mode: runs a build / test / lint command off-thread and
2//! streams its output into a read-only `*compilation*` buffer (CM.1).
3//!
4//! Runs a build/test/lint command off-thread (pipe-captured) and
5//! streams its stdout+stderr into a read-only synthetic
6//! `*compilation*` `Document` buffer live. The emacs
7//! `compilation-mode` / `M-x recompile` and vim `:make` workflow's
8//! streaming half, on Lattice's substrate.
9//!
10//! This is a **native built-in** wired through the `SubsystemBoot`
11//! install seam (not a plugin). CM.1 delivers the streaming buffer
12//! plus `:compile` / `:recompile` / `:make`. The error list
13//! (CM.2), parser registry (CM.3), and `*problems*` multibuffer
14//! view (CM.4) land in later slices — see
15//! `docs/dev/architecture/compilation-mode.md` and its slice plan.
16//!
17//! ## Shape
18//!
19//! - [`events`] — the `CompilationOutputPushed` typed event +
20//! `OutputChunk`.
21//! - [`mode`] — `CompilationMode` (`ReadOnly + NoFile` major) + its
22//! streaming drain.
23//! - [`service`] — `CompilationService` (process lifecycle,
24//! pipe-capture, kill-on-recompile) off the actor thread.
25//! - [`ex_commands`] — `:compile` / `:recompile` / `:make`.
26//! - [`install`] — the crate-owned `SubsystemBoot` entry point.
27
28mod ansi;
29mod events;
30mod ex_commands;
31mod headerline;
32mod mode;
33mod parser;
34mod parser_factory;
35mod parsers;
36mod service;
37
38pub use ansi::{AnsiPalette, CleanLine, SgrState, clean_line};
39pub use events::{CompilationOutputPushed, OutputChunk};
40pub use ex_commands::register_compilation_ex_commands;
41pub use headerline::{
42 COMPILATION_HEADERLINE_PROVIDER_ID, CompilationHeaderline, CompilationHeadlineState,
43};
44pub use mode::{CompilationMode, apply_chunk};
45pub use parser::{
46 CompilationLocation, CompilationParser, ParserRegistry, match_severity, parse_location_line,
47 scan_location_lines, scan_severities,
48};
49pub use parser_factory::{
50 CompilationParserFactories, CompilationParserFactoriesHandle, CompilationParserFactory,
51};
52pub use service::{CompilationService, CompilationServiceHandle, DefaultCompilationService};
53
54use std::path::PathBuf;
55use std::sync::Arc;
56
57use lattice_core::{BufferFlags, BufferId};
58use lattice_grammar::AppEffect;
59use lattice_grammar::effect::Effect;
60use lattice_mode::inbound::InboundBus;
61use lattice_mode::{GutterSeverityLevel, ModeActivator, SubsystemBoot};
62use lattice_protocol::error_list::{ErrorEntry, ErrorSeverity};
63
64/// CM.3c: the off-thread → host-state seam for the `*compilation*` buffer's
65/// per-buffer severity gutter index. The compilation drain (in
66/// `CompilationMode::on_activate`) sends `(buffer_id, full_index)` through
67/// this bus whenever the index changes; the `install`-registered handler
68/// maps each send to [`AppEffect::CompilationGutterSet`]. Registered as a
69/// [`ServiceRegistry`](lattice_mode::ServiceRegistry) handle under this exact
70/// alias (per the Arc/TypeId convention); the drain looks it up via
71/// `ctx.service::<CompilationGutterBusHandle>()`. `send` bakes in the editor
72/// wake, so marks reach the screen off-keystroke.
73pub type CompilationGutterBusHandle = Arc<InboundBus<(BufferId, Vec<(u32, ErrorSeverity)>)>>;
74
75/// CM.3c (2026-07-22): the off-thread → host-state seam for the
76/// `*compilation*` buffer's location-line index (theme-based
77/// highlighting). Twin of [`CompilationGutterBusHandle`]: the
78/// compilation drain sends `(buffer_id, full_location_lines)` through
79/// this bus whenever the index changes; the `install`-registered handler
80/// maps each send to [`AppEffect::CompilationLocationLines`].
81/// Registered as a ServiceRegistry handle under this exact alias; the
82/// drain looks it up via `ctx.service::<CompilationLocationBusHandle>()`.
83pub type CompilationLocationBusHandle = Arc<InboundBus<(BufferId, Vec<(u32, u32, u32)>)>>;
84
85/// CM.3d (2026-07-22): the theme colours bus — the mode sends
86/// resolved `(bg, fg)` once during activation; the handler maps to
87/// [`AppEffect::CompilationThemeColors`].
88pub type CompilationThemeColorsBusHandle = Arc<InboundBus<(u32, u32)>>;
89
90/// CM.5: write-once slot holding the interned `compilation.ansi.*`
91/// element ids the pipe readers paint captured SGR with.
92///
93/// Registered empty by [`install`] and filled by
94/// [`CompilationMode`]'s `on_activate`. The split exists because the
95/// two halves are available at different times: the service is built
96/// during Phase-B boot, and a `ThemeRegistry` is not registered until
97/// well after that, whereas mode activation happens long after boot
98/// with the full registry in hand.
99///
100/// An unfilled slot is not a failure mode — output still streams and
101/// is still escape-stripped, just uncoloured.
102pub type CompilationAnsiSlot = Arc<std::sync::OnceLock<AnsiPalette>>;
103
104/// CM.3c: map the parser-native [`ErrorSeverity`] onto the renderer-
105/// facing [`GutterSeverityLevel`] (`Error→Error`, `Warning→Warning`,
106/// `Info→Info`, `Note→Info`). The single conversion in the whole
107/// compilation-severity pipeline — the parser, the inbound bus, and the
108/// `AppEffect` payload all carry `ErrorSeverity`, and the host arm calls
109/// this once when writing the render-state slot. `Note` collapses to `Info`
110/// because `GutterSeverityLevel` has no `Note` rank; there is no reverse
111/// conversion, so nothing is lost by round-trip.
112pub fn gutter_level(severity: ErrorSeverity) -> GutterSeverityLevel {
113 match severity {
114 ErrorSeverity::Error => GutterSeverityLevel::Error,
115 ErrorSeverity::Warning => GutterSeverityLevel::Warning,
116 ErrorSeverity::Info => GutterSeverityLevel::Info,
117 ErrorSeverity::Note => GutterSeverityLevel::Info,
118 }
119}
120
121/// Synthetic name of the streaming compilation buffer. `:ls` and
122/// `:b *compilation*` reach it; `:bn` / `:bp` skip it
123/// (`listed = false`).
124pub const COMPILATION_BUFFER_NAME: &str = "*compilation*";
125
126/// Compilation-mode's synthetic-buffer flags: unlisted (skipped by
127/// `:bn` / `:bp`), non-hidden, non-ephemeral — the canonical shape for
128/// a mode-owned subsystem buffer.
129const COMPILATION_BUFFER_FLAGS: BufferFlags = BufferFlags {
130 listed: false,
131 hidden: false,
132 ephemeral: false,
133};
134
135/// Provision the `*compilation*` buffer and kick off a run — the
136/// compilation mode's own responsibility, driven through the
137/// `&mut`-backed [`ModeActivator::ensure_named_document`] creation seam.
138///
139/// Called from the `AppEffect::CompileRun` dispatch arm (which passes
140/// the `Editor` as `&mut dyn ModeActivator`). Creates + activates the
141/// `*compilation*` buffer on first use — activation runs
142/// [`CompilationMode`]'s `on_activate`, establishing the streaming drain
143/// **before** the service publishes its first `Reset` — then runs the
144/// registered [`CompilationServiceHandle`]. Idempotent on `:recompile`
145/// (reuses the buffer; the drain from the first activation stays live).
146///
147/// Returns the buffer id (so the host can activate the buffer + repaint),
148/// or `None` when the compilation service is not registered.
149pub fn start_compilation(
150 activator: &mut dyn ModeActivator,
151 cmdline: Option<String>,
152 cwd: Option<PathBuf>,
153) -> Option<BufferId> {
154 let id = activator.ensure_named_document(
155 COMPILATION_BUFFER_NAME,
156 CompilationMode::mode_id(),
157 COMPILATION_BUFFER_FLAGS,
158 );
159 // `services.get::<CompilationServiceHandle>()` returns
160 // `Arc<Arc<dyn CompilationService>>` per the ServiceRegistry
161 // Arc/TypeId convention — unwrap one layer before `run`.
162 let svc = activator.services().get::<CompilationServiceHandle>()?;
163 (*svc).clone().run(cmdline, cwd);
164 Some(id)
165}
166
167/// Wire the compilation subsystem's ex-commands, mode, service, and
168/// off-keystroke wake into the editor at boot. One Phase-B line in
169/// `editor_boot.rs`.
170pub fn install(boot: &mut impl SubsystemBoot) {
171 register_compilation_ex_commands(boot.commands_mut());
172 boot.modes_mut().register(CompilationMode).ok();
173
174 // CM.3a: the sanctioned off-thread → host-state seam for parsed
175 // error entries (LSP-diagnostics shape). The stderr reader
176 // accumulates parsed `ErrorEntry`s and sends the FULL list; this
177 // handler maps each send to `AppEffect::SetErrorList`, whose host arm
178 // calls `Editor::set_error_list` (replace-semantics — the growing
179 // list stays visible; an empty vec on a new run clears it). The wake
180 // is baked into `InboundBus::send`, so the list reaches the screen
181 // off-keystroke without a keypress.
182 let qf_bus = boot.inbound::<Vec<ErrorEntry>, _>(|entries| {
183 vec![Effect::AppAction(AppEffect::SetErrorList {
184 // EP.1: this producer owns the `Compilation` slice; an LSP
185 // republish alongside it replaces only its own.
186 source: lattice_protocol::error_list::ErrorSource::Compilation,
187 // A compile run is a new run: start at the top.
188 write: lattice_protocol::error_list::ErrorWrite::NewRun,
189 entries,
190 })]
191 });
192
193 // CM.3c: the twin off-thread → host-state seam for the `*compilation*`
194 // buffer's per-buffer severity gutter index. The drain (in
195 // `CompilationMode::on_activate`) sends `(buffer_id, full_index)`; this
196 // handler maps each send to `AppEffect::CompilationGutterSet`, whose host
197 // arm writes the render-state slot the renderer injects into
198 // `gutter_decorations`. Registered as a service so the drain — which lives
199 // in the mode's `on_activate`, not in the service — can reach it via
200 // `ctx.service`.
201 let gutter_bus =
202 boot.inbound::<(BufferId, Vec<(u32, ErrorSeverity)>), _>(|(buffer, entries)| {
203 vec![Effect::AppAction(AppEffect::CompilationGutterSet {
204 buffer: buffer.0,
205 entries,
206 })]
207 });
208 boot.register_service::<CompilationGutterBusHandle>(Arc::new(gutter_bus));
209
210 // CM.3c (2026-07-22): the location-line index bus — twin of the
211 // gutter-severity bus above. The drain sends `(buffer_id, lines)`
212 // through this whenever the index changes; the handler maps each
213 // send to `AppEffect::CompilationLocationLines`, which the host
214 // stores in the render-state slot the renderers read.
215 let location_bus = boot.inbound::<(BufferId, Vec<(u32, u32, u32)>), _>(|(buffer, lines)| {
216 vec![Effect::AppAction(AppEffect::CompilationLocationLines {
217 buffer: buffer.0,
218 lines,
219 })]
220 });
221 boot.register_service::<CompilationLocationBusHandle>(Arc::new(location_bus));
222
223 // CM.3d (2026-07-22): theme colours bus — the mode sends resolved
224 // `compilation.location` bg/fg once during activation so the
225 // renderers read from the theme rather than hardcoding RGB.
226 let theme_colors_bus = boot.inbound::<(u32, u32), _>(|(bg, fg)| {
227 vec![Effect::AppAction(AppEffect::CompilationThemeColors {
228 bg,
229 fg,
230 })]
231 });
232 boot.register_service::<CompilationThemeColorsBusHandle>(Arc::new(theme_colors_bus));
233
234 // CM.5: the slot the interned `compilation.ansi.*` element ids
235 // land in.
236 //
237 // It is a slot rather than a value because of boot ordering: this
238 // `install` runs in Phase B, well before the host registers
239 // `ThemeRegistryHandle`, so resolving the palette here would
240 // silently yield `None` forever — a build that compiles clean and
241 // never colours anything. `CompilationMode::on_activate` fills it
242 // instead, which is where a theme registry is reachable (the
243 // headerline colours are already resolved there) and which
244 // `start_compilation` guarantees runs before the first run.
245 let ansi_slot: CompilationAnsiSlot = Arc::new(std::sync::OnceLock::new());
246 boot.register_service::<CompilationAnsiSlot>(ansi_slot.clone());
247
248 // CM.6b: the plugin parser-factory registry. Registered here (rather
249 // than filled by the mode, like the ANSI slot) because its writer is
250 // the plugin loader, which runs long after boot and reaches it by
251 // service lookup — there is nothing to resolve at install time, only
252 // an empty registry to publish.
253 let parser_factories = CompilationParserFactories::new_handle();
254 boot.register_service::<CompilationParserFactoriesHandle>(parser_factories.clone());
255
256 let svc: CompilationServiceHandle = Arc::new(
257 DefaultCompilationService::new(
258 boot.event_bus().clone(),
259 boot.runtime_handle().clone(),
260 qf_bus,
261 )
262 .with_ansi_slot(ansi_slot)
263 .with_parser_factories(parser_factories),
264 );
265 boot.register_service::<CompilationServiceHandle>(svc);
266
267 // Streamed output arrives off-keystroke; wake the editor so the
268 // `*compilation*` buffer repaints without a keypress.
269 boot.wake_on_event::<CompilationOutputPushed>();
270}
271
272#[cfg(test)]
273mod gutter_level_tests {
274 use super::gutter_level;
275 use lattice_mode::GutterSeverityLevel;
276 use lattice_protocol::error_list::ErrorSeverity;
277
278 #[test]
279 fn maps_each_severity_to_a_gutter_level() {
280 // Error/Warning/Info map 1:1; Note collapses to Info (GutterSeverityLevel
281 // has no Note rank). Single conversion in the whole pipeline.
282 assert_eq!(
283 gutter_level(ErrorSeverity::Error),
284 GutterSeverityLevel::Error
285 );
286 assert_eq!(
287 gutter_level(ErrorSeverity::Warning),
288 GutterSeverityLevel::Warning
289 );
290 assert_eq!(gutter_level(ErrorSeverity::Info), GutterSeverityLevel::Info);
291 assert_eq!(gutter_level(ErrorSeverity::Note), GutterSeverityLevel::Info);
292 }
293}