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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
// "oxc.path.oxlint": "apps/oxlint/dist/cli.js", // debug with local oxlint build
"oxc.typeAware": true,
"oxc.configPath": "oxlintrc.json",
"oxc.unusedDisableDirectives": "deny",

// "oxc.path.oxfmt": "apps/oxfmt/dist/cli.js", // debug with local oxfmt build
"oxc.fmt.experimental": true,
"oxc.fmt.configPath": "oxfmtrc.jsonc",
"[javascript]": {
Expand Down
66 changes: 63 additions & 3 deletions editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ import { ConfigurationChangeEvent, Uri, workspace, WorkspaceFolder } from "vscod
import { validateSafeBinaryPath } from "./PathValidator";
import { IDisposable } from "./types";
import { VSCodeConfig } from "./VSCodeConfig";
import { WorkspaceConfig, WorkspaceConfigInterface } from "./WorkspaceConfig";
import {
OxfmtWorkspaceConfigInterface,
OxlintWorkspaceConfigInterface,
WorkspaceConfig,
WorkspaceConfigInterface,
} from "./WorkspaceConfig";

export class ConfigService implements IDisposable {
public static readonly namespace = "oxc";
private readonly _disposables: IDisposable[] = [];

/**
* Indicates whether the `oxc_language_server` is being used as the formatter.
* If true, the formatter functionality is handled by the language server itself.
*/
public useOxcLanguageServerForFormatting: boolean = false;

public vsCodeConfig: VSCodeConfig;

private workspaceConfigs: Map<string, WorkspaceConfig> = new Map();
Expand All @@ -33,10 +44,29 @@ export class ConfigService implements IDisposable {
this._disposables.push(disposeChangeListener);
}

public get languageServerConfig(): { workspaceUri: string; options: WorkspaceConfigInterface }[] {
public get languageServerConfig(): {
workspaceUri: string;
options: WorkspaceConfigInterface | OxlintWorkspaceConfigInterface;
}[] {
return [...this.workspaceConfigs.entries()].map(([path, config]) => {
const options = this.useOxcLanguageServerForFormatting
? config.toLanguageServerConfig()
: config.toOxlintConfig();

return {
workspaceUri: Uri.file(path).toString(),
options,
};
});
}

public get formatterServerConfig(): {
workspaceUri: string;
options: OxfmtWorkspaceConfigInterface;
}[] {
return [...this.workspaceConfigs.entries()].map(([path, config]) => ({
workspaceUri: Uri.file(path).toString(),
options: config.toLanguageServerConfig(),
options: config.toOxfmtConfig(),
}));
}

Expand Down Expand Up @@ -88,6 +118,36 @@ export class ConfigService implements IDisposable {
return bin;
}

public async getOxfmtServerBinPath(): Promise<string | undefined> {
let bin = this.vsCodeConfig.binPathOxfmt;
if (!bin) {
// try to find oxfmt in node_modules/.bin
const files = await workspace.findFiles("**/node_modules/.bin/oxfmt", null, 1);

return files.length > 0 ? files[0].fsPath : undefined;
}

// validates the given path is safe to use
if (validateSafeBinaryPath(bin) === false) {
return undefined;
}

if (!path.isAbsolute(bin)) {
// if the path is not absolute, resolve it to the first workspace folder
let cwd = this.workspaceConfigs.keys().next().value;
if (!cwd) {
return undefined;
}
bin = path.normalize(path.join(cwd, bin));
// strip the leading slash on Windows
if (process.platform === "win32" && bin.startsWith("\\")) {
bin = bin.slice(1);
}
}

return bin;
}

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

Expand Down
4 changes: 3 additions & 1 deletion editors/vscode/client/StatusBarItemHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export default class StatusBarItemHandler {
}

private updateFullTooltip(): void {
const text = Array.from(this.tooltipSections.values()).join("\n\n");
const text = [this.tooltipSections.get("linter"), this.tooltipSections.get("formatter")]
.filter(Boolean)
.join("\n\n---\n\n");

if (!(this.statusBarItem.tooltip instanceof MarkdownString)) {
this.statusBarItem.tooltip = new MarkdownString("", true);
Expand Down
11 changes: 11 additions & 0 deletions editors/vscode/client/VSCodeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {
private _enable!: boolean;
private _trace!: TraceLevel;
private _binPathOxlint: string | undefined;
private _binPathOxfmt: string | undefined;
private _nodePath: string | undefined;
private _requireConfig!: boolean;

Expand All @@ -25,6 +26,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {
this._enable = this.configuration.get<boolean>("enable") ?? true;
this._trace = this.configuration.get<TraceLevel>("trace.server") || "off";
this._binPathOxlint = binPathOxlint;
this._binPathOxfmt = this.configuration.get<string>("path.oxfmt");
this._nodePath = this.configuration.get<string>("path.node");
this._requireConfig = this.configuration.get<boolean>("requireConfig") ?? false;
}
Expand Down Expand Up @@ -56,6 +58,15 @@ export class VSCodeConfig implements VSCodeConfigInterface {
return this.configuration.update("path.oxlint", value);
}

get binPathOxfmt(): string | undefined {
return this._binPathOxfmt;
}

updateBinPathOxfmt(value: string | undefined): PromiseLike<void> {
this._binPathOxfmt = value;
return this.configuration.update("path.oxfmt", value);
}

get nodePath(): string | undefined {
return this._nodePath;
}
Expand Down
14 changes: 12 additions & 2 deletions editors/vscode/client/WorkspaceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ export interface WorkspaceConfigInterface {
["fmt.configPath"]?: string | null;
}

export type OxlintWorkspaceConfigInterface = Omit<
WorkspaceConfigInterface,
"fmt.experimental" | "fmt.configPath"
>;

export type OxfmtWorkspaceConfigInterface = Pick<
WorkspaceConfigInterface,
"fmt.experimental" | "fmt.configPath"
>;

export class WorkspaceConfig {
private _configPath: string | null = null;
private _tsConfigPath: string | null = null;
Expand Down Expand Up @@ -297,7 +307,7 @@ export class WorkspaceConfig {
};
}

public toOxlintConfig(): Omit<WorkspaceConfigInterface, "fmt.experimental" | "fmt.configPath"> {
public toOxlintConfig(): OxlintWorkspaceConfigInterface {
return {
run: this.runTrigger,
configPath: this.configPath ?? null,
Expand All @@ -314,7 +324,7 @@ export class WorkspaceConfig {
};
}

public toOxfmtConfig(): Pick<WorkspaceConfigInterface, "fmt.experimental" | "fmt.configPath"> {
public toOxfmtConfig(): OxfmtWorkspaceConfigInterface {
return {
["fmt.experimental"]: this.formattingExperimental,
["fmt.configPath"]: this.formattingConfigPath ?? null,
Expand Down
12 changes: 9 additions & 3 deletions editors/vscode/client/commands.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
const commandPrefix = "oxc";

export const enum OxcCommands {
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
// always available, even if no tool is active
ShowOutputChannelLint = `${commandPrefix}.showOutputChannel`,
ShowOutputChannelFmt = `${commandPrefix}.showOutputChannelFormatter`,

// only for linter.ts usage
RestartServer = `${commandPrefix}.restartServer`,
ToggleEnable = `${commandPrefix}.toggleEnable`,
RestartServerLint = `${commandPrefix}.restartServer`, // without `Linter` suffix for backward compatibility
ToggleEnableLint = `${commandPrefix}.toggleEnable`, // without `Linter` suffix for backward compatibility
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,

// only for formatter.ts usage
RestartServerFmt = `${commandPrefix}.restartServerFormatter`,
}
118 changes: 96 additions & 22 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,37 @@ import { commands, ExtensionContext, window, workspace } from "vscode";
import { OxcCommands } from "./commands";
import { ConfigService } from "./ConfigService";
import StatusBarItemHandler from "./StatusBarItemHandler";
import Formatter from "./tools/formatter";
import Linter from "./tools/linter";
import ToolInterface from "./tools/ToolInterface";

const outputChannelName = "Oxc";
const linter = new Linter();
const tools: ToolInterface[] = [];

if (process.env.SKIP_LINTER_TEST !== "true") {
tools.push(new Linter());
}
if (process.env.SKIP_FORMATTER_TEST !== "true") {
tools.push(new Formatter());
}

export async function activate(context: ExtensionContext) {
const configService = new ConfigService();

const outputChannel = window.createOutputChannel(outputChannelName, {
const outputChannelLint = window.createOutputChannel(outputChannelName + " (Lint)", {
log: true,
});

const outputChannelFormat = window.createOutputChannel(outputChannelName + " (Fmt)", {
log: true,
});

const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => {
outputChannel.show();
const showOutputLintCommand = commands.registerCommand(OxcCommands.ShowOutputChannelLint, () => {
outputChannelLint.show();
});

const showOutputFmtCommand = commands.registerCommand(OxcCommands.ShowOutputChannelFmt, () => {
outputChannelFormat.show();
});

const onDidChangeWorkspaceFoldersDispose = workspace.onDidChangeWorkspaceFolders(
Expand All @@ -33,35 +50,92 @@ export async function activate(context: ExtensionContext) {
const statusBarItemHandler = new StatusBarItemHandler(context.extension.packageJSON?.version);

context.subscriptions.push(
showOutputCommand,
showOutputLintCommand,
showOutputFmtCommand,
configService,
outputChannel,
outputChannelLint,
outputChannelFormat,
onDidChangeWorkspaceFoldersDispose,
statusBarItemHandler,
);

configService.onConfigChange = async function onConfigChange(event) {
await linter.onConfigChange(event, configService, statusBarItemHandler);
};
const binaryPath = await linter.getBinary(context, outputChannel, configService);

// For the linter this should never happen, but just in case.
if (!binaryPath) {
statusBarItemHandler.setColorAndIcon("statusBarItem.errorBackground", "error");
statusBarItemHandler.updateToolTooltip(
"linter",
"Error: No valid oxc language server binary found.",
await Promise.all(
tools.map((tool) => tool.onConfigChange(event, configService, statusBarItemHandler)),
);
statusBarItemHandler.show();
outputChannel.error("No valid oxc language server binary found.");
return;
};

const binaryPaths = await Promise.all(
tools.map((tool) =>
tool.getBinary(
context,
tool instanceof Linter ? outputChannelLint : outputChannelFormat,
configService,
),
),
);

// remove this block, when `oxfmt` binary is always required. This will be a breaking change.
if (
binaryPaths.some((path) => path?.includes("oxc_language_server")) &&
!configService.vsCodeConfig.binPathOxfmt
) {
configService.useOxcLanguageServerForFormatting = true;
}

await linter.activate(context, binaryPath, outputChannel, configService, statusBarItemHandler);
// Show status bar item after activation
await Promise.all(
tools.map((tool): Promise<void> => {
const binaryPath = binaryPaths[tools.indexOf(tool)];

// For the linter this should never happen, but just in case.
if (!binaryPath && tool instanceof Linter) {
statusBarItemHandler.setColorAndIcon("statusBarItem.errorBackground", "error");
statusBarItemHandler.updateToolTooltip(
"linter",
"**oxlint disabled**\n\nError: No valid oxc language server binary found.",
);
return Promise.resolve();
}

if (tool instanceof Formatter) {
if (configService.useOxcLanguageServerForFormatting) {
// The formatter is already handled by the linter tool in this case.
statusBarItemHandler.updateToolTooltip(
"formatter",
"**oxfmt disabled**\n\noxc_language_server is used for formatting.",
);
outputChannelFormat.appendLine("oxc_language_server is used for formatting.");
return Promise.resolve();
} else if (!binaryPath) {
// No valid binary found for the formatter.
statusBarItemHandler.updateToolTooltip(
"formatter",
"**oxfmt disabled**\n\nNo valid oxfmt binary found.",
);
outputChannelFormat.appendLine(
"No valid oxfmt binary found. Formatter will not be activated.",
);
return Promise.resolve();
}
}

// binaryPath is guaranteed to be defined here.
const binaryPathResolved = binaryPath!;

return tool.activate(
context,
binaryPathResolved,
tool instanceof Linter ? outputChannelLint : outputChannelFormat,
configService,
statusBarItemHandler,
);
}),
);

// Finally show the status bar item.
statusBarItemHandler.show();
}

export async function deactivate(): Promise<void> {
await linter.deactivate();
await Promise.all(tools.map((tool) => tool.deactivate()));
}
Loading
Loading