diff --git a/CHANGELOG.md b/CHANGELOG.md index f12c4ff..9c93f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0 +- Rework initial setup and installation management +- Add new zls hint settings (@leecannon) + ## 0.4.3 - Fix checking for ZLS updates - Always check `PATH` when `zigPath` is set to empty string diff --git a/package.json b/package.json index 80e0983..188124f 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,11 @@ "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, @@ -98,16 +103,11 @@ "default": "${workspaceFolder}/build.zig", "description": "The path to build.zig. This is only required if zig.buildOptions = build." }, - "zig.zigPath": { + "zig.path": { "type": "string", "default": null, "description": "Set a custom path to the Zig binary. Empty string will lookup zig in PATH." }, - "zig.zigVersion": { - "type": "string", - "default": null, - "description": "Version of Zig to install" - }, "zig.checkForUpdate": { "scope": "resource", "type": "boolean", @@ -161,12 +161,6 @@ ], "default": "extension" }, - "zig.zls.enabled": { - "scope": "resource", - "type": "boolean", - "description": "Whether to enable zls", - "default": true - }, "zig.trace.server": { "scope": "window", "type": "string", @@ -181,7 +175,7 @@ "zig.zls.checkForUpdate": { "scope": "resource", "type": "boolean", - "description": "Whether to automatically check for new updates", + "description": "Whether to automatically check for new updates for nightly version", "default": true }, "zig.zls.path": { diff --git a/src/extension.ts b/src/extension.ts index c3b07ce..6e04894 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,10 @@ "use strict"; import * as vscode from "vscode"; -import ZigCompilerProvider from "./zigCompilerProvider"; import { zigBuild } from "./zigBuild"; +import ZigCompilerProvider from "./zigCompilerProvider"; import { ZigFormatProvider, ZigRangeFormatProvider } from "./zigFormat"; -import { activate as activateZls, deactivate as deactivateZls } from "./zls"; import { setupZig } from "./zigSetup"; +import { activate as activateZls, deactivate as deactivateZls } from "./zls"; const ZIG_MODE: vscode.DocumentFilter = { language: "zig", scheme: "file" }; @@ -17,10 +17,10 @@ export function activate(context: vscode.ExtensionContext) { const compiler = new ZigCompilerProvider(); compiler.activate(context.subscriptions); vscode.languages.registerCodeActionsProvider("zig", compiler); - + context.subscriptions.push(logChannel); - - if (vscode.workspace.getConfiguration("zig").get("formattingProvider", "extension") === "extension") { + + if (vscode.workspace.getConfiguration("zig").get("formattingProvider") === "extension") { context.subscriptions.push( vscode.languages.registerDocumentFormattingEditProvider( ZIG_MODE, @@ -34,28 +34,17 @@ export function activate(context: vscode.ExtensionContext) { ), ); } - + buildDiagnosticCollection = vscode.languages.createDiagnosticCollection("zig"); context.subscriptions.push(buildDiagnosticCollection); - + // Commands context.subscriptions.push(vscode.commands.registerCommand("zig.build.workspace", () => zigBuild())); - - activateZls(context); + + activateZls(context) }); } export function deactivate() { deactivateZls(); } - -// Check timestamp `key` to avoid automatically checking for updates -// more than once in an hour. -export function shouldCheckUpdate(context: vscode.ExtensionContext, key: string): boolean { - const HOUR = 60 * 60 * 1000; - const timestamp = new Date().getTime(); - const old = context.globalState.get(key); - if (old === undefined || timestamp - old < HOUR) {return false;} - context.globalState.update(key, timestamp); - return true; -} diff --git a/src/zigCompilerProvider.ts b/src/zigCompilerProvider.ts index 2689c81..cab0e44 100644 --- a/src/zigCompilerProvider.ts +++ b/src/zigCompilerProvider.ts @@ -24,7 +24,7 @@ export default class ZigCompilerProvider implements vscode.CodeActionProvider { maybeDoASTGenErrorCheck(change: vscode.TextDocumentChangeEvent) { if (change.document.languageId !== "zig") {return;} - if (vscode.workspace.getConfiguration("zig").get("astCheckProvider", "zls") !== "extension") { + if (vscode.workspace.getConfiguration("zig").get("astCheckProvider") !== "extension") { this.astDiagnostics.clear(); return; } diff --git a/src/zigSetup.ts b/src/zigSetup.ts index 4ce9e8e..c6aa287 100644 --- a/src/zigSetup.ts +++ b/src/zigSetup.ts @@ -1,29 +1,17 @@ import { ExtensionContext, window, workspace } from "vscode"; import axios from "axios"; +import * as child_process from "child_process"; import { createHash } from "crypto"; import * as fs from "fs"; import mkdirp from "mkdirp"; import semver from "semver"; import * as vscode from "vscode"; -import { shouldCheckUpdate } from "./extension"; -import { execCmd, isWindows } from "./zigUtil"; +import { execCmd, getHostZigName, isWindows, shouldCheckUpdate } from "./zigUtil"; +import { install as installZLS } from "./zls"; const DOWNLOAD_INDEX = "https://ziglang.org/download/index.json"; -function getHostZigName(): string { - let os: string = process.platform; - if (os === "darwin") {os = "macos";} - if (os === "win32") {os = "windows";} - let arch: string = process.arch; - if (arch === "ia32") {arch = "x86";} - if (arch === "x64") {arch = "x86_64";} - if (arch === "arm64") {arch = "aarch64";} - if (arch === "ppc") {arch = "powerpc";} - if (arch === "ppc64") {arch = "powerpc64le";} - return `${arch}-${os}`; -} - function getNightlySemVer(url: string): string { return url.match(/-(\d+\.\d+\.\d+-dev\.\d+\+\w+)\./)[1]; } @@ -38,7 +26,7 @@ async function getVersions(): Promise { const result: ZigVersion[] = []; // eslint-disable-next-line prefer-const for (let [key, value] of Object.entries(indexJson)) { - if (key === "master") {key = "nightly";} + if (key === "master") { key = "nightly"; } if (value[hostName]) { result.push({ name: key, @@ -53,7 +41,7 @@ async function getVersions(): Promise { return result; } -async function installZig(context: ExtensionContext, version: ZigVersion): Promise { +async function install(context: ExtensionContext, version: ZigVersion) { await window.withProgress({ title: "Installing Zig...", location: vscode.ProgressLocation.Notification, @@ -68,12 +56,12 @@ async function installZig(context: ExtensionContext, version: ZigVersion): Promi } const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zig_install"); - if (fs.existsSync(installDir.fsPath)) {fs.rmSync(installDir.fsPath, { recursive: true, force: true });} + if (fs.existsSync(installDir.fsPath)) { fs.rmSync(installDir.fsPath, { recursive: true, force: true }); } mkdirp.sync(installDir.fsPath); - progress.report({ message: "Decompressing..." }); + progress.report({ message: "Extracting..." }); const tar = execCmd("tar", { - cmdArguments: ["-xJf", "-", "-C", `${installDir.fsPath}`, "--strip-components=1"], + cmdArguments: ["-xJf", "-", "-C", installDir.fsPath, "--strip-components=1"], notFoundText: "Could not find tar", }); tar.stdin.write(tarball); @@ -86,12 +74,11 @@ async function installZig(context: ExtensionContext, version: ZigVersion): Promi fs.chmodSync(zigPath, 0o755); const configuration = workspace.getConfiguration("zig"); - await configuration.update("zigPath", zigPath, true); - await configuration.update("zigVersion", version.name, true); + await configuration.update("path", zigPath, true); }); } -async function selectVersionAndInstall(context: ExtensionContext): Promise { +async function selectVersionAndInstall(context: ExtensionContext) { try { const available = await getVersions(); @@ -106,13 +93,10 @@ async function selectVersionAndInstall(context: ExtensionContext): Promise canPickMany: false, placeHolder, }); - if (selection === undefined) {return;} + if (selection === undefined) { return; } for (const option of available) { if (option.name === selection.label) { - if (option.name === "nightly") { - option.name = `nightly-${getNightlySemVer(option.url)}`; - } - await installZig(context, option); + await install(context, option); return; } } @@ -121,14 +105,14 @@ async function selectVersionAndInstall(context: ExtensionContext): Promise } } -async function checkUpdate(context: ExtensionContext): Promise { +async function checkUpdate(context: ExtensionContext) { try { const update = await getUpdatedVersion(context); - if (!update) {return;} + if (!update) return; - const response = await window.showInformationMessage(`New version of Zig available: ${update.name}`, "Install", "Cancel"); + const response = await window.showInformationMessage(`New version of Zig available: ${update.name}`, "Install", "Ignore"); if (response === "Install") { - await installZig(context, update); + await install(context, update); } } catch (err) { window.showErrorMessage(`Unable to update Zig: ${err}`); @@ -137,26 +121,25 @@ async function checkUpdate(context: ExtensionContext): Promise { async function getUpdatedVersion(context: ExtensionContext): Promise { const configuration = workspace.getConfiguration("zig"); - const zigPath = configuration.get("zigPath", null); + const zigPath = configuration.get("path"); if (zigPath) { const zigBinPath = vscode.Uri.joinPath(context.globalStorageUri, "zig_install", "zig").fsPath; - if (!zigPath.startsWith(zigBinPath)) {return null;} + if (!zigPath.startsWith(zigBinPath)) return null; } - const version = configuration.get("zigVersion", null); - if (!version) {return null;} + const buffer = child_process.execFileSync(zigPath, ["version"]); + const curVersion = semver.parse(buffer.toString("utf8")); const available = await getVersions(); - if (version.startsWith("nightly")) { - if (available[0].name === "nightly") { - const curVersion = version.slice("nightly-".length); + 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, version)) { + } else if (available.length > 2 && semver.gt(available[1].name, curVersion)) { return available[1]; } return null; @@ -165,43 +148,84 @@ async function getUpdatedVersion(context: ExtensionContext): Promise { await selectVersionAndInstall(context); + await installZLS(context, true); }); vscode.commands.registerCommand("zig.update", async () => { await checkUpdate(context); }); - const configuration = workspace.getConfiguration("zig", null); - if (configuration.get("zigPath", null) === null) { - const response = await window.showInformationMessage( - "Zig path hasn't been set, do you want to specify the path or install Zig?", - "Install", "Specify path", "Use Zig in PATH" + const configuration = workspace.getConfiguration("zig"); + if (!configuration.get("initialSetupDone")) { + await configuration.update("initialSetupDone", + await initialSetup(context) ); + } - if (response === "Install") { - await selectVersionAndInstall(context); - const configuration = workspace.getConfiguration("zig", null); - const zigPath = configuration.get("zigPath", null); - if (!zigPath) {return;} - window.showInformationMessage(`Zig was installed at '${zigPath}', add it to PATH to use it from the terminal`); - return; - } else if (response === "Specify path") { - const uris = await window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig executable", - }); + if (!configuration.get("path")) return; + if (!configuration.get("checkForUpdate")) return; + if (!shouldCheckUpdate(context, "zigUpdate")) return; + await checkUpdate(context); +} - if (uris) { - await configuration.update("zigPath", uris[0].fsPath, true); - } - } else if (response === "Use Zig in PATH") { - await configuration.update("zigPath", "", true); - } else {throw Error("zigPath not specified");} +async function initialSetup(context: ExtensionContext): Promise { + const zigConfig = workspace.getConfiguration("zig"); + const zigResponse = await 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" + ); + + if (zigResponse === "Install") { + await selectVersionAndInstall(context); + const configuration = workspace.getConfiguration("zig"); + const path = configuration.get("path"); + if (!path) return false; + window.showInformationMessage(`Zig was installed at '${path}', add it to PATH to use it from the terminal`); + } else if (zigResponse === "Specify path") { + const uris = await window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "Select Zig executable", + }); + if (!uris) return false; + + const buffer = child_process.execFileSync(uris[0].path, ["version"]); + const version = semver.parse(buffer.toString("utf8")); + if (!version) return false; + + await zigConfig.update("path", uris[0].path, true); + } else if (zigResponse === "Use Zig in PATH") { + await zigConfig.update("path", "", true); + } else return false; + + const zlsConfig = workspace.getConfiguration("zig.zls"); + const zlsResponse = await 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" + ); + + if (zlsResponse === "Install") { + await installZLS(context, false); + } else if (zlsResponse === "Specify path") { + const uris = await window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + title: "Select Zig Language Server (ZLS) executable", + }); + if (!uris) return true; + + const buffer = child_process.execFileSync(uris[0].path, ["--version"]); + const version = semver.parse(buffer.toString("utf8")); + if (!version) return true; + + await zlsConfig.update("path", uris[0].path, true); + } else if (zlsResponse === "Use ZLS in PATH") { + await zlsConfig.update("path", "", true); } - if (!shouldCheckUpdate(context, "zigUpdate")) {return;} - if (!configuration.get("checkForUpdate", true)) {return;} - await checkUpdate(context); + return true; } diff --git a/src/zigUtil.ts b/src/zigUtil.ts index d5c57e0..8bab7e3 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -1,48 +1,49 @@ import * as cp from "child_process"; import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; -import { window, workspace } from "vscode"; +import { ExtensionContext, window, workspace } from "vscode"; import which from "which"; export const isWindows = process.platform === "win32"; /** Options for execCmd */ export interface ExecCmdOptions { - /** The project root folder for this file is used as the cwd of the process */ - fileName?: string; - /** Any arguments */ - cmdArguments?: string[]; - /** Shows a message if an error occurs (in particular the command not being */ - /* found), instead of rejecting. If this happens, the promise never resolves */ - showMessageOnError?: boolean; - /** Called after the process successfully starts */ - onStart?: () => void; - /** Called when data is sent to stdout */ - onStdout?: (data: string) => void; - /** Called when data is sent to stderr */ - onStderr?: (data: string) => void; - /** Called after the command (successfully or unsuccessfully) exits */ - onExit?: () => void; - /** Text to add when command is not found (maybe helping how to install) */ - notFoundText?: string; + /** The project root folder for this file is used as the cwd of the process */ + fileName?: string; + /** Any arguments */ + cmdArguments?: string[]; + /** Shows a message if an error occurs (in particular the command not being */ + /* found), instead of rejecting. If this happens, the promise never resolves */ + showMessageOnError?: boolean; + /** Called after the process successfully starts */ + onStart?: () => void; + /** Called when data is sent to stdout */ + onStdout?: (data: string) => void; + /** Called when data is sent to stderr */ + onStderr?: (data: string) => void; + /** Called after the command (successfully or unsuccessfully) exits */ + onExit?: () => void; + /** Text to add when command is not found (maybe helping how to install) */ + notFoundText?: string; } /** Type returned from execCmd. Is a promise for when the command completes * and also a wrapper to access ChildProcess-like methods. */ export interface ExecutingCmd - extends Promise<{ stdout: string; stderr: string }> { - /** The process's stdin */ - stdin: NodeJS.WritableStream; - /** End the process */ - kill(); - /** Is the process running */ - isRunning: boolean; // tslint:disable-line + extends Promise<{ stdout: string; stderr: string }> { + /** The process's stdin */ + stdin: NodeJS.WritableStream; + /** End the process */ + kill(); + /** Is the process running */ + isRunning: boolean; // tslint:disable-line } /** Executes a command. Shows an error message if the command isn't found */ export function execCmd -(cmd: string, options: ExecCmdOptions = {}): ExecutingCmd { + (cmd: string, options: ExecCmdOptions = {}): ExecutingCmd { const { fileName, onStart, onStdout, onStderr, onExit } = options; let childProcess, firstResponse = true, wasKilledbyUs = false; @@ -52,7 +53,7 @@ export function execCmd const cmdArguments = options ? options.cmdArguments : []; childProcess = - cp.execFile(cmd, cmdArguments, { cwd: detectProjectRoot(fileName || workspace.rootPath + "/fakeFileName"), maxBuffer: 10 * 1024 * 1024 }, handleExit); + cp.execFile(cmd, cmdArguments, { cwd: detectProjectRoot(fileName || workspace.rootPath + "/fakeFileName"), maxBuffer: 10 * 1024 * 1024 }, handleExit); childProcess.stdout.on("data", (data: Buffer) => { @@ -85,11 +86,11 @@ export function execCmd if (options.showMessageOnError) { const cmdName = cmd.split(" ", 1)[0]; const cmdWasNotFound = - // Windows method apparently still works on non-English systems - (isWindows && - err.message.includes(`'${cmdName}' is not recognized`)) || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (!isWindows && (err).code === 127); + // Windows method apparently still works on non-English systems + (isWindows && + err.message.includes(`'${cmdName}' is not recognized`)) || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (!isWindows && (err).code === 127); if (cmdWasNotFound) { const notFoundText = options ? options.notFoundText : ""; @@ -147,15 +148,68 @@ export function detectProjectRoot(fileName: string): string { return undefined; } -export function getZigPath(): string { - const configuration = workspace.getConfiguration("zig"); - let zigPath = configuration.get("zigPath", null); - if (!zigPath) { - zigPath = which.sync("zig", { nothrow: true }); - if (!zigPath) { - window.showErrorMessage("zig not found in PATH"); - throw Error("zig not found in PATH"); +export function getExePath(exePath: string | null, exeName: string, optionName: string): string { + // Allow passing the ${workspaceFolder} predefined variable + // See https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables + if (exePath && exePath.includes("${workspaceFolder}")) { + // We choose the first workspaceFolder since it is ambiguous which one to use in this context + if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { + // older versions of Node (which VSCode uses) may not have String.prototype.replaceAll + exePath = exePath.replace(/\$\{workspaceFolder\}/gm, workspace.workspaceFolders[0].uri.fsPath); + } + } + + if (!exePath) { + exePath = which.sync(exeName, { nothrow: true }); + } else if (exePath.startsWith("~")) { + exePath = path.join(os.homedir(), exePath.substring(1)); + } else if (!path.isAbsolute(exePath)) { + exePath = which.sync(exePath, { nothrow: true }); + } + + 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 zigPath; + window.showErrorMessage(message); + throw Error(message); +} + +export function getZigPath(): string { + const configuration = workspace.getConfiguration("zig"); + const zigPath = configuration.get("path"); + return getExePath(zigPath, "zig", "zig.path"); +} + +// Check timestamp `key` to avoid automatically checking for updates +// more than once in an hour. +export function shouldCheckUpdate(context: ExtensionContext, key: string): boolean { + const HOUR = 60 * 60 * 1000; + const timestamp = new Date().getTime(); + const old = context.globalState.get(key); + if (old === undefined || timestamp - old < HOUR) return false; + context.globalState.update(key, timestamp); + return true; +} + +export function getHostZigName(): string { + let os: string = process.platform; + if (os == "darwin") os = "macos"; + if (os == "win32") os = "windows"; + let arch: string = process.arch; + if (arch == "ia32") arch = "x86"; + if (arch == "x64") arch = "x86_64"; + if (arch == "arm64") arch = "aarch64"; + if (arch == "ppc") arch = "powerpc"; + if (arch == "ppc64") arch = "powerpc64le"; + return `${arch}-${os}`; } diff --git a/src/zls.ts b/src/zls.ts index c5c4c2b..a6c4c23 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -5,116 +5,23 @@ import camelCase from "camelcase"; import * as child_process from "child_process"; import * as fs from "fs"; import mkdirp from "mkdirp"; -import * as os from "os"; -import * as path from "path"; -import semver from "semver"; +import semver, { SemVer } from "semver"; import * as vscode from "vscode"; import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node"; -import which from "which"; -import { shouldCheckUpdate } from "./extension"; -import { getZigPath } from "./zigUtil"; - -export let outputChannel: vscode.OutputChannel; -export let client: LanguageClient | null = null; - -export const downloadsRoot = "https://zig.pm/zls/downloads"; - -/* eslint-disable @typescript-eslint/naming-convention */ -export enum InstallationName { - x86_linux = "x86-linux", - x86_windows = "x86-windows", - x86_64_linux = "x86_64-linux", - x86_64_macos = "x86_64-macos", - x86_64_windows = "x86_64-windows", - arm_64_macos = "aarch64-macos", - arm_64_linux = "aarch64-linux", -} -/* eslint-enable @typescript-eslint/naming-convention */ - -export function getDefaultInstallationName(): InstallationName | null { - // NOTE: Not using a JS switch because they're very clunky :( - - const plat = process.platform; - const arch = process.arch; - if (arch === "ia32") { - if (plat === "linux") {return InstallationName.x86_linux;} - else if (plat === "win32") {return InstallationName.x86_windows;} - } else if (arch === "x64") { - if (plat === "linux") {return InstallationName.x86_64_linux;} - else if (plat === "darwin") {return InstallationName.x86_64_macos;} - else if (plat === "win32") {return InstallationName.x86_64_windows;} - } else if (arch === "arm64") { - if (plat === "darwin") {return InstallationName.arm_64_macos;} - if (plat === "linux") {return InstallationName.arm_64_linux;} - } - - return null; -} - -export async function installExecutable(context: ExtensionContext): Promise { - const def = getDefaultInstallationName(); - if (!def) { - window.showInformationMessage("Your system isn\"t built by our CI!\nPlease follow the instructions [here](https://github.com/zigtools/zls#from-source) to get started!"); - return null; - } - - return window.withProgress({ - title: "Installing zls...", - location: vscode.ProgressLocation.Notification, - }, async progress => { - progress.report({ message: "Downloading zls executable..." }); - const exe = (await axios.get(`${downloadsRoot}/${def}/bin/zls${def.endsWith("windows") ? ".exe" : ""}`, { - responseType: "arraybuffer" - })).data; +import { execCmd, getExePath, getHostZigName, getZigPath, isWindows, shouldCheckUpdate } from "./zigUtil"; - progress.report({ message: "Installing..." }); - const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zls_install"); - if (!fs.existsSync(installDir.fsPath)) {mkdirp.sync(installDir.fsPath);} +let outputChannel: vscode.OutputChannel; +let client: LanguageClient | null = null; - const zlsBinPath = vscode.Uri.joinPath(installDir, `zls${def.endsWith("windows") ? ".exe" : ""}`).fsPath; - const zlsBinTempPath = zlsBinPath + ".tmp"; - - // Create a new executable file. - // Do not update the existing file in place, to avoid code-signing crashes on macOS. - // https://developer.apple.com/documentation/security/updating_mac_software - fs.writeFileSync(zlsBinTempPath, exe, "binary"); - fs.chmodSync(zlsBinTempPath, 0o755); - if (fs.existsSync(zlsBinPath)) {fs.rmSync(zlsBinPath);} - fs.renameSync(zlsBinTempPath, zlsBinPath); - - const config = workspace.getConfiguration("zig.zls"); - await config.update("path", zlsBinPath, true); - - return zlsBinPath; - }); -} - -export async function checkUpdateMaybe(context: ExtensionContext) { - const configuration = workspace.getConfiguration("zig.zls"); - const checkForUpdate = configuration.get("checkForUpdate", true); - if (checkForUpdate) { - try { - await checkUpdate(context, true); - } catch (err) { - outputChannel.appendLine(`Failed to check for update. Reason: ${err.message}`); - } - } -} - -export async function startClient(context: ExtensionContext) { +async function startClient() { const configuration = workspace.getConfiguration("zig.zls"); const debugLog = configuration.get("debugLog", false); - const zlsPath = await getZLSPath(context); - - if (!zlsPath) { - promptAfterFailure(context); - return null; - } + const zlsPath = getZLSPath(); const serverOptions: ServerOptions = { command: zlsPath, @@ -133,7 +40,7 @@ export async function startClient(context: ExtensionContext) { for (const [index, param] of Object.entries(params.items)) { if (param.section === "zls.zig_exe_path") { - param.section = "zig.zigPath"; + param.section = "zig.path"; indexOfZigPath = index; } else if (param.section === "zls.enable_ast_check_diagnostics") { indexOfAstCheck = index; @@ -145,14 +52,10 @@ export async function startClient(context: ExtensionContext) { const result = await next(params, token); if (indexOfAstCheck !== null) { - result[indexOfAstCheck] = workspace.getConfiguration("zig").get("astCheckProvider", "zls") === "zls"; + result[indexOfAstCheck] = workspace.getConfiguration("zig").get("astCheckProvider") === "zls"; } if (indexOfZigPath !== null) { - try { - result[indexOfZigPath] = getZigPath(); - } catch { - result[indexOfZigPath] = "zig"; - } + result[indexOfZigPath] = getZigPath(); } return result; @@ -173,261 +76,216 @@ export async function startClient(context: ExtensionContext) { window.showWarningMessage(`Failed to run Zig Language Server (ZLS): ${reason}`); client = null; }).then(() => { - if (workspace.getConfiguration("zig").get("formattingProvider", "zls") !== "zls") - {client.getFeature("textDocument/formatting").dispose();} + if (workspace.getConfiguration("zig").get("formattingProvider") !== "zls") { + client.getFeature("textDocument/formatting").dispose(); + } }); } -export async function stopClient(): Promise { - if (client) {client.stop();} +export async function stopClient() { + if (client) client.stop(); client = null; } -export async function promptAfterFailure(context: ExtensionContext): Promise { +// returns the file system path to the zls executable +export function getZLSPath(): string { const configuration = workspace.getConfiguration("zig.zls"); - const response = await window.showWarningMessage("Couldn't find Zig Language Server (ZLS) executable", - "Install", "Specify path", "Use ZLS in PATH", "Disable" - ); + const zlsPath = configuration.get("path"); + return getExePath(zlsPath, "zls", "zig.zls.path"); +} - if (response === "Install") { - return await installExecutable(context); - } else if (response === "Specify path") { - const uris = await window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig Language Server (ZLS) executable", - }); - - if (uris) { - await configuration.update("path", uris[0].fsPath, true); - return uris[0].fsPath; - } - } else if (response === "Use ZLS in PATH") { - await configuration.update("path", "", true); - } else { - await configuration.update("enabled", false, true); - } +const downloadsRoot = "https://zigtools-releases.nyc3.digitaloceanspaces.com/zls"; - return null; +interface Version { + date: string, + builtWithZigVersion: string, + zlsVersion: string, + zlsMinimumBuildVersion: string, + commit: string, + targets: string[], } -// returns the file system path to the zls executable -export async function getZLSPath(context: ExtensionContext): Promise { - const configuration = workspace.getConfiguration("zig.zls"); - let zlsPath = configuration.get("path", null); - - // Allow passing the ${workspaceFolder} predefined variable - // See https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables - if (zlsPath && zlsPath.includes("${workspaceFolder}")) { - // We choose the first workspaceFolder since it is ambiguous which one to use in this context - if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { - // older versions of Node (which VSCode uses) may not have String.prototype.replaceAll - zlsPath = zlsPath.replace(/\$\{workspaceFolder\}/gm, workspace.workspaceFolders[0].uri.fsPath); - } - } +interface VersionIndex { + latest: string, + latestTagged: string, + releases: Record, + versions: Record, +} - if (!zlsPath) { - zlsPath = which.sync("zls", { nothrow: true }); - } else if (zlsPath.startsWith("~")) { - zlsPath = path.join(os.homedir(), zlsPath.substring(1)); - } else if (!path.isAbsolute(zlsPath)) { - zlsPath = which.sync(zlsPath, { nothrow: true }); +async function getVersionIndex(): Promise { + const index = (await axios.get(`${downloadsRoot}/index.json`)).data; + if (!index.versions[index.latest]) { + window.showErrorMessage("Invalid ZLS version index; please contact a ZLS maintainer."); + throw "Invalid ZLS version"; } + return index; +} - let message: string | null = null; +// checks whether there is newer version on master +async function checkUpdate(context: ExtensionContext) { + const configuration = 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 zlsPathExists = zlsPath !== null && fs.existsSync(zlsPath); - if (zlsPath && zlsPathExists) { - try { - fs.accessSync(zlsPath, fs.constants.R_OK | fs.constants.X_OK); - } catch { - message = `\`zls.path\` ${zlsPath} is not an executable`; - } - const stat = fs.statSync(zlsPath); - if (!stat.isFile()) { - message = `\`zls.path\` ${zlsPath} is not a file`; - } - } + // get current version + const buffer = child_process.execFileSync(zlsPath, ["--version"]); + const version = semver.parse(buffer.toString("utf8")); + if (!version) return; - if (message === null) { - if (!zlsPath) { - return null; - } else if (!zlsPathExists) { - if (await isZLSPrebuildBinary(context)) { - return null; - } - message = `Couldn't find Zig Language Server (ZLS) executable at "${zlsPath.replace(/"/gm, "\\\"")}"`; - } - } + const index = await getVersionIndex(); + // having a build number implies nightly version + const latestVersion = version.build.length === 0 ? + semver.parse(index.latestTagged) : semver.parse(index.latest); - if (message) { - await window.showErrorMessage(message); - return null; - } + if (semver.gte(version, latestVersion)) return; - return zlsPath; + const response = await window.showInformationMessage(`New version of ZLS available`, "Install", "Ignore"); + if (response === "Install") { + await installVersion(context, latestVersion); + } } -export async function checkUpdate(context: ExtensionContext, autoInstallPrebuild: boolean): Promise { - const configuration = workspace.getConfiguration("zig.zls"); - - const zlsPath = await getZLSPath(context); - if (!zlsPath) {return;} +export async function install(context: ExtensionContext, ask: boolean) { + const path = getZigPath(); - if (!await isUpdateAvailable(zlsPath)) {return;} - - const isPrebuild = await isZLSPrebuildBinary(context); - - if (autoInstallPrebuild && isPrebuild) { - await installExecutable(context); - } else { - const message = `There is a new update available for ZLS. ${!isPrebuild ? "It would replace your installation with a prebuilt binary." : ""}`; - const response = await window.showInformationMessage(message, "Install update", "Never ask again"); + const buffer = child_process.execFileSync(path, ["version"]); + let zigVersion = semver.parse(buffer.toString("utf8")); + // Zig 0.9.0 was the first version to have a tagged zls release + const zlsConfiguration = workspace.getConfiguration("zig.zls", null); + if (semver.lt(zigVersion, "0.9.0")) { + if (zlsConfiguration.get("path")) { + window.showErrorMessage(`ZLS is not available for Zig version ${zigVersion}`); + } + await zlsConfiguration.update("path", undefined); + return; + } - if (response === "Install update") { - await installExecutable(context); - } else if (response === "Never ask again") { - await configuration.update("checkForUpdate", false, true); + if (ask) { + const result = await window.showInformationMessage( + `Do you want to install ZLS (the Zig Language Server) for Zig version ${zigVersion}`, + "Install", "Ignore" + ); + if (result === "Ignore") { + await zlsConfiguration.update("path", undefined); + return; } } + let zlsVersion; + if (zigVersion.build.length !== 0) { + // Nightly, install latest ZLS + zlsVersion = semver.parse((await getVersionIndex()).latest); + } else { + // ZLS does not make releases for patches + zlsVersion = zigVersion; + zlsVersion.patch = 0; + } + try { + await installVersion(context, zlsVersion); + } catch (err) { + window.showErrorMessage(`Unable to install ZLS ${zlsVersion} for Zig version ${zigVersion}: ${err}`); + } } -// checks whether zls has been installed with `installExecutable` -export async function isZLSPrebuildBinary(context: ExtensionContext): Promise { - const configuration = workspace.getConfiguration("zig.zls"); - const zlsPath = configuration.get("path", null); - if (!zlsPath) {return false;} +async function installVersion(context: ExtensionContext, version: SemVer) { + const hostName = getHostZigName(); - const zlsBinPath = vscode.Uri.joinPath(context.globalStorageUri, "zls_install", "zls").fsPath; - return zlsPath.startsWith(zlsBinPath); -} - -// checks whether there is newer version on master -export async function isUpdateAvailable(zlsPath: string): Promise { - // get current version - const buffer = child_process.execFileSync(zlsPath, ["--version"]); - const version = semver.parse(buffer.toString("utf8")); - if (!version) {return null;} - - // compare version triple if commit id is available - if (version.prerelease.length === 0 || version.build.length === 0) { - // get latest tagged version - const tagsResponse = await axios.get("https://api.github.com/repos/zigtools/zls/tags"); - const latestVersion = tagsResponse.data[0].name; - return semver.gt(latestVersion, version); - } + await window.withProgress({ + title: "Installing zls...", + location: vscode.ProgressLocation.Notification, + }, async progress => { + const installDir = vscode.Uri.joinPath(context.globalStorageUri, "zls_install"); + if (fs.existsSync(installDir.fsPath)) fs.rmSync(installDir.fsPath, { recursive: true, force: true }); + mkdirp.sync(installDir.fsPath); - const response = await axios.get("https://api.github.com/repos/zigtools/zls/commits/master"); - const masterHash: string = response.data.sha; + const binName = `zls${isWindows ? ".exe" : ""}`; + const zlsBinPath = vscode.Uri.joinPath(installDir, binName).fsPath; - const isMaster = masterHash.startsWith(version.build[0]); + progress.report({ message: "Downloading ZLS executable..." }); + let exe: Buffer; + try { + exe = (await axios.get(`${downloadsRoot}/${version.raw}/${hostName}/zls${isWindows ? ".exe" : ""}`, { + responseType: "arraybuffer" + })).data; + } catch (err) { + // Missing prebuilt binary is reported as AccessDenied + if (err.response.status == 403) { + window.showErrorMessage(`A prebuilt ZLS ${version} binary is not available for your system. You can build it yourself with https://github.com/zigtools/zls#from-source`); + return; + } + throw err; + } + fs.writeFileSync(zlsBinPath, exe, "binary"); + fs.chmodSync(zlsBinPath, 0o755); - return !isMaster; + let config = workspace.getConfiguration("zig.zls"); + await config.update("path", zlsBinPath, true); + }); } -export async function openConfig(context: ExtensionContext): Promise { - const zlsPath = await getZLSPath(context); - if (!zlsPath) {return;} - +async function openConfig() { + const zlsPath = getZLSPath(); const buffer = child_process.execFileSync(zlsPath, ["--show-config-path"]); const path: string = buffer.toString("utf8").trimEnd(); await vscode.window.showTextDocument(vscode.Uri.file(path), { preview: false }); } -function isEnabled(): boolean { - return workspace.getConfiguration("zig.zls", null).get("enabled", true); +function checkInstalled(): boolean { + const zlsPath = workspace.getConfiguration("zig.zls").get("path"); + if (!zlsPath) window.showErrorMessage("This command cannot be run without setting 'zig.zls.path'.", { modal: true }); + return !!zlsPath; } -const zlsDisabledMessage = "zls is not enabled; if you'd like to enable it, please set 'zig.zls.enabled' to true."; export async function activate(context: ExtensionContext) { outputChannel = window.createOutputChannel("Zig Language Server"); vscode.commands.registerCommand("zig.zls.install", async () => { - if (!isEnabled()) { - window.showErrorMessage(zlsDisabledMessage); + const zigPath = workspace.getConfiguration("zig").get("path"); + if (zigPath === null) { + window.showErrorMessage("This command cannot be run without setting 'zig.path'.", { modal: true }); return; } await stopClient(); - await installExecutable(context); + await install(context, true); }); vscode.commands.registerCommand("zig.zls.stop", async () => { - if (!isEnabled()) { - window.showErrorMessage(zlsDisabledMessage); - return; - } + if (!checkInstalled()) return; await stopClient(); }); vscode.commands.registerCommand("zig.zls.startRestart", async () => { - if (!isEnabled()) { - window.showErrorMessage(zlsDisabledMessage); - return; - } + if (!checkInstalled()) return; await stopClient(); - await checkUpdateMaybe(context); - await startClient(context); + await startClient(); }); vscode.commands.registerCommand("zig.zls.openconfig", async () => { - if (!isEnabled()) { - window.showErrorMessage(zlsDisabledMessage); - return; - } + if (!checkInstalled()) return; - await openConfig(context); + await openConfig(); }); vscode.commands.registerCommand("zig.zls.update", async () => { - if (!isEnabled()) { - window.showErrorMessage(zlsDisabledMessage); - return; - } + if (!checkInstalled()) return; await stopClient(); - await checkUpdate(context, false); - await startClient(context); + await checkUpdate(context); + await startClient(); }); - if (!isEnabled()) - {return;} - - const configuration = workspace.getConfiguration("zig.zls", null); - if (!configuration.get("path", null)) { - const response = await window.showInformationMessage( - "We recommend enabling ZLS (the Zig Language Server) for a better editing experience. Would you like to install it? You can always change this later by modifying `zig.zls.enabled` in your settings.", - "Install", "Specify path", "Use ZLS in PATH", "Disable" - ); - - if (response === "Install") { - await configuration.update("enabled", true, true); - await installExecutable(context); - } else if (response === "Specify path") { - await configuration.update("enabled", true, true); - const uris = await window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - title: "Select Zig Language Server (ZLS) executable", - }); - - if (uris) { - await configuration.update("path", uris[0].fsPath, true); - } - } else { - await configuration.update("enabled", response === "Use ZLS in PATH", true); - } - } - - if (shouldCheckUpdate(context, "zlsUpdate")) { - await checkUpdateMaybe(context); + const zigConfig = vscode.workspace.getConfiguration("zig"); + if (zigConfig.get("path") === null) return; + const zlsConfig = workspace.getConfiguration("zig.zls"); + if (zlsConfig.get("path") === null) return; + if (zlsConfig.get("checkForUpdate") && shouldCheckUpdate(context, "zlsUpdate")) { + await checkUpdate(context); } - await startClient(context); + await startClient(); } export function deactivate(): Thenable {