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.

README.md

VS Code#

Polytmpl's VS Code extension. A thin TypeScript shell that:

  • registers the gotmpl language for *.tmpl and *.gotmpl files
  • contributes the polytmpl.* configuration schema (flavours, delimiters, function packages, server path)
  • launches the polytmpl LSP binary over stdio and forwards LSP messages

This README documents VS Code-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.

Highlighting strategy#

VS Code's path differs from Zed's because the VS Code extension API does not (yet) expose a stable way for third-party extensions to register a tree-sitter grammar with the editor's native engine. The polytmpl extension reaches the same end result via three layered mechanisms, in order of preference:

  1. LSP semantic tokens (primary). The LSP runs the template parser with the user's configured delimiters (req D) and publishes tokens through VS Code's long-stable Semantic Tokens API. This is the universal path and the one we rely on for accurate gotmpl highlighting. Status: scheduled for Layer 2 Step 3 — not yet implemented.
  2. First-party base-language grammars (planned). For the base layer of *.yaml.tmpl and friends, the VS Code shell will declare the embedded language in package.json's embeddedLanguages map and let VS Code's bundled YAML/JSON/HTML grammar do the work. Zero extra ship weight.
  3. Bundled tree-sitter WASM (optional, future). If client-side incremental parsing in VS Code becomes worthwhile, we can bundle web-tree-sitter plus a WASM build of tree-sitter-gotmpl. When VS Code's third-party tree-sitter API eventually stabilises, this becomes a transparent swap.

For the rationale behind picking semantic tokens as the primary path, see docs/architecture.md VS Code Rendering Paths.

Current state#

Phase 1 of Layer 2 has landed. As of the current commit:

  • ✅ The extension activates on gotmpl files and launches polytmpl-lsp over stdio.
  • ✅ The initialize / initialized / shutdown / exit handshake completes; the LSP receives initializationOptions from polytmpl.* settings.
  • No highlighting, diagnostics, completion, or hover yet. Those land in Layer 2 Steps 2–4.
  • ⏳ Per-flavour file recognition (.yaml.tmpl, .json.tmpl, …) is not registered yet; only .tmpl and .gotmpl map to gotmpl today. The flavour-specific registration will land alongside the embeddedLanguages work.

Registered languages#

VS Code language ids are independent from Zed's. They live in package.json's contributes.languages block.

Language ID Aliases File patterns Status
gotmpl Go Template *.tmpl, *.gotmpl Active
YAML/JSON/HTML Template variants *.yaml.tmpl, *.json.tmpl, *.html.tmpl, … Not yet registered (Layer 2 Step 6)

Until per-flavour ids are added, opening tests/fixtures/data.json.tmpl in the dev host will not activate the LSP. Use tests/fixtures/plain.tmpl (or any .gotmpl file) to verify the connection.

Configuration schema#

Contributed via package.json and surfaced as standard VS Code settings:

Setting Default Purpose
polytmpl.server.path polytmpl-lsp Path to the LSP binary. A bare name is resolved against $PATH.
polytmpl.flavours {} Map of base-language id → file glob patterns. Used by the LSP for per-flavour delimiter and function lookup.
polytmpl.delimiters.default { left: "{{", right: "}}" } Default template delimiters.
polytmpl.delimiters.overrides [] Per-pattern delimiter overrides.
polytmpl.functions.packages [] Go package paths whose exported functions populate the template-function index.

The shell forwards these to the LSP through the synchronize.configurationSection: "polytmpl" mechanism (workspace/didChangeConfiguration).

Build & test#

Prerequisites: run mise install at the repo root to provision Node and Go.

1. Install dependencies#

# From the repo root.
pnpm install

This installs the TypeScript compiler, VS Code type definitions, and vscode-languageclient into vscode/node_modules/.

2. Compile the TypeScript#

# One-shot build:
pnpm --filter polytmpl-vscode run compile

# Watch mode while iterating:
pnpm --filter polytmpl-vscode run watch

Output lands in vscode/out/.

3. Build the LSP binary#

The extension shells out to polytmpl-lsp. It needs to exist on the PATH that VS Code inherits — ~/.local/bin/ works because pnpm install doesn't touch it and most shell rc files put it on $PATH.

go build -o ~/.local/bin/polytmpl-lsp ./lsp

Alternative: build anywhere and set polytmpl.server.path in VS Code settings to the absolute path.

4. Launch the Extension Development Host#

VS Code launches an extension as a child process under a debug session called the Extension Development Host. This repo ships .vscode/launch.json and .vscode/tasks.json so the workflow just works.

