Skip to content

Latest commit

 

History

History
342 lines (252 loc) · 41.1 KB

File metadata and controls

342 lines (252 loc) · 41.1 KB

vim-hexedit 重构需求文档

1. 概述

vim-hexedit 的核心能力:在 Vim 内借助 xxd 完成二进制文件的十六进制编辑,以及在多种展示格式之间互相转换。

插件当前支持四种模式,并规划了第五种模式(格式解析模式,见 4.5):

  • Hex 编辑模式:可编辑的十六进制/ASCII 双视图,是插件的核心编辑能力。
  • Hex 字串模式:仅展示十六进制字符串本体,可编辑,支持插入/删除字符。
  • C 语言视图模式:只读,展示可直接用于 C 变量赋值的字节数组。
  • Python 视图模式:只读,展示可直接在 Python 3 中运行的字节串构造代码。
  • 格式解析模式(规划中):按文件格式对二进制内容做语义解析,用背景色关联字段含义,见 4.5。

由于模式数量会持续增加,本次重构除了梳理四个既有模式和新增命令外,必须在架构层面解决扩展性问题:模式之间的转换、以及"可编辑/只读"两类能力的划分,需要有统一的接口设计,而不是每新增一个模式就重新实现一遍点对点转换逻辑。架构设计见第 2 节。

此外,当前实现是"整文件读入 → 整文件 xxd → 整个 buffer"的模式,面对大文件(尤其是用户往往只关心其中一小段内容的场景)会有明显的性能和体验问题。本次重构也需要引入大文件窗口化编辑的需求,见第 5 节。

2. 架构设计:模式接口抽象与扩展性

2.1 现状问题

当前实现里,模式之间的转换是"点对点"的:例如 lib/hex2c_ui.vim 直接对 Hex 编辑模式渲染出的文本做正则替换生成 C 视图,lib/hex2py_ui.vim 同理生成 Python 视图;调度层 autoload/hexedit.vim 也是针对"HexEdit ↔ 某个模式"手写了三段几乎重复的 hexedit#ToggleHexKeep/hexedit#ToggleHex2C/hexedit#ToggleHex2Py。这种方案在只有 4 个模式时勉强够用,但存在两个扩展性问题:

  1. 模式之间是两两耦合的文本转换,模式数量增多后转换逻辑会趋向平方级增长,新增一个模式需要了解所有已有模式的文本格式细节。
  2. "是否可编辑"这个能力目前是每个模式各自用不同手法实现的(如 C/Python 视图用 feedkeys("<esc>") 硬编码阻断插入),没有统一的接口边界,无法清晰表达"哪些模式可编辑、哪些只读、哪些未来可能两者兼有"。

第 4.5 节规划的格式解析模式进一步放大了这个问题:它需要携带字段语义标注(而不仅仅是裸字节),且其"可编辑性"在 MVP 阶段和未来阶段可能不同。因此本次重构要求引入统一的中间表示与可插拔接口,为后续模式扩展留出空间。

2.2 统一中间表示(Canonical Model)

所有模式的渲染与解析都应该通过统一的"二进制 + 元数据"中间表示中转,而不是模式 A 的文本直接转换成模式 B 的文本:

模式 A 文本  --[ViewToBinary]-->  Canonical Model  --[BinaryToView]-->  模式 B 文本

Canonical Model 至少包含:

  • 原始字节序列(byte[]),是唯一权威的数据来源。
  • 可选的字段标注层(annotation layer):每个字段包含 offset、length、名称、语义/类型描述、建议高亮颜色或高亮组。该标注层在当前四种模式下默认为空,是专门为格式解析模式预留的扩展点(见 4.5)。
  • 窗口元信息(大文件场景下生效):文件总大小、当前 Canonical Model 所代表的字节区间在文件中的起始绝对偏移(window base offset)及长度。普通小文件场景下窗口即整个文件,起始偏移恒为 0;大文件窗口化模式下(见第 5 节),Canonical Model 只代表当前缓存窗口的字节,而不是整文件。字段标注层里的 offset 统一使用相对文件起始的绝对偏移,不随窗口滑动而改变含义,保证格式解析模式在大文件场景下也能正确定位字段。

任何模式接入插件时,只需要实现"从 Canonical Model 生成本模式视图"(以及如果可编辑,"从本模式视图解析回 Canonical Model"),不需要关心其他模式的文本格式。这样第 3 节要求的":Hedit/:Hex/:H2C/:H2Py 任意模式间直接互切",在架构上会落地为"N 个模式各自对接同一个中间层",而不是 N×N 的两两转换矩阵。

2.3 两组可插拔接口

每个模式在架构上应实现以下两组接口中的一组或两组,具体取决于该模式的能力:

  • 渲染接口(Renderable,所有模式必选)
    • BinaryToView(CanonicalModel) -> 缓冲区文本:把中间表示渲染成本模式的文本内容和高亮展示。
    • 负责设置对应的 filetype 及触发对应语法高亮。
  • 可编辑接口(Editable,仅部分模式实现)
    • ViewToBinary(缓冲区文本) -> CanonicalModel:把当前缓冲区文本解析回中间表示,需要包含格式校验(如 Hex 字串模式的奇偶长度校验),校验失败时拒绝转换并提示用户。
    • 光标约束钩子:限制光标只能落在该模式定义的合法可编辑位置。
    • 按键/字符输入校验钩子:限制哪些字符可以键入、以及是"原地替换"还是允许常规插入位移。

