Skip to content

Commit f535ff0

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

File tree

4 files changed

+325
-10
lines changed

4 files changed

+325
-10
lines changed

editors/vscode/client/ConfigService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ export class ConfigService implements IDisposable {
8484
return bin;
8585
}
8686

87+
public async getOxfmtServerBinPath(): Promise<string | undefined> {
88+
const files = await workspace.findFiles('**/node_modules/.bin/oxfmt', null, 1);
89+
90+
return files.length > 0 ? files[0].fsPath : undefined;
91+
}
92+
8793
private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
8894
let isConfigChanged = false;
8995

editors/vscode/client/extension.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,56 @@ import { commands, ExtensionContext, window, workspace } from 'vscode';
22

33
import { OxcCommands } from './commands';
44
import { ConfigService } from './ConfigService';
5+
import {
6+
activate as activateFormatter,
7+
deactivate as deactivateFormatter,
8+
onConfigChange as onConfigChangeFormatter,
9+
restartClient as restartFormatter,
10+
toggleClient as toggleFormatter,
11+
} from './formatter';
512
import {
613
activate as activateLinter,
714
deactivate as deactivateLinter,
815
onConfigChange as onConfigChangeLinter,
9-
restartClient,
10-
toggleClient,
16+
restartClient as restartLinter,
17+
toggleClient as toggleLinter,
1118
} from './linter';
1219

1320
const outputChannelName = 'Oxc';
1421

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

