Skip to main content

lattice_compilation/
ex_commands.rs

1//! CM.1: the `:compile` / `:recompile` / `:make` ex-commands.
2//!
3//! Each parses its (optional) argument into an
4//! `Option<String>` cmdline and returns
5//! `Effect::AppAction(AppEffect::CompileRun { cmdline })`; the
6//! host's apply-effect arm creates the `*compilation*` buffer
7//! (`Editor::ensure_named_synthetic_document`, host-side) and runs the
8//! registered [`crate::CompilationServiceHandle`]. `:compile` and
9//! `:recompile` are
10//! the emacs-canonical names for this feature (not an LSP-coupled
11//! subsystem, so the dashed-namespaced rule does not apply);
12//! `:make` is the vim-canonical alias.
13
14use std::sync::Arc;
15
16use lattice_grammar::CommandRegistry;
17use lattice_grammar::app_effect::AppEffect;
18use lattice_grammar::args::{ArgKind, ArgSpec, Args};
19use lattice_grammar::command::LatencyClass;
20use lattice_grammar::effect::Effect;
21use lattice_grammar::error::CommandError;
22use lattice_grammar::registry::{ExCommandSpec, SurfaceForm};
23
24/// Parse an already-trimmed argument string into an optional
25/// cmdline (empty ⇒ `None`).
26fn arg_to_cmdline(args: &Args) -> Option<String> {
27    match args {
28        Args::String(s) => Some(s.clone()),
29        _ => None,
30    }
31}
32
33/// Register `:compile` / `:recompile` / `:make`.
34pub fn register_compilation_ex_commands(registry: &mut CommandRegistry) {
35    // `:compile <cmd>` — run a shell command, streaming into
36    // `*compilation*`. Requires a non-empty command.
37    registry.register_ex_command(
38        "compile",
39        "Run a shell command and stream its output into the *compilation* buffer.",
40        ExCommandSpec {
41            latency_class: LatencyClass::Reflex,
42            accepts_bang: false,
43            accepts_range: false,
44            parse_args: Arc::new(|s: &str, _bang: bool| {
45                let trimmed = s.trim();
46                if trimmed.is_empty() {
47                    return Err(CommandError::BadArgs(":compile requires a command".into()));
48                }
49                Ok(Args::String(trimmed.to_string()))
50            }),
51            apply: Arc::new(|ctx| {
52                Ok(Effect::AppAction(AppEffect::CompileRun {
53                    cmdline: arg_to_cmdline(&ctx.args),
54                }))
55            }),
56            args_schema: vec![ArgSpec::required(
57                "command",
58                ArgKind::String,
59                "shell command to run",
60            )],
61            surface_form: SurfaceForm::Keyword,
62        },
63    );
64
65    // `:recompile` — re-run the last compilation command. No args.
66    registry.register_ex_command(
67        "recompile",
68        "Re-run the last compilation command, clearing and re-streaming *compilation*.",
69        ExCommandSpec {
70            latency_class: LatencyClass::Reflex,
71            accepts_bang: false,
72            accepts_range: false,
73            parse_args: Arc::new(|s: &str, _bang: bool| {
74                if s.trim().is_empty() {
75                    Ok(Args::None)
76                } else {
77                    Err(CommandError::BadArgs(
78                        ":recompile takes no arguments".into(),
79                    ))
80                }
81            }),
82            apply: Arc::new(|_ctx| Ok(Effect::AppAction(AppEffect::CompileRun { cmdline: None }))),
83            args_schema: vec![],
84            surface_form: SurfaceForm::Keyword,
85        },
86    );
87
88    // `:make [cmd]` — vim-canonical alias. Optional override runs a
89    // one-off command; bare `:make` reuses the last command.
90    registry.register_ex_command(
91        "make",
92        "Run the build command (optionally overridden) into the *compilation* buffer.",
93        ExCommandSpec {
94            latency_class: LatencyClass::Reflex,
95            accepts_bang: false,
96            accepts_range: false,
97            parse_args: Arc::new(|s: &str, _bang: bool| {
98                let trimmed = s.trim();
99                if trimmed.is_empty() {
100                    Ok(Args::None)
101                } else {
102                    Ok(Args::String(trimmed.to_string()))
103                }
104            }),
105            apply: Arc::new(|ctx| {
106                Ok(Effect::AppAction(AppEffect::CompileRun {
107                    cmdline: arg_to_cmdline(&ctx.args),
108                }))
109            }),
110            args_schema: vec![ArgSpec::optional(
111                "command",
112                ArgKind::String,
113                "build command override",
114            )],
115            surface_form: SurfaceForm::Keyword,
116        },
117    );
118
119    // CM.3d (2026-07-22): kill the running compilation via `<C-c>`.
120    // No args; the host arm calls CompilationService::kill().
121    registry.register_ex_command(
122        "compilation-kill",
123        "Kill the running compilation child process.",
124        ExCommandSpec {
125            latency_class: LatencyClass::Reflex,
126            accepts_bang: false,
127            accepts_range: false,
128            parse_args: Arc::new(|_s: &str, _bang: bool| Ok(Args::None)),
129            apply: Arc::new(|_| Ok(Effect::AppAction(AppEffect::CompilationKill))),
130            args_schema: vec![],
131            surface_form: SurfaceForm::Keyword,
132        },
133    );
134}
135
136#[cfg(test)]
137mod tests {
138    #![allow(clippy::unwrap_used, clippy::panic)]
139    use super::*;
140
141    fn ex_spec<'a>(registry: &'a CommandRegistry, name: &str) -> Option<&'a ExCommandSpec> {
142        registry
143            .id_by_name(name)
144            .and_then(|id| registry.ex_command_spec(id))
145    }
146
147    #[test]
148    fn registers_all_four_commands() {
149        let mut registry = CommandRegistry::new();
150        register_compilation_ex_commands(&mut registry);
151        assert!(ex_spec(&registry, "compile").is_some());
152        assert!(ex_spec(&registry, "recompile").is_some());
153        assert!(ex_spec(&registry, "make").is_some());
154        assert!(ex_spec(&registry, "compilation-kill").is_some());
155    }
156
157    #[test]
158    fn compile_rejects_empty_and_accepts_command() {
159        let mut registry = CommandRegistry::new();
160        register_compilation_ex_commands(&mut registry);
161        let spec = ex_spec(&registry, "compile").unwrap();
162        assert!((spec.parse_args)("", false).is_err());
163        let parsed = (spec.parse_args)("cargo build", false).unwrap();
164        match parsed {
165            Args::String(s) => assert_eq!(s, "cargo build"),
166            other => panic!("expected String args, got {other:?}"),
167        }
168    }
169
170    #[test]
171    fn make_arg_is_optional() {
172        let mut registry = CommandRegistry::new();
173        register_compilation_ex_commands(&mut registry);
174        let spec = ex_spec(&registry, "make").unwrap();
175        assert!(matches!((spec.parse_args)("", false).unwrap(), Args::None));
176        assert!(matches!(
177            (spec.parse_args)("cargo test", false).unwrap(),
178            Args::String(_)
179        ));
180    }
181}