Skip to content

Commit fbd1f92

Browse files
committed
chore(vscode): second language server connection for oxfmt
1 parent 5a29274 commit fbd1f92

File tree

11 files changed

+423
-48
lines changed

11 files changed

+423
-48
lines changed

editors/vscode/client/ConfigService.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ 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+
WorkspaceConfig,
9+
WorkspaceConfigInterface,
10+
} from "./WorkspaceConfig";
711

812
export class ConfigService implements IDisposable {
913
public static readonly namespace = "oxc";
@@ -40,6 +44,16 @@ export class ConfigService implements IDisposable {
4044
}));
4145
}
4246

47+
public get formatterServerConfig(): {
48+
workspaceUri: string;
49+
options: OxfmtWorkspaceConfigInterface;
50+
}[] {
51+
return [...this.workspaceConfigs.entries()].map(([path, config]) => ({
52+
workspaceUri: Uri.file(path).toString(),
53+
options: config.toOxfmtConfig(),
54+
}));
55+
}
56+
4357
public addWorkspaceConfig(workspace: WorkspaceFolder): void {
4458
this.workspaceConfigs.set(workspace.uri.path, new WorkspaceConfig(workspace));
4559
}
@@ -88,6 +102,12 @@ export class ConfigService implements IDisposable {
88102
return bin;
89103
}
90104

105+
public async getOxfmtServerBinPath(): Promise<string | undefined> {
106+
const files = await workspace.findFiles("**/node_modules/.bin/oxfmt", null, 1);
107+
108+
return files.length > 0 ? files[0].fsPath : undefined;
109+
}
110+
91111
private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
92112
let isConfigChanged = false;
93113

editors/vscode/client/StatusBarItemHandler.ts

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

3636
private updateFullTooltip(): void {
37-
const text = Array.from(this.tooltipSections.values()).join("\n\n");
37+
const text = Array.from(this.tooltipSections.values()).join("\n\n---\n\n");
3838

3939
if (!(this.statusBarItem.tooltip instanceof MarkdownString)) {
4040
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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ export interface WorkspaceConfigInterface {
100100
["fmt.configPath"]?: string | null;
101101
}
102102

103+
export type OxfmtWorkspaceConfigInterface = Pick<
104+
WorkspaceConfigInterface,
105+
"fmt.experimental" | "fmt.configPath"
106+
>;
107+
103108
export class WorkspaceConfig {
104109
private _configPath: string | null = null;
105110
private _tsConfigPath: string | null = null;
@@ -314,7 +319,7 @@ export class WorkspaceConfig {
314319
};
315320
}
316321

317-
public toOxfmtConfig(): Pick<WorkspaceConfigInterface, "fmt.experimental" | "fmt.configPath"> {
322+
public toOxfmtConfig(): OxfmtWorkspaceConfigInterface {
318323
return {
319324
["fmt.experimental"]: this.formattingExperimental,
320325
["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: 92 additions & 23 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,87 @@ 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;
58-
}
66+
};
67+
68+
const foundBinaries: string[] = [];
69+
70+
await Promise.all(
71+
tools.map(async (tool) => {
72+
const outputChannel = tool instanceof Linter ? outputChannelLint : outputChannelFormat;
73+
const binaryPath = await tool.getBinary(context, outputChannel, configService);
74+
75+
// For the linter this should never happen, but just in case.
76+
if (!binaryPath && tool instanceof Linter) {
77+
statusBarItemHandler.setColorAndIcon("statusBarItem.errorBackground", "error");
78+
statusBarItemHandler.updateToolTooltip(
79+
"linter",
80+
"Error: No valid oxc language server binary found.",
81+
);
82+
}
83+
84+
if (tool instanceof Formatter) {
85+
if (foundBinaries.some((path) => path.includes("oxc_language_server"))) {
86+
// The formatter is already handled by the linter tool in this case.
87+
statusBarItemHandler.updateToolTooltip(
88+
"formatter",
89+
"oxc_language_server is used for formatting.",
90+
);
91+
outputChannelFormat.appendLine("oxc_language_server is used for formatting.");
92+
return;
93+
} else if (!binaryPath) {
94+
// No valid binary found for the formatter.
95+
statusBarItemHandler.updateToolTooltip(
96+
"formatter",
97+
"No valid oxfmt binary found. Formatter will not be activated.",
98+
);
99+
outputChannelFormat.appendLine(
100+
"No valid oxfmt binary found. Formatter will not be activated.",
101+
);
102+
return;
103+
}
104+
}
105+
106+
// binaryPath is guaranteed to be defined here.
107+
const binaryPathResolved = binaryPath!;
108+
109+
// do not activate the same binary multiple times.
110+
// This only happens when the `oxc_language_server` is used, which support both linting and formatting.
111+
//
112+
// The `linter` tool is prioritized here, so the formatter will not be activated if the linter already uses the same binary.
113+
// Because the linter source code also handles formatting in that case. (for now)
114+
if (foundBinaries.includes(binaryPathResolved)) {
115+
return;
116+
}
117+
118+
foundBinaries.push(binaryPathResolved);
119+
120+
return tool.activate(
121+
context,
122+
binaryPathResolved,
123+
outputChannel,
124+
configService,
125+
statusBarItemHandler,
126+
);
127+
}),
128+
);
59129

60-
await linter.activate(context, binaryPath, outputChannel, configService, statusBarItemHandler);
61-
// Show status bar item after activation
130+
// Finally show the status bar item.
62131
statusBarItemHandler.show();
63132
}
64133

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

0 commit comments

Comments
 (0)