Skip to content
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
4 changes: 4 additions & 0 deletions editors/vscode/.vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const allTestSuites = new Map([
{
...baseTest,
files: "out_test/unit/**/*.spec.js",
workspaceFolder: multiRootWorkspaceFile,
env: {
MULTI_FOLDER_WORKSPACE: "true",
},
},
],
[
Expand Down
53 changes: 10 additions & 43 deletions editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as path from "node:path";
import {
CancellationTokenSource,
ConfigurationChangeEvent,
RelativePattern,
Uri,
workspace,
WorkspaceFolder,
Expand Down Expand Up @@ -120,13 +119,8 @@ export class ConfigService implements IDisposable {
settingsBinary: string | undefined,
defaultBinaryName: string,
): Promise<string | undefined> {
const cwd = this.workspaceConfigs.keys().next().value;
if (!cwd) {
return undefined;
}

if (!settingsBinary) {
return this.searchNodeModulesBin(cwd, defaultBinaryName);
return this.searchNodeModulesBin(defaultBinaryName);
}

if (!workspace.isTrusted) {
Expand All @@ -139,6 +133,10 @@ export class ConfigService implements IDisposable {
}

if (!path.isAbsolute(settingsBinary)) {
const cwd = this.workspaceConfigs.keys().next().value;
if (!cwd) {
return undefined;
}
// if the path is not absolute, resolve it to the first workspace folder
settingsBinary = path.normalize(path.join(cwd, settingsBinary));
settingsBinary = this.removeWindowsLeadingSlash(settingsBinary);
Expand Down Expand Up @@ -181,52 +179,21 @@ export class ConfigService implements IDisposable {
}

/**
* Search for the binary in the workspace's node_modules/.bin directory.
* Search for the binary in all workspaces' node_modules/.bin directories.
* If multiple workspaces contain the binary, the first one found is returned.
*/
private async searchNodeModulesBin(
workspacePath: string,
binaryName: string,
): Promise<string | undefined> {
// try to find the binary in workspace's node_modules/.bin.
//
// Performance: this is a fast check before searching with glob.
// glob on windows is very slow.
const binPath = this.removeWindowsLeadingSlash(
path.normalize(path.join(workspacePath, "node_modules", ".bin", binaryName)),
);
try {
await workspace.fs.stat(Uri.file(binPath));
return binPath;
} catch {
// not found, continue to glob search
}

// on Windows, also check for `.exe` extension
if (process.platform === "win32") {
const binPathExe = `${binPath}.exe`;
try {
await workspace.fs.stat(Uri.file(binPathExe));
return binPathExe;
} catch {
// not found, continue to glob search
}
}

private async searchNodeModulesBin(binaryName: string): Promise<string | undefined> {
const cts = new CancellationTokenSource();
setTimeout(() => cts.cancel(), 10000); // cancel after 10 seconds

try {
// bun package manager uses `.exe` extension on Windows
// search for both with and without `.exe` extension
const extension = process.platform === "win32" ? "{,.exe}" : "";
// fallback: search with glob
// maybe use `tinyglobby` later for better performance, VSCode can be slow on globbing large projects.
const files = await workspace.findFiles(
// search up to 3 levels deep for the binary path
new RelativePattern(
workspacePath,
`{*/,*/*,*/*/*}/node_modules/.bin/${binaryName}${extension}`,
),
// search workspace root plus up to 3 subdirectory levels for the binary path
`{,*/,*/*,*/*/*}/node_modules/.bin/${binaryName}${extension}`,
undefined,
1,
cts.token,
Expand Down
162 changes: 91 additions & 71 deletions editors/vscode/tests/unit/ConfigService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,153 +1,173 @@
import { strictEqual } from 'assert';
import { workspace } from 'vscode';
import { ConfigService } from '../../client/ConfigService.js';
import { WORKSPACE_FOLDER } from '../test-helpers.js';
import { strictEqual } from "assert";
import { workspace } from "vscode";
import { ConfigService } from "../../client/ConfigService.js";
import { WORKSPACE_FOLDER, WORKSPACE_SECOND_FOLDER } from "../test-helpers.js";

const conf = workspace.getConfiguration('oxc');
const conf = workspace.getConfiguration("oxc");

suite('ConfigService', () => {
suite("ConfigService", () => {
setup(async () => {
const keys = ['path.server', 'path.oxlint', 'path.oxfmt', 'path.tsgolint'];
const keys = ["path.server", "path.oxlint", "path.oxfmt", "path.tsgolint"];

await Promise.all(keys.map(key => conf.update(key, undefined)));
await Promise.all(keys.map((key) => conf.update(key, undefined)));
});

teardown(async () => {
const keys = ['path.server', 'path.oxlint', 'path.oxfmt', 'path.tsgolint'];
const keys = ["path.server", "path.oxlint", "path.oxfmt", "path.tsgolint"];

await Promise.all(keys.map(key => conf.update(key, undefined)));
await Promise.all(keys.map((key) => conf.update(key, undefined)));
});

const getWorkspaceFolderPlatformSafe = () => {
let workspace_path = WORKSPACE_FOLDER.uri.path;
if (process.platform === 'win32') {
workspace_path = workspace_path.replaceAll('/', '\\');
if (workspace_path.startsWith('\\')) {
const getWorkspaceFolderPlatformSafe = (folder = WORKSPACE_FOLDER) => {
let workspace_path = folder.uri.path;
if (process.platform === "win32") {
workspace_path = workspace_path.replaceAll("/", "\\");
if (workspace_path.startsWith("\\")) {
workspace_path = workspace_path.slice(1);
}
}
return workspace_path;
};

const createWorkspaceFolderFileUri = async (relativePath: string) => {
const workspace_path = getWorkspaceFolderPlatformSafe();
const path = process.platform === 'win32'
? `${workspace_path}\\${relativePath}`
: `${workspace_path}/${relativePath}`;

await workspace.fs.writeFile(
WORKSPACE_FOLDER.uri.with({ path }),
new Uint8Array(),
);
}

const deleteWorkspaceFolderFileUri = async (relativePath: string) => {
const workspace_path = getWorkspaceFolderPlatformSafe();
const path = process.platform === 'win32'
? `${workspace_path}\\${relativePath}`
: `${workspace_path}/${relativePath}`;

await workspace.fs.delete(
WORKSPACE_FOLDER.uri.with({ path }),
);
}

suite('getOxfmtServerBinPath', () => {
test('resolves relative server path with workspace folder', async () => {
const createWorkspaceFolderFileUri = async (relativePath: string, folder = WORKSPACE_FOLDER) => {
const workspace_path = getWorkspaceFolderPlatformSafe(folder);
const path =
process.platform === "win32"
? `${workspace_path}\\${relativePath}`
: `${workspace_path}/${relativePath}`;

await workspace.fs.writeFile(folder.uri.with({ path }), new Uint8Array());
};

const deleteWorkspaceFolderFileUri = async (relativePath: string, folder = WORKSPACE_FOLDER) => {
const workspace_path = getWorkspaceFolderPlatformSafe(folder);
const path =
process.platform === "win32"
? `${workspace_path}\\${relativePath}`
: `${workspace_path}/${relativePath}`;

await workspace.fs.delete(folder.uri.with({ path }));
};

suite("getOxfmtServerBinPath", () => {
test("resolves relative server path with workspace folder", async () => {
const service = new ConfigService();
const workspace_path = getWorkspaceFolderPlatformSafe();
const nonDefinedServerPath = await service.getOxfmtServerBinPath();

await createWorkspaceFolderFileUri('absolute/oxfmt');
await createWorkspaceFolderFileUri('relative/oxfmt');
await createWorkspaceFolderFileUri("absolute/oxfmt");
await createWorkspaceFolderFileUri("relative/oxfmt");

strictEqual(nonDefinedServerPath, undefined);

await conf.update('path.oxfmt', `${workspace_path}/absolute/oxfmt`);
await conf.update("path.oxfmt", `${workspace_path}/absolute/oxfmt`);
const absoluteServerPath = await service.getOxfmtServerBinPath();

strictEqual(absoluteServerPath, `${workspace_path}/absolute/oxfmt`);

await conf.update('path.oxfmt', './relative/oxfmt');
await conf.update("path.oxfmt", "./relative/oxfmt");
const relativeServerPath = await service.getOxfmtServerBinPath();

strictEqual(relativeServerPath, `${workspace_path}/relative/oxfmt`);

await deleteWorkspaceFolderFileUri('absolute/oxfmt');
await deleteWorkspaceFolderFileUri('relative/oxfmt');
await deleteWorkspaceFolderFileUri("absolute/oxfmt");
await deleteWorkspaceFolderFileUri("relative/oxfmt");
});

test('returns undefined for unsafe server path', async () => {
await createWorkspaceFolderFileUri('../unsafe/oxfmt');
test("returns undefined for unsafe server path", async () => {
await createWorkspaceFolderFileUri("../unsafe/oxfmt");
const service = new ConfigService();
await conf.update('path.oxfmt', '../unsafe/oxfmt');
await conf.update("path.oxfmt", "../unsafe/oxfmt");
const unsafeServerPath = await service.getOxfmtServerBinPath();

strictEqual(unsafeServerPath, undefined);
await deleteWorkspaceFolderFileUri('../unsafe/oxfmt');
await deleteWorkspaceFolderFileUri("../unsafe/oxfmt");
});

test('returns backslashes path on Windows', async () => {
if (process.platform !== 'win32') {
test("returns backslashes path on Windows", async () => {
if (process.platform !== "win32") {
return;
}
const service = new ConfigService();
await conf.update('path.oxfmt', './relative/oxfmt');
await conf.update("path.oxfmt", "./relative/oxfmt");
const relativeServerPath = await service.getOxfmtServerBinPath();
const workspace_path = getWorkspaceFolderPlatformSafe();

strictEqual(workspace_path[1], ':', 'The test workspace folder must be an absolute path with a drive letter on Windows');
strictEqual(
workspace_path[1],
":",
"The test workspace folder must be an absolute path with a drive letter on Windows",
);
strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxfmt`);
});
});

suite('getOxlintServerBinPath', () => {
test('resolves relative server path with workspace folder', async () => {
suite("getOxlintServerBinPath", () => {
test("resolves relative server path with workspace folder", async () => {
const service = new ConfigService();
const nonDefinedServerPath = await service.getOxlintServerBinPath();
const workspace_path = getWorkspaceFolderPlatformSafe();

await createWorkspaceFolderFileUri('absolute/oxlint');
await createWorkspaceFolderFileUri('relative/oxlint');
await createWorkspaceFolderFileUri("absolute/oxlint");
await createWorkspaceFolderFileUri("relative/oxlint");

strictEqual(nonDefinedServerPath, undefined);

await conf.update('path.oxlint', `${workspace_path}/absolute/oxlint`);
await conf.update("path.oxlint", `${workspace_path}/absolute/oxlint`);
const absoluteServerPath = await service.getOxlintServerBinPath();

strictEqual(absoluteServerPath, `${workspace_path}/absolute/oxlint`);

await conf.update('path.oxlint', './relative/oxlint');
await conf.update("path.oxlint", "./relative/oxlint");
const relativeServerPath = await service.getOxlintServerBinPath();

strictEqual(relativeServerPath, `${workspace_path}/relative/oxlint`);


await deleteWorkspaceFolderFileUri('absolute/oxlint');
await deleteWorkspaceFolderFileUri('relative/oxlint');
await deleteWorkspaceFolderFileUri("absolute/oxlint");
await deleteWorkspaceFolderFileUri("relative/oxlint");
});

test('returns undefined for unsafe server path', async () => {
await createWorkspaceFolderFileUri('../unsafe/oxlint');
test("returns undefined for unsafe server path", async () => {
await createWorkspaceFolderFileUri("../unsafe/oxlint");
const service = new ConfigService();
await conf.update('path.oxlint', '../unsafe/oxlint');
await conf.update("path.oxlint", "../unsafe/oxlint");
const unsafeServerPath = await service.getOxlintServerBinPath();

strictEqual(unsafeServerPath, undefined);
await deleteWorkspaceFolderFileUri('../unsafe/oxlint');
await deleteWorkspaceFolderFileUri("../unsafe/oxlint");
});

test('returns backslashes path on Windows', async () => {
if (process.platform !== 'win32') {
test("returns backslashes path on Windows", async () => {
if (process.platform !== "win32") {
return;
}
const service = new ConfigService();
await conf.update('path.oxlint', './relative/oxlint');
await conf.update("path.oxlint", "./relative/oxlint");
const relativeServerPath = await service.getOxlintServerBinPath();
const workspace_path = getWorkspaceFolderPlatformSafe();

strictEqual(workspace_path[1], ':', 'The test workspace folder must be an absolute path with a drive letter on Windows');
strictEqual(
workspace_path[1],
":",
"The test workspace folder must be an absolute path with a drive letter on Windows",
);
strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxlint`);
});

// Skipped due to Test API limitation?
test.skip("resolves binary path in multi-folder workspace", async () => {
const service = new ConfigService();
const workspace_path = getWorkspaceFolderPlatformSafe();

await createWorkspaceFolderFileUri("node_modules/.bin/oxlint");
await createWorkspaceFolderFileUri("node_modules/.bin/oxlint", WORKSPACE_SECOND_FOLDER);
const absoluteServerPath = await service.getOxlintServerBinPath();

// returns undefined, but it should search with glob and return the first found binary
strictEqual(absoluteServerPath, `${workspace_path}/node_modules/.bin/oxlint`);

await deleteWorkspaceFolderFileUri("node_modules/.bin/oxlint");
await deleteWorkspaceFolderFileUri("node_modules/.bin/oxlint", WORKSPACE_SECOND_FOLDER);
});
});
});
Loading