VS Code#
Polytmpl's VS Code extension. A thin TypeScript shell that:
- registers the
gotmpllanguage for*.tmpland*.gotmplfiles - 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:
- 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.
- First-party base-language grammars (planned). For the base layer of
*.yaml.tmpland friends, the VS Code shell will declare the embedded language inpackage.json'sembeddedLanguagesmap and let VS Code's bundled YAML/JSON/HTML grammar do the work. Zero extra ship weight. - Bundled tree-sitter WASM (optional, future). If client-side incremental parsing in VS Code becomes worthwhile, we can bundle
web-tree-sitterplus a WASM build oftree-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
gotmplfiles and launchespolytmpl-lspover stdio. - ✅ The
initialize/initialized/shutdown/exithandshake completes; the LSP receivesinitializationOptionsfrompolytmpl.*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.tmpland.gotmplmap togotmpltoday. The flavour-specific registration will land alongside theembeddedLanguageswork.
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.
- In VS Code, File → Open Folder…, then pick the
vscode/directory. - 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. - Press
F5(or click the green play button next to "Run Extension", or Run → Start Debugging). - VS Code first runs the
compiletask (it invokespnpm run compileand 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:
- Open
tests/fixtures/plain.tmpl(or create a new.tmplfile). VS Code should detect it asGo Templatein the bottom-right status bar. - 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 - 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 thepolytmpl-lsp starting / initialize / shutdown / exitlines appear; primary view for LSP-side behaviour.Extension Host— uncaught errors thrown fromextension.tsand 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.serverto"verbose"in VS Code settings.vscode-languageclienthonours this convention automatically based on the LanguageClient id (polytmpl); every JSON-RPC message then appears in thepolytmpl Language Serverchannel 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.