强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 3 章:生命周期

3.1 生命周期概览

LSP 的生命周期分为明确的阶段,每个阶段有严格的状态转换规则:

                    ┌──────────────┐
                    │   创建连接    │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
          ┌────────│  初始化阶段    │◀──── initialize request
          │        └──────┬───────┘
          │               │
          │        ┌──────▼───────┐
          │        │  运行阶段      │◀──── initialized notification
          │        └──────┬───────┘
          │               │
          │        ┌──────▼───────┐
          │        │  关闭阶段      │◀──── shutdown request
          │        └──────┬───────┘
          │               │
          │        ┌──────▼───────┐
          └───────▶│  退出          │◀──── exit notification
                   └──────────────┘

阶段总结

阶段Client 消息Server 消息说明
初始化initialize (Request)InitializeResult (Response)能力协商
就绪initialized (Notification)Server 可开始处理请求
运行各种请求/通知各种响应/通知正常工作阶段
关闭shutdown (Request)null (Response)优雅关闭
退出exit (Notification)进程退出

3.2 初始化阶段

3.2.1 Initialize 请求

Client 必须首先发送 initialize 请求,告知 Server 自身的能力和项目信息:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "processId": 12345,
    "clientInfo": {
      "name": "VS Code",
      "version": "1.85.0"
    },
    "locale": "zh-cn",
    "rootPath": "/home/user/my-project",
    "rootUri": "file:///home/user/my-project",
    "capabilities": {
      "textDocument": {
        "completion": {
          "completionItem": {
            "snippetSupport": true,
            "commitCharactersSupport": true,
            "documentationFormat": ["markdown", "plaintext"],
            "deprecatedSupport": true,
            "preselectSupport": true
          },
          "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]
          }
        },
        "hover": {
          "contentFormat": ["markdown", "plaintext"]
        },
        "definition": {
          "dynamicRegistration": true,
          "linkSupport": true
        },
        "publishDiagnostics": {
          "relatedInformation": true,
          "tagSupport": {
            "valueSet": [1, 2]
          }
        }
      },
      "workspace": {
        "workspaceFolders": true,
        "didChangeConfiguration": {
          "dynamicRegistration": true
        },
        "didChangeWatchedFiles": {
          "dynamicRegistration": true
        }
      }
    },
    "workspaceFolders": [
      {
        "uri": "file:///home/user/my-project",
        "name": "my-project"
      }
    ]
  }
}

3.2.2 关键参数说明

参数类型说明
processIdinteger | nullClient 进程 PID,用于检测 Client 崩溃
clientInfoobjectClient 名称和版本
rootUriDocumentUri | null工作区根目录 URI
capabilitiesobjectClient 支持的所有能力
workspaceFoldersWorkspaceFolder[]工作区文件夹列表
initializationOptionsanyServer 特定的初始化选项
trace'off' | 'messages' | 'verbose'初始日志级别

3.2.3 Server 响应

Server 返回 InitializeResult,声明自己支持的能力:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "capabilities": {
      "textDocumentSync": {
        "openClose": true,
        "change": 2,
        "willSave": false,
        "willSaveWaitUntil": false,
        "save": {
          "includeText": false
        }
      },
      "completionProvider": {
        "resolveProvider": true,
        "triggerCharacters": [".", ":", "@", "/"],
        "allCommitCharacters": ["(", ",", ";"]
      },
      "hoverProvider": true,
      "definitionProvider": true,
      "referencesProvider": true,
      "documentSymbolProvider": true,
      "workspaceSymbolProvider": true,
      "codeActionProvider": {
        "codeActionKinds": ["quickfix", "refactor"],
        "resolveProvider": true
      },
      "documentFormattingProvider": true,
      "renameProvider": {
        "prepareProvider": true
      }
    },
    "serverInfo": {
      "name": "my-language-server",
      "version": "1.0.0"
    }
  }
}

3.3 能力协商机制

3.3.1 协商原理

能力协商的核心逻辑是:Client 声明 “我能做什么”,Server 声明 “我提供什么”,二者取交集

Client Capabilities        Server Capabilities
┌──────────────────┐       ┌──────────────────┐
│  completion ✅    │       │  completion ✅    │
│  hover ✅         │  AND  │  hover ✅         │
│  rename ✅        │       │  rename ❌        │
│  folding ✅       │       │  folding ✅       │
└──────────────────┘       └──────────────────┘
              │                     │
              ▼                     ▼
         实际可用能力
    ┌──────────────────┐
    │  completion ✅    │
    │  hover ✅         │
    │  folding ✅       │
    └──────────────────┘

