Skip to content

Commit

Permalink
remove the initial setup with automatic Zig version management
Browse files Browse the repository at this point in the history
This commit replaces the initial setup with the following mechanism:
1. If the workspace contains a `.zigversion`, install the given Zig version.
2. If the workspace contains a `build.zig.zon` with a `minimum_zig_version`, install the next available Zig version.
3. Check if the "Install Zig" has been previously executed in the active workspace. If so, install that version.
3. Otherwise fallback to the latest tagged release of Zig.

Some parts of this are not fully implemented.

fixes #111
  • Loading branch information
Techatrix committed Sep 7, 2024
1 parent 9d87b9e commit ed33ca0
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 174 deletions.
5 changes: 0 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions src/zigDiagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,7 +90,8 @@ export default class ZigDiagnosticsProvider {
if (textDocument.languageId !== "zig") {
return;
}
const zigPath = getZigPath();
const zigPath = zigProvider.getZigPath();
if (!zigPath) return;

const { error, stderr } = childProcess.spawnSync(zigPath, ["ast-check"], {
input: textDocument.getText(),
Expand Down Expand Up @@ -132,7 +134,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;

const buildOption = config.get<string>("buildOption", "build");
const processArg: string[] = [buildOption];
Expand Down
52 changes: 29 additions & 23 deletions src/zigFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,45 @@ import util from "util";

import * as zls from "./zls";
import { DocumentRangeFormattingRequest, TextDocumentIdentifier } from "vscode-languageclient";
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<string>("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<string>("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();
},
};
Expand All @@ -52,7 +54,10 @@ function preCompileZigFmt() {
// This pre-compiles even if "zig.formattingProvider" is "zls".
if (vscode.workspace.getConfiguration("zig").get<string>("formattingProvider") === "off") return;

childProcess.execFile(getZigPath(), ["fmt", "--help"], {
const zigPath = zigProvider.getZigPath();
if (!zigPath) return null;

childProcess.execFile(zigPath, ["fmt", "--help"], {
timeout: 60000, // 60 seconds (this is a very high value because 'zig fmt' is just in time compiled)
});
}
Expand All @@ -77,7 +82,8 @@ async function provideDocumentRangeFormattingEdits(
}
}

const zigPath = getZigPath();
const zigPath = zigProvider.getZigPath();
if (!zigPath) return null;

const abortController = new AbortController();
token.onCancellationRequested(() => {
Expand Down
69 changes: 69 additions & 0 deletions src/zigProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import vscode from "vscode";

import semver from "semver";

import { resolveExePathAndVersion } from "./zigUtil";

export interface ExeWithVersion {
exe: string;
version: semver.SemVer;
}

export class ZigProvider implements vscode.Disposable {
onChange: vscode.EventEmitter<ExeWithVersion | null> = new vscode.EventEmitter();
private value: ExeWithVersion | null = null;
private disposables: vscode.Disposable[];

constructor() {
this.disposables = [
vscode.workspace.onDidChangeConfiguration((change) => {
if (change.affectsConfiguration("zig.path")) {
this.set(this.getZig());
}
}),
];
}

/** Returns the path and version of the Zig executable that is currently being used. */
public getZig(): ExeWithVersion | null {
const configuration = vscode.workspace.getConfiguration("zig");
const zigPath = configuration.get<string>("path", "");
if (!!zigPath) {
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;
}

return this.value;
}

/** Returns the version of the Zig executable that is currently being used. */
public getZigVersion(): semver.SemVer | null {
const result = this.getZig();
if (!result) return null;
return result.version;
}

/** Returns the path to the Zig executable that is currently being used. */
public getZigPath(): string | null {
const result = this.getZig();
if (!result) return null;
return result.exe;
}

/** 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);
}

dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
}
}
146 changes: 65 additions & 81 deletions src/zigSetup.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import path from "path";
import vscode from "vscode";

import path from "path";
import semver from "semver";
import vscode from "vscode";

import { ZigVersion, getHostZigName, getVersion, getVersionIndex, getZigPath } from "./zigUtil";
import {
ZigVersion,
getHostZigName,
getLatestTaggedZigVersion,
getVersionIndex,
getWorkspaceZigVersion,
} from "./zigUtil";
import { VersionManager } from "./versionManager";
import { restartClient } from "./zls";
import { ZigProvider } from "./zigProvider";

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.`,
);

void restartClient(context);
export async function installZig(
context: vscode.ExtensionContext,
/** The Zig Version that should be installed. */
version: semver.SemVer,
) {
try {
const exePath = await versionManager.install(version);
zigProvider.set({ exe: exePath, version: version });
await context.workspaceState.update("zig-version", version.raw);
} catch {
void vscode.window.showErrorMessage(`Failed to install Zig ${version.toString()}!`);
return null;
}
}

async function getVersions(): Promise<ZigVersion[]> {
Expand Down Expand Up @@ -73,6 +82,10 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
for (const option of available) {
if (option.name === selection.label) {
await installZig(context, option.version);

void vscode.window.showInformationMessage(
`Zig ${option.version.toString()} has been installed successfully. Relaunch your integrated terminal to make it available.`,
);
return;
}
}
Expand All @@ -85,99 +98,70 @@ async function selectVersionAndInstall(context: vscode.ExtensionContext) {
}
}

function updateZigEnvironmentVariableCollection(context: vscode.ExtensionContext) {
try {
const zigPath = getZigPath();
const envValue = path.delimiter + path.dirname(zigPath);
async function getWantedZigVersion(context: vscode.ExtensionContext): Promise<semver.SemVer | null> {
{
// TODO There should be a way to fallback to `getWorkspaceZigVersion`
const wantedZigVersion = context.workspaceState.get<string>("zig-version");
if (wantedZigVersion) return new semver.SemVer(wantedZigVersion);
}

// Supporting multiple workspaces is significantly more complex so we just look for the first workspace.
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
const wantedZigVersion = await getWorkspaceZigVersion(vscode.workspace.workspaceFolders[0].uri);
if (wantedZigVersion) return wantedZigVersion;
}

return await getLatestTaggedZigVersion();
}

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");
}
}

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<boolean>("initialSetupDone", false);
const zigPath = zigConfig.get<string>("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<boolean | null>("enable", null) === null) {
const zlsPath = zlsConfig.get<string>("path");
if (zlsPath === "" && initialSetupDone) {
await zlsConfig.update("path", "zls", true);
const zlsPath = zlsConfig.get<string>("path", "");
if (zlsPath.startsWith(context.globalStorageUri.fsPath)) {
await zlsConfig.update("path", undefined, true);
}
}
}

versionManager = new VersionManager("zig", vscode.Uri.joinPath(context.globalStorageUri, "zig"));

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);
}),
);

const configuration = vscode.workspace.getConfiguration("zig");
if (!configuration.get<boolean>("initialSetupDone")) {
await configuration.update("initialSetupDone", await initialSetup(context), true);
const wantedZigVersion = await getWantedZigVersion(context);
if (wantedZigVersion) {
await installZig(context, wantedZigVersion);
} else {
zigProvider.set(null);
}
}

async function initialSetup(context: vscode.ExtensionContext): Promise<boolean> {
const zigConfig = vscode.workspace.getConfiguration("zig");
if (!!zigConfig.get<string>("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<string>("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;
}
Loading

0 comments on commit ed33ca0

Please sign in to comment.