scaffold: VSCode/VSCodium extension wrapping testium lsp

A thin LSP client that spawns `testium lsp` and forwards messages to
VSCode. All language intelligence lives in the testium repo, so this
extension only needs republishing for editor-side UX changes.

  - package.json: declares the .tum language, registers the LSP client,
    exposes testium.serverPath and testium.trace.server settings.
  - src/extension.ts: vscode-languageclient setup over stdio, surfaces a
    user-visible error if `testium lsp` can't start.
  - syntaxes/tum.tmLanguage.json: TextMate grammar embedding source.yaml
    plus tum-specific tokens for $(name), <| python |>, {% jinja %},
    {{ jinja }}.
  - language-configuration.json: comment char, bracket pairs, auto-close
    for $(/), <|/|>, {%/%}, {{/}}.
  - README.md: install (incl. testium[lsp] extra), settings, packaging
    via vsce, publish to both VS Marketplace and Open VSX (the latter
    is what VSCodium / Cursor / Gitpod use — same .vsix artifact).

Works identically on VSCode and VSCodium — no Microsoft-proprietary
API surface used.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 14:59:22 +02:00
parent c6aa6bb07a
commit 770e4cacf1
9 changed files with 514 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
out/
*.vsix
.vscode-test/
*.tsbuildinfo

12
.vscodeignore Normal file
View File

