LSP 诊断合并

ttsc 要把插件诊断(如 @ttsc/lint 违规)合并进 tsgo 已经发布的诊断里,显示在同一条波浪线流上。这看似简单,实则要小心处理三种竞态:文档已被编辑(脏)、版本对不上、generation 过期。本页讲 Proxy 的诊断合并逻辑(packages/ttsc/internal/lspserver/lsp_proxy.go)。

两套诊断缓存

Proxy 按 uri 维护两套诊断(都在 diagnosticsMu 下):

  • upstreamDiagnostics:tsgo 发的 publishDiagnostics
  • pluginDiagnostics:ttsc 插件(lint 等)报的。

合并 = 两套拼接后一起 publishDiagnostics 给编辑器。难点在于"什么时候合并是安全的"。

generation 与 version 双守卫

每个 uri 有两个单调计数器:

  • diagnosticGeneration[uri]:每次文档变脏自增;插件诊断查询带上查询时的 generation,回来时若 generation 已变就丢弃(文档已被再编辑)。
  • documentGeneration[uri]:文档内容代次,code action / executeCommand 用它判定参数对应的文档是否仍是当初那一版。

外加 LSP 自己的 version(didChange 带的整数版本)。

prepareMergedPluginDiagnosticslsp_proxy.go:1377)实现这套守卫:在 diagnosticsMu 下检查 generation 未变、文档不脏、version 与缓存 upstream 一致,全过才合并;否则返回 (nil, nil, false) 让调用方不发布。

脏文档:先清空再说

didChange 一来,markDocumentDirtylsp_proxy.go:1243):

  1. 把 uri 标进 dirtyDocuments,记 dirtyVersions
  2. 删掉 pluginDiagnostics[uri]upstreamDiagnostics[uri]
  3. 自增 diagnosticGenerationdocumentGeneration
  4. 若之前有插件诊断,发一个 publishDiagnostics 清掉编辑器上的旧波浪线。

为什么先清空:文档一改,旧诊断的位置就可能错位。与其显示陈旧波浪线,不如先清掉、等新一轮诊断来。

markDocumentCleanlsp_proxy.go:1278)在文档稳定后清脏标志。

didOpen 与 didSave 触发重新发布

  • didOpencacheDidOpenText 缓存全文,publishPluginDiagnosticsForDidOpen 拉一轮插件诊断。
  • didSavepublishPluginDiagnosticsForDocumentNotification 重新发布(保存后文件落盘,旁车读盘能拿到最新内容)。

version 处理的微妙点

shouldRememberDirtyUpstreamDiagnosticslsp_proxy.go:1308):当 upstream 诊断带的 version 恰好等于当前脏版本时,仍记下它——因为那是 tsgo 对这一版的权威诊断,即使文档此刻标脏。adoptCachedVersion 让合并时采用缓存的 upstream version 而非输入 version,保持编辑器看到的 version 一致。

copyIntPtr / copyRawDiagnosticslsp_proxy.go:1658)到处用:诊断与 version 在锁外被复制,避免把内部缓存的切片/指针泄漏给调用方后被并发改动。

一次合并的完整时序

不变量

  • 诊断与文档状态统一在 diagnosticsMu 下读写。
  • 合并前必须过三道守卫:generation 最新、文档不脏、version 匹配。
  • 脏文档先发空 publishDiagnostics 清旧波浪线。
  • 锁外传出的诊断/version 必须深拷贝(copyRawDiagnostics/copyIntPtr)。

失败模式与缓解

竞态后果(若不防)守卫
文档已再编辑旧位置波浪线错位generation 检查
version 漂移编辑器拒收或显示错版version 匹配 + adoptCachedVersion
并发改内部缓存data racediagnosticsMu + 深拷贝
旁车慢诊断迟到、文档已变异步查询 + generation 守卫丢弃

接下来