Skip to content

Commit e1bc037

Browse files
committed
feat(language_server): request for workspace configuration when client did not send them in initialize (#10789)
``` 2025-05-04 12:10:02.510 [info] [Trace - 12:10:02 PM] Sending notification 'initialized'. 2025-05-04 12:10:02.510 [info] Params: {} 2025-05-04 12:10:02.511 [info] [Trace - 12:10:02 PM] Sending notification 'textDocument/didOpen'. 2025-05-04 12:10:02.511 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts", "languageId": "typescript", "version": 13, "text": "import .....\n" } } 2025-05-04 12:10:02.511 [info] [Trace - 12:10:02 PM] Received request 'workspace/configuration - (0)'. 2025-05-04 12:10:02.511 [info] Params: { "items": [ { "scopeUri": "file:///home/sysix/dev/oxc", "section": "oxc_language_server" } ] } 2025-05-04 12:10:02.511 [info] [Trace - 12:10:02 PM] Sending response 'workspace/configuration - (0)'. Processing request took 0ms 2025-05-04 12:10:02.511 [info] Result: [ { "run": "onType", "configPath": null, "flags": {} } ] ``` Someone (like me) would expect that the `textDocument/didOpen` will not result into any linting. But the Async Locks are preventing it (that what I could observe): ``` 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending request 'initialize - (0)'. 2025-05-07 14:32:20.901 linting file file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts 2025-05-07 14:32:20.907 [info] [Trace - 2:32:20 PM] Received notification 'textDocument/publishDiagnostics'. ``` <details> <summary>Full Log</summary> ```` 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending request 'initialize - (0)'. 2025-05-07 14:32:20.353 [info] Params: { "processId": 65760, "clientInfo": { "name": "Visual Studio Code", "version": "1.99.3" }, "locale": "en", "rootPath": "/home/sysix/dev/oxc", "rootUri": "file:///home/sysix/dev/oxc", "capabilities": { "workspace": { "applyEdit": true, "workspaceEdit": { "documentChanges": true, "resourceOperations": [ "create", "rename", "delete" ], "failureHandling": "textOnlyTransactional", "normalizesLineEndings": true, "changeAnnotationSupport": { "groupsOnLabel": true } }, "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true, "relativePatternSupport": true }, "symbol": { "dynamicRegistration": true, "symbolKind": { "valueSet": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 ] }, "tagSupport": { "valueSet": [ 1 ] }, "resolveSupport": { "properties": [ "location.range" ] } }, "codeLens": { "refreshSupport": true }, "executeCommand": { "dynamicRegistration": true }, "didChangeConfiguration": { "dynamicRegistration": true }, "workspaceFolders": true, "foldingRange": { "refreshSupport": true }, "semanticTokens": { "refreshSupport": true }, "fileOperations": { "dynamicRegistration": true, "didCreate": true, "didRename": true, "didDelete": true, "willCreate": true, "willRename": true, "willDelete": true }, "inlineValue": { "refreshSupport": true }, "inlayHint": { "refreshSupport": true }, "diagnostics": { "refreshSupport": true } }, "textDocument": { "publishDiagnostics": { "relatedInformation": true, "versionSupport": false, "tagSupport": { "valueSet": [ 1, 2 ] }, "codeDescriptionSupport": true, "dataSupport": true }, "synchronization": { "dynamicRegistration": true, "willSave": true, "willSaveWaitUntil": true, "didSave": true }, "completion": { "dynamicRegistration": true, "contextSupport": true, "completionItem": { "snippetSupport": true, "commitCharactersSupport": true, "documentationFormat": [ "markdown", "plaintext" ], "deprecatedSupport": true, "preselectSupport": true, "tagSupport": { "valueSet": [ 1 ] }, "insertReplaceSupport": true, "resolveSupport": { "properties": [ "documentation", "detail", "additionalTextEdits" ] }, "insertTextModeSupport": { "valueSet": [ 1, 2 ] }, "labelDetailsSupport": true }, "insertTextMode": 2, "completionItemKind": { "valueSet": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 ] }, "completionList": { "itemDefaults": [ "commitCharacters", "editRange", "insertTextFormat", "insertTextMode", "data" ] } }, "hover": { "dynamicRegistration": true, "contentFormat": [ "markdown", "plaintext" ] }, "signatureHelp": { "dynamicRegistration": true, "signatureInformation": { "documentationFormat": [ "markdown", "plaintext" ], "parameterInformation": { "labelOffsetSupport": true }, "activeParameterSupport": true }, "contextSupport": true }, "definition": { "dynamicRegistration": true, "linkSupport": true }, "references": { "dynamicRegistration": true }, "documentHighlight": { "dynamicRegistration": true }, "documentSymbol": { "dynamicRegistration": true, "symbolKind": { "valueSet": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 ] }, "hierarchicalDocumentSymbolSupport": true, "tagSupport": { "valueSet": [ 1 ] }, "labelSupport": true }, "codeAction": { "dynamicRegistration": true, "isPreferredSupport": true, "disabledSupport": true, "dataSupport": true, "resolveSupport": { "properties": [ "edit" ] }, "codeActionLiteralSupport": { "codeActionKind": { "valueSet": [ "", "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports" ] } }, "honorsChangeAnnotations": true }, "codeLens": { "dynamicRegistration": true }, "formatting": { "dynamicRegistration": true }, "rangeFormatting": { "dynamicRegistration": true, "rangesSupport": true }, "onTypeFormatting": { "dynamicRegistration": true }, "rename": { "dynamicRegistration": true, "prepareSupport": true, "prepareSupportDefaultBehavior": 1, "honorsChangeAnnotations": true }, "documentLink": { "dynamicRegistration": true, "tooltipSupport": true }, "typeDefinition": { "dynamicRegistration": true, "linkSupport": true }, "implementation": { "dynamicRegistration": true, "linkSupport": true }, "colorProvider": { "dynamicRegistration": true }, "foldingRange": { "dynamicRegistration": true, "rangeLimit": 5000, "lineFoldingOnly": true, "foldingRangeKind": { "valueSet": [ "comment", "imports", "region" ] }, "foldingRange": { "collapsedText": false } }, "declaration": { "dynamicRegistration": true, "linkSupport": true }, "selectionRange": { "dynamicRegistration": true }, "callHierarchy": { "dynamicRegistration": true }, "semanticTokens": { "dynamicRegistration": true, "tokenTypes": [ "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", "decorator" ], "tokenModifiers": [ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary" ], "formats": [ "relative" ], "requests": { "range": true, "full": { "delta": true } }, "multilineTokenSupport": false, "overlappingTokenSupport": false, "serverCancelSupport": true, "augmentsSyntaxTokens": true }, "linkedEditingRange": { "dynamicRegistration": true }, "typeHierarchy": { "dynamicRegistration": true }, "inlineValue": { "dynamicRegistration": true }, "inlayHint": { "dynamicRegistration": true, "resolveSupport": { "properties": [ "tooltip", "textEdits", "label.tooltip", "label.location", "label.command" ] } }, "diagnostic": { "dynamicRegistration": true, "relatedDocumentSupport": false } }, "window": { "showMessage": { "messageActionItem": { "additionalPropertiesSupport": true } }, "showDocument": { "support": true }, "workDoneProgress": true }, "general": { "staleRequestSupport": { "cancel": true, "retryOnContentModified": [ "textDocument/semanticTokens/full", "textDocument/semanticTokens/range", "textDocument/semanticTokens/full/delta" ] }, "regularExpressions": { "engine": "ECMAScript", "version": "ES2020" }, "markdown": { "parser": "marked", "version": "1.1.0" }, "positionEncodings": [ "utf-16" ] }, "notebookDocument": { "synchronization": { "dynamicRegistration": true, "executionSummarySupport": true } } }, "trace": "verbose", "workspaceFolders": [ { "uri": "file:///home/sysix/dev/oxc", "name": "oxc" } ] } 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Received response 'initialize - (0)' in 49ms. 2025-05-07 14:32:20.353 [info] Result: { "capabilities": { "textDocumentSync": 1, "codeActionProvider": { "codeActionKinds": [ "quickfix", "source.fixAll.oxc" ] }, "executeCommandProvider": { "commands": [ "oxc.fixAll" ] }, "workspace": { "workspaceFolders": { "supported": true, "changeNotifications": true } } }, "serverInfo": { "name": "oxc", "version": "0.16.9" } } 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending notification 'initialized'. 2025-05-07 14:32:20.353 [info] Params: {} 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending notification 'textDocument/didOpen'. 2025-05-07 14:32:20.353 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts", "languageId": "typescript", "version": 1, "text": "import { promises as fsPromises } from 'node:fs';\n\nimport {\n commands,\n ExtensionContext,\n FileSystemWatcher,\n RelativePattern,\n StatusBarAlignment,\n StatusBarItem,\n ThemeColor,\n window,\n workspace,\n} from 'vscode';\n\nimport {\n ConfigurationParams,\n ExecuteCommandRequest,\n MessageType,\n ShowMessageNotification,\n} from 'vscode-languageclient';\n\nimport { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';\n\nimport { join } from 'node:path';\nimport { ConfigService } from './ConfigService';\nimport { oxlintConfigFileName } from './VSCodeConfig';\n\nconst languageClientName = 'oxc';\nconst outputChannelName = 'Oxc';\nconst commandPrefix = 'oxc';\n\nconst enum OxcCommands {\n RestartServer = `${commandPrefix}.restartServer`,\n ApplyAllFixesFile = `${commandPrefix}.applyAllFixesFile`,\n ShowOutputChannel = `${commandPrefix}.showOutputChannel`,\n ToggleEnable = `${commandPrefix}.toggleEnable`,\n}\n\nconst enum LspCommands {\n FixAll = 'oxc.fixAll',\n}\n\nlet client: LanguageClient | undefined;\n\nlet myStatusBarItem: StatusBarItem;\n\nconst globalWatchers: FileSystemWatcher[] = [];\n\nexport async function activate(context: ExtensionContext) {\n const configService = new ConfigService();\n const restartCommand = commands.registerCommand(\n OxcCommands.RestartServer,\n async () => {\n if (client === undefined) {\n window.showErrorMessage('oxc client not found');\n return;\n }\n\n try {\n if (client.isRunning()) {\n await client.restart();\n window.showInformationMessage('oxc server restarted.');\n } else {\n await client.start();\n }\n } catch (err) {\n client.error('Restarting client failed', err, 'force');\n }\n },\n );\n\n const showOutputCommand = commands.registerCommand(\n OxcCommands.ShowOutputChannel,\n () => {\n client?.outputChannel?.show();\n },\n );\n\n const toggleEnable = commands.registerCommand(\n OxcCommands.ToggleEnable,\n async () => {\n await configService.vsCodeConfig.updateEnable(!configService.vsCodeConfig.enable);\n\n if (client === undefined) {\n return;\n }\n\n if (client.isRunning()) {\n if (!configService.vsCodeConfig.enable) {\n await client.stop();\n }\n } else {\n if (configService.vsCodeConfig.enable) {\n await client.start();\n }\n }\n },\n );\n\n const applyAllFixesFile = commands.registerCommand(\n OxcCommands.ApplyAllFixesFile,\n async () => {\n if (!client) {\n window.showErrorMessage('oxc client not found');\n return;\n }\n const textEditor = window.activeTextEditor;\n if (!textEditor) {\n window.showErrorMessage('active text editor not found');\n return;\n }\n\n const params = {\n command: LspCommands.FixAll,\n arguments: [{\n uri: textEditor.document.uri.toString(),\n }],\n };\n\n await client.sendRequest(ExecuteCommandRequest.type, params);\n },\n );\n\n const outputChannel = window.createOutputChannel(outputChannelName, { log: true });\n const fileWatchers = createFileEventWatchers(configService.rootServerConfig.configPath);\n\n context.subscriptions.push(\n applyAllFixesFile,\n restartCommand,\n showOutputCommand,\n toggleEnable,\n configService,\n outputChannel,\n {\n dispose: () => {\n globalWatchers.forEach((watcher) => watcher.dispose());\n },\n },\n );\n\n async function findBinary(): Promise<string> {\n let bin = configService.vsCodeConfig.binPath;\n if (bin) {\n try {\n await fsPromises.access(bin);\n return bin;\n } catch (e) {\n outputChannel.error(`Invalid bin path: ${bin}`, e);\n }\n }\n\n const workspaceFolders = workspace.workspaceFolders;\n const isWindows = process.platform === 'win32';\n\n if (workspaceFolders?.length && !isWindows) {\n try {\n return await Promise.any(\n workspaceFolders.map(async (folder) => {\n const binPath = join(\n folder.uri.fsPath,\n 'node_modules',\n '.bin',\n 'oxc_language_server',\n );\n\n await fsPromises.access(binPath);\n return binPath;\n }),\n );\n } catch {}\n }\n\n const ext = isWindows ? '.exe' : '';\n // NOTE: The `./target/release` path is aligned with the path defined in .github/workflows/release_vscode.yml\n return (\n process.env.SERVER_PATH_DEV ??\n join(context.extensionPath, `./target/release/oxc_language_server${ext}`)\n );\n }\n\n const command = await findBinary();\n const run: Executable = {\n command: command!,\n options: {\n env: {\n ...process.env,\n RUST_LOG: process.env.RUST_LOG || 'info',\n },\n },\n };\n const serverOptions: ServerOptions = {\n run,\n debug: run,\n };\n\n // If the extension is launched in debug mode then the debug server options are used\n // Otherwise the run options are used\n // Options to control the language client\n let clientOptions: LanguageClientOptions = {\n // Register the server for plain text documents\n documentSelector: [\n 'typescript',\n 'javascript',\n 'typescriptreact',\n 'javascriptreact',\n 'vue',\n 'svelte',\n ].map((lang) => ({\n language: lang,\n scheme: 'file',\n })),\n synchronize: {\n // Notify the server about file config changes in the workspace\n fileEvents: fileWatchers,\n },\n outputChannel,\n traceOutputChannel: outputChannel,\n middleware: {\n workspace: {\n configuration: (params: ConfigurationParams) => {\n return params.items.map(item => {\n if (item.section !== 'oxc_language_server') {\n return null;\n }\n\n return configService.rootServerConfig.toLanguageServerConfig() ?? null;\n });\n },\n },\n },\n };\n\n // Create the language client and start the client.\n client = new LanguageClient(\n languageClientName,\n serverOptions,\n clientOptions,\n );\n\n const onNotificationDispose = client.onNotification(ShowMessageNotification.type, (params) => {\n switch (params.type) {\n case MessageType.Debug:\n outputChannel.debug(params.message);\n break;\n case MessageType.Log:\n outputChannel.info(params.message);\n break;\n case MessageType.Info:\n window.showInformationMessage(params.message);\n break;\n case MessageType.Warning:\n window.showWarningMessage(params.message);\n break;\n case MessageType.Error:\n window.showErrorMessage(params.message);\n break;\n default:\n outputChannel.info(params.message);\n }\n });\n\n context.subscriptions.push(onNotificationDispose);\n\n const onDeleteFilesDispose = workspace.onDidDeleteFiles((event) => {\n for (const fileUri of event.files) {\n client?.diagnostics?.delete(fileUri);\n }\n });\n\n context.subscriptions.push(onDeleteFilesDispose);\n\n configService.onConfigChange = async function onConfigChange(event) {\n let settings = this.rootServerConfig.toLanguageServerConfig();\n updateStatsBar(this.vsCodeConfig.enable);\n\n if (client === undefined) {\n return;\n }\n\n // update the initializationOptions for a possible restart\n client.clientOptions.initializationOptions = { settings };\n\n if (event.affectsConfiguration('oxc.configPath')) {\n client.clientOptions.synchronize = client.clientOptions.synchronize ?? {};\n client.clientOptions.synchronize.fileEvents = createFileEventWatchers(settings.configPath);\n\n if (client.isRunning()) {\n await client.restart();\n }\n } else if (client.isRunning()) {\n await client.sendNotification('workspace/didChangeConfiguration', { settings });\n }\n };\n\n function updateStatsBar(enable: boolean) {\n if (!myStatusBarItem) {\n myStatusBarItem = window.createStatusBarItem(\n StatusBarAlignment.Right,\n 100,\n );\n myStatusBarItem.command = OxcCommands.ToggleEnable;\n context.subscriptions.push(myStatusBarItem);\n myStatusBarItem.show();\n }\n let bgColor = new ThemeColor(\n enable\n ? 'statusBarItem.activeBackground'\n : 'statusBarItem.warningBackground',\n );\n myStatusBarItem.text = `oxc: ${enable ? '$(check-all)' : '$(check)'}`;\n\n myStatusBarItem.backgroundColor = bgColor;\n }\n\n updateStatsBar(configService.vsCodeConfig.enable);\n if (configService.vsCodeConfig.enable) {\n await client.start();\n }\n}\n\nexport async function deactivate(): Promise<void> {\n if (!client) {\n return undefined;\n }\n await client.stop();\n client = undefined;\n}\n\n// FileSystemWatcher are not ready on the start and can take some seconds on bigger repositories\nfunction createFileEventWatchers(configRelativePath: string | null): FileSystemWatcher[] {\n // cleanup old watchers\n globalWatchers.forEach((watcher) => watcher.dispose());\n globalWatchers.length = 0;\n\n // create new watchers\n let localWatchers;\n if (configRelativePath !== null) {\n localWatchers = (workspace.workspaceFolders || []).map((workspaceFolder) =>\n workspace.createFileSystemWatcher(new RelativePattern(workspaceFolder, configRelativePath))\n );\n } else {\n localWatchers = [\n workspace.createFileSystemWatcher(`**/${oxlintConfigFileName}`),\n ];\n }\n\n // assign watchers to global variable, so we can cleanup them on next call\n globalWatchers.push(...localWatchers);\n\n return localWatchers;\n}\n" } } 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Received request 'workspace/configuration - (0)'. 2025-05-07 14:32:20.353 [info] Params: { "items": [ { "scopeUri": "file:///home/sysix/dev/oxc", "section": "oxc_language_server" } ] } 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending response 'workspace/configuration - (0)'. Processing request took 0ms 2025-05-07 14:32:20.353 [info] Result: [ { "run": "onType", "configPath": null, "flags": {} } ] 2025-05-07 14:32:20.353 [info] [Trace - 2:32:20 PM] Sending request 'textDocument/codeAction - (1)'. 2025-05-07 14:32:20.353 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts" }, "range": { "start": { "line": 236, "character": 18 }, "end": { "line": 236, "character": 18 } }, "context": { "diagnostics": [], "triggerKind": 2 } } 2025-05-07 14:32:20.358 [info] [Trace - 2:32:20 PM] Sending notification '$/cancelRequest'. 2025-05-07 14:32:20.358 [info] Params: { "id": 1 } 2025-05-07 14:32:20.394 [info] [Trace - 2:32:20 PM] Sending request 'textDocument/codeAction - (2)'. 2025-05-07 14:32:20.394 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts" }, "range": { "start": { "line": 236, "character": 18 }, "end": { "line": 236, "character": 18 } }, "context": { "diagnostics": [], "triggerKind": 2 } } 2025-05-07 14:32:20.857 [info] [Trace - 2:32:20 PM] Sending notification '$/cancelRequest'. 2025-05-07 14:32:20.857 [info] Params: { "id": 2 } 2025-05-07 14:32:20.868 [info] [Trace - 2:32:20 PM] Sending notification '$/setTrace'. 2025-05-07 14:32:20.868 [info] Params: { "value": "verbose" } 2025-05-07 14:32:20.874 [info] [Trace - 2:32:20 PM] Sending notification '$/setTrace'. 2025-05-07 14:32:20.874 [info] Params: { "value": "verbose" } 2025-05-07 14:32:20.881 [info] [Trace - 2:32:20 PM] Sending notification '$/setTrace'. 2025-05-07 14:32:20.881 [info] Params: { "value": "verbose" } 2025-05-07 14:32:20.901 [info] [2025-05-07T12:32:20Z INFO oxc_language_server] linting file file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts [2025-05-07T12:32:20Z INFO oxc_language_server] linting file succedded file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts 2025-05-07 14:32:20.902 [info] [Trace - 2:32:20 PM] Sending request 'textDocument/codeAction - (3)'. 2025-05-07 14:32:20.902 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts" }, "range": { "start": { "line": 236, "character": 18 }, "end": { "line": 236, "character": 18 } }, "context": { "diagnostics": [], "triggerKind": 2 } } 2025-05-07 14:32:20.902 [info] [Trace - 2:32:20 PM] Sending request 'textDocument/codeAction - (4)'. 2025-05-07 14:32:20.902 [info] Params: { "textDocument": { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts" }, "range": { "start": { "line": 236, "character": 18 }, "end": { "line": 236, "character": 18 } }, "context": { "diagnostics": [], "triggerKind": 2 } } 2025-05-07 14:32:20.903 [info] [Trace - 2:32:20 PM] Sending notification '$/cancelRequest'. 2025-05-07 14:32:20.903 [info] Params: { "id": 3 } 2025-05-07 14:32:20.907 [info] [Trace - 2:32:20 PM] Received notification 'textDocument/publishDiagnostics'. 2025-05-07 14:32:20.907 [info] Params: { "uri": "file:///home/sysix/dev/oxc/editors/vscode/client/extension.ts", "diagnostics": [], "version": 1 } ```` </details>
1 parent 9b94300 commit e1bc037

File tree

3 files changed

+102
-14
lines changed

3 files changed

+102
-14
lines changed

crates/oxc_language_server/src/main.rs

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,19 @@ impl LanguageServer for Backend {
8888
let settings = value.get_mut("settings")?.take();
8989
serde_json::from_value::<Options>(settings).ok()
9090
});
91+
let capabilities = Capabilities::from(params.capabilities);
9192

