LSP 代理设计
本页讲 Proxy(packages/ttsc/internal/lspserver/lsp_proxy.go)的双泵架构、它持有的状态、消息拦截分派、以及关闭与错误处理。
双泵架构
Proxy.Run(ctx)(lsp_proxy.go:168)起两个 goroutine:
pumpEditorToUpstream(lsp_proxy.go:205):从编辑器读帧,决定原样转发还是本地处理(executeCommand、codeAction 记账),写选定帧。编辑器关闭端(ErrFrameClosed)时关上游 writer,让 tsgo 的 Read 返回 EOF、Run 循环排空——没这个推动 tsgo 会永远等输入。pumpUpstreamToEditor:从 tsgo 读响应,增强(合并诊断/code action)后写给编辑器。
Run 循环里等两个泵都返回;任一报硬传输错误就 closeAfterPumpError 解阻另一侧。ErrFrameClosed 与 context.Canceled 折叠成 nil,让编辑器关闭不被当成崩溃。
Proxy 状态
Proxy(lsp_proxy.go:69)持有大量按 uri 键控的状态,分几组锁保护:
异步本地响应(如插件 code action 在 goroutine 里跑完)通过 asyncErrCh(容量 1)报错;reportAsyncError(lsp_proxy.go:1730)忽略 ErrFrameClosed/context.Canceled,其余 best-effort 投递。
消息分派
handleEditorEnvelope(lsp_proxy.go:247)是编辑器侧的分派中枢,返回 (handled bool, err error):handled=true 表示本地完全处理(已响应、不转发上游)。每个 case 见 子系统索引 的方法表。
关键设计:大多数消息不被拦截,handled=false 后落到 writeUpstreamFrame 原样转发。只有少数方法(initialize 响应增强、文档生命周期跟踪、codeAction、executeCommand、formatting、cancelRequest)走特殊路径。
initialize 能力增强
augmentInitializeResult(lsp_proxy.go:1449)在 tsgo 的 initialize 响应里注入 ttsc 能力:
- 当有 source 命令或 code action kind 而 upstream 没声明
codeActionProvider时,加上它(值含 ttsc 的 codeActionKinds)。 - 有命令时,把 ttsc 命令 id 并进
executeCommandProvider.commands。 - ttsc 拥有文档格式化器时,强制打开
documentFormattingProvider——即使 upstream 已声明一个,因为 tsgo 的格式化器会格式化磁盘文件、丢失未保存编辑(lsp_proxy.go:1491注释)。
它还记录 upstream 是否提供 codeActionProvider(setUpstreamCodeActionProvider),后续 codeAction 决策用它。
关闭与错误处理
closeUpstreamInput(lsp_proxy.go:239):编辑器关闭端时关上游 writer(若它也是io.Closer),让 tsgo 读到 EOF 退出。类型断言因为ProxyOptions只承诺io.Writer,但RunLSPServer实际传*io.PipeWriter。RecoverPanicAs(lsp_server.go:31):包住上游 runner,把 panic 转成ErrLSPUpstreamPanic,附 stack。注意recover()不抓runtime.Goexit——Goexit 会让 runner goroutine 干净退出、这里返回 nil。forgetCancelledRequest(lsp_proxy.go:313):$/cancelRequest命名一个编辑器已放弃的 in-flight id,代理丢掉对应 pending codeAction 条目(防 map 在长会话里无界增长),再让通知继续上游让 tsgo 自己回 cancel 错误。id 经共享归一化器 keying,所以对1.0的 cancel 能删掉存在1下的条目。
NativePluginSource:旁车委托
NativePluginSource(lsp_native_plugin_source.go:52)实现 PluginSource,把 LSP 子命令委托给声明了 capabilities.lsp 的原生旁车。它从 TTSC_LSP_PLUGINS_JSON 读 NativePluginManifest(含普通 plugins 与 lspPlugins)。
防护:每次旁车命令有 30 秒超时(nativePluginCommandTimeout)、stdout 4MB / stderr 1MB 上限(limitedBuffer,超限截断而非 OOM)。这让一个挂死或刷屏的旁车不会拖垮编辑器会话。
不变量
- 两个泵必须都返回
Run才返回;硬错误解阻另一侧。 - 所有写编辑器经
writeEditorFrame(writeMu),防 pumpEditorToUpstream 的本地响应与 pumpUpstreamToEditor 的转发帧交错。 - 每个 uri 的状态由
diagnosticsMu统一保护(文本缓存、脏标志、generation 计数器)。