├── .prettierignore ├── .gitignore ├── images ├── example.png └── zig-icon.png ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .prettierrc ├── tsconfig.json ├── .forgejo └── workflows │ └── ci.yml ├── src ├── terminalState.ts ├── extension.ts ├── zigProvider.ts ├── zigDiagnosticsProvider.ts ├── zigBuildOnSaveProvider.ts ├── zigFormat.ts ├── minisign.ts ├── zigMainCodeLens.ts ├── zigUtil.ts ├── zigTestRunnerProvider.ts ├── versionManager.ts ├── zls.ts └── zigSetup.ts ├── README.md ├── LICENSE ├── eslint.config.mjs ├── language-configuration.json ├── CHANGELOG.md ├── syntaxes └── zig.tmLanguage.json └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | *.md 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziglang/vscode-zig/master/images/example.png -------------------------------------------------------------------------------- /images/zig-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziglang/vscode-zig/master/images/zig-icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !images/zig-icon.png 3 | !images/example.png 4 | !syntaxes/zig.tmLanguage.json 5 | !language-configuration.json 6 | !out/extension.js 7 | !package.json 8 | !LICENSE 9 | !README.md 10 | !CHANGELOG.md 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "overrides": [ 5 | { 6 | "files": ["*.yml", "*.yaml", "package-lock.json", "package.json", "syntaxes/*.json"], 7 | "options": { 8 | "tabWidth": 2 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Launch Extension", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 11 | "preLaunchTask": "Build Extension" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "bundler", 4 | "target": "ES2021", 5 | "outDir": "out", 6 | "lib": ["ES2021"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | 10 | "strict": true, 11 | "allowUnreachableCode": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noPropertyAccessFromIndexSignature": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | } 13 | -------------------------------------------------------------------------------- /.forgejo/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.head_ref || github.run_id }}-${{ github.actor }} 12 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: [self-hosted, x86_64-linux] 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - run: npm install 24 | - run: npm run build 25 | - run: npm run typecheck 26 | - run: npm run lint 27 | - run: npm run format:check 28 | 29 | - name: package extension 30 | run: | 31 | npx vsce package 32 | ls -lt *.vsix 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build Extension in Background", 8 | "group": "build", 9 | "type": "npm", 10 | "script": "watch", 11 | "problemMatcher": { 12 | "base": "$tsc-watch" 13 | }, 14 | "isBackground": true 15 | }, 16 | { 17 | "label": "Build Extension", 18 | "group": "build", 19 | "type": "npm", 20 | "script": "build", 21 | "problemMatcher": { 22 | "base": "$tsc" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/terminalState.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A status monitor for a VSCode terminal. 3 | */ 4 | 5 | import vscode from "vscode"; 6 | 7 | const terminalsState = new Map(); 8 | 9 | export function getTerminalState(terminal: vscode.Terminal): boolean | undefined { 10 | return terminalsState.get(terminal); 11 | } 12 | 13 | export function registerTerminalStateManagement(): void { 14 | vscode.window.onDidOpenTerminal((terminal) => { 15 | terminalsState.set(terminal, false); 16 | }); 17 | vscode.window.onDidStartTerminalShellExecution((event) => { 18 | terminalsState.set(event.terminal, true); 19 | }); 20 | vscode.window.onDidEndTerminalShellExecution((event) => { 21 | terminalsState.set(event.terminal, false); 22 | }); 23 | vscode.window.onDidCloseTerminal((terminal) => { 24 | terminalsState.delete(terminal); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-zig 2 | 3 | [![VSCode Extension](https://img.shields.io/badge/vscode-extension-brightgreen)](https://marketplace.visualstudio.com/items?itemName=ziglang.vscode-zig) 4 | [![CI](https://github.com/ziglang/vscode-zig/workflows/CI/badge.svg)](https://github.com/ziglang/vscode-zig/actions) 5 | 6 | [Zig](http://ziglang.org/) support for Visual Studio Code. 7 | 8 | ![Syntax Highlighting, Code Completion](./images/example.png) 9 | 10 | ## Features 11 | 12 | - install and manage Zig version 13 | - syntax highlighting 14 | - basic compiler linting 15 | - automatic formatting 16 | - Run/Debug zig program 17 | - Run/Debug tests 18 | - optional [ZLS language server](https://github.com/zigtools/zls) features 19 | - completions 20 | - goto definition/declaration 21 | - document symbols 22 | - ... and [many more](https://github.com/zigtools/zls#features) 23 | 24 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Marc Tiehuis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import tseslint from "typescript-eslint"; 4 | import prettierConfig from "eslint-config-prettier"; 5 | 6 | export default tseslint.config( 7 | tseslint.configs.stylisticTypeChecked, 8 | tseslint.configs.strictTypeChecked, 9 | prettierConfig, 10 | { 11 | rules: { 12 | "@typescript-eslint/naming-convention": "error", 13 | "@typescript-eslint/switch-exhaustiveness-check": ["error", { considerDefaultExhaustiveForUnions: true }], 14 | eqeqeq: "error", 15 | "no-throw-literal": "off", 16 | "@typescript-eslint/only-throw-error": "error", 17 | "no-shadow": "off", 18 | "@typescript-eslint/no-shadow": "error", 19 | "no-duplicate-imports": "error", 20 | "sort-imports": ["error", { allowSeparatedGroups: true }], 21 | }, 22 | languageOptions: { 23 | parserOptions: { 24 | projectService: true, 25 | tsconfigRootDir: import.meta.dirname, 26 | }, 27 | }, 28 | }, 29 | { 30 | ignores: ["**/*.js", "**/*.mjs"], 31 | }, 32 | ); 33 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["'", "'"] 16 | ], 17 | "surroundingPairs": [ 18 | ["{", "}"], 19 | ["[", "]"], 20 | ["(", ")"], 21 | ["\"", "\""], 22 | ["'", "'"] 23 | ], 24 | "folding": { 25 | "markers": { 26 | "start": "// zig fmt: off\\b", 27 | "end": "// zig fmt: on\\b" 28 | } 29 | }, 30 | "onEnterRules": [ 31 | { 32 | "beforeText": "^\\s*//!.*$", 33 | "action": { 34 | "indent": "none", 35 | "appendText": "//! " 36 | } 37 | }, 38 | { 39 | "beforeText": "^\\s*///.*$", 40 | "action": { 41 | "indent": "none", 42 | "appendText": "/// " 43 | } 44 | }, 45 | { 46 | "beforeText": "^\\s*\\\\\\\\.*$", 47 | "action": { 48 | "indent": "none", 49 | "appendText": "\\\\" 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import { activate as activateZls, deactivate as deactivateZls } from "./zls"; 4 | import ZigMainCodeLensProvider from "./zigMainCodeLens"; 5 | import ZigTestRunnerProvider from "./zigTestRunnerProvider"; 6 | import { registerBuildOnSaveProvider } from "./zigBuildOnSaveProvider"; 7 | import { registerDiagnosticsProvider } from "./zigDiagnosticsProvider"; 8 | import { registerDocumentFormatting } from "./zigFormat"; 9 | import { registerTerminalStateManagement } from "./terminalState"; 10 | import { setupZig } from "./zigSetup"; 11 | 12 | export async function activate(context: vscode.ExtensionContext) { 13 | await setupZig(context).finally(() => { 14 | context.subscriptions.push(registerDiagnosticsProvider()); 15 | context.subscriptions.push(registerBuildOnSaveProvider()); 16 | context.subscriptions.push(registerDocumentFormatting()); 17 | 18 | const testRunner = new ZigTestRunnerProvider(); 19 | testRunner.activate(context.subscriptions); 20 | 21 | registerTerminalStateManagement(); 22 | ZigMainCodeLensProvider.registerCommands(context); 23 | context.subscriptions.push( 24 | vscode.languages.registerCodeLensProvider( 25 | { language: "zig", scheme: "file" }, 26 | new ZigMainCodeLensProvider(), 27 | ), 28 | vscode.commands.registerCommand("zig.toggleMultilineStringLiteral", toggleMultilineStringLiteral), 29 | ); 30 | 31 | void activateZls(context); 32 | }); 33 | } 34 | 35 | export async function deactivate() { 36 | await deactivateZls(); 37 | } 38 | 39 | async function toggleMultilineStringLiteral() { 40 | const editor = vscode.window.activeTextEditor; 41 | if (!editor) return; 42 | const { document, selection } = editor; 43 | if (document.languageId !== "zig") return; 44 | 45 | let newText = ""; 46 | let range = new vscode.Range(selection.start, selection.end); 47 | 48 | const firstLine = document.lineAt(selection.start.line); 49 | const nonWhitespaceIndex = firstLine.firstNonWhitespaceCharacterIndex; 50 | 51 | for (let lineNum = selection.start.line; lineNum <= selection.end.line; lineNum++) { 52 | const line = document.lineAt(lineNum); 53 | 54 | const isMLSL = line.text.slice(line.firstNonWhitespaceCharacterIndex).startsWith("\\\\"); 55 | const breakpoint = Math.min(nonWhitespaceIndex, line.firstNonWhitespaceCharacterIndex); 56 | 57 | const newLine = isMLSL 58 | ? line.text.slice(0, line.firstNonWhitespaceCharacterIndex) + 59 | line.text.slice(line.firstNonWhitespaceCharacterIndex).slice(2) 60 | : line.isEmptyOrWhitespace 61 | ? " ".repeat(nonWhitespaceIndex) + "\\\\" 62 | : line.text.slice(0, breakpoint) + "\\\\" + line.text.slice(breakpoint); 63 | newText += newLine; 64 | if (lineNum < selection.end.line) newText += "\n"; 65 | 66 | range = range.union(line.range); 67 | } 68 | 69 | await editor.edit((builder) => { 70 | builder.replace(range, newText); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/zigProvider.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import semver from "semver"; 4 | 5 | import { resolveExePathAndVersion, workspaceConfigUpdateNoThrow } from "./zigUtil"; 6 | 7 | interface ExeWithVersion { 8 | exe: string; 9 | version: semver.SemVer; 10 | } 11 | 12 | export class ZigProvider { 13 | onChange: vscode.EventEmitter = new vscode.EventEmitter(); 14 | private value: ExeWithVersion | null; 15 | 16 | constructor() { 17 | this.value = this.resolveZigPathConfigOption() ?? null; 18 | } 19 | 20 | /** Returns the version of the Zig executable that is currently being used. */ 21 | public getZigVersion(): semver.SemVer | null { 22 | return this.value?.version ?? null; 23 | } 24 | 25 | /** Returns the path to the Zig executable that is currently being used. */ 26 | public getZigPath(): string | null { 27 | return this.value?.exe ?? null; 28 | } 29 | 30 | /** Set the path the Zig executable. The `zig.path` config option will be ignored */ 31 | public set(value: ExeWithVersion | null) { 32 | if (value === null && this.value === null) return; 33 | if (value !== null && this.value !== null && value.version.compare(this.value.version) === 0) return; 34 | this.value = value; 35 | this.onChange.fire(value); 36 | } 37 | 38 | /** 39 | * Set the path the Zig executable. Will be saved in `zig.path` config option. 40 | * 41 | * @param zigPath The path to the zig executable. If `null`, the `zig.path` config option will be removed. 42 | */ 43 | public async setAndSave(zigPath: string | null) { 44 | const zigConfig = vscode.workspace.getConfiguration("zig"); 45 | if (!zigPath) { 46 | await workspaceConfigUpdateNoThrow(zigConfig, "path", undefined, true); 47 | return; 48 | } 49 | const newValue = this.resolveZigPathConfigOption(zigPath); 50 | if (!newValue) return; 51 | await workspaceConfigUpdateNoThrow(zigConfig, "path", newValue.exe, true); 52 | this.set(newValue); 53 | } 54 | 55 | /** Resolves the `zig.path` configuration option. */ 56 | public resolveZigPathConfigOption(zigPath?: string): ExeWithVersion | null | undefined { 57 | zigPath ??= vscode.workspace.getConfiguration("zig").get("path", ""); 58 | if (!zigPath) return null; 59 | const result = resolveExePathAndVersion(zigPath, "version"); 60 | if ("message" in result) { 61 | vscode.window 62 | .showErrorMessage(`Unexpected 'zig.path': ${result.message}`, "install Zig", "open settings") 63 | .then(async (response) => { 64 | switch (response) { 65 | case "install Zig": 66 | await workspaceConfigUpdateNoThrow( 67 | vscode.workspace.getConfiguration("zig"), 68 | "path", 69 | undefined, 70 | ); 71 | break; 72 | case "open settings": 73 | await vscode.commands.executeCommand("workbench.action.openSettings", "zig.path"); 74 | break; 75 | case undefined: 76 | break; 77 | } 78 | }); 79 | return undefined; 80 | } 81 | return result; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/zigDiagnosticsProvider.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import childProcess from "child_process"; 4 | import path from "path"; 5 | 6 | // This will be treeshaked to only the debounce function 7 | import { throttle } from "lodash-es"; 8 | 9 | import * as semver from "semver"; 10 | import * as zls from "./zls"; 11 | import { zigProvider } from "./zigSetup"; 12 | 13 | export function registerDiagnosticsProvider(): vscode.Disposable { 14 | const disposables: vscode.Disposable[] = []; 15 | 16 | const diagnosticCollection = vscode.languages.createDiagnosticCollection("zig"); 17 | disposables.push(diagnosticCollection); 18 | 19 | const throttledCollectAstCheckDiagnostics = throttle(collectAstCheckDiagnostics, 16, { trailing: true }); 20 | 21 | vscode.workspace.onDidChangeTextDocument((change) => { 22 | if (change.document.languageId !== "zig") { 23 | return; 24 | } 25 | if (zls.client !== null) { 26 | diagnosticCollection.clear(); 27 | return; 28 | } 29 | if (change.document.isClosed) { 30 | diagnosticCollection.delete(change.document.uri); 31 | } 32 | 33 | throttledCollectAstCheckDiagnostics(diagnosticCollection, change.document); 34 | }, disposables); 35 | 36 | return { 37 | dispose: () => { 38 | for (const disposable of disposables) { 39 | disposable.dispose(); 40 | } 41 | }, 42 | }; 43 | } 44 | 45 | function collectAstCheckDiagnostics( 46 | diagnosticCollection: vscode.DiagnosticCollection, 47 | textDocument: vscode.TextDocument, 48 | ): void { 49 | const zigPath = zigProvider.getZigPath(); 50 | const zigVersion = zigProvider.getZigVersion(); 51 | if (!zigPath || !zigVersion) return; 52 | 53 | const args = ["ast-check"]; 54 | 55 | const addedZonSupportVersion = new semver.SemVer("0.14.0-dev.2508+7e8be2136"); 56 | if (path.extname(textDocument.fileName) === ".zon" && semver.gte(zigVersion, addedZonSupportVersion)) { 57 | args.push("--zon"); 58 | } 59 | 60 | const { error, stderr } = childProcess.spawnSync(zigPath, args, { 61 | input: textDocument.getText(), 62 | maxBuffer: 10 * 1024 * 1024, // 10MB 63 | encoding: "utf8", 64 | stdio: ["pipe", "ignore", "pipe"], 65 | timeout: 5000, // 5 seconds 66 | }); 67 | 68 | if (error ?? stderr.length === 0) { 69 | diagnosticCollection.delete(textDocument.uri); 70 | return; 71 | } 72 | 73 | const diagnostics: Record = {}; 74 | const regex = /(\S.*):(\d*):(\d*): ([^:]*): (.*)/g; 75 | 76 | for (let match = regex.exec(stderr); match; match = regex.exec(stderr)) { 77 | const filePath = textDocument.uri.fsPath; 78 | 79 | const line = parseInt(match[2]) - 1; 80 | const column = parseInt(match[3]) - 1; 81 | const type = match[4]; 82 | const message = match[5]; 83 | 84 | const severity = 85 | type.trim().toLowerCase() === "error" 86 | ? vscode.DiagnosticSeverity.Error 87 | : vscode.DiagnosticSeverity.Information; 88 | const range = new vscode.Range(line, column, line, Infinity); 89 | 90 | const diagnosticArray = diagnostics[filePath] ?? []; 91 | diagnosticArray.push(new vscode.Diagnostic(range, message, severity)); 92 | diagnostics[filePath] = diagnosticArray; 93 | } 94 | 95 | for (const filePath in diagnostics) { 96 | const diagnostic = diagnostics[filePath]; 97 | diagnosticCollection.set(textDocument.uri, diagnostic); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/zigBuildOnSaveProvider.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import { handleConfigOption } from "./zigUtil"; 4 | 5 | export function registerBuildOnSaveProvider(): vscode.Disposable { 6 | return new BuildOnSaveProvider(); 7 | } 8 | 9 | type BuildOnSaveProviderKind = "off" | "auto" | "extension" | "zls"; 10 | 11 | class BuildOnSaveProvider implements vscode.Disposable { 12 | disposables: vscode.Disposable[] = []; 13 | /** This may be replacable with `vscode.tasks.taskExecutions` */ 14 | tasks = new Map(); 15 | 16 | constructor() { 17 | for (const folder of vscode.workspace.workspaceFolders ?? []) { 18 | void this.addOrRestart(folder); 19 | } 20 | 21 | vscode.workspace.onDidChangeWorkspaceFolders(async (e) => { 22 | for (const folder of e.added) { 23 | await this.addOrRestart(folder); 24 | } 25 | for (const folder of e.removed) { 26 | this.stop(folder); 27 | } 28 | }, this.disposables); 29 | 30 | vscode.workspace.onDidChangeConfiguration(async (e) => { 31 | if (!e.affectsConfiguration("zig.buildOnSaveProvider")) return; 32 | 33 | for (const folder of vscode.workspace.workspaceFolders ?? []) { 34 | await this.addOrRestart(folder); 35 | } 36 | }, this.disposables); 37 | } 38 | 39 | dispose() { 40 | for (const disposable of this.disposables) { 41 | disposable.dispose(); 42 | } 43 | } 44 | 45 | async addOrRestart(folder: vscode.WorkspaceFolder): Promise { 46 | this.stop(folder); 47 | 48 | const zigConfig = vscode.workspace.getConfiguration("zig", folder); 49 | const buildOnSaveProvider = zigConfig.get("buildOnSaveProvider", "auto"); 50 | const buildOnSaveArgs = zigConfig 51 | .get("buildOnSaveArgs", []) 52 | .map((unresolved) => handleConfigOption(unresolved, folder)); 53 | 54 | if (buildOnSaveProvider !== "extension") return; 55 | 56 | if (buildOnSaveArgs.includes("--build-file")) { 57 | // The build file has been explicitly provided through a command line argument 58 | } else { 59 | const workspaceBuildZigUri = vscode.Uri.joinPath(folder.uri, "build.zig"); 60 | try { 61 | await vscode.workspace.fs.stat(workspaceBuildZigUri); 62 | } catch { 63 | return; 64 | } 65 | } 66 | 67 | const task = new vscode.Task( 68 | { 69 | type: "zig", 70 | }, 71 | folder, 72 | "Zig Watch", 73 | "zig", 74 | new vscode.ShellExecution("zig", ["build", "--watch", ...buildOnSaveArgs], {}), 75 | "zig", 76 | ); 77 | task.isBackground = true; 78 | task.presentationOptions.reveal = vscode.TaskRevealKind.Never; 79 | task.presentationOptions.close = true; 80 | const taskExecutor = await vscode.tasks.executeTask(task); 81 | this.stop(folder); // Try to stop again just in case a task got started while we were suspended 82 | this.tasks.set(folder.uri.toString(), taskExecutor); 83 | 84 | vscode.workspace.onDidChangeConfiguration(async (e) => { 85 | if (e.affectsConfiguration("zig.buildOnSaveProvider", folder)) { 86 | // We previously checked that the build on save provider is "extension" so now it has to be something different 87 | this.stop(folder); 88 | return; 89 | } 90 | if (e.affectsConfiguration("zig.buildOnSaveArgs", folder)) { 91 | await this.addOrRestart(folder); 92 | } 93 | }, this.disposables); 94 | } 95 | 96 | stop(folder: vscode.WorkspaceFolder): void { 97 | const oldTask = this.tasks.get(folder.uri.toString()); 98 | if (oldTask) oldTask.terminate(); 99 | this.tasks.delete(folder.uri.toString()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/zigFormat.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import childProcess from "child_process"; 4 | import util from "util"; 5 | 6 | import { DocumentFormattingRequest, TextDocumentIdentifier } from "vscode-languageclient"; 7 | 8 | import * as zls from "./zls"; 9 | import { zigProvider } from "./zigSetup"; 10 | 11 | const execFile = util.promisify(childProcess.execFile); 12 | const ZIG_MODE: vscode.DocumentSelector = { language: "zig" }; 13 | 14 | export function registerDocumentFormatting(): vscode.Disposable { 15 | const disposables: vscode.Disposable[] = []; 16 | let registeredFormatter: vscode.Disposable | null = null; 17 | 18 | preCompileZigFmt(); 19 | zigProvider.onChange.event(() => { 20 | preCompileZigFmt(); 21 | }, disposables); 22 | 23 | const onformattingProviderChange = (change: vscode.ConfigurationChangeEvent | null) => { 24 | if (!change || change.affectsConfiguration("zig.formattingProvider", undefined)) { 25 | preCompileZigFmt(); 26 | 27 | if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "off") { 28 | // Unregister the formatting provider 29 | if (registeredFormatter !== null) registeredFormatter.dispose(); 30 | registeredFormatter = null; 31 | } else { 32 | // register the formatting provider 33 | registeredFormatter ??= vscode.languages.registerDocumentRangeFormattingEditProvider(ZIG_MODE, { 34 | provideDocumentRangeFormattingEdits, 35 | }); 36 | } 37 | } 38 | }; 39 | 40 | onformattingProviderChange(null); 41 | vscode.workspace.onDidChangeConfiguration(onformattingProviderChange, disposables); 42 | 43 | return { 44 | dispose: () => { 45 | for (const disposable of disposables) { 46 | disposable.dispose(); 47 | } 48 | if (registeredFormatter !== null) registeredFormatter.dispose(); 49 | }, 50 | }; 51 | } 52 | 53 | /** Ensures that `zig fmt` has been JIT compiled. */ 54 | function preCompileZigFmt() { 55 | // This pre-compiles even if "zig.formattingProvider" is "zls". 56 | if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "off") return; 57 | 58 | const zigPath = zigProvider.getZigPath(); 59 | if (!zigPath) return; 60 | 61 | try { 62 | childProcess.execFile(zigPath, ["fmt", "--help"], { 63 | timeout: 60000, // 60 seconds (this is a very high value because 'zig fmt' is just in time compiled) 64 | }); 65 | } catch (err) { 66 | if (err instanceof Error) { 67 | void vscode.window.showErrorMessage(`Failed to run 'zig fmt': ${err.message}`); 68 | } else { 69 | throw err; 70 | } 71 | } 72 | } 73 | 74 | async function provideDocumentRangeFormattingEdits( 75 | document: vscode.TextDocument, 76 | range: vscode.Range, 77 | options: vscode.FormattingOptions, 78 | token: vscode.CancellationToken, 79 | ): Promise { 80 | if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "zls") { 81 | if (zls.client !== null) { 82 | return await (zls.client.sendRequest( 83 | DocumentFormattingRequest.type, 84 | { 85 | textDocument: TextDocumentIdentifier.create(document.uri.toString()), 86 | options: options, 87 | }, 88 | token, 89 | ) as Promise); 90 | } 91 | } 92 | 93 | const zigPath = zigProvider.getZigPath(); 94 | if (!zigPath) return null; 95 | 96 | const abortController = new AbortController(); 97 | token.onCancellationRequested(() => { 98 | abortController.abort(); 99 | }); 100 | 101 | const promise = execFile(zigPath, ["fmt", "--stdin"], { 102 | maxBuffer: 10 * 1024 * 1024, // 10MB 103 | signal: abortController.signal, 104 | timeout: 60000, // 60 seconds (this is a very high value because 'zig fmt' is just in time compiled) 105 | }); 106 | promise.child.stdin?.end(document.getText()); 107 | 108 | const { stdout } = await promise; 109 | 110 | if (stdout.length === 0) return null; 111 | const lastLineId = document.lineCount - 1; 112 | const wholeDocument = new vscode.Range(0, 0, lastLineId, document.lineAt(lastLineId).text.length); 113 | return [new vscode.TextEdit(wholeDocument, stdout)]; 114 | } 115 | -------------------------------------------------------------------------------- /src/minisign.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ported from: https://github.com/mlugg/setup-zig/blob/main/minisign.js 3 | * 4 | * Copyright Matthew Lugg 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | import sodium from "libsodium-wrappers"; 26 | 27 | export interface Key { 28 | id: Buffer; 29 | key: Buffer; 30 | } 31 | 32 | export const ready = sodium.ready; 33 | 34 | // Parse a minisign key represented as a base64 string. 35 | // Throws exceptions on invalid keys. 36 | export function parseKey(keyString: string): Key { 37 | const keyInfo = Buffer.from(keyString, "base64"); 38 | 39 | const id = keyInfo.subarray(2, 10); 40 | const key = keyInfo.subarray(10); 41 | 42 | if (key.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) { 43 | throw new Error("invalid public key given"); 44 | } 45 | 46 | return { 47 | id: id, 48 | key: key, 49 | }; 50 | } 51 | 52 | export interface Signature { 53 | algorithm: Buffer; 54 | keyID: Buffer; 55 | signature: Buffer; 56 | trustedComment: Buffer; 57 | globalSignature: Buffer; 58 | } 59 | 60 | // Parse a buffer containing the contents of a minisign signature file. 61 | // Throws exceptions on invalid signature files. 62 | export function parseSignature(sigBuf: Buffer): Signature { 63 | const untrustedHeader = Buffer.from("untrusted comment: "); 64 | const trustedHeader = Buffer.from("trusted comment: "); 65 | 66 | // Validate untrusted comment header, and skip 67 | if (!sigBuf.subarray(0, untrustedHeader.byteLength).equals(untrustedHeader)) { 68 | throw new Error("invalid minisign signature: bad untrusted comment header"); 69 | } 70 | sigBuf = sigBuf.subarray(untrustedHeader.byteLength); 71 | 72 | // Skip untrusted comment 73 | sigBuf = sigBuf.subarray(sigBuf.indexOf("\n") + 1); 74 | 75 | // Read and skip signature info 76 | const sigInfoEnd = sigBuf.indexOf("\n"); 77 | const sigInfo = Buffer.from(sigBuf.subarray(0, sigInfoEnd).toString(), "base64"); 78 | sigBuf = sigBuf.subarray(sigInfoEnd + 1); 79 | 80 | // Extract components of signature info 81 | const algorithm = sigInfo.subarray(0, 2); 82 | const keyID = sigInfo.subarray(2, 10); 83 | const signature = sigInfo.subarray(10); 84 | 85 | // Validate trusted comment header, and skip 86 | if (!sigBuf.subarray(0, trustedHeader.byteLength).equals(trustedHeader)) { 87 | throw new Error("invalid minisign signature: bad trusted comment header"); 88 | } 89 | sigBuf = sigBuf.subarray(trustedHeader.byteLength); 90 | 91 | // Read and skip trusted comment 92 | const trustedCommentEnd = sigBuf.indexOf("\n"); 93 | const trustedComment = sigBuf.subarray(0, trustedCommentEnd); 94 | sigBuf = sigBuf.subarray(trustedCommentEnd + 1); 95 | 96 | // Read and skip global signature; handle missing trailing newline, just in case 97 | let globalSigEnd = sigBuf.indexOf("\n"); 98 | if (globalSigEnd === -1) globalSigEnd = sigBuf.length; 99 | const globalSig = Buffer.from(sigBuf.subarray(0, globalSigEnd).toString(), "base64"); 100 | sigBuf = sigBuf.subarray(sigInfoEnd + 1); // this might be length+1, but that's allowed 101 | 102 | // Validate that all data has been consumed 103 | if (sigBuf.length !== 0) { 104 | throw new Error("invalid minisign signature: trailing bytes"); 105 | } 106 | 107 | return { 108 | algorithm: algorithm, 109 | keyID: keyID, 110 | signature: signature, 111 | trustedComment: trustedComment, 112 | globalSignature: globalSig, 113 | }; 114 | } 115 | 116 | // Given a parsed key, parsed signature file, and raw file content, verifies the signature, 117 | // including the global signature (hence validating the trusted comment). Does not throw. 118 | // Returns 'true' if the signature is valid for this file, 'false' otherwise. 119 | export function verifySignature(pubkey: Key, signature: Signature, fileContent: Buffer) { 120 | if (!signature.keyID.equals(pubkey.id)) { 121 | return false; 122 | } 123 | 124 | let signedContent; 125 | if (signature.algorithm.equals(Buffer.from("ED"))) { 126 | signedContent = sodium.crypto_generichash(sodium.crypto_generichash_BYTES_MAX, fileContent); 127 | } else { 128 | signedContent = fileContent; 129 | } 130 | if (!sodium.crypto_sign_verify_detached(signature.signature, signedContent, pubkey.key)) { 131 | return false; 132 | } 133 | 134 | const globalSignedContent = Buffer.concat([signature.signature, signature.trustedComment]); 135 | if (!sodium.crypto_sign_verify_detached(signature.globalSignature, globalSignedContent, pubkey.key)) { 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | -------------------------------------------------------------------------------- /src/zigMainCodeLens.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import childProcess from "child_process"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | import util from "util"; 7 | 8 | import { getWorkspaceFolder, isWorkspaceFile } from "./zigUtil"; 9 | import { getTerminalState } from "./terminalState"; 10 | import { zigProvider } from "./zigSetup"; 11 | 12 | const execFile = util.promisify(childProcess.execFile); 13 | 14 | export default class ZigMainCodeLensProvider implements vscode.CodeLensProvider { 15 | public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { 16 | const codeLenses: vscode.CodeLens[] = []; 17 | const text = document.getText(); 18 | 19 | const mainRegex = /^(?![ \t]*\/\/\/?\s*)[ \t]*pub\s+fn\s+main\s*\(/gm; 20 | 21 | let match; 22 | while ((match = mainRegex.exec(text))) { 23 | const position = document.positionAt(match.index); 24 | const line = document.lineAt(position.line); 25 | 26 | let targetLine = line.lineNumber + 1; 27 | const prevLineText = document.lineAt(targetLine - 1).text.trim(); 28 | if (prevLineText) targetLine--; 29 | 30 | const range = document.lineAt(targetLine).range; 31 | 32 | codeLenses.push( 33 | new vscode.CodeLens(range, { title: "Run", command: "zig.run", arguments: [document.uri.fsPath] }), 34 | ); 35 | codeLenses.push( 36 | new vscode.CodeLens(range, { title: "Debug", command: "zig.debug", arguments: [document.uri.fsPath] }), 37 | ); 38 | } 39 | return codeLenses; 40 | } 41 | 42 | public static registerCommands(context: vscode.ExtensionContext) { 43 | context.subscriptions.push( 44 | vscode.commands.registerCommand("zig.run", zigRun), 45 | vscode.commands.registerCommand("zig.debug", zigDebug), 46 | ); 47 | } 48 | } 49 | 50 | function zigRun() { 51 | if (!vscode.window.activeTextEditor) return; 52 | const zigPath = zigProvider.getZigPath(); 53 | if (!zigPath) return; 54 | const filePath = vscode.window.activeTextEditor.document.uri.fsPath; 55 | const terminalName = "Run Zig Program"; 56 | const terminals = vscode.window.terminals.filter((t) => t.name === terminalName && getTerminalState(t) === false); 57 | const terminal = terminals.length > 0 ? terminals[0] : vscode.window.createTerminal(terminalName); 58 | const callOperator = /(powershell.exe$|powershell$|pwsh.exe$|pwsh$)/.test(vscode.env.shell) ? "& " : ""; 59 | terminal.show(); 60 | const wsFolder = getWorkspaceFolder(filePath); 61 | if (wsFolder && isWorkspaceFile(filePath) && hasBuildFile(wsFolder.uri.fsPath)) { 62 | terminal.sendText(`${callOperator}${escapePath(zigPath)} build run`); 63 | return; 64 | } 65 | terminal.sendText(`${callOperator}${escapePath(zigPath)} run ${escapePath(filePath)}`); 66 | } 67 | 68 | function escapePath(rawPath: string): string { 69 | if (/[ !"#$&'()*,;:<>?\[\\\]^`{|}]/.test(rawPath)) { 70 | return `"${rawPath.replaceAll('"', '"\\""')}"`; 71 | } 72 | return rawPath; 73 | } 74 | 75 | function hasBuildFile(workspaceFspath: string): boolean { 76 | const buildZigPath = path.join(workspaceFspath, "build.zig"); 77 | return fs.existsSync(buildZigPath); 78 | } 79 | 80 | async function zigDebug() { 81 | if (!vscode.window.activeTextEditor) return; 82 | const filePath = vscode.window.activeTextEditor.document.uri.fsPath; 83 | try { 84 | const workspaceFolder = getWorkspaceFolder(filePath); 85 | let binaryPath; 86 | if (workspaceFolder && isWorkspaceFile(filePath) && hasBuildFile(workspaceFolder.uri.fsPath)) { 87 | binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); 88 | } else { 89 | binaryPath = await buildDebugBinary(filePath); 90 | } 91 | if (!binaryPath) return; 92 | 93 | const config = vscode.workspace.getConfiguration("zig"); 94 | const debugAdapter = config.get("debugAdapter", "lldb"); 95 | 96 | const debugConfig: vscode.DebugConfiguration = { 97 | type: debugAdapter, 98 | name: `Debug Zig`, 99 | request: "launch", 100 | program: binaryPath, 101 | cwd: path.dirname(workspaceFolder?.uri.fsPath ?? path.dirname(filePath)), 102 | stopAtEntry: false, 103 | }; 104 | await vscode.debug.startDebugging(undefined, debugConfig); 105 | } catch (e) { 106 | if (e instanceof Error) { 107 | void vscode.window.showErrorMessage(`Failed to build debug binary: ${e.message}`); 108 | } else { 109 | void vscode.window.showErrorMessage(`Failed to build debug binary`); 110 | } 111 | } 112 | } 113 | 114 | async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { 115 | const zigPath = zigProvider.getZigPath(); 116 | if (!zigPath) return null; 117 | // Workaround because zig build doesn't support specifying the output binary name 118 | // `zig run` does support -femit-bin, but preferring `zig build` if possible 119 | const outputDir = path.join(workspacePath, "zig-out", "tmp-debug-build"); 120 | await execFile(zigPath, ["build", "--prefix", outputDir], { cwd: workspacePath }); 121 | const dirFiles = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(outputDir, "bin"))); 122 | const files = dirFiles.find(([, type]) => type === vscode.FileType.File); 123 | if (!files) { 124 | throw new Error("Unable to build debug binary"); 125 | } 126 | return path.join(outputDir, "bin", files[0]); 127 | } 128 | 129 | async function buildDebugBinary(filePath: string): Promise { 130 | const zigPath = zigProvider.getZigPath(); 131 | if (!zigPath) return null; 132 | const fileDirectory = path.dirname(filePath); 133 | const binaryName = `debug-${path.basename(filePath, ".zig")}`; 134 | const binaryPath = path.join(fileDirectory, "zig-out", "bin", binaryName); 135 | void vscode.workspace.fs.createDirectory(vscode.Uri.file(path.dirname(binaryPath))); 136 | 137 | await execFile(zigPath, ["run", filePath, `-femit-bin=${binaryPath}`], { cwd: fileDirectory }); 138 | return binaryPath; 139 | } 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.17 2 | - Fix invalid minising URL breaking package installs (@Techatrix) 3 | 4 | ## 0.6.16 5 | - Cache mirror and version list 6 | - This enables `zig.install` to work even if [ziglang.org](https://ziglang.org) is down 7 | - Fix regression with `zig.buildOnSaveArgs` config option not being sent to ZLS (@Techatrix) 8 | - Fix regression with URL path resolution disabling some mirrors (@Techatrix) 9 | 10 | ## 0.6.15 11 | - Reuse terminal created for running main function (@Anglebase) 12 | - Disable CodeLens for commented main functions and tests (@soorya-u) 13 | - Replace extension based build on save and unify config options with ZLS (@Techatrix) 14 | 15 | ## 0.6.14 16 | - Add config option for arguments used in debugging tests (@luehmann) 17 | - Disable workaround for [ziglang/zig#21905](https://github.com/ziglang/zig/issues/21905) 18 | on newer versions of the compiler (@Techatrix) 19 | 20 | ## 0.6.13 21 | - Resolve the correct tarball name for ZLS 0.15.0+ and arm (@Techatrix) 22 | - Make debug adapter used for tests and main configurable (@fred21O4) 23 | 24 | ## 0.6.12 25 | - Remove syntax highlighting for async and await keywords 26 | - Update minisign verification (@Techatrix) 27 | - Address various issues with the ZLS config middleware (@Techatrix) 28 | 29 | ## 0.6.11 30 | - Use Zig mirror list from ziglang.org 31 | 32 | ## 0.6.10 33 | - Fix installing Zig 0.14.1 with new tarball name format 34 | 35 | ## 0.6.9 36 | - Add config option for arguments used in test runner (@omissis) 37 | - Add toggleMultilineStringLiteral command (@devhawk) 38 | - Resolve file extensions and relative paths when looking up exe (@Techatrix) 39 | - Improve error messages when executable path could not be resolved (@Techatrix) 40 | - Prevent concurrent installations of exes that cause all of them to fail 41 | 42 | ## 0.6.8 43 | - Fix regression in using config options placeholders in paths. 44 | 45 | ## 0.6.7 46 | - Windows: prefer tar.exe in system32 over $PATH if available (@Techatrix) 47 | - Add a workaround for [ziglang/zig#21905](https://github.com/ziglang/zig/issues/21905) on MacOS and BSDs (@Techatrix) 48 | 49 | ## 0.6.5 50 | - Prevent the extension from overriding options in `zls.json` with default values (@Techatrix) 51 | - Fix various version management edge cases (@Techatrix) 52 | 53 | ## 0.6.4 54 | - Prevent `.zig-cache` files from being automatically revealed in the explorer (@Techatrix) 55 | - Add missing PowerShell call operator when using the run button (@BlueBlue21) 56 | - Update ZLS options (@Techatrix) 57 | - Look for zig in `$PATH` before defaulting to latest tagged release (@Techatrix) 58 | - Offer to update workspace config when selecting Zig version (@Techatrix) 59 | - Selecting a different Zig version is no longer permanent by default and will reset 60 | to the previous version after a restart. 61 | - Properly clean unused installations (@Techatrix) 62 | 63 | ## 0.6.3 64 | - Fix boolean ZLS options being ignored (@Techatrix) 65 | - Always refer to ZLS as "ZLS language server" (@Techatrix) 66 | - Ensure paths sent to terminal are properly escaped 67 | 68 | ## 0.6.2 69 | - Don't open every zig file in the workspace just to look for tests (@Techatrix) 70 | - Sync ZLS options (@Techatrix) 71 | - handle `zig.path` edge cases (@Techatrix) 72 | 73 | ## 0.6.1 74 | - Fix formatting not working when `zig.formattingProvider` was set to ZLS (@Techatrix) 75 | 76 | ## 0.6.0 77 | - Introduce a new fully featured version manager (@Techatrix) 78 | - Add Zig test runner provider (@babaldotdev) 79 | 80 | ## 0.5.9 81 | - Improve formatting provider implementation and default to using ZLS formatter (@Techatrix) 82 | - Sync ZLS options (@Techatrix) 83 | - Update ZLS install tool (@Techatrix) 84 | 85 | ## 0.5.8 86 | - Fix updating a nightly version of Zig to a tagged release 87 | 88 | ## 0.5.7 89 | - Remove `zig.zls.openopenconfig` (@Techatrix) 90 | - Automatically add `zig` to `$PATH` in the integrated terminal (@Techatrix) 91 | - Change `zig.path` and `zig.zls.path` `$PATH` lookup from empty string to executable name (@Techatrix) 92 | - The extension will handle the migration automatically 93 | - Remove ouput channel for formatting (@Techatrix) 94 | - `ast-check` already provides the same errors inline. 95 | - Allow predefined variables in all configuration options (@Jarred-Sumner) 96 | 97 | ## 0.5.6 98 | - Fix initial setup always being skippped (@Techatrix) 99 | 100 | ## 0.5.5 101 | - Fix `zig.install` when no project is open 102 | - Rework extension internals (@Techatrix) 103 | - Show progress while downloading updates (@Techatrix) 104 | - Link release notes in new Zig version notification 105 | 106 | ## 0.5.4 107 | - Fix incorrect comparisons that caused ZLS not to be started automatically (@SuperAuguste) 108 | - Ensure `zig.path` is valid in `zig.zls.install` (@unlsycn) 109 | 110 | ## 0.5.3 111 | - Fix checks on config values and versions 112 | - Fix diagnostics from Zig compiler provider (@Techatrix) 113 | - Ensure all commands are registered properly on extension startup 114 | 115 | ## 0.5.2 116 | - Update ZLS config even when Zig is not found 117 | - Disable autofix by default 118 | - Make `zig.zls.path` and `zig.path` scoped as `machine-overridable` (@alexrp) 119 | - Fix ZLS debug trace (@alexrp) 120 | - Default `zig.path` and `zig.zls.path` to look up in PATH (@alexrp) 121 | 122 | ## 0.5.1 123 | - Always use global configuration. 124 | 125 | ## 0.5.0 126 | - Rework initial setup and installation management 127 | - Add new zls hint settings (@leecannon) 128 | - Update zls settings 129 | - Fix C pointer highlighting (@tokyo4j) 130 | 131 | ## 0.4.3 132 | - Fix checking for ZLS updates 133 | - Always check `PATH` when `zigPath` is set to empty string 134 | - Fix build on save when ast check provider is ZLS 135 | - Delete old zls binary before renaming to avoid Windows permission error 136 | 137 | ## 0.4.2 138 | - Fix `Specify path` adding a leading slash on windows (@sebastianhoffmann) 139 | - Fix path given to `tar` being quoted 140 | - Add option to use `zig` found in `PATH` as `zigPath` 141 | 142 | ## 0.4.1 143 | - Fix formatting when `zigPath` includes spaces 144 | - Do not default to `zig` in `PATH` anymore 145 | 146 | ## 0.4.0 147 | - Prompt to install if prebuilt zls doesn't exist in specified path 148 | - Add `string` to the `name` of `@""` tokens 149 | - Add functionality to manage Zig installation 150 | 151 | ## 0.3.2 152 | - Make formatting provider option an enum (@alichraghi) 153 | - Only apply onEnterRules when line starts with whitespace 154 | - Highlight `.zon` files (@Techatrix) 155 | - Fix `zls` not restarting after having been updated on macOS (@ngrilly) 156 | - Support `${workspaceFolder}` in `zig.zls.path` (@Jarred-Sumner) 157 | - Make semantic token configuration an enum 158 | 159 | ## 0.3.1 160 | - Fix missing Linux AArch64 ZLS auto-installer support 161 | 162 | ## 0.3.0 163 | - Update syntax to Zig 0.10.x 164 | - Add support for optional [Zig Language Server](https://github.com/zigtools/zls) integration 165 | - Support `ast-check` diagnostics without language server integration 166 | - Minor fixes for existing extension features 167 | 168 | ## 0.2.5 169 | - Syntax updates (@Vexu) 170 | 171 | ## 0.2.4 172 | - Update syntax (@Vexu) 173 | - Fix provideCodeActions regression (@mxmn) 174 | - Add build-on-save setting (@Swoogan) 175 | - Add stderr to output panel (@Swoogan) 176 | - Add zig build to command palette (@Swoogan) 177 | 178 | Thanks to @Vexu for taking over keeping the project up to date. 179 | 180 | ## 0.2.3 181 | - Syntax updates 182 | - Improve diagnostics regex (@emekoi) 183 | - Fix eol on format (@emekoi) 184 | - Trim URI's to fix path issue (@emekoi) 185 | - Update unicode escape pattern match (@hryx) 186 | - Add configuration option for showing output channel on error (@not-fl3) 187 | 188 | ## 0.2.2 189 | - Add new usingnamespace keyword 190 | 191 | ## 0.2.1 192 | - Add correct filename to zig fmt output (@gernest) 193 | - Stop zig fmt error output taking focus on save (@CurtisFenner) 194 | 195 | ## 0.2.0 196 | - Syntax updates 197 | - Add built-in functions to syntax (@jakewakefield) 198 | - Add anyerror keyword (@Hejsil) 199 | - Add allowzero keyword (@emekoi) 200 | - Correctly find root of package using build.zig file (@gernest) 201 | - Use output channels for zig fmt error messages (@gernest) 202 | - Simplify defaults for automatic code-formatting (@hchac) 203 | 204 | ## 0.1.9 205 | - Highlight all bit size int types (@Hejsil) 206 | 207 | ## 0.1.8 16th July 2018 208 | - Add auto-formatting using `zig fmt` 209 | - Syntax updates 210 | 211 | ## 0.1.7 - 2nd March 2018 212 | - Async keyword updates 213 | - Build on save support (@Hejsil) 214 | 215 | ## 0.1.6 - 21st January 2018 216 | - Keyword updates for new zig 217 | - Basic linting functionality (@Hejsil) 218 | 219 | ## 0.1.5 - 23rd October 2017 220 | - Fix and/or word boundary display 221 | 222 | ## 0.1.4 - 23rd October 2017 223 | - Fix C string literals and allow escape characters (@scurest) 224 | 225 | ## 0.1.3 - 11th September 2017 226 | - Fix file extension 227 | 228 | ## 0.1.2 - 31st August 2017 229 | - Add new i2/u2 and align keywords 230 | 231 | ## 0.1.1 - 8th August 2017 232 | - Add new float/integer types 233 | 234 | ## 0.1.0 - 15th July 2017 235 | - Minimal syntax highlighting support 236 | -------------------------------------------------------------------------------- /src/zigUtil.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import childProcess from "child_process"; 4 | import fs from "fs"; 5 | import os from "os"; 6 | import path from "path"; 7 | 8 | import assert from "assert"; 9 | import { debounce } from "lodash-es"; 10 | import semver from "semver"; 11 | import which from "which"; 12 | 13 | /** 14 | * Replace any references to predefined variables in config string. 15 | * https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables 16 | */ 17 | export function handleConfigOption(input: string, workspaceFolder: vscode.WorkspaceFolder | "none" | "guess"): string { 18 | if (input.includes("${userHome}")) { 19 | input = input.replaceAll("${userHome}", os.homedir()); 20 | } 21 | 22 | if (workspaceFolder === "guess") { 23 | workspaceFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : "none"; 24 | } 25 | 26 | if (workspaceFolder !== "none") { 27 | input = input.replaceAll("${workspaceFolder}", workspaceFolder.uri.fsPath); 28 | input = input.replaceAll("${workspaceFolderBasename}", workspaceFolder.name); 29 | } else { 30 | // This may end up reporting a confusing error message. 31 | } 32 | 33 | const document = vscode.window.activeTextEditor?.document; 34 | if (document) { 35 | input = input.replaceAll("${file}", document.fileName); 36 | input = input.replaceAll("${fileBasename}", path.basename(document.fileName)); 37 | input = input.replaceAll( 38 | "${fileBasenameNoExtension}", 39 | path.basename(document.fileName, path.extname(document.fileName)), 40 | ); 41 | input = input.replaceAll("${fileExtname}", path.extname(document.fileName)); 42 | input = input.replaceAll("${fileDirname}", path.dirname(document.fileName)); 43 | input = input.replaceAll("${fileDirnameBasename}", path.basename(path.dirname(document.fileName))); 44 | } 45 | 46 | input = input.replaceAll("${pathSeparator}", path.sep); 47 | input = input.replaceAll("${/}", path.sep); 48 | if (input.includes("${cwd}")) { 49 | input = input.replaceAll("${cwd}", process.cwd()); 50 | } 51 | 52 | if (input.includes("${env:")) { 53 | for (let env = /\${env:([^}]+)}/.exec(input)?.[1]; env; env = /\${env:([^}]+)}/.exec(input)?.[1]) { 54 | input = input.replaceAll(`\${env:${env}}`, process.env[env] ?? ""); 55 | } 56 | } 57 | return input; 58 | } 59 | 60 | /** Resolves the absolute executable path and version of a program like Zig or ZLS. */ 61 | export function resolveExePathAndVersion( 62 | /** 63 | * - resolves '~' to the user home directory. 64 | * - resolves VS Code predefined variables. 65 | * - resolves possible executable file extensions on windows like '.exe' or '.cmd'. 66 | */ 67 | cmd: string, 68 | /** 69 | * The command-line argument that is used to query the version of the executable. 70 | * Zig uses `version`. ZLS uses `--version`. 71 | */ 72 | versionArg: string, 73 | ): { exe: string; version: semver.SemVer } | { message: string } { 74 | assert(cmd.length); 75 | 76 | // allow passing predefined variables 77 | cmd = handleConfigOption(cmd, "guess"); 78 | 79 | if (cmd.startsWith("~")) { 80 | cmd = path.join(os.homedir(), cmd.substring(1)); 81 | } 82 | 83 | const isWindows = os.platform() === "win32"; 84 | const isAbsolute = path.isAbsolute(cmd); 85 | const hasPathSeparator = !!/\//.exec(cmd) || (isWindows && !!/\\/.exec(cmd)); 86 | if (!isAbsolute && hasPathSeparator) { 87 | // A value like `./zig` would be looked up relative to the cwd of the VS Code process which makes little sense. 88 | return { 89 | message: `'${cmd}' is not valid. Use '$\{workspaceFolder}' to specify a path relative to the current workspace folder and '~' for the home directory.`, 90 | }; 91 | } 92 | 93 | const exePath = which.sync(cmd, { nothrow: true }); 94 | if (!exePath) { 95 | if (!isAbsolute) { 96 | return { message: `Could not find '${cmd}' in PATH.` }; 97 | } 98 | 99 | const stats = fs.statSync(cmd, { throwIfNoEntry: false }); 100 | if (!stats) { 101 | return { 102 | message: `'${cmd}' does not exist.`, 103 | }; 104 | } 105 | 106 | if (stats.isDirectory()) { 107 | return { 108 | message: `'${cmd}' is a directory and not an executable.`, 109 | }; 110 | } 111 | 112 | return { 113 | message: `'${cmd}' is not an executable.`, 114 | }; 115 | } 116 | 117 | const version = getVersion(exePath, versionArg); 118 | if (!version) return { message: `Failed to run '${exePath} ${versionArg}'.` }; 119 | return { exe: exePath, version: version }; 120 | } 121 | 122 | export function asyncDebounce Promise>>>( 123 | func: T, 124 | wait?: number, 125 | ): (...args: Parameters) => Promise>> { 126 | const debounced = debounce( 127 | (resolve: (value: Awaited>) => void, reject: (reason?: unknown) => void, args: Parameters) => { 128 | void func(...args) 129 | .then(resolve) 130 | .catch(reject); 131 | }, 132 | wait, 133 | ); 134 | return (...args) => 135 | new Promise((resolve, reject) => { 136 | debounced(resolve, reject, args); 137 | }); 138 | } 139 | 140 | /** 141 | * Wrapper around `vscode.WorkspaceConfiguration.update` that doesn't throw an exception. 142 | * A common cause of an exception is when the `settings.json` file is read-only or has unsaved changes. 143 | */ 144 | export async function workspaceConfigUpdateNoThrow( 145 | config: vscode.WorkspaceConfiguration, 146 | section: string, 147 | value: unknown, 148 | configurationTarget?: vscode.ConfigurationTarget | boolean | null, 149 | overrideInLanguage?: boolean, 150 | ): Promise { 151 | try { 152 | await config.update(section, value, configurationTarget, overrideInLanguage); 153 | } catch (err) { 154 | if (err instanceof Error) { 155 | void vscode.window.showErrorMessage(err.message); 156 | } else { 157 | void vscode.window.showErrorMessage("failed to update settings.json"); 158 | } 159 | } 160 | } 161 | 162 | // Check timestamp `key` to avoid automatically checking for updates 163 | // more than once in an hour. 164 | export async function shouldCheckUpdate(context: vscode.ExtensionContext, key: string): Promise { 165 | const HOUR = 60 * 60 * 1000; 166 | const timestamp = new Date().getTime(); 167 | const old = context.globalState.get(key); 168 | if (old === undefined || timestamp - old < HOUR) return false; 169 | await context.globalState.update(key, timestamp); 170 | return true; 171 | } 172 | 173 | export function getZigArchName(armName: string): string { 174 | switch (process.arch) { 175 | case "ia32": 176 | return "x86"; 177 | case "x64": 178 | return "x86_64"; 179 | case "arm": 180 | return armName; 181 | case "arm64": 182 | return "aarch64"; 183 | case "ppc": 184 | return "powerpc"; 185 | case "ppc64": 186 | return "powerpc64le"; 187 | case "loong64": 188 | return "loongarch64"; 189 | default: 190 | return process.arch; 191 | } 192 | } 193 | export function getZigOSName(): string { 194 | switch (process.platform) { 195 | case "darwin": 196 | return "macos"; 197 | case "win32": 198 | return "windows"; 199 | default: 200 | return process.platform; 201 | } 202 | } 203 | 204 | export function getVersion( 205 | filePath: string, 206 | /** 207 | * The command-line argument that is used to query the version of the executable. 208 | * Zig uses `version`. ZLS uses `--version`. 209 | */ 210 | arg: string, 211 | ): semver.SemVer | null { 212 | try { 213 | const wsFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; 214 | const buffer = childProcess.execFileSync(filePath, [arg], { cwd: wsFolder }); 215 | const versionString = buffer.toString("utf8").trim(); 216 | if (versionString === "0.2.0.83a2a36a") { 217 | // Zig 0.2.0 reports the version in a non-semver format 218 | return semver.parse("0.2.0"); 219 | } 220 | return semver.parse(versionString); 221 | } catch { 222 | return null; 223 | } 224 | } 225 | 226 | export interface ZigVersion { 227 | name: string; 228 | version: semver.SemVer; 229 | url: string; 230 | sha: string; 231 | notes?: string; 232 | isMach: boolean; 233 | } 234 | 235 | export type VersionIndex = Record< 236 | string, 237 | { 238 | version?: string; 239 | notes?: string; 240 | } & Record 241 | >; 242 | 243 | export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { 244 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); 245 | if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 246 | return vscode.workspace.workspaceFolders[0]; 247 | } 248 | return workspaceFolder; 249 | } 250 | 251 | export function isWorkspaceFile(filePath: string): boolean { 252 | const wsFolder = getWorkspaceFolder(filePath); 253 | if (!wsFolder) return false; 254 | return filePath.startsWith(wsFolder.uri.fsPath); 255 | } 256 | -------------------------------------------------------------------------------- /syntaxes/zig.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "zig", 4 | "scopeName": "source.zig", 5 | "fileTypes": ["zig", "zon"], 6 | "patterns": [ 7 | { 8 | "include": "#comments" 9 | }, 10 | { 11 | "include": "#strings" 12 | }, 13 | { 14 | "include": "#keywords" 15 | }, 16 | { 17 | "include": "#operators" 18 | }, 19 | { 20 | "include": "#punctuation" 21 | }, 22 | { 23 | "include": "#numbers" 24 | }, 25 | { 26 | "include": "#support" 27 | }, 28 | { 29 | "include": "#variables" 30 | } 31 | ], 32 | "repository": { 33 | "variables": { 34 | "patterns": [ 35 | { 36 | "name": "meta.function.declaration.zig", 37 | "patterns": [ 38 | { 39 | "match": "\\b(fn)\\s+([A-Z][a-zA-Z0-9]*)\\b", 40 | "captures": { 41 | "1": { 42 | "name": "storage.type.function.zig" 43 | }, 44 | "2": { 45 | "name": "entity.name.type.zig" 46 | } 47 | } 48 | }, 49 | { 50 | "match": "\\b(fn)\\s+([_a-zA-Z][_a-zA-Z0-9]*)\\b", 51 | "captures": { 52 | "1": { 53 | "name": "storage.type.function.zig" 54 | }, 55 | "2": { 56 | "name": "entity.name.function.zig" 57 | } 58 | } 59 | }, 60 | { 61 | "begin": "\\b(fn)\\s+@\"", 62 | "end": "\"", 63 | "name": "entity.name.function.string.zig", 64 | "beginCaptures": { 65 | "1": { 66 | "name": "storage.type.function.zig" 67 | } 68 | }, 69 | "patterns": [ 70 | { 71 | "include": "#stringcontent" 72 | } 73 | ] 74 | }, 75 | { 76 | "name": "keyword.default.zig", 77 | "match": "\\b(const|var|fn)\\b" 78 | } 79 | ] 80 | }, 81 | { 82 | "name": "meta.function.call.zig", 83 | "patterns": [ 84 | { 85 | "match": "([A-Z][a-zA-Z0-9]*)(?=\\s*\\()", 86 | "name": "entity.name.type.zig" 87 | }, 88 | { 89 | "match": "([_a-zA-Z][_a-zA-Z0-9]*)(?=\\s*\\()", 90 | "name": "entity.name.function.zig" 91 | } 92 | ] 93 | }, 94 | { 95 | "name": "meta.variable.zig", 96 | "patterns": [ 97 | { 98 | "match": "\\b[_a-zA-Z][_a-zA-Z0-9]*\\b", 99 | "name": "variable.zig" 100 | }, 101 | { 102 | "begin": "@\"", 103 | "end": "\"", 104 | "name": "variable.string.zig", 105 | "patterns": [ 106 | { 107 | "include": "#stringcontent" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ] 114 | }, 115 | "keywords": { 116 | "patterns": [ 117 | { 118 | "match": "\\binline\\b(?!\\s*\\bfn\\b)", 119 | "name": "keyword.control.repeat.zig" 120 | }, 121 | { 122 | "match": "\\b(while|for)\\b", 123 | "name": "keyword.control.repeat.zig" 124 | }, 125 | { 126 | "name": "keyword.storage.zig", 127 | "match": "\\b(extern|packed|export|pub|noalias|inline|comptime|volatile|align|linksection|threadlocal|allowzero|noinline|callconv)\\b" 128 | }, 129 | { 130 | "name": "keyword.structure.zig", 131 | "match": "\\b(struct|enum|union|opaque)\\b" 132 | }, 133 | { 134 | "name": "keyword.statement.zig", 135 | "match": "\\b(asm|unreachable)\\b" 136 | }, 137 | { 138 | "name": "keyword.control.flow.zig", 139 | "match": "\\b(break|return|continue|defer|errdefer)\\b" 140 | }, 141 | { 142 | "name": "keyword.control.async.zig", 143 | "match": "\\b(resume|suspend|nosuspend)\\b" 144 | }, 145 | { 146 | "name": "keyword.control.trycatch.zig", 147 | "match": "\\b(try|catch)\\b" 148 | }, 149 | { 150 | "name": "keyword.control.conditional.zig", 151 | "match": "\\b(if|else|switch|orelse)\\b" 152 | }, 153 | { 154 | "name": "keyword.constant.default.zig", 155 | "match": "\\b(null|undefined)\\b" 156 | }, 157 | { 158 | "name": "keyword.constant.bool.zig", 159 | "match": "\\b(true|false)\\b" 160 | }, 161 | { 162 | "name": "keyword.default.zig", 163 | "match": "\\b(test|and|or)\\b" 164 | }, 165 | { 166 | "name": "keyword.type.zig", 167 | "match": "\\b(bool|void|noreturn|type|error|anyerror|anyframe|anytype|anyopaque)\\b" 168 | }, 169 | { 170 | "name": "keyword.type.integer.zig", 171 | "match": "\\b(f16|f32|f64|f80|f128|u\\d+|i\\d+|isize|usize|comptime_int|comptime_float)\\b" 172 | }, 173 | { 174 | "name": "keyword.type.c.zig", 175 | "match": "\\b(c_char|c_short|c_ushort|c_int|c_uint|c_long|c_ulong|c_longlong|c_ulonglong|c_longdouble)\\b" 176 | } 177 | ] 178 | }, 179 | "operators": { 180 | "patterns": [ 181 | { 182 | "name": "keyword.operator.c-pointer.zig", 183 | "match": "(?<=\\[)\\*c(?=\\])" 184 | }, 185 | { 186 | "name": "keyword.operator.comparison.zig", 187 | "match": "(\\b(and|or)\\b)|(==|!=|<=|>=|<|>)" 188 | }, 189 | { 190 | "name": "keyword.operator.arithmetic.zig", 191 | "match": "(-%?|\\+%?|\\*%?|/|%)=?" 192 | }, 193 | { 194 | "name": "keyword.operator.bitwise.zig", 195 | "match": "(<<%?|>>|!|~|&|\\^|\\|)=?" 196 | }, 197 | { 198 | "name": "keyword.operator.special.zig", 199 | "match": "(==|\\+\\+|\\*\\*|->)" 200 | }, 201 | { 202 | "name": "keyword.operator.assignment.zig", 203 | "match": "=" 204 | }, 205 | { 206 | "name": "keyword.operator.question.zig", 207 | "match": "\\?" 208 | } 209 | ] 210 | }, 211 | "comments": { 212 | "patterns": [ 213 | { 214 | "name": "comment.line.documentation.zig", 215 | "begin": "//[!/](?=[^/])", 216 | "end": "$", 217 | "patterns": [ 218 | { 219 | "include": "#commentContents" 220 | } 221 | ] 222 | }, 223 | { 224 | "name": "comment.line.double-slash.zig", 225 | "begin": "//", 226 | "end": "$", 227 | "patterns": [ 228 | { 229 | "include": "#commentContents" 230 | } 231 | ] 232 | } 233 | ] 234 | }, 235 | "commentContents": { 236 | "patterns": [ 237 | { 238 | "match": "\\b(TODO|FIXME|XXX|NOTE)\\b:?", 239 | "name": "keyword.todo.zig" 240 | } 241 | ] 242 | }, 243 | "punctuation": { 244 | "patterns": [ 245 | { 246 | "name": "punctuation.accessor.zig", 247 | "match": "\\." 248 | }, 249 | { 250 | "name": "punctuation.comma.zig", 251 | "match": "," 252 | }, 253 | { 254 | "name": "punctuation.separator.key-value.zig", 255 | "match": ":" 256 | }, 257 | { 258 | "name": "punctuation.terminator.statement.zig", 259 | "match": ";" 260 | } 261 | ] 262 | }, 263 | "strings": { 264 | "patterns": [ 265 | { 266 | "name": "string.quoted.double.zig", 267 | "begin": "\"", 268 | "end": "\"", 269 | "patterns": [ 270 | { 271 | "include": "#stringcontent" 272 | } 273 | ] 274 | }, 275 | { 276 | "name": "string.multiline.zig", 277 | "begin": "\\\\\\\\", 278 | "end": "$" 279 | }, 280 | { 281 | "name": "string.quoted.single.zig", 282 | "match": "'([^'\\\\]|\\\\(x\\h{2}|[0-2][0-7]{,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.))'" 283 | } 284 | ] 285 | }, 286 | "stringcontent": { 287 | "patterns": [ 288 | { 289 | "name": "constant.character.escape.zig", 290 | "match": "\\\\([nrt'\"\\\\]|(x[0-9a-fA-F]{2})|(u\\{[0-9a-fA-F]+\\}))" 291 | }, 292 | { 293 | "name": "invalid.illegal.unrecognized-string-escape.zig", 294 | "match": "\\\\." 295 | } 296 | ] 297 | }, 298 | "numbers": { 299 | "patterns": [ 300 | { 301 | "name": "constant.numeric.hexfloat.zig", 302 | "match": "\\b0x[0-9a-fA-F][0-9a-fA-F_]*(\\.[0-9a-fA-F][0-9a-fA-F_]*)?([pP][+-]?[0-9a-fA-F_]+)?\\b" 303 | }, 304 | { 305 | "name": "constant.numeric.float.zig", 306 | "match": "\\b[0-9][0-9_]*(\\.[0-9][0-9_]*)?([eE][+-]?[0-9_]+)?\\b" 307 | }, 308 | { 309 | "name": "constant.numeric.decimal.zig", 310 | "match": "\\b[0-9][0-9_]*\\b" 311 | }, 312 | { 313 | "name": "constant.numeric.hexadecimal.zig", 314 | "match": "\\b0x[a-fA-F0-9_]+\\b" 315 | }, 316 | { 317 | "name": "constant.numeric.octal.zig", 318 | "match": "\\b0o[0-7_]+\\b" 319 | }, 320 | { 321 | "name": "constant.numeric.binary.zig", 322 | "match": "\\b0b[01_]+\\b" 323 | }, 324 | { 325 | "name": "constant.numeric.invalid.zig", 326 | "match": "\\b[0-9](([eEpP][+-])|[0-9a-zA-Z_])*(\\.(([eEpP][+-])|[0-9a-zA-Z_])*)?([eEpP][+-])?[0-9a-zA-Z_]*\\b" 327 | } 328 | ] 329 | }, 330 | "support": { 331 | "patterns": [ 332 | { 333 | "comment": "Built-in functions", 334 | "name": "support.function.builtin.zig", 335 | "match": "@[_a-zA-Z][_a-zA-Z0-9]*" 336 | } 337 | ] 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/zigTestRunnerProvider.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import childProcess from "child_process"; 4 | import path from "path"; 5 | import util from "util"; 6 | 7 | import { DebouncedFunc, throttle } from "lodash-es"; 8 | 9 | import { getWorkspaceFolder, isWorkspaceFile, workspaceConfigUpdateNoThrow } from "./zigUtil"; 10 | import { zigProvider } from "./zigSetup"; 11 | 12 | const execFile = util.promisify(childProcess.execFile); 13 | 14 | export default class ZigTestRunnerProvider { 15 | private testController: vscode.TestController; 16 | private updateTestItems: DebouncedFunc<(document: vscode.TextDocument) => void>; 17 | 18 | constructor() { 19 | this.updateTestItems = throttle( 20 | (document: vscode.TextDocument) => { 21 | this._updateTestItems(document); 22 | }, 23 | 500, 24 | { trailing: true }, 25 | ); 26 | 27 | this.testController = vscode.tests.createTestController("zigTestController", "Zig Tests"); 28 | this.testController.createRunProfile("Run", vscode.TestRunProfileKind.Run, this.runTests.bind(this), true); 29 | this.testController.createRunProfile( 30 | "Debug", 31 | vscode.TestRunProfileKind.Debug, 32 | this.debugTests.bind(this), 33 | false, 34 | ); 35 | } 36 | 37 | public activate(subscriptions: vscode.Disposable[]) { 38 | subscriptions.push( 39 | vscode.workspace.onDidOpenTextDocument((document) => { 40 | this.updateTestItems(document); 41 | }), 42 | vscode.workspace.onDidCloseTextDocument((document) => { 43 | if (!isWorkspaceFile(document.uri.fsPath)) this.deleteTestForAFile(document.uri); 44 | }), 45 | vscode.workspace.onDidChangeTextDocument((change) => { 46 | this.updateTestItems(change.document); 47 | }), 48 | vscode.workspace.onDidDeleteFiles((event) => { 49 | event.files.forEach((file) => { 50 | this.deleteTestForAFile(file); 51 | }); 52 | }), 53 | vscode.workspace.onDidRenameFiles((event) => { 54 | event.files.forEach((file) => { 55 | this.deleteTestForAFile(file.oldUri); 56 | }); 57 | }), 58 | ); 59 | } 60 | 61 | private deleteTestForAFile(uri: vscode.Uri) { 62 | this.testController.items.forEach((item) => { 63 | if (!item.uri) return; 64 | if (item.uri.fsPath === uri.fsPath) { 65 | this.testController.items.delete(item.id); 66 | } 67 | }); 68 | } 69 | 70 | private _updateTestItems(textDocument: vscode.TextDocument) { 71 | if (textDocument.languageId !== "zig") return; 72 | 73 | const regex = /^(?![ \t]*\/\/\/?\s*)[ \t]*\btest\s+(?:"([^"]+)"|([A-Za-z0-9_]\w*)|@"([^"]+)")\s*\{/gm; 74 | 75 | const matches = Array.from(textDocument.getText().matchAll(regex)); 76 | this.deleteTestForAFile(textDocument.uri); 77 | 78 | for (const match of matches) { 79 | const testDesc = match[1] || match[2] || match[3]; 80 | const isDocTest = !match[1]; 81 | const position = textDocument.positionAt(match.index); 82 | const line = textDocument.lineAt(position.line); 83 | const range = textDocument.lineAt(line.lineNumber).range; 84 | 85 | const fileName = path.basename(textDocument.uri.fsPath); 86 | 87 | // Add doctest prefix to handle scenario where test name matches one with non doctest. E.g `test foo` and `test "foo"` 88 | const testItem = this.testController.createTestItem( 89 | `${fileName}.test.${isDocTest ? "doctest." : ""}${testDesc}`, // Test id needs to be unique, so adding file name prefix 90 | `${fileName} - ${testDesc}`, 91 | textDocument.uri, 92 | ); 93 | testItem.range = range; 94 | this.testController.items.add(testItem); 95 | } 96 | } 97 | 98 | private async runTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) { 99 | const run = this.testController.createTestRun(request); 100 | // request.include will have individual test when we run test from gutter icon 101 | // if test is run from test explorer, request.include will be undefined and we run all tests that are active 102 | for (const item of request.include ?? this.testController.items) { 103 | if (token.isCancellationRequested) break; 104 | const testItem = Array.isArray(item) ? item[1] : item; 105 | 106 | run.started(testItem); 107 | const start = new Date(); 108 | run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); 109 | const { output, success } = await this.runTest(testItem); 110 | run.appendOutput(output.replaceAll("\n", "\r\n")); 111 | run.appendOutput("\r\n"); 112 | const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); 113 | 114 | if (!success) { 115 | run.failed(testItem, new vscode.TestMessage(output), elapsed); 116 | } else { 117 | run.passed(testItem, elapsed); 118 | } 119 | } 120 | run.end(); 121 | } 122 | 123 | private async runTest(test: vscode.TestItem): Promise<{ output: string; success: boolean }> { 124 | const config = vscode.workspace.getConfiguration("zig"); 125 | const zigPath = zigProvider.getZigPath(); 126 | if (!zigPath) { 127 | return { output: "Unable to run test without Zig", success: false }; 128 | } 129 | if (test.uri === undefined) { 130 | return { output: "Unable to determine file location", success: false }; 131 | } 132 | 133 | const testUri = test.uri; 134 | const wsFolder = getWorkspaceFolder(testUri.fsPath)?.uri.fsPath ?? path.dirname(testUri.fsPath); 135 | 136 | const parts = test.id.split("."); 137 | const lastPart = parts[parts.length - 1]; 138 | 139 | const testArgsConf = config.get("testArgs") ?? []; 140 | const args: string[] = 141 | testArgsConf.length > 0 142 | ? testArgsConf.map((v) => v.replace("${filter}", lastPart).replace("${path}", testUri.fsPath)) 143 | : []; 144 | 145 | try { 146 | const { stderr: output } = await execFile(zigPath, args, { cwd: wsFolder }); 147 | 148 | return { output: output.replaceAll("\n", "\r\n"), success: true }; 149 | } catch (e) { 150 | if (e instanceof Error) { 151 | if ( 152 | config.get("testArgs")?.toString() === 153 | config.inspect("testArgs")?.defaultValue?.toString() && 154 | (e.message.includes("no module named") || e.message.includes("import of file outside module path")) 155 | ) { 156 | void vscode.window 157 | .showInformationMessage("Use build script to run tests?", "Yes", "No") 158 | .then(async (response) => { 159 | if (response === "Yes") { 160 | await workspaceConfigUpdateNoThrow( 161 | config, 162 | "testArgs", 163 | ["build", "test", "-Dtest-filter=${filter}"], 164 | false, 165 | ); 166 | void vscode.commands.executeCommand( 167 | "workbench.action.openWorkspaceSettings", 168 | "@id:zig.testArgs", 169 | ); 170 | } 171 | }); 172 | } 173 | 174 | return { output: e.message.replaceAll("\n", "\r\n"), success: false }; 175 | } else { 176 | return { output: "Failed to run test\r\n", success: false }; 177 | } 178 | } 179 | } 180 | 181 | private async debugTests(req: vscode.TestRunRequest, token: vscode.CancellationToken) { 182 | const run = this.testController.createTestRun(req); 183 | for (const item of req.include ?? this.testController.items) { 184 | if (token.isCancellationRequested) break; 185 | const test = Array.isArray(item) ? item[1] : item; 186 | run.started(test); 187 | const start = new Date(); 188 | run.appendOutput(`[${start.toISOString()}] Running test: ${test.label}\r\n`); 189 | try { 190 | const [exitCode, output] = await this.debugTest(run, test); 191 | run.appendOutput(output.replaceAll("\n", "\r\n")); 192 | run.appendOutput("\r\n"); 193 | const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); 194 | if (exitCode === 0) { 195 | run.passed(test, elapsed); 196 | } else { 197 | run.failed(test, new vscode.TestMessage(output)); 198 | } 199 | } catch (e) { 200 | const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); 201 | run.failed(test, new vscode.TestMessage((e as Error).message), elapsed); 202 | } 203 | } 204 | run.end(); 205 | } 206 | 207 | private async debugTest(run: vscode.TestRun, testItem: vscode.TestItem): Promise<[number, string]> { 208 | if (testItem.uri === undefined) { 209 | throw new Error("Unable to determine file location"); 210 | } 211 | 212 | const config = vscode.workspace.getConfiguration("zig"); 213 | const debugAdapter = config.get("debugAdapter", "lldb"); 214 | 215 | const testBinaryPath = await this.buildTestBinary(run, testItem.uri.fsPath, getTestDesc(testItem)); 216 | const debugConfig: vscode.DebugConfiguration = { 217 | type: debugAdapter, 218 | name: `Debug ${testItem.label}`, 219 | request: "launch", 220 | program: testBinaryPath, 221 | cwd: path.dirname(testItem.uri.fsPath), 222 | stopAtEntry: false, 223 | }; 224 | return new Promise((resolve, reject) => { 225 | const disposables: vscode.Disposable[] = []; 226 | let exitCode = 0; 227 | let output = ""; 228 | 229 | vscode.debug.onDidTerminateDebugSession((session) => { 230 | if (session.name === debugConfig.name) { 231 | for (const disposable of disposables) { 232 | disposable.dispose(); 233 | } 234 | resolve([exitCode, output]); 235 | } 236 | }, disposables); 237 | 238 | type Message = 239 | | { type: "event"; event: "output"; body: { output: string } } 240 | | { type: "event"; event: "exited"; body: { exitCode: number } } 241 | | { type: "response" }; 242 | disposables.push( 243 | vscode.debug.registerDebugAdapterTrackerFactory(debugAdapter, { 244 | createDebugAdapterTracker() { 245 | return { 246 | onDidSendMessage: (m: Message) => { 247 | if (m.type === "event" && m.event === "output") { 248 | output += m.body.output; 249 | } 250 | if (m.type === "event" && m.event === "exited") { 251 | exitCode = m.body.exitCode; 252 | } 253 | }, 254 | }; 255 | }, 256 | }), 257 | ); 258 | 259 | vscode.debug.startDebugging(undefined, debugConfig).then( 260 | (success) => { 261 | if (!success) { 262 | for (const disposable of disposables) { 263 | disposable.dispose(); 264 | } 265 | reject(new Error("Failed to start debug session")); 266 | } 267 | }, 268 | (err: unknown) => { 269 | for (const disposable of disposables) { 270 | disposable.dispose(); 271 | } 272 | reject(err as Error); 273 | }, 274 | ); 275 | }); 276 | } 277 | 278 | private async buildTestBinary(run: vscode.TestRun, testFilePath: string, testDesc: string): Promise { 279 | const config = vscode.workspace.getConfiguration("zig"); 280 | const zigPath = zigProvider.getZigPath(); 281 | if (!zigPath) { 282 | throw new Error("Unable to build test binary without Zig"); 283 | } 284 | 285 | const wsFolder = getWorkspaceFolder(testFilePath)?.uri.fsPath ?? path.dirname(testFilePath); 286 | const outputDir = path.join(wsFolder, "zig-out", "bin"); 287 | const binaryPath = path.join(outputDir, "debug-unit-tests"); 288 | await vscode.workspace.fs.createDirectory(vscode.Uri.file(outputDir)); 289 | 290 | const debugTestArgsConf = config.get("debugTestArgs") ?? []; 291 | const args = debugTestArgsConf.map((arg) => 292 | arg.replace("${filter}", testDesc).replace("${path}", testFilePath).replace("${binaryPath}", binaryPath), 293 | ); 294 | 295 | const { stdout, stderr } = await execFile(zigPath, args, { cwd: wsFolder }); 296 | if (stderr) { 297 | run.appendOutput(stderr.replaceAll("\n", "\r\n")); 298 | throw new Error(`Failed to build test binary: ${stderr}`); 299 | } 300 | run.appendOutput(stdout.replaceAll("\n", "\r\n")); 301 | return binaryPath; 302 | } 303 | } 304 | 305 | function getTestDesc(testItem: vscode.TestItem): string { 306 | const parts = testItem.id.split("."); 307 | return parts[parts.length - 1]; 308 | } 309 | -------------------------------------------------------------------------------- /src/versionManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A version manager for Zig and ZLS. 3 | */ 4 | 5 | import vscode from "vscode"; 6 | 7 | import childProcess from "child_process"; 8 | import fs from "fs"; 9 | import util from "util"; 10 | import which from "which"; 11 | 12 | import semver from "semver"; 13 | 14 | import * as minisign from "./minisign"; 15 | import * as zigUtil from "./zigUtil"; 16 | 17 | const execFile = util.promisify(childProcess.execFile); 18 | const chmod = util.promisify(fs.chmod); 19 | 20 | /** The maxmimum number of installation that can be store until they will be removed */ 21 | const maxInstallCount = 5; 22 | /** Maps concurrent requests to install a version of an exe to a single promise */ 23 | const inProgressInstalls = new Map>(); 24 | 25 | export interface Config { 26 | context: vscode.ExtensionContext; 27 | /** The name of the application. */ 28 | title: string; 29 | /** The name of the executable file. */ 30 | exeName: string; 31 | minisignKey: minisign.Key; 32 | /** The command-line argument that should passed to `tar` to exact the tarball. */ 33 | extraTarArgs: string[]; 34 | /** 35 | * The command-line argument that should passed to the executable to query the version. 36 | * `"version"` for Zig, `"--version"` for ZLS 37 | */ 38 | versionArg: string; 39 | getMirrorUrls: () => Promise; 40 | canonicalUrl: { 41 | release: vscode.Uri; 42 | nightly: vscode.Uri; 43 | }; 44 | /** 45 | * Get the artifact file name for a specific version. 46 | * 47 | * Example: 48 | * - `zig-x86_64-windows-0.14.1.zip` 49 | * - `zls-linux-x86_64-0.14.0.tar.xz` 50 | */ 51 | getArtifactName: (version: semver.SemVer) => string; 52 | } 53 | 54 | /** Returns the path to the executable */ 55 | export async function install(config: Config, version: semver.SemVer): Promise { 56 | const key = config.exeName + version.raw; 57 | const entry = inProgressInstalls.get(key); 58 | if (entry) { 59 | return await entry; 60 | } 61 | 62 | const promise = installGuarded(config, version); 63 | inProgressInstalls.set(key, promise); 64 | 65 | return await promise.finally(() => { 66 | inProgressInstalls.delete(key); 67 | }); 68 | } 69 | 70 | async function installGuarded(config: Config, version: semver.SemVer): Promise { 71 | const exeName = config.exeName + (process.platform === "win32" ? ".exe" : ""); 72 | const subDirName = `${getTargetName()}-${version.raw}`; 73 | const exeUri = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName, subDirName, exeName); 74 | 75 | await setLastAccessTime(config, version); 76 | 77 | try { 78 | await vscode.workspace.fs.stat(exeUri); 79 | return exeUri.fsPath; 80 | } catch (e) { 81 | if (e instanceof vscode.FileSystemError) { 82 | if (e.code !== "FileNotFound") { 83 | throw e; 84 | } 85 | // go ahead an install 86 | } else { 87 | throw e; 88 | } 89 | } 90 | 91 | const tarPath = await getTarExePath(); 92 | if (!tarPath) { 93 | throw new Error(`Can't install ${config.title} because 'tar' could not be found`); 94 | } 95 | 96 | const mirrors = [...(await config.getMirrorUrls())] 97 | .map((mirror) => ({ mirror, sort: Math.random() })) 98 | .sort((a, b) => a.sort - b.sort) 99 | .map(({ mirror }) => mirror); 100 | 101 | return await vscode.window.withProgress( 102 | { 103 | title: `Installing ${config.title} ${version.toString()}`, 104 | location: vscode.ProgressLocation.Notification, 105 | cancellable: true, 106 | }, 107 | async (progress, cancelToken) => { 108 | for (const mirrorUrl of mirrors) { 109 | const mirrorName = new URL(mirrorUrl.toString()).host; 110 | try { 111 | return await installFromMirror( 112 | config, 113 | version, 114 | mirrorUrl, 115 | mirrorName, 116 | tarPath, 117 | progress, 118 | cancelToken, 119 | ); 120 | } catch {} 121 | } 122 | 123 | const canonicalUrl = 124 | version.prerelease.length === 0 ? config.canonicalUrl.release : config.canonicalUrl.nightly; 125 | const mirrorName = new URL(canonicalUrl.toString()).host; 126 | return await installFromMirror(config, version, canonicalUrl, mirrorName, tarPath, progress, cancelToken); 127 | }, 128 | ); 129 | } 130 | 131 | /** Returns the path to the executable */ 132 | async function installFromMirror( 133 | config: Config, 134 | version: semver.SemVer, 135 | mirrorUrl: vscode.Uri, 136 | mirrorName: string, 137 | tarPath: string, 138 | progress: vscode.Progress<{ 139 | message?: string; 140 | increment?: number; 141 | }>, 142 | cancelToken: vscode.CancellationToken, 143 | ): Promise { 144 | progress.report({ message: `trying ${mirrorName}` }); 145 | 146 | const isWindows = process.platform === "win32"; 147 | const exeName = config.exeName + (isWindows ? ".exe" : ""); 148 | const subDirName = `${getTargetName()}-${version.raw}`; 149 | const fileName = config.getArtifactName(version); 150 | 151 | const installDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName, subDirName); 152 | const exeUri = vscode.Uri.joinPath(installDir, exeName); 153 | const tarballUri = vscode.Uri.joinPath(installDir, fileName); 154 | 155 | const abortController = new AbortController(); 156 | cancelToken.onCancellationRequested(() => { 157 | abortController.abort(); 158 | }); 159 | 160 | const artifactUrl = new URL(fileName, mirrorUrl.toString() + (mirrorUrl.path.endsWith("/") ? "" : "/")); 161 | const artifactMinisignUrl = new URL(`${artifactUrl.toString()}.minisig`); 162 | 163 | /** 164 | * https://ziglang.org/download/community-mirrors/ encouraged the addition 165 | * of a `source` query parameter to specify what is making this request. 166 | * This extension is published as `ziglang.vscode-zig` so we use base it off that. 167 | */ 168 | artifactUrl.searchParams.set("source", "ziglang-vscode-zig"); 169 | artifactMinisignUrl.searchParams.set("source", "ziglang-vscode-zig"); 170 | 171 | const signatureResponse = await fetch(artifactMinisignUrl, { 172 | signal: abortController.signal, 173 | }); 174 | 175 | if (signatureResponse.status !== 200) { 176 | throw new Error(`${signatureResponse.statusText} (${signatureResponse.status.toString()})`); 177 | } 178 | 179 | let artifactResponse = await fetch(artifactUrl, { 180 | signal: abortController.signal, 181 | }); 182 | 183 | if (artifactResponse.status !== 200) { 184 | throw new Error(`${artifactResponse.statusText} (${artifactResponse.status.toString()})`); 185 | } 186 | 187 | progress.report({ message: `downloading from ${mirrorName}` }); 188 | 189 | const signatureData = Buffer.from(await signatureResponse.arrayBuffer()); 190 | 191 | let contentLength = artifactResponse.headers.has("content-length") 192 | ? Number(artifactResponse.headers.get("content-length")) 193 | : null; 194 | if (!Number.isFinite(contentLength)) contentLength = null; 195 | 196 | if (contentLength) { 197 | let receivedLength = 0; 198 | const progressStream = new TransformStream<{ length: number }>({ 199 | transform(chunk, controller) { 200 | receivedLength += chunk.length; 201 | const increment = (chunk.length / contentLength) * 100; 202 | const currentProgress = (receivedLength / contentLength) * 100; 203 | progress.report({ 204 | message: `downloading tarball ${currentProgress.toFixed()}%`, 205 | increment: increment, 206 | }); 207 | controller.enqueue(chunk); 208 | }, 209 | }); 210 | artifactResponse = new Response(artifactResponse.body?.pipeThrough(progressStream)); 211 | } 212 | const artifactData = Buffer.from(await artifactResponse.arrayBuffer()); 213 | 214 | progress.report({ message: "Verifying Signature..." }); 215 | 216 | await minisign.ready; 217 | 218 | const signature = minisign.parseSignature(signatureData); 219 | if (!minisign.verifySignature(config.minisignKey, signature, artifactData)) { 220 | throw new Error(`signature verification failed for '${artifactUrl.toString()}'`); 221 | } 222 | 223 | const match = /^timestamp:\d+\s+file:([^\s]+)\s+hashed$/.test(signature.trustedComment.toString()); 224 | if (!match) { 225 | throw new Error(`filename verification failed for '${artifactUrl.toString()}'`); 226 | } 227 | 228 | progress.report({ message: "Extracting..." }); 229 | 230 | try { 231 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 232 | } catch {} 233 | 234 | try { 235 | await vscode.workspace.fs.createDirectory(installDir); 236 | await vscode.workspace.fs.writeFile(tarballUri, artifactData); 237 | await execFile(tarPath, ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(config.extraTarArgs), { 238 | signal: abortController.signal, 239 | timeout: 60000, // 60 seconds 240 | }); 241 | } catch (err) { 242 | try { 243 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 244 | } catch {} 245 | if (err instanceof Error) { 246 | throw new Error(`Failed to extract ${config.title} tarball: ${err.message}`); 247 | } else { 248 | throw err; 249 | } 250 | } finally { 251 | try { 252 | await vscode.workspace.fs.delete(tarballUri, { useTrash: false }); 253 | } catch {} 254 | } 255 | 256 | const exeVersion = zigUtil.getVersion(exeUri.fsPath, config.versionArg); 257 | if (!exeVersion || exeVersion.compare(version) !== 0) { 258 | try { 259 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 260 | } catch {} 261 | // a mirror may provide the wrong version 262 | throw new Error(`Failed to validate version of ${config.title} installation!`); 263 | } 264 | 265 | await chmod(exeUri.fsPath, 0o755); 266 | 267 | try { 268 | await removeUnusedInstallations(config); 269 | } catch (err) { 270 | if (err instanceof Error) { 271 | void vscode.window.showWarningMessage( 272 | `Failed to uninstall unused ${config.title} versions: ${err.message}`, 273 | ); 274 | } else { 275 | void vscode.window.showWarningMessage(`Failed to uninstall unused ${config.title} versions`); 276 | } 277 | } 278 | 279 | return exeUri.fsPath; 280 | } 281 | 282 | /** Returns all locally installed versions */ 283 | export async function query(config: Config): Promise { 284 | const available: semver.SemVer[] = []; 285 | const prefix = getTargetName(); 286 | 287 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 288 | try { 289 | for (const [name] of await vscode.workspace.fs.readDirectory(storageDir)) { 290 | if (name.startsWith(prefix)) { 291 | available.push(new semver.SemVer(name.substring(prefix.length + 1))); 292 | } 293 | } 294 | } catch (e) { 295 | if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") { 296 | return []; 297 | } 298 | throw e; 299 | } 300 | 301 | return available; 302 | } 303 | 304 | async function getTarExePath(): Promise { 305 | if (process.platform === "win32" && process.env["SYSTEMROOT"]) { 306 | // We may be running from within Git Bash which adds GNU tar to 307 | // the $PATH but we need bsdtar to extract zip files so we look 308 | // in the system directory before falling back to the $PATH. 309 | // See https://github.com/ziglang/vscode-zig/issues/382 310 | const tarPath = `${process.env["SYSTEMROOT"]}\\system32\\tar.exe`; 311 | try { 312 | await vscode.workspace.fs.stat(vscode.Uri.file(tarPath)); 313 | return tarPath; 314 | } catch {} 315 | } 316 | return await which("tar", { nothrow: true }); 317 | } 318 | 319 | /** Set the last access time of the (installed) version. */ 320 | async function setLastAccessTime(config: Config, version: semver.SemVer): Promise { 321 | await config.context.globalState.update( 322 | `${config.exeName}-last-access-time-${getTargetName()}-${version.raw}`, 323 | Date.now(), 324 | ); 325 | } 326 | 327 | /** Remove installations with the oldest last access time until at most `VersionManager.maxInstallCount` versions remain. */ 328 | async function removeUnusedInstallations(config: Config) { 329 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 330 | 331 | const keys: { key: string; installDir: vscode.Uri; lastAccessTime: number }[] = []; 332 | 333 | try { 334 | for (const [name, fileType] of await vscode.workspace.fs.readDirectory(storageDir)) { 335 | const key = `${config.exeName}-last-access-time-${name}`; 336 | const uri = vscode.Uri.joinPath(storageDir, name); 337 | const lastAccessTime = config.context.globalState.get(key); 338 | 339 | if (!lastAccessTime || fileType !== vscode.FileType.Directory) { 340 | await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false }); 341 | } else { 342 | keys.push({ 343 | key: key, 344 | installDir: uri, 345 | lastAccessTime: lastAccessTime, 346 | }); 347 | } 348 | } 349 | } catch (e) { 350 | if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") return; 351 | throw e; 352 | } 353 | 354 | keys.sort((lhs, rhs) => rhs.lastAccessTime - lhs.lastAccessTime); 355 | 356 | for (const item of keys.slice(maxInstallCount)) { 357 | await vscode.workspace.fs.delete(item.installDir, { recursive: true, useTrash: false }); 358 | await config.context.globalState.update(item.key, undefined); 359 | } 360 | } 361 | 362 | /** Remove after some time has passed from the prefix change. */ 363 | export async function convertOldInstallPrefixes(config: Config): Promise { 364 | const oldPrefix = `${zigUtil.getZigOSName()}-${zigUtil.getZigArchName("armv7a")}`; 365 | const newPrefix = getTargetName(); 366 | 367 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 368 | try { 369 | for (const [name] of await vscode.workspace.fs.readDirectory(storageDir)) { 370 | if (!name.startsWith(oldPrefix)) continue; 371 | 372 | const version = name.substring(oldPrefix.length + 1); 373 | const oldInstallDir = vscode.Uri.joinPath(storageDir, name); 374 | const newInstallDir = vscode.Uri.joinPath(storageDir, `${newPrefix}-${version}`); 375 | try { 376 | await vscode.workspace.fs.rename(oldInstallDir, newInstallDir); 377 | } catch { 378 | // A possible cause could be that the user downgraded the extension 379 | // version to install it to the old install prefix while it was 380 | // already present with the new prefix. 381 | } 382 | 383 | const oldKey = `${config.exeName}-last-access-time-${oldPrefix}-${version}`; 384 | const newKey = `${config.exeName}-last-access-time-${newPrefix}-${version}`; 385 | const lastAccessTime = config.context.globalState.get(oldKey); 386 | if (lastAccessTime !== undefined) { 387 | config.context.globalState.update(newKey, lastAccessTime); 388 | config.context.globalState.update(oldKey, undefined); 389 | } 390 | } 391 | } catch {} 392 | } 393 | 394 | function getTargetName(): string { 395 | return `${zigUtil.getZigArchName("armv7a")}-${zigUtil.getZigOSName()}`; 396 | } 397 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-zig", 3 | "displayName": "Zig Language", 4 | "description": "Language support for the Zig programming language", 5 | "version": "0.6.17", 6 | "publisher": "ziglang", 7 | "icon": "images/zig-icon.png", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ziglang/vscode-zig" 12 | }, 13 | "engines": { 14 | "vscode": "^1.90.0" 15 | }, 16 | "categories": [ 17 | "Programming Languages" 18 | ], 19 | "activationEvents": [ 20 | "workspaceContains:build.zig", 21 | "workspaceContains:build.zig.zon", 22 | "workspaceContains:./.zigversion" 23 | ], 24 | "main": "./out/extension", 25 | "contributes": { 26 | "configurationDefaults": { 27 | "[zig]": { 28 | "editor.formatOnSave": true, 29 | "editor.defaultFormatter": "ziglang.vscode-zig", 30 | "editor.stickyScroll.defaultModel": "foldingProviderModel", 31 | "files.eol": "\n" 32 | }, 33 | "explorer.autoRevealExclude": { 34 | "**/.zig-cache": true, 35 | "**/zig-cache": true 36 | } 37 | }, 38 | "languages": [ 39 | { 40 | "id": "zig", 41 | "extensions": [ 42 | ".zig", 43 | ".zon" 44 | ], 45 | "aliases": [ 46 | "Zig" 47 | ], 48 | "configuration": "./language-configuration.json" 49 | } 50 | ], 51 | "grammars": [ 52 | { 53 | "language": "zig", 54 | "scopeName": "source.zig", 55 | "path": "./syntaxes/zig.tmLanguage.json" 56 | } 57 | ], 58 | "problemMatchers": [ 59 | { 60 | "name": "zig", 61 | "owner": "zig", 62 | "fileLocation": [ 63 | "relative", 64 | "${workspaceFolder}" 65 | ], 66 | "pattern": { 67 | "regexp": "([^\\s]*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(note|error):\\s+(.*)$", 68 | "file": 1, 69 | "line": 2, 70 | "column": 3, 71 | "severity": 4, 72 | "message": 5 73 | }, 74 | "background": { 75 | "activeOnStart": true, 76 | "beginsPattern": "^Build Summary:", 77 | "endsPattern": "^error: (\\d+ compilation errors|the following command failed with )" 78 | } 79 | } 80 | ], 81 | "taskDefinitions": [ 82 | { 83 | "type": "zig" 84 | } 85 | ], 86 | "configuration": { 87 | "type": "object", 88 | "title": "Zig", 89 | "properties": { 90 | "zig.path": { 91 | "scope": "machine-overridable", 92 | "type": "string", 93 | "description": "Set a custom path to the `zig` executable. Example: `C:/zig-windows-x86_64-0.13.0/zig.exe`. The string \"zig\" means lookup zig in PATH." 94 | }, 95 | "zig.version": { 96 | "scope": "resource", 97 | "type": "string", 98 | "description": "Specify which Zig version should be installed. Takes priority over a `.zigversion` file or a `build.zig.zon` with `minimum_zig_version`." 99 | }, 100 | "zig.formattingProvider": { 101 | "scope": "resource", 102 | "type": "string", 103 | "description": "Whether to enable formatting", 104 | "enum": [ 105 | "off", 106 | "extension", 107 | "zls" 108 | ], 109 | "enumItemLabels": [ 110 | "Off", 111 | "Extension", 112 | "ZLS language server" 113 | ], 114 | "enumDescriptions": [ 115 | "Disable formatting", 116 | "Provide formatting by directly invoking `zig fmt`", 117 | "Provide formatting by using ZLS (which matches `zig fmt`)" 118 | ], 119 | "default": "zls" 120 | }, 121 | "zig.testArgs": { 122 | "type": "array", 123 | "items": { 124 | "type": "string" 125 | }, 126 | "default": [ 127 | "test", 128 | "--test-filter", 129 | "${filter}", 130 | "${path}" 131 | ], 132 | "description": "Arguments to pass to 'zig' for running tests. Supported variables: ${filter}, ${path}.\n\nTo use test filter with a build script define a custom option to pass it to: `-Dtest-filter=${filter}`." 133 | }, 134 | "zig.debugTestArgs": { 135 | "type": "array", 136 | "items": { 137 | "type": "string" 138 | }, 139 | "default": [ 140 | "test", 141 | "${path}", 142 | "--test-filter", 143 | "${filter}", 144 | "--test-no-exec", 145 | "-femit-bin=${binaryPath}" 146 | ], 147 | "description": "Arguments to pass to 'zig' for debugging tests. Supported variables: ${filter}, ${path}, ${binaryPath}.\n\nTo debug a test with a build script add a step that installs the test binary at `zig-out/bin/debug-unit-tests`:\n```zig\nb.step(\"debug-test-unit\", \"Debug unit tests\").dependOn(&b.addInstallArtifact(unit_tests, .{ .dest_sub_path = \"debug-unit-tests\" }).step);\n```" 148 | }, 149 | "zig.debugAdapter": { 150 | "scope": "resource", 151 | "type": "string", 152 | "default": "lldb", 153 | "description": "The debug adapter command to run when starting a debug session" 154 | }, 155 | "zig.buildOnSaveProvider": { 156 | "scope": "resource", 157 | "type": "string", 158 | "description": "Specify how build on save diagnostics should be provided.", 159 | "enum": [ 160 | "off", 161 | "auto", 162 | "extension", 163 | "zls" 164 | ], 165 | "enumItemLabels": [ 166 | "Disabled", 167 | "Automatic", 168 | "Extension", 169 | "ZLS language server" 170 | ], 171 | "enumDescriptions": [ 172 | "Disable build on save", 173 | "Disabled unless ZLS has enabled [build on save](https://zigtools.org/zls/guides/build-on-save/) automatically.", 174 | "Provide build on save by running `zig build --watch`", 175 | "Provide build on save using ZLS" 176 | ], 177 | "default": "auto" 178 | }, 179 | "zig.buildOnSaveArgs": { 180 | "type": "array", 181 | "items": { 182 | "type": "string" 183 | }, 184 | "default": [], 185 | "description": "Specify which additional arguments should be passed to `zig build` when running build on save. Use the `--build-file` argument to override which build file should be used." 186 | }, 187 | "zig.zls.debugLog": { 188 | "scope": "resource", 189 | "type": "boolean", 190 | "description": "Enable debug logging in release builds of ZLS." 191 | }, 192 | "zig.zls.trace.server": { 193 | "scope": "window", 194 | "type": "string", 195 | "description": "Traces the communication between VS Code and the ZLS language server.\n\nThe log output can be accessed by running the \"Developer: Show Logs...\" command and selecting \"ZLS language server\". To display trace messages, use \"Developer: Set Log Level...\" or click the settings (⚙️) icon in the top-right corner of the output panel.", 196 | "enum": [ 197 | "off", 198 | "messages", 199 | "verbose" 200 | ], 201 | "default": "off" 202 | }, 203 | "zig.zls.enabled": { 204 | "scope": "resource", 205 | "type": "string", 206 | "description": "Whether to enable the optional ZLS language server", 207 | "enum": [ 208 | "ask", 209 | "off", 210 | "on" 211 | ], 212 | "default": "ask" 213 | }, 214 | "zig.zls.path": { 215 | "scope": "machine-overridable", 216 | "type": "string", 217 | "description": "Set a custom path to the `zls` executable. Example: `C:/zls/zig-cache/bin/zls.exe`. The string \"zls\" means lookup ZLS in PATH.", 218 | "format": "path" 219 | }, 220 | "zig.zls.enableSnippets": { 221 | "scope": "resource", 222 | "type": "boolean", 223 | "description": "Enables snippet completions when the client also supports them", 224 | "default": true 225 | }, 226 | "zig.zls.enableArgumentPlaceholders": { 227 | "scope": "resource", 228 | "type": "boolean", 229 | "description": "Whether to enable function argument placeholder completions", 230 | "default": true 231 | }, 232 | "zig.zls.completionLabelDetails": { 233 | "scope": "resource", 234 | "type": "boolean", 235 | "description": "Whether to show the function signature in completion results. May improve readability in some editors when disabled", 236 | "default": true 237 | }, 238 | "zig.zls.semanticTokens": { 239 | "scope": "resource", 240 | "type": "string", 241 | "description": "Set level of semantic tokens. `partial` only includes information that requires semantic analysis; this will usually give a better result than `full` in VS Code thanks to the Zig extension's syntax file.", 242 | "enum": [ 243 | "none", 244 | "partial", 245 | "full" 246 | ], 247 | "default": "partial" 248 | }, 249 | "zig.zls.inlayHintsShowVariableTypeHints": { 250 | "scope": "resource", 251 | "type": "boolean", 252 | "description": "Enable inlay hints for variable types", 253 | "default": true 254 | }, 255 | "zig.zls.inlayHintsShowStructLiteralFieldType": { 256 | "scope": "resource", 257 | "type": "boolean", 258 | "description": "Enable inlay hints for fields in struct and union literals", 259 | "default": true 260 | }, 261 | "zig.zls.inlayHintsShowParameterName": { 262 | "scope": "resource", 263 | "type": "boolean", 264 | "description": "Enable inlay hints for parameter names", 265 | "default": true 266 | }, 267 | "zig.zls.inlayHintsShowBuiltin": { 268 | "scope": "resource", 269 | "type": "boolean", 270 | "description": "Enable inlay hints for builtin functions", 271 | "default": true 272 | }, 273 | "zig.zls.inlayHintsExcludeSingleArgument": { 274 | "scope": "resource", 275 | "type": "boolean", 276 | "description": "Don't show inlay hints for single argument calls", 277 | "default": true 278 | }, 279 | "zig.zls.inlayHintsHideRedundantParamNames": { 280 | "scope": "resource", 281 | "type": "boolean", 282 | "description": "Hides inlay hints when parameter name matches the identifier (e.g. `foo: foo`)", 283 | "default": false 284 | }, 285 | "zig.zls.inlayHintsHideRedundantParamNamesLastToken": { 286 | "scope": "resource", 287 | "type": "boolean", 288 | "description": "Hides inlay hints when parameter name matches the last token of a parameter node (e.g. `foo: bar.foo`, `foo: &foo`)", 289 | "default": false 290 | }, 291 | "zig.zls.warnStyle": { 292 | "scope": "resource", 293 | "type": "boolean", 294 | "description": "Enables warnings for style guideline mismatches", 295 | "default": false 296 | }, 297 | "zig.zls.highlightGlobalVarDeclarations": { 298 | "scope": "resource", 299 | "type": "boolean", 300 | "description": "Whether to highlight global var declarations", 301 | "default": false 302 | }, 303 | "zig.zls.skipStdReferences": { 304 | "scope": "resource", 305 | "type": "boolean", 306 | "description": "When true, skips searching for references in the standard library. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is", 307 | "default": false 308 | }, 309 | "zig.zls.preferAstCheckAsChildProcess": { 310 | "scope": "resource", 311 | "type": "boolean", 312 | "description": "Favor using `zig ast-check` instead of the builtin one", 313 | "default": true 314 | }, 315 | "zig.zls.builtinPath": { 316 | "scope": "resource", 317 | "type": "string", 318 | "description": "Override the path to 'builtin' module. Automatically resolved if unset.", 319 | "format": "path" 320 | }, 321 | "zig.zls.zigLibPath": { 322 | "scope": "resource", 323 | "type": "string", 324 | "description": "Override the Zig library path. Will be automatically resolved using the 'zig_exe_path'.", 325 | "format": "path" 326 | }, 327 | "zig.zls.buildRunnerPath": { 328 | "scope": "resource", 329 | "type": "string", 330 | "description": "Specify a custom build runner to resolve build system information.", 331 | "format": "path" 332 | }, 333 | "zig.zls.globalCachePath": { 334 | "scope": "resource", 335 | "type": "string", 336 | "description": "Path to a directory that will be used as zig's cache. Will default to `${KnownFolders.Cache}/zls`.", 337 | "format": "path" 338 | }, 339 | "zig.zls.additionalOptions": { 340 | "scope": "resource", 341 | "type": "object", 342 | "markdownDescription": "Additional config options that should be forwarded to ZLS. Every property must have the format 'zig.zls.someOptionName'. You will **not** be warned about unused or ignored options.", 343 | "default": {}, 344 | "additionalProperties": false, 345 | "patternProperties": { 346 | "^zig\\.zls\\.[a-z]+[A-Z0-9][a-z0-9]+[A-Za-z0-9]*$": {} 347 | } 348 | } 349 | } 350 | }, 351 | "commands": [ 352 | { 353 | "command": "zig.run", 354 | "title": "Run Zig", 355 | "category": "Zig", 356 | "description": "Run the current Zig project / file" 357 | }, 358 | { 359 | "command": "zig.debug", 360 | "title": "Debug Zig", 361 | "category": "Zig", 362 | "description": "Debug the current Zig project / file" 363 | }, 364 | { 365 | "command": "zig.install", 366 | "title": "Install Zig", 367 | "category": "Zig Setup" 368 | }, 369 | { 370 | "command": "zig.toggleMultilineStringLiteral", 371 | "title": "Toggle Multiline String Literal", 372 | "category": "Zig" 373 | }, 374 | { 375 | "command": "zig.zls.enable", 376 | "title": "Enable Language Server", 377 | "category": "ZLS language server" 378 | }, 379 | { 380 | "command": "zig.zls.startRestart", 381 | "title": "Start / Restart Language Server", 382 | "category": "ZLS language server" 383 | }, 384 | { 385 | "command": "zig.zls.stop", 386 | "title": "Stop Language Server", 387 | "category": "ZLS language server" 388 | } 389 | ], 390 | "keybindings": [ 391 | { 392 | "command": "zig.toggleMultilineStringLiteral", 393 | "key": "alt+m alt+s", 394 | "when": "editorTextFocus && editorLangId == 'zig'" 395 | } 396 | ], 397 | "jsonValidation": [ 398 | { 399 | "fileMatch": "zls.json", 400 | "url": "https://raw.githubusercontent.com/zigtools/zls/master/schema.json" 401 | } 402 | ] 403 | }, 404 | "scripts": { 405 | "vscode:prepublish": "npm run build-base -- --minify", 406 | "build-base": "esbuild --bundle --external:vscode src/extension.ts --outdir=out --platform=node --target=node20 --format=cjs", 407 | "build": "npm run build-base -- --sourcemap", 408 | "watch": "npm run build-base -- --sourcemap --watch", 409 | "test": "npm run compile && node ./node_modules/vscode/bin/test", 410 | "typecheck": "tsc --noEmit", 411 | "format": "prettier --write .", 412 | "format:check": "prettier --check .", 413 | "lint": "eslint" 414 | }, 415 | "devDependencies": { 416 | "@types/libsodium-wrappers": "^0.7.14", 417 | "@types/lodash-es": "^4.17.12", 418 | "@types/node": "^20.0.0", 419 | "@types/semver": "^7.5.8", 420 | "@types/vscode": "^1.80.0", 421 | "@types/which": "^2.0.1", 422 | "@vscode/vsce": "^2.24.0", 423 | "esbuild": "^0.25.0", 424 | "eslint": "^9.0.0", 425 | "eslint-config-prettier": "^9.1.0", 426 | "prettier": "3.2.5", 427 | "typescript": "^5.4.3", 428 | "typescript-eslint": "^8.0.0" 429 | }, 430 | "dependencies": { 431 | "libsodium-wrappers": "^0.7.15", 432 | "lodash-es": "^4.17.21", 433 | "semver": "^7.5.2", 434 | "vscode-languageclient": "10.0.0-next.15", 435 | "which": "^3.0.0" 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /src/zls.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import { 4 | ConfigurationParams, 5 | LSPAny, 6 | LanguageClient, 7 | LanguageClientOptions, 8 | ResponseError, 9 | ServerOptions, 10 | } from "vscode-languageclient/node"; 11 | import { camelCase, snakeCase } from "lodash-es"; 12 | import semver from "semver"; 13 | 14 | import * as minisign from "./minisign"; 15 | import * as versionManager from "./versionManager"; 16 | import * as zigUtil from "./zigUtil"; 17 | import { zigProvider } from "./zigSetup"; 18 | 19 | const ZIG_MODE = [ 20 | { language: "zig", scheme: "file" }, 21 | { language: "zig", scheme: "untitled" }, 22 | ]; 23 | 24 | let versionManagerConfig: versionManager.Config; 25 | let statusItem: vscode.LanguageStatusItem; 26 | let outputChannel: vscode.LogOutputChannel; 27 | export let client: LanguageClient | null = null; 28 | 29 | export async function restartClient(context: vscode.ExtensionContext): Promise { 30 | const result = await getZLSPath(context); 31 | 32 | if (!result) { 33 | await stopClient(); 34 | updateStatusItem(null); 35 | return; 36 | } 37 | 38 | try { 39 | const newClient = await startClient(result.exe, result.version); 40 | void stopClient(); 41 | client = newClient; 42 | updateStatusItem(result.version); 43 | } catch (reason) { 44 | if (reason instanceof Error) { 45 | void vscode.window.showWarningMessage(`Failed to run ZLS language server: ${reason.message}`); 46 | } else { 47 | void vscode.window.showWarningMessage("Failed to run ZLS language server"); 48 | } 49 | updateStatusItem(null); 50 | } 51 | } 52 | 53 | async function startClient(zlsPath: string, zlsVersion: semver.SemVer): Promise { 54 | const configuration = vscode.workspace.getConfiguration("zig.zls"); 55 | const debugLog = configuration.get("debugLog", false); 56 | 57 | const args: string[] = []; 58 | 59 | if (debugLog) { 60 | /** `--enable-debug-log` has been deprecated in favor of `--log-level`. https://github.com/zigtools/zls/pull/1957 */ 61 | const zlsCLIRevampVersion = new semver.SemVer("0.14.0-50+3354fdc"); 62 | if (semver.lt(zlsVersion, zlsCLIRevampVersion)) { 63 | args.push("--enable-debug-log"); 64 | } else { 65 | args.push("--log-level", "debug"); 66 | } 67 | } 68 | 69 | const serverOptions: ServerOptions = { 70 | command: zlsPath, 71 | args: args, 72 | }; 73 | 74 | const clientOptions: LanguageClientOptions = { 75 | documentSelector: ZIG_MODE, 76 | outputChannel, 77 | middleware: { 78 | workspace: { 79 | configuration: configurationMiddleware, 80 | }, 81 | }, 82 | }; 83 | 84 | const languageClient = new LanguageClient("zig.zls", "ZLS language server", serverOptions, clientOptions); 85 | await languageClient.start(); 86 | // Formatting is handled by `zigFormat.ts` 87 | languageClient.getFeature("textDocument/formatting").clear(); 88 | return languageClient; 89 | } 90 | 91 | async function stopClient(): Promise { 92 | if (!client) return; 93 | const oldClient = client; 94 | client = null; 95 | // The `stop` call will send the "shutdown" notification to the LSP 96 | await oldClient.stop(); 97 | // The `dipose` call will send the "exit" request to the LSP which actually tells the child process to exit 98 | await oldClient.dispose(); 99 | } 100 | 101 | /** returns the file system path to the zls executable */ 102 | async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: string; version: semver.SemVer } | null> { 103 | const configuration = vscode.workspace.getConfiguration("zig.zls"); 104 | let zlsExePath = configuration.get("path"); 105 | let zlsVersion: semver.SemVer | null = null; 106 | 107 | if (!!zlsExePath) { 108 | // This will fail on older ZLS version that do not support `zls --version`. 109 | // It should be more likely that the given executable is invalid than someone using ZLS 0.9.0 or older. 110 | const result = zigUtil.resolveExePathAndVersion(zlsExePath, "--version"); 111 | if ("message" in result) { 112 | vscode.window 113 | .showErrorMessage(`Unexpected 'zig.zls.path': ${result.message}`, "install ZLS", "open settings") 114 | .then(async (response) => { 115 | switch (response) { 116 | case "install ZLS": 117 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 118 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 119 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined); 120 | break; 121 | case "open settings": 122 | await vscode.commands.executeCommand("workbench.action.openSettings", "zig.zls.path"); 123 | break; 124 | case undefined: 125 | break; 126 | } 127 | }); 128 | return null; 129 | } 130 | return result; 131 | } 132 | 133 | if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; 134 | 135 | const zigVersion = zigProvider.getZigVersion(); 136 | if (!zigVersion) return null; 137 | 138 | const result = await fetchVersion(context, zigVersion, true); 139 | if (!result) return null; 140 | 141 | try { 142 | zlsExePath = await versionManager.install(versionManagerConfig, result.version); 143 | zlsVersion = result.version; 144 | } catch (err) { 145 | if (err instanceof Error) { 146 | void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}: ${err.message}`); 147 | } else { 148 | void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); 149 | } 150 | return null; 151 | } 152 | 153 | return { 154 | exe: zlsExePath, 155 | version: zlsVersion, 156 | }; 157 | } 158 | 159 | function configurationMiddleware(params: ConfigurationParams): LSPAny[] | ResponseError { 160 | void validateAdditionalOptions(); 161 | return params.items.map((param) => { 162 | if (!param.section) return null; 163 | 164 | const scopeUri = param.scopeUri ? client?.protocol2CodeConverter.asUri(param.scopeUri) : undefined; 165 | const configuration = vscode.workspace.getConfiguration("zig", scopeUri); 166 | const workspaceFolder = scopeUri ? vscode.workspace.getWorkspaceFolder(scopeUri) : undefined; 167 | 168 | const updateConfigOption = (section: string, value: unknown) => { 169 | if (section === "zls.zigExePath") { 170 | return zigProvider.getZigPath(); 171 | } 172 | 173 | if (typeof value === "string") { 174 | // Make sure that `""` gets converted to `undefined` and resolve predefined values 175 | value = value ? zigUtil.handleConfigOption(value, workspaceFolder ?? "guess") : undefined; 176 | } else if (Array.isArray(value)) { 177 | value = value.map((elem: unknown) => { 178 | if (typeof elem !== "string") return elem; 179 | return zigUtil.handleConfigOption(elem, workspaceFolder ?? "guess"); 180 | }); 181 | } else if (typeof value === "object" && value !== null) { 182 | // Recursively update the config options 183 | const newValue: Record = {}; 184 | for (const [fieldName, fieldValue] of Object.entries(value)) { 185 | newValue[snakeCase(fieldName)] = updateConfigOption(section + "." + fieldName, fieldValue); 186 | } 187 | return newValue; 188 | } 189 | 190 | const inspect = configuration.inspect(section); 191 | const isDefaultValue = 192 | value === inspect?.defaultValue && 193 | inspect?.globalValue === undefined && 194 | inspect?.workspaceValue === undefined && 195 | inspect?.workspaceFolderValue === undefined; 196 | 197 | if (isDefaultValue) { 198 | if (section === "zls.semanticTokens") { 199 | // The extension has a different default value for this config 200 | // option compared to ZLS 201 | return value; 202 | } else { 203 | return undefined; 204 | } 205 | } 206 | return value; 207 | }; 208 | 209 | let additionalOptions = configuration.get>("zls.additionalOptions", {}); 210 | 211 | // Remove the `zig.zls.` prefix from the entries in `zig.zls.additionalOptions` 212 | additionalOptions = Object.fromEntries( 213 | Object.entries(additionalOptions) 214 | .filter(([key]) => key.startsWith("zig.zls.")) 215 | .map(([key, value]) => [key.slice("zig.zls.".length), value]), 216 | ); 217 | 218 | switch (configuration.get<"off" | "auto" | "extension" | "zls">("buildOnSaveProvider", "auto")) { 219 | case "auto": 220 | additionalOptions["buildOnSaveArgs"] = configuration.get("buildOnSaveArgs"); 221 | break; 222 | case "zls": 223 | additionalOptions["enableBuildOnSave"] = true; 224 | additionalOptions["buildOnSaveArgs"] = configuration.get("buildOnSaveArgs"); 225 | break; 226 | case "off": 227 | case "extension": 228 | additionalOptions["enableBuildOnSave"] = false; 229 | break; 230 | } 231 | 232 | if (param.section === "zls") { 233 | // ZLS has requested all config options. 234 | 235 | const options = { ...configuration.get>(param.section, {}) }; 236 | // Some config options are specific to the VS Code 237 | // extension. ZLS should ignore unknown values but 238 | // we remove them here anyway. 239 | delete options["debugLog"]; // zig.zls.debugLog 240 | delete options["trace"]; // zig.zls.trace.server 241 | delete options["enabled"]; // zig.zls.enabled 242 | delete options["path"]; // zig.zls.path 243 | delete options["additionalOptions"]; // zig.zls.additionalOptions 244 | 245 | return updateConfigOption(param.section, { 246 | ...additionalOptions, 247 | ...options, 248 | // eslint-disable-next-line @typescript-eslint/naming-convention 249 | zig_exe_path: zigProvider.getZigPath(), 250 | }); 251 | } else if (param.section.startsWith("zls.")) { 252 | // ZLS has requested a specific config option. 253 | 254 | // ZLS names it's config options in snake_case but the VS Code extension uses camelCase 255 | const camelCaseSection = param.section 256 | .split(".") 257 | .map((str) => camelCase(str)) 258 | .join("."); 259 | 260 | return updateConfigOption( 261 | camelCaseSection, 262 | configuration.get(camelCaseSection, additionalOptions[camelCaseSection.slice("zls.".length)]), 263 | ); 264 | } else { 265 | // Do not allow ZLS to request other editor config options. 266 | return null; 267 | } 268 | }); 269 | } 270 | 271 | async function validateAdditionalOptions(): Promise { 272 | const configuration = vscode.workspace.getConfiguration("zig.zls", null); 273 | const additionalOptions = configuration.get>("additionalOptions", {}); 274 | 275 | for (const optionName in additionalOptions) { 276 | if (!optionName.startsWith("zig.zls.")) continue; 277 | const section = optionName.slice("zig.zls.".length); 278 | 279 | const inspect = configuration.inspect(section); 280 | const doesOptionExist = inspect?.defaultValue !== undefined; 281 | if (!doesOptionExist) continue; 282 | 283 | // The extension has defined a config option with the given name but the user still used `additionalOptions`. 284 | const response = await vscode.window.showWarningMessage( 285 | `The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`, 286 | `Use ${optionName} instead`, 287 | "Show zig.zls.additionalOptions", 288 | ); 289 | switch (response) { 290 | case `Use ${optionName} instead`: 291 | const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions; 292 | await zigUtil.workspaceConfigUpdateNoThrow( 293 | configuration, 294 | "additionalOptions", 295 | Object.keys(updatedAdditionalOptions).length ? updatedAdditionalOptions : undefined, 296 | true, 297 | ); 298 | await zigUtil.workspaceConfigUpdateNoThrow(configuration, section, newValue, true); 299 | break; 300 | case "Show zig.zls.additionalOptions": 301 | await vscode.commands.executeCommand("workbench.action.openSettingsJson", { 302 | revealSetting: { key: "zig.zls.additionalOptions" }, 303 | }); 304 | break; 305 | case undefined: 306 | return; 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * Similar to https://builds.zigtools.org/index.json 313 | */ 314 | interface SelectVersionResponse { 315 | /** The ZLS version */ 316 | version: string; 317 | /** `YYYY-MM-DD` */ 318 | date: string; 319 | [artifact: string]: ArtifactEntry | string | undefined; 320 | } 321 | 322 | interface SelectVersionFailureResponse { 323 | /** 324 | * The `code` **may** be one of `SelectVersionFailureCode`. Be aware that new 325 | * codes can be added over time. 326 | */ 327 | code: number; 328 | /** A simplified explanation of why no ZLS build could be selected */ 329 | message: string; 330 | } 331 | 332 | interface ArtifactEntry { 333 | /** A download URL */ 334 | tarball: string; 335 | /** A SHA256 hash of the tarball */ 336 | shasum: string; 337 | /** Size of the tarball in bytes */ 338 | size: string; 339 | } 340 | 341 | async function fetchVersion( 342 | context: vscode.ExtensionContext, 343 | zigVersion: semver.SemVer, 344 | useCache: boolean, 345 | ): Promise<{ version: semver.SemVer; artifact: ArtifactEntry } | null> { 346 | // Should the cache be periodically cleared? 347 | const cacheKey = `zls-select-version-${zigVersion.raw}`; 348 | 349 | let response: SelectVersionResponse | SelectVersionFailureResponse | null = null; 350 | try { 351 | const url = new URL("https://releases.zigtools.org/v1/zls/select-version"); 352 | url.searchParams.append("zig_version", zigVersion.raw); 353 | url.searchParams.append("compatibility", "only-runtime"); 354 | 355 | const fetchResponse = await fetch(url); 356 | response = (await fetchResponse.json()) as SelectVersionResponse | SelectVersionFailureResponse; 357 | 358 | // Cache the response 359 | if (useCache) { 360 | await context.globalState.update(cacheKey, response); 361 | } 362 | } catch (err) { 363 | // Try to read the result from cache 364 | if (useCache) { 365 | response = context.globalState.get(cacheKey) ?? null; 366 | } 367 | 368 | if (!response) { 369 | if (err instanceof Error) { 370 | void vscode.window.showErrorMessage(`Failed to query ZLS version: ${err.message}`); 371 | } else { 372 | throw err; 373 | } 374 | return null; 375 | } 376 | } 377 | 378 | if ("message" in response) { 379 | void vscode.window.showErrorMessage(`Unable to fetch ZLS: ${response.message as string}`); 380 | return null; 381 | } 382 | const version = new semver.SemVer(response.version); 383 | const armName = semver.gte(version, "0.15.0") ? "arm" : "armv7a"; 384 | const targetName = `${zigUtil.getZigArchName(armName)}-${zigUtil.getZigOSName()}`; 385 | 386 | if (!(targetName in response)) { 387 | void vscode.window.showErrorMessage( 388 | `A prebuilt ZLS ${response.version} binary is not available for your system. You can build it yourself with https://github.com/zigtools/zls#from-source`, 389 | ); 390 | return null; 391 | } 392 | 393 | return { 394 | version: version, 395 | artifact: response[targetName] as ArtifactEntry, 396 | }; 397 | } 398 | 399 | async function isEnabled(): Promise { 400 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 401 | if (!!zlsConfig.get("path")) return true; 402 | 403 | switch (zlsConfig.get<"ask" | "off" | "on">("enabled", "ask")) { 404 | case "on": 405 | return true; 406 | case "off": 407 | return false; 408 | case "ask": { 409 | const response = await vscode.window.showInformationMessage( 410 | "We recommend enabling the ZLS language server for a better editing experience. Would you like to install it?", 411 | { modal: true }, 412 | "Yes", 413 | "No", 414 | ); 415 | switch (response) { 416 | case "Yes": 417 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 418 | return true; 419 | case "No": 420 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "off", true); 421 | return false; 422 | case undefined: 423 | return false; 424 | } 425 | } 426 | } 427 | } 428 | 429 | function updateStatusItem(version: semver.SemVer | null) { 430 | if (version) { 431 | statusItem.text = `ZLS ${version.toString()}`; 432 | statusItem.detail = "ZLS Version"; 433 | statusItem.severity = vscode.LanguageStatusSeverity.Information; 434 | statusItem.command = { 435 | title: "View Output", 436 | command: "zig.zls.openOutput", 437 | }; 438 | } else { 439 | statusItem.text = "ZLS not enabled"; 440 | statusItem.detail = undefined; 441 | statusItem.severity = vscode.LanguageStatusSeverity.Error; 442 | const zigPath = zigProvider.getZigPath(); 443 | const zigVersion = zigProvider.getZigVersion(); 444 | if (zigPath !== null && zigVersion !== null) { 445 | statusItem.command = { 446 | title: "Enable", 447 | command: "zig.zls.enable", 448 | }; 449 | } else { 450 | statusItem.command = undefined; 451 | } 452 | } 453 | } 454 | 455 | export async function activate(context: vscode.ExtensionContext) { 456 | { 457 | // This check can be removed once enough time has passed so that most users switched to the new value 458 | 459 | // remove the `zls_install` directory from the global storage 460 | try { 461 | await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zls_install"), { 462 | recursive: true, 463 | useTrash: false, 464 | }); 465 | } catch {} 466 | 467 | // convert a `zig.zls.path` that points to the global storage to `zig.zls.enabled == "on"` 468 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 469 | const zlsPath = zlsConfig.get("path", ""); 470 | if (zlsPath.startsWith(context.globalStorageUri.fsPath)) { 471 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 472 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined, true); 473 | } 474 | 475 | // convert `zig.zls.enableBuildOnSave` to `zig.buildOnSaveProvider` 476 | { 477 | const inspect = zlsConfig.inspect("enableBuildOnSave"); 478 | if (inspect?.globalValue !== undefined) { 479 | await zigUtil.workspaceConfigUpdateNoThrow( 480 | vscode.workspace.getConfiguration("zig"), 481 | "buildOnSaveProvider", 482 | inspect.globalValue ? "zls" : "off", 483 | true, 484 | ); 485 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enableBuildOnSave", undefined, true); 486 | } 487 | if (inspect?.workspaceValue !== undefined) { 488 | await zigUtil.workspaceConfigUpdateNoThrow( 489 | vscode.workspace.getConfiguration("zig"), 490 | "buildOnSaveProvider", 491 | inspect.workspaceValue ? "zls" : "off", 492 | false, 493 | ); 494 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enableBuildOnSave", undefined, false); 495 | } 496 | } 497 | 498 | // convert `zig.zls.buildOnSaveArgs` to `zig.buildOnSaveArgs` 499 | { 500 | const inspect = zlsConfig.inspect("buildOnSaveArgs"); 501 | const zigConfig = vscode.workspace.getConfiguration("zig"); 502 | if (inspect?.globalValue) { 503 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSaveArgs", inspect.globalValue, true); 504 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "buildOnSaveArgs", undefined, true); 505 | } 506 | if (inspect?.workspaceValue) { 507 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSaveArgs", inspect.workspaceValue, false); 508 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "buildOnSaveArgs", undefined, false); 509 | } 510 | } 511 | } 512 | 513 | versionManagerConfig = { 514 | context: context, 515 | title: "ZLS", 516 | exeName: "zls", 517 | extraTarArgs: [], 518 | /** https://github.com/zigtools/release-worker */ 519 | minisignKey: minisign.parseKey("RWR+9B91GBZ0zOjh6Lr17+zKf5BoSuFvrx2xSeDE57uIYvnKBGmMjOex"), 520 | versionArg: "--version", 521 | getMirrorUrls() { 522 | return Promise.resolve([]); 523 | }, 524 | canonicalUrl: { 525 | release: vscode.Uri.parse("https://builds.zigtools.org"), 526 | nightly: vscode.Uri.parse("https://builds.zigtools.org"), 527 | }, 528 | getArtifactName(version) { 529 | const fileExtension = process.platform === "win32" ? "zip" : "tar.xz"; 530 | const targetName = semver.gte(version, "0.15.0") 531 | ? `${zigUtil.getZigArchName("arm")}-${zigUtil.getZigOSName()}` 532 | : `${zigUtil.getZigOSName()}-${zigUtil.getZigArchName("armv7a")}`; 533 | return `zls-${targetName}-${version.raw}.${fileExtension}`; 534 | }, 535 | }; 536 | 537 | // Remove after some time has passed from the prefix change. 538 | await versionManager.convertOldInstallPrefixes(versionManagerConfig); 539 | 540 | outputChannel = vscode.window.createOutputChannel("ZLS language server", { log: true }); 541 | statusItem = vscode.languages.createLanguageStatusItem("zig.zls.status", ZIG_MODE); 542 | statusItem.name = "ZLS"; 543 | updateStatusItem(null); 544 | 545 | context.subscriptions.push( 546 | outputChannel, 547 | statusItem, 548 | vscode.commands.registerCommand("zig.zls.enable", async () => { 549 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 550 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 551 | }), 552 | vscode.commands.registerCommand("zig.zls.stop", async () => { 553 | await stopClient(); 554 | }), 555 | vscode.commands.registerCommand("zig.zls.startRestart", async () => { 556 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 557 | await zigUtil.workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 558 | await restartClient(context); 559 | }), 560 | vscode.commands.registerCommand("zig.zls.openOutput", () => { 561 | outputChannel.show(); 562 | }), 563 | ); 564 | 565 | if (await isEnabled()) { 566 | await restartClient(context); 567 | } 568 | 569 | // These checks are added later to avoid ZLS be started twice because `isEnabled` sets `zig.zls.enabled`. 570 | context.subscriptions.push( 571 | vscode.workspace.onDidChangeConfiguration(async (change) => { 572 | // The `zig.path` config option is handled by `zigProvider.onChange`. 573 | if ( 574 | change.affectsConfiguration("zig.zls.enabled", undefined) || 575 | change.affectsConfiguration("zig.zls.path", undefined) || 576 | change.affectsConfiguration("zig.zls.debugLog", undefined) 577 | ) { 578 | await restartClient(context); 579 | } 580 | }), 581 | zigProvider.onChange.event(async () => { 582 | await restartClient(context); 583 | }), 584 | ); 585 | } 586 | 587 | export async function deactivate(): Promise { 588 | await stopClient(); 589 | } 590 | -------------------------------------------------------------------------------- /src/zigSetup.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import fs from "fs/promises"; 4 | import path from "path"; 5 | 6 | import semver from "semver"; 7 | 8 | import * as minisign from "./minisign"; 9 | import * as versionManager from "./versionManager"; 10 | import * as zigUtil from "./zigUtil"; 11 | import { ZigProvider } from "./zigProvider"; 12 | 13 | let statusItem: vscode.StatusBarItem; 14 | let languageStatusItem: vscode.LanguageStatusItem; 15 | let versionManagerConfig: versionManager.Config; 16 | export let zigProvider: ZigProvider; 17 | 18 | /** Removes the `zig.path` config option. */ 19 | async function installZig(context: vscode.ExtensionContext, temporaryVersion?: semver.SemVer) { 20 | let version = temporaryVersion; 21 | 22 | if (!version) { 23 | const wantedZig = await getWantedZigVersion( 24 | context, 25 | Object.values(WantedZigVersionSource) as WantedZigVersionSource[], 26 | ); 27 | version = wantedZig?.version; 28 | if (wantedZig?.source === WantedZigVersionSource.workspaceBuildZigZon) { 29 | version = await findClosestSatisfyingZigVersion(context, wantedZig.version); 30 | } 31 | } 32 | 33 | if (!version) { 34 | // Lookup zig in $PATH 35 | const result = zigUtil.resolveExePathAndVersion("zig", "version"); 36 | if ("exe" in result) { 37 | await vscode.workspace.getConfiguration("zig").update("path", undefined, true); 38 | zigProvider.set(result); 39 | return; 40 | } 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 44 | if (!version) { 45 | // Default to the latest tagged release 46 | version = (await getLatestTaggedZigVersion(context)) ?? undefined; 47 | } 48 | 49 | if (!version) { 50 | await zigProvider.setAndSave(null); 51 | return; 52 | } 53 | 54 | try { 55 | const exePath = await versionManager.install(versionManagerConfig, version); 56 | const zigConfig = vscode.workspace.getConfiguration("zig"); 57 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "path", undefined, true); 58 | zigProvider.set({ exe: exePath, version: version }); 59 | } catch (err) { 60 | zigProvider.set(null); 61 | if (err instanceof Error) { 62 | void vscode.window.showErrorMessage(`Failed to install Zig ${version.toString()}: ${err.message}`); 63 | } else { 64 | void vscode.window.showErrorMessage(`Failed to install Zig ${version.toString()}!`); 65 | } 66 | } 67 | } 68 | 69 | async function findClosestSatisfyingZigVersion( 70 | context: vscode.ExtensionContext, 71 | version: semver.SemVer, 72 | ): Promise { 73 | if (version.prerelease.length !== 0) return version; 74 | 75 | try { 76 | // We can't just return `version` because `0.12.0` should return `0.12.1`. 77 | const availableVersions = (await getVersions(context)).map((item) => item.version); 78 | const selectedVersion = semver.maxSatisfying(availableVersions, `^${version.toString()}`); 79 | return selectedVersion ?? version; 80 | } catch { 81 | return version; 82 | } 83 | } 84 | 85 | async function getLatestTaggedZigVersion(context: vscode.ExtensionContext): Promise { 86 | try { 87 | const zigVersion = await getVersions(context); 88 | const latestTagged = zigVersion.find((item) => item.version.prerelease.length === 0); 89 | const result = latestTagged?.version ?? null; 90 | return result; 91 | } catch { 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * Returns a sorted list of all versions that are provided by Zig's [index.json](https://ziglang.org/download/index.json) and Mach's [index.json](https://pkg.machengine.org/zig/index.json). 98 | * [Nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) are sorted to the bottom. 99 | * 100 | * Throws an exception when no network connection is available. 101 | */ 102 | async function getVersions(context: vscode.ExtensionContext): Promise { 103 | const cacheKey = "zig-version-list"; 104 | let zigIndexJson, machIndexJson; 105 | try { 106 | [zigIndexJson, machIndexJson] = await Promise.all( 107 | ["https://ziglang.org/download/index.json", "https://pkg.machengine.org/zig/index.json"].map( 108 | async (url) => { 109 | const response = await fetch(url); 110 | return response.json() as Promise; 111 | }, 112 | ), 113 | ); 114 | } catch (error) { 115 | const cached = context.globalState.get(cacheKey); 116 | if (cached !== undefined) { 117 | for (const version of cached) { 118 | // Must be instanceof SemVer 119 | version.version = new semver.SemVer(version.version.raw); 120 | } 121 | return cached; 122 | } 123 | throw error; 124 | } 125 | const indexJson = { ...machIndexJson, ...zigIndexJson }; 126 | 127 | const result: zigUtil.ZigVersion[] = []; 128 | for (const [key, value] of Object.entries(indexJson)) { 129 | const name = key === "master" ? "nightly" : key; 130 | const version = new semver.SemVer(value.version ?? key); 131 | const targetName = `${getZigArchName(version)}-${zigUtil.getZigOSName()}`; 132 | const release = value[targetName]; 133 | if (release) { 134 | result.push({ 135 | name: name, 136 | version: version, 137 | url: release.tarball, 138 | sha: release.shasum, 139 | notes: value.notes, 140 | isMach: name.includes("mach"), 141 | }); 142 | } 143 | } 144 | if (result.length === 0) { 145 | throw Error( 146 | `no pre-built Zig is available for your system '${zigUtil.getZigArchName("arm")}-${zigUtil.getZigOSName()}}', you can build it yourself using https://github.com/ziglang/zig-bootstrap`, 147 | ); 148 | } 149 | sortVersions(result); 150 | await context.globalState.update(cacheKey, result); 151 | return result; 152 | } 153 | 154 | function getZigArchName(zigVersion: semver.SemVer): string { 155 | switch (zigVersion.compare(new semver.SemVer("0.15.0-dev.836+080ee25ec"))) { 156 | case -1: 157 | case 0: 158 | return zigUtil.getZigArchName("armv7a"); 159 | case 1: 160 | return zigUtil.getZigArchName("arm"); 161 | } 162 | } 163 | 164 | function sortVersions(versions: { name?: string; version: semver.SemVer; isMach: boolean }[]) { 165 | versions.sort((lhs, rhs) => { 166 | // Mach versions except `mach-latest` move to the end 167 | if (lhs.name !== "mach-latest" && rhs.name !== "mach-latest" && lhs.isMach !== rhs.isMach) 168 | return +lhs.isMach - +rhs.isMach; 169 | return semver.compare(rhs.version, lhs.version); 170 | }); 171 | } 172 | 173 | async function selectVersionAndInstall(context: vscode.ExtensionContext) { 174 | const offlineVersions = await versionManager.query(versionManagerConfig); 175 | 176 | const versions: { 177 | name?: string; 178 | version: semver.SemVer; 179 | /** Whether the version already installed in global extension storage */ 180 | offline: boolean; 181 | /** Whether is available in `index.json` */ 182 | online: boolean; 183 | /** Whether the version one of [Mach's nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) */ 184 | isMach: boolean; 185 | }[] = offlineVersions.map((version) => ({ 186 | version: version, 187 | offline: true, 188 | online: false, 189 | isMach: false /* We can't tell if a version is Mach while being offline */, 190 | })); 191 | 192 | try { 193 | const onlineVersions = await getVersions(context); 194 | outer: for (const onlineVersion of onlineVersions) { 195 | for (const version of versions) { 196 | if (semver.eq(version.version, onlineVersion.version)) { 197 | version.name ??= onlineVersion.name; 198 | version.online = true; 199 | version.isMach = onlineVersion.isMach; 200 | } 201 | } 202 | 203 | for (const version of versions) { 204 | if (semver.eq(version.version, onlineVersion.version) && version.name === onlineVersion.name) { 205 | continue outer; 206 | } 207 | } 208 | 209 | versions.push({ 210 | name: onlineVersion.name, 211 | version: onlineVersion.version, 212 | online: true, 213 | offline: !!offlineVersions.find((item) => semver.eq(item.version, onlineVersion.version)), 214 | isMach: onlineVersion.isMach, 215 | }); 216 | } 217 | } catch (err) { 218 | if (!offlineVersions.length) { 219 | if (err instanceof Error) { 220 | void vscode.window.showErrorMessage(`Failed to query available Zig version: ${err.message}`); 221 | } else { 222 | void vscode.window.showErrorMessage(`Failed to query available Zig version!`); 223 | } 224 | return; 225 | } else { 226 | // Only show the locally installed versions 227 | } 228 | } 229 | 230 | sortVersions(versions); 231 | const placeholderVersion = versions.find((item) => item.version.prerelease.length === 0)?.version; 232 | 233 | const items: vscode.QuickPickItem[] = []; 234 | 235 | const workspaceZig = await getWantedZigVersion(context, [ 236 | WantedZigVersionSource.workspaceZigVersionFile, 237 | WantedZigVersionSource.workspaceBuildZigZon, 238 | WantedZigVersionSource.zigVersionConfigOption, 239 | ]); 240 | if (workspaceZig !== null) { 241 | const alreadyInstalled = offlineVersions.some((item) => semver.eq(item.version, workspaceZig.version)); 242 | items.push({ 243 | label: "Use Workspace Version", 244 | description: alreadyInstalled ? "already installed" : undefined, 245 | detail: workspaceZig.version.raw, 246 | }); 247 | } 248 | 249 | const zigInPath = zigUtil.resolveExePathAndVersion("zig", "version"); 250 | if (!("message" in zigInPath)) { 251 | items.push({ 252 | label: "Use Zig in PATH", 253 | description: zigInPath.exe, 254 | detail: zigInPath.version.raw, 255 | }); 256 | } 257 | 258 | items.push( 259 | { 260 | label: "Manually Specify Path", 261 | }, 262 | { 263 | label: "", 264 | kind: vscode.QuickPickItemKind.Separator, 265 | }, 266 | ); 267 | 268 | let seenMachVersion = false; 269 | for (const item of versions) { 270 | const useName = item.isMach || item.version.prerelease.length !== 0; 271 | if (item.isMach && !seenMachVersion && item.name !== "mach-latest") { 272 | seenMachVersion = true; 273 | items.push({ 274 | label: "Mach's Nominated Zig versions", 275 | kind: vscode.QuickPickItemKind.Separator, 276 | }); 277 | } 278 | items.push({ 279 | label: (useName ? item.name : null) ?? item.version.raw, 280 | description: item.offline ? "already installed" : undefined, 281 | detail: useName ? (item.name ? item.version.raw : undefined) : undefined, 282 | }); 283 | } 284 | 285 | const selection = await vscode.window.showQuickPick(items, { 286 | title: "Select Zig version to install", 287 | canPickMany: false, 288 | placeHolder: placeholderVersion?.raw, 289 | }); 290 | if (selection === undefined) return; 291 | 292 | switch (selection.label) { 293 | case "Use Workspace Version": 294 | await installZig(context); 295 | break; 296 | case "Use Zig in PATH": 297 | const zigConfig = vscode.workspace.getConfiguration("zig"); 298 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "path", "zig", true); 299 | break; 300 | case "Manually Specify Path": 301 | const uris = await vscode.window.showOpenDialog({ 302 | canSelectFiles: true, 303 | canSelectFolders: false, 304 | canSelectMany: false, 305 | title: "Select Zig executable", 306 | }); 307 | if (!uris) return; 308 | await zigProvider.setAndSave(uris[0].fsPath); 309 | break; 310 | default: 311 | const version = new semver.SemVer(selection.detail ?? selection.label); 312 | await showUpdateWorkspaceVersionDialog(version, workspaceZig?.source); 313 | await installZig(context, version); 314 | break; 315 | } 316 | } 317 | 318 | async function showUpdateWorkspaceVersionDialog( 319 | version: semver.SemVer, 320 | source?: WantedZigVersionSource, 321 | ): Promise { 322 | const workspace = getWorkspaceFolder(); 323 | 324 | if (workspace !== null) { 325 | let buttonName; 326 | switch (source) { 327 | case WantedZigVersionSource.workspaceZigVersionFile: 328 | buttonName = "update .zigversion"; 329 | break; 330 | case WantedZigVersionSource.workspaceBuildZigZon: 331 | buttonName = "update build.zig.zon"; 332 | break; 333 | case WantedZigVersionSource.zigVersionConfigOption: 334 | buttonName = "update workspace settings"; 335 | break; 336 | case undefined: 337 | buttonName = "create .zigversion"; 338 | break; 339 | } 340 | 341 | const response = await vscode.window.showInformationMessage( 342 | `Would you like to save Zig ${version.toString()} in this workspace?`, 343 | buttonName, 344 | ); 345 | if (!response) return; 346 | } 347 | 348 | source ??= workspace 349 | ? WantedZigVersionSource.workspaceZigVersionFile 350 | : WantedZigVersionSource.zigVersionConfigOption; 351 | 352 | switch (source) { 353 | case WantedZigVersionSource.workspaceZigVersionFile: { 354 | if (!workspace) throw new Error("failed to resolve workspace folder"); 355 | 356 | const edit = new vscode.WorkspaceEdit(); 357 | edit.createFile(vscode.Uri.joinPath(workspace.uri, ".zigversion"), { 358 | overwrite: true, 359 | contents: new Uint8Array(Buffer.from(version.raw)), 360 | }); 361 | await vscode.workspace.applyEdit(edit); 362 | break; 363 | } 364 | case WantedZigVersionSource.workspaceBuildZigZon: { 365 | const metadata = await parseBuildZigZon(); 366 | if (!metadata) throw new Error("failed to parse build.zig.zon"); 367 | 368 | const edit = new vscode.WorkspaceEdit(); 369 | edit.replace(metadata.document.uri, metadata.minimumZigVersionSourceRange, version.raw); 370 | await vscode.workspace.applyEdit(edit); 371 | break; 372 | } 373 | case WantedZigVersionSource.zigVersionConfigOption: { 374 | await vscode.workspace.getConfiguration("zig").update("version", version.raw, !workspace); 375 | break; 376 | } 377 | } 378 | } 379 | 380 | interface BuildZigZonMetadata { 381 | /** The `build.zig.zon` document. */ 382 | document: vscode.TextDocument; 383 | minimumZigVersion: semver.SemVer; 384 | /** `.minimum_zig_version = "0.13.0"` */ 385 | minimumZigVersionSourceRange: vscode.Range; 386 | } 387 | 388 | function getWorkspaceFolder(): vscode.WorkspaceFolder | null { 389 | // Supporting multiple workspaces is significantly more complex so we just look for the first workspace. 390 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 391 | return vscode.workspace.workspaceFolders[0]; 392 | } 393 | return null; 394 | } 395 | 396 | /** 397 | * Look for a `build.zig.zon` in the current workspace and return the `minimum_zig_version` in it. 398 | */ 399 | async function parseBuildZigZon(): Promise { 400 | const workspace = getWorkspaceFolder(); 401 | if (!workspace) return null; 402 | 403 | const manifestUri = vscode.Uri.joinPath(workspace.uri, "build.zig.zon"); 404 | 405 | let manifest; 406 | try { 407 | manifest = await vscode.workspace.openTextDocument(manifestUri); 408 | } catch { 409 | return null; 410 | } 411 | // Not perfect, but good enough 412 | const regex = /\n\s*\.minimum_zig_version\s=\s\"(.*)\"/; 413 | const matches = regex.exec(manifest.getText()); 414 | if (!matches) return null; 415 | 416 | const versionString = matches[1]; 417 | const version = semver.parse(versionString); 418 | if (!version) return null; 419 | 420 | const startPosition = manifest.positionAt(matches.index + matches[0].length - versionString.length - 1); 421 | const endPosition = startPosition.translate(0, versionString.length); 422 | 423 | return { 424 | document: manifest, 425 | minimumZigVersion: version, 426 | minimumZigVersionSourceRange: new vscode.Range(startPosition, endPosition), 427 | }; 428 | } 429 | 430 | /** The order of these enums defines the default order in which these sources are executed. */ 431 | enum WantedZigVersionSource { 432 | /** `.zigversion` */ 433 | workspaceZigVersionFile = ".zigversion", 434 | /** The `minimum_zig_version` in `build.zig.zon` */ 435 | workspaceBuildZigZon = "build.zig.zon", 436 | /** `zig.version` */ 437 | zigVersionConfigOption = "zig.version", 438 | } 439 | 440 | /** Try to resolve the (workspace-specific) Zig version. */ 441 | async function getWantedZigVersion( 442 | context: vscode.ExtensionContext, 443 | /** List of "sources" that should are applied in the given order to resolve the wanted Zig version */ 444 | sources: WantedZigVersionSource[], 445 | ): Promise<{ 446 | version: semver.SemVer; 447 | source: WantedZigVersionSource; 448 | } | null> { 449 | let workspace: vscode.WorkspaceFolder | null = null; 450 | // Supporting multiple workspaces is significantly more complex so we just look for the first workspace. 451 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 452 | workspace = vscode.workspace.workspaceFolders[0]; 453 | } 454 | 455 | for (const source of sources) { 456 | let result: semver.SemVer | null = null; 457 | 458 | try { 459 | switch (source) { 460 | case WantedZigVersionSource.workspaceZigVersionFile: 461 | if (workspace) { 462 | const zigVersionString = await vscode.workspace.fs.readFile( 463 | vscode.Uri.joinPath(workspace.uri, ".zigversion"), 464 | ); 465 | result = semver.parse(zigVersionString.toString().trim()); 466 | } 467 | break; 468 | case WantedZigVersionSource.workspaceBuildZigZon: 469 | const metadata = await parseBuildZigZon(); 470 | if (metadata?.minimumZigVersion) { 471 | result = metadata.minimumZigVersion; 472 | } 473 | break; 474 | case WantedZigVersionSource.zigVersionConfigOption: 475 | const versionString = vscode.workspace.getConfiguration("zig").get("version"); 476 | if (versionString) { 477 | result = semver.parse(versionString); 478 | if (!result) { 479 | void vscode.window.showErrorMessage( 480 | `Invalid 'zig.version' config option. '${versionString}' is not a valid Zig version`, 481 | ); 482 | } 483 | } 484 | break; 485 | } 486 | } catch {} 487 | 488 | if (!result) continue; 489 | 490 | return { 491 | version: result, 492 | source: source, 493 | }; 494 | } 495 | return null; 496 | } 497 | 498 | function updateStatusItem(item: vscode.StatusBarItem, version: semver.SemVer | null) { 499 | item.name = "Zig Version"; 500 | item.text = version?.toString() ?? "not installed"; 501 | item.tooltip = "Select Zig Version"; 502 | item.command = { 503 | title: "Select Version", 504 | command: "zig.install", 505 | }; 506 | if (version) { 507 | item.backgroundColor = undefined; 508 | } else { 509 | item.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground"); 510 | } 511 | } 512 | 513 | function updateLanguageStatusItem(item: vscode.LanguageStatusItem, version: semver.SemVer | null) { 514 | item.name = "Zig"; 515 | if (version) { 516 | item.text = `Zig ${version.toString()}`; 517 | item.detail = "Zig Version"; 518 | item.severity = vscode.LanguageStatusSeverity.Information; 519 | } else { 520 | item.text = "Zig not installed"; 521 | item.severity = vscode.LanguageStatusSeverity.Error; 522 | } 523 | item.command = { 524 | title: "Select Version", 525 | command: "zig.install", 526 | }; 527 | } 528 | 529 | function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) { 530 | if (zigExePath) { 531 | const envValue = path.dirname(zigExePath) + path.delimiter; 532 | // This will take priority over a user-defined PATH values. 533 | context.environmentVariableCollection.prepend("PATH", envValue); 534 | } else { 535 | context.environmentVariableCollection.delete("PATH"); 536 | } 537 | } 538 | 539 | /** 540 | * Should be called when one of the following events happen: 541 | * - The Zig executable has been modified 542 | * - A workspace configuration file has been modified (e.g. `.zigversion`, `build.zig.zon`) 543 | */ 544 | async function updateStatus(context: vscode.ExtensionContext): Promise { 545 | const zigVersion = zigProvider.getZigVersion(); 546 | const zigPath = zigProvider.getZigPath(); 547 | 548 | updateStatusItem(statusItem, zigVersion); 549 | updateLanguageStatusItem(languageStatusItem, zigVersion); 550 | updateZigEnvironmentVariableCollection(context, zigPath); 551 | 552 | // Try to check whether the Zig version satifies the `minimum_zig_version` in `build.zig.zon` 553 | 554 | if (!zigVersion || !zigPath) return; 555 | const buildZigZonMetadata = await parseBuildZigZon(); 556 | if (!buildZigZonMetadata) return; 557 | if (semver.gte(zigVersion, buildZigZonMetadata.minimumZigVersion)) return; 558 | 559 | statusItem.backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground"); 560 | 561 | void vscode.window 562 | .showWarningMessage( 563 | `Your Zig version '${zigVersion.toString()}' does not satisfy the minimum Zig version '${buildZigZonMetadata.minimumZigVersion.toString()}' of your project.`, 564 | "update Zig", 565 | "open build.zig.zon", 566 | ) 567 | .then(async (response) => { 568 | switch (response) { 569 | case undefined: 570 | break; 571 | case "update Zig": { 572 | // This will source the desired Zig version with `getWantedZigVersion` which may not satisfy the minimum Zig version. 573 | // This could happen for example when the a `.zigversion` specifies `0.12.0` but `minimum_zig_version` is `0.13.0`. 574 | // The extension would install `0.12.0` and then complain again. 575 | await installZig(context); 576 | break; 577 | } 578 | case "open build.zig.zon": { 579 | void vscode.window.showTextDocument(buildZigZonMetadata.document, { 580 | selection: buildZigZonMetadata.minimumZigVersionSourceRange, 581 | }); 582 | break; 583 | } 584 | } 585 | }); 586 | } 587 | 588 | async function getMirrors(context: vscode.ExtensionContext): Promise { 589 | const cacheKey = "zig-mirror-list"; 590 | let cached = context.globalState.get(cacheKey, { timestamp: 0, mirrors: "" }); 591 | 592 | const millisecondsInDay = 24 * 60 * 60 * 1000; 593 | if (new Date().getTime() - cached.timestamp > millisecondsInDay) { 594 | try { 595 | const response = await fetch("https://ziglang.org/download/community-mirrors.txt"); 596 | if (response.status !== 200) throw Error("invalid mirrors"); 597 | const mirrorList = await response.text(); 598 | cached = { 599 | timestamp: new Date().getTime(), 600 | mirrors: mirrorList, 601 | }; 602 | await context.globalState.update(cacheKey, cached); 603 | } catch { 604 | // Cannot fetch mirrors, rely on cache. 605 | } 606 | } 607 | 608 | return cached.mirrors 609 | .trim() 610 | .split("\n") 611 | .filter((u) => !!u) 612 | .map((u) => vscode.Uri.parse(u)); 613 | } 614 | 615 | export async function setupZig(context: vscode.ExtensionContext) { 616 | { 617 | // This check can be removed once enough time has passed so that most users switched to the new value 618 | 619 | // remove the `zig_install` directory from the global storage 620 | try { 621 | await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zig_install"), { 622 | recursive: true, 623 | useTrash: false, 624 | }); 625 | } catch {} 626 | 627 | // remove a `zig.path` that points to the global storage. 628 | const zigConfig = vscode.workspace.getConfiguration("zig"); 629 | const zigPath = zigConfig.get("path", ""); 630 | if (zigPath.startsWith(context.globalStorageUri.fsPath)) { 631 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "path", undefined, true); 632 | } 633 | 634 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "initialSetupDone", undefined, true); 635 | 636 | await context.workspaceState.update("zig-version", undefined); 637 | 638 | // Remove incorrect values in the global state that have been added by 639 | // an older version of the extension. 640 | for (const key of context.globalState.keys()) { 641 | if (!key.startsWith("zig-satisfying-version-")) continue; 642 | const value = context.globalState.get(key); 643 | if (value !== undefined && typeof value !== "string") { 644 | await context.globalState.update(key, undefined); 645 | } 646 | } 647 | 648 | // convert `zig.buildOnSave` to `zig.buildOnSaveProvider` 649 | { 650 | const inspect = zigConfig.inspect("buildOnSave"); 651 | if (inspect?.globalValue !== undefined) { 652 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSaveProvider", inspect.globalValue, true); 653 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSave", undefined, true); 654 | } 655 | if (inspect?.workspaceValue !== undefined) { 656 | await zigUtil.workspaceConfigUpdateNoThrow( 657 | zigConfig, 658 | "buildOnSaveProvider", 659 | inspect.workspaceValue, 660 | false, 661 | ); 662 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSave", undefined, false); 663 | } 664 | } 665 | 666 | // convert `zig.buildArgs` to `zig.buildOnSaveArgs` 667 | { 668 | const inspect = zigConfig.inspect("buildArgs"); 669 | if (inspect?.globalValue) { 670 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSaveArgs", inspect.globalValue, true); 671 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildArgs", undefined, true); 672 | } 673 | if (inspect?.workspaceValue) { 674 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildOnSaveArgs", inspect.workspaceValue, false); 675 | await zigUtil.workspaceConfigUpdateNoThrow(zigConfig, "buildArgs", undefined, false); 676 | } 677 | } 678 | } 679 | 680 | /// Workaround https://github.com/ziglang/zig/issues/21905 681 | switch (process.platform) { 682 | case "darwin": 683 | case "freebsd": 684 | case "openbsd": 685 | case "netbsd": 686 | case "haiku": 687 | vscode.workspace.onDidSaveTextDocument(async (document) => { 688 | if (document.languageId !== "zig") return; 689 | if (document.uri.scheme !== "file") return; 690 | 691 | const zigVersion = zigProvider.getZigVersion(); 692 | if (!zigVersion) return; 693 | 694 | if (semver.gte(zigVersion, "0.15.0-dev.1372+abf179533")) return; 695 | 696 | const fsPath = document.uri.fsPath; 697 | try { 698 | await fs.copyFile(fsPath, fsPath + ".tmp", fs.constants.COPYFILE_EXCL); 699 | await fs.rename(fsPath + ".tmp", fsPath); 700 | } catch {} 701 | }, context.subscriptions); 702 | break; 703 | case "aix": 704 | case "android": 705 | case "linux": 706 | case "sunos": 707 | case "win32": 708 | case "cygwin": 709 | break; 710 | } 711 | 712 | versionManagerConfig = { 713 | context: context, 714 | title: "Zig", 715 | exeName: "zig", 716 | extraTarArgs: ["--strip-components=1"], 717 | /** https://ziglang.org/download */ 718 | minisignKey: minisign.parseKey("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"), 719 | versionArg: "version", 720 | getMirrorUrls() { 721 | return getMirrors(context); 722 | }, 723 | canonicalUrl: { 724 | release: vscode.Uri.parse("https://ziglang.org/download"), 725 | nightly: vscode.Uri.parse("https://ziglang.org/builds"), 726 | }, 727 | getArtifactName(version) { 728 | const fileExtension = process.platform === "win32" ? "zip" : "tar.xz"; 729 | if ( 730 | (version.prerelease.length === 0 && semver.gte(version, "0.14.1")) || 731 | semver.gte(version, "0.15.0-dev.631+9a3540d61") 732 | ) { 733 | return `zig-${getZigArchName(version)}-${zigUtil.getZigOSName()}-${version.raw}.${fileExtension}`; 734 | } else { 735 | return `zig-${zigUtil.getZigOSName()}-${getZigArchName(version)}-${version.raw}.${fileExtension}`; 736 | } 737 | }, 738 | }; 739 | 740 | // Remove after some time has passed from the prefix change. 741 | await versionManager.convertOldInstallPrefixes(versionManagerConfig); 742 | 743 | zigProvider = new ZigProvider(); 744 | 745 | /** There two status items because there doesn't seem to be a way to pin a language status item by default. */ 746 | statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, -1); 747 | languageStatusItem = vscode.languages.createLanguageStatusItem("zig.status", { language: "zig" }); 748 | 749 | context.environmentVariableCollection.description = "Add Zig to PATH"; 750 | 751 | const watcher1 = vscode.workspace.createFileSystemWatcher("**/.zigversion"); 752 | const watcher2 = vscode.workspace.createFileSystemWatcher("**/build.zig.zon"); 753 | 754 | const refreshZigInstallation = zigUtil.asyncDebounce(async () => { 755 | if (!vscode.workspace.getConfiguration("zig").get("path")) { 756 | await installZig(context); 757 | } else { 758 | await updateStatus(context); 759 | } 760 | }, 200); 761 | 762 | if (!vscode.workspace.getConfiguration("zig").get("path")) { 763 | await installZig(context); 764 | } 765 | await updateStatus(context); 766 | 767 | const onDidChangeActiveTextEditor = (editor: vscode.TextEditor | undefined) => { 768 | if (editor?.document.languageId === "zig") { 769 | statusItem.show(); 770 | } else { 771 | statusItem.hide(); 772 | } 773 | }; 774 | onDidChangeActiveTextEditor(vscode.window.activeTextEditor); 775 | 776 | context.subscriptions.push( 777 | statusItem, 778 | languageStatusItem, 779 | vscode.commands.registerCommand("zig.install", async () => { 780 | await selectVersionAndInstall(context); 781 | }), 782 | vscode.workspace.onDidChangeConfiguration((change) => { 783 | if (change.affectsConfiguration("zig.version")) { 784 | void refreshZigInstallation(); 785 | } 786 | if (change.affectsConfiguration("zig.path")) { 787 | const result = zigProvider.resolveZigPathConfigOption(); 788 | if (result === undefined) return; // error message already reported 789 | if (result !== null) { 790 | zigProvider.set(result); 791 | } 792 | void refreshZigInstallation(); 793 | } 794 | }), 795 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 796 | zigProvider.onChange.event(() => { 797 | void updateStatus(context); 798 | }), 799 | watcher1.onDidCreate(refreshZigInstallation), 800 | watcher1.onDidChange(refreshZigInstallation), 801 | watcher1.onDidDelete(refreshZigInstallation), 802 | watcher1, 803 | watcher2.onDidCreate(refreshZigInstallation), 804 | watcher2.onDidChange(refreshZigInstallation), 805 | watcher2.onDidDelete(refreshZigInstallation), 806 | watcher2, 807 | ); 808 | } 809 | --------------------------------------------------------------------------------