undo

undo.txt 适用于 Vim 9.2 版本。 最近更新: 2025年10月 VIM 参考手册 by Bram Moolenaar 译者: jwdu、tocer 撤销和重做 undo-redo 基础用法请参考用户手册 02.5 一节。 1. 撤销和重做命令 undo-commands 2. 两种撤销模式 undo-two-ways 3. 撤销块 undo-blocks 4. 撤销分支 undo-branches 5. 持久化撤销 undo-persistence 6. 撤销相关备注 undo-remarks

1. 撤销和重做命令 undo-commands

<Undo> 或 undo <Undo> u u 撤销 [count] 次更改。 :u :un :undo :u[ndo] 撤销一次更改。 E830 :u[ndo] {N} 跳转到第 {N} 号更改之后的状态。{N} 的含义见 undo-branches 。 CTRL-R CTRL-R 重做 [count] 次已撤销的更改。 :red :redo redo :red[o] 重做一次已撤销的更改。 U U 在最近发生更改的单行上,撤销最近全部更改。 U 命令自身 也算一次更改,因此可用 U 撤销上一次 U 。 Vim 会记录最近的更改,可用上述撤销和重做命令将文件恢复到历次更改之前的状态。也 可重新应用更改,前进到执行撤销前的文本状态。 U 命令在撤销/重做体系中来和普通命令同等对待。所以 u 可以撤销 U 命令、 CTRL-R 也可以重做对 U 命令的撤销。混用 U 、 u 和 CTRL-R 三条命令时, 会发现 U 命令会将一行恢复到上一次执行 U 命令之前的状态,这一点容易令人困 惑。多练习几次就会习惯。 U 命令总会将缓冲区标记为已修改。即使它将已修改缓冲区的内容恢复至未修改时的状 态,缓冲区仍被视为已修改。改用多次 u 命令,才能使缓冲区恢复为未修改状态。

2. 两种撤销模式 undo-two-ways

撤销和重做的工作模式依赖于 'cpoptions' 中 'u' 标志位 ( cpo-u )。有两种模式, Vim 模式 (不含 'u') 和 Vi-兼容模式 (包含 'u')。在 Vim 模式下, uu 命令撤销两 次更改,在 Vi-兼容模式下, uu 命令保持原状 (第二次撤销第一次)。 不含 'u' 的 Vim 模式: 可用撤销命令回到先前的状态。也可用重做命令向前恢复。撤销后又进行新更改时,则无 法再重做。 包含 'u' 的 Vi-兼容模式: 撤销命令会撤销上次更改,也会撤销上次撤销。重做命令则重复上次撤销命令。但 不 会 重复更改。为此可用 . 命令。 (译者注: 可理解为,默认方向为恢复, u 每次都会反转方向并移动一步 (撤销/恢复交 替), CTRL-R 则沿当前方向继续一步。新更改会复位方向)。 举例 Vim 模式 Vi-兼容模式 "uu" 两次撤销 空操作 "u CTRL-R" 空操作 两次撤销 原理: Nvi 使用 "." 命令代替 CTRL-R。遗憾的是,这与 Vi 不兼容。例如,在 Vi 中 执行 dwdwu. 会删除两个单词,而在 Nvi 中则为空操作。

3. 撤销块 undo-blocks

