Repository navigation
Language server crashes on workspace/semanticTokens/refresh when client doesn't support it #88
Description
Summary
The language server crashes shortly after opening any .m file, when used with a client that doesn't implement the server-initiated workspace/semanticTokens/refresh request (e.g. Helix).
Environment
- MATLAB-language-server: main branch (commit 2c300ba / v1.3.12)
- Node.js: v26.4.0
- Client: Helix 25.07.1
Steps to reproduce
- Configure Helix (or any LSP client that responds to unimplemented server-to-client requests with a
MethodNotFounderror rather than silently ignoring them) to usematlab_ls --stdio. - Open a
.mfile. - After indexing runs (~150ms debounce), the server sends a
workspace/semanticTokens/refreshrequest to the client. - The client replies with a
MethodNotFounderror. - The whole language server process crashes.
Actual behavior
ResponseError: Method not found: workspace/semanticTokens/refresh
at handleResponse (out/index.js:45232:48)
at handleMessage (out/index.js:45012:13)
at processMessageQueue (out/index.js:45029:17)
at Immediate.<anonymous> (out/index.js:45001:13)
at process.processImmediate (node:internal/timers:504:21) {
code: -32601,
data: undefined
}
Node.js v26.4.0
The process exits entirely.
Root cause
In src/providers/semanticTokens/SemanticTokensProvider.ts, setupSemanticTokensRefresh() sends the request without handling a rejection:
refreshTimer = setTimeout(() => {
void connection.sendRequest('workspace/semanticTokens/refresh')
}, 150)void only discards the returned promise's value — it does not attach a rejection handler. If the client rejects the request (e.g. with MethodNotFound, since workspace/semanticTokens/refresh is optional per the LSP spec and many clients don't implement it), this becomes an unhandled promise rejection, which is fatal under Node's default --unhandled-rejections=throw behavior and kills the process.
Additionally, per the LSP spec, a server should only send this request if the client declared capabilities.workspace.semanticTokens.refreshSupport. This code (introduced in #84) sends it unconditionally, with no capability check anywhere in server.ts where setupSemanticTokensRefresh is wired up.
To summarize, there are two compounding issues:
workspace/semanticTokens/refreshis optional per the LSP spec — a server should only send it if the client declaredcapabilities.workspace.semanticTokens.refreshSupport. This is never checked anywheresetupSemanticTokensRefreshis wired up.voiddiscards the returned promise but does not handle rejection. Clients that don't implement the request (e.g. Helix) reply with aMethodNotFounderror, which becomes an unhandled promise rejection and crashes the entire language server process.
Proposed fix
setupSemanticTokensRefreshtakes the negotiatedClientCapabilitiesand only registers the indexing→refresh wiring if the client declaredworkspace.semanticTokens.refreshSupport === true. The call site inserver.tsmoves from the top-level server setup into theonInitializedhandler (wherecapabilitiesis actually populated), alongside the existingworkspaceIndexer.setupCallbacks(capabilities)/workspaceFolderscapability checks — matching the pattern already used there.- Keep a defensive
.catch()on thesendRequestcall itself, in case a client declares support but still errors on a given call.
Diff
--- a/src/providers/semanticTokens/SemanticTokensProvider.ts
+++ b/src/providers/semanticTokens/SemanticTokensProvider.ts
@@ -1,4 +1,4 @@
-import { SemanticTokens, SemanticTokensParams, TextDocuments, Range, Connection } from 'vscode-languageserver'
+import { ClientCapabilities, SemanticTokens, SemanticTokensParams, TextDocuments, Range, Connection } from 'vscode-languageserver'
import MatlabLifecycleManager from '../../lifecycle/MatlabLifecycleManager'
import { TextDocument } from 'vscode-languageserver-textdocument'
import FileInfoIndex, { MatlabFunctionScopeInfo, MatlabGlobalScopeInfo } from '../../indexing/FileInfoIndex'
@@ -121,17 +121,25 @@ export default SemanticTokensProvider
/**
* Wires semantic token invalidation to document indexing.
*
* When indexing completes, this schedules a debounced refresh request
* so the client re-requests semantic tokens and updates highlighting.
+ * Only registered if the client declared support for the request, since
+ * it is optional per the LSP spec and not all clients implement it.
*/
export function setupSemanticTokensRefresh (
connection: Connection,
- documentIndexer: DocumentIndexer
+ documentIndexer: DocumentIndexer,
+ capabilities: ClientCapabilities
): void {
+ if (capabilities.workspace?.semanticTokens?.refreshSupport !== true) {
+ return
+ }
+
let refreshTimer: NodeJS.Timeout | undefined
documentIndexer.setOnIndexed(() => {
if (refreshTimer != null) clearTimeout(refreshTimer)
refreshTimer = setTimeout(() => {
- void connection.sendRequest('workspace/semanticTokens/refresh')
+ connection.sendRequest('workspace/semanticTokens/refresh').catch(() => {
+ // Defensive: ignore rejection even though refreshSupport was declared.
+ })
}, 150)
})
}
--- a/src/server.ts
+++ b/src/server.ts
@@ -186,6 +186,8 @@
workspaceIndexer.setupCallbacks(capabilities)
if (capabilities.workspace?.workspaceFolders != null) {
// If workspace folders are supported, try to synchronize the MATLAB path with the user's workspace.
pathSynchronizer = new PathSynchronizer(matlabLifecycleManager, mvm)
pathSynchronizer.initialize()
}
+
+ setupSemanticTokensRefresh(connection, documentIndexer, capabilities)
})
@@ -389,6 +391,5 @@
connection.onRequest(SemanticTokensRequest.method, async (params: SemanticTokensParams) => {
return await semanticTokensProvider.handleSemanticTokensRequest(params, documentManager)
})
- setupSemanticTokensRefresh(connection, documentIndexer)
}Testing
Verified with a scripted JSON-RPC client standing in for the LSP client:
- Client declares no
workspace.semanticTokens.refreshSupport(Helix's actual behavior): the server no longer sendsworkspace/semanticTokens/refreshat all after indexing. - Client declares
refreshSupport: true: the request is still sent and, even if rejected, no longer crashes the process.
Before this change, the server crashed with an unhandled ResponseError: Method not found: workspace/semanticTokens/refresh any time a non-supporting client (like Helix) was used.
Activity