From 41f94433dc4d9db8221acda1fe6451aeea273b02 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Tue, 24 Sep 2024 18:01:47 +0200 Subject: [PATCH] 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 | 21 ++- src/zigFormat.ts | 17 +++ src/zigSetup.ts | 112 ++++++--------- src/zls.ts | 351 +++++++++++++++++++++++------------------------ 4 files changed, 240 insertions(+), 261 deletions(-) diff --git a/package.json b/package.json index c28375b..1716d69 100644 --- a/package.json +++ b/package.json @@ -150,16 +150,16 @@ ], "default": "off" }, - "zig.zls.checkForUpdate": { + "zig.zls.enabled": { "scope": "resource", "type": "boolean", - "description": "Whether to automatically check for new updates", - "default": true + "description": "Whether to enable the optional ZLS Language Server", + "default": null }, "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": { @@ -329,23 +329,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 210cf93..1b8f740 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); @@ -61,6 +64,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..ab5f062 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); } } @@ -180,9 +182,11 @@ export async function setupZig(context: vscode.ExtensionContext) { } const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); - const zlsPath = zlsConfig.get("path"); - if (zlsPath === "" && initialSetupDone) { - await zlsConfig.update("path", "zls", true); + if (zlsConfig.get("enabled", null) === null) { + const zlsPath = zlsConfig.get("path"); + if (zlsPath === "" && initialSetupDone) { + await zlsConfig.update("path", "zls", true); + } } } @@ -192,7 +196,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 +219,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", - ); - - 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; + const version = getVersion(uris[0].path, "version"); + if (!version) return false; - 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..a0a9b35 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("enabled", false)) { + 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("enabled", false)) 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,87 @@ 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("enabled", null)) { + case true: + return true; + case false: + return false; + default: { + 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", true); + return true; + case "No": + await zlsConfig.update("enabled", false); + 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 == true` + const zlsConfig = vscode.workspace.getConfiguration("zig.zls"); + if (zlsConfig.get("enabled", null) === null) { + const zlsPath = zlsConfig.get("path", ""); + if (zlsPath.startsWith(context.globalStorageUri.fsPath)) { + await zlsConfig.update("enabled", true, 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", true); }), 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", true); + 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.enable", 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(); }