@@ -0,0 +1,12 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.git
tsconfig.json
node_modules/.cache/**
**/*.map
**/*.ts
!out/**/*.js
.github/**
.editorconfig

View File

@@ -0,0 +1,95 @@
# Testium Assist
Language support for [testium](https://git.beafrancois.fr/v-and-v/testium) `.tum`
test scripts: syntax highlighting, completion, hover, and (later)
diagnostics.
Works the same on **VSCode** and **VSCodium** — the extension uses
only the standard VSCode API and `vscode-languageclient`, no
Microsoft-proprietary surface.
## How it works
The extension is a thin LSP client that spawns the language server
shipped with testium itself (`testium lsp`). All the language
intelligence (item types, parameter completion, schema awareness)
lives in the testium repo — every new item type or parameter you add
upstream becomes available in the editor on the next testium upgrade,
without re-publishing this extension.
Two static parts live here, because they need to run *before* the LSP
has a chance to start:
- **Syntax highlighting** — a TextMate grammar under `syntaxes/`
delegates to the base YAML grammar and adds tokens for testium's
`$(name)`, `<| python expr |>`, and Jinja `{% … %}` / `{{ … }}`
constructs.
- **Editor configuration** — comment character, bracket pairs,
auto-close pairs for `$( )`, `<| |>`, `{% %}`, `{{ }}`.
## Requirements
- VSCode ≥ 1.80 (or any recent VSCodium build).
- A working `testium` installation with the `lsp` extra:
```sh
pip install 'testium[lsp]'
```
Or, when developing testium from source:
```sh
pip install -e /path/to/testium/src[lsp]
```
## Settings
| Setting | Default | Description |
|---|---|---|
| `testium.serverPath` | `testium` | Path to the `testium` executable. Set this to your venv's `bin/testium` if `testium` isn't on `$PATH`. |
| `testium.trace.server` | `off` | LSP trace level surfaced in the **Testium LSP** output channel. |
## Development
```sh
npm install
npm run compile # one-shot build
npm run watch # rebuild on save
```
Press **F5** in VSCode to launch a development host with the extension
loaded. Open any `.tum` file; the **Testium LSP** output channel shows
the language-server traffic when `testium.trace.server` is set to
`messages` or `verbose`.
## Packaging
```sh
npx vsce package # produces testium-assist-<v>.vsix
```
To install locally without publishing:
- **Code/Codium UI** — `Extensions: Install from VSIX…` from the
command palette, pick the `.vsix`.
- **CLI** — `code --install-extension testium-assist-<v>.vsix` (or
`codium --install-extension …`).
## Publishing
The extension targets two registries:
- **VS Marketplace** (used by VSCode) — `npx vsce publish`. Requires a
Microsoft publisher account.
- **Open VSX** (used by VSCodium, Cursor, Gitpod, Theia, Eclipse Che) —
`npx ovsx publish`. Free and open.
Both consume the same `.vsix` artifact. A GitHub Actions workflow that
runs both on tag push is the typical setup.
## Status
This is the MVP: completion of test item type names at the start of a
new step. Hover, parameter completion, outline and diagnostics will
follow as the testium LSP server gains those features — no extension
re-publish needed.
## License
EUPL-1.2 — same as testium.

View File

@@ -0,0 +1,38 @@
{
"comments": {
"lineComment": "#"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string"] },
{ "open": "$(", "close": ")" },
{ "open": "<|", "close": "|>" },
{ "open": "{%", "close": "%}" },
{ "open": "{{", "close": "}}" }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"indentationRules": {
"increaseIndentPattern": "^.*:\\s*$",
"decreaseIndentPattern": "^\\s+[}\\]].*$"
},
"onEnterRules": [
{
"beforeText": "^\\s*-\\s+[^:]*:\\s*$",
"action": { "indent": "indent" }
}
]
}

128
package-lock.json generated Normal file
View File

@@ -0,0 +1,128 @@
{
"name": "testium-assist",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "testium-assist",
"version": "0.1.0",
"license": "EUPL-1.2",
"dependencies": {
"vscode-languageclient": "^9.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/vscode": "^1.80.0",
"typescript": "^5.4.0"
},
"engines": {
"vscode": "^1.80.0"
}
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/vscode": {
"version": "1.120.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz",
"integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
"dependencies": {
"minimatch": "^5.1.0",
"semver": "^7.3.7",
"vscode-languageserver-protocol": "3.17.5"
},
"engines": {
"vscode": "^1.82.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
}
}
}

69
package.json Normal file
View File

@@ -0,0 +1,69 @@
{
"name": "testium-assist",
"displayName": "Testium Assist",
"description": "Language support for testium .tum test scripts. Completion, hover and diagnostics powered by the testium LSP server (`testium lsp`).",
"version": "0.1.0",
"publisher": "testium",
"license": "EUPL-1.2",
"engines": {
"vscode": "^1.80.0"
},
"categories": [
"Programming Languages"
],
"main": "./out/extension.js",
"activationEvents": [
"onLanguage:tum"
],
"contributes": {
"languages": [
{
"id": "tum",
"aliases": [
"Testium TUM",
"tum"
],
"extensions": [
".tum"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "tum",
"scopeName": "source.tum",
"path": "./syntaxes/tum.tmLanguage.json"
}
],
"configuration": {
"title": "Testium Assist",
"properties": {
"testium.serverPath": {
"type": "string",
"default": "testium",
"description": "Path to the testium executable used to start the language server (`<path> lsp`). Defaults to looking up `testium` on $PATH."
},
"testium.trace.server": {
"type": "string",
"enum": ["off", "messages", "verbose"],
"default": "off",
"description": "Trace the LSP communication between VSCode and the testium server in the Output panel."
}
}
}
},
"scripts": {
"compile": "tsc -p .",
"watch": "tsc -p . --watch",
"vscode:prepublish": "npm run compile"
},
"dependencies": {
"vscode-languageclient": "^9.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/vscode": "^1.80.0",
"typescript": "^5.4.0"
}
}

71
src/extension.ts Normal file
View File

@@ -0,0 +1,71 @@
/*
* Testium Assist — VSCode language client for .tum files.
*
* This extension is a thin LSP client: it spawns `testium lsp` and forwards
* LSP messages between VSCode and the server. All the language intelligence
* (completion, hover, diagnostics, …) lives in the testium repository itself
* (`src/testium/lsp/`), which keeps this extension shippable independently
* of testium's feature evolution.
*
* The user can override the testium executable through the
* `testium.serverPath` setting (useful when working with a virtualenv or a
* source checkout).
*/
import * as vscode from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
} from "vscode-languageclient/node";
let client: LanguageClient | undefined;
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const config = vscode.workspace.getConfiguration("testium");
const serverPath = config.get<string>("serverPath", "testium");
const serverOptions: ServerOptions = {
run: { command: serverPath, args: ["lsp"], transport: TransportKind.stdio },
debug: {
command: serverPath,
args: ["lsp"],
transport: TransportKind.stdio,
},
};
const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: "file", language: "tum" },
],
// Echo the user-visible setting into the client so the LSP trace shows up
// in the Output panel under "Testium LSP".
traceOutputChannel: vscode.window.createOutputChannel("Testium LSP"),
};
client = new LanguageClient(
"testiumLsp",
"Testium LSP",
serverOptions,
clientOptions,
);
// start() throws if the executable can't be found; we surface that as a
// user-visible message instead of a silent failure in the dev console.
try {
await client.start();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(
`Testium Assist: failed to start the language server (${serverPath} lsp). ${msg}`,
);
client = undefined;
}
}
export async function deactivate(): Promise<void> {
if (client) {
await client.stop();
client = undefined;
}
}