3.3.2 能力类型

静态能力(Static Registration):在 initialize 时一次性声明

// Server 在 InitializeResult 中声明
capabilities: {
  hoverProvider: true,
  definitionProvider: true,
  completionProvider: {
    triggerCharacters: ["."],
    resolveProvider: true,
  },
}

动态能力(Dynamic Registration):运行时动态注册/注销

// Server 在运行时注册新能力
connection.sendRequest("client/registerCapability", {
  registrations: [
    {
      id: "format-on-save",
      method: "textDocument/willSaveWaitUntil",
      registerOptions: {
        documentSelector: [{ scheme: "file", language: "python" }],
      },
    },
  ],
});

// Server 注销能力
connection.sendRequest("client/unregisterCapability", {
  unregisterations: [
    {
      id: "format-on-save",
      method: "textDocument/willSaveWaitUntil",
    },
  ],
});

3.3.3 常见能力一览

能力Server 侧键Client 侧键类型
文档同步textDocumentSync静态
代码补全completionProvidertextDocument.completion静态/动态
悬停hoverProvidertextDocument.hover静态/动态
跳转定义definitionProvidertextDocument.definition静态/动态
引用查找referencesProvidertextDocument.references静态/动态
代码格式化documentFormattingProvidertextDocument.formatting静态/动态
代码动作codeActionProvidertextDocument.codeAction静态/动态
重命名renameProvidertextDocument.rename静态/动态
代码透镜codeLensProvidertextDocument.codeLens静态/动态
语义标记semanticTokensProvidertextDocument.semanticTokens静态
折叠范围foldingRangeProvidertextDocument.foldingRange静态/动态
工作区符号workspaceSymbolProviderworkspace.symbol静态/动态

3.4 运行阶段

3.4.1 Initialized 通知

Client 收到 initialize 响应后,必须发送 initialized 通知:

{
  "jsonrpc": "2.0",
  "method": "initialized",
  "params": {}
}

收到 initialized 后,Server 可以安全地向 Client 发送请求(如 workspace/configuration)。

3.4.2 请求取消

Client 可以取消正在处理的请求:

// Client 发送取消通知
{
  "jsonrpc": "2.0",
  "method": "$/cancelRequest",
  "params": {
    "id": 42  // 要取消的请求 ID
  }
}

Server 实现示例:

import { CancellationTokenSource } from "vscode-jsonrpc";

// 跟踪活跃的请求
const activeRequests = new Map<number, CancellationTokenSource>();

connection.onRequest("textDocument/completion", async (params, token) => {
  const requestId = /* 从 context 获取 */ 0;
  const cts = new CancellationTokenSource();
  activeRequests.set(requestId, cts);

  token.onCancellationRequested(() => {
    cts.cancel();
    activeRequests.delete(requestId);
  });

  try {
    return await computeCompletions(params, cts.token);
  } finally {
    activeRequests.delete(requestId);
  }
});

3.4.3 进度报告

LSP 3.15+ 支持进度报告机制,用于长时间运行的操作:

// Server 端报告进度
connection.onRequest("textDocument/references", async (params) => {
  // 创建进度令牌
  const progressToken = await connection.sendRequest("window/workDoneProgress/create", {
    token: "references-search",
  });

  // 发送进度通知
  connection.sendProgress("window/workDoneProgress", "references-search", {
    kind: "begin",
    title: "Searching references...",
    percentage: 0,
  });

  // ... 搜索过程中更新进度
  connection.sendProgress("window/workDoneProgress", "references-search", {
    kind: "report",
    message: "Searching in src/",
    percentage: 50,
  });

  // 完成
  connection.sendProgress("window/workDoneProgress", "references-search", {
    kind: "end",
    message: "Done",
  });

  return results;
});

3.5 关闭阶段

3.5.1 优雅关闭流程

Step 1:Client 发送 shutdown 请求

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "shutdown",
  "params": null
}

Step 2:Server 清理资源并返回响应

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": null
}

Step 3:Client 发送 exit 通知

{
  "jsonrpc": "2.0",
  "method": "exit",
  "params": null
}

Step 4:Server 进程退出

3.5.2 Server 端关闭实现

let isShuttingDown = false;

