第 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 关键参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
processId | integer | null | Client 进程 PID,用于检测 Client 崩溃 |
clientInfo | object | Client 名称和版本 |
rootUri | DocumentUri | null | 工作区根目录 URI |
capabilities | object | Client 支持的所有能力 |
workspaceFolders | WorkspaceFolder[] | 工作区文件夹列表 |
initializationOptions | any | Server 特定的初始化选项 |
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 | — | 静态 |
| 代码补全 | completionProvider | textDocument.completion | 静态/动态 |
| 悬停 | hoverProvider | textDocument.hover | 静态/动态 |
| 跳转定义 | definitionProvider | textDocument.definition | 静态/动态 |
| 引用查找 | referencesProvider | textDocument.references | 静态/动态 |
| 代码格式化 | documentFormattingProvider | textDocument.formatting | 静态/动态 |
| 代码动作 | codeActionProvider | textDocument.codeAction | 静态/动态 |
| 重命名 | renameProvider | textDocument.rename | 静态/动态 |
| 代码透镜 | codeLensProvider | textDocument.codeLens | 静态/动态 |
| 语义标记 | semanticTokensProvider | textDocument.semanticTokens | 静态 |
| 折叠范围 | foldingRangeProvider | textDocument.foldingRange | 静态/动态 |
| 工作区符号 | workspaceSymbolProvider | workspace.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 后收到 exit | 0 | 正常关闭 |
| 未收到 shutdown 直接收到 exit | 1 | 异常关闭 |
| 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 章:文本同步 — 文档的打开、关闭、变更与保存全链路。