模式与接口的对应关系(不要求所有模式都可编辑,也不要求可编辑模式之外的模式用另一套完全独立的机制去"伪装"只读):

模式 Renderable Editable
Hex 编辑模式 ✅ ✅
Hex 字串模式 ✅ ✅
C 语言视图模式 ✅ ❌(只读)
Python 视图模式 ✅ ❌(只读)
格式解析模式(规划中,见 4.5) ✅ 架构预留可选项,MVP 阶段为 ❌

约定:一个模式只要未实现 Editable 接口,就自动是只读模式,调度层统一按"未实现 Editable → 禁止进入插入模式"处理,不需要每个只读模式各自手写阻断逻辑(例如现状 C/Python 视图各自在 OnInsertEnter 里 feedkeys("<esc>"),重构后应收敛为调度层的统一行为)。

2.4 对模式切换机制的架构要求

  • 切换命令(:Hedit/:Hex/:H2C/:H2Py)的实现应统一走"当前模式(若实现 Editable)ViewToBinary → Canonical Model → 目标模式 BinaryToView"的路径,不应再出现"模式 A 直接对模式 B 的已渲染文本做正则替换"的点对点转换。
  • 切换到只读模式时无需调用当前模式的 ViewToBinary(除非当前模式本身是可编辑的,此时仍需先把编辑结果吸收进 Canonical Model,再渲染成目标只读视图)。
  • 调度层(对应当前 autoload/hexedit.vim 中的状态机)需要从"针对每一对模式手写 toggle 函数"改造为"按模式名查表分发",新增模式时只需注册一个新模式对象(提供 Renderable,及可选的 Editable 实现),无需新增或修改调度函数。这一点是支撑未来格式解析模式,以及更多模式接入的前提。

2.5 状态机边界:入口与出口

模式切换命令要区分"状态机内部流转"和"状态机的进入/退出"两类不同职责,避免把边界处理逻辑混进内部调度:

  • :Hedit/:Hex/:H2C/:H2Py 四个命令只负责插件状态机内部各模式之间的相互切换(Canonical Model 保持不变,只是 View 层切换),前提是状态机已经处于激活状态(即 b:HexEditCurrentUI 非空)。它们不需要考虑、也不需要实现"如何从 Vim 日常编辑模式进入状态机"或"如何退出状态机回到 Vim 日常编辑模式"这两类边界场景。
  • 进入状态机只有两条路径:第 6 节的二进制模式/文件后缀自动触发,以及 :HexLoad(把一段文本形式的 Hex 字串转换为二进制并带入状态机)。
  • 退出状态机只有一条路径::HexDump(把 Hex 编辑模式内容转换为 Hex 字串文本后彻底脱离状态机,回到 Vim 日常编辑模式,见第 6 节第 4 条)。
  • 因此调度层的模式注册表/查表分发(2.4 最后一条)只需要覆盖"已注册的插件内部模式"之间的转换,不需要把"普通 Vim 文本"也建模成状态机里的一个模式;:HexLoad/:HexDump 作为独立的入口/出口命令单独实现,不复用四个内部切换命令的调度路径。

2.6 开发语言与运行时选型

结论:继续使用 Vimscript 实现(兼容 Vim 8.1+ 与 Neovim 的传统语法),不采用 Vim9script,也不采用 Neovim 独占的 Lua 插件系统。

  • 理由:本次是对一个已发布 Vim 插件的重构(README 记录的安装方式为 ~/.vim 直装或 Pathogen,是面向传统 Vim 插件管理的分发方式),说明现有用户群体不限于 Neovim。改用 Neovim 独占技术栈会直接放弃 Vim-only 用户,属于比架构收益更大的成本,不予采用。

  • 排除 Vim9script 的原因:Neovim 官方明确不会实现 Vim9script 语法。如果核心逻辑使用 Vim9script 编写,会间接牺牲 Neovim 兼容性,与"传统 Vimscript 保持 Vim/Neovim 双端兼容"的目标直接冲突,因此核心逻辑一律使用传统(legacy)Vimscript 语法。

  • 权衡说明:Neovim 独占的 Lua + vim.loop(libuv)异步 job + extmark 技术栈,在实现以下几个高价值需求点时确实会更顺手、性能更好:

    • Canonical Model 的类结构(Lua 面向对象设计比 Vimscript 字典模拟类更自然)。
    • 第 5 节大文件的后台滑动预加载(Neovim 的异步 job/libuv 生态比 Vim 的 job/channel 更成熟)。
    • 未来格式解析模式对大量字段做背景色高亮(extmark 通常比 Vim 的文本属性/matchadd() 性能更好、管理更灵活)。

    但传统 Vimscript 配合 Vim 8.1+/Neovim 均支持的 job_start/jobstart 异步机制,以及各自的高亮 API(Vim 的文本属性 prop_add/prop_type_add vs Neovim 的 nvim_buf_set_extmark),同样可以满足上述需求,只是需要多做一层"平台适配层"来打平两端接口差异,实现和性能打磨的工作量更大。这个额外工作量是为保住 Vim/Neovim 双端兼容性而付出的合理代价。

  • 落地要求:

    • 状态机调度、Canonical Model、各模式的 Renderable/Editable 实现、光标与输入校验钩子等核心逻辑,统一使用 Vim 8.1+ 兼容的传统 Vimscript 编写,确保在 Vim 和 Neovim 下行为一致。
    • 涉及平台能力差异的部分(高亮 API、异步 job 机制)需要抽象出独立的"平台适配层":在 Vim 下走 prop_add/job_start 等 API,在 Neovim 下走 nvim_buf_set_extmark/jobstart(或 vim.loop)等 API;上层模式实现只依赖适配层暴露的统一接口,不直接感知运行时差异。
    • 是否在 Neovim 环境下额外使用其原生能力做性能优化(如优先用 extmark 而非退化到最基础的兼容实现),可以在适配层内部做 has('nvim') 之类的运行时判断,但对外的功能行为必须保持一致,不能出现"同一功能在 Neovim 下才有、Vim 下缺失"的情况。