Important: F5 only finds the right launch configuration when the folder you opened in VS Code is vscode/ itself — not the polytmpl repo root, not any subfolder. The .vscode/launch.json lives in vscode/.vscode/launch.json and is discovered relative to the workspace root.

  1. In VS Code, File → Open Folder…, then pick the vscode/ directory.
  2. Confirm the launch config is visible: open the Run and Debug sidebar (Cmd+Shift+D). You should see a "Run Extension" entry in the dropdown at the top.
  3. Press F5 (or click the green play button next to "Run Extension", or Run → Start Debugging).
  4. VS Code first runs the compile task (it invokes pnpm run compile and waits for it to finish), then opens a second VS Code window with your extension loaded. The new window's title bar is labelled [Extension Development Host].

If you instead see "You don't have an extension for debugging ''. Should we find a … extension in the Marketplace?", the wrong folder is open: VS Code is falling back to "debug the active editor file" because it can't find the extension-host launch config. Reopen the vscode/ folder specifically.

Alternative: launch from the command line#

Skip the F5 dance and start a dev host from the terminal, useful when you'd rather keep the repo root open:

# After running step 2 (compile) at least once:
code --extensionDevelopmentPath="$PWD/vscode" --new-window

VS Code spawns a new window with the extension loaded directly. No compile-on-launch in this mode — re-run step 2 yourself between iterations.

5. Verify#

In the dev host window:

  1. Open tests/fixtures/plain.tmpl (or create a new .tmpl file). VS Code should detect it as Go Template in the bottom-right status bar.
  2. View → Output, then pick polytmpl Language Server from the dropdown. You should see the LSP's stderr stream, starting with:
    polytmpl-lsp starting
    polytmpl-lsp initialize from Visual Studio Code <version>
    polytmpl-lsp applied initializationOptions
    polytmpl-lsp initialized
    
  3. Help → Toggle Developer Tools opens Chromium devtools; the Console tab shows any extension-host JS errors.

Common failure modes:

Symptom Likely cause
"Couldn't create connection to server" / process exits immediately polytmpl-lsp not on PATH. Either build to a directory on PATH, or set polytmpl.server.path to an absolute path.
No "polytmpl Language Server" in the Output dropdown The extension didn't activate. Make sure the file you opened maps to gotmpl (.tmpl or .gotmpl); check the bottom-right language indicator.
No diagnostics or highlighting Expected for now — Layer 2 Steps 2 and 3 implement them.

6. Logs#

Several complementary places to look when something doesn't behave:

  • Output panel (View → Output), pick from the dropdown:

    • polytmpl Language Server — the LSP binary's stderr. Where the polytmpl-lsp starting / initialize / shutdown / exit lines appear; primary view for LSP-side behaviour.
    • Extension Host — uncaught errors thrown from extension.ts and the LanguageClient itself.
    • Log (Extension Host) — verbose extension-host log including activation events.
  • Developer Tools (Help → Toggle Developer Tools): the Console tab shows JS errors from the extension host.

  • Log files on disk for after-the-fact debugging — open from the command palette via Developer: Show Logs…, or browse directly:

    Platform Log path
    macOS ~/Library/Application Support/Code/logs/<session>/
    Linux ~/.config/Code/logs/<session>/
    Windows %APPDATA%\Code\logs\<session>\
  • Trace LSP messages: set polytmpl.trace.server to "verbose" in VS Code settings. vscode-languageclient honours this convention automatically based on the LanguageClient id (polytmpl); every JSON-RPC message then appears in the polytmpl Language Server channel above.

7. Iterate#

After editing TypeScript or package.json, reload the dev host (Cmd+R in the dev host window) to pick up changes. After editing Go in ../lsp/, rebuild the binary (Step 3) — the dev host will pick up the new binary on its next activation (close any open .tmpl files, then reopen).

Files in this directory#

Path Role
package.json Extension manifest: language registration, polytmpl.* config schema, activation events, dependencies.
tsconfig.json TypeScript config; compiles src/out/.
src/extension.ts Entry point: builds ServerOptions from polytmpl.server.path, wires LanguageClient, syncs the polytmpl settings section.
language-configuration.json Editor-side language features (brackets, comment toggles) for the gotmpl language.
out/ Compiled JavaScript (gitignored).
node_modules/ npm dependencies (gitignored).

Publishing (future)#

When the extension is ready for the Marketplace, the LSP binary will need a distribution story — either bundled into the VSIX per platform, or downloaded on first activation. Today's polytmpl.server.path config covers the local-dev case but not end-user installs. See the Layer 2 plan Deferred / out of scope for v1 for context.