connection.onRequest("shutdown", async () => {
  isShuttingDown = true;

  // 清理资源
  await flushDiagnostics();
  closeAllDocuments();
  clearCaches();

  return null; // shutdown 响应结果必须是 null
});

connection.onNotification("exit", () => {
  // 退出码:0 = 正常关闭(shutdown 已调用),1 = 异常退出
  process.exit(isShuttingDown ? 0 : 1);
});

// 在 shutdown 之后拒绝新的请求
connection.onRequest(async (method) => {
  if (isShuttingDown) {
    throw new rpc.ResponseError(
      rpc.ErrorCodes.InvalidRequest,
      "Server is shutting down"
    );
  }
});

3.5.3 exit 的退出码约定

场景退出码说明
收到 shutdown 后收到 exit0正常关闭
未收到 shutdown 直接收到 exit1异常关闭
Server 内部错误崩溃1未捕获异常

3.6 重连与恢复

3.6.1 Client 崩溃检测

Server 可以通过 processId 检测 Client 是否崩溃:

const clientProcessId = params.processId;

if (clientProcessId !== null) {
  // 定期检查 Client 进程是否存活
  const checkInterval = setInterval(() => {
    try {
      process.kill(clientProcessId, 0); // signal 0 = 检测进程是否存在
    } catch {
      // Client 进程已退出,Server 也应该退出
      clearInterval(checkInterval);
      process.exit(1);
    }
  }, 5000);
}

3.6.2 Client 端重连机制

class LSPClientWithReconnect {
  private maxRetries = 5;
  private retryDelay = 1000; // ms

  async connect(): Promise<void> {
    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        await this.startServer();
        await this.sendInitialize();
        console.log("Connected to LSP server");
        return;
      } catch (err) {
        console.warn(`Connection attempt ${attempt + 1} failed:`, err);
        await this.sleep(this.retryDelay * (attempt + 1));
      }
    }
    throw new Error("Failed to connect after max retries");
  }

  private async startServer(): Promise<void> {
    // 启动 Server 进程
    this.serverProcess = spawn("node", ["server.js"], {
      stdio: ["pipe", "pipe", "pipe"],
    });

    // 监听进程退出,触发重连
    this.serverProcess.on("exit", (code) => {
      if (code !== 0) {
        console.warn("Server exited unexpectedly, reconnecting...");
        setTimeout(() => this.connect(), 1000);
      }
    });
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

3.7 完整生命周期示例

import {
  createMessageConnection,
  StreamMessageReader,
  StreamMessageWriter,
} from "vscode-jsonrpc/node";
import { spawn } from "child_process";

async function main() {
  // 1. 启动 Server 进程
  const server = spawn("my-lsp-server", [], {
    stdio: ["pipe", "pipe", "pipe"],
  });

  // 2. 创建连接
  const connection = createMessageConnection(
    new StreamMessageReader(server.stdout),
    new StreamMessageWriter(server.stdin)
  );

  // 3. 注册日志监听
  connection.onNotification("window/logMessage", (params) => {
    console.log(`[Server ${params.type}] ${params.message}`);
  });

  // 4. 启动连接
  connection.listen();

  // 5. 初始化
  const initResult = await connection.sendRequest("initialize", {
    processId: process.pid,
    rootUri: "file:///home/user/project",
    capabilities: {},
  });
  console.log("Server capabilities:", initResult.capabilities);

  // 6. 发送 initialized 通知
  connection.sendNotification("initialized", {});

  // 7. 正常使用...
  // await connection.sendRequest("textDocument/completion", ...);

  // 8. 优雅关闭
  await connection.sendRequest("shutdown", null);
  connection.sendNotification("exit", {});

  // 9. 等待进程退出
  server.on("exit", () => {
    console.log("Server exited");
    connection.dispose();
  });
}

main().catch(console.error);

⚠️ 常见陷阱

陷阱说明
跳过 initialized在发送 initialized 之前就发送请求,Server 可能拒绝处理
shutdown 后继续发送请求Server 在 shutdown 后应拒绝所有新请求
exit 不退出收到 exit 后 Server 必须退出,否则 Client 需要强制 kill
未处理进程崩溃如果 Client 进程意外死亡,Server 应自行检测并退出
初始化超时Client 应设置初始化超时,避免 Server 卡住导致编辑器无响应

🔗 扩展阅读


下一章第 4 章:文本同步 — 文档的打开、关闭、变更与保存全链路。