diff --git a/.vscode/settings.json b/.vscode/settings.json index 222f58a0a93b1..19c5ebb0fc926 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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]": { diff --git a/editors/vscode/client/ConfigService.ts b/editors/vscode/client/ConfigService.ts index ce3aa362eea47..5a51f099d2031 100644 --- a/editors/vscode/client/ConfigService.ts +++ b/editors/vscode/client/ConfigService.ts @@ -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 = new Map(); @@ -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(), })); } @@ -88,6 +118,36 @@ export class ConfigService implements IDisposable { return bin; } + public async getOxfmtServerBinPath(): Promise { + 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 { let isConfigChanged = false; diff --git a/editors/vscode/client/StatusBarItemHandler.ts b/editors/vscode/client/StatusBarItemHandler.ts index 5246a0b721131..b95c549d44c3e 100644 --- a/editors/vscode/client/StatusBarItemHandler.ts +++ b/editors/vscode/client/StatusBarItemHandler.ts @@ -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); diff --git a/editors/vscode/client/VSCodeConfig.ts b/editors/vscode/client/VSCodeConfig.ts index 96fe003a5e1a5..254fe0aee1290 100644 --- a/editors/vscode/client/VSCodeConfig.ts +++ b/editors/vscode/client/VSCodeConfig.ts @@ -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; @@ -25,6 +26,7 @@ export class VSCodeConfig implements VSCodeConfigInterface { this._enable = this.configuration.get("enable") ?? true; this._trace = this.configuration.get("trace.server") || "off"; this._binPathOxlint = binPathOxlint; + this._binPathOxfmt = this.configuration.get("path.oxfmt"); this._nodePath = this.configuration.get("path.node"); this._requireConfig = this.configuration.get("requireConfig") ?? false; } @@ -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 { + this._binPathOxfmt = value; + return this.configuration.update("path.oxfmt", value); + } + get nodePath(): string | undefined { return this._nodePath; } diff --git a/editors/vscode/client/WorkspaceConfig.ts b/editors/vscode/client/WorkspaceConfig.ts index 39d68cc9fe800..9b8fadf573769 100644 --- a/editors/vscode/client/WorkspaceConfig.ts +++ b/editors/vscode/client/WorkspaceConfig.ts @@ -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; @@ -297,7 +307,7 @@ export class WorkspaceConfig { }; } - public toOxlintConfig(): Omit { + public toOxlintConfig(): OxlintWorkspaceConfigInterface { return { run: this.runTrigger, configPath: this.configPath ?? null, @@ -314,7 +324,7 @@ export class WorkspaceConfig { }; } - public toOxfmtConfig(): Pick { + public toOxfmtConfig(): OxfmtWorkspaceConfigInterface { return { ["fmt.experimental"]: this.formattingExperimental, ["fmt.configPath"]: this.formattingConfigPath ?? null, diff --git a/editors/vscode/client/commands.ts b/editors/vscode/client/commands.ts index 393294ec17e5e..d1b5cdb6dd84e 100644 --- a/editors/vscode/client/commands.ts +++ b/editors/vscode/client/commands.ts @@ -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`, } diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 50faeb146ff0a..81d5a830053c3 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -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( @@ -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 => { + 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 { - await linter.deactivate(); + await Promise.all(tools.map((tool) => tool.deactivate())); } diff --git a/editors/vscode/client/tools/formatter.ts b/editors/vscode/client/tools/formatter.ts new file mode 100644 index 0000000000000..e1eaf3ec94f8c --- /dev/null +++ b/editors/vscode/client/tools/formatter.ts @@ -0,0 +1,244 @@ +import { promises as fsPromises } from "node:fs"; + +import { + commands, + ConfigurationChangeEvent, + ExtensionContext, + LogOutputChannel, + Uri, + window, + workspace, +} from "vscode"; + +import { ConfigurationParams, ShowMessageNotification } from "vscode-languageclient"; + +import { + Executable, + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from "vscode-languageclient/node"; + +import { OxcCommands } from "../commands"; +import { ConfigService } from "../ConfigService"; +import StatusBarItemHandler from "../StatusBarItemHandler"; +import { onClientNotification, runExecutable } from "./lsp_helper"; +import ToolInterface from "./ToolInterface"; + +const languageClientName = "oxc"; + +export default class FormatterTool implements ToolInterface { + // LSP client instance + private client: LanguageClient | undefined; + + async getBinary( + _context: ExtensionContext, + outputChannel: LogOutputChannel, + configService: ConfigService, + ): Promise { + const bin = await configService.getOxfmtServerBinPath(); + if (workspace.isTrusted && bin) { + try { + await fsPromises.access(bin); + return bin; + } catch (e) { + outputChannel.error(`Invalid bin path: ${bin}`, e); + } + } + return process.env.SERVER_PATH_DEV; + } + + async activate( + context: ExtensionContext, + binaryPath: string, + outputChannel: LogOutputChannel, + configService: ConfigService, + statusBarItemHandler: StatusBarItemHandler, + ) { + const restartCommand = commands.registerCommand(OxcCommands.RestartServerFmt, async () => { + await this.restartClient(); + }); + + outputChannel.info(`Using server binary at: ${binaryPath}`); + + const run: Executable = runExecutable(binaryPath, configService.vsCodeConfig.nodePath); + + const serverOptions: ServerOptions = { + run, + debug: run, + }; + + const supportedExtensions = [ + "cjs", + "cts", + "js", + "jsx", + "mjs", + "mts", + "ts", + "tsx", + // https://github.com/oxc-project/oxc/blob/f3e9913f534e36195b9b5a6244dd21076ed8715e/crates/oxc_formatter/src/service/parse_utils.rs#L24-L45 + "_js", + "bones", + "es", + "es6", + "gs", + "jake", + "javascript", + "jsb", + "jscad", + "jsfl", + "jslib", + "jsm", + "jspre", + "jss", + "njs", + "pac", + "sjs", + "ssjs", + "xsjs", + "xsjslib", + // https://github.com/oxc-project/oxc/blob/f3e9913f534e36195b9b5a6244dd21076ed8715e/crates/oxc_formatter/src/service/parse_utils.rs#L73 + // allow `*.start.frag` and `*.end.frag`, + "frag", + ]; + + // Special filenames that are valid JS files + // https://github.com/oxc-project/oxc/blob/f3e9913f534e36195b9b5a6244dd21076ed8715e/crates/oxc_formatter/src/service/parse_utils.rs#L47C4-L52 + const specialFilenames = [ + "Jakefile", + + // covered by the "frag" extension above + // "start.frag", + // "end.frag", + ]; + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + // Options to control the language client + let clientOptions: LanguageClientOptions = { + // Register the server for plain text documents + documentSelector: [ + { + pattern: `**/*.{${supportedExtensions.join(",")}}`, + scheme: "file", + }, + ...specialFilenames.map((filename) => ({ + pattern: `**/${filename}`, + scheme: "file", + })), + ], + initializationOptions: configService.formatterServerConfig, + outputChannel, + traceOutputChannel: outputChannel, + middleware: { + workspace: { + configuration: (params: ConfigurationParams) => { + return params.items.map((item) => { + if (item.section !== "oxc_language_server") { + return null; + } + if (item.scopeUri === undefined) { + return null; + } + + return ( + configService.getWorkspaceConfig(Uri.parse(item.scopeUri))?.toOxfmtConfig() ?? null + ); + }); + }, + }, + }, + }; + + // Create the language client and start the client. + this.client = new LanguageClient(languageClientName, serverOptions, clientOptions); + + const onNotificationDispose = this.client.onNotification( + ShowMessageNotification.type, + (params) => { + onClientNotification(params, outputChannel); + }, + ); + + context.subscriptions.push(restartCommand, onNotificationDispose); + + updateStatsBar(statusBarItemHandler, configService); + if (configService.vsCodeConfig.enable) { + await this.client.start(); + } + } + + async deactivate(): Promise { + if (!this.client) { + return undefined; + } + await this.client.stop(); + this.client = undefined; + } + + async restartClient(): Promise { + if (this.client === undefined) { + window.showErrorMessage("oxc client not found"); + return; + } + + try { + if (this.client.isRunning()) { + await this.client.restart(); + window.showInformationMessage("oxc server restarted."); + } else { + await this.client.start(); + } + } catch (err) { + this.client.error("Restarting client failed", err, "force"); + } + } + + async toggleClient(configService: ConfigService): Promise { + if (this.client === undefined) { + return; + } + + if (this.client.isRunning()) { + if (!configService.vsCodeConfig.enable) { + await this.client.stop(); + } + } else { + if (configService.vsCodeConfig.enable) { + await this.client.start(); + } + } + } + + async onConfigChange( + event: ConfigurationChangeEvent, + configService: ConfigService, + statusBarItemHandler: StatusBarItemHandler, + ): Promise { + updateStatsBar(statusBarItemHandler, configService); + + if (this.client === undefined) { + return; + } + + // update the initializationOptions for a possible restart + this.client.clientOptions.initializationOptions = configService.formatterServerConfig; + + if (configService.effectsWorkspaceConfigChange(event) && this.client.isRunning()) { + await this.client.sendNotification("workspace/didChangeConfiguration", { + settings: configService.formatterServerConfig, + }); + } + } +} + +function updateStatsBar(statusBarItemHandler: StatusBarItemHandler, configService: ConfigService) { + let text = configService.vsCodeConfig.enable ? `**oxfmt enabled**\n\n` : `**oxfmt disabled**\n\n`; + + text += + `[$(terminal) Open Output](command:${OxcCommands.ShowOutputChannelFmt})\n\n` + + `[$(refresh) Restart Server](command:${OxcCommands.RestartServerFmt})\n\n`; + + statusBarItemHandler.updateToolTooltip("formatter", text); +} diff --git a/editors/vscode/client/tools/linter.ts b/editors/vscode/client/tools/linter.ts index d399e5f16ced3..7d69e0efed3e5 100644 --- a/editors/vscode/client/tools/linter.ts +++ b/editors/vscode/client/tools/linter.ts @@ -13,6 +13,7 @@ import { import { ConfigurationParams, ExecuteCommandRequest, + InitializeParams, ShowMessageNotification, } from "vscode-languageclient"; @@ -37,6 +38,16 @@ const enum LspCommands { FixAll = "oxc.fixAll", } +class NoFormatterLanguageClient extends LanguageClient { + protected fillInitializeParams(params: InitializeParams): void { + // Disable formatting capabilities to prevent conflicts with the formatter tool. + delete params.capabilities.textDocument?.formatting; + delete params.capabilities.textDocument?.rangeFormatting; + + super.fillInitializeParams(params); + } +} + export default class LinterTool implements ToolInterface { // Global flag to check if the user allows us to start the server. // When `oxc.requireConfig` is `true`, make sure one `.oxlintrc.json` file is present. @@ -78,11 +89,11 @@ export default class LinterTool implements ToolInterface { ? (await workspace.findFiles(`**/.oxlintrc.json`, "**/node_modules/**", 1)).length > 0 : true; - const restartCommand = commands.registerCommand(OxcCommands.RestartServer, async () => { + const restartCommand = commands.registerCommand(OxcCommands.RestartServerLint, async () => { await this.restartClient(); }); - const toggleEnable = commands.registerCommand(OxcCommands.ToggleEnable, async () => { + const toggleEnable = commands.registerCommand(OxcCommands.ToggleEnableLint, async () => { await configService.vsCodeConfig.updateEnable(!configService.vsCodeConfig.enable); await this.toggleClient(configService); @@ -185,8 +196,12 @@ export default class LinterTool implements ToolInterface { }, }; - // Create the language client and start the client. - this.client = new LanguageClient(languageClientName, serverOptions, clientOptions); + // If the formatter is not handled by the language server, disable formatting capabilities to prevent conflicts. + if (configService.useOxcLanguageServerForFormatting) { + this.client = new LanguageClient(languageClientName, serverOptions, clientOptions); + } else { + this.client = new NoFormatterLanguageClient(languageClientName, serverOptions, clientOptions); + } const onNotificationDispose = this.client.onNotification( ShowMessageNotification.type, @@ -285,7 +300,11 @@ export default class LinterTool implements ToolInterface { /** * Get the status bar state based on whether oxc is enabled and allowed to start. */ - getStatusBarState(enable: boolean): { bgColor: string; icon: string; tooltipText: string } { + getStatusBarState(enable: boolean): { + bgColor: string; + icon: string; + tooltipText: string; + } { if (!this.allowedToStartServer) { return { bgColor: "statusBarItem.offlineBackground", @@ -312,13 +331,13 @@ export default class LinterTool implements ToolInterface { let text = `**${tooltipText}**\n\n` + - `[$(terminal) Open Output](command:${OxcCommands.ShowOutputChannel})\n\n` + - `[$(refresh) Restart Server](command:${OxcCommands.RestartServer})\n\n`; + `[$(terminal) Open Output](command:${OxcCommands.ShowOutputChannelLint})\n\n` + + `[$(refresh) Restart Server](command:${OxcCommands.RestartServerLint})\n\n`; if (enable) { - text += `[$(stop) Stop Server](command:${OxcCommands.ToggleEnable})\n\n`; + text += `[$(stop) Stop Server](command:${OxcCommands.ToggleEnableLint})\n\n`; } else { - text += `[$(play) Start Server](command:${OxcCommands.ToggleEnable})\n\n`; + text += `[$(play) Start Server](command:${OxcCommands.ToggleEnableLint})\n\n`; } statusBarItemHandler.setColorAndIcon(bgColor, icon); diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 944028421992f..81d913c36f2ce 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -41,7 +41,12 @@ "commands": [ { "command": "oxc.restartServer", - "title": "Restart Oxc Server", + "title": "Restart oxlint Server", + "category": "Oxc" + }, + { + "command": "oxc.restartServerFormatter", + "title": "Restart oxfmt Server", "category": "Oxc" }, { @@ -51,7 +56,12 @@ }, { "command": "oxc.showOutputChannel", - "title": "Show Output Channel", + "title": "Show Output Channel (Linter)", + "category": "Oxc" + }, + { + "command": "oxc.showOutputChannelFormatter", + "title": "Show Output Channel (Formatter)", "category": "Oxc" }, { @@ -204,6 +214,11 @@ "scope": "window", "markdownDescription": "Path to an Oxc linter binary. Will be used by the language server instead of the bundled one." }, + "oxc.path.oxfmt": { + "type": "string", + "scope": "window", + "markdownDescription": "Path to an Oxc formatter binary. Will be used by the language server instead of the bundled one." + }, "oxc.path.node": { "type": "string", "scope": "window", @@ -239,7 +254,9 @@ "supported": "limited", "description": "The Extension will always use the Language Server shipped with the Extension.", "restrictedConfigurations": [ - "oxc.path.server" + "oxc.path.server", + "oxc.path.oxlint", + "oxc.path.oxfmt" ] } }, diff --git a/editors/vscode/tests/ConfigService.spec.ts b/editors/vscode/tests/ConfigService.spec.ts index d598d1bde1002..ca5a4bb513ed8 100644 --- a/editors/vscode/tests/ConfigService.spec.ts +++ b/editors/vscode/tests/ConfigService.spec.ts @@ -7,13 +7,13 @@ const conf = workspace.getConfiguration('oxc'); suite('ConfigService', () => { setup(async () => { - const keys = ['path.server']; + const keys = ['path.server', 'path.oxlint', 'path.oxfmt']; await Promise.all(keys.map(key => conf.update(key, undefined))); }); teardown(async () => { - const keys = ['path.server']; + const keys = ['path.server', 'path.oxlint', 'path.oxfmt']; await Promise.all(keys.map(key => conf.update(key, undefined))); }); @@ -29,6 +29,47 @@ suite('ConfigService', () => { return workspace_path; }; + suite('getOxfmtServerBinPath', () => { + testSingleFolderMode('resolves relative server path with workspace folder', async () => { + const service = new ConfigService(); + const nonDefinedServerPath = await service.getOxfmtServerBinPath(); + + strictEqual(nonDefinedServerPath, undefined); + + await conf.update('path.oxfmt', '/absolute/oxfmt'); + const absoluteServerPath = await service.getOxfmtServerBinPath(); + + strictEqual(absoluteServerPath, '/absolute/oxfmt'); + + await conf.update('path.oxfmt', './relative/oxfmt'); + const relativeServerPath = await service.getOxfmtServerBinPath(); + + let workspace_path = getWorkspaceFolderPlatformSafe(); + strictEqual(relativeServerPath, `${workspace_path}/relative/oxfmt`); + }); + + testSingleFolderMode('returns undefined for unsafe server path', async () => { + const service = new ConfigService(); + await conf.update('path.oxfmt', '../unsafe/oxfmt'); + const unsafeServerPath = await service.getOxfmtServerBinPath(); + + strictEqual(unsafeServerPath, undefined); + }); + + testSingleFolderMode('returns backslashes path on Windows', async () => { + if (process.platform !== 'win32') { + return; + } + const service = new ConfigService(); + await conf.update('path.oxfmt', './relative/oxfmt'); + const relativeServerPath = await service.getOxfmtServerBinPath(); + let workspace_path = getWorkspaceFolderPlatformSafe(); + + 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('getUserServerBinPath', () => { testSingleFolderMode('resolves relative server path with workspace folder', async () => { const service = new ConfigService(); @@ -36,37 +77,37 @@ suite('ConfigService', () => { strictEqual(nonDefinedServerPath, undefined); - await conf.update('path.server', '/absolute/oxc_language_server'); + await conf.update('path.oxlint', '/absolute/oxlint'); const absoluteServerPath = service.getUserServerBinPath(); - strictEqual(absoluteServerPath, '/absolute/oxc_language_server'); + strictEqual(absoluteServerPath, '/absolute/oxlint'); - await conf.update('path.server', './relative/oxc_language_server'); + await conf.update('path.oxlint', './relative/oxlint'); const relativeServerPath = service.getUserServerBinPath(); let workspace_path = getWorkspaceFolderPlatformSafe(); - strictEqual(relativeServerPath, `${workspace_path}/relative/oxc_language_server`); + strictEqual(relativeServerPath, `${workspace_path}/relative/oxlint`); }); testSingleFolderMode('returns undefined for unsafe server path', async () => { const service = new ConfigService(); - await conf.update('path.server', '../unsafe/oxc_language_server'); + await conf.update('path.oxlint', '../unsafe/oxlint'); const unsafeServerPath = service.getUserServerBinPath(); strictEqual(unsafeServerPath, undefined); }); - testSingleFolderMode('returns backslashes path on Windows', async () => { + testSingleFolderMode('returns backslashes path on Windows', async () => { if (process.platform !== 'win32') { return; } const service = new ConfigService(); - await conf.update('path.server', './relative/oxc_language_server'); + await conf.update('path.oxlint', './relative/oxlint'); const relativeServerPath = service.getUserServerBinPath(); let workspace_path = getWorkspaceFolderPlatformSafe(); strictEqual(workspace_path[1], ':', 'The test workspace folder must be an absolute path with a drive letter on Windows'); - strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxc_language_server`); + strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxlint`); }); }); }); diff --git a/editors/vscode/tests/VSCodeConfig.spec.ts b/editors/vscode/tests/VSCodeConfig.spec.ts index 6ee29a4353a0f..5c9837e6a34f0 100644 --- a/editors/vscode/tests/VSCodeConfig.spec.ts +++ b/editors/vscode/tests/VSCodeConfig.spec.ts @@ -6,7 +6,7 @@ import { testSingleFolderMode } from './test-helpers.js'; const conf = workspace.getConfiguration('oxc'); suite('VSCodeConfig', () => { - const keys = ['enable', 'requireConfig', 'trace.server', 'path.server', 'path.oxlint', 'path.node']; + const keys = ['enable', 'requireConfig', 'trace.server', 'path.server', 'path.oxlint', 'path.oxfmt', 'path.node']; setup(async () => { await Promise.all(keys.map(key => conf.update(key, undefined))); }); @@ -22,6 +22,7 @@ suite('VSCodeConfig', () => { strictEqual(config.requireConfig, false); strictEqual(config.trace, 'off'); strictEqual(config.binPathOxlint, ''); + strictEqual(config.binPathOxfmt, ''); strictEqual(config.nodePath, ''); }); @@ -40,6 +41,7 @@ suite('VSCodeConfig', () => { config.updateRequireConfig(true), config.updateTrace('messages'), config.updateBinPathOxlint('./binary'), + config.updateBinPathOxfmt('./formatter'), config.updateNodePath('./node'), ]); @@ -49,6 +51,7 @@ suite('VSCodeConfig', () => { strictEqual(wsConfig.get('requireConfig'), true); strictEqual(wsConfig.get('trace.server'), 'messages'); strictEqual(wsConfig.get('path.oxlint'), './binary'); + strictEqual(wsConfig.get('path.oxfmt'), './formatter'); strictEqual(wsConfig.get('path.node'), './node'); }); }); diff --git a/editors/vscode/tests/commands.spec.ts b/editors/vscode/tests/commands.spec.ts index fcc9302054cd1..66fc08af1c341 100644 --- a/editors/vscode/tests/commands.spec.ts +++ b/editors/vscode/tests/commands.spec.ts @@ -31,31 +31,55 @@ suite('commands', () => { testSingleFolderMode('listed commands', async () => { const oxcCommands = (await commands.getCommands(true)).filter(x => x.startsWith('oxc.')); - const extraCommands = process.env.SKIP_LINTER_TEST === 'true' ? [] : [ - 'oxc.fixAll', + const expectedCommands = [ + 'oxc.showOutputChannel', + 'oxc.showOutputChannelFormatter', ]; - deepStrictEqual([ - 'oxc.showOutputChannel', - 'oxc.restartServer', - 'oxc.toggleEnable', - 'oxc.applyAllFixesFile', // TODO: only if linter tests are enabled - ...extraCommands, - ], oxcCommands); + if (process.env.SKIP_LINTER_TEST !== 'true') { + expectedCommands.push( + 'oxc.restartServer', + 'oxc.toggleEnable', + 'oxc.applyAllFixesFile', + 'oxc.fixAll', + ); + } + + if (process.env.SKIP_FORMATTER_TEST !== 'true' && !process.env.SERVER_PATH_DEV?.includes('oxc_language_server')) { + expectedCommands.push( + 'oxc.restartServerFormatter', + ); + } + + deepStrictEqual(expectedCommands, oxcCommands); }); testSingleFolderMode('oxc.showOutputChannel', async () => { await commands.executeCommand('oxc.showOutputChannel'); - await sleep(500); + await sleep(250); notEqual(window.activeTextEditor, undefined); const uri = window.activeTextEditor!.document.uri; - strictEqual(uri.toString(), 'output:oxc.oxc-vscode.Oxc'); + strictEqual(uri.toString(), 'output:oxc.oxc-vscode.Oxc%20%28Lint%29'); + + await commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + testSingleFolderMode('oxc.showOutputChannelFormatter', async () => { + await commands.executeCommand('oxc.showOutputChannelFormatter'); + await sleep(250); + + notEqual(window.activeTextEditor, undefined); + const uri = window.activeTextEditor!.document.uri; + strictEqual(uri.toString(), 'output:oxc.oxc-vscode.Oxc%20%28Fmt%29'); await commands.executeCommand('workbench.action.closeActiveEditor'); }); testSingleFolderMode('oxc.toggleEnable', async () => { + if (process.env.SKIP_LINTER_TEST === 'true') { + return; + } const isEnabledBefore = workspace.getConfiguration('oxc').get('enable'); strictEqual(isEnabledBefore, true);