18-
const outputChannel = window.createOutputChannel(outputChannelName, {
25+
const outputChannelLint = window.createOutputChannel(outputChannelName + ' (Lint)', {
26+
log: true,
27+
});
28+
29+
const outputChannelFormat = window.createOutputChannel(outputChannelName + ' (Fmt)', {
1930
log: true,
2031
});
2132

2233
const restartCommand = commands.registerCommand(OxcCommands.RestartServer, async () => {
23-
await restartClient();
34+
if (process.env.SKIP_LINTER_TEST !== 'true') {
35+
await restartLinter();
36+
}
37+
if (process.env.SKIP_FORMATTER_TEST !== 'true') {
38+
await restartFormatter();
39+
}
2440
});
2541

2642
const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => {
27-
outputChannel.show();
43+
outputChannelLint.show();
2844
});
2945

3046
const toggleEnable = commands.registerCommand(OxcCommands.ToggleEnable, async () => {
3147
await configService.vsCodeConfig.updateEnable(!configService.vsCodeConfig.enable);
3248

33-
await toggleClient(configService);
49+
if (process.env.SKIP_LINTER_TEST !== 'true') {
50+
await toggleLinter(configService);
51+
}
52+
if (process.env.SKIP_FORMATTER_TEST !== 'true') {
53+
await toggleFormatter(configService);
54+
}
3455
});
3556

3657
const onDidChangeWorkspaceFoldersDispose = workspace.onDidChangeWorkspaceFolders(async (event) => {
@@ -47,17 +68,33 @@ export async function activate(context: ExtensionContext) {
4768
showOutputCommand,
4869
toggleEnable,
4970
configService,
50-
outputChannel,
71+
outputChannelLint,
72+
outputChannelFormat,
5173
onDidChangeWorkspaceFoldersDispose,
5274
);
5375

5476
configService.onConfigChange = async function onConfigChange(event) {
55-
await onConfigChangeLinter(context, event, configService);
77+
if (process.env.SKIP_LINTER_TEST !== 'true') {
78+
await onConfigChangeLinter(context, event, configService);
79+
}
80+
if (process.env.SKIP_FORMATTER_TEST !== 'true') {
81+
await onConfigChangeFormatter(context, event, configService);
82+
}
5683
};
5784

58-
await activateLinter(context, outputChannel, configService);
85+
if (process.env.SKIP_LINTER_TEST !== 'true') {
86+
await activateLinter(context, outputChannelLint, configService);
87+
}
88+
if (process.env.SKIP_FORMATTER_TEST !== 'true') {
89+
await activateFormatter(context, outputChannelFormat, configService);
90+
}
5991
}
6092

6193
export async function deactivate(): Promise<void> {
62-
await deactivateLinter();
94+
if (process.env.SKIP_LINTER_TEST !== 'true') {
95+
await deactivateLinter();
96+
}
97+
if (process.env.SKIP_FORMATTER_TEST !== 'true') {
98+
await deactivateFormatter();
99+
}
63100
}

editors/vscode/client/formatter.ts

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import { promises as fsPromises } from 'node:fs';
2+
3+
import {
4+
ConfigurationChangeEvent,
5+
ExtensionContext,
6+
LogOutputChannel,
7+
StatusBarAlignment,
8+
StatusBarItem,
9+
ThemeColor,
10+
Uri,
11+
window,
12+
workspace,
13+
} from 'vscode';
14+
15+
import { ConfigurationParams, MessageType, ShowMessageNotification } from 'vscode-languageclient';
16+
17+
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
18+
19+
import { ConfigService } from './ConfigService';
20+
import { OxcCommands } from './commands';
21+
22+
const languageClientName = 'oxc';
23+
24+
let client: LanguageClient | undefined;
25+
26+
let myStatusBarItem: StatusBarItem;
27+
28+
export async function activate(
29+
context: ExtensionContext,
30+
outputChannel: LogOutputChannel,
31+
configService: ConfigService,
32+
) {
33+
async function findBinary(): Promise<string | undefined> {
34+
const bin = await configService.getOxfmtServerBinPath();
35+
if (bin) {
36+
try {
37+
await fsPromises.access(bin);
38+
return bin;
39+
} catch (e) {
40+
outputChannel.error(`Invalid bin path: ${bin}`, e);
41+
}
42+
}
43+
return process.env.SERVER_PATH_DEV;
44+
}
45+
46+
const nodePath = configService.vsCodeConfig.nodePath;
47+
const serverEnv: Record<string, string> = {
48+
...process.env,
49+
RUST_LOG: process.env.RUST_LOG || 'info',
50+
};
51+
if (nodePath) {
52+
serverEnv.PATH = `${nodePath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`;
53+
}
54+
55+
const path = await findBinary();
56+
57+
if (!path) {
58+
outputChannel.error('oxfmt server binary not found.');
59+
return;
60+
}
61+
62+
outputChannel.info(`Using server binary at: ${path}`);
63+
64+
const isNode = path.endsWith('.js') || path.endsWith('.cjs') || path.endsWith('.mjs');
65+
66+
const run: Executable = isNode
67+
? {
68+
command: 'node',
69+
args: [path, '--lsp'],
70+
options: {
71+
env: serverEnv,
72+
},
73+
}
74+
: {
75+
command: path,
76+
args: ['--lsp'],
77+
options: {
78+
// On Windows we need to run the binary in a shell to be able to execute the shell npm bin script.
79+
// Searching for the right `.exe` file inside `node_modules/` is not reliable as it depends on
80+
// the package manager used (npm, yarn, pnpm, etc) and the package version.
81+
// The npm bin script is a shell script that points to the actual binary.
82+
// Security: We validated the userDefinedBinary in `configService.getUserServerBinPath()`.
83+
shell: process.platform === 'win32',
84+
env: serverEnv,
85+
},
86+
};
87+
88+
const serverOptions: ServerOptions = {
89+
run,
90+
debug: run,
91+
};
92+
93+
// see https://github.com/oxc-project/oxc/blob/9b475ad05b750f99762d63094174be6f6fc3c0eb/crates/oxc_linter/src/loader/partial_loader/mod.rs#L17-L20
94+
const supportedExtensions = ['astro', 'cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'svelte', 'ts', 'tsx', 'vue'];
95+
96+
// If the extension is launched in debug mode then the debug server options are used
97+
// Otherwise the run options are used
98+
// Options to control the language client
99+
let clientOptions: LanguageClientOptions = {
100+
// Register the server for plain text documents
101+
documentSelector: [
102+
{
103+
pattern: `**/*.{${supportedExtensions.join(',')}}`,
104+
scheme: 'file',
105+
},
106+
],
107+
initializationOptions: configService.languageServerConfig,
108+
outputChannel,
109+
traceOutputChannel: outputChannel,
110+
middleware: {
111+
workspace: {
112+
configuration: (params: ConfigurationParams) => {
113+
return params.items.map((item) => {
114+
if (item.section !== 'oxc_language_server') {
115+
return null;
116+
}
117+
if (item.scopeUri === undefined) {
118+
return null;
119+
}
120+
121+
return configService.getWorkspaceConfig(Uri.parse(item.scopeUri))?.toLanguageServerConfig() ?? null;
122+
});
123+
},
124+
},
125+
},
126+
};
127+
128+
// Create the language client and start the client.
129+
client = new LanguageClient(languageClientName, serverOptions, clientOptions);
130+
131+
const onNotificationDispose = client.onNotification(ShowMessageNotification.type, (params) => {
132+
switch (params.type) {
133+
case MessageType.Debug:
134+
outputChannel.debug(params.message);
135+
break;
136+
case MessageType.Log:
137+
outputChannel.info(params.message);
138+
break;
139+
case MessageType.Info:
140+
window.showInformationMessage(params.message);
141+
break;
142+
case MessageType.Warning:
143+
window.showWarningMessage(params.message);
144+
break;
145+
case MessageType.Error:
146+
window.showErrorMessage(params.message);
147+
break;
148+
default:
149+
outputChannel.info(params.message);
150+
}
151+
});
152+
153+
context.subscriptions.push(onNotificationDispose);
154+
155+
const onDeleteFilesDispose = workspace.onDidDeleteFiles((event) => {
156+
for (const fileUri of event.files) {
157+
client?.diagnostics?.delete(fileUri);
158+
}
159+
});
160+
161+
context.subscriptions.push(onDeleteFilesDispose);
162+
163+
configService.onConfigChange = async function onConfigChange(event) {
164+
updateStatsBar(context, this.vsCodeConfig.enable);
165+
166+
if (client === undefined) {
167+
return;
168+
}
169+
170+
// update the initializationOptions for a possible restart
171+
client.clientOptions.initializationOptions = this.languageServerConfig;
172+
173+
if (configService.effectsWorkspaceConfigChange(event) && client.isRunning()) {
174+
await client.sendNotification('workspace/didChangeConfiguration', {
175+
settings: this.languageServerConfig,
176+
});
177+
}
178+
};
179+
180+
if (configService.vsCodeConfig.enable) {
181+
await client.start();
182+
}
183+
}
184+
185+
export async function deactivate(): Promise<void> {
186+
if (!client) {
187+
return undefined;
188+
}
189+
await client.stop();
190+
client = undefined;
191+
}
192+
193+
function updateStatsBar(context: ExtensionContext, enable: boolean) {
194+
if (!myStatusBarItem) {
195+
myStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 100);
196+
myStatusBarItem.command = OxcCommands.ToggleEnable;
197+
context.subscriptions.push(myStatusBarItem);
198+
myStatusBarItem.show();
199+
}
200+
let bgColor: string;
201+
let icon: string;
202+
if (!enable) {
203+
bgColor = 'statusBarItem.warningBackground';
204+
icon = '$(check)';
205+
} else {
206+
bgColor = 'statusBarItem.activeBackground';
207+
icon = '$(check-all)';
208+
}
209+
210+
myStatusBarItem.text = `${icon} oxc (fmt)`;
211+
myStatusBarItem.backgroundColor = new ThemeColor(bgColor);
212+
}
213+
214+
export async function restartClient(): Promise<void> {
215+
if (client === undefined) {
216+
window.showErrorMessage('oxc client not found');
217+
return;
218+
}
219+
220+
try {
221+
if (client.isRunning()) {
222+
await client.restart();
223+
window.showInformationMessage('oxc server restarted.');
224+
} else {
225+
await client.start();
226+
}
227+
} catch (err) {
228+
client.error('Restarting client failed', err, 'force');
229+
}
230+
}
231+
232+
export async function toggleClient(configService: ConfigService): Promise<void> {
233+
if (client === undefined) {
234+
return;
235+
}
236+
237+
if (client.isRunning()) {
238+
if (!configService.vsCodeConfig.enable) {
239+
await client.stop();
240+
}
241+
} else {
242+
if (configService.vsCodeConfig.enable) {
243+
await client.start();
244+
}
245+
}
246+
}
247+
248+
export async function onConfigChange(
249+
context: ExtensionContext,
250+
event: ConfigurationChangeEvent,
251+
configService: ConfigService,
252+
): Promise<void> {
253+
updateStatsBar(context, configService.vsCodeConfig.enable);
254+
255+
if (client === undefined) {
256+
return;
257+
}
258+
259+
// update the initializationOptions for a possible restart
260+
client.clientOptions.initializationOptions = configService.languageServerConfig;
261+
262+
if (configService.effectsWorkspaceConfigChange(event) && client.isRunning()) {
263+
await client.sendNotification('workspace/didChangeConfiguration', {
264+
settings: configService.languageServerConfig,
265+
});
266+
}
267+
}

editors/vscode/tests/commands.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ teardown(async () => {
2929

3030
suite('commands', () => {
3131
testSingleFolderMode('listed commands', async () => {
32+
// Skip tests if linter tests are disabled
33+
if (process.env.SKIP_LINTER_TEST === 'true') {
34+
return;
35+
}
36+
3237
const oxcCommands = (await commands.getCommands(true)).filter(x => x.startsWith('oxc.'));
3338

3439
const extraCommands = process.env.SKIP_LINTER_TEST === 'true' ? [] : [

0 commit comments

Comments
 (0)