oh-my-pi edit 工具深度解析:hashline 行锚定快照补丁语言的设计与源码实现
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
本文基于 oh-my-pi 仓库中 edit 工具文档 展开,完整讲解默认hashline编辑模式的选型逻辑、[PATH#TAG]快照段输入契约、规范补丁语言(PUT/CUT/REM/MV 与寄存器机制)、块锚定规则、执行与输出行为,以及限制、校验与常见失败模式。读完你可以理解该工具如何通过"行锚定 + 文件快照标签"让模型对源码做可验证的精确编辑,并能对照仓库源码(Rust 引擎 与 TypeScript 工具入口)复核每一处行为约定。
edit 工具与模式选择
edit是 oh-my-pi 的必备(essential)内置工具,loadMode标记为"essential"、执行并发为"exclusive"(独占串行),定义见 EditTool 类。默认的hashline模式消费一个"行锚定"的补丁字符串,直接编辑已存在的文件。
从 resolveEditMode 实现 看,激活的"线上协议"(wire contract)按以下顺序解析:
- 模型专属的配置变体(
settings.getEditVariantForModel); - 环境变量
PI_EDIT_VARIANT; - 配置项
edit.mode; - 默认
hashline(DEFAULT_EDIT_MODE)。
支持的模式有hashline、apply_patch、patch、replace(EditMode 类型定义 中还包含实验性的sloppy)。一个值得注意的降级逻辑:除非设置了PI_STRICT_EDIT_MODE,否则短名单中的部分模型族(源码中为 kimi、mimo、deepseek、stepfun,见 edit-mode.ts#L43-L53)会被自动切换到replace契约。
选择不同模式时,工具的 schema、提示词、示例、渲染器乃至可选的自定义 Lark 约束解码格式都会整体切换——EditTool.parameters 按模式返回replaceEditSchema/patchEditSchema/applyPatchSchema/hashlineEditParamsSchema;customFormat会返回当前模式的 Lark 语法以启用约束解码。在apply_patch自定义工具模式下,线上的工具名为apply_patch,但分发仍到达同一个内部工具(customWireName)。
本文聚焦默认 hashline 契约;对应提示词与语法分别位于 hashline.md 模型提示词 和 hashline.lark 规范语法。
输入契约:一个input字符串承载多个[PATH#TAG]段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
input | string | 是 | 一个或多个[PATH#TAG]段,每段包含 hashline 操作。严格自定义工具语法要求段落包在*** Begin Patch/*** End Patch信封中;常规解析器也接受无信封的载荷 |
每个段落编辑一个已存在的文件,且必须复制来自最近一次行锚定的read、grep或成功edit结果中的四位大写十六进制快照标签(TAG):
[src/example.ts#1A2B] PUT 4.=4: +const value = 2;创建或整体覆写文件应使用write工具;hashline 在应用阶段会拒绝未带标签的行锚定编辑。
从 hashline.lark 语法 可以确认该契约的机器可读形态:file_header: "[" filename "#" file_hash "]",其中file_hash: /[0-9A-F]{4}/——标签固定为 4 位大写十六进制。整份语法仅 27 行,清晰界定了put_hunk、cut_hunk、rem_hunk、mv_hunk四类 hunk 及寄存器后缀register: " @" /[A-Za-z0-9_-]+/。
规范补丁语言
所有行号都指原始带标签快照中的行号,而不是同一次调用中先前 hunk 影响后的行号。完整操作表如下(继承自 docs/tools/edit.md 并对照语法核实):
| 形式 | 效果 |
|---|---|
PUT N.=M: | 用后续+TEXT体行替换原始(含端点)行N..M |
PUT N*: | 替换从第N行开始的多行语法块 |
PUT <N:/PUT >N: | 在第N行前/后立即插入体行;PUT <1:即文件头部 |
PUT >$: | 在文件尾部追加上体行 |
PUT >N*: | 插入到从第N行开始的语法块结束之后 |
CUT N.=M/CUT N* | 删除并捕获一个含端点范围或已解析的块;加@name则写入命名寄存器 |
PUT <N/PUT >N/PUT >$ | 将匿名寄存器粘贴到指定缝隙 |
PUT <N @name/PUT >N @name/PUT >$ @name | 将命名寄存器粘贴到指定缝隙 |
PUT N.=M @name/PUT N* @name | 用命名寄存器替换一个范围或块;范围/块粘贴必须使用命名寄存器 |
REM | 删除该段对应的文件 |
MV DEST | 在该段内先前编辑全部生效后,移动/重命名该段文件;目的地含空格时需加引号 |
寄存器规则(详见 clipboard.rs 与 store.rs):
- 寄存器名由 ASCII 字母、数字、
_、-组成; - 匿名寄存器是批次局部的,每次调用从空开始;命名寄存器在会话内持久,且只在其写入落地后才对外可见;
- 操作跨段按从上到下顺序执行,因此前一段的
CUT可以为后一段的PUT供料; - 重复粘贴不会消耗寄存器。
体行(body rows)约定
只有携带体的PUT ...:头(即带冒号的形式)才接受体行。每个体行都是+TEXT;单独一个+表示插入空行。体行是最终内容,绝不是 unified-diff 的 before/after 对。以-或+开头的字面内容必须写成+-...或++...。CUT、基于寄存器的PUT、REM、MV均不接受体行。
这一点在 模型提示词 中被反复强调:"NEVER-old, bare, or context rows: range deletes; body is final content"(绝不使用-old行、裸行或上下文行:范围本身负责删除,体是最终内容)。这也解释了后文"常见失败"中的 unified-diff 污染问题。
块锚定(Block anchors)
块形式(N*)从起始行解析到 tree-sitter 语法节点的结束位置(实现见 block.rs 与 syntax.rs):
- 必须锚定构造体的开头行,绝不能锚定闭合定界符、最后一个可见行、空行或块内部语句;
- 单行节点会被拒绝,并给出改用对应显式行操作的指引;
PUT >N*:在无块可解析时会降级为普通的PUT >N:并附带警告;而替换/剪切的块形式(PUT N*:、CUT N*)则会直接失败而不是猜测;- 前导的装饰器、属性、doc-comment 可能是独立的语法节点:当解析器将其与声明归组时应锚定第一个装饰器,否则使用显式范围;
- 独立的行注释不会被自动扫入块中;
- 在 Markdown 中,标题的块包含其正文以及更深层小节,一直延伸到下一个同级或更高级别的标题。
使用原则:用紧凑的范围,把不相邻的改动拆成独立 hunk;不要仅用edit来重新格式化或改变代码风格——实质性编辑之后应运行项目自身的格式化器。
示例
给定文件内容(read输出的形态):
[greet.py#A1B2] 1:@cache 2:def greet(name): 3: print("Hello, " + name) 4: 5:greet("world")1. 替换带装饰器的函数而不触碰调用方:
*** Begin Patch [greet.py#A1B2] PUT 1*: +@cache +def greet(name): + print(f"Hi, {name}") *** End Patch这里PUT 1*:从第 1 行的@cache装饰器锚定,解析出整个函数块(含装饰器),因此一次操作即可整体替换。
2. 用命名寄存器把它移到另一个已读文件:
*** Begin Patch [greet.py#A1B2] CUT 1* @fn [lib/greet.py#3C4D] PUT <1 @fn *** End Patch前段CUT 1* @fn删除并捕获函数块到命名寄存器@fn;后段PUT <1 @fn将其粘贴到目标文件第 1 行之前。
3. 编辑后重命名:
*** Begin Patch [greet.py#A1B2] PUT 5.=5: +greet("team") MV lib/welcome.py *** End PatchMV在该段先前编辑生效后执行,最终内容落到lib/welcome.py。
执行流程、输出与副作用
hashline 在一次工具调用内完成应用;它不使用ast_edit那种分阶段的xd://resolve/xd://reject流程(对比 ast-edit 文档)。
成功段的返回内容:一个全新的[path#TAG]头(行号与标签均已更新)、可选的块解析行与移动行、可用的紧凑编辑后预览,以及当恢复或规范化产生警告时的Warnings:块。EditToolDetails结构(定义于 renderer.ts)可包含 unifieddiff、firstChangedLine、诊断/格式化结果、操作类型(hashline 模式下为update或delete)、路径/移动元数据、快照以及按文件的结果;多段输入返回一个聚合结果(聚合逻辑见 aggregateDetails,其中对多文件结果的oldText+newText总量设有 32KB 预算,超出的条目退化为纯文本)。
流式预览:openArgStream 为每次工具调用建立EditSession,把增量文本推给原生引擎;原生侧的预览入口在 HashlineEngine::preview——流式期间解析失败只返回空预览而非错误。预览策略解析"在途载荷"中已完整的部分并计算只读 diff;对未解析的临时块、过期标签、空粘贴等情况跳过,而不把部分输入呈现为最终失败;真正的执行阶段会正常地重新读取并校验。
多段调用的原子性边界:所有段先全部解析并准备,写盘才开始,因此语法、锚点与 no-op 类错误可以 fail fast。随后文件按顺序写入;如果操作系统写入失败,已落地的前缀可能保持已应用状态,命名寄存器的会话状态也只推进到该已落地前缀为止。执行主体见 execute 方法,其中还包括解析回归(parse regression)时的自动语法修复与记录。
限制与校验
以下约束继承自文档,并逐项在 Rust 引擎中可对应到实现:
- 快照标签:4 位大写十六进制字符,由规范化后的文件内容派生,记录在会话快照存储中(store.rs 负责会话级快照、剪贴板寄存器与 no-op 循环状态);
- 可见行范围:
read/grep的暴露范围很重要——指向已记录可见范围之外行的编辑会被拒绝;编辑被省略或未显示的区域前,必须重新读取; - 范围规则:范围为含端点、必须有序,且在检查目标文件实际边界之前,先受100,000 行展开放大上限约束(源码常量
MAX_EXPANDED_RANGE_LINES: u32 = 100_000,见 parser.rs#L28); - 重叠检测:重叠的编辑或多个操作指向同一原始锚点会被拒绝;
- 同路径合并:同一路径的段会被合并,使它们的原始行锚点共同生效;若交错出现的同路径段会使已编写的寄存器顺序变得含糊,剪贴板操作会被拒绝;
- 过期标签恢复:过期标签会尝试基于快照的恢复(recovery.rs);仅当记录的快照链能证明唯一安全结果时才应用恢复,否则返回"与当前上下文不匹配";
- no-op 拒绝:字节级完全相同的编辑是错误;重复同一 no-op 载荷三次会触发 no-op 循环保护升级。
常见失败与解析器恢复
文档列出的常见失败模式:
- 缺失/格式错误的
[PATH#TAG]、未知快照标签、或路径已不存在; - 锚点超出文件边界、超出已记录的可见行范围、落在省略区域、或基于无法安全恢复的过期快照;
- 反转或重叠的范围;
- 带体的
PUT体为空、无体操作下出现体行、未知命名寄存器、或在无明确匿名CUT之前做匿名粘贴; - 块锚定落在不支持/无效的语法树上、空行/闭合行、或单行节点上;
- unified-diff 污染(
@@、apply-patch 哨兵行、-old行)混入 hashline 操作与最终内容+行; REM/MV冲突、非法移动目的地、目标碰撞、或文件系统写入失败;- 补丁解析与应用后恰好等于既有字节(无变化)。
解析器对模型常见的"手滑"提供了有限恢复:可选信封、无害的头噪声、部分裸行与范围拼写变体。解析器在修复输入时会输出警告。文档同时明确:调用方应当(SHOULD)只输出上文的规范语法——恢复行为不是第二套公开语法。
源码索引
| 关注点 | 路径 |
|---|---|
| 工具入口、模式分派、流式预览与执行 | packages/coding-agent/src/edit/index.ts |
模式解析(PI_EDIT_VARIANT/edit.mode/ 模型降级) | packages/coding-agent/src/utils/edit-mode.ts |
| 各模式参数 schema | packages/coding-agent/src/edit/schemas.ts |
| hashline 引擎(preview / stage / inspect) | crates/pi-edit/src/modes/hashline/mod.rs |
| 解析器与范围放大上限 | crates/pi-edit/src/modes/hashline/parser.rs |
| 补丁暂存与应用 | crates/pi-edit/src/modes/hashline/patcher.rs、apply.rs |
| 过期标签恢复 | crates/pi-edit/src/modes/hashline/recovery.rs |
| 块解析 / tree-sitter 语法 | crates/pi-edit/src/modes/hashline/block.rs、syntax.rs |
| 寄存器(剪贴板) | crates/pi-edit/src/modes/hashline/clipboard.rs |
| 快照存储、no-op 循环状态 | crates/pi-edit/src/store.rs |
| 规范约束解码语法(Lark) | crates/pi-edit/grammars/hashline.lark |
| 模型面向提示词(完整版 / 紧凑版) | crates/pi-edit/prompts/hashline.md、packages/coding-agent/src/edit/hashline-compact.md |
| 工具文档原文 | docs/tools/edit.md |
需要说明一点:原始文档的 Source 小节曾指向packages/hashline/src/*(如input.ts、parser.ts、apply.ts);从当前仓库结构看,hashline 引擎已经迁入 Rust crate crates/pi-edit,对应模块为modes/hashline/下的同名文件,本文的引用以当前仓库实际路径为准。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考