LSP 代理设计

本页讲 Proxypackages/ttsc/internal/lspserver/lsp_proxy.go)的双泵架构、它持有的状态、消息拦截分派、以及关闭与错误处理。

双泵架构

Proxy.Run(ctx)lsp_proxy.go:168)起两个 goroutine:

  • pumpEditorToUpstreamlsp_proxy.go:205):从编辑器读帧,决定原样转发还是本地处理(executeCommand、codeAction 记账),写选定帧。编辑器关闭端(ErrFrameClosed)时关上游 writer,让 tsgo 的 Read 返回 EOF、Run 循环排空——没这个推动 tsgo 会永远等输入。
  • pumpUpstreamToEditor:从 tsgo 读响应,增强(合并诊断/code action)后写给编辑器。

Run 循环里等两个泵都返回;任一报硬传输错误就 closeAfterPumpError 解阻另一侧。ErrFrameClosedcontext.Canceled 折叠成 nil,让编辑器关闭不被当成崩溃。

Proxy 状态

Proxylsp_proxy.go:69)持有大量按 uri 键控的状态,分几组锁保护:

状态组内容
写编辑器writeMu串行化 WriteFrame 到 editorOut
写上游upstreamWriteMu串行化写 upstreamIn
pending 请求pendingMupendingActions/pendingCommands/pendingInitialize/pendingLocalActions/pendingAugmentingActions
upstream 能力capabilityMuupstreamCodeActionProvider
诊断 + 文档diagnosticsMuupstreamDiagnostics/pluginDiagnostics/diagnosticGeneration/documentGeneration/dirtyDocuments/dirtyVersions/documentText

异步本地响应(如插件 code action 在 goroutine 里跑完)通过 asyncErrCh(容量 1)报错;reportAsyncErrorlsp_proxy.go:1730)忽略 ErrFrameClosed/context.Canceled,其余 best-effort 投递。

消息分派

handleEditorEnvelopelsp_proxy.go:247)是编辑器侧的分派中枢,返回 (handled bool, err error)handled=true 表示本地完全处理(已响应、不转发上游)。每个 case 见 子系统索引 的方法表。

关键设计:大多数消息被拦截,handled=false 后落到 writeUpstreamFrame 原样转发。只有少数方法(initialize 响应增强、文档生命周期跟踪、codeAction、executeCommand、formatting、cancelRequest)走特殊路径。

initialize 能力增强

augmentInitializeResultlsp_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 决策用它。

关闭与错误处理

  • closeUpstreamInputlsp_proxy.go:239):编辑器关闭端时关上游 writer(若它也是 io.Closer),让 tsgo 读到 EOF 退出。类型断言因为 ProxyOptions 只承诺 io.Writer,但 RunLSPServer 实际传 *io.PipeWriter
  • RecoverPanicAslsp_server.go:31):包住上游 runner,把 panic 转成 ErrLSPUpstreamPanic,附 stack。注意 recover() 不抓 runtime.Goexit——Goexit 会让 runner goroutine 干净退出、这里返回 nil。
  • forgetCancelledRequestlsp_proxy.go:313):$/cancelRequest 命名一个编辑器已放弃的 in-flight id,代理丢掉对应 pending codeAction 条目(防 map 在长会话里无界增长),再让通知继续上游让 tsgo 自己回 cancel 错误。id 经共享归一化器 keying,所以对 1.0 的 cancel 能删掉存在 1 下的条目。

NativePluginSource:旁车委托

NativePluginSourcelsp_native_plugin_source.go:52)实现 PluginSource,把 LSP 子命令委托给声明了 capabilities.lsp 的原生旁车。它从 TTSC_LSP_PLUGINS_JSONNativePluginManifest(含普通 pluginslspPlugins)。

防护:每次旁车命令有 30 秒超时(nativePluginCommandTimeout)、stdout 4MB / stderr 1MB 上限(limitedBuffer,超限截断而非 OOM)。这让一个挂死或刷屏的旁车不会拖垮编辑器会话。

不变量

  • 两个泵必须都返回 Run 才返回;硬错误解阻另一侧。
  • 所有写编辑器经 writeEditorFramewriteMu),防 pumpEditorToUpstream 的本地响应与 pumpUpstreamToEditor 的转发帧交错。
  • 每个 uri 的状态由 diagnosticsMu 统一保护(文本缓存、脏标志、generation 计数器)。

失败模式

失败行为
编辑器关闭端ErrFrameClosed 折叠成 nil,关上游让 tsgo 排空
上游 panicErrLSPUpstreamPanic 包装,附 stack
旁车超时30s 后取消,命令失败
旁车刷屏stdout/stderr 上限截断
解析帧失败ParseEnvelope 失败时原样转发上游

接下来