2.7 源码目录结构

在前述架构(Canonical Model、Renderable/Editable 接口、模式注册表、平台适配层)落地时,源码目录建议按以下方式组织:

vim-hexedit/
├── plugin/
│   └── hexedit.vim              # 瘦入口:g:配置项默认值、command/autocmd 注册,不含业务逻辑
├── autoload/
│   ├── hexedit.vim              # 顶层调度:HexLoad/HexDump/HGoto 等入口出口命令、按模式名分发的调度函数
│   └── hexedit/
│       ├── model.vim            # Canonical Model:字节存取、字段标注层、窗口元信息/patch写回(对应2.2、第5节)
│       ├── registry.vim         # 模式注册表:模式对象注册/查找(对应2.4节"查表分发")
│       ├── platform.vim         # 平台适配层:统一封装 Vim/Neovim 的高亮与异步 job API(对应2.6节)
│       ├── modes/
│       │   ├── hexedit.vim      # Hex 编辑模式(Renderable+Editable)
│       │   ├── hexstr.vim       # Hex 字串模式(Renderable+Editable,取代现 hexkeep_ui.vim)
│       │   ├── c_view.vim       # C 语言视图模式(Renderable only)
│       │   ├── py_view.vim      # Python 视图模式(Renderable only)
│       │   └── format_parse.vim # 格式解析模式(4.5节,本次仅占位/接口预留,不实现功能)
│       └── parsers/             # 预留:未来格式解析器注册目录,本次为空或仅放接口说明
├── syntax/
│   ├── vhex.vim                 # 沿用
│   └── keephex.vim              # 沿用(Hex 字串模式的语法文件,filetype 名不变)
├── doc/
│   └── hexedit.txt              # 新增,第8节要求的 Vim 帮助文档
├── README.md
└── REQUIREMENTS.md

