11 KiB
JSON + YAML grammars — framing (side quest, highlight family)
Revision 4 — 2026-07-21. Status: PR #123 open and awaiting review;
the public and checkpoint branches are at fully gated 5c202c5, rebased
onto main f8096ff.
The JSON provider and Red Hat YAML 1.24.0 have each passed their
PATH-gated pmacs acceptance, in addition to the deterministic fake-server
config-push proof and the YAML standalone protocol smoke.
Intent. Add tree-sitter-json and tree-sitter-yaml grammars (plus
their language servers) to the bundle. Two config formats that pmacs
currently renders as plain text, and — the reason this is the natural
next side quest — the honest gate on the Jupyter .ipynb path (JSON)
and an immediate payoff from the injection engine just shipped (#122):
the markdown block grammar's injections.scm already sets
injection.language "yaml" for --- frontmatter and "toml" for +++
frontmatter, so registering YAML lights up YAML frontmatter highlighting
with zero extra wiring (TOML frontmatter already works — toml landed in
#118 and injections in #122). This is a mostly-additive grammar-gap-style
change, following the #118 pattern, with the frontmatter/fence synergy as
the demonstrable headline.
Ground truth (as of main @ 56eb67e, #121)
- Adding a grammar is a one-line
LanguageEntryincrate::syntax::BUILTIN_LANGUAGES(name,extensions,loader,highlights_query,injections_query) + atree-sitter-foodep. The Luabuffer.after-loadpath picks it up automatically; detection is extension → LSP filetype → filename → shebang (resolve_active_language). - Grammar name MUST equal the
pmacs.lsp.config.<name>key — grammar detection wins over the filetype map, so the name it resolves is the id the LSP client keys off (the #118 invariant; there's an acceptance test that pins every grammar-gap language to its config key). - LSP configs are
pmacs.lsp.config.<name> = … or { command, args, [settings|init_options] }(builtin/runtime/lsp.lua); no json/yaml config today.pmacs.lsp.filetypesis the LSP-only extension fallback (consulted only whenlanguage_for_pathmisses). - Injection synergy (#122). The bundled
tree_sitter_md:: INJECTION_QUERY_BLOCKcontains:((minus_metadata) @injection.content (#set! injection.language "yaml"))—----fenced frontmatter,((plus_metadata) @injection.content (#set! injection.language "toml"))—+++-fenced frontmatter,- fenced code blocks via the dynamic info-string.
So a registered
yamlgrammar is injected into markdown frontmatter automatically, and```json/```yaml/```ymlfences resolve (yml→yaml is already indefault_injection_aliases;jsonis the bundled name).
Confirmed crate facts (probed against the registry + a build under tree-sitter 0.26):
tree-sitter-json0.24.8 —pub const LANGUAGE: LanguageFn(viatree-sitter-language, the modern shared ABI) +HIGHLIGHTS_QUERY. Compiles and links under our tree-sitter 0.26. NoINJECTIONS_QUERY(JSON embeds nothing).tree-sitter-yaml0.7.2 — same shape (LANGUAGE: LanguageFn,HIGHLIGHTS_QUERY,tree-sitter-languagedep). Compiles under 0.26. NoINJECTIONS_QUERY.- Both are the ABI-current crates — not a
tree-sitter ^0.20fork (the dockerfile trap from #118). A single build confirmed link + compile; runtimeset_languageis pinned by the ABI acceptance test.
Decisions
Q#JY1 — Two LanguageEntrys, self-contained highlights, no injections
Add json and yaml to BUILTIN_LANGUAGES, each
highlights_query: &[…::HIGHLIGHTS_QUERY] (self-contained, no
; inherits: delta), injections_query: &[]. Extensions:
- json:
.json. (.jsonc/.json5— comment/trailing-comma variants the plain JSON grammar rejects — are deferred; a.jsoncgrammar or a lenient mode is a separate call.) - yaml:
.yaml,.yml.
Root kinds (pinned by the ABI test): json document, yaml stream.
Q#JY2 — LSP configs: vscode-json-language-server + yaml-language-server
- json: binary
vscode-json-language-server --stdio(the VS Code JSON server). It is push-model: it reads config fromworkspace/didChangeConfigurationand does not issueworkspace/configurationpulls — so pmacs, which previously only answered pulls, must now also push adidChangeConfigurationafterinitialized(a general LSP-client fix insrc/lsp.rs; pull servers ignore it).json.validate.enableis set explicitly true — a missing value reads as false and silently disables validation, so an emptyjson = {}is wrong. The server does not auto-associatepackage.json/tsconfig.json(it starts with empty contributions); explicit$schemarefs or configuredjson.schemas/ ajson/schemaAssociationspush (not implemented) are required. Schema retrieval performs network access for remote$schemaURLs, left enabled (handledSchemaProtocols = {"file"}would disable it but break remote schemas without avscode/contentimpl). Provider: pin@t1ckbase/vscode-langservers-extracted@2.0.2(npm install -g @t1ckbase/vscode-langservers-extracted@2.0.2). Its published payload bundles the JSON server from VS Code 1.129.0, preserves thevscode-json-language-servercommand, and was live-smoked through initialize → config push → invalid-JSON diagnostic → shutdown. The unscoped package is stale; the current@zed-industriespayload has a broken JSON launcher, so neither is the recommended provider. - yaml:
yaml-language-server --stdio(Red Hat). Its settings handler reads the sectionsyaml,http,[yaml],editor,files(viadidChangeConfiguration/ pulls) — all ship present-not-null. It does not upload telemetry itself (it emitstelemetry/eventto the client; pmacs has no uploader), so aredhat.telemetrysetting is inert and is not shipped. SchemaStore / remote schema retrieval performs network access by default.
Both servers stay external (installed by the user), adding no
licensing payload to pmacs; if either is ever bundled, retain its MIT +
dependency notices. The exact sections are pinned in the config + a
test (not merely "some non-nil table exists"). The pinned JSON
provider was installed into an isolated temporary prefix and
live-smoked through pmacs. Red Hat yaml-language-server@1.24.0 was
also installed in an isolated prefix and live-smoked over stdio: its
initial configuration pull was exactly yaml, http, [yaml],
editor, files; opening the document caused a second scoped
[yaml] pull; invalid YAML produced a parser diagnostic; shutdown was
clean. Both providers have also passed their PATH-gated pmacs acceptance.
Config-push delivery is proven deterministically through the fake server's
config sink. Servers activate only if installed; the grammar is the
always-on value.
Q#JY3 — Filetype fallback + alias entries
Add pmacs.lsp.filetypes entries (json→json, yaml/yml→yaml) as the
stable-id fallback (grammar detection wins in practice, same role as the
cuda/lua entries). default_injection_aliases already has yml→yaml;
json/yaml are bundled names needing no alias. Special filenames
(.prettierrc, docker-compose.yml is already .yml, extensionless
CI/config yaml) are deferred to the filename map as a follow-up.
Q#JY4 — Frontmatter/fence highlighting is the headline, and it's free
No new injection wiring: registering yaml makes the existing markdown
minus_metadata→yaml injection resolve, and ```json/```yaml
fences resolve through the #122 engine. Acceptance proves both end to end
(this is the demonstrable payoff and the tie-back to injections).
Bets
- The two crates are ABI-current and drop in like the #118 grammar-gap languages — verified by a build; the ABI test is the runtime pin.
- The frontmatter/fence synergy needs zero engine changes — it falls out of #122 + the markdown injection query.
- The two configuration models are now observed: JSON consumes the
pushed full settings object; YAML 1.24.0 pulls the five documented
sections plus a document-scoped
[yaml]request. The remaining bet was that pmacs answers the real YAML server correctly end to end; the PATH-gated acceptance now proves that against version 1.24.0.
Deferred (named)
.jsonc/.json5(comments / trailing commas) — needs a lenient grammar or variant entry.- Special-filename detection for extensionless config files (
.prettierrc, CI yaml) via the filename map. - JSON schema wiring (custom
json.schemas/yaml.schemassettings) beyond the servers' built-in schema stores. - The Jupyter
.ipynbarc itself (JSON is its prerequisite, not its delivery).
Acceptance
builtin_languages_include_json_and_yaml— entries present, claim their extensions, ship non-empty highlights.json_grammar_loads_and_parses— ABI:set_language+ parse a JSON object; rootdocument, no error (the runtime ABI pin).yaml_grammar_loads_and_parses— ABI: parse a YAML mapping; rootstream, no error.json_yaml_highlights_compile— both highlights queries compile and resolve several capture classes.language_for_path_resolves_json_yaml—.json→json,.yaml/.yml→ yaml.json_yaml_align_with_lsp_configs— grammar name == thepmacs.lsp.config.<name>key (the #118 invariant).yaml_frontmatter_injects_in_markdown— a markdown doc with a---\nkey: val\n---frontmatter yields ayamlchild layer that highlights; the headline synergy with #122.json_fence_injects_in_markdown— a```jsonfence yields ajsonchild layer.m4_json_yaml_lsp_configs_pin_command_and_sections— the configs pin the binary +json.validate.enable = true+ the exact section sets (json:json,http; yaml:yaml,http,[yaml],editor,files; no inertredhat.telemetry) — pinned, not merely non-nil.m4_5_initial_config_pushed_via_did_change_configuration— the daemon PUSHESworkspace/didChangeConfigurationafterinitialized(the push-model delivery path), verified through the fake server's config sink. Without it, push-only servers' settings are inert.m4_real_json_provider_receives_config_and_reports_diagnostics— PATH-gated live smoke for the pinned provider: initialize through pmacs, receive the pushed default config, open invalid JSON, and publish a syntax diagnostic. Skips when the binary is absent.m4_real_yaml_provider_pulls_config_and_reports_diagnostics— PATH-gated live smoke for Red Hatyaml-language-server@1.24.0: auto-attach through pmacs, disable SchemaStore and Kubernetes CRD catalog network access for determinism, reach initialized, open invalid YAML, publish a diagnostic, and remain alive.
Risks / interactions
- LSP configuration (Q#JY2) — JSON push, YAML standalone pulls, and the real YAML-through-pmacs path are observed. Both live provider tests remain PATH-gated, so release verification must put the pinned binaries on PATH rather than accepting their skip paths.
- Themes / injections — untouched. This is pure grammar+detection addition; it consumes the #122 engine, doesn't change it. No protocol bump.
.ymlvs.yaml— both map toyaml; no collision with any existing entry.