1. LSP: VSCode + TypeScript
VSCode 是通过 LSP 向 tsserver 发送名为 'completionInfo' 的消息,来实现自动补全的。
请求消息示例如下,
{"seq":35,"type":"request","command":"completionInfo","arguments":{"file":"/Users/thzt/.../index.ts","line":1,"offset":8,"includeExternalModuleExports":true,"includeInsertTextCompletions":true,"triggerCharacter":".","includeAutomaticOptionalChainCompletions":true}}
返回消息示例如下(太长已截断),
{ "seq": 0, "type": "response", "command": "completionInfo", "request_seq": 35, "success": true, "performanceData": { "updateGraphDurationMs": 8 }, "body": { "isGlobalCompletion": false, "isMemberCompletion": true, "isNewIdentifierLocation": false, "entries": [ { "name": "__dirname", "kind": "var", "kindModifiers": "declare", "sortText": "0" }, ...
VSCode 1.45.1 和 TypeScript 3.7.3 的联合调试,可以看这里:
https://www.jianshu.com/p/c6032eb3e8ce
2. 触发补全
VSCode 最后发送消息的的源码位置位于,
VSCode 1.45.1: extensions/typescript-language-features/src/tsServer/spawner.ts#L233
通过向子进程的 stdin
写入内容来发送消息,
this._process.stdin!.write(JSON.stringify(serverRequest) + '\r\n', 'utf8');
tsserver 接受消息的源码位置位于,
TypeScript 3.7.3: src/tsserver/server.ts#L575
使用了 readline
模块,监听当前进程的 stdin,
rl.on("line", (input: string) => {
// ...
});
3. 返回数据
但是补全信息的数据量有时候是会很大的,甚至超出了父子进程一次可传输的数据量。
tsserver 返回补全信息的位置位于,
TypeScript 3.7.3: src/server/session.ts#L769
可以看到,tsserver 返回了大量的补全数据,
this.host.write(msgText);
VSCode 是这样接收的,源码位置位于,
VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts#L90
可以看到,数据是以 Buffer
进行传输的,且一次传输长度只有 8192
。
readable.on('data', data => this.onLengthData(data));
4. 数据处理
VSCode 采用了分批接收数据,再合并一起处理的方式,获取了完整的从子进程返回的内容。
源码位于,VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts#L99 onLengthData
函数中,
private onLengthData(data: Buffer | string): void {
if (this.isDisposed) {
return;
}
try {
this.buffer.append(data); // 将每次收到的 buffer 暂存起来
while (true) {
if (this.nextMessageLength === -1) {
this.nextMessageLength = this.buffer.tryReadContentLength(); // 读取 Content-Length 后的数字,表示消息长度
if (this.nextMessageLength === -1) {
return;
}
}
const msg = this.buffer.tryReadContent(this.nextMessageLength); // 读取给定长度的消息
if (msg === null) {
return;
}
this.nextMessageLength = -1;
const json = JSON.parse(msg);
this._onData.fire(json);
}
} catch (e) {
this._onError.fire(e);
}
}
子进程返回的消息,进行了如下编码格式,
位于 TypeScript 3.7.3: src/server/session.ts#L137 formatMessage
函数中,
`Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`
这些消息太长了,会以最长为 8192
的单元,分多次发送出去。
VSCode 对这样的消息进行了如下处理,
- 每次收到消息,都将单元消息存起来
- 每次接受到消息,都尝试解码现存的所有单元中的内容,解码方式如下,
- 先读取消息头里
Content-Length
后面的数字,表示本批次所有消息的长度 - 然后再读取消息体
- 先读取消息头里
关键逻辑都在 VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts 文件中。
5. 示例
事实上,涉及父子进程通过 stdio 进行通信时,都会有上述消息被拆分发送的问题,
所以我这里写了一个示例项目,来展示这样的问题应该如何处理。
github: ipc-chunk
参考
VSCode 1.45.1
TypeScript 3.7.3