Skip to content

Commit eef3fe2

Browse files
committed
chore(vscode): second language server connection for oxfmt
1 parent 43156ae commit eef3fe2

File tree

13 files changed

+563
-65
lines changed

13 files changed

+563
-65
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{
2+
// "oxc.path.oxlint": "apps/oxlint/dist/cli.js", // debug with local oxlint build
23
"oxc.typeAware": true,
34
"oxc.configPath": "oxlintrc.json",
45
"oxc.unusedDisableDirectives": "deny",
6+
7+
// "oxc.path.oxfmt": "apps/oxfmt/dist/cli.js", // debug with local oxfmt build
58
"oxc.fmt.experimental": true,
69
"oxc.fmt.configPath": "oxfmtrc.jsonc",
710
"[javascript]": {

editors/vscode/client/ConfigService.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import { ConfigurationChangeEvent, Uri, workspace, WorkspaceFolder } from "vscod
33
import { validateSafeBinaryPath } from "./PathValidator";
44
import { IDisposable } from "./types";
55
import { VSCodeConfig } from "./VSCodeConfig";
6-
import { WorkspaceConfig, WorkspaceConfigInterface } from "./WorkspaceConfig";
6+
import {
7+
OxfmtWorkspaceConfigInterface,
8+
OxlintWorkspaceConfigInterface,
9+
WorkspaceConfig,
10+
WorkspaceConfigInterface,
11+
} from "./WorkspaceConfig";
712

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

17+
/**
18+
* Indicates whether the `oxc_language_server` is being used as the formatter.
19+
* If true, the formatter functionality is handled by the language server itself.
20+
*/
21+
public useOxcLanguageServerForFormatting: boolean = false;
22+
1223
public vsCodeConfig: VSCodeConfig;
1324

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

36-
public get languageServerConfig(): { workspaceUri: string; options: WorkspaceConfigInterface }[] {
47+
public get languageServerConfig(): {
48+
workspaceUri: string;
49+
options: WorkspaceConfigInterface | OxlintWorkspaceConfigInterface;
50+
}[] {
51+
return [...this.workspaceConfigs.entries()].map(([path, config]) => {
52+
const options = this.useOxcLanguageServerForFormatting
53+
? config.toLanguageServerConfig()
54+
: config.toOxlintConfig();
55+
56+
return {
57+
workspaceUri: Uri.file(path).toString(),
58+
options,
59+
};
60+
});
61+
}
62+
63+
public get formatterServerConfig(): {
64+
workspaceUri: string;
65+
options: OxfmtWorkspaceConfigInterface;
66+
}[] {
3767
return [...this.workspaceConfigs.entries()].map(([path, config]) => ({
3868
workspaceUri: Uri.file(path).toString(),
39-
options: config.toLanguageServerConfig(),
69+
options: config.toOxfmtConfig(),
4070
}));
4171
}
4272

@@ -88,6 +118,36 @@ export class ConfigService implements IDisposable {
88118
return bin;
89119
}
90120

121+
public async getOxfmtServerBinPath(): Promise<string | undefined> {
122+
let bin = this.vsCodeConfig.binPathOxfmt;
123+
if (!bin) {
124+
// try to find oxfmt in node_modules/.bin
125+
const files = await workspace.findFiles("**/node_modules/.bin/oxfmt", null, 1);
126+
127+
return files.length > 0 ? files[0].fsPath : undefined;
128+
}
129+
130+
// validates the given path is safe to use
131+
if (validateSafeBinaryPath(bin) === false) {
132+
return undefined;
133+
}
134+
135+
if (!path.isAbsolute(bin)) {
136+
// if the path is not absolute, resolve it to the first workspace folder
137+
let cwd = this.workspaceConfigs.keys().next().value;
138+
if (!cwd) {
139+
return undefined;
140+
}
141+
bin = path.normalize(path.join(cwd, bin));
142+
// strip the leading slash on Windows
143+
if (process.platform === "win32" && bin.startsWith("\\")) {
144+
bin = bin.slice(1);
145+
}
146+
}
147+
148+
return bin;
149+
}
150+
91151
private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
92152
let isConfigChanged = false;
93153

editors/vscode/client/StatusBarItemHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export default class StatusBarItemHandler {
3434
}
3535

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

3941
if (!(this.statusBarItem.tooltip instanceof MarkdownString)) {
4042
this.statusBarItem.tooltip = new MarkdownString("", true);

editors/vscode/client/VSCodeConfig.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {
55
private _enable!: boolean;
66
private _trace!: TraceLevel;
77
private _binPathOxlint: string | undefined;
8+
private _binPathOxfmt: string | undefined;
89
private _nodePath: string | undefined;
910
private _requireConfig!: boolean;
1011

@@ -25,6 +26,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {
2526
this._enable = this.configuration.get<boolean>("enable") ?? true;
2627
this._trace = this.configuration.get<TraceLevel>("trace.server") || "off";
2728
this._binPathOxlint = binPathOxlint;
29+
this._binPathOxfmt = this.configuration.get<string>("path.oxfmt");
2830
this._nodePath = this.configuration.get<string>("path.node");
2931
this._requireConfig = this.configuration.get<boolean>("requireConfig") ?? false;
3032
}
@@ -56,6 +58,15 @@ export class VSCodeConfig implements VSCodeConfigInterface {
5658
return this.configuration.update("path.oxlint", value);
5759
}
5860

61+
get binPathOxfmt(): string | undefined {
62+
return this._binPathOxfmt;
63+
}
64+
65+
updateBinPathOxfmt(value: string | undefined): PromiseLike<void> {
66+
this._binPathOxfmt = value;
67+
return this.configuration.update("path.oxfmt", value);
68+
}
69+
5970
get nodePath(): string | undefined {
6071
return this._nodePath;
6172
}

editors/vscode/client/WorkspaceConfig.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ export interface WorkspaceConfigInterface {
100100
["fmt.configPath"]?: string | null;
101101
}
102102

103+
export type OxlintWorkspaceConfigInterface = Omit<
104+
WorkspaceConfigInterface,
105+
"fmt.experimental" | "fmt.configPath"
106+
>;
107+
108+
export type OxfmtWorkspaceConfigInterface = Pick<
109+
WorkspaceConfigInterface,
110+
"fmt.experimental" | "fmt.configPath"
111+
>;
112+
103113
export class WorkspaceConfig {
104114
private _configPath: string | null = null;
105115
private _tsConfigPath: string | null = null;
@@ -297,7 +307,7 @@ export class WorkspaceConfig {
297307
};
298308
}
299309

300-
public toOxlintConfig(): Omit<WorkspaceConfigInterface, "fmt.experimental" | "fmt.configPath"> {
310+
public toOxlintConfig(): OxlintWorkspaceConfigInterface {
301311
return {
302312
run: this.runTrigger,
303313
configPath: this.configPath ?? null,
@@ -314,7 +324,7 @@ export class WorkspaceConfig {
314324
};
315325
}
316326

317-
public toOxfmtConfig(): Pick<WorkspaceConfigInterface, "fmt.experimental" | "fmt.configPath"> {
327+
public toOxfmtConfig(): OxfmtWorkspaceConfigInterface {
318328
return {
319329
["fmt.experimental"]: this.formattingExperimental,
320330
["fmt.configPath"]: this.formattingConfigPath ?? null,

editors/vscode/client/commands.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
const commandPrefix = "oxc";
22

33
export const enum OxcCommands {
4-
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
4+
// always available, even if no tool is active
5+
ShowOutputChannelLint = `${commandPrefix}.showOutputChannel`,
6+
ShowOutputChannelFmt = `${commandPrefix}.showOutputChannelFormatter`,
7+
58
// only for linter.ts usage
6-
RestartServer = `${commandPrefix}.restartServer`,
7-
ToggleEnable = `${commandPrefix}.toggleEnable`,
9+
RestartServerLint = `${commandPrefix}.restartServer`, // without `Linter` suffix for backward compatibility
10+
ToggleEnableLint = `${commandPrefix}.toggleEnable`, // without `Linter` suffix for backward compatibility
811
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,
12+
13+
// only for formatter.ts usage
14+
RestartServerFmt = `${commandPrefix}.restartServerFormatter`,
915
}

editors/vscode/client/extension.ts

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,37 @@ import { commands, ExtensionContext, window, workspace } from "vscode";
33
import { OxcCommands } from "./commands";
44
import { ConfigService } from "./ConfigService";
55
import StatusBarItemHandler from "./StatusBarItemHandler";
6+
import Formatter from "./tools/formatter";
67
import Linter from "./tools/linter";
8+
import ToolInterface from "./tools/ToolInterface";
79

810
const outputChannelName = "Oxc";
9-
const linter = new Linter();
11+
const tools: ToolInterface[] = [];
12+
13+
if (process.env.SKIP_LINTER_TEST !== "true") {
14+
tools.push(new Linter());
15+
}
16+
if (process.env.SKIP_FORMATTER_TEST !== "true") {
17+
tools.push(new Formatter());
18+
}
1019

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

14-
const outputChannel = window.createOutputChannel(outputChannelName, {
23+
const outputChannelLint = window.createOutputChannel(outputChannelName + " (Lint)", {
24+
log: true,
25+
});
26+
27+
const outputChannelFormat = window.createOutputChannel(outputChannelName + " (Fmt)", {
1528
log: true,
1629
});
1730

18-
const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => {
19-
outputChannel.show();
31+
const showOutputLintCommand = commands.registerCommand(OxcCommands.ShowOutputChannelLint, () => {
32+
outputChannelLint.show();
33+
});
34+
35+
const showOutputFmtCommand = commands.registerCommand(OxcCommands.ShowOutputChannelFmt, () => {
36+
outputChannelFormat.show();
2037
});
2138

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

3552
context.subscriptions.push(
36-
showOutputCommand,
53+
showOutputLintCommand,
54+
showOutputFmtCommand,
3755
configService,
38-
outputChannel,
56+
outputChannelLint,
57+
outputChannelFormat,
3958
onDidChangeWorkspaceFoldersDispose,
4059
statusBarItemHandler,
4160
);
4261

4362
configService.onConfigChange = async function onConfigChange(event) {
44-
await linter.onConfigChange(event, configService, statusBarItemHandler);
45-
};
46-
const binaryPath = await linter.getBinary(context, outputChannel, configService);
47-
48-
// For the linter this should never happen, but just in case.
49-
if (!binaryPath) {
50-
statusBarItemHandler.setColorAndIcon("statusBarItem.errorBackground", "error");
51-
statusBarItemHandler.updateToolTooltip(
52-
"linter",
53-
"Error: No valid oxc language server binary found.",
63+
await Promise.all(
64+
tools.map((tool) => tool.onConfigChange(event, configService, statusBarItemHandler)),
5465
);
55-
statusBarItemHandler.show();
56-
outputChannel.error("No valid oxc language server binary found.");
57-
return;
66+
};
67+
68+
const binaryPaths = await Promise.all(
69+
tools.map((tool) =>
70+
tool.getBinary(
71+
context,
72+
tool instanceof Linter ? outputChannelLint : outputChannelFormat,
73+
configService,
74+
),
75+
),
76+
);
77+
78+
// remove this block, when `oxfmt` binary is always required. This will be a breaking change.
79+
if (
80+
binaryPaths.some((path) => path?.includes("oxc_language_server")) &&
81+
!configService.vsCodeConfig.binPathOxfmt
82+
) {
83+
configService.useOxcLanguageServerForFormatting = true;
5884
}
5985

60-
await linter.activate(context, binaryPath, outputChannel, configService, statusBarItemHandler);
61-
// Show status bar item after activation
86+
await Promise.all(
87+
tools.map((tool): Promise<void> => {
88+
const binaryPath = binaryPaths[tools.indexOf(tool)];
89+
90+
// For the linter this should never happen, but just in case.
91+
if (!binaryPath && tool instanceof Linter) {
92+
statusBarItemHandler.setColorAndIcon("statusBarItem.errorBackground", "error");
93+
statusBarItemHandler.updateToolTooltip(
94+
"linter",
95+
"**oxlint disabled**\n\nError: No valid oxc language server binary found.",
96+
);
97+
return Promise.resolve();
98+
}
99+
100+
if (tool instanceof Formatter) {
101+
if (configService.useOxcLanguageServerForFormatting) {
102+
// The formatter is already handled by the linter tool in this case.
103+
statusBarItemHandler.updateToolTooltip(
104+
"formatter",
105+
"**oxfmt disabled**\n\noxc_language_server is used for formatting.",
106+
);
107+
outputChannelFormat.appendLine("oxc_language_server is used for formatting.");
108+
return Promise.resolve();
109+
} else if (!binaryPath) {
110+
// No valid binary found for the formatter.
111+
statusBarItemHandler.updateToolTooltip(
112+
"formatter",
113+
"**oxfmt disabled**\n\nNo valid oxfmt binary found.",
114+
);
115+
outputChannelFormat.appendLine(
116+
"No valid oxfmt binary found. Formatter will not be activated.",
117+
);
118+
return Promise.resolve();
119+
}
120+
}
121+
122+
// binaryPath is guaranteed to be defined here.
123+
const binaryPathResolved = binaryPath!;
124+
125+
return tool.activate(
126+
context,
127+
binaryPathResolved,
128+
tool instanceof Linter ? outputChannelLint : outputChannelFormat,
129+
configService,
130+
statusBarItemHandler,
131+
);
132+
}),
133+
);
134+
135+
// Finally show the status bar item.
62136
statusBarItemHandler.show();
63137
}
64138

65139
export async function deactivate(): Promise<void> {
66-
await linter.deactivate();
140+
await Promise.all(tools.map((tool) => tool.deactivate()));
67141
}

0 commit comments

Comments
 (0)