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 带的整数版本)。
prepareMergedPluginDiagnostics(lsp_proxy.go:1377)实现这套守卫:在 diagnosticsMu 下检查 generation 未变、文档不脏、version 与缓存 upstream 一致,全过才合并;否则返回 (nil, nil, false) 让调用方不发布。
脏文档:先清空再说
didChange 一来,markDocumentDirty(lsp_proxy.go:1243):
- 把 uri 标进
dirtyDocuments,记dirtyVersions; - 删掉
pluginDiagnostics[uri]与upstreamDiagnostics[uri]; - 自增
diagnosticGeneration与documentGeneration; - 若之前有插件诊断,发一个空
publishDiagnostics清掉编辑器上的旧波浪线。
为什么先清空:文档一改,旧诊断的位置就可能错位。与其显示陈旧波浪线,不如先清掉、等新一轮诊断来。
markDocumentClean(lsp_proxy.go:1278)在文档稳定后清脏标志。
didOpen 与 didSave 触发重新发布
didOpen:cacheDidOpenText缓存全文,publishPluginDiagnosticsForDidOpen拉一轮插件诊断。didSave:publishPluginDiagnosticsForDocumentNotification重新发布(保存后文件落盘,旁车读盘能拿到最新内容)。
version 处理的微妙点
shouldRememberDirtyUpstreamDiagnostics(lsp_proxy.go:1308):当 upstream 诊断带的 version 恰好等于当前脏版本时,仍记下它——因为那是 tsgo 对这一版的权威诊断,即使文档此刻标脏。adoptCachedVersion 让合并时采用缓存的 upstream version 而非输入 version,保持编辑器看到的 version 一致。
copyIntPtr / copyRawDiagnostics(lsp_proxy.go:1658)到处用:诊断与 version 在锁外被复制,避免把内部缓存的切片/指针泄漏给调用方后被并发改动。
一次合并的完整时序
不变量
- 诊断与文档状态统一在
diagnosticsMu下读写。 - 合并前必须过三道守卫:generation 最新、文档不脏、version 匹配。
- 脏文档先发空 publishDiagnostics 清旧波浪线。
- 锁外传出的诊断/version 必须深拷贝(
copyRawDiagnostics/copyIntPtr)。