This repository was archived by the owner on Nov 25, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 75
Run and Debug zig program and tests #242
Merged
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a7aee34
Add zig code runner
seg4lt 419a92e
Add a test run support for zig files
seg4lt 35aac82
Better test failure capture using try catch
seg4lt d9450cb
Find all zig tests in the folder and register them
seg4lt 2e28acb
Support for non-workspace files
seg4lt 7280119
Add run and debug command
seg4lt b945cdc
Handle doctests
seg4lt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import vscode from "vscode"; | ||
|
|
||
| import childProcess from "child_process"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import util from "util"; | ||
|
|
||
| import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; | ||
|
|
||
| const execFile = util.promisify(childProcess.execFile); | ||
|
|
||
| export default class ZigMainCodeLensProvider implements vscode.CodeLensProvider { | ||
| public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult<vscode.CodeLens[]> { | ||
| 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() { | ||
| 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); | ||
| if (wsFolder && isWorkspaceFile(filePath) && hasBuildFile(wsFolder.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() { | ||
| if (!vscode.window.activeTextEditor) return; | ||
| const filePath = vscode.window.activeTextEditor.document.uri.fsPath; | ||
| 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); | ||
| } | ||
|
|
||
| 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}`); | ||
| } | ||
| } | ||
|
|
||
| async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise<string> { | ||
| // Workaround because zig build doesn't support specifying the output binary name | ||
| // `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 }); | ||
| 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]); | ||
| } | ||
|
|
||
| async function buildDebugBinary(filePath: string): Promise<string> { | ||
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import vscode from "vscode"; | ||
|
|
||
| import childProcess from "child_process"; | ||
| import path from "path"; | ||
| import util from "util"; | ||
|
|
||
| import { DebouncedFunc, throttle } from "lodash-es"; | ||
|
|
||
| import { getWorkspaceFolder, getZigPath, isWorkspaceFile } 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), | ||
| false, | ||
| ); | ||
| void this.findAndRegisterTests(); | ||
| } | ||
|
|
||
| public activate(subscriptions: vscode.Disposable[]) { | ||
| subscriptions.push( | ||
| 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); | ||
| }), | ||
| 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)); | ||
| this.deleteTestForAFile(textDocument.uri); | ||
|
|
||
| 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 prefix | ||
| `${fileName} - ${testDesc}`, | ||
| textDocument.uri, | ||
| ); | ||
| testItem.range = range; | ||
| this.testController.items.add(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(); | ||
| run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); | ||
| 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 { | ||
| run.passed(testItem, elapsed); | ||
| } | ||
| } | ||
| run.end(); | ||
| } | ||
|
|
||
| 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 }; | ||
| } | ||
| const parts = test.id.split("."); | ||
| const lastPart = parts[parts.length - 1]; | ||
| const args = ["test", "--test-filter", lastPart, test.uri.fsPath]; | ||
| 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) { | ||
| 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<string> { | ||
| 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)); | ||
|
|
||
| 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]; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.