LSP 代理子系统

packages/ttsc/internal/lspserver(~2900 LOC)是 ttscserver 的心脏:一个字节级 LSP 代理,坐在编辑器与 tsgo --lsp --stdio 之间,把 ttsc 插件的诊断、code action、workspace/executeCommand 处理器和文档格式化合并进同一条流。

本子系统页面:

  • 设计:双泵架构、Proxy 状态、消息拦截分派。
  • 诊断合并:upstream 与 plugin 诊断如何合并、generation/version 守卫、脏文档处理。
  • code action 与命令:codeAction 增强、executeCommand 路由、命令 id 前缀。
  • 格式化与脏文档跟踪:格式化活缓冲区、文档文本缓存、UTF-16 偏移映射。

它在做什么

ttscserver(cmd/ttscserver/main.go)只做参数解析、版本、构造 NativePluginSource(读 TTSC_LSP_PLUGINS_JSON),然后委托 lspserver.RunLSPServer。真正的代理逻辑在 Proxylsp_proxy.go)。

三个角色

角色是谁职责
编辑器VS Code 等 LSP 客户端发 initialize/didOpen/didChange/codeAction/executeCommand
上游tsgo --lsp --stdio 子进程提供 hover/completion/definition/诊断
PluginSourcettsc 插件旁车(如 lint)贡献 ttsc 插件诊断/code action/命令

PluginSource 是个接口;生产实现是 NativePluginSourcelsp_native_plugin_source.go),它把 LSP 子命令委托给声明了 capabilities.lsp 的原生旁车。无贡献时用 NullPluginSource{}

ttsc 拦截的方法

handleEditorEnvelopelsp_proxy.go:247)按方法分派,拦截这些:

方法ttsc 做什么
initialize(响应)记录请求 id;响应里增强 capabilities(加 codeAction/executeCommand/format provider)
textDocument/didOpen缓存文档文本,发布插件诊断
textDocument/didChange把编辑 splice 进缓存文本,标脏文档
textDocument/didSave重新发布插件诊断
textDocument/didClose逐出文档文本,清诊断
textDocument/codeAction(请求)决定本地处理还是转发-增强
workspace/executeCommand(请求)ttsc 拥有的命令本地处理
textDocument/formatting(请求)ttsc 拥有格式化器时格式化活缓冲区
$/cancelRequest丢弃对应 pending 条目,再转发

其余消息原样转发上游。

为什么是字节级代理而非完整 LSP server

ttsc 不想重新实现 hover/completion/definition——那些 tsgo 已经做得很好。它只想在 tsgo 之上增量增强:合并几类额外诊断、追加几个 code action、处理几个自己的命令、把格式化重定向到 lint 旁车。字节级代理是侵入最小的方式:大多数帧原样穿过,只有少数被拦截或增强。这也意味着 ttsc 自动继承 tsgo LSP 的所有能力,无需逐一桥接。

并发模型

Runlsp_proxy.go:168)起两个 goroutine:pumpEditorToUpstreampumpUpstreamToEditor。两个方向各一个泵,靠多把锁保护共享状态(writeMuupstreamWriteMupendingMucapabilityMudiagnosticsMu)。异步完成的本地响应(如插件 code action 查询在 goroutine 里跑完)通过 asyncErrCh 报错。详见 设计

接下来