View File

@@ -0,0 +1,81 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Testium TUM",
"scopeName": "source.tum",
"patterns": [
{ "include": "#tum-tokens" },
{ "include": "source.yaml" }
],
"repository": {
"tum-tokens": {
"patterns": [
{ "include": "#globaldict-reference" },
{ "include": "#eval-expression" },
{ "include": "#jinja-statement" },
{ "include": "#jinja-expression" }
]
},
"globaldict-reference": {
"name": "variable.other.tum.globaldict",
"begin": "\\$\\(",
"end": "\\)",
"beginCaptures": {
"0": { "name": "punctuation.definition.variable.begin.tum" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.variable.end.tum" }
},
"patterns": [
{ "name": "entity.name.variable.tum", "match": "[A-Za-z_][A-Za-z0-9_]*" }
]
},
"eval-expression": {
"name": "meta.embedded.expression.tum",
"begin": "<\\|",
"end": "\\|>",
"beginCaptures": {
"0": { "name": "punctuation.definition.expression.begin.tum" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.expression.end.tum" }
},
"patterns": [
{ "include": "source.python" }
]
},
"jinja-statement": {
"name": "meta.embedded.block.jinja.tum",
"begin": "\\{%-?",
"end": "-?%\\}",
"beginCaptures": {
"0": { "name": "punctuation.definition.template-statement.begin.tum" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.template-statement.end.tum" }
},
"patterns": [
{ "name": "keyword.control.jinja.tum",
"match": "\\b(for|endfor|if|elif|else|endif|set|include|extends|block|endblock|macro|endmacro|in|and|or|not|is|with|without|context)\\b" },
{ "name": "string.quoted.double.jinja.tum", "match": "\"(?:[^\"\\\\]|\\\\.)*\"" },
{ "name": "string.quoted.single.jinja.tum", "match": "'(?:[^'\\\\]|\\\\.)*'" },
{ "name": "constant.numeric.jinja.tum", "match": "\\b[0-9]+(?:\\.[0-9]+)?\\b" }
]
},
"jinja-expression": {
"name": "meta.embedded.expression.jinja.tum",
"begin": "\\{\\{-?",
"end": "-?\\}\\}",
"beginCaptures": {
"0": { "name": "punctuation.definition.template-expression.begin.tum" }
},
"endCaptures": {
"0": { "name": "punctuation.definition.template-expression.end.tum" }
},
"patterns": [
{ "name": "string.quoted.double.jinja.tum", "match": "\"(?:[^\"\\\\]|\\\\.)*\"" },
{ "name": "string.quoted.single.jinja.tum", "match": "'(?:[^'\\\\]|\\\\.)*'" },
{ "name": "variable.other.jinja.tum", "match": "[A-Za-z_][A-Za-z0-9_]*" }
]
}
}
}

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"lib": ["ES2022"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", ".vscode-test"]
}