Skip to content

Commit b61c6ca

Browse files
committed
chore(vscode): second language server connection for oxfmt
1 parent ca939c5 commit b61c6ca

File tree

11 files changed

+381
-46
lines changed

11 files changed

+381
-46
lines changed

editors/vscode/client/ConfigService.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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 { OxfmtWorkspaceConfigInterface, WorkspaceConfig, WorkspaceConfigInterface } from './WorkspaceConfig';
77

88
export class ConfigService implements IDisposable {
99
public static readonly namespace = 'oxc';
@@ -36,6 +36,13 @@ export class ConfigService implements IDisposable {
3636
}));
3737
}
3838

39+
public get formatterServerConfig(): { workspaceUri: string; options: OxfmtWorkspaceConfigInterface }[] {
40+
return [...this.workspaceConfigs.entries()].map(([path, config]) => ({
41+
workspaceUri: Uri.file(path).toString(),
42+
options: config.toOxfmtConfig(),
43+
}));
44+
}
45+
3946
public addWorkspaceConfig(workspace: WorkspaceFolder): void {
4047
this.workspaceConfigs.set(workspace.uri.path, new WorkspaceConfig(workspace));
4148
}
@@ -84,6 +91,12 @@ export class ConfigService implements IDisposable {
8491
return bin;
8592
}
8693

94+
public async getOxfmtServerBinPath(): Promise<string | undefined> {
95+
const files = await workspace.findFiles('**/node_modules/.bin/oxfmt', null, 1);
96+
97+
return files.length > 0 ? files[0].fsPath : undefined;
98+
}
99+
87100
private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
88101
let isConfigChanged = false;
89102

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export interface WorkspaceConfigInterface {
100100
['fmt.configPath']?: string | null;
101101
}
102102

103+
export type OxfmtWorkspaceConfigInterface = Pick<WorkspaceConfigInterface, 'fmt.experimental' | 'fmt.configPath'>;
104+
103105
export class WorkspaceConfig {
104106
private _configPath: string | null = null;
105107
private _tsConfigPath: string | null = null;
@@ -291,7 +293,7 @@ export class WorkspaceConfig {
291293
};
292294
}
293295

294-
public toOxfmtConfig(): Pick<WorkspaceConfigInterface, 'fmt.experimental' | 'fmt.configPath'> {
296+
public toOxfmtConfig(): OxfmtWorkspaceConfigInterface {
295297
return {
296298
['fmt.experimental']: this.formattingExperimental,
297299
['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: 77 additions & 21 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)', {
1524
log: true,
1625
});
1726

18-
const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => {
19-
outputChannel.show();
27+
const outputChannelFormat = window.createOutputChannel(outputChannelName + ' (Fmt)', {
28+
log: true,
29+
});
30+
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(async (event) => {
@@ -31,32 +48,71 @@ export async function activate(context: ExtensionContext) {
3148
const statusBarItemHandler = new StatusBarItemHandler(context.extension.packageJSON?.version);
3249

3350
context.subscriptions.push(
34-
showOutputCommand,
51+
showOutputLintCommand,
52+
showOutputFmtCommand,
3553
configService,
36-
outputChannel,
54+
outputChannelLint,
55+
outputChannelFormat,
3756
onDidChangeWorkspaceFoldersDispose,
3857
statusBarItemHandler,
3958
);
4059

4160
configService.onConfigChange = async function onConfigChange(event) {
42-
await linter.onConfigChange(event, configService, statusBarItemHandler);
61+
await Promise.all(tools.map((tool) => tool.onConfigChange(event, configService, statusBarItemHandler)));
4362
};
44-
const binaryPath = await linter.getBinary(context, outputChannel, configService);
45-
46-
// For the linter this should never happen, but just in case.
47-
if (!binaryPath) {
48-
statusBarItemHandler.setColorAndIcon('statusBarItem.errorBackground', 'error');
49-
statusBarItemHandler.updateToolTooltip('linter', 'Error: No valid oxc language server binary found.');
50-
statusBarItemHandler.show();
51-
outputChannel.error('No valid oxc language server binary found.');
52-
return;
53-
}
54-
55-
await linter.activate(context, binaryPath, outputChannel, configService, statusBarItemHandler);
56-
// Show status bar item after activation
63+
64+
const foundBinaries: string[] = [];
65+
66+
await Promise.all(
67+
tools.map(async (tool) => {
68+
const outputChannel = tool instanceof Linter ? outputChannelLint : outputChannelFormat;
69+
const binaryPath = await tool.getBinary(context, outputChannel, configService);
70+
71+
// For the linter this should never happen, but just in case.
72+
if (!binaryPath && tool instanceof Linter) {
73+
statusBarItemHandler.setColorAndIcon('statusBarItem.errorBackground', 'error');
74+
statusBarItemHandler.updateToolTooltip('linter', 'Error: No valid oxc language server binary found.');
75+
}
76+
77+
if (tool instanceof Formatter) {
78+
if (foundBinaries.some((path) => path.includes('oxc_language_server'))) {
79+
// The formatter is already handled by the linter tool in this case.
80+
statusBarItemHandler.updateToolTooltip('formatter', 'oxc_language_server is used for formatting.');
81+
outputChannelFormat.appendLine('oxc_language_server is used for formatting.');
82+
return;
83+
} else if (!binaryPath) {
84+
// No valid binary found for the formatter.
85+
statusBarItemHandler.updateToolTooltip(
86+
'formatter',
87+
'No valid oxfmt binary found. Formatter will not be activated.',
88+
);
89+
outputChannelFormat.appendLine('No valid oxfmt binary found. Formatter will not be activated.');
90+
return;
91+
}
92+
}
93+
94+
// binaryPath is guaranteed to be defined here.
95+
const binaryPathResolved = binaryPath!;
96+
97+
// do not activate the same binary multiple times.
98+
// This only happens when the `oxc_language_server` is used, which support both linting and formatting.
99+
//
100+
// The `linter` tool is prioritized here, so the formatter will not be activated if the linter already uses the same binary.
101+
// Because the linter source code also handles formatting in that case. (for now)
102+
if (foundBinaries.includes(binaryPathResolved)) {
103+
return;
104+
}
105+
106+
foundBinaries.push(binaryPathResolved);
107+
108+
return tool.activate(context, binaryPathResolved, outputChannel, configService, statusBarItemHandler);
109+
}),
110+
);
111+
112+
// Finally show the status bar item.
57113
statusBarItemHandler.show();
58114
}
59115

60116
export async function deactivate(): Promise<void> {
61-
await linter.deactivate();
117+
await Promise.all(tools.map((tool) => tool.deactivate()));
62118
}

0 commit comments

Comments
 (0)