From cbed8f0df654dc1e2f18a88e9fb8dfba390d6478 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 3 Jun 2025 18:50:31 +0200 Subject: [PATCH] feat: show a squiggly line where the error was thrown (#613) --- README.md | 1 + package.json | 5 +++++ src/config.ts | 2 ++ src/debug.ts | 3 +++ src/diagnostic.ts | 30 ++++++++++++++++++++++++++++++ src/extension.ts | 8 ++++++++ src/runner.ts | 10 ++++++++++ test-e2e/tester.ts | 2 +- 8 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/diagnostic.ts diff --git a/README.md b/README.md index 3b636e6..e9773f4 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ These options are resolved relative to the [workspace file](https://code.visuals - `vitest.debugOutFiles`: If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with `!` the files are excluded. If not specified, the generated code is expected in the same directory as its source. Default: `["${workspaceFolder}/**/*.(m|c|)js", "!**/node_modules/**"]` - `vitest.maximumConfigs`: The maximum amount of configs that Vitest extension can load. If exceeded, the extension will show a warning suggesting to use a workspace config file. Default: `3` - `vitest.logLevel`: How verbose should the logger be in the "Output" channel. Default: `info` +- `vitest.applyDiagnostic`: Show a squiggly line where the error was thrown. This also enables the error count in the File Tab. Default: `true` - `vitest.experimentalStaticAstCollect`: uses AST parses to collect tests instead of running files and collecting them at runtime. Default: `true` ### Commands diff --git a/package.json b/package.json index 5580520..53318ae 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,11 @@ "markdownDescription": "Additional arguments to pass to the Vitest CLI. Note that some arguments will be ignored: `watch`, `reporter`, `api`, and `ui`. Example: `--mode=staging`.", "type": "string", "scope": "resource" + }, + "vitest.applyDiagnostic": { + "description": "Show a squiggly line where the error was thrown. This also enables the error count in the File Tab.", + "type": "boolean", + "default": true } } } diff --git a/src/config.ts b/src/config.ts index 8fe8660..c4d2c46 100644 --- a/src/config.ts +++ b/src/config.ts @@ -64,6 +64,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { const cliArguments = get('cliArguments') const debugOutFiles = get('debugOutFiles', []) + const applyDiagnostic = get('applyDiagnostic', true) return { env: get>('nodeEnv', null), @@ -73,6 +74,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) { terminalShellArgs, terminalShellPath, shellType, + applyDiagnostic, cliArguments, nodeExecArgs, experimentalStaticAstCollect, diff --git a/src/debug.ts b/src/debug.ts index dcba5e1..227ec1d 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -15,11 +15,13 @@ import type { WsConnectionMetadata } from './api/ws' import { waitForWsConnection } from './api/ws' import type { ExtensionWorkerProcess } from './api/types' import { findNode } from './utils' +import type { ExtensionDiagnostic } from './diagnostic' export async function debugTests( controller: vscode.TestController, tree: TestTree, pkg: VitestPackage, + diagnostic: ExtensionDiagnostic | undefined, request: vscode.TestRunRequest, token: vscode.CancellationToken, @@ -118,6 +120,7 @@ export async function debugTests( controller, tree, api, + diagnostic, ) disposables.push(api, runner) diff --git a/src/diagnostic.ts b/src/diagnostic.ts new file mode 100644 index 0000000..1386850 --- /dev/null +++ b/src/diagnostic.ts @@ -0,0 +1,30 @@ +import * as vscode from 'vscode' + +export class ExtensionDiagnostic { + private diagnostic = vscode.languages.createDiagnosticCollection('Vitest') + + addDiagnostic(testFile: vscode.Uri, errors: vscode.TestMessage[]) { + const diagnostics: vscode.Diagnostic[] = [...this.diagnostic.get(testFile) || []] + errors.forEach((error) => { + const range = error.location?.range + if (!range) { + return + } + const diagnostic = new vscode.Diagnostic( + range, + error.message.toString(), + vscode.DiagnosticSeverity.Error, + ) + diagnostics.push(diagnostic) + }) + this.diagnostic.set(testFile, diagnostics) + } + + deleteDiagnostic(testFile: vscode.Uri) { + this.diagnostic.delete(testFile) + } + + clearDiagnostic() { + this.diagnostic.clear() + } +} diff --git a/src/extension.ts b/src/extension.ts index 4579954..1d109dc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { TagsManager } from './tagsManager' import { coverageContext } from './coverage' import { ExtensionTerminalProcess } from './api/terminal' import { debugTests } from './debug' +import { ExtensionDiagnostic } from './diagnostic' export async function activate(context: vscode.ExtensionContext) { const extension = new VitestExtension() @@ -38,6 +39,7 @@ class VitestExtension { private runners: TestRunner[] = [] private disposables: vscode.Disposable[] = [] + private diagnostic: ExtensionDiagnostic | undefined /** @internal */ _debugDisposable: vscode.Disposable | undefined @@ -141,6 +143,7 @@ class VitestExtension { this.testController, this.testTree, api, + this.diagnostic, ) this.runners.push(runner) @@ -182,6 +185,7 @@ class VitestExtension { this.testController, this.testTree, api.package, + this.diagnostic, request, token, @@ -245,6 +249,10 @@ class VitestExtension { } async activate() { + this.diagnostic = getConfig().applyDiagnostic + ? new ExtensionDiagnostic() + : undefined + this.loadingTestItem.busy = true this.testController.items.replace([this.loadingTestItem]) diff --git a/src/runner.ts b/src/runner.ts index bfa5429..2304f56 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -13,6 +13,7 @@ import { showVitestError } from './utils' import { coverageContext, readCoverageReport } from './coverage' import { normalizeDriveLetter } from './worker/utils' import type { SerializedTestSpecification } from './api/rpc' +import type { ExtensionDiagnostic } from './diagnostic' export class TestRunner extends vscode.Disposable { private continuousRequests = new Set() @@ -32,6 +33,7 @@ export class TestRunner extends vscode.Disposable { private readonly controller: vscode.TestController, private readonly tree: TestTree, private readonly api: VitestFolderAPI, + private readonly diagnostic: ExtensionDiagnostic | undefined, ) { super(() => { log.verbose?.('Disposing test runner') @@ -56,6 +58,10 @@ export class TestRunner extends vscode.Disposable { log.verbose?.('Not starting the runner because tests are being collected for', ...files.map(f => this.relative(f))) } else { + files.forEach((file) => { + const uri = vscode.Uri.file(file) + this.diagnostic?.deleteDiagnostic(uri) + }) log.verbose?.('Starting a test run because', ...files.map(f => this.relative(f)), 'triggered a watch rerun event') this.startTestRun(files) } @@ -121,6 +127,7 @@ export class TestRunner extends vscode.Disposable { api.onFinished(async (files = [], unhandledError, collecting) => { const testRun = this.testRun + if (!testRun) { log.verbose?.('No test run to finish for', files.map(f => this.relative(f.filepath)).join(', ')) if (!files.length) { @@ -504,6 +511,9 @@ export class TestRunner extends vscode.Disposable { log.verbose?.(`Test failed, but no errors found for "${test.label}"`) return } + if (test.uri) { + this.diagnostic?.addDiagnostic(test.uri, errors) + } log.verbose?.(`Marking "${test.label}" as failed with ${errors.length} errors`) testRun.failed(test, errors, result.duration) break diff --git a/test-e2e/tester.ts b/test-e2e/tester.ts index 59dafbf..2fd27c1 100644 --- a/test-e2e/tester.ts +++ b/test-e2e/tester.ts @@ -41,7 +41,7 @@ class TesterTree { getFileItem(file: string) { const name = basename(file) - return new TesterTestItem(name, this.page.locator(`[aria-label*="${name} "]`), this.page) + return new TesterTestItem(name, this.page.locator(`[aria-label*="${name} ("]`), this.page) } async expand(path: string) { -- knotmirror 0.0.1