From 770e4cacf169a40386bdb5a0b31daf5393aada24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Fri, 29 May 2026 14:59:22 +0200 Subject: [PATCH] scaffold: VSCode/VSCodium extension wrapping testium lsp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 5 ++ .vscodeignore | 12 ++++ README.md | 95 ++++++++++++++++++++++++++ language-configuration.json | 38 +++++++++++ package-lock.json | 128 +++++++++++++++++++++++++++++++++++ package.json | 69 +++++++++++++++++++ src/extension.ts | 71 +++++++++++++++++++ syntaxes/tum.tmLanguage.json | 81 ++++++++++++++++++++++ tsconfig.json | 15 ++++ 9 files changed, 514 insertions(+) create mode 100644 .gitignore create mode 100644 .vscodeignore create mode 100644 language-configuration.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/extension.ts create mode 100644 syntaxes/tum.tmLanguage.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a6c8d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +out/ +*.vsix +.vscode-test/ +*.tsbuildinfo diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..f4fb3d5 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.git +tsconfig.json +node_modules/.cache/** +**/*.map +**/*.ts +!out/**/*.js +.github/** +.editorconfig diff --git a/README.md b/README.md index e69de29..d823419 100644 --- a/README.md +++ b/README.md @@ -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-.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-.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. diff --git a/language-configuration.json b/language-configuration.json new file mode 100644 index 0000000..bd2dbc8 --- /dev/null +++ b/language-configuration.json @@ -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" } + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3b6feb9 --- /dev/null +++ b/package-lock.json @@ -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==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ff58062 --- /dev/null +++ b/package.json @@ -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 (` 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" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..36ab0c7 --- /dev/null +++ b/src/extension.ts @@ -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 { + const config = vscode.workspace.getConfiguration("testium"); + const serverPath = config.get("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 { + if (client) { + await client.stop(); + client = undefined; + } +} diff --git a/syntaxes/tum.tmLanguage.json b/syntaxes/tum.tmLanguage.json new file mode 100644 index 0000000..93841ed --- /dev/null +++ b/syntaxes/tum.tmLanguage.json @@ -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_]*" } + ] + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..58d5a04 --- /dev/null +++ b/tsconfig.json @@ -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"] +}