LSP code action 与命令
ttsc 在 tsgo 的 LSP 之上追加自己的 code action(如 lint 修复、format)和 workspace/executeCommand 处理器(如应用 WorkspaceEdit)。本页讲 Proxy 的 codeAction 增强与 executeCommand 路由,以及多宿主场景下的命令 id 前缀。源码 packages/ttsc/internal/lspserver/lsp_proxy.go。
codeAction:本地处理 vs 转发增强
handleCodeActionRequest(lsp_proxy.go:335)决定一个 codeAction 请求怎么处理:
shouldForwardCodeActionRequest(lsp_proxy.go:434):当 upstream 提供 codeAction 且请求不是"仅插件 kind"时转发。isPluginOnlyCodeActionRequest(lsp_proxy.go:441)检查请求的 context.Only 是否全在插件 kind 集合内(source.fixAll.ttsc,以及 ttsc 拥有格式化器时的 source.format)——若是,没必要打扰上游,直接本地处理。
本地路径(仅插件 kind)
异步 goroutine 跑 source.CodeActions(uri, range, ctx),但前后都用 isDocumentCleanAt(uri, generation) 守卫:查询前文档变脏就回空数组;查询后变脏就丢弃结果。pendingLocalActions 记账 + takePendingLocalCodeAction 保证一个 id 只回一次(防止 cancel 与完成竞态)。
转发-增强路径
转发上游,记 pendingActions[id] = pending(带 generation)。upstream 响应回来时 appendCodeActions(lsp_proxy.go:1683)把 ttsc code action 拼进去:
- 拒绝拼进错误响应(JSON-RPC §5.1 禁止同帧同时有 result 和 error)。
- 拒绝拼进非数组结果(LSP 要求 codeAction 结果是数组或 null;其他形状原样转发免得损坏)。
- 拼前再次
isDocumentCleanAt守卫——文档变脏就不拼。
executeCommand:ttsc 拥有的命令本地处理
tryExecuteCommand(lsp_proxy.go:472):
- 解析
command+arguments; sourceCommandID去掉可能的前缀(见下),ownsCommand判定是否 ttsc 拥有;- 不拥有 → 返回
false让它流向上游 tsgo; - 拥有 → 若参数含脏文档,立即回 nil(不在脏文档上执行命令);
- 否则记
pendingCommands,异步completeExecuteCommand。
completeExecuteCommand(lsp_proxy.go:502)调 source.ExecuteCommand(command, args):
ErrCommandNotHandled(命令被广告了但 source 没处理)→ 回错误 "advertised but not handled"。- 其他错误 → 回错误(但只在文档仍干净时,
writeExecuteCommandErrorIfClean)。 - 成功 → 把 WorkspaceEdit 放进 executeCommand 响应里返回(
writeExecuteCommandResultIfClean),而非发workspace/applyEdit服务端→客户端请求。
为什么把 edit 放响应里(lsp_proxy.go:514 注释):ttsc 拥有两端(它的 VS Code 扩展),扩展自己应用 edit。坚持单方向避免在代理里跟踪自己发出的 outgoing request id。
脏守卫贯穿始终
writeExecuteCommandResultIfClean(lsp_proxy.go:1749)在 writeMu + diagnosticsMu 下检查多项:参数文档 generation 是否变、参数是否含脏文档、WorkspaceEdit 目标文档是否变或脏。任一为真就回 nil(不应用过期 edit)。这保证命令执行期间文档被编辑时,不会把基于旧内容算出的 edit 应用到新内容上。
命令 id 前缀(多宿主场景)
ExecuteCommandIDPrefix(ProxyOptions)让运行多个代理实例、共用一个全局命令注册表的宿主避免命令 id 冲突。advertisedCommandID(lsp_proxy.go:1531)给广告的命令 id 加前缀;进来的带前缀 id 经 sourceCommandID 映射回去再分派。rewriteCodeActionCommands(lsp_proxy.go:1538)把 code action 里嵌的命令 id 也加前缀。
SuppressExecuteCommandProvider / SuppressedExecuteCommandIDs 让那些自己注册 wrapper 命令的客户端把 ttsc 命令 id 排除在 initialize 响应外(advertisedCommandIDs,lsp_proxy.go:1510)。
不变量
- 脏文档上不执行命令、不应用 edit、不增强 code action。
- pending 记账(
pendingActions/pendingLocalActions/pendingCommands)保证一个 id 只回一次。 - WorkspaceEdit 通过 executeCommand 响应回,不发 applyEdit 请求。
- code action 只能拼进数组/null 结果,绝不拼进错误响应。