From ba0e34006a878ac81cea221e01fd629ea6be8bd2 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 25 Oct 2024 17:40:12 +0200 Subject: [PATCH] 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 | 193 +++++++++++++++++++++++++++++++----------------- src/zls.ts | 83 ++++++++++++--------- 3 files changed, 174 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 69c93a6..b1dc85f 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 32b9183..b1283f5 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -1,27 +1,43 @@ +import vscode from "vscode"; + import path from "path"; import semver from "semver"; -import vscode from "vscode"; -import { ZigVersion, getHostZigName, getVersion, getVersionIndex } from "./zigUtil"; +import { ZigVersion, getHostZigName, getVersionIndex } from "./zigUtil"; import { VersionManager } from "./versionManager"; import { ZigProvider } from "./zigProvider"; -import { restartClient } from "./zls"; let versionManager: VersionManager; export let zigProvider: ZigProvider; -export async function installZig(context: vscode.ExtensionContext, version: semver.SemVer) { - const zigPath = await versionManager.install(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(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()}!`); + } + return; + } } async function getVersions(): Promise { @@ -74,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; } } @@ -87,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); @@ -101,21 +205,14 @@ 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. // 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"); - const initialSetupDone = zigConfig.get("initialSetupDone", false); - const zigPath = zigConfig.get("path"); - if (zigPath === "" && initialSetupDone) { - await zigConfig.update("path", "zig", true); - } - - const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + // remove a `zig.path` that points to the global storage. + const zlsConfig = vscode.workspace.getConfiguration("zig"); if (zlsConfig.get("enabled", null) === null) { - const zlsPath = zlsConfig.get("path"); - if (zlsPath === "" && initialSetupDone) { - await zlsConfig.update("path", "zls", true); + const zlsPath = zlsConfig.get("path", ""); + if (zlsPath.startsWith(context.globalStorageUri.fsPath)) { + await zlsConfig.update("path", undefined, true); } } } @@ -138,49 +235,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 fac8375..181b6bb 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -15,7 +15,7 @@ import axios from "axios"; import camelCase from "camelcase"; import semver from "semver"; -import { getHostZigName, getVersion, handleConfigOption } from "./zigUtil"; +import { getHostZigName, getVersion, handleConfigOption, resolveExePathAndVersion } from "./zigUtil"; import { VersionManager } from "./versionManager"; import { zigProvider } from "./zigSetup"; @@ -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("enabled", false)) { - await stopClient(); - return; - } - const result = await getZLSPath(context); if (!result) return; @@ -92,39 +86,58 @@ 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("enabled", false)) 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("enabled", false)) return null; - if (!zigProvider.zigVersion) return null; + if (!zigProvider.zigVersion) return null; - const result = await fetchVersion(context, zigProvider.zigVersion, true); - if (!result) return null; + const result = await fetchVersion(context, zigProvider.zigVersion, true); + if (!result) return null; - try { - zlsExePath = await versionManager.install(result.version); - zlsVersion = result.version; - } catch { + try { + zlsExePath = await versionManager.install(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, }; } @@ -367,6 +380,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 ( @@ -381,10 +402,6 @@ export async function activate(context: vscode.ExtensionContext) { await restartClient(context); }), ); - - if (await isEnabled()) { - await restartClient(context); - } } export async function deactivate(): Promise {