Skip to content

Commit c922d13

Browse files
committed
refactor(vscode): split lsp releated code to linter.ts
1 parent bd99750 commit c922d13

File tree

3 files changed

+382
-321
lines changed

3 files changed

+382
-321
lines changed

editors/vscode/client/commands.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const commandPrefix = 'oxc';
2+
3+
export const enum OxcCommands {
4+
RestartServer = `${commandPrefix}.restartServer`,
5+
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
6+
ToggleEnable = `${commandPrefix}.toggleEnable`,
7+
// only for linter.ts usage
8+
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,
9+
}

editors/vscode/client/extension.ts

Lines changed: 25 additions & 321 deletions
Original file line numberDiff line numberDiff line change
@@ -1,359 +1,63 @@
1-
import { promises as fsPromises } from 'node:fs';
1+
import { commands, ExtensionContext, window, workspace } from 'vscode';
22

3-
import {
4-
commands,
5-
ExtensionContext,
6-
StatusBarAlignment,
7-
StatusBarItem,
8-
ThemeColor,
9-
Uri,
10-
window,
11-
workspace,
12-
} from 'vscode';
13-
14-
import {
15-
ConfigurationParams,
16-
ExecuteCommandRequest,
17-
MessageType,
18-
ShowMessageNotification,
19-
} from 'vscode-languageclient';
20-
21-
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
22-
23-
import { join } from 'node:path';
3+
import { OxcCommands } from './commands';
244
import { ConfigService } from './ConfigService';
25-
import { VSCodeConfig } from './VSCodeConfig';
5+
import {
6+
activate as activateLinter,
7+
deactivate as deactivateLinter,
8+
onConfigChange as onConfigChangeLinter,
9+
restartClient,
10+
toggleClient,
11+
} from './linter';
2612

27-
const languageClientName = 'oxc';
2813
const outputChannelName = 'Oxc';
29-
const commandPrefix = 'oxc';
30-
31-
const enum OxcCommands {
32-
RestartServer = `${commandPrefix}.restartServer`,
33-
ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,
34-
ShowOutputChannel = `${commandPrefix}.showOutputChannel`,
35-
ToggleEnable = `${commandPrefix}.toggleEnable`,
36-
}
37-
38-
const enum LspCommands {
39-
FixAll = 'oxc.fixAll',
40-
}
41-
42-
let client: LanguageClient | undefined;
43-
44-
let myStatusBarItem: StatusBarItem;
45-
46-
// Global flag to check if the user allows us to start the server.
47-
// When `oxc.requireConfig` is `true`, make sure one `.oxlintrc.json` file is present.
48-
let allowedToStartServer: boolean;
4914