单次撤销命令通常撤销一次键入命令产生的全部更改。这一组可撤销更改序列构成一个撤 销块。例如,如果键入命令调用函数,则该函数调用的所有命令可被一次性整体撤销。 要编写函数或脚本,不新建独立可撤销更改,而是将本次更改合并到上一个撤销块,可用 以下命令: :undoj :undojoin E790 :undoj[oin] 将后续更改并入前一个撤销块。 警告: 谨慎使用。该命令可能导致用户无法正常撤销更改。撤 销或重做之后不可使用。 适合在更改过程中需要提示用户输入的场合。例如会调用 getchar() 的函数。务必确 保此前存在需要合并的相关更改。 本命令无法单独工作,因为下一次按键会开启新更改。但可如此处理: :undojoin | delete 执行后, u 命令会同时撤销 :delete 命令以及上一次更改。 undo-break undo-close-block 与之相反,要使后续更改新建独立撤销块,可在插入模式下按 CTRL-G u。适合将插入操 作拆分为多段可撤销单元。例如可按句子拆分。 i_CTRL-G_u 修改 'undolevels' 值也会结束当前撤销块。即使新值和原值相同,也会如此。可显式使 用 g:undolevels 读写 'undolevels' 选项的全局值。 Vim9 脚本用法: &g:undolevels = &g:undolevels 老式脚本用法: >vim let &g:undolevels = &g:undolevels 注意外观相似的赋值语句 `let &undolevels=&undolevels`,在局部选项值与全局值不一 致时,无法保留其全局值。例如: " 初始 'undolevels' 全局和局部值不同。 let &g:undolevels = 1000 let &l:undolevels = 2000 " 以下赋值语句会将全局值改为 2000: let &undolevels = &undolevels

4. 撤销分支 undo-branches undo-tree

以上仅讨论了单线的撤销/重做历史。Vim 支持撤销分支。撤销若干更改后,另起一个新 更改,此时,已撤销的更改历史就会形成分支。下述命令可切换到该分支。 用户手册 usr_32.txt 有详细说明。 :undol :undolist :undol[ist] 列出撤销树的所有叶节点。示例: 编号 变更 时间 保存 88 88 2010/01/04 14:25:53 108 107 08/07 12:47:51 136 46 13:33:01 7 166 164 3 秒前 "编号" 列为更改号。该编号持续递增,用于定位相应的可 撤销更改,参见 :undo 。 "变更" 列为从撤销树的根节点到该叶节点所需的更改数量。 "时间" 列为更改发生的时间。共四种格式: N 秒前 HH:MM:SS 时、分、秒 MM/DD HH:MM:SS 月、日、时、分、秒 YYYY/MM/DD HH:MM:SS 年、月、日、时、分、秒 "保存" 列标记更改是否已写入磁盘,并记录是第几次写入文 件。可搭配 :later 和 :earlier 命令使用。 要查看更多信息,可用 undotree() 函数。 g- g- 切换到较早的文本状态。给出 [count] 时,重复执行指定次 数。 :ea :earlier :ea[rlier] {count} 切换到 {count} 次较早的文本状态。 :ea[rlier] {N}s 切换到约 {N} 秒前的文本状态。 :ea[rlier] {N}m 切换到约 {N} 分钟前的文本状态。 :ea[rlier] {N}h 切换到约 {N} 小时前的文本状态。 :ea[rlier] {N}d 切换到约 {N} 天前的文本状态。 :ea[rlier] {N}f 切换到 {N} 次较早文件写入时的文本状态。 如果上次文件写入后有更改,":earlier 1f" 会恢复上次写入 时的文本。无更改时则会切换到再上一次写入时的文本。 处于首次文件写入时的文本状态、或文件从未写入时, ":earlier 1f" 会退回到首次更改前最早的文本状态。 g+ g+ 切换到较新的文本状态。给出 [count] 时,重复执行指定次 数。 :lat :later :lat[er] {count} 切换到 {count} 次较新的文本状态。 :lat[er] {N}s 切换到约 {N} 秒后的文本状态。 :lat[er] {N}m 切换到约 {N} 分钟后的文本状态。 :lat[er] {N}h 切换到约 {N} 小时后的文本状态。 :lat[er] {N}d 切换到约 {N} 天后的文本状态。 :lat[er] {N}f 切换到 {N} 次较新的文件写入时的文本状态。 处于最后一次文件写入时的文本状态时,":lat[er] 1f" 会切 换到最新的文本状态。 注意 如果由于 'undolevels' 选项导致撤销信息被清空,历史文本状态将无法再访问。 在不同时间状态之间切换时,一次发生多个更改属于正常现象。通过撤销树跳转、然后又 进行了新更改后就会发生这种情况。 示 例 初始文本: one two three 按三次 "x" 删除第一个单词: ne two three e two three two three 按三次 "u" 撤销: e two three ne two three one two three 按三次 "x" 删除第二个单词: one wo three one o three one three 按三次 "g-" 撤销: one o three one wo three two three 现在回到了第一个撤销分支,也就是在删除 "one" 之后的状态。继续按 "g-",可回到原 始文本: e two three ne two three one two three 使用 ":later 1h" 跳转到最后一次更改: one three 使用 ":earlier 1h" 再次回到初始状态: one two three 注意 使用 "u" 和 CTRL-R 无法访问全部可能的文本状态,但重复 "g-" 和 "g+" 可以。

