From a7aee3411c9efcf91b75185d4750acd650ad017f Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 14:57:55 -0500 Subject: [PATCH 1/7] Add zig code runner --- src/extension.ts | 9 +++++ src/zigMainCodeLens.ts | 88 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/zigMainCodeLens.ts diff --git a/src/extension.ts b/src/extension.ts index a503e78..bacf875 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import vscode from "vscode"; import { activate as activateZls, deactivate as deactivateZls } from "./zls"; import ZigCompilerProvider from "./zigCompilerProvider"; +import { ZigMainCodeLensProvider } from "./zigMainCodeLens"; import { registerDocumentFormatting } from "./zigFormat"; import { setupZig } from "./zigSetup"; @@ -12,6 +13,14 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerDocumentFormatting()); + ZigMainCodeLensProvider.registerCommands(context); + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { language: 'zig', scheme: 'file' }, + new ZigMainCodeLensProvider() + ) + ); + void activateZls(context); }); } diff --git a/src/zigMainCodeLens.ts b/src/zigMainCodeLens.ts new file mode 100644 index 0000000..0a1beaf --- /dev/null +++ b/src/zigMainCodeLens.ts @@ -0,0 +1,88 @@ +import childProcess from "child_process"; +import fs from "fs"; +import { getZigPath } from "./zigUtil"; +import path from "path"; +import util from "util"; +import vscode from "vscode"; + +const execFile = util.promisify(childProcess.execFile); + +export class ZigMainCodeLensProvider implements vscode.CodeLensProvider { + public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { + const codeLenses: vscode.CodeLens[] = []; + const text = document.getText(); + + const mainRegex = /pub\s+fn\s+main\s*\(/g; + let match; + while ((match = mainRegex.exec(text))) { + const position = document.positionAt(match.index); + const range = new vscode.Range(position, position); + codeLenses.push( + new vscode.CodeLens(range, { title: "Run", command: "zig.run", arguments: [document.uri.fsPath] }), + ); + codeLenses.push( + new vscode.CodeLens(range, { title: "Debug", command: "zig.debug", arguments: [document.uri.fsPath] }), + ); + } + return codeLenses; + } + + public static registerCommands(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand("zig.run", zigRun), + vscode.commands.registerCommand("zig.debug", zigDebug), + ); + } +} + +function zigRun(filePath: string) { + const terminal = vscode.window.createTerminal("Run Zig Program"); + terminal.show(); + + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (workspaceFolder && hasBuildFile(workspaceFolder.uri.fsPath)) { + terminal.sendText(`${getZigPath()} build run`); + return; + } + terminal.sendText(`${getZigPath()} run "${filePath}"`); +} + +function hasBuildFile(workspaceFspath: string): boolean { + const buildZigPath = path.join(workspaceFspath, "build.zig"); + return fs.existsSync(buildZigPath); +} + +async function zigDebug(filePath: string) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + let binaryPath = ""; + + if (workspaceFolder && hasBuildFile(workspaceFolder.uri.fsPath)) { + binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); + } else { + binaryPath = filePath; + } + + const debugConfig: vscode.DebugConfiguration = { + type: "lldb", + name: `Debug Zig`, + request: "launch", + program: binaryPath, + cwd: path.dirname(workspaceFolder?.uri.fsPath ?? filePath), + stopAtEntry: false, + }; + await vscode.debug.startDebugging(undefined, debugConfig); +} + +async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { + // Workaround because zig build doesn't support specifying the output binary name + // `zig run` does support -femit-bin, but what if build file has custom build logic? + const outputDir = path.join(workspacePath, "zig-out", "tmp-debug-build"); + const zigPath = getZigPath(); + await execFile(zigPath, ["build", "--prefix", outputDir], { cwd: workspacePath }); + const dirFiles = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(outputDir, "bin"))); + const files = dirFiles.find(([, type]) => type === vscode.FileType.File); + if (!files) { + throw new Error("Unable to build debug binary"); + } + return path.join(outputDir, "bin", files[0]); +} From 419a92ec254f4a69675d41a04d83b41e98a64368 Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 15:08:51 -0500 Subject: [PATCH 2/7] Add a test run support for zig files --- src/extension.ts | 4 + src/zigTestRunnerProvider.ts | 189 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/zigTestRunnerProvider.ts diff --git a/src/extension.ts b/src/extension.ts index bacf875..6e20c06 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import vscode from "vscode"; import { activate as activateZls, deactivate as deactivateZls } from "./zls"; import ZigCompilerProvider from "./zigCompilerProvider"; import { ZigMainCodeLensProvider } from "./zigMainCodeLens"; +import ZigTestRunnerProvider from "./zigTestRunnerProvider"; import { registerDocumentFormatting } from "./zigFormat"; import { setupZig } from "./zigSetup"; @@ -13,6 +14,9 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(registerDocumentFormatting()); + const testRunner = new ZigTestRunnerProvider(); + testRunner.activate(context.subscriptions); + ZigMainCodeLensProvider.registerCommands(context); context.subscriptions.push( vscode.languages.registerCodeLensProvider( diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts new file mode 100644 index 0000000..aa7220f --- /dev/null +++ b/src/zigTestRunnerProvider.ts @@ -0,0 +1,189 @@ +import vscode from "vscode"; + +import childProcess from "child_process"; +import path from "path"; +import util from "util"; + +import { DebouncedFunc, throttle } from "lodash-es"; + +import { getZigPath } from "./zigUtil"; + +const execFile = util.promisify(childProcess.execFile); + +export default class ZigTestRunnerProvider { + private testController: vscode.TestController; + private updateTestItems: DebouncedFunc<(document: vscode.TextDocument) => void>; + + constructor() { + this.updateTestItems = throttle( + (document: vscode.TextDocument) => { + this._updateTestItems(document); + }, + 500, + { trailing: true }, + ); + + this.testController = vscode.tests.createTestController("zigTestController", "Zig Tests"); + this.testController.createRunProfile("Run", vscode.TestRunProfileKind.Run, this.runTests.bind(this), true); + this.testController.createRunProfile( + "Debug", + vscode.TestRunProfileKind.Debug, + this.debugTests.bind(this), + true, + ); + } + + public activate(subscriptions: vscode.Disposable[]) { + subscriptions.push( + vscode.workspace.onDidOpenTextDocument((document) => { + this.updateTestItems(document); + }), + vscode.workspace.onDidChangeTextDocument((change) => { + this.updateTestItems(change.document); + }), + vscode.workspace.onDidCloseTextDocument((document) => { + this.testController.items.forEach((item) => { + if (item.uri === document.uri) { + this.testController.items.delete(item.id); + } + }); + }), + ); + } + + private _updateTestItems(textDocument: vscode.TextDocument) { + if (textDocument.languageId !== "zig") return; + + const regex = /\btest\s+"([^"]+)"\s*\{/g; + const matches = Array.from(textDocument.getText().matchAll(regex)); + const newTests: vscode.TestItem[] = []; + + for (const match of matches) { + const testDesc = match[1]; + const position = textDocument.positionAt(match.index); + const range = new vscode.Range(position, position.translate(0, match[0].length)); + const fileName = path.basename(textDocument.uri.fsPath); + + const testItem = this.testController.createTestItem( + `${fileName}.test.${testDesc}`, // Test id needs to be unique, so adding file name here + `${fileName} - ${testDesc}`, + textDocument.uri, + ); + testItem.range = range; + this.testController.items.add(testItem); + newTests.push(testItem); + } + } + + private async runTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) { + const run = this.testController.createTestRun(request); + // request.include will have individual test when we run test from gutter icon + // if test is run from test explorer, request.include will be undefined and we run all tests that are active + for (const item of request.include ?? this.testController.items) { + if (token.isCancellationRequested) break; + const testItem = Array.isArray(item) ? item[1] : item; + run.started(testItem); + const start = new Date(); + try { + run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); + const { output, success } = await this.runTest(run, testItem); + run.appendOutput(output.replaceAll("\n", "\r\n")); + run.appendOutput("\r\n"); + const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); + if (!success) { + run.failed(testItem, new vscode.TestMessage(new vscode.MarkdownString(output)), elapsed); + } else { + run.passed(testItem, elapsed); + } + } catch (e) { + const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); + run.failed(testItem, new vscode.TestMessage((e as Error).message), elapsed); + } + } + run.end(); + } + + private async runTest(run: vscode.TestRun, test: vscode.TestItem): Promise<{ output: string; success: boolean }> { + const zigPath = getZigPath(); + if (test.uri === undefined) { + return { output: "Unable to determine file location", success: false }; + } + // Unwrapping test desc from test id + const parts = test.id.split("."); + const lastPart = parts[parts.length - 1]; + const args = ["test", "--test-filter", lastPart, test.uri.fsPath]; + run.appendOutput(`Running command: ${zigPath} ${args.join(" ")}\r\n`); + + // Zig prints to stderr regardless of success/failure + const { stderr: output } = await execFile(zigPath, args); + const success = + !output.toLowerCase().includes("...FAIL") && + !output.toLowerCase().includes("error:") && + !output.toLowerCase().includes("test command failed"); + return { output: output.replaceAll("\n", "\r\n"), success }; + } + + private async debugTests(req: vscode.TestRunRequest, token: vscode.CancellationToken) { + const run = this.testController.createTestRun(req); + for (const item of req.include ?? this.testController.items) { + if (token.isCancellationRequested) break; + const test = Array.isArray(item) ? item[1] : item; + run.started(test); + try { + await this.debugTest(run, test); + run.passed(test); + } catch (e) { + run.failed(test, new vscode.TestMessage((e as Error).message)); + } + } + run.end(); + } + + private async debugTest(run: vscode.TestRun, testItem: vscode.TestItem) { + if (testItem.uri === undefined) { + throw new Error("Unable to determine file location"); + } + const testBinaryPath = await this.buildTestBinary(run, testItem.uri.fsPath, getTestDesc(testItem)); + const debugConfig: vscode.DebugConfiguration = { + type: "lldb", + name: `Debug ${testItem.label}`, + request: "launch", + program: testBinaryPath, + cwd: path.dirname(testItem.uri.fsPath), + stopAtEntry: false, + }; + await vscode.debug.startDebugging(undefined, debugConfig); + } + + private async buildTestBinary(run: vscode.TestRun, testFilePath: string, testDesc: string): Promise { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(testFilePath)); + if (!workspaceFolder) { + throw new Error("No workspace folder found"); + } + const outputDir = path.join(workspaceFolder.uri.fsPath, "zig-out", "tmp-debug-build", "bin"); + const binaryName = `test-${path.basename(testFilePath, ".zig")}`; + const binaryPath = path.join(outputDir, binaryName); + await vscode.workspace.fs.createDirectory(vscode.Uri.file(outputDir)); + + const zigPath = getZigPath(); + const { stdout, stderr } = await execFile(zigPath, [ + "test", + testFilePath, + "--test-filter", + testDesc, + "--test-no-exec", + `-femit-bin=${binaryPath}`, + ]); + if (stderr) { + run.appendOutput(stderr.replaceAll("\n", "\r\n")); + throw new Error(`Failed to build test binary: ${stderr}`); + } + run.appendOutput(stdout.replaceAll("\n", "\r\n")); + return binaryPath; + } +} + +function getTestDesc(testItem: vscode.TestItem): string { + const parts = testItem.id.split("."); + return parts[parts.length - 1]; +} From 35aac82eca677a65e6f032659d857c5d20fd968f Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 19:41:41 -0500 Subject: [PATCH 3/7] Better test failure capture using try catch --- src/zigTestRunnerProvider.ts | 43 ++++++++++++++---------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts index aa7220f..6a505c1 100644 --- a/src/zigTestRunnerProvider.ts +++ b/src/zigTestRunnerProvider.ts @@ -29,7 +29,7 @@ export default class ZigTestRunnerProvider { "Debug", vscode.TestRunProfileKind.Debug, this.debugTests.bind(this), - true, + false, ); } @@ -65,7 +65,7 @@ export default class ZigTestRunnerProvider { const fileName = path.basename(textDocument.uri.fsPath); const testItem = this.testController.createTestItem( - `${fileName}.test.${testDesc}`, // Test id needs to be unique, so adding file name here + `${fileName}.test.${testDesc}`, // Test id needs to be unique, so adding file name prefix `${fileName} - ${testDesc}`, textDocument.uri, ); @@ -84,20 +84,15 @@ export default class ZigTestRunnerProvider { const testItem = Array.isArray(item) ? item[1] : item; run.started(testItem); const start = new Date(); - try { - run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); - const { output, success } = await this.runTest(run, testItem); - run.appendOutput(output.replaceAll("\n", "\r\n")); - run.appendOutput("\r\n"); - const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); - if (!success) { - run.failed(testItem, new vscode.TestMessage(new vscode.MarkdownString(output)), elapsed); - } else { - run.passed(testItem, elapsed); - } - } catch (e) { - const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); - run.failed(testItem, new vscode.TestMessage((e as Error).message), elapsed); + run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); + const { output, success } = await this.runTest(run, testItem); + run.appendOutput(output.replaceAll("\n", "\r\n")); + run.appendOutput("\r\n"); + const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); + if (!success) { + run.failed(testItem, new vscode.TestMessage(output), elapsed); + } else { + run.passed(testItem, elapsed); } } run.end(); @@ -108,19 +103,15 @@ export default class ZigTestRunnerProvider { if (test.uri === undefined) { return { output: "Unable to determine file location", success: false }; } - // Unwrapping test desc from test id const parts = test.id.split("."); const lastPart = parts[parts.length - 1]; const args = ["test", "--test-filter", lastPart, test.uri.fsPath]; - run.appendOutput(`Running command: ${zigPath} ${args.join(" ")}\r\n`); - - // Zig prints to stderr regardless of success/failure - const { stderr: output } = await execFile(zigPath, args); - const success = - !output.toLowerCase().includes("...FAIL") && - !output.toLowerCase().includes("error:") && - !output.toLowerCase().includes("test command failed"); - return { output: output.replaceAll("\n", "\r\n"), success }; + try { + const { stderr: output } = await execFile(zigPath, args); + return { output: output.replaceAll("\n", "\r\n"), success: true }; + } catch (e) { + return { output: (e as Error).message.replaceAll("\n", "\r\n"), success: false }; + } } private async debugTests(req: vscode.TestRunRequest, token: vscode.CancellationToken) { From d9450cbc6fb8840bfebcd390dd8c12f42ae9b15a Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 20:58:44 -0500 Subject: [PATCH 4/7] Find all zig tests in the folder and register them --- src/zigTestRunnerProvider.ts | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts index 6a505c1..151c720 100644 --- a/src/zigTestRunnerProvider.ts +++ b/src/zigTestRunnerProvider.ts @@ -31,6 +31,7 @@ export default class ZigTestRunnerProvider { this.debugTests.bind(this), false, ); + void this.findAndRegisterTests(); } public activate(subscriptions: vscode.Disposable[]) { @@ -41,22 +42,44 @@ export default class ZigTestRunnerProvider { vscode.workspace.onDidChangeTextDocument((change) => { this.updateTestItems(change.document); }), - vscode.workspace.onDidCloseTextDocument((document) => { - this.testController.items.forEach((item) => { - if (item.uri === document.uri) { - this.testController.items.delete(item.id); - } + vscode.workspace.onDidDeleteFiles((event) => { + event.files.forEach((file) => { + this.deleteTestForAFile(file); + }); + }), + vscode.workspace.onDidRenameFiles((event) => { + event.files.forEach((file) => { + this.deleteTestForAFile(file.oldUri); }); }), ); } + private deleteTestForAFile(uri: vscode.Uri) { + this.testController.items.forEach((item) => { + if (!item.uri) return; + if (item.uri.fsPath === uri.fsPath) { + this.testController.items.delete(item.id); + } + }); + } + + private async findAndRegisterTests() { + const files = await vscode.workspace.findFiles("**/*.zig"); + for (const file of files) { + try { + const doc = await vscode.workspace.openTextDocument(file); + this._updateTestItems(doc); + } catch {} + } + } + private _updateTestItems(textDocument: vscode.TextDocument) { if (textDocument.languageId !== "zig") return; const regex = /\btest\s+"([^"]+)"\s*\{/g; const matches = Array.from(textDocument.getText().matchAll(regex)); - const newTests: vscode.TestItem[] = []; + this.deleteTestForAFile(textDocument.uri); for (const match of matches) { const testDesc = match[1]; @@ -71,7 +94,6 @@ export default class ZigTestRunnerProvider { ); testItem.range = range; this.testController.items.add(testItem); - newTests.push(testItem); } } From 2e28acb3ac60fce58d72ad43018e0798ba11df57 Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 22:39:46 -0500 Subject: [PATCH 5/7] Support for non-workspace files --- src/zigMainCodeLens.ts | 57 +++++++++++++++++++++++------------- src/zigTestRunnerProvider.ts | 18 +++++++----- src/zigUtil.ts | 14 +++++++++ 3 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/zigMainCodeLens.ts b/src/zigMainCodeLens.ts index 0a1beaf..5f17dcb 100644 --- a/src/zigMainCodeLens.ts +++ b/src/zigMainCodeLens.ts @@ -1,9 +1,11 @@ +import vscode from "vscode"; + import childProcess from "child_process"; import fs from "fs"; -import { getZigPath } from "./zigUtil"; import path from "path"; import util from "util"; -import vscode from "vscode"; + +import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); @@ -38,9 +40,8 @@ export class ZigMainCodeLensProvider implements vscode.CodeLensProvider { function zigRun(filePath: string) { const terminal = vscode.window.createTerminal("Run Zig Program"); terminal.show(); - - const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); - if (workspaceFolder && hasBuildFile(workspaceFolder.uri.fsPath)) { + const wsFolder = getWorkspaceFolder(filePath); + if (wsFolder && isWorkspaceFile(filePath) && hasBuildFile(wsFolder.uri.fsPath)) { terminal.sendText(`${getZigPath()} build run`); return; } @@ -53,24 +54,27 @@ function hasBuildFile(workspaceFspath: string): boolean { } async function zigDebug(filePath: string) { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); - let binaryPath = ""; + try { + const workspaceFolder = getWorkspaceFolder(filePath); + let binaryPath = ""; + if (workspaceFolder && isWorkspaceFile(filePath) && hasBuildFile(workspaceFolder.uri.fsPath)) { + binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); + } else { + binaryPath = await buildDebugBinary(filePath); + } - if (workspaceFolder && hasBuildFile(workspaceFolder.uri.fsPath)) { - binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); - } else { - binaryPath = filePath; + const debugConfig: vscode.DebugConfiguration = { + type: "lldb", + name: `Debug Zig`, + request: "launch", + program: binaryPath, + cwd: path.dirname(workspaceFolder?.uri.fsPath ?? path.dirname(filePath)), + stopAtEntry: false, + }; + await vscode.debug.startDebugging(undefined, debugConfig); + } catch (e) { + void vscode.window.showErrorMessage(`Failed to build debug binary: ${(e as Error).message}`); } - - const debugConfig: vscode.DebugConfiguration = { - type: "lldb", - name: `Debug Zig`, - request: "launch", - program: binaryPath, - cwd: path.dirname(workspaceFolder?.uri.fsPath ?? filePath), - stopAtEntry: false, - }; - await vscode.debug.startDebugging(undefined, debugConfig); } async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { @@ -86,3 +90,14 @@ async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { + const zigPath = getZigPath(); + const fileDirectory = path.dirname(filePath); + const binaryName = `debug-${path.basename(filePath, ".zig")}`; + const binaryPath = path.join(fileDirectory, "zig-out", "bin", binaryName); + void vscode.workspace.fs.createDirectory(vscode.Uri.file(path.dirname(binaryPath))); + + await execFile(zigPath, ["run", filePath, `-femit-bin=${binaryPath}`], { cwd: fileDirectory }); + return binaryPath; +} diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts index 151c720..4888a4a 100644 --- a/src/zigTestRunnerProvider.ts +++ b/src/zigTestRunnerProvider.ts @@ -6,7 +6,7 @@ import util from "util"; import { DebouncedFunc, throttle } from "lodash-es"; -import { getZigPath } from "./zigUtil"; +import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); @@ -39,6 +39,9 @@ export default class ZigTestRunnerProvider { vscode.workspace.onDidOpenTextDocument((document) => { this.updateTestItems(document); }), + vscode.workspace.onDidCloseTextDocument((document) => { + !isWorkspaceFile(document.uri.fsPath) && this.deleteTestForAFile(document.uri); + }), vscode.workspace.onDidChangeTextDocument((change) => { this.updateTestItems(change.document); }), @@ -104,13 +107,15 @@ export default class ZigTestRunnerProvider { for (const item of request.include ?? this.testController.items) { if (token.isCancellationRequested) break; const testItem = Array.isArray(item) ? item[1] : item; + run.started(testItem); const start = new Date(); run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); - const { output, success } = await this.runTest(run, testItem); + const { output, success } = await this.runTest(testItem); run.appendOutput(output.replaceAll("\n", "\r\n")); run.appendOutput("\r\n"); const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); + if (!success) { run.failed(testItem, new vscode.TestMessage(output), elapsed); } else { @@ -120,7 +125,7 @@ export default class ZigTestRunnerProvider { run.end(); } - private async runTest(run: vscode.TestRun, test: vscode.TestItem): Promise<{ output: string; success: boolean }> { + private async runTest(test: vscode.TestItem): Promise<{ output: string; success: boolean }> { const zigPath = getZigPath(); if (test.uri === undefined) { return { output: "Unable to determine file location", success: false }; @@ -169,11 +174,8 @@ export default class ZigTestRunnerProvider { } private async buildTestBinary(run: vscode.TestRun, testFilePath: string, testDesc: string): Promise { - const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(testFilePath)); - if (!workspaceFolder) { - throw new Error("No workspace folder found"); - } - const outputDir = path.join(workspaceFolder.uri.fsPath, "zig-out", "tmp-debug-build", "bin"); + const wsFolder = getWorkspaceFolder(testFilePath)?.uri.fsPath ?? path.dirname(testFilePath); + const outputDir = path.join(wsFolder, "zig-out", "tmp-debug-build", "bin"); const binaryName = `test-${path.basename(testFilePath, ".zig")}`; const binaryPath = path.join(outputDir, binaryName); await vscode.workspace.fs.createDirectory(vscode.Uri.file(outputDir)); diff --git a/src/zigUtil.ts b/src/zigUtil.ts index ad5916c..8d5ce53 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -217,3 +217,17 @@ export async function downloadAndExtractArtifact( }, ); } + +export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + return vscode.workspace.workspaceFolders[0]; + } + return workspaceFolder; +} + +export function isWorkspaceFile(filePath: string): boolean { + const wsFolder = getWorkspaceFolder(filePath); + if (!wsFolder) return false; + return filePath.startsWith(wsFolder.uri.fsPath); +} From 7280119ad7879e83362f9081ba6691aa9541c4c4 Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Sat, 2 Nov 2024 23:52:56 -0500 Subject: [PATCH 6/7] Add run and debug command --- README.md | 2 ++ package.json | 12 ++++++++++++ src/extension.ts | 8 ++++---- src/zigMainCodeLens.ts | 12 ++++++++---- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b156988..6a6e371 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ - syntax highlighting - basic compiler linting - automatic formatting +- Run/Debug zig program +- Run/Debug tests - optional [Zig Language Server](https://github.com/zigtools/zls) features - completions - goto definition/declaration diff --git a/package.json b/package.json index c28375b..9f32a8d 100644 --- a/package.json +++ b/package.json @@ -312,6 +312,18 @@ } }, "commands": [ + { + "command": "zig.run", + "title": "Run Zig", + "category": "Zig", + "description": "Run the current Zig project / file" + }, + { + "command": "zig.debug", + "title": "Debug Zig", + "category": "Zig", + "description": "Debug the current Zig project / file" + }, { "command": "zig.build.workspace", "title": "Build Workspace", diff --git a/src/extension.ts b/src/extension.ts index 6e20c06..07667ed 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,7 @@ import vscode from "vscode"; import { activate as activateZls, deactivate as deactivateZls } from "./zls"; import ZigCompilerProvider from "./zigCompilerProvider"; -import { ZigMainCodeLensProvider } from "./zigMainCodeLens"; +import ZigMainCodeLensProvider from "./zigMainCodeLens"; import ZigTestRunnerProvider from "./zigTestRunnerProvider"; import { registerDocumentFormatting } from "./zigFormat"; import { setupZig } from "./zigSetup"; @@ -20,9 +20,9 @@ export async function activate(context: vscode.ExtensionContext) { ZigMainCodeLensProvider.registerCommands(context); context.subscriptions.push( vscode.languages.registerCodeLensProvider( - { language: 'zig', scheme: 'file' }, - new ZigMainCodeLensProvider() - ) + { language: "zig", scheme: "file" }, + new ZigMainCodeLensProvider(), + ), ); void activateZls(context); diff --git a/src/zigMainCodeLens.ts b/src/zigMainCodeLens.ts index 5f17dcb..0aba94f 100644 --- a/src/zigMainCodeLens.ts +++ b/src/zigMainCodeLens.ts @@ -9,7 +9,7 @@ import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); -export class ZigMainCodeLensProvider implements vscode.CodeLensProvider { +export default class ZigMainCodeLensProvider implements vscode.CodeLensProvider { public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { const codeLenses: vscode.CodeLens[] = []; const text = document.getText(); @@ -37,7 +37,9 @@ export class ZigMainCodeLensProvider implements vscode.CodeLensProvider { } } -function zigRun(filePath: string) { +function zigRun() { + if (!vscode.window.activeTextEditor) return; + const filePath = vscode.window.activeTextEditor.document.uri.fsPath; const terminal = vscode.window.createTerminal("Run Zig Program"); terminal.show(); const wsFolder = getWorkspaceFolder(filePath); @@ -53,7 +55,9 @@ function hasBuildFile(workspaceFspath: string): boolean { return fs.existsSync(buildZigPath); } -async function zigDebug(filePath: string) { +async function zigDebug() { + if (!vscode.window.activeTextEditor) return; + const filePath = vscode.window.activeTextEditor.document.uri.fsPath; try { const workspaceFolder = getWorkspaceFolder(filePath); let binaryPath = ""; @@ -79,7 +83,7 @@ async function zigDebug(filePath: string) { async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { // Workaround because zig build doesn't support specifying the output binary name - // `zig run` does support -femit-bin, but what if build file has custom build logic? + // `zig run` does support -femit-bin, but preferring `zig build` if possible const outputDir = path.join(workspacePath, "zig-out", "tmp-debug-build"); const zigPath = getZigPath(); await execFile(zigPath, ["build", "--prefix", outputDir], { cwd: workspacePath }); From b945cdcc736c1ca3ca072adc5c388e7bc210bd2d Mon Sep 17 00:00:00 2001 From: Aman Tuladhar Date: Tue, 19 Nov 2024 19:41:04 -0600 Subject: [PATCH 7/7] Handle doctests --- src/zigTestRunnerProvider.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts index 4888a4a..d1eb8c6 100644 --- a/src/zigTestRunnerProvider.ts +++ b/src/zigTestRunnerProvider.ts @@ -80,18 +80,20 @@ export default class ZigTestRunnerProvider { private _updateTestItems(textDocument: vscode.TextDocument) { if (textDocument.languageId !== "zig") return; - const regex = /\btest\s+"([^"]+)"\s*\{/g; + const regex = /\btest\s+(?:"([^"]+)"|([^\s{]+))\s*\{/g; const matches = Array.from(textDocument.getText().matchAll(regex)); this.deleteTestForAFile(textDocument.uri); for (const match of matches) { - const testDesc = match[1]; + const testDesc = match[1] || match[2]; + const isDocTest = !!match[2]; const position = textDocument.positionAt(match.index); const range = new vscode.Range(position, position.translate(0, match[0].length)); const fileName = path.basename(textDocument.uri.fsPath); + // Add doctest prefix to handle scenario where test name matches one with non doctest. E.g `test foo` and `test "foo"` const testItem = this.testController.createTestItem( - `${fileName}.test.${testDesc}`, // Test id needs to be unique, so adding file name prefix + `${fileName}.test.${isDocTest ? "doctest." : ""}${testDesc}`, // Test id needs to be unique, so adding file name prefix `${fileName} - ${testDesc}`, textDocument.uri, );