9293
// ToDo: add support for multiple workspace folders
9394
// maybe fallback when the client does not support it
9495
let root_worker = WorkspaceWorker::new(&params.root_uri.unwrap());
95-
// ToDo: only call init_linter when the client passed a valid Options struct
96-
// if not and the client supports `workspace/configuration`, we should request them in `initialized`
97-
root_worker.init_linter(&options.clone().unwrap_or_default()).await;
96+
97+
// When the client did not send our custom `initialization_options`,
98+
// or the client does not support `workspace/configuration` request,
99+
// start the linter. We do not start the linter when the client support the request,
100+
// we will init the linter after requesting for the workspace configuration.
101+
if !capabilities.workspace_configuration || options.is_some() {
102+
root_worker.init_linter(&options.clone().unwrap_or_default()).await;
103+
}
98104

99105
*self.workspace_workers.lock().await = vec![root_worker];
100106

@@ -103,7 +109,6 @@ impl LanguageServer for Backend {
103109
info!("language server version: {server_version}");
104110
}
105111

106-
let capabilities = Capabilities::from(params.capabilities);
107112
self.capabilities.set(capabilities.clone()).map_err(|err| {
108113
let message = match err {
109114
SetError::AlreadyInitializedError(_) => {
@@ -125,6 +130,44 @@ impl LanguageServer for Backend {
125130
})
126131
}
127132

133+
async fn initialized(&self, _params: InitializedParams) {
134+
debug!("oxc initialized.");
135+
136+
if !self.capabilities.get().unwrap().workspace_configuration {
137+
// every worker should be initialized already in `initialize` request
138+
return;
139+
}
140+
141+
let workers = &*self.workspace_workers.lock().await;
142+
let needed_configurations =
143+
ConcurrentHashMap::with_capacity_and_hasher(workers.len(), FxBuildHasher);
144+
let needed_configurations = needed_configurations.pin_owned();
145+
for worker in workers {
146+
if worker.needs_init_linter().await {
147+
needed_configurations.insert(worker.get_root_uri().unwrap(), worker);
148+
}
149+
}
150+
151+
let configurations =
152+
self.request_workspace_configuration(needed_configurations.keys().collect()).await;
153+
for (index, worker) in needed_configurations.values().enumerate() {
154+
worker
155+
.init_linter(
156+
configurations
157+
.get(index)
158+
.unwrap_or(&None)
159+
.as_ref()
160+
.unwrap_or(&Options::default()),
161+
)
162+
.await;
163+
}
164+
}
165+
166+
async fn shutdown(&self) -> Result<()> {
167+
self.clear_all_diagnostics().await;
168+
Ok(())
169+
}
170+
128171
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
129172
let workers = self.workspace_workers.lock().await;
130173
let new_diagnostics: papaya::HashMap<String, Vec<Diagnostic>, FxBuildHasher> =
@@ -255,15 +298,6 @@ impl LanguageServer for Backend {
255298
self.publish_all_diagnostics(x).await;
256299
}
257300

258-
async fn initialized(&self, _params: InitializedParams) {
259-
debug!("oxc initialized.");
260-
}
261-
262-
async fn shutdown(&self) -> Result<()> {
263-
self.clear_all_diagnostics().await;
264-
Ok(())
265-
}
266-
267301
async fn did_save(&self, params: DidSaveTextDocumentParams) {
268302
debug!("oxc server did save");
269303
let uri = &params.text_document.uri;
@@ -396,6 +430,38 @@ impl LanguageServer for Backend {
396430
}
397431

398432
impl Backend {
433+
/// Request the workspace configuration from the client
434+
/// and return the options for each workspace folder.
435+
/// The check if the client support workspace configuration, should be done before.
436+
async fn request_workspace_configuration(&self, uris: Vec<&Uri>) -> Vec<Option<Options>> {
437+
let length = uris.len();
438+
let config_items = uris
439+
.into_iter()
440+
.map(|uri| ConfigurationItem {
441+
scope_uri: Some(uri.clone()),
442+
section: Some("oxc_language_server".into()),
443+
})
444+
.collect::<Vec<_>>();
445+
446+
let Ok(configs) = self.client.configuration(config_items).await else {
447+
debug!("failed to get configuration");
448+
// return none for each workspace folder
449+
return vec![None; length];
450+
};
451+
452+
let mut options = vec![];
453+
for config in configs {
454+
options.push(serde_json::from_value::<Options>(config).ok());
455+
}
456+
457+
debug_assert!(
458+
options.len() == length,
459+
"the number of configuration items should be the same as the number of workspace folders"
460+
);
461+
462+
options
463+
}
464+
399465
// clears all diagnostics for workspace folders
400466
async fn clear_all_diagnostics(&self) {
401467
let mut cleared_diagnostics = vec![];

crates/oxc_language_server/src/worker.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ impl WorkspaceWorker {
5656
Some(ServerLinter::new(self.root_uri.get().unwrap(), options));
5757
}
5858

59+
pub async fn needs_init_linter(&self) -> bool {
60+
self.server_linter.read().await.is_none()
61+
}
62+
5963
pub async fn remove_diagnostics(&self, uri: &Uri) {
6064
self.diagnostics_report_map.read().await.pin().remove(&uri.to_string());
6165
}

editors/vscode/client/extension.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import {
1212
workspace,
1313
} from 'vscode';
1414

15-
import { ExecuteCommandRequest, MessageType, ShowMessageNotification } from 'vscode-languageclient';
15+
import {
16+
ConfigurationParams,
17+
ExecuteCommandRequest,
18+
MessageType,
19+
ShowMessageNotification,
20+
} from 'vscode-languageclient';
1621

1722
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
1823

@@ -213,6 +218,19 @@ export async function activate(context: ExtensionContext) {
213218
},
214219
outputChannel,
215220
traceOutputChannel: outputChannel,
221+
middleware: {
222+
workspace: {
223+
configuration: (params: ConfigurationParams) => {
224+
return params.items.map(item => {
225+
if (item.section !== 'oxc_language_server') {
226+
return null;
227+
}
228+
229+
return configService.rootServerConfig.toLanguageServerConfig() ?? null;
230+
});
231+
},
232+
},
233+
},
216234
};
217235

218236
// Create the language client and start the client.

0 commit comments

Comments
 (0)