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
MethodNotFound error rather than silently ignoring them) to use matlab_ls --stdio.
- Open a
.m file.
- After indexing runs (~150ms debounce), the server sends a
workspace/semanticTokens/refresh request to the client.
- The client replies with a
MethodNotFound error.
- 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/refresh is optional per the LSP spec — a server should only send it if the client declared capabilities.workspace.semanticTokens.refreshSupport. This is never checked anywhere setupSemanticTokensRefresh is wired up.
void discards the returned promise but does not handle rejection. Clients that don't implement the request (e.g. Helix) reply with a MethodNotFound error, which becomes an unhandled promise rejection and crashes the entire language server process.
Proposed fix
setupSemanticTokensRefresh takes the negotiated ClientCapabilities and only registers the indexing→refresh wiring if the client declared workspace.semanticTokens.refreshSupport === true. The call site in server.ts moves from the top-level server setup into the onInitialized handler (where capabilities is actually populated), alongside the existing workspaceIndexer.setupCallbacks(capabilities) / workspaceFolders capability checks — matching the pattern already used there.
- Keep a defensive
.catch() on the sendRequest call 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 sends workspace/semanticTokens/refresh at 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.
Summary
The language server crashes shortly after opening any
.mfile, when used with a client that doesn't implement the server-initiatedworkspace/semanticTokens/refreshrequest (e.g. Helix).Environment
Steps to reproduce
MethodNotFounderror rather than silently ignoring them) to usematlab_ls --stdio..mfile.workspace/semanticTokens/refreshrequest to the client.MethodNotFounderror.Actual behavior
The process exits entirely.
Root cause
In
src/providers/semanticTokens/SemanticTokensProvider.ts,setupSemanticTokensRefresh()sends the request without handling a rejection:voidonly discards the returned promise's value — it does not attach a rejection handler. If the client rejects the request (e.g. withMethodNotFound, sinceworkspace/semanticTokens/refreshis 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=throwbehavior 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 inserver.tswheresetupSemanticTokensRefreshis 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..catch()on thesendRequestcall itself, in case a client declares support but still errors on a given call.Diff
Testing
Verified with a scripted JSON-RPC client standing in for the LSP client:
workspace.semanticTokens.refreshSupport(Helix's actual behavior): the server no longer sendsworkspace/semanticTokens/refreshat all after indexing.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/refreshany time a non-supporting client (like Helix) was used.