Skip to content
Closed
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
64 changes: 51 additions & 13 deletions editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as path from "node:path";
import { promises as fs } from "node:fs";
import { ConfigurationChangeEvent, RelativePattern, Uri, workspace, WorkspaceFolder } from "vscode";
import { DiagnosticPullMode } from "vscode-languageclient";
import { validateSafeBinaryPath } from "./PathValidator";
Expand Down Expand Up @@ -113,20 +114,8 @@ export class ConfigService implements IDisposable {
settingsBinary: string | undefined,
defaultPattern: string,
): Promise<string | undefined> {
const cwd = this.workspaceConfigs.keys().next().value;
if (!cwd) {
return undefined;
}

if (!settingsBinary) {
// try to find the binary in node_modules/.bin, resolve to the first workspace folder
const files = await workspace.findFiles(
new RelativePattern(cwd, `**/node_modules/.bin/${defaultPattern}`),
null,
1,
);

return files.length > 0 ? files[0].fsPath : undefined;
return this.searchBinaryInWorkspaces(defaultPattern);
}

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

if (!path.isAbsolute(settingsBinary)) {
// if the path is not absolute, resolve it to the first workspace folder
const cwd = this.workspaceConfigs.keys().next().value;
if (!cwd) {
return undefined;
}
settingsBinary = path.normalize(path.join(cwd, settingsBinary));
// strip the leading slash on Windows
if (process.platform === "win32" && settingsBinary.startsWith("\\")) {
Expand All @@ -150,6 +143,51 @@ export class ConfigService implements IDisposable {
return settingsBinary;
}

/**
* Search for binary in all workspace folders, using optimized strategy:
* 1. Check direct paths in all workspace node_modules/.bin/ first
* 2. Fall back to recursive glob search only if needed
*/
private async searchBinaryInWorkspaces(defaultPattern: string): Promise<string | undefined> {
const workspacePaths = Array.from(this.workspaceConfigs.keys());

if (workspacePaths.length === 0) {
return undefined;
}

// First, try direct path checks in all workspace folders
for (const workspacePath of workspacePaths) {
const directPath = path.join(workspacePath, "node_modules", ".bin", defaultPattern);
try {
await fs.access(directPath);
// Convert to proper file system path format (handles Windows path normalization)
return Uri.file(directPath).fsPath;
} catch {
// File doesn't exist, continue to next workspace
}
}

// If direct path checks fail, fall back to recursive glob search
// Try each workspace folder in order
for (const workspacePath of workspacePaths) {
try {
const files = await workspace.findFiles(
new RelativePattern(workspacePath, `**/node_modules/.bin/${defaultPattern}`),
null,
1,
);

if (files.length > 0) {
return files[0].fsPath;
}
} catch {
// Glob search failed (timeout, permission issues, etc.), try next workspace
}
}

return undefined;
}

private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
let isConfigChanged = false;

Expand Down
72 changes: 71 additions & 1 deletion editors/vscode/tests/unit/ConfigService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { strictEqual } from 'assert';
import { workspace } from 'vscode';
import { Uri, workspace, WorkspaceEdit } from 'vscode';
import { ConfigService } from '../../client/ConfigService.js';
import { WORKSPACE_FOLDER } from '../test-helpers.js';

Expand Down Expand Up @@ -110,4 +110,74 @@ suite('ConfigService', () => {
strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxlint`);
});
});

suite('Binary Auto-Discovery', () => {
const createTestBinary = async (binaryName: string, workspaceUri: Uri = WORKSPACE_FOLDER.uri) => {
const binDir = Uri.joinPath(workspaceUri, 'node_modules', '.bin');
const binaryUri = Uri.joinPath(binDir, binaryName);
const edit = new WorkspaceEdit();
edit.createFile(binaryUri, {
contents: Buffer.from('#!/usr/bin/env node\nconsole.log("test");'),
overwrite: true,
});
await workspace.applyEdit(edit);
return binaryUri.fsPath;
};

const cleanupTestBinary = async (binaryName: string, workspaceUri: Uri = WORKSPACE_FOLDER.uri) => {
const binDir = Uri.joinPath(workspaceUri, 'node_modules', '.bin');
const binaryUri = Uri.joinPath(binDir, binaryName);
const edit = new WorkspaceEdit();
edit.deleteFile(binaryUri, { ignoreIfNotExists: true });
await workspace.applyEdit(edit);
};

teardown(async () => {
await cleanupTestBinary('oxlint');
await cleanupTestBinary('oxfmt');
});

test('finds binary in workspace node_modules/.bin/', async () => {
const expectedPath = await createTestBinary('oxlint');
const service = new ConfigService();

const binaryPath = await service.getOxlintServerBinPath();

strictEqual(binaryPath, expectedPath);
});

test('returns undefined when binary not found', async () => {
const service = new ConfigService();

const binaryPath = await service.getOxlintServerBinPath();

strictEqual(binaryPath, undefined);
});

test('finds oxfmt binary in workspace', async () => {
const expectedPath = await createTestBinary('oxfmt');
const service = new ConfigService();

const binaryPath = await service.getOxfmtServerBinPath();

strictEqual(binaryPath, expectedPath);
});

test('returns properly formatted file system path', async () => {
await createTestBinary('oxlint');
const service = new ConfigService();

const binaryPath = await service.getOxlintServerBinPath();

strictEqual(typeof binaryPath, 'string');
if (process.platform === 'win32') {
// On Windows, fsPath should use backslashes and have drive letter
strictEqual(binaryPath!.includes('/'), false, 'Windows path should not contain forward slashes');
strictEqual(binaryPath![1], ':', 'Windows path should have drive letter');
} else {
// On Unix, fsPath should use forward slashes
strictEqual(binaryPath!.startsWith('/'), true, 'Unix path should start with /');
}
});
});
});