You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从零构建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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 02:52:32