Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

Commit 3f56584

Browse files
authored
Merge pull request #442 from ziglang/techatrix/zls-config-middleware
address various issues with the ZLS config middleware
2 parents 7353e66 + d881ff7 commit 3f56584

File tree

5 files changed

+130
-107
lines changed

5 files changed

+130
-107
lines changed

package-lock.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,6 @@
412412
"typescript-eslint": "^8.0.0"
413413
},
414414
"dependencies": {
415-
"camelcase": "^7.0.1",
416415
"libsodium-wrappers": "^0.7.15",
417416
"lodash-es": "^4.17.21",
418417
"semver": "^7.5.2",

src/zigDiagnosticsProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export default class ZigDiagnosticsProvider {
160160
if (!buildFilePath) break;
161161
processArg.push("--build-file");
162162
try {
163-
processArg.push(path.resolve(handleConfigOption(buildFilePath)));
163+
processArg.push(path.resolve(handleConfigOption(buildFilePath, workspaceFolder)));
164164
} catch {
165165
//
166166
}

src/zigUtil.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@ import which from "which";
1414
* Replace any references to predefined variables in config string.
1515
* https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables
1616
*/
17-
export function handleConfigOption(input: string): string {
17+
export function handleConfigOption(input: string, workspaceFolder: vscode.WorkspaceFolder | "none" | "guess"): string {
1818
if (input.includes("${userHome}")) {
1919
input = input.replaceAll("${userHome}", os.homedir());
2020
}
2121

22-
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
23-
input = input.replaceAll("${workspaceFolder}", vscode.workspace.workspaceFolders[0].uri.fsPath);
24-
input = input.replaceAll("${workspaceFolderBasename}", vscode.workspace.workspaceFolders[0].name);
22+
if (workspaceFolder === "guess") {
23+
workspaceFolder = vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : "none";
24+
}
25+
26+
if (workspaceFolder !== "none") {
27+
input = input.replaceAll("${workspaceFolder}", workspaceFolder.uri.fsPath);
28+
input = input.replaceAll("${workspaceFolderBasename}", workspaceFolder.name);
29+
} else {
30+
// This may end up reporting a confusing error message.
2531
}
2632

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

7076
// allow passing predefined variables
71-
cmd = handleConfigOption(cmd);
77+
cmd = handleConfigOption(cmd, "guess");
7278

7379
if (cmd.startsWith("~")) {
7480
cmd = path.join(os.homedir(), cmd.substring(1));

src/zls.ts

Lines changed: 118 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import vscode from "vscode";
22

33
import {
4-
CancellationToken,
54
ConfigurationParams,
65
LSPAny,
76
LanguageClient,
87
LanguageClientOptions,
9-
RequestHandler,
108
ResponseError,
119
ServerOptions,
1210
} from "vscode-languageclient/node";
13-
import camelCase from "camelcase";
11+
import { camelCase, snakeCase } from "lodash-es";
1412
import semver from "semver";
1513

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

168-
async function configurationMiddleware(
169-
params: ConfigurationParams,
170-
token: CancellationToken,
171-
next: RequestHandler<ConfigurationParams, LSPAny[], void>,
172-
): Promise<LSPAny[] | ResponseError> {
173-
const optionIndices: Record<string, number | undefined> = {};
174-
175-
params.items.forEach((param, index) => {
176-
if (param.section) {
177-
if (param.section === "zls.zig_exe_path") {
178-
param.section = "zig.path";
179-
} else {
180-
param.section = `zig.zls.${camelCase(param.section.slice(4))}`;
181-
}
182-
optionIndices[param.section] = index;
183-
}
184-
});
166+
function configurationMiddleware(params: ConfigurationParams): LSPAny[] | ResponseError {
167+
void validateAdditionalOptions();
168+
return params.items.map((param) => {
169+
if (!param.section) return null;
185170

186-
const result = await next(params, token);
187-
if (result instanceof ResponseError) {
188-
return result;
189-
}
171+
const scopeUri = param.scopeUri ? client?.protocol2CodeConverter.asUri(param.scopeUri) : undefined;
172+
const configuration = vscode.workspace.getConfiguration("zig", scopeUri);
173+
const workspaceFolder = scopeUri ? vscode.workspace.getWorkspaceFolder(scopeUri) : undefined;
190174

191-
const configuration = vscode.workspace.getConfiguration("zig.zls");
175+
const updateConfigOption = (section: string, value: unknown) => {
176+
if (section === "zls.zigExePath") {
177+
return zigProvider.getZigPath();
178+
}
192179

193-
for (const name in optionIndices) {
194-
const index = optionIndices[name] as unknown as number;
195-
const section = name.slice("zig.zls.".length);
196-
const configValue = configuration.get(section);
197-
if (typeof configValue === "string") {
198-
// Make sure that `""` gets converted to `null` and resolve predefined values
199-
result[index] = configValue ? handleConfigOption(configValue) : null;
200-
}
180+
if (typeof value === "string") {
181+
// Make sure that `""` gets converted to `undefined` and resolve predefined values
182+
value = value ? handleConfigOption(value, workspaceFolder ?? "guess") : undefined;
183+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
184+
// Recursively update the config options
185+
const newValue: Record<string, unknown> = {};
186+
for (const [fieldName, fieldValue] of Object.entries(value)) {
187+
newValue[snakeCase(fieldName)] = updateConfigOption(section + "." + fieldName, fieldValue);
188+
}
189+
return newValue;
190+
}
201191

202-
const inspect = configuration.inspect(section);
203-
const isDefaultValue =
204-
configValue === inspect?.defaultValue &&
205-
inspect?.globalValue === undefined &&
206-
inspect?.workspaceValue === undefined &&
207-
inspect?.workspaceFolderValue === undefined;
208-
if (isDefaultValue) {
209-
if (name === "zig.zls.semanticTokens") {
210-
// The extension has a different default value for this config
211-
// option compared to ZLS
212-
continue;
192+
const inspect = configuration.inspect(section);
193+
const isDefaultValue =
194+
value === inspect?.defaultValue &&
195+
inspect?.globalValue === undefined &&
196+
inspect?.workspaceValue === undefined &&
197+
inspect?.workspaceFolderValue === undefined;
198+
199+
if (isDefaultValue) {
200+
if (section === "zls.semanticTokens") {
201+
// The extension has a different default value for this config
202+
// option compared to ZLS
203+
return value;
204+
} else {
205+
return undefined;
206+
}
213207
}
214-
result[index] = null;
215-
}
216-
}
208+
return value;
209+
};
217210

218-
const indexOfZigPath = optionIndices["zig.path"];
219-
if (indexOfZigPath !== undefined) {
220-
result[indexOfZigPath] = zigProvider.getZigPath();
221-
}
211+
let additionalOptions = configuration.get<Record<string, unknown>>("zls.additionalOptions", {});
212+
213+
// Remove the `zig.zls.` prefix from the entries in `zig.zls.additionalOptions`
214+
additionalOptions = Object.fromEntries(
215+
Object.entries(additionalOptions)
216+
.filter(([key]) => key.startsWith("zig.zls."))
217+
.map(([key, value]) => [key.slice("zig.zls.".length), value]),
218+
);
219+
220+
if (param.section === "zls") {
221+
// ZLS has requested all config options.
222+
223+
const options = { ...configuration.get<Record<string, unknown>>(param.section, {}) };
224+
// Some config options are specific to the VS Code
225+
// extension. ZLS should ignore unknown values but
226+
// we remove them here anyway.
227+
delete options["debugLog"]; // zig.zls.debugLog
228+
delete options["trace"]; // zig.zls.trace.server
229+
delete options["enabled"]; // zig.zls.enabled
230+
delete options["path"]; // zig.zls.path
231+
delete options["additionalOptions"]; // zig.zls.additionalOptions
232+
233+
return updateConfigOption(param.section, {
234+
...additionalOptions,
235+
...options,
236+
// eslint-disable-next-line @typescript-eslint/naming-convention
237+
zig_exe_path: zigProvider.getZigPath(),
238+
});
239+
} else if (param.section.startsWith("zls.")) {
240+
// ZLS has requested a specific config option.
241+
242+
// ZLS names it's config options in snake_case but the VS Code extension uses camelCase
243+
const camelCaseSection = param.section
244+
.split(".")
245+
.map((str) => camelCase(str))
246+
.join(".");
247+
248+
return updateConfigOption(
249+
camelCaseSection,
250+
configuration.get(camelCaseSection, additionalOptions[camelCaseSection.slice("zls.".length)]),
251+
);
252+
} else {
253+
// Do not allow ZLS to request other editor config options.
254+
return null;
255+
}
256+
});
257+
}
222258

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

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

228-
const doesOptionExist = configuration.inspect(section)?.defaultValue !== undefined;
229-
if (doesOptionExist) {
230-
// The extension has defined a config option with the given name but the user still used `additionalOptions`.
231-
const response = await vscode.window.showWarningMessage(
232-
`The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`,
233-
`Use ${optionName} instead`,
234-
"Show zig.zls.additionalOptions",
235-
);
236-
switch (response) {
237-
case `Use ${optionName} instead`:
238-
const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions;
239-
await workspaceConfigUpdateNoThrow(
240-
configuration,
241-
"additionalOptions",
242-
updatedAdditionalOptions,
243-
true,
244-
);
245-
await workspaceConfigUpdateNoThrow(configuration, section, newValue, true);
246-
break;
247-
case "Show zig.zls.additionalOptions":
248-
await vscode.commands.executeCommand("workbench.action.openSettingsJson", {
249-
revealSetting: { key: "zig.zls.additionalOptions" },
250-
});
251-
continue;
252-
case undefined:
253-
continue;
254-
}
255-
}
256-
257-
const optionIndex = optionIndices[optionName];
258-
if (!optionIndex) {
259-
// ZLS has not requested a config option with the given name.
260-
continue;
267+
const inspect = configuration.inspect(section);
268+
const doesOptionExist = inspect?.defaultValue !== undefined;
269+
if (!doesOptionExist) continue;
270+
271+
// The extension has defined a config option with the given name but the user still used `additionalOptions`.
272+
const response = await vscode.window.showWarningMessage(
273+
`The config option 'zig.zls.additionalOptions' contains the already existing option '${optionName}'`,
274+
`Use ${optionName} instead`,
275+
"Show zig.zls.additionalOptions",
276+
);
277+
switch (response) {
278+
case `Use ${optionName} instead`:
279+
const { [optionName]: newValue, ...updatedAdditionalOptions } = additionalOptions;
280+
await workspaceConfigUpdateNoThrow(
281+
configuration,
282+
"additionalOptions",
283+
Object.keys(updatedAdditionalOptions).length ? updatedAdditionalOptions : undefined,
284+
true,
285+
);
286+
await workspaceConfigUpdateNoThrow(configuration, section, newValue, true);
287+
break;
288+
case "Show zig.zls.additionalOptions":
289+
await vscode.commands.executeCommand("workbench.action.openSettingsJson", {
290+
revealSetting: { key: "zig.zls.additionalOptions" },
291+
});
292+
break;
293+
case undefined:
294+
return;
261295
}
262-
263-
result[optionIndex] = additionalOptions[optionName];
264296
}
265-
266-
return result as unknown[];
267297
}
268298

269299
/**

0 commit comments

Comments
 (0)