A polyglot Go-template language toolkit (syntax highlighting, diagnostics, and completion) for Go templates embedded in any base language.
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 2 folders 4 files
README.md

Zed#

Polytmpl's Zed extension. A thin Rust shell compiled to WebAssembly that:

  • registers per-flavour languages for *.tmpl, *.yaml.tmpl, *.json.tmpl, *.html.tmpl, …
  • bundles the tree-sitter-gotmpl grammar
  • (once Layer 2 lands) launches the polytmpl LSP binary

This README documents Zed-specific details that do not belong in the top-level README. For project-wide architecture, see docs/architecture.md and docs/adrs/01-architecture.md.

The inverted injection pattern#

The per-flavour languages here do not host a base language (YAML, JSON, HTML, …) and inject gotmpl inside it. They do the opposite: gotmpl is the host grammar, and the base language is injected by name into gotmpl's text regions (everything outside {{ … }}).

The reason is an editor-extension constraint. A Zed extension's language config.toml grammar = "…" field resolves only against grammars the extension declares in its own extension.toml. It cannot reference Zed's built-in grammars (yaml, json, …). If we tried the intuitive direction (grammar = "yaml" in a hypothetical YAML Template), Zed fails at load time with no such grammar yaml. The injection lookup ((#set! injection.language "yaml")) does resolve against Zed's full language registry, including built-ins — so the inverted direction works:

; yaml-template/injections.scm
((text) @injection.content
 (#set! injection.language "yaml")
 (#set! injection.combined))

The combined flag merges every text region into one logical base-language parse, so YAML/JSON/HTML structure is preserved across template actions instead of being parsed fragment-by-fragment.

This is the same pattern used by cabrinha/helm.zed and the Helm dialect of ngalaiko/tree-sitter-go-template. The ADR implementation notes capture the postmortem of why the obvious direction does not work.

The duplication trade-off#

Zed loads tree-sitter queries per language, not per grammar. Every per-flavour language needs its own highlights.scm even though they all share the gotmpl grammar. We accept the duplication and use the workspace generator script (see Adding a new flavour below) to keep the copies in sync. Each generated file carries a header comment flagging the sync requirement.

Current languages#

All flavours share grammar = "gotmpl". Edit the matching config.toml to change file patterns.

Directory Display name File patterns Injected base Availability
languages/gotmpl/ Go Template *.tmpl, *.gotmpl (none — pure gotmpl) always
languages/yaml-template/ YAML Template *.yaml.tmpl, *.yml.tmpl yaml Zed built-in
languages/json-template/ JSON Template *.json.tmpl json Zed built-in
languages/toml-template/ TOML Template *.toml.tmpl toml Zed built-in
languages/sql-template/ SQL Template *.sql.tmpl sql community extension (install separately)
languages/html-template/ HTML Template *.html.tmpl, *.gohtml html Automatically installed (see docs)

If you open an *.html.tmpl file without the Zed HTML extension installed, the non-template parts will appear unhighlighted but the gotmpl actions still colour correctly.

Adding a new flavour#

Use the workspace generator script:

pnpm --filter polytmpl-scripts run generate-zed-language <lang-id> [display-name]

Examples:

pnpm --filter polytmpl-scripts run generate-zed-language toml
pnpm --filter polytmpl-scripts run generate-zed-language dockerfile "Dockerfile Template"

The generator writes config.toml, injections.scm, and highlights.scm into a new languages/<lang-id>-template/ directory using the inverted-injection pattern, and prints a post-generation checklist. See scripts/README.md for full options.

Build & test#

Prerequisites: run mise install at the repo root to provision Rust and the tree-sitter CLI.

One-time setup for local development#

This setup is required only for testing the extension as a dev extension on your own machine. End users installing the published extension from the Zed Extension Registry need none of this — Zed downloads a prebuilt WASM artifact and runs it directly. No Rust, no Cargo target, no compilation on the user's side.

Zed runs as a macOS GUI app and queries mise shims at the global default, not the per-directory default declared in mise.toml. Set the global Rust version and add the WebAssembly target once:

mise use -g rust@1.95.0
rustup target add wasm32-wasip2

Build the WASM#

# From the repo root:
cargo build --manifest-path zed/Cargo.toml --target wasm32-wasip2 --release

# Or from this directory:
cargo build --target wasm32-wasip2 --release

Zed rebuilds the extension automatically every time you reinstall it as a dev extension, so this local build is only useful for catching compile errors early.

Configure local offline loading#

extension.toml's [grammars.gotmpl] block points at your local repo path and at a commit whose tree contains the tree-sitter-gotmpl/ subtree you want to test:

[grammars.gotmpl]
repository = "/absolute/path/to/polytmpl"
commit     = "<a SHA in this repo whose tree-sitter-gotmpl/ matches what you want>"
path       = "tree-sitter-gotmpl"

Bump the commit SHA whenever tree-sitter-gotmpl/ changes. Editor-side files (languages/, src/) are read straight from this directory and do not need a SHA bump.

Install the dev extension#

In Zed:

  1. Cmd+Shift+Pzed: install dev extension
  2. Select this zed/ directory.

Zed compiles the Rust crate to WASM, checks out the grammar at the configured commit, and registers everything under languages/.

Logs#

Almost every dev-extension issue surfaces in Zed's main log: extension build output, grammar query errors, LSP launches, and the LSP binary's own stderr all stream there.

Platform Log path
macOS ~/Library/Logs/Zed/Zed.log
Linux ~/.local/share/zed/logs/Zed.log
Windows %LOCALAPPDATA%\Zed\logs\Zed.log

Open from the command palette: zed: open log. Or follow polytmpl-related lines as you test:

tail -f ~/Library/Logs/Zed/Zed.log | grep -iE 'polytmpl|gotmpl|extension|language server|grammar'

Useful lines to scan for:

Pattern Meaning
INFO [lsp] starting language server process. binary path: "…polytmpl-lsp" Zed found and launched the LSP binary. Phase 1 success signal.
polytmpl-lsp starting / initialize from Zed … The LSP's own stderr — confirms the handshake started.
ERROR [extension::extension_builder] … Rust crate or grammar compilation failed during dev install.
ERROR [language::language_registry] failed to load language … A languages/<name>/ config or query file is malformed; the message usually names the offending grammar or capture.
ERROR [language_core::grammar] missing required capture … A .scm file is missing a @injection.content or similar required capture.

Verify#

Open one of the fixtures in ../tests/fixtures/:

  • plain.tmpl — pure Go template, no base layer.
  • config.yaml.tmpl — YAML base + gotmpl actions.
  • data.json.tmpl — JSON base + gotmpl actions.
  • simple.html.tmpl — HTML base + gotmpl actions (requires Zed's community HTML extension).

Both layers should be highlighted. Common failure modes:

Symptom Likely cause
No highlighting anywhere Extension didn't load. Check ~/Library/Logs/Zed/Zed.log.
Base language highlighted, gotmpl actions plain The per-flavour language's highlights.scm is missing or out of sync with languages/gotmpl/highlights.scm. Regenerate with the script.
gotmpl actions highlighted, base language plain The injected language id is not registered in Zed (built-in or community extension). Install the relevant extension or adjust injections.scm.

Files in this directory#

Path Role
extension.toml Extension manifest: metadata, grammar declaration, (future) language-server registration.
Cargo.toml, Cargo.lock, src/ Rust crate compiled to wasm32-wasip2. Currently a minimal shell that will be wired to launch the polytmpl LSP.
languages/<name>/ Per-language configuration: config.toml plus highlights.scm and injections.scm.