plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
b5f5be1
— Peter John
2026-09-08T15:03:28+05:30
feat(cli): fix stale Helix grammar path and embed a precompiled grammar .so
- plum-cli/Cargo.toml +1 -0
- plum-cli/build.rs +58 -0
- plum-cli/src/editor.rs +91 -3
plum-cli/Cargo.toml
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name = "plum-cli"
|
|
3
3
|
version = "0.1.0"
|
|
4
4
|
edition = "2021"
|
|
5
|
+
build = "build.rs"
|
|
5
6
|
|
|
6
7
|
[[bin]]
|
|
7
8
|
name = "plum"
|
plum-cli/build.rs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//! Precompiles the `tree-sitter-plum` grammar into a native shared library at
|
|
2
|
+
//! `plum`-build time (using whatever C compiler is already on the machine —
|
|
3
|
+
//! the same one Cargo itself needs to build `tree-sitter-plum`'s own
|
|
4
|
+
//! `scanner.c`/`parser.c` for in-process parsing), so `plum editor helix` can
|
|
5
|
+
//! just copy the finished `.so` into Helix's `runtime/grammars/` directory
|
|
6
|
+
//! instead of shelling out to `hx --grammar build` (which requires `hx` on
|
|
7
|
+
//! PATH, a working C compiler on the *installing* machine again, and a
|
|
8
|
+
//! `languages.toml` grammar `source.path` that still resolves).
|
|
9
|
+
//!
|
|
10
|
+
//! If the compiler invocation fails (no C compiler found), this falls back to
|
|
11
|
+
//! writing an empty placeholder rather than failing the whole `plum-cli`
|
|
12
|
+
//! build — `editor::installHelix` detects the empty payload and falls back to
|
|
13
|
+
//! the old `hx --grammar build` codepath at install time instead.
|
|
14
|
+
|
|
15
|
+
use std::env;
|
|
16
|
+
use std::path::PathBuf;
|
|
17
|
+
use std::process::Command;
|
|
18
|
+
|
|
19
|
+
fn main() {
|
|
20
|
+
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
21
|
+
let src_dir = manifest_dir.join("../plum-tooling/tree-sitter-plum/src");
|
|
22
|
+
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
23
|
+
let so_path = out_dir.join("plum-grammar.so");
|
|
24
|
+
|
|
25
|
+
println!("cargo:rerun-if-changed={}", src_dir.join("parser.c").display());
|
|
26
|
+
println!("cargo:rerun-if-changed={}", src_dir.join("scanner.c").display());
|
|
27
|
+
|
|
28
|
+
let cc = env::var("CC").unwrap_or_else(|_| "cc".to_string());
|
|
29
|
+
let status = Command::new(&cc)
|
|
30
|
+
.arg("-shared")
|
|
31
|
+
.arg("-fPIC")
|
|
32
|
+
.arg("-O2")
|
|
33
|
+
.arg("-I")
|
|
34
|
+
.arg(&src_dir)
|
|
35
|
+
.arg("-o")
|
|
36
|
+
.arg(&so_path)
|
|
37
|
+
.arg(src_dir.join("parser.c"))
|
|
38
|
+
.arg(src_dir.join("scanner.c"))
|
|
39
|
+
.status();
|
|
40
|
+
|
|
41
|
+
match status {
|
|
42
|
+
Ok(s) if s.success() => {
|
|
43
|
+
println!("cargo:rustc-env=PLUM_GRAMMAR_SO={}", so_path.display());
|
|
44
|
+
}
|
|
45
|
+
other => {
|
|
46
|
+
std::fs::write(&so_path, []).expect("failed to write placeholder grammar .so");
|
|
47
|
+
println!("cargo:rustc-env=PLUM_GRAMMAR_SO={}", so_path.display());
|
|
48
|
+
match other {
|
|
49
|
+
Ok(s) => println!(
|
|
50
|
+
"cargo:warning=`{cc}` exited with {s} while precompiling the Plum tree-sitter grammar; `plum editor helix` will fall back to `hx --grammar build`"
|
|
51
|
+
),
|
|
52
|
+
Err(e) => println!(
|
|
53
|
+
"cargo:warning=could not run `{cc}` to precompile the Plum tree-sitter grammar ({e}); `plum editor helix` will fall back to `hx --grammar build`"
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
plum-cli/src/editor.rs
CHANGED
|
@@ -44,6 +44,12 @@ const HELIX_TAGS: &str =
|
|
|
44
44
|
const HELIX_TEXTOBJECTS: &str =
|
|
45
45
|
include_str!("../../plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm");
|
|
46
46
|
|
|
47
|
+
/// Precompiled by `build.rs` from the same `parser.c`/`scanner.c` that back
|
|
48
|
+
/// in-process parsing, on whatever machine builds this `plum` binary. Empty
|
|
49
|
+
/// when `build.rs` couldn't find a C compiler — see `buildGrammar` for the
|
|
50
|
+
/// fallback used in that case.
|
|
51
|
+
const HELIX_GRAMMAR_SO: &[u8] = include_bytes!(env!("PLUM_GRAMMAR_SO"));
|
|
52
|
+
|
|
47
53
|
/// The absolute path to the `tree-sitter-plum` grammar crate on the machine that
|
|
48
54
|
/// *built* this `plum` binary. Baked in at compile time so a contributor running
|
|
49
55
|
/// `cargo run -p plum-cli -- editor helix` from their own checkout gets a
|
|
@@ -109,8 +115,20 @@ fn installHelix() -> Result<()> {
|
|
|
109
115
|
// into a shared library under runtime/grammars/. `hx --health <lang>`
|
|
110
116
|
// reports the tree-sitter parser as present just because the config is
|
|
111
117
|
// valid, even when that .so has never been built, which makes a missing
|
|
112
|
-
// build step easy to miss.
|
|
118
|
+
// build step easy to miss. We already have one precompiled (by our own
|
|
119
|
+
// `build.rs`, on this same machine) embedded in the binary, so just drop
|
|
120
|
+
// it in directly instead of shelling out to `hx --grammar build`.
|
|
121
|
+
if HELIX_GRAMMAR_SO.is_empty() {
|
|
113
|
-
|
|
122
|
+
buildGrammar();
|
|
123
|
+
} else {
|
|
124
|
+
let grammars_dir = config_dir.join("runtime").join("grammars");
|
|
125
|
+
fs::create_dir_all(&grammars_dir)
|
|
126
|
+
.with_context(|| format!("failed to create {}", grammars_dir.display()))?;
|
|
127
|
+
let so_path = grammars_dir.join("plum.so");
|
|
128
|
+
fs::write(&so_path, HELIX_GRAMMAR_SO)
|
|
129
|
+
.with_context(|| format!("failed to write {}", so_path.display()))?;
|
|
130
|
+
println!("installed precompiled Plum tree-sitter grammar to {}", so_path.display());
|
|
131
|
+
}
|
|
114
132
|
Ok(())
|
|
115
133
|
}
|
|
116
134
|
|
|
@@ -169,7 +187,41 @@ fn mergeLanguagesToml(path: &Path) -> Result<()> {
|
|
|
169
187
|
.unwrap_or(false);
|
|
170
188
|
|
|
171
189
|
if already_present {
|
|
190
|
+
let current_path = treeSitterPlumDir();
|
|
191
|
+
let stale_path = doc
|
|
192
|
+
.get_mut("grammar")
|
|
193
|
+
.and_then(|v| v.as_array_of_tables_mut())
|
|
194
|
+
.and_then(|arr| arr.iter_mut().find(|t| t.get("name").and_then(|n| n.as_str()) == Some("plum")))
|
|
195
|
+
.and_then(|grammar| {
|
|
196
|
+
let existing_path = grammar
|
|
197
|
+
.get("source")
|
|
198
|
+
.and_then(|s| s.as_inline_table())
|
|
199
|
+
.and_then(|s| s.get("path"))
|
|
200
|
+
.and_then(|p| p.as_str())
|
|
201
|
+
.map(|s| s.to_string());
|
|
202
|
+
if existing_path.as_deref() != Some(current_path.as_str()) {
|
|
203
|
+
let mut source = toml_edit::InlineTable::new();
|
|
204
|
+
source.insert("path", current_path.clone().into());
|
|
205
|
+
grammar["source"] = toml_edit::Item::Value(toml_edit::Value::InlineTable(source));
|
|
206
|
+
existing_path
|
|
207
|
+
} else {
|
|
208
|
+
None
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
match stale_path {
|
|
213
|
+
Some(old) => {
|
|
214
|
+
fs::write(path, doc.to_string())
|
|
215
|
+
.with_context(|| format!("failed to write {}", path.display()))?;
|
|
216
|
+
println!(
|
|
217
|
+
"{} already has a `plum` language entry; updated its stale grammar path ({old} -> {current_path})",
|
|
218
|
+
path.display()
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
None => {
|
|
172
|
-
|
|
222
|
+
println!("{} already has a `plum` language entry, leaving it as-is", path.display());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
173
225
|
return Ok(());
|
|
174
226
|
}
|
|
175
227
|
|
|
@@ -265,4 +317,40 @@ mod tests {
|
|
|
265
317
|
|
|
266
318
|
std::fs::remove_dir_all(&dir).ok();
|
|
267
319
|
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn mergeLanguagesTomlFixesStaleGrammarPath() {
|
|
323
|
+
let dir = std::env::temp_dir().join(format!("plum-editor-test-stale-{}", std::process::id()));
|
|
324
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
325
|
+
let path = dir.join("languages.toml");
|
|
326
|
+
|
|
327
|
+
std::fs::write(
|
|
328
|
+
&path,
|
|
329
|
+
r##"[[language]]
|
|
330
|
+
name = "plum"
|
|
331
|
+
scope = "source.plum"
|
|
332
|
+
injection-regex = "plum"
|
|
333
|
+
file-types = ["plum"]
|
|
334
|
+
comment-tokens = "#"
|
|
335
|
+
indent = { tab-width = 2, unit = " " }
|
|
336
|
+
|
|
337
|
+
[[grammar]]
|
|
338
|
+
name = "plum"
|
|
339
|
+
source = { path = "/some/stale/path/tree-sitter-plum" }
|
|
340
|
+
"##,
|
|
341
|
+
)
|
|
342
|
+
.unwrap();
|
|
343
|
+
|
|
344
|
+
mergeLanguagesToml(&path).unwrap();
|
|
345
|
+
let updated = std::fs::read_to_string(&path).unwrap();
|
|
346
|
+
assert!(!updated.contains("/some/stale/path/tree-sitter-plum"));
|
|
347
|
+
assert!(updated.contains(&treeSitterPlumDir()));
|
|
348
|
+
|
|
349
|
+
// running again should now be a stable no-op
|
|
350
|
+
mergeLanguagesToml(&path).unwrap();
|
|
351
|
+
let second = std::fs::read_to_string(&path).unwrap();
|
|
352
|
+
assert_eq!(updated, second, "re-running after the fix should be a no-op");
|
|
353
|
+
|
|
354
|
+
std::fs::remove_dir_all(&dir).ok();
|
|
355
|
+
}
|
|
268
356
|
}
|