Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/five-apricots-resolve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"biome": minor
---

Resolve VS Code variables in custom Biome path settings such as `biome.lsp.bin`
and `biome.configurationPath`.
82 changes: 81 additions & 1 deletion src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
import { displayName } from "../package.json";
import type Biome from "./biome";
import { supportedLanguages } from "./constants";
import { config, type SafeSpawnSyncOptions, safeSpawnSync } from "./utils";
import {
config,
normalizeBiomeSettings,
type SafeSpawnSyncOptions,
safeSpawnSync,
} from "./utils";

export default class Session {
private static watcherSupportCache = new Map<
Expand Down Expand Up @@ -198,6 +203,81 @@ export default class Session {
traceOutputChannel: outputChannel,
documentSelector: this.createDocumentSelector(),
workspaceFolder: this.folder,
middleware: {
workspace: {
configuration: async (params, token, next) => {
const settings = await next(params, token);

if (!Array.isArray(settings)) {
return settings;
}

return params.items.map((item, index) => {
const resource = item.scopeUri
? Uri.parse(item.scopeUri)
: undefined;
const value = settings[index];

if (item.section === "biome") {
return normalizeBiomeSettings(value, resource);
}

if (
item.section === "biome.configurationPath" &&
typeof value === "string"
) {
return (
normalizeBiomeSettings(
{ configurationPath: value },
resource,
) as { configurationPath?: string }
).configurationPath;
}

if (
(item.section === "biome.lsp.bin" ||
item.section === "biome.lspBin") &&
(typeof value === "string" ||
(typeof value === "object" &&
value !== null &&
!Array.isArray(value)))
) {
if (item.section === "biome.lspBin") {
return (
normalizeBiomeSettings({ lspBin: value }, resource) as {
lspBin?: unknown;
}
).lspBin;
}

return (
normalizeBiomeSettings({ lsp: { bin: value } }, resource) as {
lsp?: { bin?: unknown };
}
).lsp?.bin;
}

if (
(item.section === undefined || item.section === null) &&
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
"biome" in value
) {
return {
...(value as Record<string, unknown>),
biome: normalizeBiomeSettings(
(value as Record<string, unknown>).biome,
resource,
),
};
}

return value;
});
},
},
},
initializationOptions: {
...(this.singleFileFolder && {
rootUri: this.singleFileFolder,
Expand Down
117 changes: 114 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,113 @@ export const config: {
return workspace.getConfiguration("biome", options?.scope).get<T>(key);
};

/**
* Resolves common VS Code variables in user-provided settings.
*
* This mirrors the behavior used by other language tool extensions for path-like
* settings such as `${workspaceFolder}`, `${userHome}`, and `${env:NAME}`.
*/
export const resolveVariables = (
value: string,
workspaceFolder?: WorkspaceFolder,
): string => {
let resolved = value;
const substitutions = new Map<string, string>();
const home = process.env.HOME || process.env.USERPROFILE;
const userHomeVariable = "$" + "{userHome}";
const workspaceFolderVariable = "$" + "{workspaceFolder}";
const cwdVariable = "$" + "{cwd}";

if (home) {
substitutions.set(userHomeVariable, home);
}

if (workspaceFolder) {
substitutions.set(workspaceFolderVariable, workspaceFolder.uri.fsPath);
}

substitutions.set(cwdVariable, process.cwd());

for (const folder of workspace.workspaceFolders ?? []) {
substitutions.set(`\${workspaceFolder:${folder.name}}`, folder.uri.fsPath);
}

for (const [key, envValue] of Object.entries(process.env)) {
if (envValue !== undefined) {
substitutions.set(`\${env:${key}}`, envValue);
}
}

for (const [key, substitution] of substitutions) {
resolved = resolved.replaceAll(key, substitution);
}

return resolved;
};

const resolvePathSetting = (
value: string,
workspaceFolder?: WorkspaceFolder,
): string => resolveVariables(value, workspaceFolder);

const isStringRecord = (value: unknown): value is Record<string, string> =>
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.values(value).every((entry) => typeof entry === "string");

export const normalizeBiomeSettings = (
settings: unknown,
resource?: Uri,
): unknown => {
if (
typeof settings !== "object" ||
settings === null ||
Array.isArray(settings)
) {
return settings;
}

const workspaceFolder = resource
? workspace.getWorkspaceFolder(resource)
: undefined;
const result = { ...(settings as Record<string, unknown>) };

if (typeof result.configurationPath === "string") {
result.configurationPath = resolvePathSetting(
result.configurationPath,
workspaceFolder,
);
}

if (typeof result.lspBin === "string") {
result.lspBin = resolvePathSetting(result.lspBin, workspaceFolder);
}

if (
typeof result.lsp === "object" &&
result.lsp !== null &&
!Array.isArray(result.lsp)
) {
const lspSettings = { ...(result.lsp as Record<string, unknown>) };

if (typeof lspSettings.bin === "string") {
lspSettings.bin = resolvePathSetting(lspSettings.bin, workspaceFolder);
} else if (isStringRecord(lspSettings.bin)) {
lspSettings.bin = Object.fromEntries(
Object.entries(lspSettings.bin).map(([key, entry]) => [
key,
resolvePathSetting(entry, workspaceFolder),
]),
);
}

result.lsp = lspSettings;
}

return result;
};

/**
* Retrieves the `biome.lsp.bin` setting
*
Expand All @@ -79,13 +186,17 @@ export const getLspBin = (
}) || config<string>("lspBin", { scope: workspaceFolder }); // deprecated setting for fallback.

const resolvePath = (lspBin: string, workspaceFolder?: WorkspaceFolder) => {
const resolvedPath = resolvePathSetting(lspBin, workspaceFolder);

// If the specified path is relative, resolve it against the root of
// the workspace folder (if any).
if (workspaceFolder && !isAbsolute(lspBin)) {
return Uri.file(Utils.resolvePath(workspaceFolder.uri, lspBin).fsPath);
if (workspaceFolder && !isAbsolute(resolvedPath)) {
return Uri.file(
Utils.resolvePath(workspaceFolder.uri, resolvedPath).fsPath,
);
}

return Uri.file(lspBin);
return Uri.file(resolvedPath);
};

if (typeof lspBin === "string") {
Expand Down