pmacs/docs/json-yaml-framing.md

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 LanguageEntry in crate::syntax::BUILTIN_LANGUAGES (name, extensions, loader, highlights_query, injections_query) + a tree-sitter-foo dep. The Lua buffer.after-load path 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.filetypes is the LSP-only extension fallback (consulted only when language_for_path misses).
  • Injection synergy (#122). The bundled tree_sitter_md:: INJECTION_QUERY_BLOCK contains:
    • ((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 yaml grammar is injected into markdown frontmatter automatically, and ```json/```yaml/```yml fences resolve (yml→yaml is already in default_injection_aliases; json is the bundled name).

Confirmed crate facts (probed against the registry + a build under tree-sitter 0.26):

  1. tree-sitter-json 0.24.8pub const LANGUAGE: LanguageFn (via tree-sitter-language, the modern shared ABI) + HIGHLIGHTS_QUERY. Compiles and links under our tree-sitter 0.26. No INJECTIONS_QUERY (JSON embeds nothing).
  2. tree-sitter-yaml 0.7.2 — same shape (LANGUAGE: LanguageFn, HIGHLIGHTS_QUERY, tree-sitter-language dep). Compiles under 0.26. No INJECTIONS_QUERY.
  3. Both are the ABI-current crates — not a tree-sitter ^0.20 fork (the dockerfile trap from #118). A single build confirmed link + compile; runtime set_language is 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 .jsonc grammar 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 from workspace/didChangeConfiguration and does not issue workspace/configuration pulls — so pmacs, which previously only answered pulls, must now also push a didChangeConfiguration after initialized (a general LSP-client fix in src/lsp.rs; pull servers ignore it). json.validate.enable is set explicitly true — a missing value reads as false and silently disables validation, so an empty json = {} is wrong. The server does not auto-associate package.json/tsconfig.json (it starts with empty contributions); explicit $schema refs or configured json.schemas / a json/schemaAssociations push (not implemented) are required. Schema retrieval performs network access for remote $schema URLs, left enabled (handledSchemaProtocols = {"file"} would disable it but break remote schemas without a vscode/content impl). 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 the vscode-json-language-server command, and was live-smoked through initialize → config push → invalid-JSON diagnostic → shutdown. The unscoped package is stale; the current @zed-industries payload has a broken JSON launcher, so neither is the recommended provider.
  • yaml: yaml-language-server --stdio (Red Hat). Its settings handler reads the sections yaml, http, [yaml], editor, files (via didChangeConfiguration / pulls) — all ship present-not-null. It does not upload telemetry itself (it emits telemetry/event to the client; pmacs has no uploader), so a redhat.telemetry setting 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

  1. 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.
  2. The frontmatter/fence synergy needs zero engine changes — it falls out of #122 + the markdown injection query.
  3. 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.schemas settings) beyond the servers' built-in schema stores.
  • The Jupyter .ipynb arc itself (JSON is its prerequisite, not its delivery).

Acceptance

  1. builtin_languages_include_json_and_yaml — entries present, claim their extensions, ship non-empty highlights.
  2. json_grammar_loads_and_parses — ABI: set_language + parse a JSON object; root document, no error (the runtime ABI pin).
  3. yaml_grammar_loads_and_parses — ABI: parse a YAML mapping; root stream, no error.
  4. json_yaml_highlights_compile — both highlights queries compile and resolve several capture classes.
  5. language_for_path_resolves_json_yaml.json→json, .yaml/.yml→ yaml.
  6. json_yaml_align_with_lsp_configs — grammar name == the pmacs.lsp.config.<name> key (the #118 invariant).
  7. yaml_frontmatter_injects_in_markdown — a markdown doc with a ---\nkey: val\n--- frontmatter yields a yaml child layer that highlights; the headline synergy with #122.
  8. json_fence_injects_in_markdown — a ```json fence yields a json child layer.
  9. 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 inert redhat.telemetry) — pinned, not merely non-nil.
  10. m4_5_initial_config_pushed_via_did_change_configuration — the daemon PUSHES workspace/didChangeConfiguration after initialized (the push-model delivery path), verified through the fake server's config sink. Without it, push-only servers' settings are inert.
  11. 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.
  12. m4_real_yaml_provider_pulls_config_and_reports_diagnostics — PATH-gated live smoke for Red Hat yaml-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.
  • .yml vs .yaml — both map to yaml; no collision with any existing entry.