4.4.3 append-to-buffer 中的 save-excursion

append-to-bufferlet 表达式的函数体由一个 save-excursion 表达式构成。

save-excursion 函数会保存光标位置,并在其函数体内的所有表达式执行完毕后,将光标恢复到该位置。此外,save-excursion 还会记录原始缓冲区并在最后恢复。这就是 save-excursionappend-to-buffer 中的使用方式。

顺便一提,这里值得注意:Lisp 函数通常会按照一定格式排版,多行结构内的所有内容都会比所在块的首个符号向右缩进更多。在本函数定义中,let 的缩进比 defun 更深,save-excursion 的缩进又比 let 更深,如下所示:

(defun ...
  ...
  ...
  (let...
    (save-excursion
      ...

这种排版约定让我们可以清晰地看出,save-excursion 函数体内的代码被其自身括号包裹,正如 save-excursion 本身被 let 的括号包裹一样:

(let ((oldbuf (current-buffer)))
  (save-excursion
    ...
    (set-buffer ...)
    (insert-buffer-substring oldbuf start end)
    ...))

save-excursion 函数的使用可以看作是填充模板的过程:

(save-excursion
  first-expression-in-body
  second-expression-in-body
   ...
  last-expression-in-body)

在本函数中,save-excursion 的函数体只包含一个表达式,即 let* 表达式。你已经了解 let 函数,而 let* 函数有所不同。它允许 Emacs 按顺序依次设置变量列表中的每个变量,使得变量列表后半部分的变量可以使用前面已设置变量的值。

来看 append-to-buffer 中的 let* 表达式:

(let* ((append-to (get-buffer-create buffer))
       (windows (get-buffer-window-list append-to t t))
       point)
  BODY...)

可以看到 append-to 被绑定到 (get-buffer-create buffer) 的返回值。下一行中,append-to 被用作 get-buffer-window-list 的参数;如果使用普通 let 表达式,这是无法实现的。注意 point 会自动绑定到 nil,与 let 语句中的行为一致。

现在我们重点关注 let* 表达式函数体内的 set-bufferinsert-buffer-substring 函数。

在早期版本中,set-buffer 表达式的写法很简单:

(set-buffer (get-buffer-create buffer))

而现在的写法是:

(set-buffer append-to)

原因是 append-to 已经在 let* 表达式前面绑定为 (get-buffer-create buffer)

append-to-buffer 函数的作用是将当前缓冲区的文本插入到指定缓冲区。而 insert-buffer-substring 的作用恰好相反——它从另一个缓冲区复制文本到当前缓冲区。这就是为什么 append-to-buffer 定义开头使用 let,将局部符号 oldbuf 绑定到 current-buffer 的返回值。

insert-buffer-substring 表达式如下:

(insert-buffer-substring oldbuf start end)

insert-buffer-substring 函数从第一个参数指定的缓冲区复制字符串,并将其插入到当前缓冲区。本例中,该函数的参数是 let 创建并绑定的变量 oldbuf,即执行 append-to-buffer 命令时所在的原始缓冲区。

insert-buffer-substring 完成工作后,save-excursion 会将操作恢复到原始缓冲区,append-to-buffer 也就完成了任务。

用框架形式表示,函数体的执行逻辑如下:

(let (bind-oldbuf-to-value-of-current-buffer)
  (save-excursion                       ; Keep track of buffer.
    change-buffer
    insert-substring-from-oldbuf-into-buffer)

  change-back-to-original-buffer-when-finished
let-the-local-meaning-of-oldbuf-disappear-when-finished

总而言之,append-to-buffer 的工作流程如下:将当前缓冲区保存到变量 oldbuf;获取目标缓冲区(不存在则创建)并切换 Emacs 当前操作缓冲区;使用 oldbuf 将原始缓冲区的区域文本插入到新缓冲区;最后通过 save-excursion 回到原始缓冲区。

通过分析 append-to-buffer,你已经了解了一个相当复杂的函数。它展示了 letsave-excursion 的用法,以及如何切换并返回其他缓冲区。许多函数定义都以类似方式使用 letsave-excursionset-buffer