oh-my-pi LSP 工具详解:用语言服务器实现符号级导航、重命名与诊断
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
oh-my-pi(⌥ Coding agent with the IDE wired in)的lsp工具把语言服务器协议(LSP)的能力直接暴露给 Agent 模型:它通过file + line + symbol的定位模型查询 language server,完成跨文件的符号感知导航、重构与诊断,弥补纯文本工具(grep/sed/ast_edit)看不见调用点(callsite)的盲区。读完本文,你将掌握lsp全部 14 种 action 的调用约定、#N出现次数选择器、apply语义,以及它背后"定位解析 → 客户端复用 → JSON-RPC 请求 → 编辑回写"的完整调用链。
工具定位:为什么 Agent 需要"符号感知"的代码智能
文本工具(如grep、sed、ast_edit)基于字面量或语法树工作,但跨文件的重构和引用分析需要语义信息:一个符号可能被 shadow、被 re-export、被其他文件以别名引用,纯文本搜索会静默遗漏调用点。oh-my-pi 的lsp工具正是为此而生——它在模型提示词中这样定义自己:
Symbol-aware code intelligence from language servers — navigation, refactors, and diagnostics where text tools miss callsites.
该工具的模型面提示词位于 packages/coding-agent/src/prompts/tools/lsp.md,工具本体实现在 packages/coding-agent/src/lsp/tool.ts,参数 schema 定义在 packages/coding-agent/src/lsp/types.ts。它是"可发现(discoverable)"而非"热加载"的工具,由 packages/coding-agent/src/tools/index.ts 通过LspTool.createIf注册——只有当session.enableLsp !== false且lsp.enabled(默认true)时才会出现。
操作总览:14 种 action
lsp工具的核心参数是action枚举,schema 定义在 types.ts:
action: "'diagnostics' | 'definition' | 'references' | 'hover' | 'symbols' | 'rename' | 'rename_file' | 'code_actions' | 'type_definition' | 'implementation' | 'status' | 'reload' | 'capabilities' | 'request'"每种 action 的语义可归纳如下:
- 定位型:
definition、type_definition、implementation、references、hover——基于file + line + symbol定位后查询对应 LSP 方法; - 重构型:
rename(符号重命名)、rename_file(文件移动 + 引用重写)、code_actions(快速修复/服务器已知重构); - 查询型:
diagnostics(诊断)、symbols(符号列表/工作区搜索)、status(服务器状态)、capabilities(服务器能力)、request(任意原始 LSP 方法)。
位置模型:file + line + symbol
lsp的所有单文件 action 都采用统一的**基于位置(position-based)**定位方式,模型提示词中的约定为:
Position-based:
file+line+symbol(substring;#Nfor Nth match).lineis 1-indexed.
即:
file为文件路径;line为1 起始(1-indexed)的行号,与 LSP 协议内部 0 起始(0-indexed)不同,工具负责转换;symbol为目标行上的子串,用于解析该行的列(column)位置;支持name#N形式选择该行第 N 次出现(N 为 1 起始,默认 1)。
源码级解析:resolveSymbolColumn
列解析实现在 packages/coding-agent/src/lsp/utils.ts 的resolveSymbolColumn():
- 未提供
symbol时,取目标行第一个非空白字符的列位置(firstNonWhitespaceColumn); - 提供
symbol时,先按parseSymbolSpec解析name#N形式(utils.ts),规则是贪婪匹配^(.+)#(\d+)$,因此#name#2会被解析为 symbol=#name(TS 私有字段)、occurrence=2; - 匹配顺序:先精确匹配,再大小写不敏感匹配;若
symbol是裸标识符(如foo),还会强制要求词边界(前后不能是标识符字符),避免把foobar也算进去; #N越界或符号未找到时显式抛错:Symbol "..." not found on line N/occurrence N is out of bounds on line N,绝不静默回退。
关键的"不静默回退"设计
对于definition、references、rename这三个 action,当针对 project-aware(项目感知型)服务器且提供了line时,必须提供symbol——即使省略symbol本可以回退到行首第一个非空白字符,实现也选择抛ToolError而非静默回退。原因正如提示词所写:
Project-aware lookups ERROR without
symbol— no silent fallback on missing/ambiguous matches.
这是防止"定位到错误符号却返回看似正确的结果"的防御性设计,体现在 tool.ts 对应 action 分支的校验中。
rename:默认应用的符号重命名
rename的调用约定:
rename— applies by default;apply: falsepreviews. Project-aware lookups ERROR withoutsymbol— no silent fallback on missing/ambiguous matches.
- 必填:
file、new_name;可选line、symbol、apply、timeout; - 默认应用:
apply !== false即立即通过applyWorkspaceEdit()落盘(tool.ts); - 预览:显式传
apply: false时,用formatWorkspaceEdit()渲染改动摘要而不写入; - 执行流程:等待项目加载完成 → 发送
textDocument/rename→ 接收WorkspaceEdit→ 应用或预览。
输出形如Applied rename:(附应用的行级改动)或Rename preview:(汇总编辑),无编辑时返回Rename returned no edits。其完整行为记录在 docs/tools/lsp.md 的rename小节。
code_actions:先列出,再精确应用一个
- `code_actions` — lists by default; apply ONE with `apply: true` + `query` (title substring or index).- 默认列出:发送
textDocument/codeAction(零宽 range 在解析出的位置上),query作为context.only: [query]传给服务器,作为服务器端 kind 过滤器; - 应用一个:
apply: true且query非空时,query变成客户端选择器——可以是 0 起始的数字索引,也可以是 action 标题的大小写不敏感子串; - 已知限制:
apply: true但省略query时,当前实现会回落到列出模式而不应用(docs/tools/lsp.md 的 Notes 中明确记录); - 应用时走
applyCodeAction()(utils.ts):可选codeAction/resolve补齐细节 → 应用WorkspaceEdit→ 可选执行workspace/executeCommand;裸Command只执行命令。
提示词建议:imports、quick-fix 和服务器已知的重构,优先用code_actions而非手改——这能保证改动与服务器语义一致。
rename_file:移动文件并重写所有引用
- `rename_file` — moves file AND rewrites all imports/references; applies by default.rename_file是lsp独有的"原子级"文件移动操作:
- 必填:
file(源路径)、new_name(目标路径);可选apply、timeout; - 前置校验严格:源目标相同、源不存在、目标已存在、重命名集为空都会返回错误;目录重命名超过 1000 个文件会拒绝执行(
MAX_RENAME_PAIRS,定义于 tool.ts); - 对每个匹配
fileTypes的非自定义 LSP 服务器发送workspace/willRenameFiles,收集返回的WorkspaceEdit和服务器备注; - 应用模式把各服务器的文本编辑按 URI 合并(project-aware 主服务器在重叠时优先,其他服务器的重叠编辑被丢弃并记录),从单次快照对每个 URI 应用一次,然后执行磁盘重命名,再发送
textDocument/didClose(清理已打开的旧文件)与workspace/didRenameFiles; - 关键安全阀:若某个支持
willRenameFiles的服务器请求失败,应用模式会中止整个重命名且不移动任何文件——避免移动路径后留下悬空引用(tool.ts 注释引用了 issue #8380)。
apply: false时输出Rename preview: <源> → <目标>及各服务器编辑摘要;应用时输出Renamed ...加上每服务器的编辑行。
diagnostics:单文件、glob 与工作区三级诊断
- `diagnostics` — path, glob (`src/**/*.ts`), or `file: "*"` for workspace.- 单文件/glob:
resolveDiagnosticTargets()(utils.ts)先用Bun.Glob展开模式,超过MAX_GLOB_DIAGNOSTIC_TARGETS(前 20 个匹配)时截断并给出警告; - 每个匹配文件会查询所有适用的服务器:真实 LSP 服务器会等待项目加载、
refreshFile()后waitForDiagnostics()等待新鲜的publishDiagnostics;自定义 linter 客户端(Biome/SwiftLint/LspLinter)则直接调用lint(file); - 结果按 range+message 去重,并按严重级别排序输出;单目标无问题时输出
OK,有则输出摘要:\n分组诊断; - 工作区模式
file: "*":按 Rust → TypeScript → Go → Python 的优先级选择首个匹配的项目类型,分别运行cargo check --message-format=short、npx tsc --noEmit、pyright或go build(go.work会先读go work edit -json再对每个 module 构建,失败回退./...);未知项目返回提示信息而不启动检查器。
diagnostics是唯一同时查询普通 LSP 服务器与自定义 linter 客户端的 action(docs/tools/lsp.md Modes/Variants 小节)。
symbols:文件符号树与工作区符号搜索
- `symbols` — `file` lists file symbols; `file: "*"` + `query` searches workspace.- 文档模式:发送
textDocument/documentSymbol到主服务器,若首个条目带selectionRange则格式化为层级结构的DocumentSymbol(含子节点缩进),否则格式化扁平的SymbolInformation; - 工作区模式:
file: "*"且必须提供query(缺失时返回Error: query parameter required for workspace symbol search),向每个非自定义服务器发送workspace/symbol,结果经filterWorkspaceSymbols()(按 name/containerName/文件路径子串过滤)与dedupeWorkspaceSymbols()去重后,截断到WORKSPACE_SYMBOL_LIMIT(前 200 条); - 输出形如
Found N symbol(s) matching "query":后接name @ file:line:col。
reload:重启服务器并重读配置
- `reload` — restart one server (`file`) or all (`*`); `reload *` re-reads LSP config.- 工作区模式(
file省略或"*"):先清空该 cwd 的配置缓存,从磁盘重读配置,再重载所有新配置的非自定义服务器——这是配置变更后重新生效的入口; - 单文件模式:保留缓存配置,只重载该文件的主服务器;
- 两种模式都会清除匹配的最近初始化失败记录。对 rust-analyzer 服务器,
reloadServer()会先尝试rust-analyzer/reloadWorkspace请求(该请求只有 rust-analyzer 实现,发送给 Roslyn 等服务器会使其崩溃,因此按服务器二进制/名称门控);随后回退到workspace/didChangeConfiguration通知;通知也失败时直接拆除客户端,由下一次请求冷启动重建。共享 mux 客户端会先发送 mux 重启通知以替换共享服务器(docs/tools/lsp.mdreload小节)。
request:原始 LSP 方法直通
- `request` — raw: `query` = method, `payload` = JSON params (else auto-built).request是逃生舱口,允许对单个服务器发送任意 LSP 方法:
- 必填
query(方法名),可选file、line、symbol、payload、timeout; - 服务器选择:
file具体时用该文件的主非 linter 服务器,否则用第一个配置的非自定义服务器; - 参数构建优先级(tool.ts):
- 提供
payload→ 解析 JSON 原样使用(JSON 非法会返回错误并附details.success: false); file具体且带line→ 构建{ textDocument: { uri }, position: { line: line - 1, character } },character 由resolveSymbolColumn()解析;file具体但无line→{ textDocument: { uri } };- 否则 →
{};
- 提供
- 成功输出
<server> ← <method>:加格式化结果(非字符串结果JSON.stringify(..., null, 2));失败输出LSP error from <server> on <method>: ...并回显截断到 400 字符的请求参数。
Critical 使用规则:何时必须用 lsp
模型提示词的最后一段是强制的行为约束:
- 符号感知工作(rename、references、definition、code actions)只要服务器可用,就必须用
lsp——它跟随 shadowing、re-export 和文本工具会遗漏的跨文件用法; - 绝不在
lsp rename/rename_file可用时用ast_edit/sed/手改做跨文件重命名——文本重命名会静默丢弃调用点; - imports、quick-fix 和服务器已知重构优先
code_actions,再考虑手改。
这三条规则对应了工具设计上"宁可报错也不静默出错"的整体哲学。
底层调用链与运行模型
一次典型的lsp调用在 tool.ts 的LspTool.execute()中经历:
- 超时钳制:
clampTimeout("lsp", timeout, ...),默认 20 秒、范围 5–300 秒,并受全局tools.maxTimeout上限约束;生成AbortSignal.timeout(...)与调用方信号合并; - 配置加载:
getConfig(cwd)按 cwd 缓存LspConfig(config.ts),合并defaults.json与各级 JSON/YAML 覆盖;工作区reload是显式的缓存失效例外; - 服务器路由:
getServersForFile()按扩展名或精确 basename 匹配fileTypes,主服务器排在 linter 之前;导航/重构路径用getLspServersForFile()进一步过滤掉自定义 linter 客户端(Biome/SwiftLint 等绝不会成为导航目标); - 客户端复用:
getOrCreateClient()按command:cwd缓存客户端;lsp.shared(SDK 会话默认true)时优先使用 broker 管理的项目 mux 共享传输(Unix socket / Windows named pipe),失败静默回退到私有子进程;随后完成initialize握手、缓存 capabilities、发送initialized; - 请求收发:
sendRequest()(client.ts)分配递增 JSON-RPC id,安装中止与超时处理,中止时发送$/cancelRequest; - 编辑回写:返回编辑的 action 用
applyWorkspaceEdit()应用或formatWorkspaceEdit()预览;rename_file额外执行磁盘重命名并发送workspace/didRenameFiles。
配置细节(文件位置、优先级、ServerConfig字段、内置服务器清单)可继续阅读 docs/lsp-config.md;各 action 的完整输入/输出/执行语义见 docs/tools/lsp.md;回归行为有 test/tools/lsp-regressions.test.ts 与 test/tools/index.test.ts 覆盖。
小结
lsp工具把 LSP 的语义能力封装成对 Agent 友好的统一接口:1-indexed 的line、子串加#N的symbol选择器、默认应用/显式预览的apply语义,以及对缺失与歧义"显式报错"的防御策略,共同保证了符号级操作的高保真。配合 14 种 action 与"优先级使用 lsp"的规则,Agent 得以在 IDE 级别理解代码结构,而不是在文本层盲改。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考