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 转发增强

handleCodeActionRequestlsp_proxy.go:335)决定一个 codeAction 请求怎么处理:

shouldForwardCodeActionRequestlsp_proxy.go:434):当 upstream 提供 codeAction 且请求不是"仅插件 kind"时转发。isPluginOnlyCodeActionRequestlsp_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 响应回来时 appendCodeActionslsp_proxy.go:1683)把 ttsc code action 拼进去:

  • 拒绝拼进错误响应(JSON-RPC §5.1 禁止同帧同时有 result 和 error)。
  • 拒绝拼进非数组结果(LSP 要求 codeAction 结果是数组或 null;其他形状原样转发免得损坏)。
  • 拼前再次 isDocumentCleanAt 守卫——文档变脏就不拼。

executeCommand:ttsc 拥有的命令本地处理

tryExecuteCommandlsp_proxy.go:472):

  1. 解析 command + arguments
  2. sourceCommandID 去掉可能的前缀(见下),ownsCommand 判定是否 ttsc 拥有;
  3. 不拥有 → 返回 false 让它流向上游 tsgo;
  4. 拥有 → 若参数含脏文档,立即回 nil(不在脏文档上执行命令);
  5. 否则记 pendingCommands,异步 completeExecuteCommand

completeExecuteCommandlsp_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。

脏守卫贯穿始终

writeExecuteCommandResultIfCleanlsp_proxy.go:1749)在 writeMu + diagnosticsMu 下检查多项:参数文档 generation 是否变、参数是否含脏文档、WorkspaceEdit 目标文档是否变或脏。任一为真就回 nil(不应用过期 edit)。这保证命令执行期间文档被编辑时,不会把基于旧内容算出的 edit 应用到新内容上。

命令 id 前缀(多宿主场景)

ExecuteCommandIDPrefixProxyOptions)让运行多个代理实例、共用一个全局命令注册表的宿主避免命令 id 冲突。advertisedCommandIDlsp_proxy.go:1531)给广告的命令 id 加前缀;进来的带前缀 id 经 sourceCommandID 映射回去再分派。rewriteCodeActionCommandslsp_proxy.go:1538)把 code action 里嵌的命令 id 也加前缀。

SuppressExecuteCommandProvider / SuppressedExecuteCommandIDs 让那些自己注册 wrapper 命令的客户端把 ttsc 命令 id 排除在 initialize 响应外(advertisedCommandIDslsp_proxy.go:1510)。

不变量

  • 脏文档上不执行命令、不应用 edit、不增强 code action。
  • pending 记账(pendingActions/pendingLocalActions/pendingCommands)保证一个 id 只回一次。
  • WorkspaceEdit 通过 executeCommand 响应回,不发 applyEdit 请求。
  • code action 只能拼进数组/null 结果,绝不拼进错误响应。

失败模式

失败行为
命令广告了但 source 不处理回 "advertised but not handled" 错误
命令执行期间文档变脏回 nil(不应用过期 edit)
upstream codeAction 返回非数组原样转发,不增强
cancel 与完成竞态pending 记账保证只回一次

接下来