33.33 原子变更组

在数据库术语中,原子(atomic) 变更是不可分割的变更—要么完全成功,要么完全失败, 不会出现部分成功的情况。Lisp 程序可以将对一个或多个缓冲区的一系列变更, 作为一个 原子变更组(atomic change group) 执行,即要么整套变更全部应用到缓冲区, 要么在出错时全部不生效。

若只对当前缓冲区执行此操作,只需在执行变更的代码外层包裹 atomic-change-group 调用即可,示例如下:

(atomic-change-group
  (insert foo)
  (delete-region x y))

如果在 atomic-change-group 函数体内部发生错误(或其他非本地退出), 该函数会撤销函数体执行期间对该缓冲区所做的所有变更。 此类变更组对其他缓冲区无影响—这些缓冲区的变更会保留。

若需要更复杂的操作,例如让多个缓冲区的变更属于同一个原子组, 则需要直接调用 atomic-change-group 所使用的底层函数。

Function: prepare-change-group &optional buffer

该函数为缓冲区 buffer 创建一个变更组,buffer 默认为当前缓冲区。 它返回一个代表该变更组的句柄。你必须使用此句柄来激活变更组,并在之后完成它。

要使用变更组,必须先对其进行 激活(activate),且必须在修改 buffer 文本之前完成激活。

Function: activate-change-group handle

该函数激活由句柄 handle 标识的变更组。

激活变更组后,你对该缓冲区所做的任何修改都会归入该组。 在缓冲区中完成所有期望的修改后,必须 完成(finish) 该变更组。 有两种方式:接受(并最终确定)所有变更,或全部取消。

Function: accept-change-group handle

该函数接受由句柄 handle 指定的变更组中的所有变更,并使其最终生效。

Function: cancel-change-group handle

该函数取消并撤销由句柄 handle 指定的变更组中的所有变更。

你可以使用 undo-amalgamate-change-group, 将变更组中的部分或全部变更合并为 undo 命令的单个操作单元(see 撤销)。

Function: undo-amalgamate-change-group

将自 handle 标识状态以来,变更组中所做的所有变更合并。 该函数会移除 handle 所描述状态之后所有变更的撤销记录之间的撤销边界。 通常 handleprepare-change-group 返回的句柄, 此时变更组开始以来的所有变更会被合并为单个撤销单元。

代码中应使用 unwind-protect 确保变更组始终被正确完成。 activate-change-group 调用应放在 unwind-protect 内部, 防止用户在其执行后立即按下 C-g。 (这也是 prepare-change-groupactivate-change-group 分离为两个函数的原因之一,因为通常你会在 unwind-protect 开始前 调用 prepare-change-group。) 变更组完成后,请勿再次使用该句柄——尤其不要尝试两次完成同一个组。

要创建多缓冲区变更组,需对每个目标缓冲区分别调用一次 prepare-change-group, 然后使用 nconc 合并返回值,示例如下:

(nconc (prepare-change-group buffer-1)
       (prepare-change-group buffer-2))

之后只需调用一次 activate-change-group 即可激活多缓冲区变更组, 并通过单次调用 accept-change-groupcancel-change-group 完成操作。

对同一缓冲区嵌套使用多个变更组可按预期正常工作。 对同一缓冲区非嵌套地使用变更组会使 Emacs 出现混乱,因此应避免; 对任一缓冲区启动的第一个变更组,应是最后一个被完成的。

Emacs 跟踪变更组的方式是:沿着 buffer-undo-list 逐个遍历 cdr, 最终应能找到调用 prepare-change-group 时设置的 cons 单元。

如果 buffer-undo-list 中不再包含该 cons 单元, Emacs 会丢失对变更组的跟踪,导致取消变更组时报错。 为避免此问题,在变更组激活期间,不要调用任何可能修改撤销列表的函数, 尤其是像 delete-char 这类会调用 undo-auto-amalgamate 的 “合并(amalgamating)” 类命令。