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' ;
243import { ConfigService } from './ConfigService' ;
25- import { VSCodeConfig } from './VSCodeConfig' ;
4+ import {
5+ activate as activateLinter ,
6+ deactivate as deactivateLinter ,
7+ onConfigChange as onConfigChangeLinter ,
8+ restartClient ,
9+ toggleClient ,
10+ } from './linter' ;
2611
27- const languageClientName = 'oxc' ;
2812const outputChannelName = 'Oxc' ;
2913const commandPrefix = 'oxc' ;
3014
@@ -35,325 +19,52 @@ const enum OxcCommands {
3519 ToggleEnable = `${ commandPrefix } .toggleEnable` ,
3620}
3721
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-
5022export async function activate ( context : ExtensionContext ) {
5123 const configService = new ConfigService ( ) ;
52- allowedToStartServer = configService . vsCodeConfig . requireConfig
53- ? ( await workspace . findFiles ( `**/.oxlintrc.json` , '**/node_modules/**' , 1 ) ) . length > 0
54- : true ;
5524
56- const restartCommand = commands . registerCommand ( OxcCommands . RestartServer , async ( ) => {
57- if ( client === undefined ) {
58- window . showErrorMessage ( 'oxc client not found' ) ;
59- return ;
60- }
25+ const outputChannel = window . createOutputChannel ( outputChannelName , {
26+ log : true ,
27+ } ) ;
6128
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- }
29+ const restartCommand = commands . registerCommand ( OxcCommands . RestartServer , async ( ) => {
30+ await restartClient ( ) ;
7231 } ) ;
7332
7433 const showOutputCommand = commands . registerCommand ( OxcCommands . ShowOutputChannel , ( ) => {
75- client ?. outputChannel ? .show ( ) ;
34+ outputChannel . show ( ) ;
7635 } ) ;
7736
7837 const toggleEnable = commands . registerCommand ( OxcCommands . ToggleEnable , async ( ) => {
7938 await configService . vsCodeConfig . updateEnable ( ! configService . vsCodeConfig . enable ) ;
8039
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- }
40+ await toggleClient ( configService ) ;
9441 } ) ;
9542
96- const applyAllFixesFile = commands . registerCommand ( OxcCommands . ApplyAllFixesFile , async ( ) => {
97- if ( ! client ) {
98- window . showErrorMessage ( 'oxc client not found' ) ;
99- return ;
43+ const onDidChangeWorkspaceFoldersDispose = workspace . onDidChangeWorkspaceFolders ( async ( event ) => {
44+ for ( const folder of event . added ) {
45+ configService . addWorkspaceConfig ( folder ) ;
10046 }
101- const textEditor = window . activeTextEditor ;
102- if ( ! textEditor ) {
103- window . showErrorMessage ( 'active text editor not found' ) ;
104- return ;
47+ for ( const folder of event . removed ) {
48+ configService . removeWorkspaceConfig ( folder ) ;
10549 }
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 ,
12150 } ) ;
12251
12352 context . subscriptions . push (
124- applyAllFixesFile ,
12553 restartCommand ,
12654 showOutputCommand ,
12755 toggleEnable ,
12856 configService ,
12957 outputChannel ,
58+ onDidChangeWorkspaceFoldersDispose ,
13059 ) ;
13160
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-
27861 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- }
62+ await onConfigChangeLinter ( context , event , configService ) ;
29363 } ;
29464
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- }
65+ activateLinter ( context , outputChannel , configService ) ;
30366}
30467
30568export 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 ) ;
69+ await deactivateLinter ( ) ;
35970}
0 commit comments