main
1#!/usr/bin/env nix-shell
2#! nix-shell -i python3 -p python3 nurl
3"""
4Generate grammar information for Helix editor by parsing languages.toml
5and fetching source information using nurl in parallel.
6"""
7
8import argparse
9import asyncio
10import json
11import os
12import sys
13import tomllib
14from dataclasses import dataclass
15from pathlib import Path
16from typing import Any
17
18
19@dataclass
20class Grammar:
21 name: str
22 git_url: str
23 rev: str
24 subpath: str | None = None
25
26
27async def run_nurl(url: str, rev: str, semaphore: asyncio.Semaphore) -> dict[str, Any]:
28 """Run nurl command for a single grammar and return parsed JSON output."""
29 async with semaphore:
30 proc = await asyncio.create_subprocess_exec(
31 "nurl",
32 url,
33 rev,
34 "--json",
35 stdout=asyncio.subprocess.PIPE,
36 stderr=asyncio.subprocess.PIPE,
37 )
38 stdout, stderr = await proc.communicate()
39
40 if proc.returncode != 0:
41 raise RuntimeError(f"nurl failed for {url}@{rev}: {stderr.decode()}")
42
43 return json.loads(stdout.decode())
44
45
46def parse_languages_toml(toml_path: Path) -> list[Grammar]:
47 """Parse languages.toml and extract grammar information."""
48 with open(toml_path, "rb") as f:
49 config = tomllib.load(f)
50
51 grammars = []
52 for grammar in config.get("grammar", []):
53 if "source" not in grammar:
54 continue
55
56 source = grammar["source"]
57 if "git" not in source or "rev" not in source:
58 continue
59
60 grammars.append(
61 Grammar(
62 name=grammar["name"].replace("_", "-"),
63 git_url=source["git"],
64 rev=source["rev"],
65 subpath=source.get("subpath"),
66 )
67 )
68
69 return grammars
70
71
72async def fetch_all_grammars(
73 grammars: list[Grammar], max_parallel: int
74) -> dict[str, Any]:
75 """Fetch nurl information for all grammars in parallel."""
76 semaphore = asyncio.Semaphore(max_parallel)
77 results = {}
78 total = len(grammars)
79 completed = 0
80
81 tasks = []
82 for grammar in grammars:
83 task = run_nurl(grammar.git_url, grammar.rev, semaphore)
84 tasks.append((grammar, task))
85
86 for grammar, task in tasks:
87 try:
88 result = await task
89 results[grammar.name] = {
90 "nurl": result,
91 "subpath": grammar.subpath,
92 }
93 completed += 1
94 print(f"[{completed}/{total}] ✓ {grammar.name}", file=sys.stderr)
95 except Exception as e:
96 completed += 1
97 print(f"[{completed}/{total}] ✗ {grammar.name}: {e}", file=sys.stderr)
98 results[grammar.name] = {"error": str(e)}
99
100 return results
101
102
103async def main():
104 parser = argparse.ArgumentParser(
105 description="Generate grammar information for Helix editor"
106 )
107 parser.add_argument(
108 "languages_toml",
109 type=Path,
110 help="path to languages.toml from Helix repository",
111 )
112 parser.add_argument(
113 "-o",
114 "--output",
115 type=Path,
116 default=Path("grammars.json"),
117 help="output JSON file (default: grammars.json)",
118 )
119 parser.add_argument(
120 "-j",
121 "--jobs",
122 type=int,
123 default=os.cpu_count(),
124 help=f"number of parallel nurl instances (default: {os.cpu_count()})",
125 )
126
127 args = parser.parse_args()
128
129 if not args.languages_toml.exists():
130 print(f"Error: {args.languages_toml} not found", file=sys.stderr)
131 sys.exit(1)
132
133 print(f"Parsing {args.languages_toml}...", file=sys.stderr)
134 grammars = parse_languages_toml(args.languages_toml)
135 print(f"Found {len(grammars)} grammars", file=sys.stderr)
136
137 print(f"Fetching grammar information ({args.jobs} parallel jobs)...", file=sys.stderr)
138 results = await fetch_all_grammars(grammars, args.jobs)
139
140 errors = [name for name, data in results.items() if "error" in data]
141 if errors:
142 print(f"\nFailed grammars ({len(errors)}):", file=sys.stderr)
143 for name in errors:
144 print(f" - {name}: {results[name]['error']}", file=sys.stderr)
145 sys.exit(1)
146
147 with open(args.output, "w") as f:
148 json.dump(results, f, indent=2)
149 f.write('\n')
150
151 print(f"\nResults written to {args.output}", file=sys.stderr)
152
153
154if __name__ == "__main__":
155 asyncio.run(main())