5015
export async function activate(context: ExtensionContext) {
5116
const configService = new ConfigService();
52-
allowedToStartServer = configService.vsCodeConfig.requireConfig
53-
? (await workspace.findFiles(`**/.oxlintrc.json`, '**/node_modules/**', 1)).length > 0
54-
: true;
5517

56-
const restartCommand = commands.registerCommand(OxcCommands.RestartServer, async () => {
57-
if (client === undefined) {
58-
window.showErrorMessage('oxc client not found');
59-
return;
60-
}
18+
const outputChannel = window.createOutputChannel(outputChannelName, {
19+
log: true,
20+
});
6121

62-
try {
63-
if (client.isRunning()) {
64-
await client.restart();
65-
window.showInformationMessage('oxc server restarted.');
66-
} else {
67-
await client.start();
68-
}
69-
} catch (err) {
70-
client.error('Restarting client failed', err, 'force');
71-
}
22+
const restartCommand = commands.registerCommand(OxcCommands.RestartServer, async () => {
23+
await restartClient();
7224
});
7325

7426
const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => {
75-
client?.outputChannel?.show();
27+
outputChannel.show();
7628
});
7729

7830
const toggleEnable = commands.registerCommand(OxcCommands.ToggleEnable, async () => {
7931
await configService.vsCodeConfig.updateEnable(!configService.vsCodeConfig.enable);
8032

81-
if (client === undefined || !allowedToStartServer) {
82-
return;
83-
}
84-
85-
if (client.isRunning()) {
86-
if (!configService.vsCodeConfig.enable) {
87-
await client.stop();
88-
}
89-
} else {
90-
if (configService.vsCodeConfig.enable) {
91-
await client.start();
92-
}
93-
}
33+
await toggleClient(configService);
9434
});
9535

96-
const applyAllFixesFile = commands.registerCommand(OxcCommands.ApplyAllFixesFile, async () => {
97-
if (!client) {
98-
window.showErrorMessage('oxc client not found');
99-
return;
36+
const onDidChangeWorkspaceFoldersDispose = workspace.onDidChangeWorkspaceFolders(async (event) => {
37+
for (const folder of event.added) {
38+
configService.addWorkspaceConfig(folder);
10039
}
101-
const textEditor = window.activeTextEditor;
102-
if (!textEditor) {
103-
window.showErrorMessage('active text editor not found');
104-
return;
40+
for (const folder of event.removed) {
41+
configService.removeWorkspaceConfig(folder);
10542
}
106-
107-
const params = {
108-
command: LspCommands.FixAll,
109-
arguments: [
110-
{
111-
uri: textEditor.document.uri.toString(),
112-
},
113-
],
114-
};
115-
116-
await client.sendRequest(ExecuteCommandRequest.type, params);
117-
});
118-
119-
const outputChannel = window.createOutputChannel(outputChannelName, {
120-
log: true,
12143
});
12244

12345
context.subscriptions.push(
124-
applyAllFixesFile,
12546
restartCommand,
12647
showOutputCommand,
12748
toggleEnable,
12849
configService,
12950
outputChannel,
51+
onDidChangeWorkspaceFoldersDispose,
13052
);
13153

132-
async function findBinary(): Promise<string> {
133-
const bin = configService.getUserServerBinPath();
134-
if (workspace.isTrusted && bin) {
135-
try {
136-
await fsPromises.access(bin);
137-
return bin;
138-
} catch (e) {
139-
outputChannel.error(`Invalid bin path: ${bin}`, e);
140-
}
141-
}
142-
const ext = process.platform === 'win32' ? '.exe' : '';
143-
// NOTE: The `./target/release` path is aligned with the path defined in .github/workflows/release_vscode.yml
144-
return process.env.SERVER_PATH_DEV ?? join(context.extensionPath, `./target/release/oxc_language_server${ext}`);
145-
}
146-
147-
const nodePath = configService.vsCodeConfig.nodePath;
148-
const serverEnv: Record<string, string> = {
149-
...process.env,
150-
RUST_LOG: process.env.RUST_LOG || 'info',
151-
};
152-
if (nodePath) {
153-
serverEnv.PATH = `${nodePath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`;
154-
}
155-
156-
const path = await findBinary();
157-
158-
const run: Executable =
159-
process.env.OXLINT_LSP_TEST === 'true'
160-
? {
161-
command: 'node',
162-
args: [path!, '--lsp'],
163-
options: {
164-
env: serverEnv,
165-
},
166-
}
167-
: {
168-
command: path!,
169-
args: ['--lsp'],
170-
options: {
171-
// On Windows we need to run the binary in a shell to be able to execute the shell npm bin script.
172-
// Searching for the right `.exe` file inside `node_modules/` is not reliable as it depends on
173-
// the package manager used (npm, yarn, pnpm, etc) and the package version.
174-
// The npm bin script is a shell script that points to the actual binary.
175-
// Security: We validated the userDefinedBinary in `configService.getUserServerBinPath()`.
176-
shell: process.platform === 'win32',
177-
env: serverEnv,
178-
},
179-
};
180-
181-
const serverOptions: ServerOptions = {
182-
run,
183-
debug: run,
184-
};
185-
186-
outputChannel.info(`Using server binary at: ${path}`);
187-
188-
// see https://github.com/oxc-project/oxc/blob/9b475ad05b750f99762d63094174be6f6fc3c0eb/crates/oxc_linter/src/loader/partial_loader/mod.rs#L17-L20
189-
const supportedExtensions = ['astro', 'cjs', 'cts', 'js', 'jsx', 'mjs', 'mts', 'svelte', 'ts', 'tsx', 'vue'];
190-
191-
// If the extension is launched in debug mode then the debug server options are used
192-
// Otherwise the run options are used
193-
// Options to control the language client
194-
let clientOptions: LanguageClientOptions = {
195-
// Register the server for plain text documents
196-
documentSelector: [
197-
{
198-
pattern: `**/*.{${supportedExtensions.join(',')}}`,
199-
scheme: 'file',
200-
},
201-
],
202-
initializationOptions: configService.languageServerConfig,
203-
outputChannel,
204-
traceOutputChannel: outputChannel,
205-
middleware: {
206-
handleDiagnostics: (uri, diagnostics, next) => {
207-
for (const diag of diagnostics) {
208-
// https://github.com/oxc-project/oxc/issues/12404
209-
if (typeof diag.code === 'object' && diag.code?.value === 'eslint-plugin-unicorn(filename-case)') {
210-
diag.message += '\nYou may need to close the file and restart VSCode after renaming a file by only casing.';
211-
}
212-
}
213-
next(uri, diagnostics);
214-
},
215-
workspace: {
216-
configuration: (params: ConfigurationParams) => {
217-
return params.items.map((item) => {
218-
if (item.section !== 'oxc_language_server') {
219-
return null;
220-
}
221-
if (item.scopeUri === undefined) {
222-
return null;
223-
}
224-
225-
return configService.getWorkspaceConfig(Uri.parse(item.scopeUri))?.toLanguageServerConfig() ?? null;
226-
});
227-
},
228-
},
229-
},
230-
};
231-
232-
// Create the language client and start the client.
233-
client = new LanguageClient(languageClientName, serverOptions, clientOptions);
234-
235-
const onNotificationDispose = client.onNotification(ShowMessageNotification.type, (params) => {
236-
switch (params.type) {
237-
case MessageType.Debug:
238-
outputChannel.debug(params.message);
239-
break;
240-
case MessageType.Log:
241-
outputChannel.info(params.message);
242-
break;
243-
case MessageType.Info:
244-
window.showInformationMessage(params.message);
245-
break;
246-
case MessageType.Warning:
247-
window.showWarningMessage(params.message);
248-
break;
249-
case MessageType.Error:
250-
window.showErrorMessage(params.message);
251-
break;
252-
default:
253-
outputChannel.info(params.message);
254-
}
255-
});
256-
257-
context.subscriptions.push(onNotificationDispose);
258-
259-
const onDeleteFilesDispose = workspace.onDidDeleteFiles((event) => {
260-
for (const fileUri of event.files) {
261-
client?.diagnostics?.delete(fileUri);
262-
}
263-
});
264-
265-
context.subscriptions.push(onDeleteFilesDispose);
266-
267-
const onDidChangeWorkspaceFoldersDispose = workspace.onDidChangeWorkspaceFolders(async (event) => {
268-
for (const folder of event.added) {
269-
configService.addWorkspaceConfig(folder);
270-
}
271-
for (const folder of event.removed) {
272-
configService.removeWorkspaceConfig(folder);
273-
}
274-
});
275-
276-
context.subscriptions.push(onDidChangeWorkspaceFoldersDispose);
277-
27854
configService.onConfigChange = async function onConfigChange(event) {
279-
updateStatsBar(context, this.vsCodeConfig.enable);
280-
281-
if (client === undefined) {
282-
return;
283-
}
284-
285-
// update the initializationOptions for a possible restart
286-
client.clientOptions.initializationOptions = this.languageServerConfig;
287-
288-
if (configService.effectsWorkspaceConfigChange(event) && client.isRunning()) {
289-
await client.sendNotification('workspace/didChangeConfiguration', {
290-
settings: this.languageServerConfig,
291-
});
292-
}
55+
await onConfigChangeLinter(context, event, configService);
29356
};
29457

295-
updateStatsBar(context, configService.vsCodeConfig.enable);
296-
if (allowedToStartServer) {
297-
if (configService.vsCodeConfig.enable) {
298-
await client.start();
299-
}
300-
} else {
301-
generateActivatorByConfig(configService.vsCodeConfig, context);
302-
}
58+
await activateLinter(context, outputChannel, configService);
30359
}
30460

30561
export async function deactivate(): Promise<void> {
306-
if (!client) {
307-
return undefined;
308-
}
309-
await client.stop();
310-
client = undefined;
311-
}
312-
313-
function updateStatsBar(context: ExtensionContext, enable: boolean) {
314-
if (!myStatusBarItem) {
315-
myStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 100);
316-
myStatusBarItem.command = OxcCommands.ToggleEnable;
317-
context.subscriptions.push(myStatusBarItem);
318-
myStatusBarItem.show();
319-
}
320-
let bgColor: string;
321-
let icon: string;
322-
if (!allowedToStartServer) {
323-
bgColor = 'statusBarItem.offlineBackground';
324-
icon = '$(circle-slash)';
325-
} else if (!enable) {
326-
bgColor = 'statusBarItem.warningBackground';
327-
icon = '$(check)';
328-
} else {
329-
bgColor = 'statusBarItem.activeBackground';
330-
icon = '$(check-all)';
331-
}
332-
333-
myStatusBarItem.text = `${icon} oxc`;
334-
myStatusBarItem.backgroundColor = new ThemeColor(bgColor);
335-
}
336-
337-
function generateActivatorByConfig(config: VSCodeConfig, context: ExtensionContext): void {
338-
const watcher = workspace.createFileSystemWatcher('**/.oxlintrc.json', false, true, !config.requireConfig);
339-
watcher.onDidCreate(async () => {
340-
allowedToStartServer = true;
341-
updateStatsBar(context, config.enable);
342-
if (client && !client.isRunning() && config.enable) {
343-
await client.start();
344-
}
345-
});
346-
347-
watcher.onDidDelete(async () => {
348-
// only can be called when config.requireConfig
349-
allowedToStartServer = (await workspace.findFiles(`**/.oxlintrc.json`, '**/node_modules/**', 1)).length > 0;
350-
if (!allowedToStartServer) {
351-
updateStatsBar(context, false);
352-
if (client && client.isRunning()) {
353-
await client.stop();
354-
}
355-
}
356-
});
357-
358-
context.subscriptions.push(watcher);
62+
await deactivateLinter();
35963
}

0 commit comments

Comments
 (0)