设计要点:

  • lib/ 目录被 autoload/hexedit/ 的多级子目录取代:现状 lib/*_ui.vim 靠 runtime 手动加载,四个模式一次性全部加载进内存;改成规范的 autoload 嵌套命名空间(autoload/hexedit/modes/xxx.vim 对应 hexedit#modes#xxx#...())后,Vim 原生支持按需懒加载,只有真正切到某个模式时才会加载对应文件,顺带解决第 9 节提到的现状加载方式问题。
  • model.vim/registry.vim/platform.vim 三个文件分别对应第 2 节定的三个架构支柱——中间表示、模式调度、平台差异隔离——避免继续散落在各模式文件里各自为政。
  • modes/format_parse.vim 和 parsers/ 目录本次只是占位,不写具体实现,但目录结构提前留出位置,符合 4.5 节"不能出现实现时才发现要推翻架构"的要求。
  • plugin/hexedit.vim 收缩成纯粹的注册入口:呼应第 9 节提到的"现状里 b: 变量在插件加载时错误计算一次"的技术债,这类初始化逻辑应挪到 autoload 里按 buffer 触发,而不是在插件加载时执行一次。

3. 命令一览与命名变更

新命令 对应旧命令(废弃) 作用
:Hedit :Hexedit 从任意模式切换到 Hex 编辑模式
:Hex :Hexkeep 从任意模式切换到 Hex 字串模式
:H2C :Hex2C 从任意模式切换到 C 语言视图模式(只读)
:H2Py :Hex2Py 从任意模式切换到 Python 视图模式(只读)
:HexLoad (命名不变) 将当前文本形式的 Hex 字串缓冲区转换为二进制内容,并进入 Hex 编辑模式
:HexDump (新增) 将插件状态机内任意模式的内容转换为 Hex 字串文本,并脱离插件状态机,回到 Vim 日常编辑模式
:HGoto {offset} (新增) 大文件窗口化模式下,跳转到文件内任意绝对偏移并重新定位缓存窗口,见第 5 节
:Hsearch / :HsearchClean 旧名 :Hexsearch/:HexsearchClean,已重命名 Hex 编辑模式内的十六进制字符串搜索/清除搜索状态;大文件窗口化模式下的搜索范围需求见 5.7

旧命令 :Hexedit :Hexkeep :Hex2C :Hex2Py 直接废弃移除,不保留兼容别名。

:Hedit :Hex :H2C :H2Py 四个命令要求支持从任意模式直接切换到目标模式,而不仅限于"从 Hex 编辑模式切出/切回"的二元 toggle(例如在 C 视图模式下应能直接执行 :H2Py 切到 Python 视图,无需先回到 Hex 编辑模式)。该需求依赖第 2 节的 Canonical Model 与调度层改造才能以可扩展的方式实现。

需要注意:"任意模式"仅指插件状态机内部的四个模式互相之间,不涉及 Vim 日常编辑模式。七个命令里只有 :HexLoad 和 :HexDump 与 Vim 日常编辑模式直接相关(分别是状态机的唯一入口和唯一出口),其余命令均为状态机内部的状态转换,详见 2.5。

4. 各模式详细需求

4.1 Hex 编辑模式(:Hedit)— Renderable + Editable

  • 视觉:偏移(address)| Hex | ASCII 三栏布局,栏间以固定分隔符展示。
  • 光标限制:
    • 光标永远不能停留在偏移栏;光标移动即将落入偏移栏时,立即定位到本行 Hex 区域最左侧。
    • 光标不能停留在 Hex 栏内字节分组之间的空格,也不能停留在 Hex/ASCII 分隔符上。
    • 光标只能停留在 Hex 区域或 ASCII 区域内的有效字符位置。
  • 编辑行为:进入插入模式后,每次键入仅用于"原地替换"当前位置字符,不做常规文本插入位移:
    • Hex 栏:仅接受十六进制字符 0-9a-fA-F,输入非法字符时不生效/回退。
    • ASCII 栏:接受任意可键入字符,直接替换该位置字节,并同步刷新对应 Hex 栏。
    • 在文件末尾继续键入时,自动追加新的一行/一个字节,保持网格完整。
  • 文件类型:filetype=vhex,配合专属语法高亮。
  • 大文件场景:本模式是大文件窗口化编辑(第 5 节)的主要落地对象,窗口化状态下的光标限制、原地替换行为不变,只是渲染内容变成"当前缓存窗口"而非整文件。

4.2 Hex 字串模式(:Hex)— Renderable + Editable

  • 视觉:仅展示十六进制字符串内容,不含偏移列和 ASCII 列,按分组配置展示,例如:
    4141 4242 ... 4343 4444
    
  • 可编辑:允许直接编辑字符串本体,包括插入、删除字符——这是与 Hex 编辑模式"定长原地替换"的关键区别,本模式允许改变总字节数。
  • 切换校验:从本模式切换到其他模式时,需重新解析并校验合法的十六进制序列(长度必须为偶数、字符必须合法),即 Editable 接口的 ViewToBinary 校验逻辑;校验失败时给出提示,且不执行切换。
  • 文件类型:filetype=keephex,配合专属语法高亮。
  • 大文件场景:本模式允许变长编辑,与大文件窗口化的"定长 patch 写回"假设冲突,具体处理见 5.4。

4.3 C 语言视图模式(:H2C)— Renderable only(只读)

  • 只读:未实现 Editable 接口,任何进入插入模式的尝试应被调度层统一阻止/退回普通模式。
  • 视觉:输出需包含完整的变量声明包裹,例如:
    unsigned char buf[] = {
        0x41, 0x41, 0x42, 0x42, 0x43, 0x43, 0x44, 0x44, // AABBCCDD
    };
    每行数据行尾以 // AABBCCDD 形式的注释展示该行对应的 ASCII 内容。变量名(如 buf)的具体命名规则可在实现阶段确定,可考虑做成配置项。
  • 文件类型:filetype=c,复用 Vim 内置 C 语法高亮。
  • 大文件场景:若从窗口化的 Hex 编辑模式切入,本模式只展示当前缓存窗口对应的字节,见 5.5。

4.4 Python 视图模式(:H2Py)— Renderable only(只读)

  • 只读:未实现 Editable 接口,策略同 C 语言视图模式。
  • 视觉与格式:必须是 Python 3 可直接运行的代码,禁止使用 Python 2 专属的 str.decode('hex')。应改用 bytes.fromhex(...) 或 binascii.unhexlify(...) 等 Python 3 等价写法,例如:
    bytes.fromhex(
        "41 42 43 44"  ### ABCD
    )
    具体的多行拼接样式(单次调用 vs 分段 ### 注释 习惯)在实现阶段确定,需求层面只锁定"生成的代码必须能在 Python 3 环境下直接执行"。
  • 文件类型:filetype=python,复用 Vim 内置 Python 语法高亮。
  • 大文件场景:同 4.3,只展示当前缓存窗口对应的字节。

4.5 格式解析模式(规划中,架构预留,非本次实现范围)

这是未来会加入的第五种模式。本次重构不要求实现其具体功能,但要求第 2 节的接口抽象与 Canonical Model 设计必须能够支撑它落地,不能出现"实现时才发现要推翻现有架构"的情况。

  • 目标:按照特定文件格式(如常见文件头、自定义协议、TLV 结构等)对二进制内容进行结构化解析,得到一组字段(每个字段包含 offset、length、名称、语义说明),对应写入 Canonical Model 的字段标注层(见 2.2)。
  • 视觉:在 Hex 视图或专属视图上,用不同背景色高亮不同字段,颜色与字段语义相关联(例如文件头、长度字段、保留字段各用不同颜色区分),光标停在某字段范围内时可提示该字段的名称/语义。
  • 可编辑性:MVP 阶段定位为 Renderable only(只读展示,用于辅助理解二进制结构)。是否支持"选中某字段直接编辑该字段值"(即字段级 Editable 能力)留作后续迭代;由于采用了 2.3 节的可插拔接口设计,未来为该模式补充 Editable 接口时,不需要改动其他模式或调度层代码。
  • 解析器可插拔:不同文件格式的解析规则应作为独立的"格式解析器"注册到插件中——每种格式一个解析器,输入为 Canonical Model 的原始字节序列,输出为字段标注列表,写回 Canonical Model 的标注层。具体解析器实现和首批支持的文件格式不在本次需求范围内;本次只需确认 Canonical Model 已预留字段标注层,能够承载解析器的输出,且解析器的注册方式不要求修改核心调度逻辑。
  • 大文件场景:字段可能跨越当前缓存窗口边界,本次需求只标注这个边界情况的存在,具体处理方式(如自动扩大缓存窗口以容纳完整字段)留给该模式正式立项时详细设计。

5. 大文件优化需求

5.1 背景与目标

当前实现是"整文件读入 → 整文件 xxd → 整个 buffer"的模式:无论文件多大,都会一次性转换并加载进 Vim buffer。而实际使用中,面对大文件时用户往往只关心其中一小段内容。本节需求的目标是:文件超过一定大小时,Hex 编辑模式自动切换为"窗口化编辑",只加载、渲染、编辑用户当前关注的字节区间,同时保证滚动/跳转体验尽量流畅、保存操作尽量高效。

5.2 触发方式

  • 文件大小超过 g:hexedit_large_file_threshold(配置项,见第 7 节)时,自动以窗口化方式进入 Hex 编辑模式,无需用户额外操作,与第 6 节既有的"二进制模式自动进入 Hex 编辑模式"触发逻辑自然衔接——即在原有触发条件之上,追加"按文件大小判断是否启用窗口化"这一步。
  • 未超过阈值的文件行为不变,仍然整文件加载(即窗口化是大文件的专属行为,不影响现状的小文件体验)。

5.3 窗口模型:可见窗口与缓存窗口

窗口化编辑引入两层窗口概念:

  • 可见窗口(Viewport):当前渲染进 Vim buffer、供用户查看和编辑的字节区间,大小由 g:hexedit_window_size 配置(默认给一个合理值,如 64KB,具体默认值在实现阶段结合 xxd 渲染性能测定)。
  • 缓存窗口(Cache Window):比可见窗口更大的后台预加载区间,作为滑动缓冲,避免每次滚动/跳转都触发一次新的 xxd 调用。缓存窗口应为可见窗口的整数倍。

居中保持策略:插件需要在后台持续保证当前可见窗口/编辑位置落在缓存窗口的 30%–70% 区间(即缓存窗口中间 40% 的"安全区")内。当用户滚动或编辑触及缓存窗口的上边界(低于 30%)或下边界(超过 70%)时,插件以当前所在位置为中心,重新计算并预加载新的缓存窗口(必要时淘汰超出范围的旧缓存内容),使当前内容始终回到新缓存窗口的中心区域。这一滑动过程应尽量对用户无感,不打断正在进行的编辑操作。

对齐要求:无论是初始加载还是居中重定位,缓存窗口的起始偏移都必须按 g:octets_per_line(默认 16,即 0x10)向下取整对齐到文件自身的行边界网格,而不是直接取"目标偏移 - 缓存窗口一半"的原始计算结果。否则同一个文件字节,会因为恰好被哪个窗口加载到而显示在不同的列位置——不符合传统十六进制编辑器/xxd 里"同一偏移永远对应同一列"的预期。对齐后如果窗口末尾超出文件末尾,交给窗口读取逻辑自身的长度裁剪处理(天然会裁到文件末尾,不需要额外补偿)。

5.4 编辑与写回

  • 定长编辑(默认路径):只要缓存窗口内字节总长度不变(Hex 编辑模式的原地替换天然满足这一点),保存时只需把当前缓存窗口对应的原文件字节区间做定长 patch 写回(例如按窗口起始偏移做定位写入),不需要重写整个文件,从而保证大文件保存的效率。
  • 变长编辑(插入/删除字节):窗口内如果发生了字节总长度变化(例如切换到 Hex 字串模式做了插入/删除),插件必须能检测到这种"变长"操作。此时不直接静默执行,而是在保存前明确提示用户:这类编辑会导致窗口后续字节整体错位、无法再安全地只 patch 当前窗口区域,操作将降级为读取并重写整个文件,可能显著影响大文件场景下的读写速度和内存占用;需要用户显式确认后才执行。用户取消确认时,已做的编辑仍保留在 buffer 中,允许用户继续编辑或撤销变长操作。

5.5 只读视图模式与窗口化的关系

C 语言视图、Python 视图等只读模式,如果是从窗口化的 Hex 编辑模式切换过去的,只应展示当前缓存窗口范围内的字节,而不是整个文件;视图中需要有明确标注,提示当前展示的是文件的哪个偏移区间(例如在视图首行标注类似"当前窗口: 0x00001000 - 0x00011000 / 共 0x0A000000"的信息),避免用户误以为看到的是全文件内容。

5.6 导航与跳转

  • 新增命令 :HGoto {offset}:跳转到文件内任意绝对偏移,{offset} 支持十进制或 0x 前缀十六进制写法;跳转后按 5.3 的居中保持策略重新定位缓存窗口,确保跳转目标落在新缓存窗口的中心区域。
  • 普通的上下滚动/翻页由 5.3 的"边界触发滑动加载"策略自动处理,不需要用户额外操作。
  • 跳转前的未保存改动保护::HGoto(以及 5.7 节 Hsearch 命中后的跳转)在大文件窗口化模式下,如果需要重新加载缓存窗口,必须先检查当前缓冲区是否有未保存的改动(&modified);有未保存改动时应拒绝跳转并提示用户先保存,不能自动保存、也不能静默丢弃编辑——这与 5.3 节"自动滑动仅在无未保存改动时才触发"的安全原则保持一致。非窗口化(小文件)场景不受影响,因为跳转不涉及重新加载。
  • G / gg 需要对应整个文件的末尾/开头:Vim 原生的 G(跳转到最后一行)与 gg(跳转到第一行)默认是"buffer 相对"的,而窗口化模式下 buffer 只装着当前缓存窗口的内容——如果不做处理,G/gg 只会跳到"当前已加载窗口"的最后一行/第一行,而不是文件真正的末尾/开头,不符合用户预期。Hex 编辑模式需要在窗口化场景下改写 G/gg 的语义:G 跳转到文件最后一个字节(total_size - 1),gg 跳转到文件第一个字节(偏移 0),两者都按需重新加载缓存窗口——跳转逻辑与 :HGoto 共享(见 5.7 末尾的架构要求),因此也自动享有同一条"未保存改动时拒绝跳转"的安全保护。非窗口化(小文件)场景下 G/gg 保持 Vim 原生行为不变,因为此时 buffer 本身就是整个文件,无需特殊处理。

5.7 :Hsearch 的搜索范围

背景::Hsearch/:HsearchClean(见第 3 节命令表)延续自重构前的插件,原始实现直接在当前 buffer 的全部行上做文本匹配。在小文件(非窗口化)场景下这没有问题,因为 buffer 内容就是整个文件的渲染结果;但在大文件窗口化模式下,buffer 只装着当前缓存窗口的内容,会导致 :Hsearch 事实上只能在"当前窗口"内命中——即使匹配的字节序列实际存在于文件的其他偏移区间,也会被报告为"未找到",这不符合用户对"搜索"的直觉预期。

需求:当搜索目标关联着实际文件时(即 model.path 非空,不区分是否处于窗口化状态,统一处理),:Hsearch 与 n 键重复搜索的范围应扩展为整个文件,而不仅限于当前已加载的窗口。仅当内容没有关联文件时(例如通过 :HexLoad 载入、且从未保存过的纯内存内容,model.path 为空),才退回到当前"buffer 文本直接搜索"的方式,因为此时没有磁盘文件可供检索。

已知限制(本次需求范围内接受,非本次修复目标):文件绑定场景下的搜索基于磁盘上的文件内容,不感知当前窗口内尚未保存的编辑——如果搜索目标恰好落在你刚编辑但还未 :w 的字节范围内,搜索可能找不到(或找到编辑前的旧内容)。避免方式:搜索前先保存。

实现方式(惰性管道,避免整文件预处理):

  • 不做"先把整个文件转换成十六进制文本再搜索"(那样等价于每次搜索都要完整扫描一遍文件),而是从某个起始字节偏移 start_byte 开始,构造惰性 shell 管道:

    tail -c +{start_byte+1} {file} | xxd -p | tr -d '\n' | grep -m1 -b -o -i "{hex_pattern}"
    
    • tail -c +N 从指定字节偏移开始读文件(对常规文件通常用 lseek 定位,起跳不需要真的读完前面的字节)。
    • xxd -p 把字节流转成连续十六进制 ASCII 文本,tr -d '\n' 去掉换行,这样搜索退化为一次纯文本子串查找——彻底避免直接对二进制内容做正则匹配时的可移植性问题(例如 grep -P 依赖 GNU 扩展,BSD grep 不支持)和二进制安全问题(数据中可能嵌入的 \n/\0 字节干扰行匹配)。
    • grep -m1 -b -o 只要第一个匹配就退出并输出该匹配在这段十六进制文本里的字符偏移;命中的文件字节偏移 = start_byte + 字符偏移 / 2。
    • 关键在 -m1 带来的管道短路效应:grep 拿到匹配就退出,不再读取更多输入,上游 tr/xxd/tail 因而提前收到 SIGPIPE 一并停止——如果匹配点离 start_byte 不远,管道几乎瞬间返回,不需要处理到文件末尾;只有匹配确实很靠后或根本不存在时,才接近于扫完整个文件的开销,这是任何无索引全文件搜索理论上都无法避免的下限。
    • 涉及的 tail/xxd/tr/grep 均为标准 Unix 工具的通用选项(-b/-o/-m/-i 在 BSD grep 和 GNU grep 上均可用),不引入新的第三方依赖,与 model.vim 现有的可移植性原则一致。
  • n 重复搜索:将 start_byte 设为"上一次命中结束的字节偏移",重新执行同一条管道,因此每次 n 也只扫描到下一个命中为止,而不是每次都从头重新搜索整个文件。

命中后的跳转:无论是 :Hsearch 首次命中还是 n 重复命中,都需要判断命中偏移是否落在当前已加载的缓存窗口内:

  • 在窗口内:直接把光标移动到对应字节的精确列位置。
  • 不在窗口内(大文件全文件搜索场景下的常态):需要以命中偏移为中心重新加载缓存窗口,再把光标定位过去——这与 :HGoto 在窗口化模式下的收尾动作完全一致。

架构要求:与 :HGoto 共享跳转逻辑::HGoto {offset} 当前实现分两步——解析用户输入的偏移字符串,以及"给定一个已知的数字偏移,按是否窗口化选择跳转方式"。第二步与 Hsearch 命中后需要的逻辑完全相同,应抽取为一个独立的共享函数(跳转函数),供 :HGoto、:Hsearch 首次命中、n 重复命中共同调用,避免同一段"窗口化判断 + 跳转"逻辑写两份;上面 5.6 节提到的"未保存改动保护"也应该实现在这个共享函数里,确保 :HGoto 和 Hsearch 触发的跳转享有同样的安全保证。

6. 核心触发逻辑需求

  1. 二进制模式自动进入:当 Vim 以二进制模式(&binary)打开文件时,自动进入 Hex 编辑模式;若文件大小超过 g:hexedit_large_file_threshold,按第 5 节以窗口化方式进入。
  2. 按后缀自动识别为二进制:当 Vim 尝试打开 .bin / .dat / .hex / .o 后缀的文件时,自动 setlocal binary noeol,进而按第 1 条自然进入 Hex 编辑模式(同样受大文件窗口化规则影响)。
  3. 文本形式的 Hex 字串加载:当 Vim 以文本形式打开一个 Hex 字符串时,执行 :HexLoad 可将文本内容转换为二进制内容,并切换到 Hex 编辑模式进行展示。
    • 写回方式确认(6.3):如果当前 buffer 关联着一个真实文件(即执行 :HexLoad 时 buffer 已有文件名),此时该文件的原始形态是文本(装着 hex 字符串),而 :HexLoad 之后的编辑很自然会想保存回同一个文件——但"保存"具体应该是把编辑结果解码成二进制覆盖原文件,还是仍然以文本(hex 字符串)形式写回,这件事插件不能替用户默认决定,必须显式询问一次:保存时保持文件原样(二进制),还是转化为 HexString 写入?(对应运行时英文提示 "keep the file in its original (binary) form, or convert it to a hex string?",见第 8 节"运行时提示消息使用英文"的约定)。选择结果贯穿这次 :HexLoad 会话(存于 Canonical Model 的 write_mode 字段),决定后续 :w 的行为:
      • 选"保持原样(二进制)":行为与现状一致,按 5.4 节的定长 patch / 变长确认重写逻辑写入原始字节。
      • 选"转化为 HexString"::w 改为把当前字节对应的 Hex 字串文本写回原文件(始终整文件覆盖,不存在"定长 patch"的概念,因为文本没有字节级别的定位语义);大文件窗口化模式下同样只覆盖当前缓存窗口对应的内容,与 :HexDump 的既有限制保持一致。
    • 未关联真实文件(buffer 无文件名)时不存在此歧义,跳过询问。
    • 通过二进制模式/文件后缀自动触发进入 Hex 编辑模式的场景(第 1、2 条)不涉及"文本→二进制"的形态转换,不需要这个确认,write_mode 恒为二进制。
  4. 导出为 Hex 字串并脱离编辑状态:当 Vim 处于插件状态机内的任意模式(不限于 Hex 编辑模式,:Hex/:H2C/:H2Py/未来的格式解析模式同样可以)时,执行 :HexDump 可将当前内容转换为 Hex 字串文本,并切换到 Vim 日常编辑模式:
    • 如果当前模式可编辑(Hex 编辑模式、Hex 字串模式),先走一次该模式自己的 ViewToBinary,把缓冲区里的最新编辑吸收进 Canonical Model,再转成 Hex 字串导出——与切换命令(:Hedit/:Hex/:H2C/:H2Py)离开可编辑模式时的处理方式一致。
    • 如果当前模式只读(C 语言视图、Python 视图等),缓冲区文本不是可信的字节来源,直接使用现有 Canonical Model(未经改动,因为只读模式本来就不允许编辑)导出。
    • 与 :Hex(Hex 字串模式)的区别::Hex 切换后仍由插件状态机管理,可再用 :Hedit/:H2C/:H2Py 直接互切;:HexDump 是终态操作——转换后彻底退出插件状态机、还原按键行为和 filetype,缓冲区表现等同于普通文本文件。
    • :HexDump 之后若需要再次进入二进制编辑,完全依赖用户手动执行 :HexLoad,不提供额外的快捷方式或标记。
    • 大文件窗口化模式下,:HexDump 只能作用于当前缓存窗口的内容(导出整文件对应的 Hex 字串在大文件场景下不现实),需在文档中明确这一限制。

7. 配置项需求

变量 默认值 说明
g:group_octets_num 2 每组显示的字节数
g:octets_per_line 16 每行显示的字节数
g:hexedit_patterns *.bin,*.dat,*.hex,*.o 自动识别为二进制模式的文件名匹配模式
g:hexedit_xxd_options 按上述选项拼接生成 传递给 xxd 的附加参数
g:hexedit_low_up 'lower' 控制 Hex 编辑模式、Hex 字串模式展示的十六进制字母大小写('lower'/'upper'),需真正接入 xxd -u 参数生效
g:hexedit_large_file_threshold 待实现阶段确定合理默认值 触发大文件窗口化编辑的文件大小阈值(字节)
g:hexedit_window_size 待实现阶段确定合理默认值 大文件窗口化模式下,可见窗口(Viewport)的字节数大小
g:hexedit_cache_window_ratio 待实现阶段确定合理默认值 缓存窗口相对可见窗口的倍数,决定后台预加载缓冲区的大小
g:hexedit_highlight_byte_value 0 Hex 编辑模式下,将等于该值的字节单独高亮显示(默认高亮 0x00);设为 -1 表示不启用
g:hexedit_highlight_byte_hlgroup 'NonText' g:hexedit_highlight_byte_value 启用时,目标字节链接到的高亮组

关于目标字节高亮的实现要求:不能用裸文本搜索目标字节的十六进制表示(如直接找子串 "00"),因为在 g:group_octets_num 大于 1 时,同一格子内相邻字节之间没有分隔符(如字节 0xF0 紧跟 0x0A 会渲染成连续的 "f00a"),裸文本搜索会在两个不同字节的交界处产生假匹配。必须按字节格子边界生成匹配规则(对格子内每个字节偏移量单独生成一条锚定到格子起点的匹配规则),确保只有真正等于目标值的单个字节会被高亮,不会跨字节误匹配。

8. 非功能性需求

  • 新增/更新 Vim 帮助文档,覆盖四种模式、七个命令(:Hedit :Hex :H2C :H2Py :HexLoad :HexDump :HGoto)及全部 g: 配置项,使 :help 可查阅插件用法;文档中需说明 Renderable/Editable 接口划分,便于后续贡献者理解如何新增模式;同时需要说明大文件窗口化行为、:HGoto 用法及变长编辑降级提示的含义。
  • 重写 README,补充命令与模式说明表格,明确各模式的可编辑性/只读性,替代当前只记录 :Hsearch 用法的说明;补充大文件场景下的使用建议。
  • 已有语法高亮文件继续沿用;C/Python 视图复用 Vim 内置语法高亮,无需新增语法文件;格式解析模式的字段背景色高亮方案(如基于 matchadd()/prop_type)留待实现阶段设计,本次只需确认第 2 节的字段标注层能提供高亮所需的 offset/length/颜色信息。
  • 运行时提示消息使用英文:插件代码里所有面向用户的运行时提示(echom 输出、confirm() 弹窗文案、以及最终会被回显给用户的错误/状态信息字符串),一律使用英文,不使用中文——即便本 REQUIREMENTS.md 文档本身是中文写就。源码注释、REQUIREMENTS.md、README 的中英文版本等面向贡献者/使用文档的内容不受此限制,可以继续中文/双语。这条约束在文档层面只在此处统一声明,不在每个模式的需求描述里逐条重复。

9. 后续实现阶段需留意的细节(非阻塞性)

  • C 语言视图变量名(如 buf)的具体命名规则/是否可配置,留待实现阶段确定。
  • Python 视图的具体多行拼接样式,留待实现阶段确定,只需满足"Python 3 可直接运行"的硬性要求。
  • Canonical Model、Renderable/Editable 接口、模式注册表的具体实现方式(沿用现状的字典模拟类,还是引入其他组织方式),需在 2.6 节"传统 Vimscript、不使用 Vim9script"的约束下由实现阶段确定,本文档只锁定接口职责划分,不锁定具体代码组织细节。
  • 格式解析模式的首批文件格式支持范围、具体解析器实现、字段高亮的具体交互方式(悬浮提示 vs 状态栏展示等)留待该模式正式立项时另行梳理需求;其背景色高亮能力依赖 2.6 节规划的平台适配层(Vim 文本属性 / Neovim extmark)。
  • g:hexedit_large_file_threshold/g:hexedit_window_size/g:hexedit_cache_window_ratio 的具体默认值,需要在实现阶段结合 xxd/dd 的实际性能测定后给出,本文档只锁定这几个配置项的存在和用途。
  • 缓存窗口的后台预加载/滑动,依据 2.6 节的平台适配层设计,在 Vim 下用 job_start、在 Neovim 下用 jobstart/vim.loop 实现异步"无感"加载;若某一端异步能力受限,也可以退化为同步阻塞式重新加载(体验稍差但实现简单),具体取舍留待实现阶段验证。
  • 定长 patch 写回的具体系统调用方式(如 dd conv=notrunc 定位写入,或调用一段小的 Python/Perl 脚本做随机访问写入)留待实现阶段选型。
  • 核心逻辑面向 Vim 8.1+ 与 Neovim 的最低版本号要求,留待实现阶段结合目标高亮/异步 API 的实际最低支持版本确定。
  • 实现阶段应一并评估并清理现有代码中的技术债,包括但不限于:
    • plugin/hexedit.vim 中部分布局参数使用 b: 缓冲区变量却在插件加载时(而非每个 buffer 打开时)计算一次,存在作用域错误风险。
    • 未被调用的调试代码,以及绑定了 autocmd 事件但各模式均未实现对应处理方法的空转逻辑。