Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,6 @@
"typescript-eslint": "^8.0.0"
},
"dependencies": {
"camelcase": "^7.0.1",
"libsodium-wrappers": "^0.7.15",
"lodash-es": "^4.17.21",
"semver": "^7.5.2",
Expand Down
2 changes: 1 addition & 1 deletion src/zigDiagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export default class ZigDiagnosticsProvider {
if (!buildFilePath) break;
processArg.push("--build-file");
try {
processArg.push(path.resolve(handleConfigOption(buildFilePath)));
processArg.push(path.resolve(handleConfigOption(buildFilePath, workspaceFolder)));
} catch {
//
}
Expand Down
16 changes: 11 additions & 5 deletions src/zigUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ import which from "which";
* 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 {
export function handleConfigOption(input: string, workspaceFolder: vscode.WorkspaceFolder | "none" | "guess"): string {
if (input.includes("${userHome}")) {
input = input.replaceAll("${userHome}", os.homedir());
}

if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
input = input.replaceAll("${workspaceFolder}", vscode.workspace.workspaceFolders[0].uri.fsPath);
input = input.replaceAll("${workspaceFolderBasename}", vscode.workspace.workspaceFolders[0].name);
if (workspaceFolder === "guess") {
workspaceFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : "none";
}

if (workspaceFolder !== "none") {
input = input.replaceAll("${workspaceFolder}", workspaceFolder.uri.fsPath);
input = input.replaceAll("${workspaceFolderBasename}", workspaceFolder.name);
} else {
// This may end up reporting a confusing error message.
}

const document = vscode.window.activeTextEditor?.document;
Expand Down Expand Up @@ -68,7 +74,7 @@ export function resolveExePathAndVersion(
assert(cmd.length);

// allow passing predefined variables
cmd = handleConfigOption(cmd);
cmd = handleConfigOption(cmd, "guess");

if (cmd.startsWith("~")) {
cmd = path.join(os.homedir(), cmd.substring(1));
Expand Down
206 changes: 118 additions & 88 deletions src/zls.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import vscode from "vscode";

import {
CancellationToken,
ConfigurationParams,
LSPAny,
LanguageClient,
LanguageClientOptions,
RequestHandler,
ResponseError,
ServerOptions,
} from "vscode-languageclient/node";
import camelCase from "camelcase";
import { camelCase, snakeCase } from "lodash-es";
import semver from "semver";

import * as minisign from "./minisign";
Expand Down Expand Up @@ -165,105 +163,137 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri
};
}

async function configurationMiddleware(
params: ConfigurationParams,
token: CancellationToken,
next: RequestHandler<ConfigurationParams, LSPAny[], void>,
): Promise<LSPAny[] | ResponseError> {
const optionIndices: Record<string, number | undefined> = {};

params.items.forEach((param, index) => {
if (param.section) {
if (param.section === "zls.zig_exe_path") {
param.section = "zig.path";
} else {
param.section = `zig.zls.${camelCase(param.section.slice(4))}`;
}
optionIndices[param.section] = index;
}
});
function configurationMiddleware(params: ConfigurationParams): LSPAny[] | ResponseError {
void validateAdditionalOptions();
return params.items.map((param) => {
if (!param.section) return null;

const result = await next(params, token);
if (result instanceof ResponseError) {
return result;
}
const scopeUri = param.scopeUri ? client?.protocol2CodeConverter.asUri(param.scopeUri) : undefined;
const configuration = vscode.workspace.getConfiguration("zig", scopeUri);
const workspaceFolder = scopeUri ? vscode.workspace.getWorkspaceFolder(scopeUri) : undefined;

const configuration = vscode.workspace.getConfiguration("zig.zls");
const updateConfigOption = (section: string, value: unknown) => {
if (section === "zls.zigExePath") {
return zigProvider.getZigPath();
}

for (const name in optionIndices) {
const index = optionIndices[name] as unknown as number;
const section = name.slice("zig.zls.".length);
const configValue = configuration.get(section);
if (typeof configValue === "string") {
// Make sure that `""` gets converted to `null` and resolve predefined values
result[index] = configValue ? handleConfigOption(configValue) : null;
}
if (typeof value === "string") {
// Make sure that `""` gets converted to `undefined` and resolve predefined values
value = value ? handleConfigOption(value, workspaceFolder ?? "guess") : undefined;
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
// Recursively update the config options
const newValue: Record<string, unknown> = {};
for (const [fieldName, fieldValue] of Object.entries(value)) {
newValue[snakeCase(fieldName)] = updateConfigOption(section + "." + fieldName, fieldValue);
}
return newValue;
}

const inspect = configuration.inspect(section);
const isDefaultValue =
configValue === inspect?.defaultValue &&
inspect?.globalValue === undefined &&
inspect?.workspaceValue === undefined &&
inspect?.workspaceFolderValue === undefined;
if (isDefaultValue) {
if (name === "zig.zls.semanticTokens") {
// The extension has a different default value for this config
// option compared to ZLS
continue;
const inspect = configuration.inspect(section);
const isDefaultValue =
value === inspect?.defaultValue &&
inspect?.globalValue === undefined &&
inspect?.workspaceValue === undefined &&
inspect?.workspaceFolderValue === undefined;

if (isDefaultValue) {
if (section === "zls.semanticTokens") {
// The extension has a different default value for this config
// option compared to ZLS
return value;
} else {
return undefined;
}
}
result[index] = null;
}
}
return value;
};

const indexOfZigPath = optionIndices["zig.path"];
if (indexOfZigPath !== undefined) {
result[indexOfZigPath] = zigProvider.getZigPath();
}
let additionalOptions = configuration.get<Record<string, unknown>>("zls.additionalOptions", {});

// Remove the `zig.zls.` prefix from the entries in `zig.zls.additionalOptions`
additionalOptions = Object.fromEntries(
Object.entries(additionalOptions)
.filter(([key]) => key.startsWith("zig.zls."))
.map(([key, value]) => [key.slice("zig.zls.".length), value]),
);

if (param.section === "zls") {
// ZLS has requested all config options.

const options = { ...configuration.get<Record<string, unknown>>(param.section, {}) };
// Some config options are specific to the VS Code
// extension. ZLS should ignore unknown values but
// we remove them here anyway.
delete options["debugLog"]; // zig.zls.debugLog
delete options["trace"]; // zig.zls.trace.server
delete options["enabled"]; // zig.zls.enabled
delete options["path"]; // zig.zls.path
delete options["additionalOptions"]; // zig.zls.additionalOptions

return updateConfigOption(param.section, {
...additionalOptions,
...options,
// eslint-disable-next-line @typescript-eslint/naming-convention
zig_exe_path: zigProvider.getZigPath(),
});
} else if (param.section.startsWith("zls.")) {
// ZLS has requested a specific config option.

// ZLS names it's config options in snake_case but the VS Code extension uses camelCase
const camelCaseSection = param.section
.split(".")
.map((str) => camelCase(str))
.join(".");

return updateConfigOption(
camelCaseSection,
configuration.get(camelCaseSection, additionalOptions[camelCaseSection.slice("zls.".length)]),
);
} else {
// Do not allow ZLS to request other editor config options.
return null;
}
});
}

async function validateAdditionalOptions(): Promise<void> {
const configuration = vscode.workspace.getConfiguration("zig.zls", null);
const additionalOptions = configuration.get<Record<string, unknown>>("additionalOptions", {});

for (const optionName in additionalOptions) {
if (!optionName.startsWith("zig.zls.")) continue;
const section = optionName.slice("zig.zls.".length);

const doesOptionExist = configuration.inspect(section)?.defaultValue !== undefined;
if (doesOptionExist) {
// The extension has defined a config option with the given name but the user still used `additionalOptions`.
const response = await vscode.window.showWarningMessage(
`The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`,
`Use ${optionName} instead`,
"Show zig.zls.additionalOptions",
);
switch (response) {
case `Use ${optionName} instead`:
const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions;
await workspaceConfigUpdateNoThrow(
configuration,
"additionalOptions",
updatedAdditionalOptions,
true,
);
await workspaceConfigUpdateNoThrow(configuration, section, newValue, true);
break;
case "Show zig.zls.additionalOptions":
await vscode.commands.executeCommand("workbench.action.openSettingsJson", {
revealSetting: { key: "zig.zls.additionalOptions" },
});
continue;
case undefined:
continue;
}
}

const optionIndex = optionIndices[optionName];
if (!optionIndex) {
// ZLS has not requested a config option with the given name.
continue;
const inspect = configuration.inspect(section);
const doesOptionExist = inspect?.defaultValue !== undefined;
if (!doesOptionExist) continue;

// The extension has defined a config option with the given name but the user still used `additionalOptions`.
const response = await vscode.window.showWarningMessage(
`The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`,
`Use ${optionName} instead`,
"Show zig.zls.additionalOptions",
);
switch (response) {
case `Use ${optionName} instead`:
const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions;
await workspaceConfigUpdateNoThrow(
configuration,
"additionalOptions",
Object.keys(updatedAdditionalOptions).length ? updatedAdditionalOptions : undefined,
true,
);
await workspaceConfigUpdateNoThrow(configuration, section, newValue, true);
break;
case "Show zig.zls.additionalOptions":
await vscode.commands.executeCommand("workbench.action.openSettingsJson", {
revealSetting: { key: "zig.zls.additionalOptions" },
});
break;
case undefined:
return;
}

result[optionIndex] = additionalOptions[optionName];
}

return result as unknown[];
}

/**
Expand Down