VS Code Integration
End goal for spice-lsp: a VS Code extension that launches the Rust language server and provides a first-class editing experience for SPICE netlists.
This chapter covers extension layout, development workflow, configuration, and publishing.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ VS Code Extension Host (Node.js) │
│ │
│ package.json ── contributes languages, config, commands │
│ extension.ts ── activates LanguageClient │
│ language-configuration.json ── comments, brackets, auto-close│
└───────────────────────────┬──────────────────────────────────┘
│ spawns process
▼
┌──────────────────────────────────────────────────────────────┐
│ spice-lsp binary (Rust, stdio JSON-RPC) │
│ initialize → didOpen/didChange → publishDiagnostics │
└──────────────────────────────────────────────────────────────┘
The extension is intentionally thin: no parsing in TypeScript. All language intelligence stays in Rust so Neovim and other clients can share the same binary.
Repository layout
editors/vscode/
├── .vscode/
│ ├── launch.json # F5 Extension Development Host
│ └── tasks.json # compile before launch
├── package.json
├── tsconfig.json
├── src/
│ └── extension.ts
├── language-configuration.json
└── README.md # Marketplace-facing extension readme
package.json
Key fields:
| Field | Purpose |
|---|---|
engines.vscode | Minimum VS Code version |
activationEvents | onLanguage:spice, onCommand:spiceLsp.restartServer, onCommand:spiceLsp.setDialect |
main | ./out/extension.js (esbuild bundle) |
contributes.languages | Register spice language id and file extensions |
contributes.configuration | spiceLsp.serverPath, spiceLsp.trace.server, spiceLsp.dialect |
contributes.commands | spiceLsp.restartServer, spiceLsp.setDialect — register first in activate; do not await LSP start before returning |
Example language contribution:
{
"languages": [{
"id": "spice",
"aliases": ["SPICE", "spice"],
"extensions": [".cir", ".sp", ".spf", ".net", ".ckt"],
"configuration": "./language-configuration.json"
}]
}
language-configuration.json
Teach VS Code comment syntax and line continuation behavior:
{
"comments": {
"lineComment": "*"
},
"brackets": [["(", ")"]],
"autoClosingPairs": [
{ "open": "(", "close": ")" }
]
}
Comment toggle uses * (language-configuration.json allows only one lineComment). ; and $ comments are highlighted by the TextMate grammar (syntaxes/spice.tmLanguage.json). Tree-sitter highlights.scm can back semantic tokens later.
extension.ts
Minimal Language Client setup. Register both palette commands before any await, then start the client in the background. If activate awaits a slow/hung client.start(), VS Code times out onCommand activation and reports command 'spiceLsp.setDialect' not found (same for Restart Server):
import * as vscode from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
} from "vscode-languageclient/node";
let client: LanguageClient | undefined;
async function startClient(serverPath: string) {
const serverOptions: ServerOptions = {
command: serverPath,
args: [],
transport: TransportKind.stdio,
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "spice" }],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.{cir,sp,spf,net,ckt}"),
},
};
client = new LanguageClient("spiceLsp", "SPICE Language Server", serverOptions, clientOptions);
await client.start();
}
export async function activate(context: vscode.ExtensionContext) {
const config = vscode.workspace.getConfiguration("spiceLsp");
const serverPath = config.get<string>("serverPath") || "spice-lsp";
context.subscriptions.push(
vscode.commands.registerCommand("spiceLsp.restartServer", async () => {
await client?.stop();
await startClient(serverPath);
}),
vscode.commands.registerCommand("spiceLsp.setDialect", async () => {
/* QuickPick → update spiceLsp.dialect → restart client */
}),
);
void startClient(serverPath).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
void vscode.window.showErrorMessage(`Failed to start SPICE LSP: ${message}`);
});
}
export async function deactivate() {
await client?.stop();
}
Development workflow
One-time setup
pixi add nodejs=22 # if not already in pixi.toml
cd editors/vscode
npm install
Add devDependencies in package.json:
{
"devDependencies": {
"@types/vscode": "^1.90.0",
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"@vscode/vsce": "^3.0.0"
},
"dependencies": {
"vscode-languageclient": "^9.0.0"
}
}
Daily loop
# terminal 1 — Rust server
pixi run build
# terminal 2 — extension
cd editors/vscode
npm run watch # esbuild --watch
# VS Code: F5 to launch Extension Development Host
Set absolute path to debug binary in Development Host settings:
{
"spiceLsp.serverPath": "/path/to/spice-lsp/target/debug/spice-lsp"
}
Or use launch.json env / preLaunchTask to build Rust first.
Verify integration
Follow Demo and testing VS Code section.
Bundling the server binary
The Marketplace extension ships a platform-specific binary inside the .vsix under bin/<platform>-<arch>/:
| Platform id | OS / arch | Notes |
|---|---|---|
linux-x64 | Linux x86_64 | Linked for glibc 2.31+ (Ubuntu 20.04 / Debian 11+) via Zig |
linux-arm64 | Linux ARM64 | Same glibc 2.31 floor |
darwin-x64 | macOS Intel | |
darwin-arm64 | macOS Apple Silicon | |
win32-x64 | Windows x64 |
There is no win32-arm64 bundle today. Unsupported platforms must set spiceLsp.serverPath or put spice-lsp on PATH.
Linux CI builds use scripts/zig-cc-*.sh so binaries from ubuntu-latest (glibc 2.39) still load on hosts with glibc 2.31. A plain cargo build on a newer distro may require a newer glibc — use the Zig wrappers for release artifacts.
At activation, the extension resolves the binary in this order:
spiceLsp.serverPathsetting (if set)- Bundled binary at
bin/<platform>-<arch>/spice-lspinside the extension - Local dev paths under
target/debugortarget/release(F5 from this repo) spice-lsponPATH
Package locally
Build a release binary for the current platform and create a .vsix:
pixi run build
pixi run ext-package
# output: editors/vscode/spice-lsp-0.2.0.vsix
Install side-loaded:
code --install-extension editors/vscode/spice-lsp-0.2.0.vsix
CI release workflow
The Release VS Code extension workflow runs on every push to main (and on manual workflow_dispatch / vscode-v* tags):
- Bumps the patch version in
editors/vscode/package.jsonand commits it tomain - Cross-compiles
spice-lspfor all supported platform ids - Assembles a single
.vsixcontaining every platform binary - Uploads the VSIX as a GitHub Actions artifact
- Creates a GitHub Release tagged
vscode-v<version> - Publishes to the VS Code Marketplace from the same package job (
VSCE_PATrequired)
| Strategy | Pros | Cons |
|---|---|---|
| User PATH | Simplest for local dev | Poor UX for end users |
Setting serverPath | Flexible | Manual configuration |
Bundle in .vsix | Works offline; Marketplace default | Larger artifact; CI builds all platforms |
| Download from GitHub Releases on activate | Small VSIX | Requires network on first run |
The Marketplace release uses bundle in .vsix.
TextMate grammar
Syntax highlighting ships as:
editors/vscode/syntaxes/spice.tmLanguage.json
Registered in package.json:
"grammars": [{
"language": "spice",
"scopeName": "source.spice",
"path": "./syntaxes/spice.tmLanguage.json"
}]
The grammar colors * / ; / $ comments, . directives, instance lines, and numeric literals. Tree-sitter-based highlighting via nvim-treesitter is separate; VS Code can adopt semantic tokens when the LSP advertises semanticTokensProvider (future).
Marketplace listing icon: editors/vscode/media/icon.png (package.json "icon" field).
Publishing
One-time Marketplace setup
Do this once before the first CI publish succeeds:
- Sign in to the Visual Studio Marketplace publisher management page with a Microsoft account.
- Create a publisher whose Publisher ID matches
editors/vscode/package.json(AmirhosseinDavoodyin this repo). The ID is permanent and must match exactly. - Create a Personal Access Token in Azure DevOps (not portal.azure.com):
- Open https://dev.azure.com and sign in with the same Microsoft account used for the Marketplace publisher.
- If prompted, create a free Azure DevOps organization (any name is fine; it is only a container for the PAT).
- Click your profile avatar (top right) → Personal access tokens
Direct link: https://dev.azure.com/_usersSettings/tokens - + New Token:
- Name: e.g.
vscode-marketplace - Organization: All accessible organizations
- Expiration: choose a duration you are willing to rotate
- Scopes: Custom defined → enable Marketplace → Manage
- Name: e.g.
- Create and copy the token immediately (it is shown once)
- In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret
- Name:
VSCE_PAT - Value: the Azure DevOps PAT from step 3
- Name:
- Confirm Marketplace listing metadata is ready in
editors/vscode/:README.md(Marketplace landing page — include a Quick start so users know what to do after install)LICENSE(MITmatchespackage.json)publisher,displayName,description,engines.vscode
Optional local dry-run before relying on CI:
pixi run build
pixi run ext-package
# output: editors/vscode/spice-lsp-<version>.vsix
# packaging fails if the esbuild bundle is missing LanguageClient or terminateProcess.sh
The extension is esbuild-bundled (npm run compile → out/extension.js with vscode-languageclient inlined). Package and publish with vsce … --no-dependencies so the VSIX does not ship node_modules.
Release from CI (automatic)
Every push to main runs Release VS Code extension:
- Patch-bumps
editors/vscode/package.json(e.g.0.2.0→0.2.1) - Commits
chore(vscode): bump extension to <version>(viaGITHUB_TOKEN, which does not re-trigger the workflow) - Builds platform binaries, packages the VSIX, creates GitHub Release
vscode-v<version>, and runsvsce publish
Manual options:
- Actions tab → Release VS Code extension → Run workflow — optional bump + publish flags
- Push a tag
vscode-v*from your machine to publish the version already inpackage.json(no auto-bump)
The workflow always uploads the .vsix as an Actions artifact.
Release manually
pixi run build
pixi run ext-package
cd editors/vscode
# bump version first if this version was already published
npm version patch --no-git-tag-version
npx vsce publish --no-dependencies --packagePath "$(ls -t *.vsix | head -1)" # requires VSCE_PAT
Pre-publish checklist:
- Publisher ID matches
package.jsonpublisherfield -
VSCE_PATrepository secret configured -
README.mddescribes bundled-binary behavior for end users -
LICENSEaligned with repo (MITinpackage.json) -
engines.vscodeset to tested minimum version
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Server not starting | Binary not on PATH / wrong serverPath / unsupported platform | Set spiceLsp.serverPath, then SPICE LSP: Restart Server; check Output → SPICE Language Server |
version 'GLIBC_2.3x' not found | Host glibc older than the binary | Update to a Marketplace build linked for glibc 2.31+, or build locally and set spiceLsp.serverPath |
spiceLsp.restartServer / spiceLsp.setDialect not found | Extension never activated, activate hung on LSP start, or Marketplace build predates the command (setDialect needs ≥ 0.2.10) | Update the extension; reload the window; open a .cir/.sp file or run the command (auto-activates). Prefer builds that register commands before awaiting client.start() |
| No SPICE Language Server in Output | Extension did not activate | Open a SPICE file or run SPICE LSP: Restart Server / Set Dialect…; check Developer: Show Running Extensions for activation errors |
| Extension activates with module errors | Unbundled VSIX missing node_modules | Use an esbuild-bundled release (vsce package --no-dependencies after npm run compile) |
| No diagnostics | Wrong language id | Ensure file extension maps to spice |
| Stale diagnostics | Server crash | Check Output → SPICE Language Server; restart server |
| Wrong binary arch | Download mismatch / unsupported platform | Pick correct release asset or build from source |
Beyond VS Code
The same spice-lsp binary enables other editors:
| Editor | Integration |
|---|---|
| Neovim | vim.lsp.enable or lspconfig custom server |
| Helix | language-server.spice-lsp in user config |
| Zed | Community extension calling the binary |
VS Code is the reference client; keep editor-specific code out of Rust.
Related
- MVP guide — M6 extension milestone
- Demo and testing
- LSP features
- Architecture