diff --git a/package-lock.json b/package-lock.json index c3839af..ebff43f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.6.11", "license": "MIT", "dependencies": { - "camelcase": "^7.0.1", "libsodium-wrappers": "^0.7.15", "lodash-es": "^4.17.21", "semver": "^7.5.2", @@ -1224,17 +1223,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", diff --git a/package.json b/package.json index ab1e129..7eeb4af 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/zigDiagnosticsProvider.ts b/src/zigDiagnosticsProvider.ts index f6b9b99..29576e6 100644 --- a/src/zigDiagnosticsProvider.ts +++ b/src/zigDiagnosticsProvider.ts @@ -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 { // } diff --git a/src/zigUtil.ts b/src/zigUtil.ts index 5ba3bab..c554e9a 100644 --- a/src/zigUtil.ts +++ b/src/zigUtil.ts @@ -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; @@ -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)); diff --git a/src/zls.ts b/src/zls.ts index e8228c0..177d7a0 100644 --- a/src/zls.ts +++ b/src/zls.ts @@ -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"; @@ -165,105 +163,137 @@ async function getZLSPath(context: vscode.ExtensionContext): Promise<{ exe: stri }; } -async function configurationMiddleware( - params: ConfigurationParams, - token: CancellationToken, - next: RequestHandler, -): Promise { - const optionIndices: Record = {}; - - 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 = {}; + 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>("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>(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 { + const configuration = vscode.workspace.getConfiguration("zig.zls", null); const additionalOptions = configuration.get>("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[]; } /**