从零构建LSP客户端:Deno语言服务器语义token请求无响应排查
Deno LSP语义token请求无响应问题排查
我编写了如下TypeScript代码,用于启动Deno Language Server并尝试获取文件的语义token,但服务器始终不响应我的语义token请求,最终readMessage函数超时。请问哪里出错了?
const TIMEOUT = 2000; // how long to wait for a response from server - only relevant for messages (2 way comms) not notifications (1 way comms) // Function to read a complete message based on Content-Length async function readMessage(lsp: Deno.ChildProcess): Promise<string | null> { const reader = lsp.stdout.getReader(); const decoder = new TextDecoder(); let buffer = ""; try { while (true) { // Timeout in case server never responds const { value, done } = await Promise.race([ reader.read(), new Promise<ReadableStreamReadResult<Uint8Array>>((_, reject) => setTimeout(reject, TIMEOUT, new Error("timeout")) ), ]); console.log("...Read Something"); if (done) return null; // No more data available, end of stream buffer += decoder.decode(value, { stream: true }); // Check for Content-Length header and parse it const contentLengthMatch = buffer.match(/^Content-Length: (\d+)\r\n\r\n/); if (contentLengthMatch) { const contentLength = parseInt(contentLengthMatch[1], 10); const headerLength = contentLengthMatch[0].length; // Ensure we have received the entire message body if (buffer.length >= headerLength + contentLength) { const message = buffer.slice( headerLength, headerLength + contentLength, ); return message; // Return the complete message } } } } catch (e) { //handle any errors by checking stderr then rethrowing let stderr; try { stderr = lsp.stderr.getReader(); const { value, done } = await stderr.read(); console.log("*<==", done, decoder.decode(value, { stream: true })); } catch (e) { console.log("When logging stderr:", e); } finally { stderr?.releaseLock(); } throw e; } finally { reader.releaseLock(); } } // Function to send a message to the LSP server function sendMessage( lsp: Deno.ChildProcess, message: string, ): Promise<string | null> { sendNotification(lsp, message); return readMessage(lsp); } // Function to send a message to the LSP server async function sendNotification(lsp: Deno.ChildProcess, message: string) { const writer = lsp.stdin.getWriter(); const encoder = new TextEncoder(); const contentLength = message.length; const formattedMessage = `Content-Length: ${contentLength}\r\n\r\n${message}`; try { console.log("==SENDING==>", formattedMessage); await writer.write(encoder.encode(formattedMessage)); console.log("==SENT=="); } finally { writer.releaseLock(); } } if (import.meta.main) { // Spawn the LSP server const lsp = new Deno.Command(Deno.execPath(), { args: ["lsp", "--log-level", "debug"], stdin: "piped", stdout: "piped", //stderr: "piped", }).spawn(); try { // Send the initialize message const initializeMessage = `{ "jsonrpc": "2.0", "id": "initializeMessage", "method": "initialize", "params": { "processId": null, "capabilities": { "textDocument": { "semanticTokens": { "requests": { "range": false, "full": true }, "formats": ["relative"], "tokenTypes": [ "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator" ], "tokenModifiers": [ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary" ] } } } } }`; // Read the initialize response const initializeResponse = await sendMessage(lsp, initializeMessage); console.log("<==", initializeResponse); // Send the file open notification const markdownFileOpenMessage = `{ "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": { "textDocument": { "uri": "file:///C:/file1.ts", "languageId": "typescript", "version": 1, "text": "const TIMEOUT: number = 2000" } } }`; await sendNotification(lsp, markdownFileOpenMessage); const semanticTokensMessage = `{ "jsonrpc": "2.0", "id": "semanticTokensMessage", "method": "textDocument/semanticTokens/full", "params": { "textDocument": { "uri": "file:///C:/file1.ts" } } }`; const tokensResponse = await sendMessage(lsp, semanticTokensMessage); // time-out here on this line console.log("<==", tokensResponse); } finally { lsp.unref(); // Unreference the process to allow the program to exit if done } }
运行命令
deno run --allow-run --allow-read main.ts
示例输出
Task dev deno run --watch --allow-run --allow-read main.ts Watcher Process started. ==SENDING==> Content-Length: 875 { "jsonrpc": "2.0", "id": "initializeMessage", "method": "initialize", "params": { "processId": null, "capabilities": { "textDocument": { "semanticTokens": { "requests": { "range": false, "full": true }, "formats": ["relative"], "tokenTypes": [ "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator" ], "tokenModifiers": [ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary" ] } } } } } ==SENT== deno lsp command is intended to be run by text editors and IDEs and shouldn't be run manually. Visit https://docs.deno.com/runtime/getting_started/setup_your_environment/ for instruction how to setup your favorite text editor. Press Ctrl+C to exit. Starting Deno language server... version: 2.0.2 (release, x86_64-pc-windows-msvc) executable: C:\Users\Greedo\.deno\bin\deno.exe ...Read Something ...Read Something <== {"jsonrpc":"2.0","result":{"capabilities":{"textDocumentSync":{"openClose":true,"change":2,"save":{}},"selectionRangeProvider":true,"hoverProvider":true,"completionProvider":{"resolveProvider":true,"triggerCharacters":[".","\"","'","`","/","@","<","#"],"allCommitCharacters":[".",";","("],"signatureHelpProvider":{"triggerCharacters":[",","(","<"],"retriggerCharacters":[")"]},"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":true,"referencesProvider":true,"documentHighlightProvider":true,"documentSymbolProvider":{"label":"Deno"},"workspaceSymbolProvider":true,"codeActionProvider":true,"codeLensProvider":{"resolveProvider":true},"documentFormattingProvider":true,"renameProvider":true,"foldingRangeProvider":true,"executeCommandProvider":{"commands":["deno.cache","deno.reloadImportRegistries"]},"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"callHierarchyProvider":true,"semanticTokensProvider":{"legend":{"tokenTypes":["class","enum","interface","namespace","typeParameter","type","parameter","variable","enumMember","property","function","method"],"tokenModifiers":["declaration","static","async","readonly","defaultLibrary","local"]},"range":true,"full":true},"inlayHintProvider":true,"experimental":{"denoConfigTasks":true,"testingApi":true,"didRefreshDenoConfigurationTreeNotifications":true}},"serverInfo":{"name":"deno-language-server","version":"2.0.2 (release, x86_64-pc-windows-msvc)"}},"id":"initializeMessage"} ==SENDING==> Content-Length: 220 { "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": { "textDocument": { "uri": "file:///C:/file1.ts", "languageId": "typescript", "version": 1, "text": "const TIMEOUT: number = 2000" } } } ==SENT== ==SENDING==> Content-Length: 169 { "jsonrpc": "2.0", "id": "semanticTokensMessage", "method": "textDocument/semanticTokens/full", "params": { "textDocument": { "uri": "file:///C:/file1.ts" } } } ==SENT== When logging stderr: TypeError: Cannot get 'stderr': 'stderr' is not piped at ChildProcess.get stderr (ext:runtime/40_process.js:263:13) at readMessage (file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:44:20) at eventLoopTick (ext:core/01_core.js:214:9) at async file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:156:28 error: Uncaught (in promise) Error: timeout setTimeout(reject, TIMEOUT, new Error("timeout")) ^ at file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:15:39 at new Promise (<anonymous>) at readMessage (file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:14:9) at sendMessage (file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:64:10) at file:///C:/Users/Greedo/Documents/Macros/Deno/LSPdemo/main.ts:156:34 at eventLoopTick (ext:core/01_core.js:175:7)
问题原因及修复方案
1. 缺少initialized通知
LSP规范明确要求,客户端在收到initialize响应后,必须发送initialized通知,告知服务器初始化流程完成。服务器通常会在收到这个通知后才开始处理后续的文本相关请求(比如语义token获取)。
在initializeResponse之后添加以下代码:
// Send initialized notification const initializedMessage = `{ "jsonrpc": "2.0", "method": "initialized", "params": {} }`; await sendNotification(lsp, initializedMessage);
2. stderr未配置导致错误
代码中注释掉了stderr: "piped",但错误处理逻辑里尝试读取stderr,导致抛出TypeError: Cannot get 'stderr': 'stderr' is not piped。解决方法二选一:
- 取消注释
stderr: "piped",让进程的stderr被管道捕获; - 删除错误处理中读取stderr的代码块,避免无效的stderr访问。
3. 超时时间过短
2000ms的超时对于首次分析文件的语义token可能不够,尤其是在环境资源有限的情况下。可以将TIMEOUT调整为5000ms或更长:
const TIMEOUT = 5000;
修正后的核心代码片段
if (import.meta.main) { // Spawn the LSP server const lsp = new Deno.Command(Deno.execPath(), { args: ["lsp", "--log-level", "debug"], stdin: "piped", stdout: "piped", stderr: "piped", // 启用stderr管道 }).spawn(); try { // ... 发送initialize请求并接收响应 ... const initializeResponse = await sendMessage(lsp, initializeMessage); console.log("<==", initializeResponse); // 新增:发送initialized通知 const initializedMessage = `{ "jsonrpc": "2.0", "method": "initialized", "params": {} }`; await sendNotification(lsp, initializedMessage); // ... 发送didOpen通知 ... await sendNotification(lsp, markdownFileOpenMessage); // ... 发送语义token请求 ... const tokensResponse = await sendMessage(lsp, semanticTokensMessage); console.log("<==", tokensResponse); } finally { lsp.unref(); } }
内容的提问来源于stack exchange,提问作者Greedo
相关产品推荐
相关产品推荐

