From bb81d52f785dbb8723f961d17d748159850c433a Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 03:56:15 +0100 Subject: [PATCH 01/25] automatically manage ZLS versions These changes make ZLS an "invisible" component of the extension after the initial setup. This also removes the `zig.zls.checkForUpdate` command. I am not sure if it even needs to be brought back. I've made sure that no error is reported if no internet connection is available. The last installed ZLS version should be reused. closes #136 because `zig.zls.startRestart` will just enable ZLS instead of complaining --- package.json | 28 ++-- src/zigFormat.ts | 17 +++ src/zigSetup.ts | 112 +++++---------- src/zls.ts | 349 +++++++++++++++++++++++------------------------ 4 files changed, 240 insertions(+), 266 deletions(-) diff --git a/package.json b/package.json index 9f32a8d..603cffa 100644 --- a/package.json +++ b/package.json @@ -150,16 +150,21 @@ ], "default": "off" }, - "zig.zls.checkForUpdate": { + "zig.zls.enabled": { "scope": "resource", - "type": "boolean", - "description": "Whether to automatically check for new updates", - "default": true + "type": "string", + "description": "Whether to enable the optional ZLS Language Server", + "enum": [ + "ask", + "off", + "on" + ], + "default": "ask" }, "zig.zls.path": { "scope": "machine-overridable", "type": "string", - "description": "Path to `zls` executable. Example: `C:/zls/zig-cache/bin/zls.exe`. The string \"zls\" means lookup ZLS in PATH.", + "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.", "format": "path" }, "zig.zls.enableSnippets": { @@ -341,23 +346,18 @@ "category": "Zig Setup" }, { - "command": "zig.zls.install", - "title": "Install Server", + "command": "zig.zls.enable", + "title": "Enable Language Server", "category": "Zig Language Server" }, { "command": "zig.zls.startRestart", - "title": "Start / Restart Server", + "title": "Start / Restart Language Server", "category": "Zig Language Server" }, { "command": "zig.zls.stop", - "title": "Stop Server", - "category": "Zig Language Server" - }, - { - "command": "zig.zls.update", - "title": "Check for Server Updates", + "title": "Stop Language Server", "category": "Zig Language Server" } ], diff --git a/src/zigFormat.ts b/src/zigFormat.ts index f954100..e1aab19 100644 --- a/src/zigFormat.ts +++ b/src/zigFormat.ts @@ -3,6 +3,9 @@ import vscode from "vscode"; import childProcess from "child_process"; import util from "util"; +import { DocumentRangeFormattingRequest, TextDocumentIdentifier } from "vscode-languageclient"; + +import * as zls from "./zls"; import { getZigPath } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); @@ -76,6 +79,20 @@ async function provideDocumentRangeFormattingEdits( options: vscode.FormattingOptions, token: vscode.CancellationToken, ): Promise { + if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "zls") { + if (zls.client !== null) { + return await (zls.client.sendRequest( + DocumentRangeFormattingRequest.type, + { + textDocument: TextDocumentIdentifier.create(document.uri.toString()), + range: range, + options: options, + }, + token, + ) as Promise); + } + } + const zigPath = getZigPath(); const abortController = new AbortController(); diff --git a/src/zigSetup.ts b/src/zigSetup.ts index f255c4f..319ce4a 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -5,7 +5,7 @@ import semver from "semver"; import vscode from "vscode"; import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; -import { installZLS } from "./zls"; +import { restartClient } from "./zls"; const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json"; @@ -40,6 +40,8 @@ export async function installZig(context: vscode.ExtensionContext, version: ZigV void vscode.window.showInformationMessage( `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, ); + + void restartClient(context); } } @@ -169,7 +171,7 @@ function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext export async function setupZig(context: vscode.ExtensionContext) { { - // convert an empty string for `zig.path` and `zig.zls.path` to `zig` and `zls` respectively. + // convert an empty string for `zig.path` to `zig`. // This check can be removed once enough time has passed so that most users switched to the new value const zigConfig = vscode.workspace.getConfiguration("zig"); @@ -178,12 +180,6 @@ export async function setupZig(context: vscode.ExtensionContext) { if (zigPath === "" && initialSetupDone) { await zigConfig.update("path", "zig", true); } - - const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); - const zlsPath = zlsConfig.get("path"); - if (zlsPath === "" && initialSetupDone) { - await zlsConfig.update("path", "zls", true); - } } context.environmentVariableCollection.description = "Add Zig to PATH"; @@ -192,7 +188,6 @@ export async function setupZig(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("zig.install", async () => { await selectVersionAndInstall(context); - await installZLS(context, true); }), vscode.commands.registerCommand("zig.update", async () => { await checkUpdate(context); @@ -216,75 +211,40 @@ export async function setupZig(context: vscode.ExtensionContext) { async function initialSetup(context: vscode.ExtensionContext): Promise { const zigConfig = vscode.workspace.getConfiguration("zig"); + if (!!zigConfig.get("path")) return true; + + const zigResponse = await vscode.window.showInformationMessage( + "Zig path hasn't been set, do you want to specify the path or install Zig?", + { modal: true }, + "Install", + "Specify path", + "Use Zig in PATH", + ); + switch (zigResponse) { + case "Install": + await selectVersionAndInstall(context); + const zigPath = vscode.workspace.getConfiguration("zig").get("path"); + if (!zigPath) return false; + break; + case "Specify path": + const uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "Select Zig executable", + }); + if (!uris) return false; - if (!zigConfig.get("path")) { - const zigResponse = await vscode.window.showInformationMessage( - "Zig path hasn't been set, do you want to specify the path or install Zig?", - { modal: true }, - "Install", - "Specify path", - "Use Zig in PATH", - ); - switch (zigResponse) { - case "Install": - await selectVersionAndInstall(context); - const zigPath = vscode.workspace.getConfiguration("zig").get("path"); - if (!zigPath) return false; - break; - case "Specify path": - const uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig executable", - }); - if (!uris) return false; - - const version = getVersion(uris[0].path, "version"); - if (!version) return false; - - await zigConfig.update("path", uris[0].path, true); - break; - case "Use Zig in PATH": - await zigConfig.update("path", "zig", true); - break; - case undefined: - return false; - } - } - - const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); - - if (!zlsConfig.get("path")) { - const zlsResponse = await vscode.window.showInformationMessage( - "We recommend enabling ZLS (the Zig Language Server) for a better editing experience. Would you like to install it?", - { modal: true }, - "Install", - "Specify path", - "Use ZLS in PATH", - ); + const version = getVersion(uris[0].path, "version"); + if (!version) return false; - switch (zlsResponse) { - case "Install": - await installZLS(context, false); - break; - case "Specify path": - const uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig Language Server (ZLS) executable", - }); - if (!uris) return true; - - await zlsConfig.update("path", uris[0].path, true); - break; - case "Use ZLS in PATH": - await zlsConfig.update("path", "zls", true); - break; - case undefined: - break; - } + await zigConfig.update("path", uris[0].path, true); + break; + case "Use Zig in PATH": + await zigConfig.update("path", "zig", true); + break; + case undefined: + return false; } return true; diff --git a/src/zls.ts b/src/zls.ts index 214f136..3471565 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -15,36 +15,49 @@ import axios from "axios"; import camelCase from "camelcase"; import semver from "semver"; -import { - downloadAndExtractArtifact, - getExePath, - getHostZigName, - getVersion, - getZigPath, - handleConfigOption, - shouldCheckUpdate, -} from "./zigUtil"; - -let outputChannel: vscode.OutputChannel; -export let client: LanguageClient | null = null; +import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, handleConfigOption } from "./zigUtil"; +import { existsSync } from "fs"; const ZIG_MODE: DocumentSelector = [ { language: "zig", scheme: "file" }, { language: "zig", scheme: "untitled" }, ]; -async function startClient() { +let outputChannel: vscode.OutputChannel; +export let client: LanguageClient | null = null; + +export async function restartClient(context: vscode.ExtensionContext): Promise { const configuration = vscode.workspace.getConfiguration("zig.zls"); - const debugLog = configuration.get("debugLog", false); + if (!configuration.get("path") && configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") { + await stopClient(); + return; + } - const zlsPath = getZLSPath(); + const result = await getZLSPath(context); + if (!result) return; + + try { + const newClient = await startClient(result.exe); + await stopClient(); + client = newClient; + } catch (reason) { + if (reason instanceof Error) { + void vscode.window.showWarningMessage(`Failed to run Zig Language Server (ZLS): ${reason.message}`); + } else { + void vscode.window.showWarningMessage("Failed to run Zig Language Server (ZLS)"); + } + } +} + +async function startClient(zlsPath: string): Promise { + const configuration = vscode.workspace.getConfiguration("zig.zls"); + const debugLog = configuration.get("debugLog", false); const serverOptions: ServerOptions = { command: zlsPath, args: debugLog ? ["--enable-debug-log"] : [], }; - // Options to control the language client const clientOptions: LanguageClientOptions = { documentSelector: ZIG_MODE, outputChannel, @@ -55,42 +68,81 @@ async function startClient() { }, }; - // Create the language client and start the client. - client = new LanguageClient("zig.zls", "Zig Language Server", serverOptions, clientOptions); - - return client - .start() - .catch((reason: unknown) => { - if (reason instanceof Error) { - void vscode.window.showWarningMessage(`Failed to run Zig Language Server (ZLS): ${reason.message}`); - } else { - void vscode.window.showWarningMessage("Failed to run Zig Language Server (ZLS)"); - } - client = null; - }) - .then(() => { - if (client && vscode.workspace.getConfiguration("zig").get("formattingProvider") !== "zls") { - client.getFeature("textDocument/formatting").dispose(); - } - }); + const languageClient = new LanguageClient("zig.zls", "Zig Language Server", serverOptions, clientOptions); + await languageClient.start(); + // Formatting is handled by `zigFormat.ts` + languageClient.getFeature("textDocument/formatting").dispose(); + return languageClient; } -export async function stopClient() { - if (client) { - // The `stop` call will send the "shutdown" notification to the LSP - await client.stop(); - // The `dipose` call will send the "exit" request to the LSP which actually tells the child process to exit - await client.dispose(); - } +async function stopClient(): Promise { + if (!client) return; + // The `stop` call will send the "shutdown" notification to the LSP + await client.stop(); + // The `dipose` call will send the "exit" request to the LSP which actually tells the child process to exit + await client.dispose(); client = null; } /** returns the file system path to the zls executable */ -export function getZLSPath(): string { +async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: string; version: semver.SemVer } | null> { const configuration = vscode.workspace.getConfiguration("zig.zls"); - const zlsPath = configuration.get("path"); - const exePath = zlsPath !== "zls" ? zlsPath : null; // the string "zls" means lookup in PATH - return getExePath(exePath, "zls", "zig.zls.path"); + let zlsExePath = configuration.get("path"); + let zlsVersion: semver.SemVer | null = null; + + if (!zlsExePath) { + if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; + + let zigVersion: semver.SemVer | null; + try { + zigVersion = getVersion(getZigPath(), "version"); + } catch { + return null; + } + if (!zigVersion) return null; + + const result = await fetchVersion(context, zigVersion, true); + if (!result) return null; + + const isWindows = process.platform === "win32"; + const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zls", result.version.raw); + zlsExePath = vscode.Uri.joinPath(installDir, isWindows ? "zls.exe" : "zls").fsPath; + zlsVersion = result.version; + + if (!existsSync(zlsExePath)) { + try { + await downloadAndExtractArtifact( + "ZLS", + "zls", + installDir, + result.artifact.tarball, + result.artifact.shasum, + [], + ); + } catch { + void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); + return null; + } + } + } + + const checkedZLSVersion = getVersion(zlsExePath, "--version"); + if (!checkedZLSVersion) { + void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`); + return null; + } + if (zlsVersion && checkedZLSVersion.compare(zlsVersion) !== 0) { + // The Matrix is broken! + void vscode.window.showErrorMessage( + `Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`, + ); + return null; + } + + return { + exe: zlsExePath, + version: checkedZLSVersion, + }; } async function configurationMiddleware( @@ -190,7 +242,7 @@ interface SelectVersionResponse { [artifact: string]: ArtifactEntry | string | undefined; } -export interface SelectVersionFailureResponse { +interface SelectVersionFailureResponse { /** * The `code` **may** be one of `SelectVersionFailureCode`. Be aware that new * codes can be added over time. @@ -210,9 +262,14 @@ interface ArtifactEntry { } async function fetchVersion( + context: vscode.ExtensionContext, zigVersion: semver.SemVer, + useCache: boolean, ): Promise<{ version: semver.SemVer; artifact: ArtifactEntry } | null> { - let response: SelectVersionResponse | SelectVersionFailureResponse; + // Should the cache be periodically cleared? + const cacheKey = `zls-select-version-${zigVersion.raw}`; + + let response: SelectVersionResponse | SelectVersionFailureResponse | null = null; try { response = ( await axios.get( @@ -226,13 +283,25 @@ async function fetchVersion( }, ) ).data; + + // Cache the response + if (useCache) { + await context.globalState.update(cacheKey, response); + } } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage(`Failed to query ZLS version: ${err.message}`); - } else { - throw err; + // Try to read the result from cache + if (useCache) { + response = context.globalState.get(cacheKey) ?? null; + } + + if (!response) { + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Failed to query ZLS version: ${err.message}`); + } else { + throw err; + } + return null; } - return null; } if ("message" in response) { @@ -255,157 +324,85 @@ async function fetchVersion( }; } -// checks whether there is newer version on master -async function checkUpdate(context: vscode.ExtensionContext) { - const configuration = vscode.workspace.getConfiguration("zig.zls"); - const zlsPath = configuration.get("path"); - const zlsBinPath = vscode.Uri.joinPath(context.globalStorageUri, "zls_install", "zls").fsPath; - if (!zlsPath?.startsWith(zlsBinPath)) return; - - const zigVersion = getVersion(getZigPath(), "version"); - if (!zigVersion) return; - - const currentVersion = getVersion(zlsPath, "--version"); - if (!currentVersion) return; - - const result = await fetchVersion(zigVersion); - if (!result) return; - - if (semver.gte(currentVersion, result.version)) return; - - const response = await vscode.window.showInformationMessage("New version of ZLS available", "Install", "Ignore"); - switch (response) { - case "Install": - await installZLSVersion(context, result.artifact); - break; - case "Ignore": - case undefined: - break; - } -} - -export async function installZLS(context: vscode.ExtensionContext, ask: boolean) { - const zigVersion = getVersion(getZigPath(), "version"); - if (!zigVersion) { - const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); - await zlsConfiguration.update("path", undefined, true); - return undefined; - } - - const result = await fetchVersion(zigVersion); - if (!result) return; - - if (ask) { - const selected = await vscode.window.showInformationMessage( - `Do you want to install ZLS (the Zig Language Server) for Zig version ${result.version.toString()}`, - "Install", - "Ignore", - ); - switch (selected) { - case "Install": - break; - case "Ignore": - const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); - await zlsConfiguration.update("path", undefined, true); - return; - case undefined: - return; +async function isEnabled(): Promise { + const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + if (!!zlsConfig.get("path")) return true; + + switch (zlsConfig.get<"ask" | "off" | "on">("enabled", "ask")) { + case "on": + return true; + case "off": + return false; + case "ask": { + const response = await vscode.window.showInformationMessage( + "We recommend enabling the ZLS Language Server for a better editing experience. Would you like to install it?", + { modal: true }, + "Yes", + "No", + ); + switch (response) { + case "Yes": + await zlsConfig.update("enabled", "on"); + return true; + case "No": + await zlsConfig.update("enabled", "off"); + return false; + case undefined: + return false; + } } } - - await installZLSVersion(context, result.artifact); -} - -async function installZLSVersion(context: vscode.ExtensionContext, artifact: ArtifactEntry) { - const zlsPath = await downloadAndExtractArtifact( - "ZLS", - "zls", - vscode.Uri.joinPath(context.globalStorageUri, "zls_install"), - artifact.tarball, - artifact.shasum, - [], - ); - - const zlsConfiguration = vscode.workspace.getConfiguration("zig.zls", null); - await zlsConfiguration.update("path", zlsPath ?? undefined, true); } -function checkInstalled(): boolean { - const zlsPath = vscode.workspace.getConfiguration("zig.zls").get("path"); - if (!zlsPath) { - void vscode.window.showErrorMessage("This command cannot be run without setting 'zig.zls.path'.", { - modal: true, - }); - return false; +export async function activate(context: vscode.ExtensionContext) { + { + // This check can be removed once enough time has passed so that most users switched to the new value + + // convert a `zig.zls.path` that points to the global storage to `zig.zls.enabled == "on"` + const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + const zlsPath = zlsConfig.get("path", ""); + if (zlsPath.startsWith(context.globalStorageUri.fsPath)) { + await zlsConfig.update("enabled", "on", true); + await zlsConfig.update("path", undefined, true); + } } - return true; -} -export async function activate(context: vscode.ExtensionContext) { outputChannel = vscode.window.createOutputChannel("Zig Language Server"); context.subscriptions.push( outputChannel, - vscode.commands.registerCommand("zig.zls.install", async () => { - try { - getZigPath(); - } catch { - void vscode.window.showErrorMessage("This command cannot be run without a valid zig path.", { - modal: true, - }); - return; - } - - await stopClient(); - await installZLS(context, false); + vscode.commands.registerCommand("zig.zls.enable", async () => { + const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + await zlsConfig.update("enabled", "on"); }), vscode.commands.registerCommand("zig.zls.stop", async () => { - if (!checkInstalled()) return; - await stopClient(); }), vscode.commands.registerCommand("zig.zls.startRestart", async () => { - if (!checkInstalled()) return; - - await stopClient(); - await startClient(); + const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + await zlsConfig.update("enabled", "on"); + await restartClient(context); }), - vscode.commands.registerCommand("zig.zls.update", async () => { - if (!checkInstalled()) return; - - await stopClient(); - await checkUpdate(context); + vscode.commands.registerCommand("zig.zls.openOutput", () => { + outputChannel.show(); }), vscode.workspace.onDidChangeConfiguration(async (change) => { if ( + change.affectsConfiguration("zig.path", undefined) || + change.affectsConfiguration("zig.zls.enabled", undefined) || change.affectsConfiguration("zig.zls.path", undefined) || change.affectsConfiguration("zig.zls.debugLog", undefined) ) { - await stopClient(); - const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); - if (zlsConfig.get("path")) { - await startClient(); - } - } - if (client && change.affectsConfiguration("zig.formattingProvider", undefined)) { - client.getFeature("textDocument/formatting").dispose(); - if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "zls") { - client - .getFeature("textDocument/formatting") - .initialize(client.initializeResult?.capabilities ?? {}, ZIG_MODE); - } + await restartClient(context); } }), ); - const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); - if (!zlsConfig.get("path")) return; - if (zlsConfig.get("checkForUpdate") && (await shouldCheckUpdate(context, "zlsUpdate"))) { - await checkUpdate(context); + if (await isEnabled()) { + await restartClient(context); } - await startClient(); } -export function deactivate(): Thenable { - return stopClient(); +export async function deactivate(): Promise { + await stopClient(); } From 4ce7ae222576ce48e6b59d943ceabf76c95138c4 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:44:55 +0100 Subject: [PATCH 02/25] add a VersionManager that installs Zig and ZLS fixes #111 --- src/versionManager.ts | 170 ++++++++++++++++++++++++++++++++++++++++++ src/zigSetup.ts | 53 ++++++++----- src/zigUtil.ts | 135 ++++++++------------------------- src/zls.ts | 42 +++++------ 4 files changed, 254 insertions(+), 146 deletions(-) create mode 100644 src/versionManager.ts diff --git a/src/versionManager.ts b/src/versionManager.ts new file mode 100644 index 0000000..1dbaba0 --- /dev/null +++ b/src/versionManager.ts @@ -0,0 +1,170 @@ +/** + * A version manager for Zig and ZLS. + * + * Expects a provider that follows the following scheme: + * `${PROVIDER_URL}/${NAME}-${OS}-${ARCH}-${VERSION}.${FILE_EXTENSION}` + * + * Example: + * - `https://ziglang.org/download/0.13.0/zig-windows-x86_64-0.13.0.zip` + * - `https://builds.zigtools.org/zls-linux-x86_64-0.13.0.tar.xz` + */ + +import vscode from "vscode"; + +import childProcess from "child_process"; +import fs from "fs"; +import util from "util"; +import which from "which"; + +import axios from "axios"; +import semver from "semver"; + +import { getVersion, getZigArchName, getZigOSName } from "./zigUtil"; + +const execFile = util.promisify(childProcess.execFile); +const chmod = util.promisify(fs.chmod); + +export interface Config { + context: vscode.ExtensionContext; + /** The name of the application. */ + title: string; + /** The name of the executable file. */ + exeName: string; + /** The command-line argument that should passed to `tar` to exact the tarball. */ + extraTarArgs: string[]; + /** + * The command-line argument that should passed to the executable to query the version. + * `"version"` for Zig, `"--version"` for ZLS + */ + versionArg: string; + canonicalUrl: { + release: vscode.Uri; + nightly: vscode.Uri; + }; +} + +/** Returns the path to the executable */ +export async function install(config: Config, version: semver.SemVer): Promise { + const exeName = config.exeName + (process.platform === "win32" ? ".exe" : ""); + const subDirName = `${getZigOSName()}-${getZigArchName()}-${version.raw}`; + const exeUri = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName, subDirName, exeName); + + try { + await vscode.workspace.fs.stat(exeUri); + return exeUri.fsPath; + } catch (e) { + if (e instanceof vscode.FileSystemError) { + if (e.code !== "FileNotFound") { + throw e; + } + // go ahead an install + } else { + throw e; + } + } + + const canonicalUrl = version.prerelease.length === 0 ? config.canonicalUrl.release : config.canonicalUrl.nightly; + const mirrorName = new URL(canonicalUrl.toString()).host; + return await installFromMirror(config, version, canonicalUrl, mirrorName); +} + +/** Returns the path to the executable */ +async function installFromMirror( + config: Config, + version: semver.SemVer, + mirrorUrl: vscode.Uri, + mirrorName: string, +): Promise { + const isWindows = process.platform === "win32"; + const fileExtension = isWindows ? "zip" : "tar.xz"; + const exeName = config.exeName + (isWindows ? ".exe" : ""); + const subDirName = `${getZigOSName()}-${getZigArchName()}-${version.raw}`; + const fileName = `${config.exeName}-${subDirName}.${fileExtension}`; + + const installDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName, subDirName); + const exeUri = vscode.Uri.joinPath(installDir, exeName); + const tarballUri = vscode.Uri.joinPath(installDir, fileName); + + const tarPath = await which("tar", { nothrow: true }); + if (!tarPath) { + throw new Error(`Downloaded ${config.title} tarball can't be extracted because 'tar' could not be found`); + } + + return await vscode.window.withProgress( + { + title: `Installing ${config.title} from ${mirrorName}`, + location: vscode.ProgressLocation.Notification, + }, + async (progress, cancelToken) => { + const abortController = new AbortController(); + cancelToken.onCancellationRequested(() => { + abortController.abort(); + }); + + const artifactUrl = vscode.Uri.joinPath(mirrorUrl, fileName); + /** https://github.com/mlugg/setup-zig adds a `?source=github-actions` query parameter so we add our own. */ + const artifactUrlWithQuery = artifactUrl.with({ query: "source=vscode-zig" }); + + const artifactResponse = await axios.get(artifactUrlWithQuery.toString(), { + responseType: "arraybuffer", + signal: abortController.signal, + onDownloadProgress: (progressEvent) => { + if (progressEvent.total) { + const increment = (progressEvent.bytes / progressEvent.total) * 100; + progress.report({ + message: progressEvent.progress + ? `downloading tarball ${(progressEvent.progress * 100).toFixed()}%` + : "downloading tarball...", + increment: increment, + }); + } + }, + }); + const artifactData = Buffer.from(artifactResponse.data); + + try { + await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); + } catch {} + await vscode.workspace.fs.createDirectory(installDir); + await vscode.workspace.fs.writeFile(tarballUri, new Uint8Array(artifactData)); + + progress.report({ message: "Extracting..." }); + try { + await execFile( + tarPath, + ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(config.extraTarArgs), + { + signal: abortController.signal, + timeout: 60000, // 60 seconds + }, + ); + } catch (err) { + try { + await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); + } catch {} + if (err instanceof Error) { + throw new Error(`Failed to extract ${config.title} tarball: ${err.message}`); + } else { + throw err; + } + } finally { + try { + await vscode.workspace.fs.delete(tarballUri, { useTrash: false }); + } catch {} + } + + const exeVersion = getVersion(exeUri.fsPath, config.versionArg); + if (!exeVersion || exeVersion.compare(version) !== 0) { + try { + await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); + } catch {} + // a mirror may provide the wrong version + throw new Error(`Failed to validate version of ${config.title} installation!`); + } + + await chmod(exeUri.fsPath, 0o755); + + return exeUri.fsPath; + }, + ); +} diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 319ce4a..07cba1d 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -4,9 +4,12 @@ import axios from "axios"; import semver from "semver"; import vscode from "vscode"; -import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; +import * as versionManager from "./versionManager"; +import { getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; import { restartClient } from "./zls"; +let versionManagerConfig: versionManager.Config; + const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json"; function getNightlySemVer(url: string): string { @@ -22,27 +25,20 @@ interface ZigVersion { url: string; sha: string; notes?: string; + version: semver.SemVer; } -export async function installZig(context: vscode.ExtensionContext, version: ZigVersion) { - const zigPath = await downloadAndExtractArtifact( - "Zig", - "zig", - vscode.Uri.joinPath(context.globalStorageUri, "zig_install"), - version.url, - version.sha, - ["--strip-components=1"], - ); - if (zigPath !== null) { - const configuration = vscode.workspace.getConfiguration("zig"); - await configuration.update("path", zigPath, true); +export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { + const zigPath = await versionManager.install(versionManagerConfig, version); - void vscode.window.showInformationMessage( - `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, - ); + const configuration = vscode.workspace.getConfiguration("zig"); + await configuration.update("path", zigPath, true); - void restartClient(context); - } + void vscode.window.showInformationMessage( + `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, + ); + + void restartClient(context); } async function getVersions(): Promise { @@ -51,13 +47,18 @@ async function getVersions(): Promise { const result: ZigVersion[] = []; for (let key in indexJson) { const value = indexJson[key]; + let version: semver.SemVer; if (key === "master") { key = "nightly"; + version = new semver.SemVer((value as unknown as { version: string }).version); + } else { + version = new semver.SemVer(key); } const release = value[hostName]; if (release) { result.push({ name: key, + version: version, url: release.tarball, sha: release.shasum, notes: (value as { notes?: string }).notes, @@ -90,7 +91,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { if (selection === undefined) return; for (const option of available) { if (option.name === selection.label) { - await installZig(context, option); + await installZig(context, option.version); return; } } @@ -117,7 +118,7 @@ async function checkUpdate(context: vscode.ExtensionContext) { ); switch (response) { case "Install": - await installZig(context, update); + await installZig(context, update.version); break; case "Ignore": case undefined: @@ -182,6 +183,18 @@ export async function setupZig(context: vscode.ExtensionContext) { } } + versionManagerConfig = { + context: context, + title: "Zig", + exeName: "zig", + extraTarArgs: ["--strip-components=1"], + versionArg: "version", + canonicalUrl: { + release: vscode.Uri.parse("https://ziglang.org/download"), + nightly: vscode.Uri.parse("https://ziglang.org/builds"), + }, + }; + context.environmentVariableCollection.description = "Add Zig to PATH"; updateZigEnvironmentVariableCollection(context); diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 8d5ce53..49ed7df 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -1,20 +1,13 @@ import vscode from "vscode"; import childProcess from "child_process"; -import crypto from "crypto"; import fs from "fs"; import os from "os"; import path from "path"; -import { promisify } from "util"; -import assert from "assert"; -import axios from "axios"; import semver from "semver"; import which from "which"; -const execFile = promisify(childProcess.execFile); -const chmod = promisify(fs.chmod); - // Replace any references to predefined variables in config string. // https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables export function handleConfigOption(input: string): string { @@ -103,18 +96,37 @@ export async function shouldCheckUpdate(context: vscode.ExtensionContext, key: s return true; } +export function getZigArchName(): string { + switch (process.arch) { + case "ia32": + return "x86"; + case "x64": + return "x86_64"; + case "arm": + return "armv7a"; + case "arm64": + return "aarch64"; + case "ppc": + return "powerpc"; + case "ppc64": + return "powerpc64le"; + default: + return process.arch; + } +} +export function getZigOSName(): string { + switch (process.platform) { + case "darwin": + return "macos"; + case "win32": + return "windows"; + default: + return process.platform; + } +} + export function getHostZigName(): string { - let platform: string = process.platform; - if (platform === "darwin") platform = "macos"; - if (platform === "win32") platform = "windows"; - let arch: string = process.arch; - if (arch === "ia32") arch = "x86"; - if (arch === "x64") arch = "x86_64"; - if (arch === "arm") arch = "armv7a"; - if (arch === "arm64") arch = "aarch64"; - if (arch === "ppc") arch = "powerpc"; - if (arch === "ppc64") arch = "powerpc64le"; - return `${arch}-${platform}`; + return `${getZigArchName()}-${getZigOSName()}`; } export function getVersion(filePath: string, arg: string): semver.SemVer | null { @@ -131,93 +143,6 @@ export function getVersion(filePath: string, arg: string): semver.SemVer | null } } -export async function downloadAndExtractArtifact( - /** e.g. `Zig` or `ZLS` */ - title: string, - /** e.g. `zig` or `zls` */ - executableName: string, - /** e.g. inside `context.globalStorageUri` */ - installDir: vscode.Uri, - artifactUrl: string, - /** The expected sha256 hash (in hex) of the artifact/tarball. */ - sha256: string, - /** Extract arguments that should be passed to `tar`. e.g. `--strip-components=1` */ - extraTarArgs: string[], -): Promise { - assert.strictEqual(sha256.length, 64); - - return await vscode.window.withProgress( - { - title: `Installing ${title}`, - location: vscode.ProgressLocation.Notification, - }, - async (progress) => { - progress.report({ message: `downloading ${title} tarball...` }); - const response = await axios.get(artifactUrl, { - responseType: "arraybuffer", - onDownloadProgress: (progressEvent) => { - if (progressEvent.total) { - const increment = (progressEvent.bytes / progressEvent.total) * 100; - progress.report({ - message: progressEvent.progress - ? `downloading tarball ${(progressEvent.progress * 100).toFixed()}%` - : "downloading tarball...", - increment: increment, - }); - } - }, - }); - const tarHash = crypto.createHash("sha256").update(response.data).digest("hex"); - if (tarHash !== sha256) { - throw Error(`hash of downloaded tarball ${tarHash} does not match expected hash ${sha256}`); - } - - const tarPath = await which("tar", { nothrow: true }); - if (!tarPath) { - void vscode.window.showErrorMessage( - `Downloaded ${title} tarball can't be extracted because 'tar' could not be found`, - ); - return null; - } - - const tarballUri = vscode.Uri.joinPath(installDir, path.basename(artifactUrl)); - - try { - await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); - } catch {} - await vscode.workspace.fs.createDirectory(installDir); - await vscode.workspace.fs.writeFile(tarballUri, response.data); - - progress.report({ message: "Extracting..." }); - try { - await execFile(tarPath, ["-xf", tarballUri.fsPath, "-C", installDir.fsPath].concat(extraTarArgs), { - timeout: 60000, // 60 seconds - }); - } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage(`Failed to extract ${title} tarball: ${err.message}`); - } else { - throw err; - } - return null; - } finally { - try { - await vscode.workspace.fs.delete(tarballUri, { useTrash: false }); - } catch {} - } - - progress.report({ message: "Installing..." }); - - const isWindows = process.platform === "win32"; - const exeName = `${executableName}${isWindows ? ".exe" : ""}`; - const exePath = vscode.Uri.joinPath(installDir, exeName).fsPath; - await chmod(exePath, 0o755); - - return exePath; - }, - ); -} - export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { diff --git a/src/zls.ts b/src/zls.ts index 3471565..539b6a4 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -15,14 +15,15 @@ import axios from "axios"; import camelCase from "camelcase"; import semver from "semver"; -import { downloadAndExtractArtifact, getHostZigName, getVersion, getZigPath, handleConfigOption } from "./zigUtil"; -import { existsSync } from "fs"; +import * as versionManager from "./versionManager"; +import { getHostZigName, getVersion, getZigPath, handleConfigOption } from "./zigUtil"; const ZIG_MODE: DocumentSelector = [ { language: "zig", scheme: "file" }, { language: "zig", scheme: "untitled" }, ]; +let versionManagerConfig: versionManager.Config; let outputChannel: vscode.OutputChannel; export let client: LanguageClient | null = null; @@ -104,25 +105,12 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri const result = await fetchVersion(context, zigVersion, true); if (!result) return null; - const isWindows = process.platform === "win32"; - const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zls", result.version.raw); - zlsExePath = vscode.Uri.joinPath(installDir, isWindows ? "zls.exe" : "zls").fsPath; - zlsVersion = result.version; - - if (!existsSync(zlsExePath)) { - try { - await downloadAndExtractArtifact( - "ZLS", - "zls", - installDir, - result.artifact.tarball, - result.artifact.shasum, - [], - ); - } catch { - void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); - return null; - } + try { + zlsExePath = await versionManager.install(versionManagerConfig, result.version); + zlsVersion = result.version; + } catch { + void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); + return null; } } @@ -367,6 +355,18 @@ export async function activate(context: vscode.ExtensionContext) { } } + versionManagerConfig = { + context: context, + title: "ZLS", + exeName: "zls", + extraTarArgs: [], + versionArg: "--version", + canonicalUrl: { + release: vscode.Uri.parse("https://builds.zigtools.org"), + nightly: vscode.Uri.parse("https://builds.zigtools.org"), + }, + }; + outputChannel = vscode.window.createOutputChannel("Zig Language Server"); context.subscriptions.push( From 8a5edb65660e95b848cbaa613774c43d07fab5db Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 20 Sep 2024 21:24:35 +0200 Subject: [PATCH 03/25] refactor: rename ZigCompilerProvider to ZigDiagnosticsProvider --- src/extension.ts | 4 ++-- src/{zigCompilerProvider.ts => zigDiagnosticsProvider.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/{zigCompilerProvider.ts => zigDiagnosticsProvider.ts} (99%) diff --git a/src/extension.ts b/src/extension.ts index 07667ed..88e496f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,7 @@ import vscode from "vscode"; import { activate as activateZls, deactivate as deactivateZls } from "./zls"; -import ZigCompilerProvider from "./zigCompilerProvider"; +import ZigDiagnosticsProvider from "./zigDiagnosticsProvider"; import ZigMainCodeLensProvider from "./zigMainCodeLens"; import ZigTestRunnerProvider from "./zigTestRunnerProvider"; import { registerDocumentFormatting } from "./zigFormat"; @@ -9,7 +9,7 @@ import { setupZig } from "./zigSetup"; export async function activate(context: vscode.ExtensionContext) { await setupZig(context).finally(() => { - const compiler = new ZigCompilerProvider(); + const compiler = new ZigDiagnosticsProvider(); compiler.activate(context.subscriptions); context.subscriptions.push(registerDocumentFormatting()); diff --git a/src/zigCompilerProvider.ts b/src/zigDiagnosticsProvider.ts similarity index 99% rename from src/zigCompilerProvider.ts rename to src/zigDiagnosticsProvider.ts index 7d06633..d4ca3f9 100644 --- a/src/zigCompilerProvider.ts +++ b/src/zigDiagnosticsProvider.ts @@ -9,7 +9,7 @@ import { DebouncedFunc, throttle } from "lodash-es"; import * as zls from "./zls"; import { getZigPath, handleConfigOption } from "./zigUtil"; -export default class ZigCompilerProvider { +export default class ZigDiagnosticsProvider { private buildDiagnostics!: vscode.DiagnosticCollection; private astDiagnostics!: vscode.DiagnosticCollection; private dirtyChange = new WeakMap(); From a7f963f2c075003649dc3cc362055093f7afca5f Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:39:32 +0100 Subject: [PATCH 04/25] refactor: move version index download into zigUtil.ts --- src/zigSetup.ts | 16 ++-------------- src/zigUtil.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 07cba1d..5730102 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -5,29 +5,17 @@ import semver from "semver"; import vscode from "vscode"; import * as versionManager from "./versionManager"; -import { getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; +import { VersionIndex, ZigVersion, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; import { restartClient } from "./zls"; let versionManagerConfig: versionManager.Config; -const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json"; - function getNightlySemVer(url: string): string { const matches = url.match(/-(\d+\.\d+\.\d+(-dev\.\d+\+\w+)?)\./); if (!matches) throw new Error(`url '${url}' does not contain a semantic version!`); return matches[1]; } -type VersionIndex = Record>; - -interface ZigVersion { - name: string; - url: string; - sha: string; - notes?: string; - version: semver.SemVer; -} - export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { const zigPath = await versionManager.install(versionManagerConfig, version); @@ -42,8 +30,8 @@ export async function installZig(context: vscode.ExtensionContext, version: semv } async function getVersions(): Promise { + const indexJson = (await axios.get("https://ziglang.org/download/index.json", {})).data; const hostName = getHostZigName(); - const indexJson = (await axios.get(DOWNLOAD_INDEX, {})).data; const result: ZigVersion[] = []; for (let key in indexJson) { const value = indexJson[key]; diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 49ed7df..c555587 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -143,6 +143,19 @@ export function getVersion(filePath: string, arg: string): semver.SemVer | null } } +export interface ZigVersion { + name: string; + version: semver.SemVer; + url: string; + sha: string; + notes?: string; +} + +export type VersionIndex = Record< + string, + Record +>; + export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); if (!workspaceFolder && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { From f1a2e83517548acd07e64a3f086d45a8a0321214 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 04:23:04 +0100 Subject: [PATCH 05/25] remove the Zig update feature The manual update command is already covered by the 'Select and Install' popup. --- package.json | 11 -------- src/zigSetup.ts | 68 +------------------------------------------------ 2 files changed, 1 insertion(+), 78 deletions(-) diff --git a/package.json b/package.json index 603cffa..19989a6 100644 --- a/package.json +++ b/package.json @@ -107,12 +107,6 @@ "type": "string", "description": "Set a custom path to the Zig binary. The string \"zig\" means lookup zig in PATH." }, - "zig.checkForUpdate": { - "scope": "resource", - "type": "boolean", - "description": "Whether to automatically check for new updates", - "default": true - }, "zig.formattingProvider": { "scope": "resource", "type": "string", @@ -340,11 +334,6 @@ "title": "Install Zig", "category": "Zig Setup" }, - { - "command": "zig.update", - "title": "Check for Zig Updates", - "category": "Zig Setup" - }, { "command": "zig.zls.enable", "title": "Enable Language Server", diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 5730102..793a095 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -5,17 +5,11 @@ import semver from "semver"; import vscode from "vscode"; import * as versionManager from "./versionManager"; -import { VersionIndex, ZigVersion, getHostZigName, getVersion, getZigPath, shouldCheckUpdate } from "./zigUtil"; +import { VersionIndex, ZigVersion, getHostZigName, getVersion, getZigPath } from "./zigUtil"; import { restartClient } from "./zls"; let versionManagerConfig: versionManager.Config; -function getNightlySemVer(url: string): string { - const matches = url.match(/-(\d+\.\d+\.\d+(-dev\.\d+\+\w+)?)\./); - if (!matches) throw new Error(`url '${url}' does not contain a semantic version!`); - return matches[1]; -} - export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { const zigPath = await versionManager.install(versionManagerConfig, version); @@ -92,59 +86,6 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { } } -async function checkUpdate(context: vscode.ExtensionContext) { - try { - const update = await getUpdatedVersion(context); - if (!update) return; - - const notes = update.notes ? ` [${update.notes}](${update.notes})` : ""; - - const response = await vscode.window.showInformationMessage( - `New version of Zig available: ${update.name}${notes}`, - "Install", - "Ignore", - ); - switch (response) { - case "Install": - await installZig(context, update.version); - break; - case "Ignore": - case undefined: - break; - } - } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage(`Unable to update Zig: ${err.message}`); - } else { - throw err; - } - } -} - -async function getUpdatedVersion(context: vscode.ExtensionContext): Promise { - const configuration = vscode.workspace.getConfiguration("zig"); - const zigPath = configuration.get("path"); - const zigBinPath = vscode.Uri.joinPath(context.globalStorageUri, "zig_install", "zig").fsPath; - if (!zigPath?.startsWith(zigBinPath)) return null; - - const curVersion = getVersion(zigPath, "version"); - if (!curVersion) return null; - - const available = await getVersions(); - if (curVersion.prerelease.length !== 0) { - if (available[0].name === "nightly") { - const newVersion = getNightlySemVer(available[0].url); - if (semver.gt(newVersion, curVersion)) { - available[0].name = `nightly-${newVersion}`; - return available[0]; - } - } - } else if (available.length > 2 && semver.gt(available[1].name, curVersion)) { - return available[1]; - } - return null; -} - function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext) { try { const zigPath = getZigPath(); @@ -190,9 +131,6 @@ export async function setupZig(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zig.install", async () => { await selectVersionAndInstall(context); }), - vscode.commands.registerCommand("zig.update", async () => { - await checkUpdate(context); - }), vscode.workspace.onDidChangeConfiguration((change) => { if (change.affectsConfiguration("zig.path")) { updateZigEnvironmentVariableCollection(context); @@ -204,10 +142,6 @@ export async function setupZig(context: vscode.ExtensionContext) { if (!configuration.get("initialSetupDone")) { await configuration.update("initialSetupDone", await initialSetup(context), true); } - - if (!configuration.get("checkForUpdate")) return; - if (!(await shouldCheckUpdate(context, "zigUpdate"))) return; - await checkUpdate(context); } async function initialSetup(context: vscode.ExtensionContext): Promise { From 955eb40e3aa0829b9f86447e43f132e8139c3557 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:38:37 +0100 Subject: [PATCH 06/25] add zigProvider.ts --- src/zigDiagnosticsProvider.ts | 9 +++-- src/zigFormat.ts | 55 ++++++++++++++--------------- src/zigMainCodeLens.ts | 22 +++++++----- src/zigProvider.ts | 66 +++++++++++++++++++++++++++++++++++ src/zigSetup.ts | 25 +++++++------ src/zigTestRunnerProvider.ts | 16 ++++++--- src/zigUtil.ts | 57 +++++++++++++++++++----------- src/zls.ts | 27 ++++++-------- 8 files changed, 185 insertions(+), 92 deletions(-) create mode 100644 src/zigProvider.ts diff --git a/src/zigDiagnosticsProvider.ts b/src/zigDiagnosticsProvider.ts index d4ca3f9..9c4363f 100644 --- a/src/zigDiagnosticsProvider.ts +++ b/src/zigDiagnosticsProvider.ts @@ -7,7 +7,8 @@ import path from "path"; import { DebouncedFunc, throttle } from "lodash-es"; import * as zls from "./zls"; -import { getZigPath, handleConfigOption } from "./zigUtil"; +import { handleConfigOption } from "./zigUtil"; +import { zigProvider } from "./zigSetup"; export default class ZigDiagnosticsProvider { private buildDiagnostics!: vscode.DiagnosticCollection; @@ -89,7 +90,8 @@ export default class ZigDiagnosticsProvider { if (textDocument.languageId !== "zig") { return; } - const zigPath = getZigPath(); + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; const { error, stderr } = childProcess.spawnSync(zigPath, ["ast-check"], { input: textDocument.getText(), maxBuffer: 10 * 1024 * 1024, // 10MB @@ -134,7 +136,8 @@ export default class ZigDiagnosticsProvider { private _doCompile(textDocument: vscode.TextDocument) { const config = vscode.workspace.getConfiguration("zig"); - const zigPath = getZigPath(); + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; const buildOption = config.get("buildOption", "build"); const processArg: string[] = [buildOption]; diff --git a/src/zigFormat.ts b/src/zigFormat.ts index e1aab19..72858ee 100644 --- a/src/zigFormat.ts +++ b/src/zigFormat.ts @@ -6,43 +6,45 @@ import util from "util"; import { DocumentRangeFormattingRequest, TextDocumentIdentifier } from "vscode-languageclient"; import * as zls from "./zls"; -import { getZigPath } from "./zigUtil"; +import { zigProvider } from "./zigSetup"; const execFile = util.promisify(childProcess.execFile); const ZIG_MODE: vscode.DocumentSelector = { language: "zig" }; export function registerDocumentFormatting(): vscode.Disposable { + const disposables: vscode.Disposable[] = []; let registeredFormatter: vscode.Disposable | null = null; preCompileZigFmt(); - vscode.workspace.onDidChangeConfiguration((change: vscode.ConfigurationChangeEvent) => { - if ( - change.affectsConfiguration("zig.path", undefined) || - change.affectsConfiguration("zig.formattingProvider", undefined) - ) { + zigProvider.onChange.event(() => { + preCompileZigFmt(); + }, disposables); + + const onformattingProviderChange = (change: vscode.ConfigurationChangeEvent | null) => { + if (!change || change.affectsConfiguration("zig.formattingProvider", undefined)) { preCompileZigFmt(); - } - }); - const onformattingProviderChange = () => { - if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "off") { - // Unregister the formatting provider - if (registeredFormatter !== null) registeredFormatter.dispose(); - registeredFormatter = null; - } else { - // register the formatting provider - registeredFormatter ??= vscode.languages.registerDocumentRangeFormattingEditProvider(ZIG_MODE, { - provideDocumentRangeFormattingEdits, - }); + if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "off") { + // Unregister the formatting provider + if (registeredFormatter !== null) registeredFormatter.dispose(); + registeredFormatter = null; + } else { + // register the formatting provider + registeredFormatter ??= vscode.languages.registerDocumentRangeFormattingEditProvider(ZIG_MODE, { + provideDocumentRangeFormattingEdits, + }); + } } }; - onformattingProviderChange(); - const registeredDidChangeEvent = vscode.workspace.onDidChangeConfiguration(onformattingProviderChange); + onformattingProviderChange(null); + vscode.workspace.onDidChangeConfiguration(onformattingProviderChange, disposables); return { dispose: () => { - registeredDidChangeEvent.dispose(); + for (const disposable of disposables) { + disposable.dispose(); + } if (registeredFormatter !== null) registeredFormatter.dispose(); }, }; @@ -53,12 +55,8 @@ function preCompileZigFmt() { // This pre-compiles even if "zig.formattingProvider" is "zls". if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "off") return; - let zigPath: string; - try { - zigPath = getZigPath(); - } catch { - return; - } + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; try { childProcess.execFile(zigPath, ["fmt", "--help"], { @@ -93,7 +91,8 @@ async function provideDocumentRangeFormattingEdits( } } - const zigPath = getZigPath(); + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; const abortController = new AbortController(); token.onCancellationRequested(() => { diff --git a/src/zigMainCodeLens.ts b/src/zigMainCodeLens.ts index 0aba94f..e82b819 100644 --- a/src/zigMainCodeLens.ts +++ b/src/zigMainCodeLens.ts @@ -5,7 +5,8 @@ import fs from "fs"; import path from "path"; import util from "util"; -import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; +import { getWorkspaceFolder, isWorkspaceFile } from "./zigUtil"; +import { zigProvider } from "./zigSetup"; const execFile = util.promisify(childProcess.execFile); @@ -39,15 +40,17 @@ export default class ZigMainCodeLensProvider implements vscode.CodeLensProvider function zigRun() { if (!vscode.window.activeTextEditor) return; + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return; const filePath = vscode.window.activeTextEditor.document.uri.fsPath; const terminal = vscode.window.createTerminal("Run Zig Program"); terminal.show(); const wsFolder = getWorkspaceFolder(filePath); if (wsFolder && isWorkspaceFile(filePath) && hasBuildFile(wsFolder.uri.fsPath)) { - terminal.sendText(`${getZigPath()} build run`); + terminal.sendText(`${zigPath} build run`); return; } - terminal.sendText(`${getZigPath()} run "${filePath}"`); + terminal.sendText(`${zigPath} run "${filePath}"`); } function hasBuildFile(workspaceFspath: string): boolean { @@ -60,12 +63,13 @@ async function zigDebug() { const filePath = vscode.window.activeTextEditor.document.uri.fsPath; try { const workspaceFolder = getWorkspaceFolder(filePath); - let binaryPath = ""; + let binaryPath; if (workspaceFolder && isWorkspaceFile(filePath) && hasBuildFile(workspaceFolder.uri.fsPath)) { binaryPath = await buildDebugBinaryWithBuildFile(workspaceFolder.uri.fsPath); } else { binaryPath = await buildDebugBinary(filePath); } + if (!binaryPath) return; const debugConfig: vscode.DebugConfiguration = { type: "lldb", @@ -81,11 +85,12 @@ async function zigDebug() { } } -async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { +async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; // Workaround because zig build doesn't support specifying the output binary name // `zig run` does support -femit-bin, but preferring `zig build` if possible const outputDir = path.join(workspacePath, "zig-out", "tmp-debug-build"); - const zigPath = getZigPath(); await execFile(zigPath, ["build", "--prefix", outputDir], { cwd: workspacePath }); const dirFiles = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path.join(outputDir, "bin"))); const files = dirFiles.find(([, type]) => type === vscode.FileType.File); @@ -95,8 +100,9 @@ async function buildDebugBinaryWithBuildFile(workspacePath: string): Promise { - const zigPath = getZigPath(); +async function buildDebugBinary(filePath: string): Promise { + const zigPath = zigProvider.getZigPath(); + if (!zigPath) return null; const fileDirectory = path.dirname(filePath); const binaryName = `debug-${path.basename(filePath, ".zig")}`; const binaryPath = path.join(fileDirectory, "zig-out", "bin", binaryName); diff --git a/src/zigProvider.ts b/src/zigProvider.ts new file mode 100644 index 0000000..3f05c2a --- /dev/null +++ b/src/zigProvider.ts @@ -0,0 +1,66 @@ +import vscode from "vscode"; + +import semver from "semver"; + +import { resolveExePathAndVersion } from "./zigUtil"; + +interface ExeWithVersion { + exe: string; + version: semver.SemVer; +} + +export class ZigProvider implements vscode.Disposable { + onChange: vscode.EventEmitter = new vscode.EventEmitter(); + private value: ExeWithVersion | null; + private disposables: vscode.Disposable[]; + + constructor() { + this.value = this.resolveZigPathConfigOption(); + this.disposables = [ + vscode.workspace.onDidChangeConfiguration((change) => { + if (change.affectsConfiguration("zig.path")) { + const newValue = this.resolveZigPathConfigOption(); + if (newValue) { + this.value = newValue; + this.set(this.value); + } + } + }), + ]; + } + + /** Returns the version of the Zig executable that is currently being used. */ + public getZigVersion(): semver.SemVer | null { + return this.value?.version ?? null; + } + + /** Returns the path to the Zig executable that is currently being used. */ + public getZigPath(): string | null { + return this.value?.exe ?? null; + } + + /** Override which zig executable should be used. The `zig.path` config option will be ignored */ + public set(value: ExeWithVersion | null) { + this.value = value; + this.onChange.fire(value); + } + + /** Resolves the `zig.path` configuration option */ + private resolveZigPathConfigOption(): ExeWithVersion | null { + const zigPath = vscode.workspace.getConfiguration("zig").get("path", ""); + if (!zigPath) return null; + const exePath = zigPath !== "zig" ? zigPath : null; // the string "zig" means lookup in PATH + const result = resolveExePathAndVersion(exePath, "zig", "zig.path", "version"); + if ("message" in result) { + void vscode.window.showErrorMessage(`'zig.path' is not valid: ${result.message}`); + return null; + } + return result; + } + + dispose() { + for (const disposable of this.disposables) { + disposable.dispose(); + } + } +} diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 793a095..6fba705 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -5,10 +5,12 @@ import semver from "semver"; import vscode from "vscode"; import * as versionManager from "./versionManager"; -import { VersionIndex, ZigVersion, getHostZigName, getVersion, getZigPath } from "./zigUtil"; +import { VersionIndex, ZigVersion, getHostZigName, getVersion } from "./zigUtil"; +import { ZigProvider } from "./zigProvider"; import { restartClient } from "./zls"; let versionManagerConfig: versionManager.Config; +export let zigProvider: ZigProvider; export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { const zigPath = await versionManager.install(versionManagerConfig, version); @@ -86,15 +88,14 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { } } -function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext) { - try { - const zigPath = getZigPath(); - const envValue = path.delimiter + path.dirname(zigPath); +function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) { + if (zigExePath) { + const envValue = path.delimiter + path.dirname(zigExePath); // Calling `append` means that zig from a user-defined PATH value will take precedence. // The added value may have already been added by the user but since we // append, it doesn't have any observable. context.environmentVariableCollection.append("PATH", envValue); - } catch { + } else { context.environmentVariableCollection.delete("PATH"); } } @@ -124,17 +125,19 @@ export async function setupZig(context: vscode.ExtensionContext) { }, }; + zigProvider = new ZigProvider(); + context.environmentVariableCollection.description = "Add Zig to PATH"; - updateZigEnvironmentVariableCollection(context); context.subscriptions.push( + zigProvider, vscode.commands.registerCommand("zig.install", async () => { await selectVersionAndInstall(context); }), - vscode.workspace.onDidChangeConfiguration((change) => { - if (change.affectsConfiguration("zig.path")) { - updateZigEnvironmentVariableCollection(context); - } + zigProvider.onChange.event((result) => { + const { exe } = result ?? { exe: null, version: null }; + + updateZigEnvironmentVariableCollection(context, exe); }), ); diff --git a/src/zigTestRunnerProvider.ts b/src/zigTestRunnerProvider.ts index ef0eae7..2c0af84 100644 --- a/src/zigTestRunnerProvider.ts +++ b/src/zigTestRunnerProvider.ts @@ -6,7 +6,8 @@ import util from "util"; import { DebouncedFunc, throttle } from "lodash-es"; -import { getWorkspaceFolder, getZigPath, isWorkspaceFile } from "./zigUtil"; +import { getWorkspaceFolder, isWorkspaceFile } from "./zigUtil"; +import { zigProvider } from "./zigSetup"; const execFile = util.promisify(childProcess.execFile); @@ -85,7 +86,7 @@ export default class ZigTestRunnerProvider { this.deleteTestForAFile(textDocument.uri); for (const match of matches) { - const testDesc = match[1] || match[2] || match [3]; + const testDesc = match[1] || match[2] || match[3]; const isDocTest = !match[1]; const position = textDocument.positionAt(match.index); const range = new vscode.Range(position, position.translate(0, match[0].length)); @@ -128,7 +129,10 @@ export default class ZigTestRunnerProvider { } private async runTest(test: vscode.TestItem): Promise<{ output: string; success: boolean }> { - const zigPath = getZigPath(); + const zigPath = zigProvider.getZigPath(); + if (!zigPath) { + return { output: "Unable to run test without Zig", success: false }; + } if (test.uri === undefined) { return { output: "Unable to determine file location", success: false }; } @@ -176,13 +180,17 @@ export default class ZigTestRunnerProvider { } private async buildTestBinary(run: vscode.TestRun, testFilePath: string, testDesc: string): Promise { + const zigPath = zigProvider.getZigPath(); + if (!zigPath) { + throw new Error("Unable to build test binary without Zig"); + } + const wsFolder = getWorkspaceFolder(testFilePath)?.uri.fsPath ?? path.dirname(testFilePath); const outputDir = path.join(wsFolder, "zig-out", "tmp-debug-build", "bin"); const binaryName = `test-${path.basename(testFilePath, ".zig")}`; const binaryPath = path.join(outputDir, binaryName); await vscode.workspace.fs.createDirectory(vscode.Uri.file(outputDir)); - const zigPath = getZigPath(); const { stdout, stderr } = await execFile(zigPath, [ "test", testFilePath, diff --git a/src/zigUtil.ts b/src/zigUtil.ts index c555587..15a7f9b 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -47,7 +47,20 @@ export function handleConfigOption(input: string): string { return input; } -export function getExePath(exePath: string | null | undefined, exeName: string, optionName: string): string { +/** Resolves the absolute executable path and version of a program like Zig or ZLS. */ +export function resolveExePathAndVersion( + /** `null` means lookup in PATH */ + exePath: string | null, + /** e.g. `zig` or `zig` */ + exeName: string, + /** e.g. `zig.path` or `zig.zls.path` */ + optionName: string, + /** + * The command-line argument that is used to query the version of the executable. + * Zig uses `version`. ZLS uses `--version`. + */ + versionArg: string, +): { exe: string; version: semver.SemVer } | { message: string } { if (!exePath) { exePath = which.sync(exeName, { nothrow: true }); } else { @@ -61,28 +74,23 @@ export function getExePath(exePath: string | null | undefined, exeName: string, } } - let message; if (!exePath) { - message = `Could not find ${exeName} in PATH`; - } else if (!fs.existsSync(exePath)) { - message = `\`${optionName}\` ${exePath} does not exist`; - } else { - try { - fs.accessSync(exePath, fs.constants.R_OK | fs.constants.X_OK); - return exePath; - } catch { - message = `\`${optionName}\` ${exePath} is not an executable`; - } + return { message: `Could not find ${exeName} in PATH` }; + } + + if (!fs.existsSync(exePath)) { + return { message: `\`${optionName}\` ${exePath} does not exist` }; + } + + try { + fs.accessSync(exePath, fs.constants.R_OK | fs.constants.X_OK); + } catch { + return { message: `\`${optionName}\` ${exePath} is not an executable` }; } - void vscode.window.showErrorMessage(message); - throw Error(message); -} -export function getZigPath(): string { - const configuration = vscode.workspace.getConfiguration("zig"); - const zigPath = configuration.get("path"); - const exePath = zigPath !== "zig" ? zigPath : null; // the string "zig" means lookup in PATH - return getExePath(exePath, "zig", "zig.path"); + const version = getVersion(exePath, versionArg); + if (!version) return { message: `Failed to run '${exePath} ${versionArg}'!` }; + return { exe: exePath, version: version }; } // Check timestamp `key` to avoid automatically checking for updates @@ -129,7 +137,14 @@ export function getHostZigName(): string { return `${getZigArchName()}-${getZigOSName()}`; } -export function getVersion(filePath: string, arg: string): semver.SemVer | null { +export function getVersion( + filePath: string, + /** + * The command-line argument that is used to query the version of the executable. + * Zig uses `version`. ZLS uses `--version`. + */ + arg: string, +): semver.SemVer | null { try { const buffer = childProcess.execFileSync(filePath, [arg]); const versionString = buffer.toString("utf8").trim(); diff --git a/src/zls.ts b/src/zls.ts index 539b6a4..859d530 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -16,7 +16,8 @@ import camelCase from "camelcase"; import semver from "semver"; import * as versionManager from "./versionManager"; -import { getHostZigName, getVersion, getZigPath, handleConfigOption } from "./zigUtil"; +import { getHostZigName, getVersion, handleConfigOption } from "./zigUtil"; +import { zigProvider } from "./zigSetup"; const ZIG_MODE: DocumentSelector = [ { language: "zig", scheme: "file" }, @@ -94,12 +95,7 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri if (!zlsExePath) { if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; - let zigVersion: semver.SemVer | null; - try { - zigVersion = getVersion(getZigPath(), "version"); - } catch { - return null; - } + const zigVersion = zigProvider.getZigVersion(); if (!zigVersion) return null; const result = await fetchVersion(context, zigVersion, true); @@ -169,13 +165,7 @@ async function configurationMiddleware( const indexOfZigPath = optionIndices["zig.path"]; if (indexOfZigPath !== undefined) { - try { - result[indexOfZigPath] = getZigPath(); - } catch { - // ZLS will try to find Zig by itself and likely fail as well. - // This will cause two "Zig can't be found in $PATH" error messages to be reported. - result[indexOfZigPath] = null; - } + result[indexOfZigPath] = zigProvider.getZigPath(); } const additionalOptions = configuration.get>("additionalOptions", {}); @@ -330,10 +320,10 @@ async function isEnabled(): Promise { ); switch (response) { case "Yes": - await zlsConfig.update("enabled", "on"); + await zlsConfig.update("enabled", "on", true); return true; case "No": - await zlsConfig.update("enabled", "off"); + await zlsConfig.update("enabled", "off", true); return false; case undefined: return false; @@ -387,8 +377,8 @@ export async function activate(context: vscode.ExtensionContext) { outputChannel.show(); }), vscode.workspace.onDidChangeConfiguration(async (change) => { + // The `zig.path` config option is handled by `zigProvider.onChange`. if ( - change.affectsConfiguration("zig.path", undefined) || change.affectsConfiguration("zig.zls.enabled", undefined) || change.affectsConfiguration("zig.zls.path", undefined) || change.affectsConfiguration("zig.zls.debugLog", undefined) @@ -396,6 +386,9 @@ export async function activate(context: vscode.ExtensionContext) { await restartClient(context); } }), + zigProvider.onChange.event(async () => { + await restartClient(context); + }), ); if (await isEnabled()) { From 896fc2bd954a1fc61917d2a4d44fe0593bc02a3e Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:41:20 +0100 Subject: [PATCH 07/25] remove the initial setup with automatic Zig version management This commit replaces the initial setup with the following mechanism: 1. Check if the "Install Zig" has been previously executed in the active workspace. If so, install that version. 2. If the workspace contains a `.zigversion`, install the given Zig version. 3. If the workspace contains a `build.zig.zon` with a `minimum_zig_version`, install the next available Zig version. 4. Otherwise fallback to the latest tagged release of Zig. Some parts of this are not fully implemented. fixes #111 --- package.json | 5 -- src/zigSetup.ts | 184 ++++++++++++++++++++++++++++++++---------------- src/zls.ts | 85 +++++++++++++--------- 3 files changed, 173 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 19989a6..de4a44b 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,6 @@ "type": "object", "title": "Zig", "properties": { - "zig.initialSetupDone": { - "type": "boolean", - "default": false, - "description": "Has the initial setup been done yet?" - }, "zig.buildOnSave": { "type": "boolean", "default": false, diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 6fba705..dbdb60d 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -1,28 +1,43 @@ +import vscode from "vscode"; + import path from "path"; import axios from "axios"; import semver from "semver"; -import vscode from "vscode"; import * as versionManager from "./versionManager"; -import { VersionIndex, ZigVersion, getHostZigName, getVersion } from "./zigUtil"; +import { VersionIndex, ZigVersion, getHostZigName } from "./zigUtil"; import { ZigProvider } from "./zigProvider"; -import { restartClient } from "./zls"; let versionManagerConfig: versionManager.Config; export let zigProvider: ZigProvider; -export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { - const zigPath = await versionManager.install(versionManagerConfig, version); - - const configuration = vscode.workspace.getConfiguration("zig"); - await configuration.update("path", zigPath, true); - - void vscode.window.showInformationMessage( - `Zig has been installed successfully. Relaunch your integrated terminal to make it available.`, +/** Removes the `zig.path` config option. */ +async function installZig(context: vscode.ExtensionContext) { + const wantedZig = await getWantedZigVersion( + context, + Object.values(WantedZigVersionSource) as WantedZigVersionSource[], ); + if (!wantedZig) { + await vscode.workspace.getConfiguration("zig").update("path", undefined, true); + zigProvider.set(null); + return; + } - void restartClient(context); + try { + const exePath = await versionManager.install(versionManagerConfig, wantedZig.version); + await vscode.workspace.getConfiguration("zig").update("path", undefined, true); + zigProvider.set({ exe: exePath, version: wantedZig.version }); + } catch (err) { + zigProvider.set(null); + if (err instanceof Error) { + void vscode.window.showErrorMessage( + `Failed to install Zig ${wantedZig.version.toString()}: ${err.message}`, + ); + } else { + void vscode.window.showErrorMessage(`Failed to install Zig ${wantedZig.version.toString()}!`); + } + } } async function getVersions(): Promise { @@ -75,7 +90,12 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { if (selection === undefined) return; for (const option of available) { if (option.name === selection.label) { - await installZig(context, option.version); + await context.workspaceState.update("zig-version", option.version.raw); + await installZig(context); + + void vscode.window.showInformationMessage( + `Zig ${option.version.toString()} has been installed successfully. Relaunch your integrated terminal to make it available.`, + ); return; } } @@ -88,6 +108,89 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { } } +/** The order of these enums defines the default order in which these sources are executed. */ +enum WantedZigVersionSource { + workspaceState = "workspace-state", + /** `.zigversion` */ + workspaceZigVersionFile = ".zigversion", + /** The `minimum_zig_version` in `build.zig.zon` */ + workspaceBuildZigZon = "build.zig.zon", + latestTagged = "latest-tagged", +} + +/** Try to resolve the (workspace-specific) Zig version. */ +async function getWantedZigVersion( + context: vscode.ExtensionContext, + /** List of "sources" that should are applied in the given order to resolve the wanted Zig version */ + sources: WantedZigVersionSource[], +): Promise<{ + version: semver.SemVer; + source: WantedZigVersionSource; +} | null> { + let workspace: vscode.WorkspaceFolder | null = null; + // Supporting multiple workspaces is significantly more complex so we just look for the first workspace. + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + workspace = vscode.workspace.workspaceFolders[0]; + } + + for (const source of sources) { + let result: semver.SemVer | null = null; + + try { + switch (source) { + case WantedZigVersionSource.workspaceState: + // `context.workspaceState` appears to behave like `context.globalState` when outside of a workspace + // There is currently no way to remove the specified zig version. + const wantedZigVersion = context.workspaceState.get("zig-version"); + result = wantedZigVersion ? new semver.SemVer(wantedZigVersion) : null; + break; + case WantedZigVersionSource.workspaceZigVersionFile: + if (workspace) { + const zigVersionString = await vscode.workspace.fs.readFile( + vscode.Uri.joinPath(workspace.uri, ".zigversion"), + ); + result = semver.parse(zigVersionString.toString().trim()); + } + break; + case WantedZigVersionSource.workspaceBuildZigZon: + if (workspace) { + const manifest = await vscode.workspace.fs.readFile( + vscode.Uri.joinPath(workspace.uri, "build.zig.zon"), + ); + // Not perfect, but good enough + const matches = /\n\s*\.minimum_zig_version\s=\s\"(.*)\"/.exec(manifest.toString()); + if (matches) { + result = semver.parse(matches[1]); + } + } + break; + case WantedZigVersionSource.latestTagged: + const cacheKey = "zig-latest-tagged"; + try { + const zigVersion = await getVersions(); + const latestTagged = zigVersion.find((item) => item.version.prerelease.length === 0); + result = latestTagged?.version ?? null; + await context.globalState.update(cacheKey, latestTagged?.version.raw); + } catch { + const latestTagged = context.globalState.get(cacheKey, null); + if (latestTagged) { + result = new semver.SemVer(latestTagged); + } + } + break; + } + } catch {} + + if (!result) continue; + + return { + version: result, + source: source, + }; + } + return null; +} + function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) { if (zigExePath) { const envValue = path.delimiter + path.dirname(zigExePath); @@ -102,14 +205,13 @@ function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext export async function setupZig(context: vscode.ExtensionContext) { { - // convert an empty string for `zig.path` to `zig`. // This check can be removed once enough time has passed so that most users switched to the new value + // remove a `zig.path` that points to the global storage. const zigConfig = vscode.workspace.getConfiguration("zig"); - const initialSetupDone = zigConfig.get("initialSetupDone", false); - const zigPath = zigConfig.get("path"); - if (zigPath === "" && initialSetupDone) { - await zigConfig.update("path", "zig", true); + const zigPath = zigConfig.get("path", ""); + if (zigPath.startsWith(context.globalStorageUri.fsPath)) { + await zigConfig.update("path", undefined, true); } } @@ -141,49 +243,7 @@ export async function setupZig(context: vscode.ExtensionContext) { }), ); - const configuration = vscode.workspace.getConfiguration("zig"); - if (!configuration.get("initialSetupDone")) { - await configuration.update("initialSetupDone", await initialSetup(context), true); + if (!vscode.workspace.getConfiguration("zig").get("path")) { + await installZig(context); } } - -async function initialSetup(context: vscode.ExtensionContext): Promise { - const zigConfig = vscode.workspace.getConfiguration("zig"); - if (!!zigConfig.get("path")) return true; - - const zigResponse = await vscode.window.showInformationMessage( - "Zig path hasn't been set, do you want to specify the path or install Zig?", - { modal: true }, - "Install", - "Specify path", - "Use Zig in PATH", - ); - switch (zigResponse) { - case "Install": - await selectVersionAndInstall(context); - const zigPath = vscode.workspace.getConfiguration("zig").get("path"); - if (!zigPath) return false; - break; - case "Specify path": - const uris = await vscode.window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig executable", - }); - if (!uris) return false; - - const version = getVersion(uris[0].path, "version"); - if (!version) return false; - - await zigConfig.update("path", uris[0].path, true); - break; - case "Use Zig in PATH": - await zigConfig.update("path", "zig", true); - break; - case undefined: - return false; - } - - return true; -} diff --git a/src/zls.ts b/src/zls.ts index 859d530..3b74921 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -16,7 +16,7 @@ import camelCase from "camelcase"; import semver from "semver"; import * as versionManager from "./versionManager"; -import { getHostZigName, getVersion, handleConfigOption } from "./zigUtil"; +import { getHostZigName, getVersion, handleConfigOption, resolveExePathAndVersion } from "./zigUtil"; import { zigProvider } from "./zigSetup"; const ZIG_MODE: DocumentSelector = [ @@ -29,12 +29,6 @@ let outputChannel: vscode.OutputChannel; export let client: LanguageClient | null = null; export async function restartClient(context: vscode.ExtensionContext): Promise { - const configuration = vscode.workspace.getConfiguration("zig.zls"); - if (!configuration.get("path") && configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") { - await stopClient(); - return; - } - const result = await getZLSPath(context); if (!result) return; @@ -92,40 +86,59 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri let zlsExePath = configuration.get("path"); let zlsVersion: semver.SemVer | null = null; - if (!zlsExePath) { - if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; + if (!!zlsExePath) { + // This will fail on older ZLS version that do not support `zls --version`. + // It should be more likely that the given executable is invalid than someone using ZLS 0.9.0 or older. + const result = resolveExePathAndVersion(zlsExePath, "zls", "zig.zls.path", "--version"); + if ("message" in result) { + void vscode.window.showErrorMessage(result.message); + return null; + } + return result; + } + + if (configuration.get<"ask" | "off" | "on">("enabled", "ask") !== "on") return null; - const zigVersion = zigProvider.getZigVersion(); - if (!zigVersion) return null; + const zigVersion = zigProvider.getZigVersion(); + if (!zigVersion) return null; - const result = await fetchVersion(context, zigVersion, true); - if (!result) return null; + const result = await fetchVersion(context, zigVersion, true); + if (!result) return null; - try { - zlsExePath = await versionManager.install(versionManagerConfig, result.version); - zlsVersion = result.version; - } catch { + try { + zlsExePath = await versionManager.install(versionManagerConfig, result.version); + zlsVersion = result.version; + } catch (err) { + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}: ${err.message}`); + } else { void vscode.window.showErrorMessage(`Failed to install ZLS ${result.version.toString()}!`); - return null; } - } - - const checkedZLSVersion = getVersion(zlsExePath, "--version"); - if (!checkedZLSVersion) { - void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`); return null; } - if (zlsVersion && checkedZLSVersion.compare(zlsVersion) !== 0) { - // The Matrix is broken! - void vscode.window.showErrorMessage( - `Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`, - ); - return null; + + /** `--version` has been added in https://github.com/zigtools/zls/pull/583 */ + const zlsVersionArgAdded = new semver.SemVer("0.10.0-dev.150+cb5eeb0b4"); + + if (semver.gte(zlsVersion, zlsVersionArgAdded)) { + // Verify the installation by quering the version + const checkedZLSVersion = getVersion(zlsExePath, "--version"); + if (!checkedZLSVersion) { + void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`); + return null; + } + + if (checkedZLSVersion.compare(zlsVersion) !== 0) { + // The Matrix is broken! + void vscode.window.showWarningMessage( + `Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`, + ); + } } return { exe: zlsExePath, - version: checkedZLSVersion, + version: zlsVersion, }; } @@ -376,6 +389,14 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zig.zls.openOutput", () => { outputChannel.show(); }), + ); + + if (await isEnabled()) { + await restartClient(context); + } + + // These checks are added later to avoid ZLS be started twice because `isEnabled` sets `zig.zls.enabled`. + context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (change) => { // The `zig.path` config option is handled by `zigProvider.onChange`. if ( @@ -390,10 +411,6 @@ export async function activate(context: vscode.ExtensionContext) { await restartClient(context); }), ); - - if (await isEnabled()) { - await restartClient(context); - } } export async function deactivate(): Promise { From ac7f8eb17c5114b2334e1fe52158536cb62be866 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 03:27:52 +0100 Subject: [PATCH 08/25] add status item for ZLS --- src/zls.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/zls.ts b/src/zls.ts index 3b74921..47cb01a 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -25,11 +25,14 @@ const ZIG_MODE: DocumentSelector = [ ]; let versionManagerConfig: versionManager.Config; +let statusItem: vscode.LanguageStatusItem; let outputChannel: vscode.OutputChannel; export let client: LanguageClient | null = null; export async function restartClient(context: vscode.ExtensionContext): Promise { const result = await getZLSPath(context); + updateStatusItem(result?.version ?? null); + if (!result) return; try { @@ -345,6 +348,32 @@ async function isEnabled(): Promise { } } +function updateStatusItem(version: semver.SemVer | null) { + if (version) { + statusItem.text = `ZLS ${version.toString()}`; + statusItem.detail = "ZLS Version"; + statusItem.severity = vscode.LanguageStatusSeverity.Information; + statusItem.command = { + title: "View Output", + command: "zig.zls.openOutput", + }; + } else { + statusItem.text = "ZLS not enabled"; + statusItem.detail = undefined; + statusItem.severity = vscode.LanguageStatusSeverity.Error; + const zigPath = zigProvider.getZigPath(); + const zigVersion = zigProvider.getZigVersion(); + if (zigPath !== null && zigVersion !== null) { + statusItem.command = { + title: "Enable", + command: "zig.zls.enable", + }; + } else { + statusItem.command = undefined; + } + } +} + export async function activate(context: vscode.ExtensionContext) { { // This check can be removed once enough time has passed so that most users switched to the new value @@ -371,9 +400,13 @@ export async function activate(context: vscode.ExtensionContext) { }; outputChannel = vscode.window.createOutputChannel("Zig Language Server"); + statusItem = vscode.languages.createLanguageStatusItem("zig.zls.status", ZIG_MODE); + statusItem.name = "ZLS"; + updateStatusItem(null); context.subscriptions.push( outputChannel, + statusItem, vscode.commands.registerCommand("zig.zls.enable", async () => { const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); await zlsConfig.update("enabled", "on"); From 41159be1fbff77dff6a2026eecdc66ee396f5ce1 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 20 Sep 2024 21:04:21 +0200 Subject: [PATCH 09/25] add status items for Zig --- src/zigSetup.ts | 54 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index dbdb60d..d5c35a8 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -9,6 +9,8 @@ import * as versionManager from "./versionManager"; import { VersionIndex, ZigVersion, getHostZigName } from "./zigUtil"; import { ZigProvider } from "./zigProvider"; +let statusItem: vscode.StatusBarItem; +let languageStatusItem: vscode.LanguageStatusItem; let versionManagerConfig: versionManager.Config; export let zigProvider: ZigProvider; @@ -191,6 +193,37 @@ async function getWantedZigVersion( return null; } +function updateStatusItem(item: vscode.StatusBarItem, version: semver.SemVer | null) { + item.name = "Zig"; + item.text = `Zig ${version?.toString() ?? "not installed"}`; + item.tooltip = "Select Zig Version"; + item.command = { + title: "Select Version", + command: "zig.install", + }; + if (version) { + item.backgroundColor = undefined; + } else { + item.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground"); + } +} + +function updateLanguageStatusItem(item: vscode.LanguageStatusItem, version: semver.SemVer | null) { + item.name = "Zig"; + if (version) { + item.text = `Zig ${version.toString()}`; + item.detail = "Zig Version"; + item.severity = vscode.LanguageStatusSeverity.Information; + } else { + item.text = "Zig not installed"; + item.severity = vscode.LanguageStatusSeverity.Error; + } + item.command = { + title: "Select Version", + command: "zig.install", + }; +} + function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext, zigExePath: string | null) { if (zigExePath) { const envValue = path.delimiter + path.dirname(zigExePath); @@ -229,15 +262,34 @@ export async function setupZig(context: vscode.ExtensionContext) { zigProvider = new ZigProvider(); + /** There two status items because there doesn't seem to be a way to pin a language status item by default. */ + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, -1); + languageStatusItem = vscode.languages.createLanguageStatusItem("zig.status", { language: "zig" }); + context.environmentVariableCollection.description = "Add Zig to PATH"; + const onDidChangeActiveTextEditor = (editor: vscode.TextEditor | undefined) => { + if (editor?.document.languageId === "zig") { + statusItem.show(); + } else { + statusItem.hide(); + } + }; + onDidChangeActiveTextEditor(vscode.window.activeTextEditor); + context.subscriptions.push( zigProvider, + statusItem, + languageStatusItem, vscode.commands.registerCommand("zig.install", async () => { await selectVersionAndInstall(context); }), + vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), zigProvider.onChange.event((result) => { - const { exe } = result ?? { exe: null, version: null }; + const { exe, version } = result ?? { exe: null, version: null }; + + updateStatusItem(statusItem, version); + updateLanguageStatusItem(languageStatusItem, version); updateZigEnvironmentVariableCollection(context, exe); }), From 27ba4db3d2c61aa82b3349102c4ecb877c0f94f4 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Tue, 24 Sep 2024 18:22:07 +0200 Subject: [PATCH 10/25] add `zig.version` config option. --- package.json | 7 ++++++- src/zigSetup.ts | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index de4a44b..18fb7c6 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,12 @@ "zig.path": { "scope": "machine-overridable", "type": "string", - "description": "Set a custom path to the Zig binary. The string \"zig\" means lookup zig in PATH." + "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." + }, + "zig.version": { + "scope": "resource", + "type": "string", + "description": "Specify which Zig version should be installed. Takes priority over a `.zigversion` file or a `build.zig.zon` with `minimum_zig_version`." }, "zig.formattingProvider": { "scope": "resource", diff --git a/src/zigSetup.ts b/src/zigSetup.ts index d5c35a8..0dab1d5 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -117,6 +117,8 @@ enum WantedZigVersionSource { workspaceZigVersionFile = ".zigversion", /** The `minimum_zig_version` in `build.zig.zon` */ workspaceBuildZigZon = "build.zig.zon", + /** `zig.version` */ + zigVersionConfigOption = "zig.version", latestTagged = "latest-tagged", } @@ -166,6 +168,17 @@ async function getWantedZigVersion( } } break; + case WantedZigVersionSource.zigVersionConfigOption: + const versionString = vscode.workspace.getConfiguration("zig").get("version"); + if (versionString) { + result = semver.parse(versionString); + if (!result) { + void vscode.window.showErrorMessage( + `Invalid 'zig.version' config option. '${versionString}' is not a valid Zig version`, + ); + } + } + break; case WantedZigVersionSource.latestTagged: const cacheKey = "zig-latest-tagged"; try { @@ -284,6 +297,14 @@ export async function setupZig(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zig.install", async () => { await selectVersionAndInstall(context); }), + vscode.workspace.onDidChangeConfiguration(async (change) => { + // The `zig.path` config option is handled by `zigProvider.onChange`. + if (change.affectsConfiguration("zig.version")) { + if (!vscode.workspace.getConfiguration("zig").get("path")) { + await installZig(context); + } + } + }), vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), zigProvider.onChange.event((result) => { const { exe, version } = result ?? { exe: null, version: null }; From 89ca2ebfb7fbee03365fc6d2810114d3d2b9fb63 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 03:28:36 +0100 Subject: [PATCH 11/25] watch for changes to `build.zig.zon` and `.zigversion` --- src/zigSetup.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 0dab1d5..46f067f 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -281,6 +281,18 @@ export async function setupZig(context: vscode.ExtensionContext) { context.environmentVariableCollection.description = "Add Zig to PATH"; + const watcher1 = vscode.workspace.createFileSystemWatcher("**/.zigversion"); + const watcher2 = vscode.workspace.createFileSystemWatcher("**/build.zig.zon"); + + const refreshZigInstallation = async () => { + if (!vscode.workspace.getConfiguration("zig").get("path")) { + await installZig(context); + } else { + updateStatusItem(statusItem, zigProvider.getZigVersion()); + updateLanguageStatusItem(languageStatusItem, zigProvider.getZigVersion()); + } + }; + const onDidChangeActiveTextEditor = (editor: vscode.TextEditor | undefined) => { if (editor?.document.languageId === "zig") { statusItem.show(); @@ -300,9 +312,7 @@ export async function setupZig(context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(async (change) => { // The `zig.path` config option is handled by `zigProvider.onChange`. if (change.affectsConfiguration("zig.version")) { - if (!vscode.workspace.getConfiguration("zig").get("path")) { - await installZig(context); - } + await refreshZigInstallation(); } }), vscode.window.onDidChangeActiveTextEditor(onDidChangeActiveTextEditor), @@ -314,9 +324,15 @@ export async function setupZig(context: vscode.ExtensionContext) { updateZigEnvironmentVariableCollection(context, exe); }), + watcher1.onDidCreate(refreshZigInstallation), + watcher1.onDidChange(refreshZigInstallation), + watcher1.onDidDelete(refreshZigInstallation), + watcher1, + watcher2.onDidCreate(refreshZigInstallation), + watcher2.onDidChange(refreshZigInstallation), + watcher2.onDidDelete(refreshZigInstallation), + watcher2, ); - if (!vscode.workspace.getConfiguration("zig").get("path")) { - await installZig(context); - } + await refreshZigInstallation(); } From 03a13024719cb6213f82ccd57836451f3e6f1f34 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:34:40 +0100 Subject: [PATCH 12/25] rework the 'select and install' popup - add the ability to remove the workspace zig-version - add an option to manually specify the path to Zig - add an option to select Zig from PATH (if available) - display the exact version of nightly - display which versions are already installed on the host machine - make the popup usable without an internet connection --- src/versionManager.ts | 22 +++++++ src/zigSetup.ts | 147 ++++++++++++++++++++++++++++++++++-------- src/zigUtil.ts | 17 +++-- 3 files changed, 155 insertions(+), 31 deletions(-) diff --git a/src/versionManager.ts b/src/versionManager.ts index 1dbaba0..0ff121d 100644 --- a/src/versionManager.ts +++ b/src/versionManager.ts @@ -168,3 +168,25 @@ async function installFromMirror( }, ); } + +/** Returns all locally installed versions */ +export async function query(config: Config): Promise { + const available: semver.SemVer[] = []; + const prefix = `${getZigOSName()}-${getZigArchName()}`; + + const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); + try { + for (const [name] of await vscode.workspace.fs.readDirectory(storageDir)) { + if (name.startsWith(prefix)) { + available.push(new semver.SemVer(name.substring(prefix.length + 1))); + } + } + } catch (e) { + if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") { + return []; + } + throw e; + } + + return available; +} diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 46f067f..bd83ee2 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -6,7 +6,7 @@ import axios from "axios"; import semver from "semver"; import * as versionManager from "./versionManager"; -import { VersionIndex, ZigVersion, getHostZigName } from "./zigUtil"; +import { VersionIndex, ZigVersion, getHostZigName, resolveExePathAndVersion } from "./zigUtil"; import { ZigProvider } from "./zigProvider"; let statusItem: vscode.StatusBarItem; @@ -42,6 +42,10 @@ async function installZig(context: vscode.ExtensionContext) { } } +/** + * Returns a sorted list of all versions that are provided by [index.json](https://ziglang.org/download/index.json). + * Throws an exception when no network connection is available. + */ async function getVersions(): Promise { const indexJson = (await axios.get("https://ziglang.org/download/index.json", {})).data; const hostName = getHostZigName(); @@ -71,43 +75,132 @@ async function getVersions(): Promise { `no pre-built Zig is available for your system '${hostName}', you can build it yourself using https://github.com/ziglang/zig-bootstrap`, ); } + result.sort((lhs, rhs) => semver.compare(rhs.version, lhs.version)); return result; } async function selectVersionAndInstall(context: vscode.ExtensionContext) { - try { - const available = await getVersions(); + const offlineVersions = await versionManager.query(versionManagerConfig); + + const versions: { + version: semver.SemVer; + /** Whether the version already installed in global extension storage */ + offline: boolean; + /** Whether is available in `index.json` */ + online: boolean; + }[] = offlineVersions.map((version) => ({ + version: version, + offline: true, + online: false, + })); - const items: vscode.QuickPickItem[] = []; - for (const option of available) { - items.push({ label: option.name }); - } - // Recommend latest stable release. - const placeHolder = available.length > 2 ? available[1].name : undefined; - const selection = await vscode.window.showQuickPick(items, { - title: "Select Zig version to install", - canPickMany: false, - placeHolder: placeHolder, - }); - if (selection === undefined) return; - for (const option of available) { - if (option.name === selection.label) { - await context.workspaceState.update("zig-version", option.version.raw); - await installZig(context); - - void vscode.window.showInformationMessage( - `Zig ${option.version.toString()} has been installed successfully. Relaunch your integrated terminal to make it available.`, - ); - return; + try { + outer: for (const onlineVersion of await getVersions()) { + for (const version of versions) { + if (semver.eq(version.version, onlineVersion.version)) { + version.online = true; + continue outer; + } } + versions.push({ + version: onlineVersion.version, + online: true, + offline: false, + }); } } catch (err) { - if (err instanceof Error) { - void vscode.window.showErrorMessage(`Unable to install Zig: ${err.message}`); + if (!offlineVersions.length) { + if (err instanceof Error) { + void vscode.window.showErrorMessage(`Failed to query available Zig version: ${err.message}`); + } else { + void vscode.window.showErrorMessage(`Failed to query available Zig version!`); + } + return; } else { - throw err; + // Only show the locally installed versions } } + + versions.sort((lhs, rhs) => semver.compare(rhs.version, lhs.version)); + const placeholderVersion = versions.find((item) => item.version.prerelease.length === 0)?.version; + + const items: vscode.QuickPickItem[] = []; + + const workspaceZig = await getWantedZigVersion(context, [ + WantedZigVersionSource.workspaceZigVersionFile, + WantedZigVersionSource.workspaceBuildZigZon, + WantedZigVersionSource.zigVersionConfigOption, + ]); + if (workspaceZig !== null) { + const alreadyInstalled = offlineVersions.some((item) => semver.eq(item.version, workspaceZig.version)); + items.push({ + label: "Use Workspace Version", + description: alreadyInstalled ? "already installed" : undefined, + detail: workspaceZig.version.raw, + }); + } + + const zigInPath = resolveExePathAndVersion(null, "zig", null, "version"); + if (!("message" in zigInPath)) { + items.push({ + label: "Use Zig in PATH", + description: zigInPath.exe, + detail: zigInPath.version.raw, + }); + } + + items.push( + { + label: "Manually Specify Path", + }, + { + label: "", + kind: vscode.QuickPickItemKind.Separator, + }, + ); + + for (const item of versions) { + const isNightly = item.online && item.version.prerelease.length !== 0; + items.push({ + label: isNightly ? "nightly" : item.version.raw, + description: item.offline ? "already installed" : undefined, + detail: isNightly ? item.version.raw : undefined, + }); + } + + const selection = await vscode.window.showQuickPick(items, { + title: "Select Zig version to install", + canPickMany: false, + placeHolder: placeholderVersion?.raw, + }); + if (selection === undefined) return; + + switch (selection.label) { + case "Use Workspace Version": + await context.workspaceState.update("zig-version", undefined); + await installZig(context); + break; + case "Use Zig in PATH": + await vscode.workspace.getConfiguration("zig").update("path", "zig", true); + break; + case "Manually Specify Path": + const uris = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "Select Zig executable", + }); + if (!uris) return; + await vscode.workspace.getConfiguration("zig").update("path", uris[0].path, true); + break; + default: + const version = new semver.SemVer( + selection.label === "nightly" ? selection.detail ?? selection.label : selection.label, + ); + await context.workspaceState.update("zig-version", version.raw); + await installZig(context); + break; + } } /** The order of these enums defines the default order in which these sources are executed. */ diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 15a7f9b..60d55f8 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -5,6 +5,7 @@ import fs from "fs"; import os from "os"; import path from "path"; +import assert from "assert"; import semver from "semver"; import which from "which"; @@ -53,14 +54,16 @@ export function resolveExePathAndVersion( exePath: string | null, /** e.g. `zig` or `zig` */ exeName: string, - /** e.g. `zig.path` or `zig.zls.path` */ - optionName: string, + /** e.g. `zig.path` or `zig.zls.path`. Can be null if `exePath === null` */ + optionName: string | null, /** * The command-line argument that is used to query the version of the executable. * Zig uses `version`. ZLS uses `--version`. */ versionArg: string, ): { exe: string; version: semver.SemVer } | { message: string } { + /* `optionName === null` implies `exePath === null` */ + assert(optionName !== null || exePath === null); if (!exePath) { exePath = which.sync(exeName, { nothrow: true }); } else { @@ -79,13 +82,19 @@ export function resolveExePathAndVersion( } if (!fs.existsSync(exePath)) { - return { message: `\`${optionName}\` ${exePath} does not exist` }; + return { + message: optionName ? `\`${optionName}\` ${exePath} does not exist` : `${exePath} does not exist`, + }; } try { fs.accessSync(exePath, fs.constants.R_OK | fs.constants.X_OK); } catch { - return { message: `\`${optionName}\` ${exePath} is not an executable` }; + return { + message: optionName + ? `\`${optionName}\` ${exePath} is not an executable` + : `${exePath} is not an executable`, + }; } const version = getVersion(exePath, versionArg); From 6b09de7a2333a9303d33c6a25c3303e45ee29ec3 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 04:11:13 +0100 Subject: [PATCH 13/25] handle version resolution for tagged releases without network connection --- src/zigSetup.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index bd83ee2..939e0e7 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -26,6 +26,10 @@ async function installZig(context: vscode.ExtensionContext) { return; } + if (wantedZig.source === WantedZigVersionSource.workspaceBuildZigZon) { + wantedZig.version = await findClosestSatisfyingZigVersion(context, wantedZig.version); + } + try { const exePath = await versionManager.install(versionManagerConfig, wantedZig.version); await vscode.workspace.getConfiguration("zig").update("path", undefined, true); @@ -42,6 +46,25 @@ async function installZig(context: vscode.ExtensionContext) { } } +async function findClosestSatisfyingZigVersion( + context: vscode.ExtensionContext, + version: semver.SemVer, +): Promise { + if (version.prerelease.length !== 0) return version; + const cacheKey = `zig-satisfying-version-${version.raw}`; + + try { + // We can't just return `version` because `0.12.0` should return `0.12.1`. + const availableVersions = (await getVersions()).map((item) => item.version); + const selectedVersion = semver.maxSatisfying(availableVersions, `^${version.toString()}`); + await context.globalState.update(cacheKey, selectedVersion ?? undefined); + return selectedVersion ?? version; + } catch { + const selectedVersion = context.globalState.get(cacheKey, null); + return selectedVersion ? new semver.SemVer(selectedVersion) : version; + } +} + /** * Returns a sorted list of all versions that are provided by [index.json](https://ziglang.org/download/index.json). * Throws an exception when no network connection is available. From df027d1e5fe5fac97705d603c3c3f19581765e0e Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:34:05 +0100 Subject: [PATCH 14/25] implement automatic version removal --- src/extension.ts | 3 ++- src/versionManager.ts | 48 +++++++++++++++++++++++++++++++++++++++++++ src/zigSetup.ts | 4 ++++ src/zls.ts | 1 + 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 88e496f..ba161e6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,11 @@ import vscode from "vscode"; import { activate as activateZls, deactivate as deactivateZls } from "./zls"; +import { deactivate as deactivateSetupZig, setupZig } from "./zigSetup"; import ZigDiagnosticsProvider from "./zigDiagnosticsProvider"; import ZigMainCodeLensProvider from "./zigMainCodeLens"; import ZigTestRunnerProvider from "./zigTestRunnerProvider"; import { registerDocumentFormatting } from "./zigFormat"; -import { setupZig } from "./zigSetup"; export async function activate(context: vscode.ExtensionContext) { await setupZig(context).finally(() => { @@ -31,4 +31,5 @@ export async function activate(context: vscode.ExtensionContext) { export async function deactivate() { await deactivateZls(); + await deactivateSetupZig(); } diff --git a/src/versionManager.ts b/src/versionManager.ts index 0ff121d..71ed5dd 100644 --- a/src/versionManager.ts +++ b/src/versionManager.ts @@ -24,6 +24,9 @@ import { getVersion, getZigArchName, getZigOSName } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); const chmod = util.promisify(fs.chmod); +/** The maxmimum number of installation that can be store until they will be removed */ +const maxInstallCount = 5; + export interface Config { context: vscode.ExtensionContext; /** The name of the application. */ @@ -49,6 +52,8 @@ export async function install(config: Config, version: semver.SemVer): Promise { return available; } + +/** Set the last access time of the (installed) version. */ +async function setLastAccessTime(config: Config, version: semver.SemVer): Promise { + await config.context.globalState.update( + `${config.exeName}-last-access-time-${getZigOSName()}-${getZigArchName()}-${version.raw}`, + Date.now(), + ); +} + +/** Remove installations with the oldest last access time until at most `VersionManager.maxInstallCount` versions remain. */ +export async function removeUnusedInstallations(config: Config) { + const storageDir = vscode.Uri.joinPath(config.context.globalStorageUri, config.exeName); + + const keys: { key: string; installDir: vscode.Uri; lastAccessTime: number }[] = []; + + try { + for (const [name, fileType] of await vscode.workspace.fs.readDirectory(storageDir)) { + const key = `${config.exeName}-last-access-time-${name}`; + const uri = vscode.Uri.joinPath(storageDir, name); + const lastAccessTime = config.context.globalState.get(key); + + if (!lastAccessTime || fileType !== vscode.FileType.Directory) { + await vscode.workspace.fs.delete(uri, { recursive: true, useTrash: false }); + } else { + keys.push({ + key: key, + installDir: uri, + lastAccessTime: lastAccessTime, + }); + } + } + } catch (e) { + if (e instanceof vscode.FileSystemError && e.code === "FileNotFound") return; + throw e; + } + + keys.sort((lhs, rhs) => lhs.lastAccessTime - rhs.lastAccessTime); + + for (const item of keys.slice(maxInstallCount)) { + await vscode.workspace.fs.delete(item.installDir, { recursive: true, useTrash: false }); + await config.context.globalState.update(item.key, undefined); + } +} diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 939e0e7..40b33e4 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -452,3 +452,7 @@ export async function setupZig(context: vscode.ExtensionContext) { await refreshZigInstallation(); } + +export async function deactivate() { + await versionManager.removeUnusedInstallations(versionManagerConfig); +} diff --git a/src/zls.ts b/src/zls.ts index 47cb01a..ccfa956 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -448,4 +448,5 @@ export async function activate(context: vscode.ExtensionContext) { export async function deactivate(): Promise { await stopClient(); + await versionManager.removeUnusedInstallations(versionManagerConfig); } From 13493a2ac2e8143721c6f2f2b3da538667ad97a5 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:35:10 +0100 Subject: [PATCH 15/25] verify Zig and ZLS installations with minisign This unfortunately has a large impact on the bundle size: 600kb -> 1.3mb --- package-lock.json | 24 +++++++++++ package.json | 6 ++- src/minisign.ts | 92 +++++++++++++++++++++++++++++++++++++++++++ src/versionManager.ts | 21 ++++++++++ src/zigSetup.ts | 3 ++ src/zls.ts | 3 ++ 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/minisign.ts diff --git a/package-lock.json b/package-lock.json index cbc0243..d1b8a85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "dependencies": { "axios": "^1.7.4", "camelcase": "^7.0.1", + "libsodium-wrappers": "^0.7.15", "lodash-es": "^4.17.21", "semver": "^7.5.2", "vscode-languageclient": "8.0.2-next.5", "which": "^3.0.0" }, "devDependencies": { + "@types/libsodium-wrappers": "^0.7.14", "@types/lodash-es": "^4.17.12", "@types/mocha": "^2.2.48", "@types/node": "^18.0.0", @@ -183,6 +185,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/libsodium-wrappers": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.14.tgz", + "integrity": "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", @@ -2039,6 +2048,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.15" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", diff --git a/package.json b/package.json index 18fb7c6..b172766 100644 --- a/package.json +++ b/package.json @@ -368,23 +368,25 @@ "lint": "eslint ." }, "devDependencies": { + "@types/libsodium-wrappers": "^0.7.14", "@types/lodash-es": "^4.17.12", "@types/mocha": "^2.2.48", "@types/node": "^18.0.0", "@types/vscode": "^1.80.0", "@types/which": "^2.0.1", + "@vscode/test-electron": "^2.3.9", "@vscode/vsce": "^2.24.0", "esbuild": "^0.12.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "prettier": "3.2.5", "typescript": "^5.4.3", - "typescript-eslint": "^7.4.0", - "@vscode/test-electron": "^2.3.9" + "typescript-eslint": "^7.4.0" }, "dependencies": { "axios": "^1.7.4", "camelcase": "^7.0.1", + "libsodium-wrappers": "^0.7.15", "lodash-es": "^4.17.21", "semver": "^7.5.2", "vscode-languageclient": "8.0.2-next.5", diff --git a/src/minisign.ts b/src/minisign.ts new file mode 100644 index 0000000..eadad8a --- /dev/null +++ b/src/minisign.ts @@ -0,0 +1,92 @@ +/** + * Ported from: https://github.com/mlugg/setup-zig/blob/main/main.js (MIT) + */ + +import sodium from "libsodium-wrappers"; + +export interface Key { + id: Buffer; + key: Buffer; +} + +// Parse a minisign key represented as a base64 string. +// Throws exceptions on invalid keys. +export function parseKey(keyString: string): Key { + const keyInfo = Buffer.from(keyString, "base64"); + + const id = keyInfo.subarray(2, 10); + const key = keyInfo.subarray(10); + + if (key.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) { + throw new Error("invalid public key given"); + } + + return { + id: id, + key: key, + }; +} + +export interface Signature { + algorithm: Buffer; + keyID: Buffer; + signature: Buffer; +} + +// Parse a buffer containing the contents of a minisign signature file. +// Throws exceptions on invalid signature files. +export function parseSignature(sigBuf: Buffer): Signature { + const untrustedHeader = Buffer.from("untrusted comment: "); + + // Validate untrusted comment header, and skip + if (!sigBuf.subarray(0, untrustedHeader.byteLength).equals(untrustedHeader)) { + throw new Error("file format not recognised"); + } + sigBuf = sigBuf.subarray(untrustedHeader.byteLength); + + // Skip untrusted comment + sigBuf = sigBuf.subarray(sigBuf.indexOf("\n") + 1); + + // Read and skip signature info + const sigInfoEnd = sigBuf.indexOf("\n"); + const sigInfo = Buffer.from(sigBuf.subarray(0, sigInfoEnd).toString(), "base64"); + sigBuf = sigBuf.subarray(sigInfoEnd + 1); + + // Extract components of signature info + const algorithm = sigInfo.subarray(0, 2); + const keyID = sigInfo.subarray(2, 10); + const signature = sigInfo.subarray(10); + + // We don't look at the trusted comment or global signature, so we're done. + + return { + algorithm: algorithm, + keyID: keyID, + signature: signature, + }; +} + +// Given a parsed key, parsed signature file, and raw file content, verifies the +// signature. Does not throw. Returns 'true' if the signature is valid for this +// file, 'false' otherwise. +export function verifySignature(pubkey: Key, signature: Signature, fileContent: Buffer) { + let signedContent; + if (signature.algorithm.equals(Buffer.from("ED"))) { + signedContent = sodium.crypto_generichash(sodium.crypto_generichash_BYTES_MAX, fileContent); + } else { + signedContent = fileContent; + } + + if (!signature.keyID.equals(pubkey.id)) { + return false; + } + + if (!sodium.crypto_sign_verify_detached(signature.signature, signedContent, pubkey.key)) { + return false; + } + + // Since we don't use the trusted comment, we don't bother verifying the global signature. + // If we were to start using the trusted comment for any purpose, we must add this. + + return true; +} diff --git a/src/versionManager.ts b/src/versionManager.ts index 71ed5dd..2ddad58 100644 --- a/src/versionManager.ts +++ b/src/versionManager.ts @@ -19,6 +19,7 @@ import which from "which"; import axios from "axios"; import semver from "semver"; +import * as minisign from "./minisign"; import { getVersion, getZigArchName, getZigOSName } from "./zigUtil"; const execFile = util.promisify(childProcess.execFile); @@ -33,6 +34,7 @@ export interface Config { title: string; /** The name of the executable file. */ exeName: string; + minisignKey: minisign.Key; /** The command-line argument that should passed to `tar` to exact the tarball. */ extraTarArgs: string[]; /** @@ -110,6 +112,15 @@ async function installFromMirror( /** https://github.com/mlugg/setup-zig adds a `?source=github-actions` query parameter so we add our own. */ const artifactUrlWithQuery = artifactUrl.with({ query: "source=vscode-zig" }); + const artifactMinisignUrl = vscode.Uri.joinPath(mirrorUrl, `${fileName}.minisig`); + const artifactMinisignUrlWithQuery = artifactMinisignUrl.with({ query: "source=vscode-zig" }); + + const signatureResponse = await axios.get(artifactMinisignUrlWithQuery.toString(), { + responseType: "arraybuffer", + signal: abortController.signal, + }); + const signatureData = Buffer.from(signatureResponse.data); + const artifactResponse = await axios.get(artifactUrlWithQuery.toString(), { responseType: "arraybuffer", signal: abortController.signal, @@ -127,6 +138,16 @@ async function installFromMirror( }); const artifactData = Buffer.from(artifactResponse.data); + progress.report({ message: "Verifying Signature..." }); + + const signature = minisign.parseSignature(signatureData); + if (!minisign.verifySignature(config.minisignKey, signature, artifactData)) { + try { + await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); + } catch {} + throw new Error(`signature verification failed for '${artifactUrl.toString()}'`); + } + try { await vscode.workspace.fs.delete(installDir, { recursive: true, useTrash: false }); } catch {} diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 40b33e4..318e567 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -5,6 +5,7 @@ import path from "path"; import axios from "axios"; import semver from "semver"; +import * as minisign from "./minisign"; import * as versionManager from "./versionManager"; import { VersionIndex, ZigVersion, getHostZigName, resolveExePathAndVersion } from "./zigUtil"; import { ZigProvider } from "./zigProvider"; @@ -382,6 +383,8 @@ export async function setupZig(context: vscode.ExtensionContext) { title: "Zig", exeName: "zig", extraTarArgs: ["--strip-components=1"], + /** https://ziglang.org/download */ + minisignKey: minisign.parseKey("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"), versionArg: "version", canonicalUrl: { release: vscode.Uri.parse("https://ziglang.org/download"), diff --git a/src/zls.ts b/src/zls.ts index ccfa956..f2971f0 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -15,6 +15,7 @@ import axios from "axios"; import camelCase from "camelcase"; import semver from "semver"; +import * as minisign from "./minisign"; import * as versionManager from "./versionManager"; import { getHostZigName, getVersion, handleConfigOption, resolveExePathAndVersion } from "./zigUtil"; import { zigProvider } from "./zigSetup"; @@ -392,6 +393,8 @@ export async function activate(context: vscode.ExtensionContext) { title: "ZLS", exeName: "zls", extraTarArgs: [], + /** https://github.com/zigtools/release-worker */ + minisignKey: minisign.parseKey("RWR+9B91GBZ0zOjh6Lr17+zKf5BoSuFvrx2xSeDE57uIYvnKBGmMjOex"), versionArg: "--version", canonicalUrl: { release: vscode.Uri.parse("https://builds.zigtools.org"), From 43834ee3718539c98b649ac995ef47649cfded1b Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 07:50:01 +0100 Subject: [PATCH 16/25] Use mirrors for downloading Zig tarballs For now, the list of mirrors has been manually copied over from mlugg/setup-zig. Adding mirrors for ZLS is also possible even though none exist. fixes #238 --- src/versionManager.ts | 23 +++++++++++++++++++++++ src/zigSetup.ts | 8 ++++++++ src/zls.ts | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/versionManager.ts b/src/versionManager.ts index 2ddad58..9bfd751 100644 --- a/src/versionManager.ts +++ b/src/versionManager.ts @@ -42,6 +42,7 @@ export interface Config { * `"version"` for Zig, `"--version"` for ZLS */ versionArg: string; + mirrorUrls: vscode.Uri[]; canonicalUrl: { release: vscode.Uri; nightly: vscode.Uri; @@ -70,6 +71,28 @@ export async function install(config: Config, version: semver.SemVer): Promise ({ mirror, sort: Math.random() })) + .sort((a, b) => a.sort - b.sort) + .map(({ mirror }) => mirror); + + for (const mirrorUrl of mirrors) { + const mirrorName = new URL(mirrorUrl.toString()).host; + try { + return await installFromMirror(config, version, mirrorUrl, mirrorName); + } catch (err) { + if (err instanceof Error) { + void vscode.window.showWarningMessage( + `Failed to download ${config.exeName} from ${mirrorName}: ${err.message}, trying different mirror`, + ); + } else { + void vscode.window.showWarningMessage( + `Failed to download ${config.exeName} from ${mirrorName}, trying different mirror`, + ); + } + } + } + const canonicalUrl = version.prerelease.length === 0 ? config.canonicalUrl.release : config.canonicalUrl.nightly; const mirrorName = new URL(canonicalUrl.toString()).host; return await installFromMirror(config, version, canonicalUrl, mirrorName); diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 318e567..b341655 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -386,6 +386,14 @@ export async function setupZig(context: vscode.ExtensionContext) { /** https://ziglang.org/download */ minisignKey: minisign.parseKey("RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"), versionArg: "version", + // taken from https://github.com/mlugg/setup-zig/blob/main/mirrors.json + mirrorUrls: [ + vscode.Uri.parse("https://pkg.machengine.org/zig"), + vscode.Uri.parse("https://zigmirror.hryx.net/zig"), + vscode.Uri.parse("https://zig.linus.dev/zig"), + vscode.Uri.parse("https://fs.liujiacai.net/zigbuilds"), + vscode.Uri.parse("https://zigmirror.nesovic.dev/zig"), + ], canonicalUrl: { release: vscode.Uri.parse("https://ziglang.org/download"), nightly: vscode.Uri.parse("https://ziglang.org/builds"), diff --git a/src/zls.ts b/src/zls.ts index f2971f0..7a6d150 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -227,7 +227,7 @@ async function configurationMiddleware( } /** - * Similar to https://ziglang.org/download/index.json + * Similar to https://builds.zigtools.org/index.json */ interface SelectVersionResponse { /** The ZLS version */ @@ -396,6 +396,7 @@ export async function activate(context: vscode.ExtensionContext) { /** https://github.com/zigtools/release-worker */ minisignKey: minisign.parseKey("RWR+9B91GBZ0zOjh6Lr17+zKf5BoSuFvrx2xSeDE57uIYvnKBGmMjOex"), versionArg: "--version", + mirrorUrls: [], canonicalUrl: { release: vscode.Uri.parse("https://builds.zigtools.org"), nightly: vscode.Uri.parse("https://builds.zigtools.org"), From 60635f436150b530a31bfbd60793b4adbcf756df Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 5 Sep 2024 20:51:49 +0200 Subject: [PATCH 17/25] remove outdated zig_install and zls_install directory --- src/zigSetup.ts | 8 ++++++++ src/zls.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index b341655..6b884fc 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -370,6 +370,14 @@ export async function setupZig(context: vscode.ExtensionContext) { { // This check can be removed once enough time has passed so that most users switched to the new value + // remove the `zig_install` directory from the global storage + try { + await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zig_install"), { + recursive: true, + useTrash: false, + }); + } catch {} + // remove a `zig.path` that points to the global storage. const zigConfig = vscode.workspace.getConfiguration("zig"); const zigPath = zigConfig.get("path", ""); diff --git a/src/zls.ts b/src/zls.ts index 7a6d150..7e908ac 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -379,6 +379,14 @@ export async function activate(context: vscode.ExtensionContext) { { // This check can be removed once enough time has passed so that most users switched to the new value + // remove the `zls_install` directory from the global storage + try { + await vscode.workspace.fs.delete(vscode.Uri.joinPath(context.globalStorageUri, "zls_install"), { + recursive: true, + useTrash: false, + }); + } catch {} + // convert a `zig.zls.path` that points to the global storage to `zig.zls.enabled == "on"` const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); const zlsPath = zlsConfig.get("path", ""); From b877fda2d0708580ba3b221e58ef32cb8db7d030 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 20 Sep 2024 20:23:45 +0200 Subject: [PATCH 18/25] update .vscodeignore --- .vscodeignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscodeignore b/.vscodeignore index 91b64a4..252cb12 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,8 +1,11 @@ +.github/** .vscode/** .vscode-test/** out/test/** src/** .gitignore +.prettierignore +eslint.config.mjs **/tsconfig.json **/tslint.json **/*.map From 5e37e6155646926fb40ed85454c201e1060360f8 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Mon, 30 Sep 2024 19:32:54 +0200 Subject: [PATCH 19/25] ZLS has deprecated `--enable-debug-log` in favor of `--log-level` It is expected to be removed in ZLS 0.14.0 --- src/zls.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/zls.ts b/src/zls.ts index 7e908ac..0a9577a 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -37,7 +37,7 @@ export async function restartClient(context: vscode.ExtensionContext): Promise { +async function startClient(zlsPath: string, zlsVersion: semver.SemVer): Promise { const configuration = vscode.workspace.getConfiguration("zig.zls"); const debugLog = configuration.get("debugLog", false); + const args: string[] = []; + + if (debugLog) { + /** `--enable-debug-log` has been deprecated in favor of `--log-level`. https://github.com/zigtools/zls/pull/1957 */ + const zlsCLIRevampVersion = new semver.SemVer("0.14.0-50+3354fdc"); + if (semver.lt(zlsVersion, zlsCLIRevampVersion)) { + args.push("--enable-debug-log"); + } else { + args.push("--log-level", "debug"); + } + } + const serverOptions: ServerOptions = { command: zlsPath, - args: debugLog ? ["--enable-debug-log"] : [], + args: args, }; const clientOptions: LanguageClientOptions = { From d5430e3e005c335c468f22ceeb1f9466ac437e7d Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 09:55:29 +0100 Subject: [PATCH 20/25] add support for Mach's Nominated Zig versions --- src/zigSetup.ts | 78 ++++++++++++++++++++++++++++++++++--------------- src/zigUtil.ts | 6 +++- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 6b884fc..3573e26 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -67,30 +67,32 @@ async function findClosestSatisfyingZigVersion( } /** - * Returns a sorted list of all versions that are provided by [index.json](https://ziglang.org/download/index.json). + * 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). + * [Nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) are sorted to the bottom. + * * Throws an exception when no network connection is available. */ async function getVersions(): Promise { - const indexJson = (await axios.get("https://ziglang.org/download/index.json", {})).data; + const [zigIndexJson, machIndexJson] = await Promise.all([ + axios.get("https://ziglang.org/download/index.json", {}), + axios.get("https://pkg.machengine.org/zig/index.json", {}), + ]); + const indexJson = { ...machIndexJson.data, ...zigIndexJson.data }; + const hostName = getHostZigName(); const result: ZigVersion[] = []; - for (let key in indexJson) { - const value = indexJson[key]; - let version: semver.SemVer; - if (key === "master") { - key = "nightly"; - version = new semver.SemVer((value as unknown as { version: string }).version); - } else { - version = new semver.SemVer(key); - } + for (const [key, value] of Object.entries(indexJson)) { + const name = key === "master" ? "nightly" : key; + const version = new semver.SemVer(value.version ?? key); const release = value[hostName]; if (release) { result.push({ - name: key, + name: name, version: version, url: release.tarball, sha: release.shasum, - notes: (value as { notes?: string }).notes, + notes: value.notes, + isMach: name.includes("mach"), }); } } @@ -99,37 +101,61 @@ async function getVersions(): Promise { `no pre-built Zig is available for your system '${hostName}', you can build it yourself using https://github.com/ziglang/zig-bootstrap`, ); } - result.sort((lhs, rhs) => semver.compare(rhs.version, lhs.version)); + sortVersions(result); return result; } +function sortVersions(versions: { name?: string; version: semver.SemVer; isMach: boolean }[]) { + versions.sort((lhs, rhs) => { + // Mach versions except `mach-latest` move to the end + if (lhs.name !== "mach-latest" && rhs.name !== "mach-latest" && lhs.isMach !== rhs.isMach) + return +lhs.isMach - +rhs.isMach; + return semver.compare(rhs.version, lhs.version); + }); +} + async function selectVersionAndInstall(context: vscode.ExtensionContext) { const offlineVersions = await versionManager.query(versionManagerConfig); const versions: { + name?: string; version: semver.SemVer; /** Whether the version already installed in global extension storage */ offline: boolean; /** Whether is available in `index.json` */ online: boolean; + /** Whether the version one of [Mach's nominated Zig versions](https://machengine.org/docs/nominated-zig/#nominated-zig-history) */ + isMach: boolean; }[] = offlineVersions.map((version) => ({ version: version, offline: true, online: false, + isMach: false /* We can't tell if a version is Mach while being offline */, })); try { - outer: for (const onlineVersion of await getVersions()) { + const onlineVersions = await getVersions(); + outer: for (const onlineVersion of onlineVersions) { for (const version of versions) { if (semver.eq(version.version, onlineVersion.version)) { + version.name ??= onlineVersion.name; version.online = true; + version.isMach = onlineVersion.isMach; + } + } + + for (const version of versions) { + if (semver.eq(version.version, onlineVersion.version) && version.name === onlineVersion.name) { continue outer; } } + versions.push({ + name: onlineVersion.name, version: onlineVersion.version, online: true, - offline: false, + offline: !!offlineVersions.find((item) => semver.eq(item.version, onlineVersion.version)), + isMach: onlineVersion.isMach, }); } } catch (err) { @@ -145,7 +171,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { } } - versions.sort((lhs, rhs) => semver.compare(rhs.version, lhs.version)); + sortVersions(versions); const placeholderVersion = versions.find((item) => item.version.prerelease.length === 0)?.version; const items: vscode.QuickPickItem[] = []; @@ -183,12 +209,20 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { }, ); + let seenMachVersion = false; for (const item of versions) { - const isNightly = item.online && item.version.prerelease.length !== 0; + const useName = item.isMach || item.version.prerelease.length !== 0; + if (item.isMach && !seenMachVersion && item.name !== "mach-latest") { + seenMachVersion = true; + items.push({ + label: "Mach's Nominated Zig versions", + kind: vscode.QuickPickItemKind.Separator, + }); + } items.push({ - label: isNightly ? "nightly" : item.version.raw, + label: (useName ? item.name : null) ?? item.version.raw, description: item.offline ? "already installed" : undefined, - detail: isNightly ? item.version.raw : undefined, + detail: useName ? (item.name ? item.version.raw : undefined) : undefined, }); } @@ -218,9 +252,7 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) { await vscode.workspace.getConfiguration("zig").update("path", uris[0].path, true); break; default: - const version = new semver.SemVer( - selection.label === "nightly" ? selection.detail ?? selection.label : selection.label, - ); + const version = new semver.SemVer(selection.detail ?? selection.label); await context.workspaceState.update("zig-version", version.raw); await installZig(context); break; diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 60d55f8..33f2e3d 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -173,11 +173,15 @@ export interface ZigVersion { url: string; sha: string; notes?: string; + isMach: boolean; } export type VersionIndex = Record< string, - Record + { + version?: string; + notes?: string; + } & Record >; export function getWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined { From 669477397d32f33de45b9fe5f63cc19b031e3f28 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 23 Nov 2024 10:14:51 +0100 Subject: [PATCH 21/25] add an alias name for the "Zig" language so that it is capitalized --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index b172766..54e218d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ ".zig", ".zon" ], + "aliases": [ + "Zig" + ], "configuration": "./language-configuration.json" } ], From e70b884487ddc0b32f1853fb3c154543598c3181 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 23 Nov 2024 10:15:39 +0100 Subject: [PATCH 22/25] update Zig Version status bar item - removed redundant "Zig" from text - changed the Name (that appears in the context menu) from "Zig" to "Zig Version" --- src/zigSetup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 3573e26..0c39035 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -356,8 +356,8 @@ async function getWantedZigVersion( } function updateStatusItem(item: vscode.StatusBarItem, version: semver.SemVer | null) { - item.name = "Zig"; - item.text = `Zig ${version?.toString() ?? "not installed"}`; + item.name = "Zig Version"; + item.text = version?.toString() ?? "not installed"; item.tooltip = "Select Zig Version"; item.command = { title: "Select Version", From fd95aa65e006ec60f504d782a7428c8d4fb024d5 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 23 Nov 2024 11:39:21 +0100 Subject: [PATCH 23/25] remove unnecessary ZLS version check A similar test is already done in versionManager.ts --- src/zls.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/zls.ts b/src/zls.ts index 0a9577a..3b401e6 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -17,7 +17,7 @@ import semver from "semver"; import * as minisign from "./minisign"; import * as versionManager from "./versionManager"; -import { getHostZigName, getVersion, handleConfigOption, resolveExePathAndVersion } from "./zigUtil"; +import { getHostZigName, handleConfigOption, resolveExePathAndVersion } from "./zigUtil"; import { zigProvider } from "./zigSetup"; const ZIG_MODE: DocumentSelector = [ @@ -133,25 +133,6 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri return null; } - /** `--version` has been added in https://github.com/zigtools/zls/pull/583 */ - const zlsVersionArgAdded = new semver.SemVer("0.10.0-dev.150+cb5eeb0b4"); - - if (semver.gte(zlsVersion, zlsVersionArgAdded)) { - // Verify the installation by quering the version - const checkedZLSVersion = getVersion(zlsExePath, "--version"); - if (!checkedZLSVersion) { - void vscode.window.showErrorMessage(`Unable to check ZLS version. '${zlsExePath} --version' failed!`); - return null; - } - - if (checkedZLSVersion.compare(zlsVersion) !== 0) { - // The Matrix is broken! - void vscode.window.showWarningMessage( - `Encountered unexpected ZLS version. Expected '${zlsVersion.toString()}' from '${zlsExePath} --version' but got '${checkedZLSVersion.toString()}'!`, - ); - } - } - return { exe: zlsExePath, version: zlsVersion, From d8d73e021be037d0fa8c302bca9285b101924433 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sat, 23 Nov 2024 12:09:51 +0100 Subject: [PATCH 24/25] remove the outdated `initialSetupDone` config option from settings This should help when going back to the extension version. Otherwise it wont show the initial setup popup again. --- src/zigSetup.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 0c39035..20b20fc 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -416,6 +416,8 @@ export async function setupZig(context: vscode.ExtensionContext) { if (zigPath.startsWith(context.globalStorageUri.fsPath)) { await zigConfig.update("path", undefined, true); } + + await zigConfig.update("initialSetupDone", undefined, true); } versionManagerConfig = { From 11eb5db345183d8590c6763aa9b7a97edd340c96 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 6 Dec 2024 21:13:31 +0100 Subject: [PATCH 25/25] stop ZLS when setting zig.zls.enabled to "off" while ZLS is running This also fixes some cases where the status item would incorrectly state that ZLS is running --- src/zls.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/zls.ts b/src/zls.ts index 3b401e6..d8e15df 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -32,20 +32,25 @@ export let client: LanguageClient | null = null; export async function restartClient(context: vscode.ExtensionContext): Promise { const result = await getZLSPath(context); - updateStatusItem(result?.version ?? null); - if (!result) return; + if (!result) { + await stopClient(); + updateStatusItem(null); + return; + } try { const newClient = await startClient(result.exe, result.version); await stopClient(); client = newClient; + updateStatusItem(result.version); } catch (reason) { if (reason instanceof Error) { void vscode.window.showWarningMessage(`Failed to run Zig Language Server (ZLS): ${reason.message}`); } else { void vscode.window.showWarningMessage("Failed to run Zig Language Server (ZLS)"); } + updateStatusItem(null); } }