├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── images ├── example.png └── zig-icon.png ├── language-configuration.json ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── minisign.ts ├── versionManager.ts ├── zigDiagnosticsProvider.ts ├── zigFormat.ts ├── zigMainCodeLens.ts ├── zigProvider.ts ├── zigSetup.ts ├── zigTestRunnerProvider.ts ├── zigUtil.ts └── zls.ts ├── syntaxes └── zig.tmLanguage.json └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - run: npm install 13 | - run: npm run build 14 | - run: npm run typecheck 15 | - run: npm run lint 16 | - run: npm run format:check 17 | 18 | - name: package extension 19 | run: | 20 | npx vsce package 21 | ls -lt *.vsix 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | *.md 4 | -------------------------------------------------------------------------------- /.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/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.10 2 | - Fix installing Zig 0.14.1 with new tarball name format 3 | 4 | ## 0.6.9 5 | - Add config option for arguments used in test runner (@omissis) 6 | - Add toggleMultilineStringLiteral command (@devhawk) 7 | - Resolve file extensions and relative paths when looking up exe (@Techatrix) 8 | - Improve error messages when executable path could not be resolved (@Techatrix) 9 | - Prevent concurrent installations of exes that cause all of them to fail 10 | 11 | ## 0.6.8 12 | - Fix regression in using config options placeholders in paths. 13 | 14 | ## 0.6.7 15 | - Windows: prefer tar.exe in system32 over $PATH if available (@Techatrix) 16 | - Add a workaround for [ziglang/zig#21905](https://github.com/ziglang/zig/issues/21905) on MacOS and BSDs (@Techatrix) 17 | 18 | ## 0.6.5 19 | - Prevent the extension from overriding options in `zls.json` with default values (@Techatrix) 20 | - Fix various version management edge cases (@Techatrix) 21 | 22 | ## 0.6.4 23 | - Prevent `.zig-cache` files from being automatically revealed in the explorer (@Techatrix) 24 | - Add missing PowerShell call operator when using the run button (@BlueBlue21) 25 | - Update ZLS options (@Techatrix) 26 | - Look for zig in `$PATH` before defaulting to latest tagged release (@Techatrix) 27 | - Offer to update workspace config when selecting Zig version (@Techatrix) 28 | - Selecting a different Zig version is no longer permanent by default and will reset 29 | to the previous version after a restart. 30 | - Properly clean unused installations (@Techatrix) 31 | 32 | ## 0.6.3 33 | - Fix boolean ZLS options being ignored (@Techatrix) 34 | - Always refer to ZLS as "ZLS language server" (@Techatrix) 35 | - Ensure paths sent to terminal are properly escaped 36 | 37 | ## 0.6.2 38 | - Don't open every zig file in the workspace just to look for tests (@Techatrix) 39 | - Sync ZLS options (@Techatrix) 40 | - handle `zig.path` edge cases (@Techatrix) 41 | 42 | ## 0.6.1 43 | - Fix formatting not working when `zig.formattingProvider` was set to ZLS (@Techatrix) 44 | 45 | ## 0.6.0 46 | - Introduce a new fully featured version manager (@Techatrix) 47 | - Add Zig test runner provider (@babaldotdev) 48 | 49 | ## 0.5.9 50 | - Improve formatting provider implementation and default to using ZLS formatter (@Techatrix) 51 | - Sync ZLS options (@Techatrix) 52 | - Update ZLS install tool (@Techatrix) 53 | 54 | ## 0.5.8 55 | - Fix updating a nightly version of Zig to a tagged release 56 | 57 | ## 0.5.7 58 | - Remove `zig.zls.openopenconfig` (@Techatrix) 59 | - Automatically add `zig` to `$PATH` in the integrated terminal (@Techatrix) 60 | - Change `zig.path` and `zig.zls.path` `$PATH` lookup from empty string to executable name (@Techatrix) 61 | - The extension will handle the migration automatically 62 | - Remove ouput channel for formatting (@Techatrix) 63 | - `ast-check` already provides the same errors inline. 64 | - Allow predefined variables in all configuration options (@Jarred-Sumner) 65 | 66 | ## 0.5.6 67 | - Fix initial setup always being skippped (@Techatrix) 68 | 69 | ## 0.5.5 70 | - Fix `zig.install` when no project is open 71 | - Rework extension internals (@Techatrix) 72 | - Show progress while downloading updates (@Techatrix) 73 | - Link release notes in new Zig version notification 74 | 75 | ## 0.5.4 76 | - Fix incorrect comparisons that caused ZLS not to be started automatically (@SuperAuguste) 77 | - Ensure `zig.path` is valid in `zig.zls.install` (@unlsycn) 78 | 79 | ## 0.5.3 80 | - Fix checks on config values and versions 81 | - Fix diagnostics from Zig compiler provider (@Techatrix) 82 | - Ensure all commands are registered properly on extension startup 83 | 84 | ## 0.5.2 85 | - Update ZLS config even when Zig is not found 86 | - Disable autofix by default 87 | - Make `zig.zls.path` and `zig.path` scoped as `machine-overridable` (@alexrp) 88 | - Fix ZLS debug trace (@alexrp) 89 | - Default `zig.path` and `zig.zls.path` to look up in PATH (@alexrp) 90 | 91 | ## 0.5.1 92 | - Always use global configuration. 93 | 94 | ## 0.5.0 95 | - Rework initial setup and installation management 96 | - Add new zls hint settings (@leecannon) 97 | - Update zls settings 98 | - Fix C pointer highlighting (@tokyo4j) 99 | 100 | ## 0.4.3 101 | - Fix checking for ZLS updates 102 | - Always check `PATH` when `zigPath` is set to empty string 103 | - Fix build on save when ast check provider is ZLS 104 | - Delete old zls binary before renaming to avoid Windows permission error 105 | 106 | ## 0.4.2 107 | - Fix `Specify path` adding a leading slash on windows (@sebastianhoffmann) 108 | - Fix path given to `tar` being quoted 109 | - Add option to use `zig` found in `PATH` as `zigPath` 110 | 111 | ## 0.4.1 112 | - Fix formatting when `zigPath` includes spaces 113 | - Do not default to `zig` in `PATH` anymore 114 | 115 | ## 0.4.0 116 | - Prompt to install if prebuilt zls doesn't exist in specified path 117 | - Add `string` to the `name` of `@""` tokens 118 | - Add functionality to manage Zig installation 119 | 120 | ## 0.3.2 121 | - Make formatting provider option an enum (@alichraghi) 122 | - Only apply onEnterRules when line starts with whitespace 123 | - Highlight `.zon` files (@Techatrix) 124 | - Fix `zls` not restarting after having been updated on macOS (@ngrilly) 125 | - Support `${workspaceFolder}` in `zig.zls.path` (@Jarred-Sumner) 126 | - Make semantic token configuration an enum 127 | 128 | ## 0.3.1 129 | - Fix missing Linux AArch64 ZLS auto-installer support 130 | 131 | ## 0.3.0 132 | - Update syntax to Zig 0.10.x 133 | - Add support for optional [Zig Language Server](https://github.com/zigtools/zls) integration 134 | - Support `ast-check` diagnostics without language server integration 135 | - Minor fixes for existing extension features 136 | 137 | ## 0.2.5 138 | - Syntax updates (@Vexu) 139 | 140 | ## 0.2.4 141 | - Update syntax (@Vexu) 142 | - Fix provideCodeActions regression (@mxmn) 143 | - Add build-on-save setting (@Swoogan) 144 | - Add stderr to output panel (@Swoogan) 145 | - Add zig build to command palette (@Swoogan) 146 | 147 | Thanks to @Vexu for taking over keeping the project up to date. 148 | 149 | ## 0.2.3 150 | - Syntax updates 151 | - Improve diagnostics regex (@emekoi) 152 | - Fix eol on format (@emekoi) 153 | - Trim URI's to fix path issue (@emekoi) 154 | - Update unicode escape pattern match (@hryx) 155 | - Add configuration option for showing output channel on error (@not-fl3) 156 | 157 | ## 0.2.2 158 | - Add new usingnamespace keyword 159 | 160 | ## 0.2.1 161 | - Add correct filename to zig fmt output (@gernest) 162 | - Stop zig fmt error output taking focus on save (@CurtisFenner) 163 | 164 | ## 0.2.0 165 | - Syntax updates 166 | - Add built-in functions to syntax (@jakewakefield) 167 | - Add anyerror keyword (@Hejsil) 168 | - Add allowzero keyword (@emekoi) 169 | - Correctly find root of package using build.zig file (@gernest) 170 | - Use output channels for zig fmt error messages (@gernest) 171 | - Simplify defaults for automatic code-formatting (@hchac) 172 | 173 | ## 0.1.9 174 | - Highlight all bit size int types (@Hejsil) 175 | 176 | ## 0.1.8 16th July 2018 177 | - Add auto-formatting using `zig fmt` 178 | - Syntax updates 179 | 180 | ## 0.1.7 - 2nd March 2018 181 | - Async keyword updates 182 | - Build on save support (@Hejsil) 183 | 184 | ## 0.1.6 - 21st January 2018 185 | - Keyword updates for new zig 186 | - Basic linting functionality (@Hejsil) 187 | 188 | ## 0.1.5 - 23rd October 2017 189 | - Fix and/or word boundary display 190 | 191 | ## 0.1.4 - 23rd October 2017 192 | - Fix C string literals and allow escape characters (@scurest) 193 | 194 | ## 0.1.3 - 11th September 2017 195 | - Fix file extension 196 | 197 | ## 0.1.2 - 31st August 2017 198 | - Add new i2/u2 and align keywords 199 | 200 | ## 0.1.1 - 8th August 2017 201 | - Add new float/integer types 202 | 203 | ## 0.1.0 - 15th July 2017 204 | - Minimal syntax highlighting support 205 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziglang/vscode-zig/59b1eb0e6ac0ad5fe3f571841c7952ff831613bc/images/example.png -------------------------------------------------------------------------------- /images/zig-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziglang/vscode-zig/59b1eb0e6ac0ad5fe3f571841c7952ff831613bc/images/zig-icon.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-zig", 3 | "displayName": "Zig Language", 4 | "description": "Language support for the Zig programming language", 5 | "version": "0.6.10", 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 | ], 23 | "main": "./out/extension", 24 | "contributes": { 25 | "configurationDefaults": { 26 | "[zig]": { 27 | "editor.formatOnSave": true, 28 | "editor.defaultFormatter": "ziglang.vscode-zig", 29 | "editor.stickyScroll.defaultModel": "foldingProviderModel", 30 | "files.eol": "\n" 31 | }, 32 | "explorer.autoRevealExclude": { 33 | "**/.zig-cache": true, 34 | "**/zig-cache": true 35 | } 36 | }, 37 | "languages": [ 38 | { 39 | "id": "zig", 40 | "extensions": [ 41 | ".zig", 42 | ".zon" 43 | ], 44 | "aliases": [ 45 | "Zig" 46 | ], 47 | "configuration": "./language-configuration.json" 48 | } 49 | ], 50 | "grammars": [ 51 | { 52 | "language": "zig", 53 | "scopeName": "source.zig", 54 | "path": "./syntaxes/zig.tmLanguage.json" 55 | } 56 | ], 57 | "problemMatchers": [ 58 | { 59 | "name": "zig", 60 | "owner": "zig", 61 | "fileLocation": [ 62 | "relative", 63 | "${workspaceFolder}" 64 | ], 65 | "pattern": { 66 | "regexp": "([^\\s]*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(note|error):\\s+(.*)$", 67 | "file": 1, 68 | "line": 2, 69 | "column": 3, 70 | "severity": 4, 71 | "message": 5 72 | } 73 | } 74 | ], 75 | "configuration": { 76 | "type": "object", 77 | "title": "Zig", 78 | "properties": { 79 | "zig.buildOnSave": { 80 | "type": "boolean", 81 | "default": false, 82 | "description": "Compiles code on file save using the settings specified in 'Build Option'. Should not be used with ZLS's build on save feature." 83 | }, 84 | "zig.buildOption": { 85 | "type": "string", 86 | "default": "build", 87 | "enum": [ 88 | "build", 89 | "build-exe", 90 | "build-lib", 91 | "build-obj" 92 | ], 93 | "description": "Which build command Zig should use to build the code." 94 | }, 95 | "zig.buildArgs": { 96 | "type": "array", 97 | "items": { 98 | "type": "string" 99 | }, 100 | "default": [], 101 | "description": "Extra arguments to passed to Zig." 102 | }, 103 | "zig.buildFilePath": { 104 | "type": "string", 105 | "default": "${workspaceFolder}/build.zig", 106 | "description": "The path to build.zig. This is only required if zig.buildOptions = build." 107 | }, 108 | "zig.path": { 109 | "scope": "machine-overridable", 110 | "type": "string", 111 | "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." 112 | }, 113 | "zig.version": { 114 | "scope": "resource", 115 | "type": "string", 116 | "description": "Specify which Zig version should be installed. Takes priority over a `.zigversion` file or a `build.zig.zon` with `minimum_zig_version`." 117 | }, 118 | "zig.formattingProvider": { 119 | "scope": "resource", 120 | "type": "string", 121 | "description": "Whether to enable formatting", 122 | "enum": [ 123 | "off", 124 | "extension", 125 | "zls" 126 | ], 127 | "enumItemLabels": [ 128 | "Off", 129 | "Extension", 130 | "ZLS language server" 131 | ], 132 | "enumDescriptions": [ 133 | "Disable formatting", 134 | "Provide formatting by directly invoking `zig fmt`", 135 | "Provide formatting by using ZLS (which matches `zig fmt`)" 136 | ], 137 | "default": "zls" 138 | }, 139 | "zig.testArgs": { 140 | "type": "array", 141 | "items": { 142 | "type": "string" 143 | }, 144 | "default": [ 145 | "test", 146 | "--test-filter", 147 | "${filter}", 148 | "${path}" 149 | ], 150 | "description": "Arguments to pass to 'zig' for running tests. Supported variables: ${filter}, ${path}." 151 | }, 152 | "zig.zls.debugLog": { 153 | "scope": "resource", 154 | "type": "boolean", 155 | "description": "Enable debug logging in release builds of ZLS." 156 | }, 157 | "zig.zls.trace.server": { 158 | "scope": "window", 159 | "type": "string", 160 | "description": "Traces the communication between VS Code and the language server.", 161 | "enum": [ 162 | "off", 163 | "messages", 164 | "verbose" 165 | ], 166 | "default": "off" 167 | }, 168 | "zig.zls.enabled": { 169 | "scope": "resource", 170 | "type": "string", 171 | "description": "Whether to enable the optional ZLS language server", 172 | "enum": [ 173 | "ask", 174 | "off", 175 | "on" 176 | ], 177 | "default": "ask" 178 | }, 179 | "zig.zls.path": { 180 | "scope": "machine-overridable", 181 | "type": "string", 182 | "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.", 183 | "format": "path" 184 | }, 185 | "zig.zls.enableSnippets": { 186 | "scope": "resource", 187 | "type": "boolean", 188 | "description": "Enables snippet completions when the client also supports them", 189 | "default": true 190 | }, 191 | "zig.zls.enableArgumentPlaceholders": { 192 | "scope": "resource", 193 | "type": "boolean", 194 | "description": "Whether to enable function argument placeholder completions", 195 | "default": true 196 | }, 197 | "zig.zls.completionLabelDetails": { 198 | "scope": "resource", 199 | "type": "boolean", 200 | "description": "Whether to show the function signature in completion results. May improve readability in some editors when disabled", 201 | "default": true 202 | }, 203 | "zig.zls.enableBuildOnSave": { 204 | "scope": "resource", 205 | "type": "boolean", 206 | "description": "Whether to enable build-on-save diagnostics. Will be automatically enabled if the `build.zig` has declared a 'check' step.\n\nFor more infromation, checkout the [Build-On-Save](https://zigtools.org/zls/guides/build-on-save/) Guide.", 207 | "default": null 208 | }, 209 | "zig.zls.buildOnSaveArgs": { 210 | "scope": "resource", 211 | "type": "array", 212 | "description": "Specify which arguments should be passed to Zig when running build-on-save.\n\nIf the `build.zig` has declared a 'check' step, it will be preferred over the default 'install' step.", 213 | "default": [] 214 | }, 215 | "zig.zls.semanticTokens": { 216 | "scope": "resource", 217 | "type": "string", 218 | "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.", 219 | "enum": [ 220 | "none", 221 | "partial", 222 | "full" 223 | ], 224 | "default": "partial" 225 | }, 226 | "zig.zls.inlayHintsShowVariableTypeHints": { 227 | "scope": "resource", 228 | "type": "boolean", 229 | "description": "Enable inlay hints for variable types", 230 | "default": true 231 | }, 232 | "zig.zls.inlayHintsShowStructLiteralFieldType": { 233 | "scope": "resource", 234 | "type": "boolean", 235 | "description": "Enable inlay hints for fields in struct and union literals", 236 | "default": true 237 | }, 238 | "zig.zls.inlayHintsShowParameterName": { 239 | "scope": "resource", 240 | "type": "boolean", 241 | "description": "Enable inlay hints for parameter names", 242 | "default": true 243 | }, 244 | "zig.zls.inlayHintsShowBuiltin": { 245 | "scope": "resource", 246 | "type": "boolean", 247 | "description": "Enable inlay hints for builtin functions", 248 | "default": true 249 | }, 250 | "zig.zls.inlayHintsExcludeSingleArgument": { 251 | "scope": "resource", 252 | "type": "boolean", 253 | "description": "Don't show inlay hints for single argument calls", 254 | "default": true 255 | }, 256 | "zig.zls.inlayHintsHideRedundantParamNames": { 257 | "scope": "resource", 258 | "type": "boolean", 259 | "description": "Hides inlay hints when parameter name matches the identifier (e.g. `foo: foo`)", 260 | "default": false 261 | }, 262 | "zig.zls.inlayHintsHideRedundantParamNamesLastToken": { 263 | "scope": "resource", 264 | "type": "boolean", 265 | "description": "Hides inlay hints when parameter name matches the last token of a parameter node (e.g. `foo: bar.foo`, `foo: &foo`)", 266 | "default": false 267 | }, 268 | "zig.zls.warnStyle": { 269 | "scope": "resource", 270 | "type": "boolean", 271 | "description": "Enables warnings for style guideline mismatches", 272 | "default": false 273 | }, 274 | "zig.zls.highlightGlobalVarDeclarations": { 275 | "scope": "resource", 276 | "type": "boolean", 277 | "description": "Whether to highlight global var declarations", 278 | "default": false 279 | }, 280 | "zig.zls.skipStdReferences": { 281 | "scope": "resource", 282 | "type": "boolean", 283 | "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", 284 | "default": false 285 | }, 286 | "zig.zls.preferAstCheckAsChildProcess": { 287 | "scope": "resource", 288 | "type": "boolean", 289 | "description": "Favor using `zig ast-check` instead of the builtin one", 290 | "default": true 291 | }, 292 | "zig.zls.builtinPath": { 293 | "scope": "resource", 294 | "type": "string", 295 | "description": "Override the path to 'builtin' module. Automatically resolved if unset.", 296 | "format": "path" 297 | }, 298 | "zig.zls.zigLibPath": { 299 | "scope": "resource", 300 | "type": "string", 301 | "description": "Override the Zig library path. Will be automatically resolved using the 'zig_exe_path'.", 302 | "format": "path" 303 | }, 304 | "zig.zls.buildRunnerPath": { 305 | "scope": "resource", 306 | "type": "string", 307 | "description": "Specify a custom build runner to resolve build system information.", 308 | "format": "path" 309 | }, 310 | "zig.zls.globalCachePath": { 311 | "scope": "resource", 312 | "type": "string", 313 | "description": "Path to a directory that will be used as zig's cache. Will default to `${KnownFolders.Cache}/zls`.", 314 | "format": "path" 315 | }, 316 | "zig.zls.additionalOptions": { 317 | "scope": "resource", 318 | "type": "object", 319 | "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.", 320 | "default": {}, 321 | "additionalProperties": false, 322 | "patternProperties": { 323 | "^zig\\.zls\\.[a-z]+[A-Z0-9][a-z0-9]+[A-Za-z0-9]*$": {} 324 | } 325 | } 326 | } 327 | }, 328 | "commands": [ 329 | { 330 | "command": "zig.run", 331 | "title": "Run Zig", 332 | "category": "Zig", 333 | "description": "Run the current Zig project / file" 334 | }, 335 | { 336 | "command": "zig.debug", 337 | "title": "Debug Zig", 338 | "category": "Zig", 339 | "description": "Debug the current Zig project / file" 340 | }, 341 | { 342 | "command": "zig.build.workspace", 343 | "title": "Build Workspace", 344 | "category": "Zig", 345 | "description": "Build the current project using 'zig build'" 346 | }, 347 | { 348 | "command": "zig.install", 349 | "title": "Install Zig", 350 | "category": "Zig Setup" 351 | }, 352 | { 353 | "command": "zig.toggleMultilineStringLiteral", 354 | "title": "Toggle Multiline String Literal", 355 | "category": "Zig" 356 | }, 357 | { 358 | "command": "zig.zls.enable", 359 | "title": "Enable Language Server", 360 | "category": "ZLS language server" 361 | }, 362 | { 363 | "command": "zig.zls.startRestart", 364 | "title": "Start / Restart Language Server", 365 | "category": "ZLS language server" 366 | }, 367 | { 368 | "command": "zig.zls.stop", 369 | "title": "Stop Language Server", 370 | "category": "ZLS language server" 371 | } 372 | ], 373 | "keybindings": [ 374 | { 375 | "command": "zig.toggleMultilineStringLiteral", 376 | "key": "alt+m alt+s", 377 | "when": "editorTextFocus && editorLangId == 'zig'" 378 | } 379 | ], 380 | "jsonValidation": [ 381 | { 382 | "fileMatch": "zls.json", 383 | "url": "https://raw.githubusercontent.com/zigtools/zls/master/schema.json" 384 | } 385 | ] 386 | }, 387 | "scripts": { 388 | "vscode:prepublish": "npm run build-base -- --minify", 389 | "build-base": "esbuild --bundle --external:vscode src/extension.ts --outdir=out --platform=node --target=node20 --format=cjs", 390 | "build": "npm run build-base -- --sourcemap", 391 | "watch": "npm run build-base -- --sourcemap --watch", 392 | "test": "npm run compile && node ./node_modules/vscode/bin/test", 393 | "typecheck": "tsc --noEmit", 394 | "format": "prettier --write .", 395 | "format:check": "prettier --check .", 396 | "lint": "eslint" 397 | }, 398 | "devDependencies": { 399 | "@types/libsodium-wrappers": "^0.7.14", 400 | "@types/lodash-es": "^4.17.12", 401 | "@types/node": "^20.0.0", 402 | "@types/semver": "^7.5.8", 403 | "@types/vscode": "^1.80.0", 404 | "@types/which": "^2.0.1", 405 | "@vscode/vsce": "^2.24.0", 406 | "esbuild": "^0.25.0", 407 | "eslint": "^9.0.0", 408 | "eslint-config-prettier": "^9.1.0", 409 | "prettier": "3.2.5", 410 | "typescript": "^5.4.3", 411 | "typescript-eslint": "^8.0.0" 412 | }, 413 | "dependencies": { 414 | "camelcase": "^7.0.1", 415 | "libsodium-wrappers": "^0.7.15", 416 | "lodash-es": "^4.17.21", 417 | "semver": "^7.5.2", 418 | "vscode-languageclient": "10.0.0-next.15", 419 | "which": "^3.0.0" 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import { activate as activateZls, deactivate as deactivateZls } from "./zls"; 4 | import ZigDiagnosticsProvider from "./zigDiagnosticsProvider"; 5 | import ZigMainCodeLensProvider from "./zigMainCodeLens"; 6 | import ZigTestRunnerProvider from "./zigTestRunnerProvider"; 7 | import { registerDocumentFormatting } from "./zigFormat"; 8 | import { setupZig } from "./zigSetup"; 9 | 10 | export async function activate(context: vscode.ExtensionContext) { 11 | await setupZig(context).finally(() => { 12 | const compiler = new ZigDiagnosticsProvider(); 13 | compiler.activate(context.subscriptions); 14 | 15 | context.subscriptions.push(registerDocumentFormatting()); 16 | 17 | const testRunner = new ZigTestRunnerProvider(); 18 | testRunner.activate(context.subscriptions); 19 | 20 | ZigMainCodeLensProvider.registerCommands(context); 21 | context.subscriptions.push( 22 | vscode.languages.registerCodeLensProvider( 23 | { language: "zig", scheme: "file" }, 24 | new ZigMainCodeLensProvider(), 25 | ), 26 | vscode.commands.registerCommand("zig.toggleMultilineStringLiteral", toggleMultilineStringLiteral), 27 | ); 28 | 29 | void activateZls(context); 30 | }); 31 | } 32 | 33 | export async function deactivate() { 34 | await deactivateZls(); 35 | } 36 | 37 | async function toggleMultilineStringLiteral() { 38 | const editor = vscode.window.activeTextEditor; 39 | if (!editor) return; 40 | const { document, selection } = editor; 41 | if (document.languageId !== "zig") return; 42 | 43 | let newText = ""; 44 | let range = new vscode.Range(selection.start, selection.end); 45 | 46 | const firstLine = document.lineAt(selection.start.line); 47 | const nonWhitespaceIndex = firstLine.firstNonWhitespaceCharacterIndex; 48 | 49 | for (let lineNum = selection.start.line; lineNum <= selection.end.line; lineNum++) { 50 | const line = document.lineAt(lineNum); 51 | 52 | const isMLSL = line.text.slice(line.firstNonWhitespaceCharacterIndex).startsWith("\\\\"); 53 | const breakpoint = Math.min(nonWhitespaceIndex, line.firstNonWhitespaceCharacterIndex); 54 | 55 | const newLine = isMLSL 56 | ? line.text.slice(0, line.firstNonWhitespaceCharacterIndex) + 57 | line.text.slice(line.firstNonWhitespaceCharacterIndex).slice(2) 58 | : line.isEmptyOrWhitespace 59 | ? " ".repeat(nonWhitespaceIndex) + "\\\\" 60 | : line.text.slice(0, breakpoint) + "\\\\" + line.text.slice(breakpoint); 61 | newText += newLine; 62 | if (lineNum < selection.end.line) newText += "\n"; 63 | 64 | range = range.union(line.range); 65 | } 66 | 67 | await editor.edit((builder) => { 68 | builder.replace(range, newText); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /src/minisign.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ported from: https://github.com/mlugg/setup-zig/blob/main/main.js (MIT) 3 | */ 4 | 5 | import sodium from "libsodium-wrappers"; 6 | 7 | export interface Key { 8 | id: Buffer; 9 | key: Buffer; 10 | } 11 | 12 | // Parse a minisign key represented as a base64 string. 13 | // Throws exceptions on invalid keys. 14 | export function parseKey(keyString: string): Key { 15 | const keyInfo = Buffer.from(keyString, "base64"); 16 | 17 | const id = keyInfo.subarray(2, 10); 18 | const key = keyInfo.subarray(10); 19 | 20 | if (key.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) { 21 | throw new Error("invalid public key given"); 22 | } 23 | 24 | return { 25 | id: id, 26 | key: key, 27 | }; 28 | } 29 | 30 | export interface Signature { 31 | algorithm: Buffer; 32 | keyID: Buffer; 33 | signature: Buffer; 34 | } 35 | 36 | // Parse a buffer containing the contents of a minisign signature file. 37 | // Throws exceptions on invalid signature files. 38 | export function parseSignature(sigBuf: Buffer): Signature { 39 | const untrustedHeader = Buffer.from("untrusted comment: "); 40 | 41 | // Validate untrusted comment header, and skip 42 | if (!sigBuf.subarray(0, untrustedHeader.byteLength).equals(untrustedHeader)) { 43 | throw new Error("file format not recognised"); 44 | } 45 | sigBuf = sigBuf.subarray(untrustedHeader.byteLength); 46 | 47 | // Skip untrusted comment 48 | sigBuf = sigBuf.subarray(sigBuf.indexOf("\n") + 1); 49 | 50 | // Read and skip signature info 51 | const sigInfoEnd = sigBuf.indexOf("\n"); 52 | const sigInfo = Buffer.from(sigBuf.subarray(0, sigInfoEnd).toString(), "base64"); 53 | sigBuf = sigBuf.subarray(sigInfoEnd + 1); 54 | 55 | // Extract components of signature info 56 | const algorithm = sigInfo.subarray(0, 2); 57 | const keyID = sigInfo.subarray(2, 10); 58 | const signature = sigInfo.subarray(10); 59 | 60 | // We don't look at the trusted comment or global signature, so we're done. 61 | 62 | return { 63 | algorithm: algorithm, 64 | keyID: keyID, 65 | signature: signature, 66 | }; 67 | } 68 | 69 | // Given a parsed key, parsed signature file, and raw file content, verifies the 70 | // signature. Does not throw. Returns 'true' if the signature is valid for this 71 | // file, 'false' otherwise. 72 | export function verifySignature(pubkey: Key, signature: Signature, fileContent: Buffer) { 73 | let signedContent; 74 | if (signature.algorithm.equals(Buffer.from("ED"))) { 75 | signedContent = sodium.crypto_generichash(sodium.crypto_generichash_BYTES_MAX, fileContent); 76 | } else { 77 | signedContent = fileContent; 78 | } 79 | 80 | if (!signature.keyID.equals(pubkey.id)) { 81 | return false; 82 | } 83 | 84 | if (!sodium.crypto_sign_verify_detached(signature.signature, signedContent, pubkey.key)) { 85 | return false; 86 | } 87 | 88 | // Since we don't use the trusted comment, we don't bother verifying the global signature. 89 | // If we were to start using the trusted comment for any purpose, we must add this. 90 | 91 | return true; 92 | } 93 | -------------------------------------------------------------------------------- /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 { getHostZigName, getVersion, getZigArchName, getZigOSName } 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 | mirrorUrls: vscode.Uri[]; 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 = `${getHostZigName()}-${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 = [...config.mirrorUrls] 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 = `${getHostZigName()}-${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 = vscode.Uri.joinPath(mirrorUrl, fileName); 161 | /** https://github.com/mlugg/setup-zig adds a `?source=github-actions` query parameter so we add our own. */ 162 | const artifactUrlWithQuery = artifactUrl.with({ query: "source=vscode-zig" }); 163 | 164 | const artifactMinisignUrl = vscode.Uri.joinPath(mirrorUrl, `${fileName}.minisig`); 165 | const artifactMinisignUrlWithQuery = artifactMinisignUrl.with({ query: "source=vscode-zig" }); 166 | 167 | const signatureResponse = await fetch(artifactMinisignUrlWithQuery.toString(), { 168 | signal: abortController.signal, 169 | }); 170 | 171 | if (signatureResponse.status !== 200) { 172 | throw new Error(`${signatureResponse.statusText} (${signatureResponse.status.toString()})`); 173 | } 174 | 175 | let artifactResponse = await fetch(artifactUrlWithQuery.toString(), { 176 | signal: abortController.signal, 177 | }); 178 | 179 | if (artifactResponse.status !== 200) { 180 | throw new Error(`${artifactResponse.statusText} (${artifactResponse.status.toString()})`); 181 | } 182 | 183 | progress.report({ message: `downloading from ${mirrorName}` }); 184 | 185 | const signatureData = Buffer.from(await signatureResponse.arrayBuffer()); 186 | 187 | let contentLength = artifactResponse.headers.has("content-length") 188 | ? Number(artifactResponse.headers.get("content-length")) 189 | : null; 190 | if (!Number.isFinite(contentLength)) contentLength = null; 191 | 192 | if (contentLength) { 193 | let receivedLength = 0; 194 | const progressStream = new TransformStream<{ length: number }>({ 195 | transform(chunk, controller) { 196 | receivedLength += chunk.length; 197 | const increment = (chunk.length / contentLength) * 100; 198 | const currentProgress = (receivedLength / contentLength) * 100; 199 | progress.report({ 200 | message: `downloading tarball ${currentProgress.toFixed()}%`, 201 | increment: increment, 202 | }); 203 | controller.enqueue(chunk); 204 | }, 205 | }); 206 | artifactResponse = new Response(artifactResponse.body?.pipeThrough(progressStream)); 207 | } 208 | const artifactData = Buffer.from(await artifactResponse.arrayBuffer()); 209 | 210 | progress.report({ message: "Verifying Signature..." }); 211 | 212 | const signature = minisign.parseSignature(signatureData); 213 | if (!minisign.verifySignature(config.minisignKey, signature, artifactData)) { 214 | try { 215 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 216 | } catch {} 217 | throw new Error(`signature verification failed for '${artifactUrl.toString()}'`); 218 | } 219 | 220 | try { 221 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 222 | } catch {} 223 | await vscode.workspace.fs.createDirectory(installDir); 224 | await vscode.workspace.fs.writeFile(tarballUri, artifactData); 225 | 226 | progress.report({ message: "Extracting..." }); 227 | try { 228 | await execFile(tarPath, ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(config.extraTarArgs), { 229 | signal: abortController.signal, 230 | timeout: 60000, // 60 seconds 231 | }); 232 | } catch (err) { 233 | try { 234 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 235 | } catch {} 236 | if (err instanceof Error) { 237 | throw new Error(`Failed to extract ${config.title} tarball: ${err.message}`); 238 | } else { 239 | throw err; 240 | } 241 | } finally { 242 | try { 243 | await vscode.workspace.fs.delete(tarballUri, { useTrash: false }); 244 | } catch {} 245 | } 246 | 247 | const exeVersion = getVersion(exeUri.fsPath, config.versionArg); 248 | if (!exeVersion || exeVersion.compare(version) !== 0) { 249 | try { 250 | await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); 251 | } catch {} 252 | // a mirror may provide the wrong version 253 | throw new Error(`Failed to validate version of ${config.title} installation!`); 254 | } 255 | 256 | await chmod(exeUri.fsPath, 0o755); 257 | 258 | try { 259 | await removeUnusedInstallations(config); 260 | } catch (err) { 261 | if (err instanceof Error) { 262 | void vscode.window.showWarningMessage( 263 | `Failed to uninstall unused ${config.title} versions: ${err.message}`, 264 | ); 265 | } else { 266 | void vscode.window.showWarningMessage(`Failed to uninstall unused ${config.title} versions`); 267 | } 268 | } 269 | 270 | return exeUri.fsPath; 271 | } 272 | 273 | /** Returns all locally installed versions */ 274 | export async function query(config: Config): Promise { 275 | const available: semver.SemVer[] = []; 276 | const prefix = getHostZigName(); 277 | 278 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 279 | try { 280 | for (const [name] of await vscode.workspace.fs.readDirectory(storageDir)) { 281 | if (name.startsWith(prefix)) { 282 | available.push(new semver.SemVer(name.substring(prefix.length + 1))); 283 | } 284 | } 285 | } catch (e) { 286 | if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") { 287 | return []; 288 | } 289 | throw e; 290 | } 291 | 292 | return available; 293 | } 294 | 295 | async function getTarExePath(): Promise { 296 | if (process.platform === "win32" && process.env["SYSTEMROOT"]) { 297 | // We may be running from within Git Bash which adds GNU tar to 298 | // the $PATH but we need bsdtar to extract zip files so we look 299 | // in the system directory before falling back to the $PATH. 300 | // See https://github.com/ziglang/vscode-zig/issues/382 301 | const tarPath = `${process.env["SYSTEMROOT"]}\\system32\\tar.exe`; 302 | try { 303 | await vscode.workspace.fs.stat(vscode.Uri.file(tarPath)); 304 | return tarPath; 305 | } catch {} 306 | } 307 | return await which("tar", { nothrow: true }); 308 | } 309 | 310 | /** Set the last access time of the (installed) version. */ 311 | async function setLastAccessTime(config: Config, version: semver.SemVer): Promise { 312 | await config.context.globalState.update( 313 | `${config.exeName}-last-access-time-${getHostZigName()}-${version.raw}`, 314 | Date.now(), 315 | ); 316 | } 317 | 318 | /** Remove installations with the oldest last access time until at most `VersionManager.maxInstallCount` versions remain. */ 319 | async function removeUnusedInstallations(config: Config) { 320 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 321 | 322 | const keys: { key: string; installDir: vscode.Uri; lastAccessTime: number }[] = []; 323 | 324 | try { 325 | for (const [name, fileType] of await vscode.workspace.fs.readDirectory(storageDir)) { 326 | const key = `${config.exeName}-last-access-time-${name}`; 327 | const uri = vscode.Uri.joinPath(storageDir, name); 328 | const lastAccessTime = config.context.globalState.get(key); 329 | 330 | if (!lastAccessTime || fileType !== vscode.FileType.Directory) { 331 | await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false }); 332 | } else { 333 | keys.push({ 334 | key: key, 335 | installDir: uri, 336 | lastAccessTime: lastAccessTime, 337 | }); 338 | } 339 | } 340 | } catch (e) { 341 | if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") return; 342 | throw e; 343 | } 344 | 345 | keys.sort((lhs, rhs) => rhs.lastAccessTime - lhs.lastAccessTime); 346 | 347 | for (const item of keys.slice(maxInstallCount)) { 348 | await vscode.workspace.fs.delete(item.installDir, { recursive: true, useTrash: false }); 349 | await config.context.globalState.update(item.key, undefined); 350 | } 351 | } 352 | 353 | /** Remove after some time has passed from the prefix change. */ 354 | export async function convertOldInstallPrefixes(config: Config): Promise { 355 | const oldPrefix = `${getZigOSName()}-${getZigArchName()}`; 356 | const newPrefix = `${getZigArchName()}-${getZigOSName()}`; 357 | 358 | const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); 359 | try { 360 | for (const [name] of await vscode.workspace.fs.readDirectory(storageDir)) { 361 | if (!name.startsWith(oldPrefix)) continue; 362 | 363 | const version = name.substring(oldPrefix.length + 1); 364 | const oldInstallDir = vscode.Uri.joinPath(storageDir, name); 365 | const newInstallDir = vscode.Uri.joinPath(storageDir, `${newPrefix}-${version}`); 366 | try { 367 | await vscode.workspace.fs.rename(oldInstallDir, newInstallDir); 368 | } catch { 369 | // A possible cause could be that the user downgraded the extension 370 | // version to install it to the old install prefix while it was 371 | // already present with the new prefix. 372 | } 373 | 374 | const oldKey = `${config.exeName}-last-access-time-${oldPrefix}-${version}`; 375 | const newKey = `${config.exeName}-last-access-time-${newPrefix}-${version}`; 376 | const lastAccessTime = config.context.globalState.get(oldKey); 377 | if (lastAccessTime !== undefined) { 378 | config.context.globalState.update(newKey, lastAccessTime); 379 | config.context.globalState.update(oldKey, undefined); 380 | } 381 | } 382 | } catch {} 383 | } 384 | -------------------------------------------------------------------------------- /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 { DebouncedFunc, throttle } from "lodash-es"; 8 | 9 | import * as semver from "semver"; 10 | import * as zls from "./zls"; 11 | import { handleConfigOption } from "./zigUtil"; 12 | import { zigProvider } from "./zigSetup"; 13 | 14 | export default class ZigDiagnosticsProvider { 15 | private buildDiagnostics!: vscode.DiagnosticCollection; 16 | private astDiagnostics!: vscode.DiagnosticCollection; 17 | private dirtyChange = new WeakMap(); 18 | 19 | private doASTGenErrorCheck: DebouncedFunc<(change: vscode.TextDocumentChangeEvent) => void>; 20 | private doCompile: DebouncedFunc<(textDocument: vscode.TextDocument) => void>; 21 | 22 | constructor() { 23 | this.doASTGenErrorCheck = throttle( 24 | (change: vscode.TextDocumentChangeEvent) => { 25 | this._doASTGenErrorCheck(change.document); 26 | }, 27 | 16, 28 | { 29 | trailing: true, 30 | }, 31 | ); 32 | this.doCompile = throttle((textDocument: vscode.TextDocument) => { 33 | this._doCompile(textDocument); 34 | }, 60); 35 | } 36 | 37 | public activate(subscriptions: vscode.Disposable[]) { 38 | this.buildDiagnostics = vscode.languages.createDiagnosticCollection("zig"); 39 | this.astDiagnostics = vscode.languages.createDiagnosticCollection("zig"); 40 | 41 | subscriptions.push( 42 | this.buildDiagnostics, 43 | this.astDiagnostics, 44 | vscode.workspace.onDidChangeTextDocument((change) => { 45 | this.maybeDoASTGenErrorCheck(change); 46 | }), 47 | vscode.workspace.onDidSaveTextDocument((change) => { 48 | this.maybeDoBuildOnSave(change); 49 | }), 50 | vscode.commands.registerCommand("zig.build.workspace", () => { 51 | if (!vscode.window.activeTextEditor) return; 52 | this.doCompile(vscode.window.activeTextEditor.document); 53 | }), 54 | ); 55 | } 56 | 57 | maybeDoASTGenErrorCheck(change: vscode.TextDocumentChangeEvent) { 58 | if (change.document.languageId !== "zig") { 59 | return; 60 | } 61 | if (zls.client !== null) { 62 | this.astDiagnostics.clear(); 63 | return; 64 | } 65 | if (change.document.isClosed) { 66 | this.astDiagnostics.delete(change.document.uri); 67 | } 68 | 69 | this.doASTGenErrorCheck(change); 70 | } 71 | 72 | maybeDoBuildOnSave(document: vscode.TextDocument) { 73 | if (document.languageId !== "zig") return; 74 | if (document.isUntitled) return; 75 | 76 | const config = vscode.workspace.getConfiguration("zig"); 77 | if ( 78 | config.get("buildOnSave") && 79 | this.dirtyChange.has(document.uri) && 80 | this.dirtyChange.get(document.uri) !== document.isDirty && 81 | !document.isDirty 82 | ) { 83 | this.doCompile(document); 84 | } 85 | 86 | this.dirtyChange.set(document.uri, document.isDirty); 87 | } 88 | 89 | private _doASTGenErrorCheck(textDocument: vscode.TextDocument) { 90 | const zigPath = zigProvider.getZigPath(); 91 | const zigVersion = zigProvider.getZigVersion(); 92 | if (!zigPath || !zigVersion) return; 93 | 94 | const args = ["ast-check"]; 95 | 96 | const addedZonSupportVersion = new semver.SemVer("0.14.0-dev.2508+7e8be2136"); 97 | if (path.extname(textDocument.fileName) === ".zon" && semver.gte(zigVersion, addedZonSupportVersion)) { 98 | args.push("--zon"); 99 | } 100 | 101 | const { error, stderr } = childProcess.spawnSync(zigPath, args, { 102 | input: textDocument.getText(), 103 | maxBuffer: 10 * 1024 * 1024, // 10MB 104 | encoding: "utf8", 105 | stdio: ["pipe", "ignore", "pipe"], 106 | timeout: 5000, // 5 seconds 107 | }); 108 | 109 | if (error ?? stderr.length === 0) { 110 | this.astDiagnostics.delete(textDocument.uri); 111 | return; 112 | } 113 | 114 | const diagnostics: Record = {}; 115 | const regex = /(\S.*):(\d*):(\d*): ([^:]*): (.*)/g; 116 | 117 | for (let match = regex.exec(stderr); match; match = regex.exec(stderr)) { 118 | const filePath = textDocument.uri.fsPath; 119 | 120 | const line = parseInt(match[2]) - 1; 121 | const column = parseInt(match[3]) - 1; 122 | const type = match[4]; 123 | const message = match[5]; 124 | 125 | const severity = 126 | type.trim().toLowerCase() === "error" 127 | ? vscode.DiagnosticSeverity.Error 128 | : vscode.DiagnosticSeverity.Information; 129 | const range = new vscode.Range(line, column, line, Infinity); 130 | 131 | const diagnosticArray = diagnostics[filePath] ?? []; 132 | diagnosticArray.push(new vscode.Diagnostic(range, message, severity)); 133 | diagnostics[filePath] = diagnosticArray; 134 | } 135 | 136 | for (const filePath in diagnostics) { 137 | const diagnostic = diagnostics[filePath]; 138 | this.astDiagnostics.set(textDocument.uri, diagnostic); 139 | } 140 | } 141 | 142 | private _doCompile(textDocument: vscode.TextDocument) { 143 | const config = vscode.workspace.getConfiguration("zig"); 144 | 145 | const zigPath = zigProvider.getZigPath(); 146 | if (!zigPath) return; 147 | 148 | const buildOption = config.get("buildOption", "build"); 149 | const processArg: string[] = [buildOption]; 150 | let workspaceFolder = vscode.workspace.getWorkspaceFolder(textDocument.uri); 151 | if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 152 | workspaceFolder = vscode.workspace.workspaceFolders[0]; 153 | } 154 | if (!workspaceFolder) return; 155 | const cwd = workspaceFolder.uri.fsPath; 156 | 157 | switch (buildOption) { 158 | case "build": { 159 | const buildFilePath = config.get("buildFilePath"); 160 | if (!buildFilePath) break; 161 | processArg.push("--build-file"); 162 | try { 163 | processArg.push(path.resolve(handleConfigOption(buildFilePath))); 164 | } catch { 165 | // 166 | } 167 | break; 168 | } 169 | default: 170 | processArg.push(textDocument.fileName); 171 | break; 172 | } 173 | 174 | const extraArgs = config.get("buildArgs", []); 175 | extraArgs.forEach((element) => { 176 | processArg.push(element); 177 | }); 178 | 179 | let decoded = ""; 180 | const child = childProcess.spawn(zigPath, processArg, { cwd }); 181 | if (child.pid) { 182 | child.stderr.on("data", (data: string) => { 183 | decoded += data; 184 | }); 185 | child.stdout.on("end", () => { 186 | this.doCompile.cancel(); 187 | const diagnostics: Record = {}; 188 | const regex = /(\S.*):(\d*):(\d*): ([^:]*): (.*)/g; 189 | 190 | this.buildDiagnostics.clear(); 191 | for (let match = regex.exec(decoded); match; match = regex.exec(decoded)) { 192 | let resolvedPath = match[1].trim(); 193 | try { 194 | if (!resolvedPath.includes(cwd)) { 195 | resolvedPath = path.resolve(cwd, resolvedPath); 196 | } 197 | } catch { 198 | // 199 | } 200 | 201 | const line = parseInt(match[2]) - 1; 202 | const column = parseInt(match[3]) - 1; 203 | const type = match[4]; 204 | const message = match[5]; 205 | 206 | // De-dupe build errors with ast errors 207 | if (this.astDiagnostics.has(textDocument.uri)) { 208 | for (const diag of this.astDiagnostics.get(textDocument.uri) ?? []) { 209 | if (diag.range.start.line === line && diag.range.start.character === column) { 210 | continue; 211 | } 212 | } 213 | } 214 | 215 | const severity = 216 | type.trim().toLowerCase() === "error" 217 | ? vscode.DiagnosticSeverity.Error 218 | : vscode.DiagnosticSeverity.Information; 219 | const range = new vscode.Range(line, column, line, Infinity); 220 | 221 | const diagnosticArray = diagnostics[resolvedPath] ?? []; 222 | diagnosticArray.push(new vscode.Diagnostic(range, message, severity)); 223 | diagnostics[resolvedPath] = diagnosticArray; 224 | } 225 | 226 | for (const filePath in diagnostics) { 227 | const diagnostic = diagnostics[filePath]; 228 | this.buildDiagnostics.set(vscode.Uri.file(filePath), diagnostic); 229 | } 230 | }); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /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/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 { zigProvider } from "./zigSetup"; 10 | 11 | const execFile = util.promisify(childProcess.execFile); 12 | 13 | export default class ZigMainCodeLensProvider implements vscode.CodeLensProvider { 14 | public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { 15 | const codeLenses: vscode.CodeLens[] = []; 16 | const text = document.getText(); 17 | 18 | const mainRegex = /pub\s+fn\s+main\s*\(/g; 19 | let match; 20 | while ((match = mainRegex.exec(text))) { 21 | const position = document.positionAt(match.index); 22 | const range = new vscode.Range(position, position); 23 | codeLenses.push( 24 | new vscode.CodeLens(range, { title: "Run", command: "zig.run", arguments: [document.uri.fsPath] }), 25 | ); 26 | codeLenses.push( 27 | new vscode.CodeLens(range, { title: "Debug", command: "zig.debug", arguments: [document.uri.fsPath] }), 28 | ); 29 | } 30 | return codeLenses; 31 | } 32 | 33 | public static registerCommands(context: vscode.ExtensionContext) { 34 | context.subscriptions.push( 35 | vscode.commands.registerCommand("zig.run", zigRun), 36 | vscode.commands.registerCommand("zig.debug", zigDebug), 37 | ); 38 | } 39 | } 40 | 41 | function zigRun() { 42 | if (!vscode.window.activeTextEditor) return; 43 | const zigPath = zigProvider.getZigPath(); 44 | if (!zigPath) return; 45 | const filePath = vscode.window.activeTextEditor.document.uri.fsPath; 46 | const terminal = vscode.window.createTerminal("Run Zig Program"); 47 | const callOperator = /(powershell.exe$|powershell$|pwsh.exe$|pwsh$)/.test(vscode.env.shell) ? "& " : ""; 48 | terminal.show(); 49 | const wsFolder = getWorkspaceFolder(filePath); 50 | if (wsFolder && isWorkspaceFile(filePath) && hasBuildFile(wsFolder.uri.fsPath)) { 51 | terminal.sendText(`${callOperator}${escapePath(zigPath)} build run`); 52 | return; 53 | } 54 | terminal.sendText(`${callOperator}${escapePath(zigPath)} run ${escapePath(filePath)}`); 55 | } 56 | 57 | function escapePath(rawPath: string): string { 58 | if (/[ !"#$&'()*,;:<>?\[\\\]^`{|}]/.test(rawPath)) { 59 | return `"${rawPath.replaceAll('"', '"\\""')}"`; 60 | } 61 | return rawPath; 62 | } 63 | 64 | function hasBuildFile(workspaceFspath: string): boolean { 65 | const buildZigPath = path.join(workspaceFspath, "build.zig"); 66 | return fs.existsSync(buildZigPath); 67 | } 68 | 69 | async function zigDebug() { 70 | if (!vscode.window.activeTextEditor) return; 71 | const filePath = vscode.window.activeTextEditor.document.uri.fsPath; 72 | try { 73 | const workspaceFolder = getWorkspaceFolder(filePath); 74 | let binaryPath; 75 | if (workspaceFolder && isWorkspaceFile(filePath) && hasBuildFile(workspaceFolder.uri.fsPath)) { 76 | binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); 77 | } else { 78 | binaryPath = await buildDebugBinary(filePath); 79 | } 80 | if (!binaryPath) return; 81 | 82 | const debugConfig: vscode.DebugConfiguration = { 83 | type: "lldb", 84 | name: `Debug Zig`, 85 | request: "launch", 86 | program: binaryPath, 87 | cwd: path.dirname(workspaceFolder?.uri.fsPath ?? path.dirname(filePath)), 88 | stopAtEntry: false, 89 | }; 90 | await vscode.debug.startDebugging(undefined, debugConfig); 91 | } catch (e) { 92 | if (e instanceof Error) { 93 | void vscode.window.showErrorMessage(`Failed to build debug binary: ${e.message}`); 94 | } else { 95 | void vscode.window.showErrorMessage(`Failed to build debug binary`); 96 | } 97 | } 98 | } 99 | 100 | async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { 101 | const zigPath = zigProvider.getZigPath(); 102 | if (!zigPath) return null; 103 | // Workaround because zig build doesn't support specifying the output binary name 104 | // `zig run` does support -femit-bin, but preferring `zig build` if possible 105 | const outputDir = path.join(workspacePath, "zig-out", "tmp-debug-build"); 106 | await execFile(zigPath, ["build", "--prefix", outputDir], { cwd: workspacePath }); 107 | const dirFiles = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(outputDir, "bin"))); 108 | const files = dirFiles.find(([, type]) => type === vscode.FileType.File); 109 | if (!files) { 110 | throw new Error("Unable to build debug binary"); 111 | } 112 | return path.join(outputDir, "bin", files[0]); 113 | } 114 | 115 | async function buildDebugBinary(filePath: string): Promise { 116 | const zigPath = zigProvider.getZigPath(); 117 | if (!zigPath) return null; 118 | const fileDirectory = path.dirname(filePath); 119 | const binaryName = `debug-${path.basename(filePath, ".zig")}`; 120 | const binaryPath = path.join(fileDirectory, "zig-out", "bin", binaryName); 121 | void vscode.workspace.fs.createDirectory(vscode.Uri.file(path.dirname(binaryPath))); 122 | 123 | await execFile(zigPath, ["run", filePath, `-femit-bin=${binaryPath}`], { cwd: fileDirectory }); 124 | return binaryPath; 125 | } 126 | -------------------------------------------------------------------------------- /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/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 { 11 | VersionIndex, 12 | ZigVersion, 13 | asyncDebounce, 14 | getHostZigName, 15 | getZigArchName, 16 | getZigOSName, 17 | resolveExePathAndVersion, 18 | workspaceConfigUpdateNoThrow, 19 | } from "./zigUtil"; 20 | import { ZigProvider } from "./zigProvider"; 21 | 22 | let statusItem: vscode.StatusBarItem; 23 | let languageStatusItem: vscode.LanguageStatusItem; 24 | let versionManagerConfig: versionManager.Config; 25 | export let zigProvider: ZigProvider; 26 | 27 | /** Removes the `zig.path` config option. */ 28 | async function installZig(context: vscode.ExtensionContext, temporaryVersion?: semver.SemVer) { 29 | let version = temporaryVersion; 30 | 31 | if (!version) { 32 | const wantedZig = await getWantedZigVersion( 33 | context, 34 | Object.values(WantedZigVersionSource) as WantedZigVersionSource[], 35 | ); 36 | version = wantedZig?.version; 37 | if (wantedZig?.source === WantedZigVersionSource.workspaceBuildZigZon) { 38 | version = await findClosestSatisfyingZigVersion(context, wantedZig.version); 39 | } 40 | } 41 | 42 | if (!version) { 43 | // Lookup zig in $PATH 44 | const result = resolveExePathAndVersion("zig", "version"); 45 | if ("exe" in result) { 46 | await vscode.workspace.getConfiguration("zig").update("path", undefined, true); 47 | zigProvider.set(result); 48 | return; 49 | } 50 | } 51 | 52 | if (!version) { 53 | // Default to the latest tagged release 54 | version = (await getLatestTaggedZigVersion(context)) ?? undefined; 55 | } 56 | 57 | if (!version) { 58 | await zigProvider.setAndSave(null); 59 | return; 60 | } 61 | 62 | try { 63 | const exePath = await versionManager.install(versionManagerConfig, version); 64 | const zigConfig = vscode.workspace.getConfiguration("zig"); 65 | await workspaceConfigUpdateNoThrow(zigConfig, "path", undefined, true); 66 | zigProvider.set({ exe: exePath, version: version }); 67 | } catch (err) { 68 | zigProvider.set(null); 69 | if (err instanceof Error) { 70 | void vscode.window.showErrorMessage(`Failed to install Zig ${version.toString()}: ${err.message}`); 71 | } else { 72 | void vscode.window.showErrorMessage(`Failed to install Zig ${version.toString()}!`); 73 | } 74 | } 75 | } 76 | 77 | async function findClosestSatisfyingZigVersion( 78 | context: vscode.ExtensionContext, 79 | version: semver.SemVer, 80 | ): Promise { 81 | if (version.prerelease.length !== 0) return version; 82 | const cacheKey = `zig-satisfying-version-${version.raw}`; 83 | 84 | try { 85 | // We can't just return `version` because `0.12.0` should return `0.12.1`. 86 | const availableVersions = (await getVersions()).map((item) => item.version); 87 | const selectedVersion = semver.maxSatisfying(availableVersions, `^${version.toString()}`); 88 | await context.globalState.update(cacheKey, selectedVersion ?? undefined); 89 | return selectedVersion ?? version; 90 | } catch { 91 | const selectedVersion = context.globalState.get(cacheKey, null); 92 | return selectedVersion ? new semver.SemVer(selectedVersion) : version; 93 | } 94 | } 95 | 96 | async function getLatestTaggedZigVersion(context: vscode.ExtensionContext): Promise { 97 | const cacheKey = "zig-latest-tagged"; 98 | try { 99 | const zigVersion = await getVersions(); 100 | const latestTagged = zigVersion.find((item) => item.version.prerelease.length === 0); 101 | const result = latestTagged?.version ?? null; 102 | await context.globalState.update(cacheKey, latestTagged?.version.raw); 103 | return result; 104 | } catch { 105 | const latestTagged = context.globalState.get(cacheKey, null); 106 | if (latestTagged) { 107 | return new semver.SemVer(latestTagged); 108 | } 109 | return null; 110 | } 111 | } 112 | 113 | /** 114 | * 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). 115 | * [Nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) are sorted to the bottom. 116 | * 117 | * Throws an exception when no network connection is available. 118 | */ 119 | async function getVersions(): Promise { 120 | const [zigIndexJson, machIndexJson] = await Promise.all( 121 | ["https://ziglang.org/download/index.json", "https://pkg.machengine.org/zig/index.json"].map(async (url) => { 122 | const response = await fetch(url); 123 | return response.json() as Promise; 124 | }), 125 | ); 126 | const indexJson = { ...machIndexJson, ...zigIndexJson }; 127 | 128 | const hostName = getHostZigName(); 129 | const result: ZigVersion[] = []; 130 | for (const [key, value] of Object.entries(indexJson)) { 131 | const name = key === "master" ? "nightly" : key; 132 | const version = new semver.SemVer(value.version ?? key); 133 | const release = value[hostName]; 134 | if (release) { 135 | result.push({ 136 | name: name, 137 | version: version, 138 | url: release.tarball, 139 | sha: release.shasum, 140 | notes: value.notes, 141 | isMach: name.includes("mach"), 142 | }); 143 | } 144 | } 145 | if (result.length === 0) { 146 | throw Error( 147 | `no pre-built Zig is available for your system '${hostName}', you can build it yourself using https://github.com/ziglang/zig-bootstrap`, 148 | ); 149 | } 150 | sortVersions(result); 151 | return result; 152 | } 153 | 154 | function sortVersions(versions: { name?: string; version: semver.SemVer; isMach: boolean }[]) { 155 | versions.sort((lhs, rhs) => { 156 | // Mach versions except `mach-latest` move to the end 157 | if (lhs.name !== "mach-latest" && rhs.name !== "mach-latest" && lhs.isMach !== rhs.isMach) 158 | return +lhs.isMach - +rhs.isMach; 159 | return semver.compare(rhs.version, lhs.version); 160 | }); 161 | } 162 | 163 | async function selectVersionAndInstall(context: vscode.ExtensionContext) { 164 | const offlineVersions = await versionManager.query(versionManagerConfig); 165 | 166 | const versions: { 167 | name?: string; 168 | version: semver.SemVer; 169 | /** Whether the version already installed in global extension storage */ 170 | offline: boolean; 171 | /** Whether is available in `index.json` */ 172 | online: boolean; 173 | /** Whether the version one of [Mach's nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) */ 174 | isMach: boolean; 175 | }[] = offlineVersions.map((version) => ({ 176 | version: version, 177 | offline: true, 178 | online: false, 179 | isMach: false /* We can't tell if a version is Mach while being offline */, 180 | })); 181 | 182 | try { 183 | const onlineVersions = await getVersions(); 184 | outer: for (const onlineVersion of onlineVersions) { 185 | for (const version of versions) { 186 | if (semver.eq(version.version, onlineVersion.version)) { 187 | version.name ??= onlineVersion.name; 188 | version.online = true; 189 | version.isMach = onlineVersion.isMach; 190 | } 191 | } 192 | 193 | for (const version of versions) { 194 | if (semver.eq(version.version, onlineVersion.version) && version.name === onlineVersion.name) { 195 | continue outer; 196 | } 197 | } 198 | 199 | versions.push({ 200 | name: onlineVersion.name, 201 | version: onlineVersion.version, 202 | online: true, 203 | offline: !!offlineVersions.find((item) => semver.eq(item.version, onlineVersion.version)), 204 | isMach: onlineVersion.isMach, 205 | }); 206 | } 207 | } catch (err) { 208 | if (!offlineVersions.length) { 209 | if (err instanceof Error) { 210 | void vscode.window.showErrorMessage(`Failed to query available Zig version: ${err.message}`); 211 | } else { 212 | void vscode.window.showErrorMessage(`Failed to query available Zig version!`); 213 | } 214 | return; 215 | } else { 216 | // Only show the locally installed versions 217 | } 218 | } 219 | 220 | sortVersions(versions); 221 | const placeholderVersion = versions.find((item) => item.version.prerelease.length === 0)?.version; 222 | 223 | const items: vscode.QuickPickItem[] = []; 224 | 225 | const workspaceZig = await getWantedZigVersion(context, [ 226 | WantedZigVersionSource.workspaceZigVersionFile, 227 | WantedZigVersionSource.workspaceBuildZigZon, 228 | WantedZigVersionSource.zigVersionConfigOption, 229 | ]); 230 | if (workspaceZig !== null) { 231 | const alreadyInstalled = offlineVersions.some((item) => semver.eq(item.version, workspaceZig.version)); 232 | items.push({ 233 | label: "Use Workspace Version", 234 | description: alreadyInstalled ? "already installed" : undefined, 235 | detail: workspaceZig.version.raw, 236 | }); 237 | } 238 | 239 | const zigInPath = resolveExePathAndVersion("zig", "version"); 240 | if (!("message" in zigInPath)) { 241 | items.push({ 242 | label: "Use Zig in PATH", 243 | description: zigInPath.exe, 244 | detail: zigInPath.version.raw, 245 | }); 246 | } 247 | 248 | items.push( 249 | { 250 | label: "Manually Specify Path", 251 | }, 252 | { 253 | label: "", 254 | kind: vscode.QuickPickItemKind.Separator, 255 | }, 256 | ); 257 | 258 | let seenMachVersion = false; 259 | for (const item of versions) { 260 | const useName = item.isMach || item.version.prerelease.length !== 0; 261 | if (item.isMach && !seenMachVersion && item.name !== "mach-latest") { 262 | seenMachVersion = true; 263 | items.push({ 264 | label: "Mach's Nominated Zig versions", 265 | kind: vscode.QuickPickItemKind.Separator, 266 | }); 267 | } 268 | items.push({ 269 | label: (useName ? item.name : null) ?? item.version.raw, 270 | description: item.offline ? "already installed" : undefined, 271 | detail: useName ? (item.name ? item.version.raw : undefined) : undefined, 272 | }); 273 | } 274 | 275 | const selection = await vscode.window.showQuickPick(items, { 276 | title: "Select Zig version to install", 277 | canPickMany: false, 278 | placeHolder: placeholderVersion?.raw, 279 | }); 280 | if (selection === undefined) return; 281 | 282 | switch (selection.label) { 283 | case "Use Workspace Version": 284 | await installZig(context); 285 | break; 286 | case "Use Zig in PATH": 287 | const zigConfig = vscode.workspace.getConfiguration("zig"); 288 | await workspaceConfigUpdateNoThrow(zigConfig, "path", "zig", true); 289 | break; 290 | case "Manually Specify Path": 291 | const uris = await vscode.window.showOpenDialog({ 292 | canSelectFiles: true, 293 | canSelectFolders: false, 294 | canSelectMany: false, 295 | title: "Select Zig executable", 296 | }); 297 | if (!uris) return; 298 | await zigProvider.setAndSave(uris[0].fsPath); 299 | break; 300 | default: 301 | const version = new semver.SemVer(selection.detail ?? selection.label); 302 | await showUpdateWorkspaceVersionDialog(version, workspaceZig?.source); 303 | await installZig(context, version); 304 | break; 305 | } 306 | } 307 | 308 | async function showUpdateWorkspaceVersionDialog( 309 | version: semver.SemVer, 310 | source?: WantedZigVersionSource, 311 | ): Promise { 312 | const workspace = getWorkspaceFolder(); 313 | 314 | if (workspace !== null) { 315 | let buttonName; 316 | switch (source) { 317 | case WantedZigVersionSource.workspaceZigVersionFile: 318 | buttonName = "update .zigversion"; 319 | break; 320 | case WantedZigVersionSource.workspaceBuildZigZon: 321 | buttonName = "update build.zig.zon"; 322 | break; 323 | case WantedZigVersionSource.zigVersionConfigOption: 324 | buttonName = "update workspace settings"; 325 | break; 326 | case undefined: 327 | buttonName = "create .zigversion"; 328 | break; 329 | } 330 | 331 | const response = await vscode.window.showInformationMessage( 332 | `Would you like to save Zig ${version.toString()} in this workspace?`, 333 | buttonName, 334 | ); 335 | if (!response) return; 336 | } 337 | 338 | source ??= workspace 339 | ? WantedZigVersionSource.workspaceZigVersionFile 340 | : WantedZigVersionSource.zigVersionConfigOption; 341 | 342 | switch (source) { 343 | case WantedZigVersionSource.workspaceZigVersionFile: { 344 | if (!workspace) throw new Error("failed to resolve workspace folder"); 345 | 346 | const edit = new vscode.WorkspaceEdit(); 347 | edit.createFile(vscode.Uri.joinPath(workspace.uri, ".zigversion"), { 348 | overwrite: true, 349 | contents: new Uint8Array(Buffer.from(version.raw)), 350 | }); 351 | await vscode.workspace.applyEdit(edit); 352 | break; 353 | } 354 | case WantedZigVersionSource.workspaceBuildZigZon: { 355 | const metadata = await parseBuildZigZon(); 356 | if (!metadata) throw new Error("failed to parse build.zig.zon"); 357 | 358 | const edit = new vscode.WorkspaceEdit(); 359 | edit.replace(metadata.document.uri, metadata.minimumZigVersionSourceRange, version.raw); 360 | await vscode.workspace.applyEdit(edit); 361 | break; 362 | } 363 | case WantedZigVersionSource.zigVersionConfigOption: { 364 | await vscode.workspace.getConfiguration("zig").update("version", version.raw, !workspace); 365 | break; 366 | } 367 | } 368 | } 369 | 370 | interface BuildZigZonMetadata { 371 | /** The `build.zig.zon` document. */ 372 | document: vscode.TextDocument; 373 | minimumZigVersion: semver.SemVer; 374 | /** `.minimum_zig_version = "0.13.0"` */ 375 | minimumZigVersionSourceRange: vscode.Range; 376 | } 377 | 378 | function getWorkspaceFolder(): vscode.WorkspaceFolder | null { 379 | // Supporting multiple workspaces is significantly more complex so we just look for the first workspace. 380 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 381 | return vscode.workspace.workspaceFolders[0]; 382 | } 383 | return null; 384 | } 385 | 386 | /** 387 | * Look for a `build.zig.zon` in the current workspace and return the `minimum_zig_version` in it. 388 | */ 389 | async function parseBuildZigZon(): Promise { 390 | const workspace = getWorkspaceFolder(); 391 | if (!workspace) return null; 392 | 393 | const manifestUri = vscode.Uri.joinPath(workspace.uri, "build.zig.zon"); 394 | 395 | let manifest; 396 | try { 397 | manifest = await vscode.workspace.openTextDocument(manifestUri); 398 | } catch { 399 | return null; 400 | } 401 | // Not perfect, but good enough 402 | const regex = /\n\s*\.minimum_zig_version\s=\s\"(.*)\"/; 403 | const matches = regex.exec(manifest.getText()); 404 | if (!matches) return null; 405 | 406 | const versionString = matches[1]; 407 | const version = semver.parse(versionString); 408 | if (!version) return null; 409 | 410 | const startPosition = manifest.positionAt(matches.index + matches[0].length - versionString.length - 1); 411 | const endPosition = startPosition.translate(0, versionString.length); 412 | 413 | return { 414 | document: manifest, 415 | minimumZigVersion: version, 416 | minimumZigVersionSourceRange: new vscode.Range(startPosition, endPosition), 417 | }; 418 | } 419 | 420 | /** The order of these enums defines the default order in which these sources are executed. */ 421 | enum WantedZigVersionSource { 422 | /** `.zigversion` */ 423 | workspaceZigVersionFile = ".zigversion", 424 | /** The `minimum_zig_version` in `build.zig.zon` */ 425 | workspaceBuildZigZon = "build.zig.zon", 426 | /** `zig.version` */ 427 | zigVersionConfigOption = "zig.version", 428 | } 429 | 430 | /** Try to resolve the (workspace-specific) Zig version. */ 431 | async function getWantedZigVersion( 432 | context: vscode.ExtensionContext, 433 | /** List of "sources" that should are applied in the given order to resolve the wanted Zig version */ 434 | sources: WantedZigVersionSource[], 435 | ): Promise<{ 436 | version: semver.SemVer; 437 | source: WantedZigVersionSource; 438 | } | null> { 439 | let workspace: vscode.WorkspaceFolder | null = null; 440 | // Supporting multiple workspaces is significantly more complex so we just look for the first workspace. 441 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 442 | workspace = vscode.workspace.workspaceFolders[0]; 443 | } 444 | 445 | for (const source of sources) { 446 | let result: semver.SemVer | null = null; 447 | 448 | try { 449 | switch (source) { 450 | case WantedZigVersionSource.workspaceZigVersionFile: 451 | if (workspace) { 452 | const zigVersionString = await vscode.workspace.fs.readFile( 453 | vscode.Uri.joinPath(workspace.uri, ".zigversion"), 454 | ); 455 | result = semver.parse(zigVersionString.toString().trim()); 456 | } 457 | break; 458 | case WantedZigVersionSource.workspaceBuildZigZon: 459 | const metadata = await parseBuildZigZon(); 460 | if (metadata?.minimumZigVersion) { 461 | result = metadata.minimumZigVersion; 462 | } 463 | break; 464 | case WantedZigVersionSource.zigVersionConfigOption: 465 | const versionString = vscode.workspace.getConfiguration("zig").get("version"); 466 | if (versionString) { 467 | result = semver.parse(versionString); 468 | if (!result) { 469 | void vscode.window.showErrorMessage( 470 | `Invalid 'zig.version' config option. '${versionString}' is not a valid Zig version`, 471 | ); 472 | } 473 | } 474 | break; 475 | } 476 | } catch {} 477 | 478 | if (!result) continue; 479 | 480 | return { 481 | version: result, 482 | source: source, 483 | }; 484 | } 485 | return null; 486 | } 487 | 488 | function updateStatusItem(item: vscode.StatusBarItem, version: semver.SemVer | null) { 489 | item.name = "Zig Version"; 490 | item.text = version?.toString() ?? "not installed"; 491 | item.tooltip = "Select Zig Version"; 492 | item.command = { 493 | title: "Select Version", 494 | command: "zig.install", 495 | }; 496 | if (version) { 497 | item.backgroundColor = undefined; 498 | } else { 499 | item.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground"); 500 | } 501 | } 502 | 503 | function updateLanguageStatusItem(item: vscode.LanguageStatusItem, version: semver.SemVer | null) { 504 | item.name = "Zig"; 505 | if (version) { 506 | item.text = `Zig ${version.toString()}`; 507 | item.detail = "Zig Version"; 508 | item.severity = vscode.LanguageStatusSeverity.Information; 509 | } else { 510 | item.text = "Zig not installed"; 511 | item.severity = vscode.LanguageStatusSeverity.Error; 512 | } 513 | item.command = { 514 | title: "Select Version", 515 | command: "zig.install", 516 | }; 517 | } 518 | 519 | function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) { 520 | if (zigExePath) { 521 | const envValue = path.dirname(zigExePath) + path.delimiter; 522 | // This will take priority over a user-defined PATH values. 523 | context.environmentVariableCollection.prepend("PATH", envValue); 524 | } else { 525 | context.environmentVariableCollection.delete("PATH"); 526 | } 527 | } 528 | 529 | /** 530 | * Should be called when one of the following events happen: 531 | * - The Zig executable has been modified 532 | * - A workspace configuration file has been modified (e.g. `.zigversion`, `build.zig.zon`) 533 | */ 534 | async function updateStatus(context: vscode.ExtensionContext): Promise { 535 | const zigVersion = zigProvider.getZigVersion(); 536 | const zigPath = zigProvider.getZigPath(); 537 | 538 | updateStatusItem(statusItem, zigVersion); 539 | updateLanguageStatusItem(languageStatusItem, zigVersion); 540 | updateZigEnvironmentVariableCollection(context, zigPath); 541 | 542 | // Try to check whether the Zig version satifies the `minimum_zig_version` in `build.zig.zon` 543 | 544 | if (!zigVersion || !zigPath) return; 545 | const buildZigZonMetadata = await parseBuildZigZon(); 546 | if (!buildZigZonMetadata) return; 547 | if (semver.gte(zigVersion, buildZigZonMetadata.minimumZigVersion)) return; 548 | 549 | statusItem.backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground"); 550 | 551 | void vscode.window 552 | .showWarningMessage( 553 | `Your Zig version '${zigVersion.toString()}' does not satisfy the minimum Zig version '${buildZigZonMetadata.minimumZigVersion.toString()}' of your project.`, 554 | "update Zig", 555 | "open build.zig.zon", 556 | ) 557 | .then(async (response) => { 558 | switch (response) { 559 | case undefined: 560 | break; 561 | case "update Zig": { 562 | // This will source the desired Zig version with `getWantedZigVersion` which may not satisfy the minimum Zig version. 563 | // This could happen for example when the a `.zigversion` specifies `0.12.0` but `minimum_zig_version` is `0.13.0`. 564 | // The extension would install `0.12.0` and then complain again. 565 | await installZig(context); 566 | break; 567 | } 568 | case "open build.zig.zon": { 569 | void vscode.window.showTextDocument(buildZigZonMetadata.document, { 570 | selection: buildZigZonMetadata.minimumZigVersionSourceRange, 571 | }); 572 | break; 573 | } 574 | } 575 | }); 576 | } 577 | 578 | export async function setupZig(context: vscode.ExtensionContext) { 579 | { 580 | // This check can be removed once enough time has passed so that most users switched to the new value 581 | 582 | // remove the `zig_install` directory from the global storage 583 | try { 584 | await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zig_install"), { 585 | recursive: true, 586 | useTrash: false, 587 | }); 588 | } catch {} 589 | 590 | // remove a `zig.path` that points to the global storage. 591 | const zigConfig = vscode.workspace.getConfiguration("zig"); 592 | const zigPath = zigConfig.get("path", ""); 593 | if (zigPath.startsWith(context.globalStorageUri.fsPath)) { 594 | await workspaceConfigUpdateNoThrow(zigConfig, "path", undefined, true); 595 | } 596 | 597 | await workspaceConfigUpdateNoThrow(zigConfig, "initialSetupDone", undefined, true); 598 | 599 | await context.workspaceState.update("zig-version", undefined); 600 | } 601 | 602 | /// Workaround https://github.com/ziglang/zig/issues/21905 603 | switch (process.platform) { 604 | case "darwin": 605 | case "freebsd": 606 | case "openbsd": 607 | case "netbsd": 608 | case "haiku": 609 | vscode.workspace.onDidSaveTextDocument(async (document) => { 610 | if (document.languageId !== "zig") return; 611 | if (document.uri.scheme !== "file") return; 612 | 613 | const fsPath = document.uri.fsPath; 614 | try { 615 | await fs.copyFile(fsPath, fsPath + ".tmp", fs.constants.COPYFILE_EXCL); 616 | await fs.rename(fsPath + ".tmp", fsPath); 617 | } catch {} 618 | }, context.subscriptions); 619 | break; 620 | case "aix": 621 | case "android": 622 | case "linux": 623 | case "sunos": 624 | case "win32": 625 | case "cygwin": 626 | break; 627 | } 628 | 629 | versionManagerConfig = { 630 | context: context, 631 | title: "Zig", 632 | exeName: "zig", 633 | extraTarArgs: ["--strip-components=1"], 634 | /** https://ziglang.org/download */ 635 | minisignKey: minisign.parseKey("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"), 636 | versionArg: "version", 637 | // taken from https://github.com/mlugg/setup-zig/blob/main/mirrors.json 638 | mirrorUrls: [ 639 | vscode.Uri.parse("https://pkg.machengine.org/zig"), 640 | vscode.Uri.parse("https://zigmirror.hryx.net/zig"), 641 | vscode.Uri.parse("https://zig.linus.dev/zig"), 642 | vscode.Uri.parse("https://fs.liujiacai.net/zigbuilds"), 643 | vscode.Uri.parse("https://zigmirror.nesovic.dev/zig"), 644 | ], 645 | canonicalUrl: { 646 | release: vscode.Uri.parse("https://ziglang.org/download"), 647 | nightly: vscode.Uri.parse("https://ziglang.org/builds"), 648 | }, 649 | getArtifactName(version) { 650 | const fileExtension = process.platform === "win32" ? "zip" : "tar.xz"; 651 | if ( 652 | (version.prerelease.length === 0 && semver.gte(version, "0.14.1")) || 653 | semver.gte(version, "0.15.0-dev.631+9a3540d61") 654 | ) { 655 | return `zig-${getZigArchName()}-${getZigOSName()}-${version.raw}.${fileExtension}`; 656 | } else { 657 | return `zig-${getZigOSName()}-${getZigArchName()}-${version.raw}.${fileExtension}`; 658 | } 659 | }, 660 | }; 661 | 662 | // Remove after some time has passed from the prefix change. 663 | await versionManager.convertOldInstallPrefixes(versionManagerConfig); 664 | 665 | zigProvider = new ZigProvider(); 666 | 667 | /** There two status items because there doesn't seem to be a way to pin a language status item by default. */ 668 | statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, -1); 669 | languageStatusItem = vscode.languages.createLanguageStatusItem("zig.status", { language: "zig" }); 670 | 671 | context.environmentVariableCollection.description = "Add Zig to PATH"; 672 | 673 | const watcher1 = vscode.workspace.createFileSystemWatcher("**/.zigversion"); 674 | const watcher2 = vscode.workspace.createFileSystemWatcher("**/build.zig.zon"); 675 | 676 | const refreshZigInstallation = asyncDebounce(async () => { 677 | if (!vscode.workspace.getConfiguration("zig").get("path")) { 678 | await installZig(context); 679 | } else { 680 | await updateStatus(context); 681 | } 682 | }, 200); 683 | 684 | if (!vscode.workspace.getConfiguration("zig").get("path")) { 685 | await installZig(context); 686 | } 687 | await updateStatus(context); 688 | 689 | const onDidChangeActiveTextEditor = (editor: vscode.TextEditor | undefined) => { 690 | if (editor?.document.languageId === "zig") { 691 | statusItem.show(); 692 | } else { 693 | statusItem.hide(); 694 | } 695 | }; 696 | onDidChangeActiveTextEditor(vscode.window.activeTextEditor); 697 | 698 | context.subscriptions.push( 699 | statusItem, 700 | languageStatusItem, 701 | vscode.commands.registerCommand("zig.install", async () => { 702 | await selectVersionAndInstall(context); 703 | }), 704 | vscode.workspace.onDidChangeConfiguration((change) => { 705 | if (change.affectsConfiguration("zig.version")) { 706 | void refreshZigInstallation(); 707 | } 708 | if (change.affectsConfiguration("zig.path")) { 709 | const result = zigProvider.resolveZigPathConfigOption(); 710 | if (result === undefined) return; // error message already reported 711 | if (result !== null) { 712 | zigProvider.set(result); 713 | } 714 | void refreshZigInstallation(); 715 | } 716 | }), 717 | vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), 718 | zigProvider.onChange.event(() => { 719 | void updateStatus(context); 720 | }), 721 | watcher1.onDidCreate(refreshZigInstallation), 722 | watcher1.onDidChange(refreshZigInstallation), 723 | watcher1.onDidDelete(refreshZigInstallation), 724 | watcher1, 725 | watcher2.onDidCreate(refreshZigInstallation), 726 | watcher2.onDidChange(refreshZigInstallation), 727 | watcher2.onDidDelete(refreshZigInstallation), 728 | watcher2, 729 | ); 730 | } 731 | -------------------------------------------------------------------------------- /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 = /\btest\s+(?:"([^"]+)"|([a-zA-Z0-9_][\w]*)|@"([^"]+)")\s*\{/g; 74 | const matches = Array.from(textDocument.getText().matchAll(regex)); 75 | this.deleteTestForAFile(textDocument.uri); 76 | 77 | for (const match of matches) { 78 | const testDesc = match[1] || match[2] || match[3]; 79 | const isDocTest = !match[1]; 80 | const position = textDocument.positionAt(match.index); 81 | const range = new vscode.Range(position, position.translate(0, match[0].length)); 82 | const fileName = path.basename(textDocument.uri.fsPath); 83 | 84 | // Add doctest prefix to handle scenario where test name matches one with non doctest. E.g `test foo` and `test "foo"` 85 | const testItem = this.testController.createTestItem( 86 | `${fileName}.test.${isDocTest ? "doctest." : ""}${testDesc}`, // Test id needs to be unique, so adding file name prefix 87 | `${fileName} - ${testDesc}`, 88 | textDocument.uri, 89 | ); 90 | testItem.range = range; 91 | this.testController.items.add(testItem); 92 | } 93 | } 94 | 95 | private async runTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) { 96 | const run = this.testController.createTestRun(request); 97 | // request.include will have individual test when we run test from gutter icon 98 | // if test is run from test explorer, request.include will be undefined and we run all tests that are active 99 | for (const item of request.include ?? this.testController.items) { 100 | if (token.isCancellationRequested) break; 101 | const testItem = Array.isArray(item) ? item[1] : item; 102 | 103 | run.started(testItem); 104 | const start = new Date(); 105 | run.appendOutput(`[${start.toISOString()}] Running test: ${testItem.label}\r\n`); 106 | const { output, success } = await this.runTest(testItem); 107 | run.appendOutput(output.replaceAll("\n", "\r\n")); 108 | run.appendOutput("\r\n"); 109 | const elapsed = new Date().getMilliseconds() - start.getMilliseconds(); 110 | 111 | if (!success) { 112 | run.failed(testItem, new vscode.TestMessage(output), elapsed); 113 | } else { 114 | run.passed(testItem, elapsed); 115 | } 116 | } 117 | run.end(); 118 | } 119 | 120 | private async runTest(test: vscode.TestItem): Promise<{ output: string; success: boolean }> { 121 | const config = vscode.workspace.getConfiguration("zig"); 122 | const zigPath = zigProvider.getZigPath(); 123 | if (!zigPath) { 124 | return { output: "Unable to run test without Zig", success: false }; 125 | } 126 | if (test.uri === undefined) { 127 | return { output: "Unable to determine file location", success: false }; 128 | } 129 | 130 | const testUri = test.uri; 131 | const wsFolder = getWorkspaceFolder(testUri.fsPath)?.uri.fsPath ?? path.dirname(testUri.fsPath); 132 | 133 | const parts = test.id.split("."); 134 | const lastPart = parts[parts.length - 1]; 135 | 136 | const testArgsConf = config.get("testArgs") ?? []; 137 | const args: string[] = 138 | testArgsConf.length > 0 139 | ? testArgsConf.map((v) => v.replace("${filter}", lastPart).replace("${path}", testUri.fsPath)) 140 | : []; 141 | 142 | try { 143 | const { stderr: output } = await execFile(zigPath, args, { cwd: wsFolder }); 144 | 145 | return { output: output.replaceAll("\n", "\r\n"), success: true }; 146 | } catch (e) { 147 | if (e instanceof Error) { 148 | if ( 149 | config.get("testArgs")?.toString() === 150 | config.inspect("testArgs")?.defaultValue?.toString() && 151 | (e.message.includes("no module named") || e.message.includes("import of file outside module path")) 152 | ) { 153 | void vscode.window 154 | .showInformationMessage("Use build script to run tests?", "Yes", "No") 155 | .then(async (response) => { 156 | if (response === "Yes") { 157 | await workspaceConfigUpdateNoThrow( 158 | config, 159 | "testArgs", 160 | ["build", "test", "-Dtest-filter=${filter}"], 161 | false, 162 | ); 163 | void vscode.commands.executeCommand( 164 | "workbench.action.openWorkspaceSettings", 165 | "@id:zig.testArgs", 166 | ); 167 | } 168 | }); 169 | } 170 | 171 | return { output: e.message.replaceAll("\n", "\r\n"), success: false }; 172 | } else { 173 | return { output: "Failed to run test\r\n", success: false }; 174 | } 175 | } 176 | } 177 | 178 | private async debugTests(req: vscode.TestRunRequest, token: vscode.CancellationToken) { 179 | const run = this.testController.createTestRun(req); 180 | for (const item of req.include ?? this.testController.items) { 181 | if (token.isCancellationRequested) break; 182 | const test = Array.isArray(item) ? item[1] : item; 183 | run.started(test); 184 | try { 185 | await this.debugTest(run, test); 186 | run.passed(test); 187 | } catch (e) { 188 | run.failed(test, new vscode.TestMessage((e as Error).message)); 189 | } 190 | } 191 | run.end(); 192 | } 193 | 194 | private async debugTest(run: vscode.TestRun, testItem: vscode.TestItem) { 195 | if (testItem.uri === undefined) { 196 | throw new Error("Unable to determine file location"); 197 | } 198 | const testBinaryPath = await this.buildTestBinary(run, testItem.uri.fsPath, getTestDesc(testItem)); 199 | const debugConfig: vscode.DebugConfiguration = { 200 | type: "lldb", 201 | name: `Debug ${testItem.label}`, 202 | request: "launch", 203 | program: testBinaryPath, 204 | cwd: path.dirname(testItem.uri.fsPath), 205 | stopAtEntry: false, 206 | }; 207 | await vscode.debug.startDebugging(undefined, debugConfig); 208 | } 209 | 210 | private async buildTestBinary(run: vscode.TestRun, testFilePath: string, testDesc: string): Promise { 211 | const zigPath = zigProvider.getZigPath(); 212 | if (!zigPath) { 213 | throw new Error("Unable to build test binary without Zig"); 214 | } 215 | 216 | const wsFolder = getWorkspaceFolder(testFilePath)?.uri.fsPath ?? path.dirname(testFilePath); 217 | const outputDir = path.join(wsFolder, "zig-out", "tmp-debug-build", "bin"); 218 | const binaryName = `test-${path.basename(testFilePath, ".zig")}`; 219 | const binaryPath = path.join(outputDir, binaryName); 220 | await vscode.workspace.fs.createDirectory(vscode.Uri.file(outputDir)); 221 | 222 | const { stdout, stderr } = await execFile(zigPath, [ 223 | "test", 224 | testFilePath, 225 | "--test-filter", 226 | testDesc, 227 | "--test-no-exec", 228 | `-femit-bin=${binaryPath}`, 229 | ]); 230 | if (stderr) { 231 | run.appendOutput(stderr.replaceAll("\n", "\r\n")); 232 | throw new Error(`Failed to build test binary: ${stderr}`); 233 | } 234 | run.appendOutput(stdout.replaceAll("\n", "\r\n")); 235 | return binaryPath; 236 | } 237 | } 238 | 239 | function getTestDesc(testItem: vscode.TestItem): string { 240 | const parts = testItem.id.split("."); 241 | return parts[parts.length - 1]; 242 | } 243 | -------------------------------------------------------------------------------- /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): string { 18 | if (input.includes("${userHome}")) { 19 | input = input.replaceAll("${userHome}", os.homedir()); 20 | } 21 | 22 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 23 | input = input.replaceAll("${workspaceFolder}", vscode.workspace.workspaceFolders[0].uri.fsPath); 24 | input = input.replaceAll("${workspaceFolderBasename}", vscode.workspace.workspaceFolders[0].name); 25 | } 26 | 27 | const document = vscode.window.activeTextEditor?.document; 28 | if (document) { 29 | input = input.replaceAll("${file}", document.fileName); 30 | input = input.replaceAll("${fileBasename}", path.basename(document.fileName)); 31 | input = input.replaceAll( 32 | "${fileBasenameNoExtension}", 33 | path.basename(document.fileName, path.extname(document.fileName)), 34 | ); 35 | input = input.replaceAll("${fileExtname}", path.extname(document.fileName)); 36 | input = input.replaceAll("${fileDirname}", path.dirname(document.fileName)); 37 | input = input.replaceAll("${fileDirnameBasename}", path.basename(path.dirname(document.fileName))); 38 | } 39 | 40 | input = input.replaceAll("${pathSeparator}", path.sep); 41 | input = input.replaceAll("${/}", path.sep); 42 | if (input.includes("${cwd}")) { 43 | input = input.replaceAll("${cwd}", process.cwd()); 44 | } 45 | 46 | if (input.includes("${env:")) { 47 | for (let env = /\${env:([^}]+)}/.exec(input)?.[1]; env; env = /\${env:([^}]+)}/.exec(input)?.[1]) { 48 | input = input.replaceAll(`\${env:${env}}`, process.env[env] ?? ""); 49 | } 50 | } 51 | return input; 52 | } 53 | 54 | /** Resolves the absolute executable path and version of a program like Zig or ZLS. */ 55 | export function resolveExePathAndVersion( 56 | /** 57 | * - resolves '~' to the user home directory. 58 | * - resolves VS Code predefined variables. 59 | * - resolves possible executable file extensions on windows like '.exe' or '.cmd'. 60 | */ 61 | cmd: string, 62 | /** 63 | * The command-line argument that is used to query the version of the executable. 64 | * Zig uses `version`. ZLS uses `--version`. 65 | */ 66 | versionArg: string, 67 | ): { exe: string; version: semver.SemVer } | { message: string } { 68 | assert(cmd.length); 69 | 70 | // allow passing predefined variables 71 | cmd = handleConfigOption(cmd); 72 | 73 | if (cmd.startsWith("~")) { 74 | cmd = path.join(os.homedir(), cmd.substring(1)); 75 | } 76 | 77 | const isWindows = os.platform() === "win32"; 78 | const isAbsolute = path.isAbsolute(cmd); 79 | const hasPathSeparator = !!/\//.exec(cmd) || (isWindows && !!/\\/.exec(cmd)); 80 | if (!isAbsolute && hasPathSeparator) { 81 | // A value like `./zig` would be looked up relative to the cwd of the VS Code process which makes little sense. 82 | return { 83 | message: `'${cmd}' is not valid. Use '$\{workspaceFolder}' to specify a path relative to the current workspace folder and '~' for the home directory.`, 84 | }; 85 | } 86 | 87 | const exePath = which.sync(cmd, { nothrow: true }); 88 | if (!exePath) { 89 | if (!isAbsolute) { 90 | return { message: `Could not find '${cmd}' in PATH.` }; 91 | } 92 | 93 | const stats = fs.statSync(cmd, { throwIfNoEntry: false }); 94 | if (!stats) { 95 | return { 96 | message: `'${cmd}' does not exist.`, 97 | }; 98 | } 99 | 100 | if (stats.isDirectory()) { 101 | return { 102 | message: `'${cmd}' is a directory and not an executable.`, 103 | }; 104 | } 105 | 106 | return { 107 | message: `'${cmd}' is not an executable.`, 108 | }; 109 | } 110 | 111 | const version = getVersion(exePath, versionArg); 112 | if (!version) return { message: `Failed to run '${exePath} ${versionArg}'.` }; 113 | return { exe: exePath, version: version }; 114 | } 115 | 116 | export function asyncDebounce Promise>>>( 117 | func: T, 118 | wait?: number, 119 | ): (...args: Parameters) => Promise>> { 120 | const debounced = debounce( 121 | (resolve: (value: Awaited>) => void, reject: (reason?: unknown) => void, args: Parameters) => { 122 | void func(...args) 123 | .then(resolve) 124 | .catch(reject); 125 | }, 126 | wait, 127 | ); 128 | return (...args) => 129 | new Promise((resolve, reject) => { 130 | debounced(resolve, reject, args); 131 | }); 132 | } 133 | 134 | /** 135 | * Wrapper around `vscode.WorkspaceConfiguration.update` that doesn't throw an exception. 136 | * A common cause of an exception is when the `settings.json` file is read-only. 137 | */ 138 | export async function workspaceConfigUpdateNoThrow( 139 | config: vscode.WorkspaceConfiguration, 140 | section: string, 141 | value: unknown, 142 | configurationTarget?: vscode.ConfigurationTarget | boolean | null, 143 | overrideInLanguage?: boolean, 144 | ): Promise { 145 | try { 146 | await config.update(section, value, configurationTarget, overrideInLanguage); 147 | } catch (err) { 148 | if (err instanceof Error) { 149 | void vscode.window.showErrorMessage(err.message); 150 | } else { 151 | void vscode.window.showErrorMessage("failed to update settings.json"); 152 | } 153 | } 154 | } 155 | 156 | // Check timestamp `key` to avoid automatically checking for updates 157 | // more than once in an hour. 158 | export async function shouldCheckUpdate(context: vscode.ExtensionContext, key: string): Promise { 159 | const HOUR = 60 * 60 * 1000; 160 | const timestamp = new Date().getTime(); 161 | const old = context.globalState.get(key); 162 | if (old === undefined || timestamp - old < HOUR) return false; 163 | await context.globalState.update(key, timestamp); 164 | return true; 165 | } 166 | 167 | export function getZigArchName(): string { 168 | switch (process.arch) { 169 | case "ia32": 170 | return "x86"; 171 | case "x64": 172 | return "x86_64"; 173 | case "arm": 174 | return "armv7a"; 175 | case "arm64": 176 | return "aarch64"; 177 | case "ppc": 178 | return "powerpc"; 179 | case "ppc64": 180 | return "powerpc64le"; 181 | default: 182 | return process.arch; 183 | } 184 | } 185 | export function getZigOSName(): string { 186 | switch (process.platform) { 187 | case "darwin": 188 | return "macos"; 189 | case "win32": 190 | return "windows"; 191 | default: 192 | return process.platform; 193 | } 194 | } 195 | 196 | export function getHostZigName(): string { 197 | return `${getZigArchName()}-${getZigOSName()}`; 198 | } 199 | 200 | export function getVersion( 201 | filePath: string, 202 | /** 203 | * The command-line argument that is used to query the version of the executable. 204 | * Zig uses `version`. ZLS uses `--version`. 205 | */ 206 | arg: string, 207 | ): semver.SemVer | null { 208 | try { 209 | const buffer = childProcess.execFileSync(filePath, [arg]); 210 | const versionString = buffer.toString("utf8").trim(); 211 | if (versionString === "0.2.0.83a2a36a") { 212 | // Zig 0.2.0 reports the verion in a non-semver format 213 | return semver.parse("0.2.0"); 214 | } 215 | return semver.parse(versionString); 216 | } catch { 217 | return null; 218 | } 219 | } 220 | 221 | export interface ZigVersion { 222 | name: string; 223 | version: semver.SemVer; 224 | url: string; 225 | sha: string; 226 | notes?: string; 227 | isMach: boolean; 228 | } 229 | 230 | export type VersionIndex = Record< 231 | string, 232 | { 233 | version?: string; 234 | notes?: string; 235 | } & Record 236 | >; 237 | 238 | export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { 239 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); 240 | if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { 241 | return vscode.workspace.workspaceFolders[0]; 242 | } 243 | return workspaceFolder; 244 | } 245 | 246 | export function isWorkspaceFile(filePath: string): boolean { 247 | const wsFolder = getWorkspaceFolder(filePath); 248 | if (!wsFolder) return false; 249 | return filePath.startsWith(wsFolder.uri.fsPath); 250 | } 251 | -------------------------------------------------------------------------------- /src/zls.ts: -------------------------------------------------------------------------------- 1 | import vscode from "vscode"; 2 | 3 | import { 4 | CancellationToken, 5 | ConfigurationParams, 6 | LSPAny, 7 | LanguageClient, 8 | LanguageClientOptions, 9 | RequestHandler, 10 | ResponseError, 11 | ServerOptions, 12 | } from "vscode-languageclient/node"; 13 | import camelCase from "camelcase"; 14 | import semver from "semver"; 15 | 16 | import * as minisign from "./minisign"; 17 | import * as versionManager from "./versionManager"; 18 | import { 19 | getHostZigName, 20 | getZigArchName, 21 | getZigOSName, 22 | handleConfigOption, 23 | resolveExePathAndVersion, 24 | workspaceConfigUpdateNoThrow, 25 | } from "./zigUtil"; 26 | import { zigProvider } from "./zigSetup"; 27 | 28 | const ZIG_MODE = [ 29 | { language: "zig", scheme: "file" }, 30 | { language: "zig", scheme: "untitled" }, 31 | ]; 32 | 33 | let versionManagerConfig: versionManager.Config; 34 | let statusItem: vscode.LanguageStatusItem; 35 | let outputChannel: vscode.LogOutputChannel; 36 | export let client: LanguageClient | null = null; 37 | 38 | export async function restartClient(context: vscode.ExtensionContext): Promise { 39 | const result = await getZLSPath(context); 40 | 41 | if (!result) { 42 | await stopClient(); 43 | updateStatusItem(null); 44 | return; 45 | } 46 | 47 | try { 48 | const newClient = await startClient(result.exe, result.version); 49 | void stopClient(); 50 | client = newClient; 51 | updateStatusItem(result.version); 52 | } catch (reason) { 53 | if (reason instanceof Error) { 54 | void vscode.window.showWarningMessage(`Failed to run ZLS language server: ${reason.message}`); 55 | } else { 56 | void vscode.window.showWarningMessage("Failed to run ZLS language server"); 57 | } 58 | updateStatusItem(null); 59 | } 60 | } 61 | 62 | async function startClient(zlsPath: string, zlsVersion: semver.SemVer): Promise { 63 | const configuration = vscode.workspace.getConfiguration("zig.zls"); 64 | const debugLog = configuration.get("debugLog", false); 65 | 66 | const args: string[] = []; 67 | 68 | if (debugLog) { 69 | /** `--enable-debug-log` has been deprecated in favor of `--log-level`. https://github.com/zigtools/zls/pull/1957 */ 70 | const zlsCLIRevampVersion = new semver.SemVer("0.14.0-50+3354fdc"); 71 | if (semver.lt(zlsVersion, zlsCLIRevampVersion)) { 72 | args.push("--enable-debug-log"); 73 | } else { 74 | args.push("--log-level", "debug"); 75 | } 76 | } 77 | 78 | const serverOptions: ServerOptions = { 79 | command: zlsPath, 80 | args: args, 81 | }; 82 | 83 | const clientOptions: LanguageClientOptions = { 84 | documentSelector: ZIG_MODE, 85 | outputChannel, 86 | middleware: { 87 | workspace: { 88 | configuration: configurationMiddleware, 89 | }, 90 | }, 91 | }; 92 | 93 | const languageClient = new LanguageClient("zig.zls", "ZLS language server", serverOptions, clientOptions); 94 | await languageClient.start(); 95 | // Formatting is handled by `zigFormat.ts` 96 | languageClient.getFeature("textDocument/formatting").clear(); 97 | return languageClient; 98 | } 99 | 100 | async function stopClient(): Promise { 101 | if (!client) return; 102 | const oldClient = client; 103 | client = null; 104 | // The `stop` call will send the "shutdown" notification to the LSP 105 | await oldClient.stop(); 106 | // The `dipose` call will send the "exit" request to the LSP which actually tells the child process to exit 107 | await oldClient.dispose(); 108 | } 109 | 110 | /** returns the file system path to the zls executable */ 111 | async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: string; version: semver.SemVer } | null> { 112 | const configuration = vscode.workspace.getConfiguration("zig.zls"); 113 | let zlsExePath = configuration.get("path"); 114 | let zlsVersion: semver.SemVer | null = null; 115 | 116 | if (!!zlsExePath) { 117 | // This will fail on older ZLS version that do not support `zls --version`. 118 | // It should be more likely that the given executable is invalid than someone using ZLS 0.9.0 or older. 119 | const result = resolveExePathAndVersion(zlsExePath, "--version"); 120 | if ("message" in result) { 121 | vscode.window 122 | .showErrorMessage(`Unexpected 'zig.zls.path': ${result.message}`, "install ZLS", "open settings") 123 | .then(async (response) => { 124 | switch (response) { 125 | case "install ZLS": 126 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 127 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 128 | await workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined); 129 | break; 130 | case "open settings": 131 | await vscode.commands.executeCommand("workbench.action.openSettings", "zig.zls.path"); 132 | break; 133 | case undefined: 134 | break; 135 | } 136 | }); 137 | return null; 138 | } 139 | return result; 140 | } 141 | 142 | if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; 143 | 144 | const zigVersion = zigProvider.getZigVersion(); 145 | if (!zigVersion) return null; 146 | 147 | const result = await fetchVersion(context, zigVersion, true); 148 | if (!result) return null; 149 | 150 | try { 151 | zlsExePath = await versionManager.install(versionManagerConfig, result.version); 152 | zlsVersion = result.version; 153 | } catch (err) { 154 | if (err instanceof Error) { 155 | void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}: ${err.message}`); 156 | } else { 157 | void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); 158 | } 159 | return null; 160 | } 161 | 162 | return { 163 | exe: zlsExePath, 164 | version: zlsVersion, 165 | }; 166 | } 167 | 168 | async function configurationMiddleware( 169 | params: ConfigurationParams, 170 | token: CancellationToken, 171 | next: RequestHandler, 172 | ): Promise { 173 | const optionIndices: Record = {}; 174 | 175 | params.items.forEach((param, index) => { 176 | if (param.section) { 177 | if (param.section === "zls.zig_exe_path") { 178 | param.section = "zig.path"; 179 | } else { 180 | param.section = `zig.zls.${camelCase(param.section.slice(4))}`; 181 | } 182 | optionIndices[param.section] = index; 183 | } 184 | }); 185 | 186 | const result = await next(params, token); 187 | if (result instanceof ResponseError) { 188 | return result; 189 | } 190 | 191 | const configuration = vscode.workspace.getConfiguration("zig.zls"); 192 | 193 | for (const name in optionIndices) { 194 | const index = optionIndices[name] as unknown as number; 195 | const section = name.slice("zig.zls.".length); 196 | const configValue = configuration.get(section); 197 | if (typeof configValue === "string") { 198 | // Make sure that `""` gets converted to `null` and resolve predefined values 199 | result[index] = configValue ? handleConfigOption(configValue) : null; 200 | } 201 | 202 | const inspect = configuration.inspect(section); 203 | const isDefaultValue = 204 | configValue === inspect?.defaultValue && 205 | inspect?.globalValue === undefined && 206 | inspect?.workspaceValue === undefined && 207 | inspect?.workspaceFolderValue === undefined; 208 | if (isDefaultValue) { 209 | if (name === "zig.zls.semanticTokens") { 210 | // The extension has a different default value for this config 211 | // option compared to ZLS 212 | continue; 213 | } 214 | result[index] = null; 215 | } 216 | } 217 | 218 | const indexOfZigPath = optionIndices["zig.path"]; 219 | if (indexOfZigPath !== undefined) { 220 | result[indexOfZigPath] = zigProvider.getZigPath(); 221 | } 222 | 223 | const additionalOptions = configuration.get>("additionalOptions", {}); 224 | 225 | for (const optionName in additionalOptions) { 226 | const section = optionName.slice("zig.zls.".length); 227 | 228 | const doesOptionExist = configuration.inspect(section)?.defaultValue !== undefined; 229 | if (doesOptionExist) { 230 | // The extension has defined a config option with the given name but the user still used `additionalOptions`. 231 | const response = await vscode.window.showWarningMessage( 232 | `The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`, 233 | `Use ${optionName} instead`, 234 | "Show zig.zls.additionalOptions", 235 | ); 236 | switch (response) { 237 | case `Use ${optionName} instead`: 238 | const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions; 239 | await workspaceConfigUpdateNoThrow( 240 | configuration, 241 | "additionalOptions", 242 | updatedAdditionalOptions, 243 | true, 244 | ); 245 | await workspaceConfigUpdateNoThrow(configuration, section, newValue, true); 246 | break; 247 | case "Show zig.zls.additionalOptions": 248 | await vscode.commands.executeCommand("workbench.action.openSettingsJson", { 249 | revealSetting: { key: "zig.zls.additionalOptions" }, 250 | }); 251 | continue; 252 | case undefined: 253 | continue; 254 | } 255 | } 256 | 257 | const optionIndex = optionIndices[optionName]; 258 | if (!optionIndex) { 259 | // ZLS has not requested a config option with the given name. 260 | continue; 261 | } 262 | 263 | result[optionIndex] = additionalOptions[optionName]; 264 | } 265 | 266 | return result as unknown[]; 267 | } 268 | 269 | /** 270 | * Similar to https://builds.zigtools.org/index.json 271 | */ 272 | interface SelectVersionResponse { 273 | /** The ZLS version */ 274 | version: string; 275 | /** `YYYY-MM-DD` */ 276 | date: string; 277 | [artifact: string]: ArtifactEntry | string | undefined; 278 | } 279 | 280 | interface SelectVersionFailureResponse { 281 | /** 282 | * The `code` **may** be one of `SelectVersionFailureCode`. Be aware that new 283 | * codes can be added over time. 284 | */ 285 | code: number; 286 | /** A simplified explanation of why no ZLS build could be selected */ 287 | message: string; 288 | } 289 | 290 | interface ArtifactEntry { 291 | /** A download URL */ 292 | tarball: string; 293 | /** A SHA256 hash of the tarball */ 294 | shasum: string; 295 | /** Size of the tarball in bytes */ 296 | size: string; 297 | } 298 | 299 | async function fetchVersion( 300 | context: vscode.ExtensionContext, 301 | zigVersion: semver.SemVer, 302 | useCache: boolean, 303 | ): Promise<{ version: semver.SemVer; artifact: ArtifactEntry } | null> { 304 | // Should the cache be periodically cleared? 305 | const cacheKey = `zls-select-version-${zigVersion.raw}`; 306 | 307 | let response: SelectVersionResponse | SelectVersionFailureResponse | null = null; 308 | try { 309 | const url = new URL("https://releases.zigtools.org/v1/zls/select-version"); 310 | url.searchParams.append("zig_version", zigVersion.raw); 311 | url.searchParams.append("compatibility", "only-runtime"); 312 | 313 | const fetchResponse = await fetch(url); 314 | response = (await fetchResponse.json()) as SelectVersionResponse | SelectVersionFailureResponse; 315 | 316 | // Cache the response 317 | if (useCache) { 318 | await context.globalState.update(cacheKey, response); 319 | } 320 | } catch (err) { 321 | // Try to read the result from cache 322 | if (useCache) { 323 | response = context.globalState.get(cacheKey) ?? null; 324 | } 325 | 326 | if (!response) { 327 | if (err instanceof Error) { 328 | void vscode.window.showErrorMessage(`Failed to query ZLS version: ${err.message}`); 329 | } else { 330 | throw err; 331 | } 332 | return null; 333 | } 334 | } 335 | 336 | if ("message" in response) { 337 | void vscode.window.showErrorMessage(`Unable to fetch ZLS: ${response.message as string}`); 338 | return null; 339 | } 340 | 341 | const hostName = getHostZigName(); 342 | 343 | if (!(hostName in response)) { 344 | void vscode.window.showErrorMessage( 345 | `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`, 346 | ); 347 | return null; 348 | } 349 | 350 | return { 351 | version: new semver.SemVer(response.version), 352 | artifact: response[hostName] as ArtifactEntry, 353 | }; 354 | } 355 | 356 | async function isEnabled(): Promise { 357 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 358 | if (!!zlsConfig.get("path")) return true; 359 | 360 | switch (zlsConfig.get<"ask" | "off" | "on">("enabled", "ask")) { 361 | case "on": 362 | return true; 363 | case "off": 364 | return false; 365 | case "ask": { 366 | const response = await vscode.window.showInformationMessage( 367 | "We recommend enabling the ZLS language server for a better editing experience. Would you like to install it?", 368 | { modal: true }, 369 | "Yes", 370 | "No", 371 | ); 372 | switch (response) { 373 | case "Yes": 374 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 375 | return true; 376 | case "No": 377 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "off", true); 378 | return false; 379 | case undefined: 380 | return false; 381 | } 382 | } 383 | } 384 | } 385 | 386 | function updateStatusItem(version: semver.SemVer | null) { 387 | if (version) { 388 | statusItem.text = `ZLS ${version.toString()}`; 389 | statusItem.detail = "ZLS Version"; 390 | statusItem.severity = vscode.LanguageStatusSeverity.Information; 391 | statusItem.command = { 392 | title: "View Output", 393 | command: "zig.zls.openOutput", 394 | }; 395 | } else { 396 | statusItem.text = "ZLS not enabled"; 397 | statusItem.detail = undefined; 398 | statusItem.severity = vscode.LanguageStatusSeverity.Error; 399 | const zigPath = zigProvider.getZigPath(); 400 | const zigVersion = zigProvider.getZigVersion(); 401 | if (zigPath !== null && zigVersion !== null) { 402 | statusItem.command = { 403 | title: "Enable", 404 | command: "zig.zls.enable", 405 | }; 406 | } else { 407 | statusItem.command = undefined; 408 | } 409 | } 410 | } 411 | 412 | export async function activate(context: vscode.ExtensionContext) { 413 | { 414 | // This check can be removed once enough time has passed so that most users switched to the new value 415 | 416 | // remove the `zls_install` directory from the global storage 417 | try { 418 | await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zls_install"), { 419 | recursive: true, 420 | useTrash: false, 421 | }); 422 | } catch {} 423 | 424 | // convert a `zig.zls.path` that points to the global storage to `zig.zls.enabled == "on"` 425 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 426 | const zlsPath = zlsConfig.get("path", ""); 427 | if (zlsPath.startsWith(context.globalStorageUri.fsPath)) { 428 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 429 | await workspaceConfigUpdateNoThrow(zlsConfig, "path", undefined, true); 430 | } 431 | } 432 | 433 | versionManagerConfig = { 434 | context: context, 435 | title: "ZLS", 436 | exeName: "zls", 437 | extraTarArgs: [], 438 | /** https://github.com/zigtools/release-worker */ 439 | minisignKey: minisign.parseKey("RWR+9B91GBZ0zOjh6Lr17+zKf5BoSuFvrx2xSeDE57uIYvnKBGmMjOex"), 440 | versionArg: "--version", 441 | mirrorUrls: [], 442 | canonicalUrl: { 443 | release: vscode.Uri.parse("https://builds.zigtools.org"), 444 | nightly: vscode.Uri.parse("https://builds.zigtools.org"), 445 | }, 446 | getArtifactName(version) { 447 | const fileExtension = process.platform === "win32" ? "zip" : "tar.xz"; 448 | return `zls-${getZigOSName()}-${getZigArchName()}-${version.raw}.${fileExtension}`; 449 | }, 450 | }; 451 | 452 | // Remove after some time has passed from the prefix change. 453 | await versionManager.convertOldInstallPrefixes(versionManagerConfig); 454 | 455 | outputChannel = vscode.window.createOutputChannel("ZLS language server", { log: true }); 456 | statusItem = vscode.languages.createLanguageStatusItem("zig.zls.status", ZIG_MODE); 457 | statusItem.name = "ZLS"; 458 | updateStatusItem(null); 459 | 460 | context.subscriptions.push( 461 | outputChannel, 462 | statusItem, 463 | vscode.commands.registerCommand("zig.zls.enable", async () => { 464 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 465 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 466 | }), 467 | vscode.commands.registerCommand("zig.zls.stop", async () => { 468 | await stopClient(); 469 | }), 470 | vscode.commands.registerCommand("zig.zls.startRestart", async () => { 471 | const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); 472 | await workspaceConfigUpdateNoThrow(zlsConfig, "enabled", "on", true); 473 | await restartClient(context); 474 | }), 475 | vscode.commands.registerCommand("zig.zls.openOutput", () => { 476 | outputChannel.show(); 477 | }), 478 | ); 479 | 480 | if (await isEnabled()) { 481 | await restartClient(context); 482 | } 483 | 484 | // These checks are added later to avoid ZLS be started twice because `isEnabled` sets `zig.zls.enabled`. 485 | context.subscriptions.push( 486 | vscode.workspace.onDidChangeConfiguration(async (change) => { 487 | // The `zig.path` config option is handled by `zigProvider.onChange`. 488 | if ( 489 | change.affectsConfiguration("zig.zls.enabled", undefined) || 490 | change.affectsConfiguration("zig.zls.path", undefined) || 491 | change.affectsConfiguration("zig.zls.debugLog", undefined) 492 | ) { 493 | await restartClient(context); 494 | } 495 | }), 496 | zigProvider.onChange.event(async () => { 497 | await restartClient(context); 498 | }), 499 | ); 500 | } 501 | 502 | export async function deactivate(): Promise { 503 | await stopClient(); 504 | } 505 | -------------------------------------------------------------------------------- /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(await|resume|suspend|async|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(usingnamespace|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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------