如何防止Emacs设置撤销边界?

9
我编写了一个 Emacs Lisp 函数,它调用一个 shell 命令来处理给定的字符串,并返回结果字符串。这里是一个简化的示例,它只调用 tr 将文本转换为大写:
(defun test-shell-command (str)
  "Apply tr to STR to convert lowercase letters to uppercase."
  (let ((buffer (generate-new-buffer "*temp*")))
    (with-current-buffer buffer
      (insert str)
      (call-process-region (point-min) (point-max) "tr" t t nil "'a-z'" "'A-Z'")
      (buffer-string))))

该函数创建一个临时缓冲区,插入文本,调用tr,替换文本为结果,并返回结果。
上述函数按预期工作,但是当我编写一个包装器来将命令应用于区域时,会向撤消历史记录添加两个步骤。以下是另一个示例:
(defun test-shell-command-region (begin end)
  "Apply tr to region from BEGIN to END."
  (interactive "*r")
  (insert (test-shell-command (delete-and-extract-region begin end))))

当我调用M-x test-shell-command-on-region时,区域内的文本将被替换为大写文本,但是当我按下C-_undo)时,撤销历史记录中的第一步是删除文本的状态。向后两步,原始文本将被恢复。
我的问题是,如何防止将中间步骤添加到撤消历史记录中? 我已经阅读了 Emacs documentation on undo, 但据我所知,它似乎没有解决这个问题。
以下是一个函数,通过调用内置的Emacs函数upcase来完成相同的操作:使用delete-and-extract-region的结果,并将结果传递给insert
(defun test-upcase-region (begin end)
  "Apply upcase to region from BEGIN to END."
  (interactive "*r")
  (insert (upcase (delete-and-extract-region begin end))))

当调用M-x test-upcase-region时,撤销历史记录中只有一个步骤,这是预期的。因此,似乎调用test-shell-command会创建一个撤销边界。是否可以以某种方式避免这种情况?


防止命令混乱 (undo) 的常规方法是找到另一种不会这样做的方式。这里的例子是,你几乎永远不应该创建临时缓冲区,而是应该使用对象进行操作。 - PascalVKooten
除了手动读写临时文件之外,我不确定是否还有其他捕获进程输出的方法。即使是异步进程命令(如start-process),似乎也希望将输出发送到缓冲区。 - Jason Blevins
1
@JasonBlevins shell-command-on-region 的第五个参数是 REPLACE,那你为什么需要将其包装起来呢? - event_jr
@event_jr 很好的观点:我这样写是为了遵循“不要重复自己”的原则,以便有一个返回字符串的函数和另一个操作区域的函数,但不重复程序名称和参数。当然,避免重复自己的方法有多种,你的评论指出了其他良好的结构方式。 - Jason Blevins
3个回答

7
关键是缓冲区名称。请参见维护撤销
“在新创建的缓冲区中记录撤销信息通常是启用的;但是,如果缓冲区名称以空格开头,则最初禁用撤销记录。您可以使用以下两个函数来明确启用或禁用撤销记录,或者通过自己设置buffer-undo-list 来启用或禁用撤销记录。” with-temp-buffer创建一个名为␣*temp*的缓冲区(注意前导空格),而您的函数使用*temp*
要在代码中删除撤销边界,请使用带有前导空格的缓冲区名称或使用buffer-disable-undo明确禁用临时缓冲区中的撤销记录。
但通常情况下,请使用with-temp-buffer方法。这是Emacs中此类事情的标准方式,使任何阅读您代码的人都可以清楚地了解您的意图。此外,with-temp-buffer会尽力正确清除临时缓冲区。
至于为什么在临时缓冲区中的撤销会在当前缓冲区中创建撤销边界:如果上一个更改是可撤销的并且在其他缓冲区中进行了更改(在这种情况下为临时缓冲区),则会创建一个隐式边界。来自undo-boundary
“所有缓冲区修改都会在上一个可撤销更改在另一个缓冲区中进行时添加边界。这是为了确保每个命令在它所做更改的每个缓冲区中都会产生边界。”
因此,抑制临时缓冲区中的撤销将同时删除当前缓冲区中的撤销边界:以前的更改现在不可撤销,因此不会创建隐式边界。

1
我验证了在原始函数中使用(generate-new-buffer " *temp*")是有效的,并且with-temp-buffer确实创建了一个以空格开头的缓冲区。我阅读了文档的那一部分,但我认为这不是问题所在,因为撤消信息是针对每个缓冲区特定的。我为什么要关心是否在*temp*缓冲区记录撤消信息呢?但是我从文档中没有获取到的是,显然,在其他缓冲区记录撤消信息的行为必须在当前缓冲区设置一个撤消边界。 - Jason Blevins
@JasonBlevins 如果之前的更改是可撤销的并且在其他缓冲区中进行的,那么就会创建一个隐式边界,在您的情况下是临时缓冲区。我已经编辑了我的答案以解释细节。希望现在一切都清楚了。 - user355252

4

有许多情况不适合使用临时缓冲区,例如很难调试问题。

在这些情况下,您可以使用 let-bind undo-inhibit-record-point 来阻止 Emacs 决定放置边界:

(let ((undo-inhibit-record-point t))
  ;; Then record boundaries manually
  (undo-boundary)
  (do-lots-of-stuff)
  (undo-boundary))

2
在这种情况下的解决方案是使用with-temp-buffer创建临时输出缓冲区,而不是使用generate-new-buffer显式地创建一个。以下第一个函数的替代版本不会创建撤销边界:
(defun test-shell-command (str)
  "Apply tr to STR to convert lowercase letters to uppercase."
  (with-temp-buffer
    (insert str)
    (call-process-region (point-min) (point-max) "tr" t t nil "'a-z'" "'A-Z'")
    (buffer-string)))

我无法确定generate-new-buffer是否确实创建了撤销边界,但这个修复方法解决了问题。 generate-new-buffer 调用了 C 源代码中定义的 get-buffer-create,但我无法快速确定在撤销历史记录方面发生了什么。
我怀疑问题可能与Emacs Lisp 手册条目中关于undo-boundary的以下内容有关:

所有缓冲区修改都会在前一个可撤消更改是在其他缓冲区中进行时添加边界。这是为了确保每个命令在它进行更改的每个缓冲区中都会产生边界。

即使with-temp-buffer 宏像原始函数一样调用了generate-new-buffer,但with-temp-buffer的文档说明未保存任何撤销信息(尽管 Emacs Lisp 源代码中没有任何暗示会出现这种情况的内容):

默认情况下,此宏创建的缓冲区不记录撤销(参见 Undo)。但可以在 body 中启用该功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接