12 KiB
JSON + YAML grammars — framing (side quest, highlight family)
Revision 4 — 2026-07-21. Status: MERGED as #123 on 2026-07-21.
(Status line corrected 2026-08-01: it read "PR #123 open and awaiting
review" for six weeks after the PR merged. The revision-4 body below is
the as-reviewed text and is left unedited.) At review the public and
checkpoint branches were 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.