|
1 | | -import { promises as fsPromises } from 'node:fs'; |
| 1 | +import { commands, ExtensionContext, window, workspace } from 'vscode'; |
2 | 2 |
|
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'; |
24 | 4 | 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'; |
26 | 12 |
|
27 | | -const languageClientName = 'oxc'; |
28 | 13 | 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; |
49 | 14 |
|
50 | 15 | export async function activate(context: ExtensionContext) { |
51 | 16 | const configService = new ConfigService(); |
52 | | - allowedToStartServer = configService.vsCodeConfig.requireConfig |
53 | | - ? (await workspace.findFiles(`**/.oxlintrc.json`, '**/node_modules/**', 1)).length > 0 |
54 | | - : true; |
55 | 17 |
|
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 | + }); |
61 | 21 |
|
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(); |
72 | 24 | }); |
73 | 25 |
|
74 | 26 | const showOutputCommand = commands.registerCommand(OxcCommands.ShowOutputChannel, () => { |
75 | | - client?.outputChannel?.show(); |
| 27 | + outputChannel.show(); |
76 | 28 | }); |
77 | 29 |
|
78 | 30 | const toggleEnable = commands.registerCommand(OxcCommands.ToggleEnable, async () => { |
79 | 31 | await configService.vsCodeConfig.updateEnable(!configService.vsCodeConfig.enable); |
80 | 32 |
|
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); |
94 | 34 | }); |
95 | 35 |
|
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); |
100 | 39 | } |
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); |
105 | 42 | } |
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, |
121 | 43 | }); |
122 | 44 |
|
123 | 45 | context.subscriptions.push( |
124 | | - applyAllFixesFile, |
125 | 46 | restartCommand, |
126 | 47 | showOutputCommand, |
127 | 48 | toggleEnable, |
128 | 49 | configService, |
129 | 50 | outputChannel, |
| 51 | + onDidChangeWorkspaceFoldersDispose, |
130 | 52 | ); |
131 | 53 |
|
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 | | - |
278 | 54 | 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); |
293 | 56 | }; |
294 | 57 |
|
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); |
303 | 59 | } |
304 | 60 |
|
305 | 61 | 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(); |
359 | 63 | } |
0 commit comments