kill-new 函数

在 22 版本中,kill-new 函数的定义如下:

(defun kill-new (string &optional replace yank-handler)
  "将 STRING 设为删除环中最新的删除内容。
设置 `kill-ring-yank-pointer' 指向该内容。

若 `interprogram-cut-function' 非 nil,将其应用于 STRING。
可选第二个参数 REPLACE 非 nil 表示 STRING 会替换删除环的头部,
而非添加到列表中。
..."
  (if (> (length string) 0)
      (if yank-handler
          (put-text-property 0 (length string)
                             'yank-handler yank-handler string))
    (if yank-handler
        (signal 'args-out-of-range
                (list string "yank-handler specified for empty string"))))
  (if (fboundp 'menu-bar-update-yank-menu)
      (menu-bar-update-yank-menu string (and replace (car kill-ring))))
  (if (and replace kill-ring)
      (setcar kill-ring string)
    (push string kill-ring)
    (if (> (length kill-ring) kill-ring-max)
        (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))
  (setq kill-ring-yank-pointer kill-ring)
  (if interprogram-cut-function
      (funcall interprogram-cut-function string (not replace))))

(注意该函数并非交互式函数。)

和往常一样,我们可以分段查看这个函数。

函数定义中包含可选参数 yank-handler, 调用时该参数用于告知函数如何处理 文本附加的属性,例如粗体或斜体。这部分我们略过不讲。

文档字符串的第一行含义清晰:

将 STRING 设为删除环中最新的删除内容。

我们暂时跳过文档字符串的其余部分。

同时,我们也先跳过开头的 if 表达式 以及涉及 menu-bar-update-yank-menu 的代码行。 我们会在下方进行说明。

关键代码行如下:

  (if (and replace kill-ring)
      ;; then
      (setcar kill-ring string)
    ;; else
    (push string kill-ring)
    (if (> (length kill-ring) kill-ring-max)
        ;; avoid overly long kill ring
        (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))
  (setq kill-ring-yank-pointer kill-ring)
  (if interprogram-cut-function
      (funcall interprogram-cut-function string (not replace))))

条件判断为 (and replace kill-ring)。 当两个条件同时满足时结果为真:删除环不为空, 且 replace 变量为真。

kill-append 函数将 replace 设为真, 且删除环中至少有一项内容时,会执行 setcar 表达式:

(setcar kill-ring string)

setcar 函数会将 kill-ring 列表的 第一个元素实际修改为 string 的值,直接替换第一个元素。

反之,如果删除环为空,或者 replace 为假, 则执行条件的 else 分支:

(push string kill-ring)

push 将第一个参数添加到第二个参数的头部。 它与较早的写法类似:

(setq kill-ring (cons string kill-ring))

也与较新的写法类似:

(add-to-list kill-ring string)

条件为假时,表达式首先会将 string 作为新元素添加到现有删除环头部,构造一个新的删除环 (这就是 push 的作用)。 随后执行第二个 if 语句, 用于防止删除环变得过长。

我们按顺序查看这两个表达式。

else 分支中的 push 语句 将待删除字符串添加到旧删除环中, 并将结果设为删除环的新值。

我们可以通过示例理解其工作方式。

首先执行:

(setq example-list '("here is a clause" "another clause"))

使用 C-x C-e 对该表达式求值后, 你可以对 example-list 求值并查看结果:

example-list
     ⇒ ("here is a clause" "another clause")

现在,我们可以对如下表达式求值, 为该列表添加一个新元素:

(push "a third clause" example-list)

再次对 example-list 求值时,结果为:

example-list
     ⇒ ("a third clause" "here is a clause" "another clause")

可见 push 将第三个条目添加到了列表头部。

接下来是 if 语句的第二部分。 该表达式用于限制删除环的最大长度,代码如下:

(if (> (length kill-ring) kill-ring-max)
    (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil))

代码会检查删除环长度是否超出允许的最大值, 该值由 kill-ring-max 指定(默认为 120)。 如果删除环过长,代码会将其最后一个元素设为 nil。 它通过两个函数实现:nthcdrsetcdr

我们之前已经了解过 setcdr(see setcdr)。 它用于设置列表的 CDR, 就像 setcar 用于设置列表的 CAR。 不过在本例中,setcdr 并非设置整个删除环的 CDRnthcdr 函数使其作用于删除环倒数第二个元素的 CDR — 这意味着,由于倒数第二个元素的 CDR 就是删除环的最后一个元素, 该操作会设置删除环的最后一个元素。

nthcdr 函数会反复获取列表的 CDR — 对 CDRCDR 再取 CDR … 重复执行 N 次后返回结果。 (See nthcdr.)

因此,如果我们有一个四元素列表, 但只需要保留三个元素, 就可以将倒数第二个元素的 CDR 设为 nil, 从而缩短列表。 (如果你将最后一个元素设为非 nil 的其他值, 列表并不会被缩短。See setcdr。)

你可以依次对下面三个表达式求值,直观观察列表缩短效果。 首先将 trees 设为 (maple oak pine birch), 然后将其第二个 CDRCDR 设为 nil, 再查看 trees 的值:

(setq trees (list 'maple 'oak 'pine 'birch))
     ⇒ (maple oak pine birch)

(setcdr (nthcdr 2 trees) nil)
     ⇒ nil

trees
     ⇒ (maple oak pine)

setcdr 表达式返回的值为 nil, 因为这正是被设置的 CDR 内容。)

再重申一次:在 kill-new 中, nthcdr 会按删除环最大长度减一的次数获取 CDR, 然后 setcdr 将该元素的 CDR (即删除环中后续的所有元素)设为 nil, 从而避免删除环无限增长。

kill-new 函数中倒数第二个表达式为:

(setq kill-ring-yank-pointer kill-ring)

kill-ring-yank-pointer 是一个全局变量, 被设置为指向 kill-ring

尽管 kill-ring-yank-pointer 被称为“指针”, 它本质上和删除环一样都是变量。 不过这个命名是为了帮助人类理解其用途。

现在回到函数体开头的表达式:

  (if (fboundp 'menu-bar-update-yank-menu)
       (menu-bar-update-yank-menu string (and replace (car kill-ring))))

它以一个 if 表达式开头。

本例中,表达式首先检查 menu-bar-update-yank-menu 是否作为函数存在,若存在则调用它。 如果被测试的符号拥有非空的函数定义, fboundp 函数就会返回真。 如果该符号的函数定义为空,我们就会收到错误提示, 就像之前故意制造错误时一样(see Generate an Error Message)。

then 分支包含一个以 and 函数为首个元素的表达式。

and 特殊形式会依次对每个参数求值, 直到某个参数返回 nil, 此时整个 and 表达式返回 nil; 如果所有参数都不返回 nil, 则返回最后一个参数的求值结果。 (在 Emacs Lisp 中,非 nil 的值均视为真。) 换句话说,and 表达式只有在所有参数都为真时才返回真值。 (See 本章小结.)

该表达式用于确定 传递给 menu-bar-update-yank-menu 的第二个参数是否为真。

menu-bar-update-yank-menu 是支持菜单栏中 “编辑(Edit)”项里 “选择并粘贴(Select and Paste)” 菜单的函数之一; 你可以通过鼠标查看已保存的各段文本, 并选择一段进行粘贴。

kill-new 函数的最后一个表达式 会将新复制的字符串同步到窗口系统中 用于程序间复制粘贴的机制中。 例如在 X 窗口系统中,x-select-text 函数 会接收该字符串并存储到 X 系统管理的内存中, 你可以在 Xterm 等其他程序中粘贴该字符串。

该表达式如下:

  (if interprogram-cut-function
      (funcall interprogram-cut-function string (not replace))))

如果存在 interprogram-cut-function, Emacs 就会执行 funcall, 将其第一个参数作为函数调用, 并将剩余参数传递给它。 (顺便一提,在我看来, 这个 if 表达式可以替换为 与函数开头类似的 and 表达式。)

我们不会进一步讨论窗口系统与其他程序, 只需知道这是让 GNU Emacs 能够与其他程序良好协同工作的机制即可。

这段将文本加入删除环的代码(无论是拼接至现有元素还是作为新元素), 将引出用于取回从缓冲区剪切文本的代码 — 即取回命令。 不过在讨论取回命令之前, 最好先了解列表在计算机中的实现方式, 这能澄清 “指针(pointer)” 等术语的内在含义。 但在此之前,我们先穿插介绍一下 C 语言相关内容。