5. 持久化撤销 undo-persistence persistent-undo

卸载缓冲区时,Vim 默认会销毁该缓冲区创建的撤销树。开启 'undofile' 选项后,Vim 会在写入文件时自动保存撤销历史,重新编辑文件时恢复撤销历史。 'undofile' 选项在文件写入后,BufWritePost 事件触发前检查。要控制哪些文件需要保 存撤销信息,可用 BufWritePre 自动命令: au BufWritePre /tmp/* setlocal noundofile Vim 将撤销树单独保存在独立的撤销文件里,每个编辑文件对应一份,通过简单文件路径 映射方案决定撤销文件名。Vim 会自动检测撤销文件与文件在保存时的状态是否保持同步 (通过文件内容哈希校验),如果原文件在生成撤销文件后发生更改,则忽略该撤销文件, 防止数据损坏。如果撤销文件所有者与原文件不一致,除非撤销文件的所有者是当前用 户,同样忽略该撤销文件。通过调整 'verbose' 值,可在打开文件时看到相关提示。 撤销文件默认保存在原文件同目录下,但可用 'undodir' 选项改变。 原文件启用加密时,撤销文件内的文本也会加密。并使用相同密钥与加密算法。 encryption 备注 撤销文件不会保存文本属性。缓冲区处于加载状态时,文本属性可恢复,但无法通 过撤销文件恢复。理据: 恢复文本属性要求对应的属性类型定义与保存前完全一致,这点 无法保证。 也可通过 :wundo" 和 ":rundo" 命令来手动保存及恢复撤销历史: :wu :wundo :wu[ndo][!] {file} 将撤销历史写入 {file}。 如果 {file} 已存在且并非撤销文件 (文件头部魔术数不符),命令失 败。但可用 ! 强制写入。 文件存在且是合法撤销文件时,会进行覆盖。无撤销历史时,则不写入 任何内容。 实现细节: 覆盖时先删除原有文件,再新建同名文件。因此无法在写保 护 (只读) 目录下覆盖已有撤销文件。 :rund :rundo :rund[o] {file} 从 {file} 读取撤销历史。 可在自动命令中使用这两条命令,手动指定撤销历史文件名。例如: au BufReadPost * call ReadUndo() au BufWritePost * call WriteUndo() func ReadUndo() if filereadable(expand('%:h') .. '/UNDO/' .. expand('%:t')) rundo %:h/UNDO/%:t endif endfunc func WriteUndo() let dirname = expand('%:h') .. '/UNDO' if !isdirectory(dirname) call mkdir(dirname) endif wundo %:h/UNDO/%:t endfunc 使用这些命令时应关闭 'undofile',否则每次保存文件都会生成两份撤销文件。 可用 undofile() 函数获取 Vim 缺省使用的撤销文件名。 注意: 开启 'undofile' 时,读写撤销文件时大部分的错误信息会被屏蔽,除非设置了 'verbose'。而 :wundo 和 :rundo 会输出更多错误信息,例如文件读写失败等。 注意: Vim 从不自动删除撤销文件。需要手动清理。 读取已有撤销文件失败的可能原因: E822 无法打开,文件权限不许可。 E823 文件头魔术数不符。通常意味着不是合法撤销文件。 E824 撤销文件版本号指示由更高版本 Vim 生成。需要用对应新版本打开。要保留该 文件中的撤销信息,请勿保存缓冲区。 "文件内容已变动,不能使用撤销信息" 文件文本和生成撤销文件时不一致。不能加载撤销信息,否则会损坏文本。这也 可能是因为 'encoding' 值和生成撤销文件时不同。 E825 撤销文件内容无效,不可使用。 E826 撤销文件已加密,但解密失败。 E827 撤销文件已加密,但当前 Vim 版本不支持加密。换其他版本 Vim 打开。 E832 撤销文件经已加密但未设置 'key',原文本文件本身未加密。一般原因是原文本 文件曾经加密保存,后来又被覆盖为未加密内容。建议删除此撤销文件。 "不读取撤销文件,所有者不同" 撤销文件所有者和文本文件所有者不一致。出于安全考虑,不使用该撤销文件。 写入撤销文件失败的可能原因: E828 无法创建目标文件。可能目录无写入权限。 "无法在 'undodir' 中的任何目录中写入撤销文件" 'undodir' 内所有目录均不可用。 "无法读取文件,不会用撤销文件覆盖之" 目标撤销文件已存在,但无法读取。可尝试删除或重命令该文件。 "不会覆盖,这不是撤销文件" 目标撤销文件已存在,但头部魔术数不符,不是合法撤销文件。可尝试删除或重 命名该文件。 "跳过写入撤销文件,没什么可撤销的" 没有可撤销的更改或 'undolevels' 为负,此时跳过撤销文件的写入。 E829 写入撤销文件时出错。可以重试。

6. 撤销相关备注 undo-remarks

可记忆的更改次数由 'undolevels' 选项决定。设为零时,总是采用 Vi-兼容撤销模式。 设为负数时,禁用撤销。内存不足时可使用该设置。 clear-undo 设置 'undolevels' 为 -1 时,不会立即消除撤销信息,要等到下一次文本更改时才会清 空。要强制清空撤销历史,可用以下代码: >vim :let old_undolevels = &l:undolevels :setlocal undolevels=-1 :exe "normal a \<BS>\<Esc>" :let &l:undolevels = old_undolevels :unlet old_undolevels 注意 使用 &l:undolevels 显式计取 'undolevels' 局部选项值,使用 :setlocal 仅修改局部选项值 (局部值优先于相应的全局值)。直接用 &undolevels 保存选项值的 行为不可预测;有时读取局部值 (有局部值时),有时读取全局值 (无局部值时)。另外, 如果已经设置过局部值,使用 `:set undolevels` 修改选项值会同时发动全局和局部 值,保存与恢复两者会更加麻烦。 缓冲区位置标记 ('a 至 'z) 会随文本一同保存与恢复。 当所有更改被全部撤销后,缓冲区会被标记为未修改。此时可用 :q 正常退出 Vim, 无需 :q! 。注意 未修改状态是相对于上一次文件写入状态而言。执行 :w 后再按 u ,缓冲区内容相对已保存文件发生了变化。因此,缓冲区此时会被标记为已修改。 使用手动折叠 folding 时,折叠信息不会被随撤销操作保存与恢复。只有更改完全发 生在折叠内部,不改变折叠首尾行时。才不会影响该折叠。 编号寄存器也可用于恢复删除文本。每次删除的文本都会存入 "1 寄存器。原 "1 寄存器 的内容会移到 "2 寄存器,依次类推。"9 寄存器的原有内容会丢失。现在可通过命令 "1P 恢复最近删除的文本。(如果删除文本来自最近的删除或复制操作,直接 P 或 p 也可以,因为它们会放置无名寄存器的内容)。而使用 "3P 可恢复三次删除操作之 前的删除文本。 redo-register 要恢复多段已删除文本,可用重复命令 . 的特殊特性: 它会递增所用寄存器的编号。 例如,先执行 "1P ,之后按 . 会执行 "2P 。反复执行 . 可依次插入所有编号寄 存器的内容。 示例: 用 dd.... 删除五行文本后,可用 "1P.... 依次恢复。 如果不知道删除文本现在存放在几号寄存器,可用 :display 命令查看。另一种方法 是先用 "1P 尝试一号寄存器,如果不对,执行 u. 命令。撤销本次放置,并使用二 号寄存器重复放置命令。反复执行 u. 直到执行目标文本为止。 vim:tw=78:ts=8:noet:ft=help:norl: