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。真正的代理逻辑在 Proxy(lsp_proxy.go)。
三个角色
PluginSource 是个接口;生产实现是 NativePluginSource(lsp_native_plugin_source.go),它把 LSP 子命令委托给声明了 capabilities.lsp 的原生旁车。无贡献时用 NullPluginSource{}。
ttsc 拦截的方法
handleEditorEnvelope(lsp_proxy.go:247)按方法分派,拦截这些:
其余消息原样转发上游。
为什么是字节级代理而非完整 LSP server
ttsc 不想重新实现 hover/completion/definition——那些 tsgo 已经做得很好。它只想在 tsgo 之上增量增强:合并几类额外诊断、追加几个 code action、处理几个自己的命令、把格式化重定向到 lint 旁车。字节级代理是侵入最小的方式:大多数帧原样穿过,只有少数被拦截或增强。这也意味着 ttsc 自动继承 tsgo LSP 的所有能力,无需逐一桥接。
并发模型
Run(lsp_proxy.go:168)起两个 goroutine:pumpEditorToUpstream 与 pumpUpstreamToEditor。两个方向各一个泵,靠多把锁保护共享状态(writeMu、upstreamWriteMu、pendingMu、capabilityMu、diagnosticsMu)。异步完成的本地响应(如插件 code action 查询在 goroutine 里跑完)通过 asyncErrCh 报错。详见 设计。