马儿慢些走

人生惶惶,悲喜一场。

【Elisp官方指南,翻译】4 一些与缓冲区相关的函数

【Elisp官方指南,翻译】4 一些与缓冲区相关的函数

原文地址:https://www.gnu.org/software/emacs/manual/html_node/eintr/Buffer-Walk-Through.html

备注:使用通义千文进行翻译,略微修正。

4 一些与缓冲区相关的函数

在本章中,我们将详细探讨GNU Emacs中使用的一些函数。这被称为“逐步讲解”。这些函数被用作Lisp代码的例子,但并非虚构例子;除了第一个简化的函数定义之外,这些函数展示了GNU Emacs中实际使用的代码。你可以从这些定义中学到很多知识。这里描述的函数都与缓冲区有关。之后,我们将研究其他函数。

4.1 查找更多信息

在本逐步讲解中,每当遇到新的函数时,我都会进行描述,有时详尽解释,有时简要说明。如果你感兴趣,随时可以通过键入C-h f然后输入函数名(再按RET键)来获取任何Emacs Lisp函数的完整文档。类似地,你也可以通过键入C-h v然后输入变量名(再按RET键)来获取变量的完整文档。

此外,describe-function命令还会告诉你函数定义的位置。

将光标置于包含该函数的文件名上并按下RET键。在这种情况下,RET表示“按钮”而非“返回”或“回车”。Emacs会直接带你跳转到函数定义处。

更一般地说,如果你想查看函数原始源代码,可以使用xref-find-definitions函数直接跳转过去。xref-find-definitions不仅适用于Lisp和C等语言,还支持多种其他语言以及非编程文本。例如,xref-find-definitions可以跳转到本文档Texinfo源文件中的各个节点(前提是已使用etags工具记录了随Emacs一起提供的手册中的所有节点;参阅《GNU Emacs 手册》中的Create Tags Table部分)。

要使用xref-find-definitions命令,可键入M-.(即同时按住META键敲击句号键,或者先按ESC键再敲击句号键),然后在提示符下输入你想查看源代码的函数名,比如mark-whole-buffer,之后再按RET键。(如果命令未出现提示,则带参数调用它:C-u M-.;参见针对交互式的不同选项)。Emacs会切换缓冲区并在屏幕上显示函数的源代码11。要切换回当前缓冲区,请键入M-, 或者C-x b RET。(在某些键盘上,META键标记为ALT键)

顺便提一下,包含Lisp代码的文件通常被称为库。这个比喻源于专业图书馆的概念,如法律图书馆或工程图书馆,而不是一般的图书馆。每个库(或文件)都包含了与特定主题或活动相关的函数,例如处理缩写和其他快速输入技巧的abbrev.el库,以及提供帮助功能的help.el库。(有时,一个单一活动的代码可能由多个库提供,就像各种rmail...文件为阅读电子邮件提供代码一样。)在《GNU Emacs 手册》中,你会看到诸如“C-h p命令允许你通过主题关键词搜索标准Emacs Lisp库”的句子。

脚注
(11)
如果Emacs不是显示Lisp函数的源代码,而是询问你想要访问哪个标签表,请从主要模式为Emacs Lisp或Lisp Interaction的缓冲区中调用M-.命令。

4.2 简化的beginning-of-buffer定义

beginning-of-buffer命令是一个很好的起点,因为它易于理解且你可能已经很熟悉。作为交互式命令使用时,beginning-of-buffer会将光标移动到缓冲区的开头,并在原位置留下标记。通常它被绑定到M-<快捷键。

在本节中,我们将讨论一个简化的函数版本,该版本展示了它最常见的用法。这个简化版函数可以正常工作,但未包含处理复杂选项的代码。在另一个部分,我们将描述整个函数的完整定义。(参见beginning-of-buffer的完整定义)

在查看代码之前,让我们先考虑一下函数定义需要包含的内容:它必须包含一个表达式,使函数具有交互性,可以通过键入M-x beginning-of-buffer或类似M-<这样的快捷键调用;它必须包含在缓冲区原位置留下标记的代码;以及将光标移动到缓冲区开头的代码。

以下是该简化版函数的完整文本:

(defun simplified-beginning-of-buffer ()
  "移动光标至缓冲区开头;
在原位置保留标记。"
  (interactive)
  (push-mark)
  (goto-char (point-min)))

像所有函数定义一样,此定义在defun宏之后有五个组成部分:

  1. 函数名:在这个例子中是simplified-beginning-of-buffer。
  2. 参数列表:在这个例子中是一个空列表,(),
  3. 文档字符串。
  4. 交互式表达式。
  5. 函数体。

在这个函数定义中,参数列表为空,这意味着该函数不需要任何参数。(当我们查看完整函数的定义时,会看到它可以接受一个可选参数。)

交互式表达式告诉Emacs该函数打算以交互方式使用。在这个示例中,interactive没有参数,因为simplified-beginning-of-buffer不需要参数。

函数体由两行组成:

(push-mark)
(goto-char (point-min))

其中第一行表达式(push-mark)在Lisp解释器评估后,在当前光标位置设置一个标记,无论其当前位置在哪里。这个标记的位置被保存在mark环中。

接下来一行是(goto-char (point-min))。这个表达式将光标跳转到缓冲区中的最小点,即缓冲区的开头(如果进行了窄化操作,则跳转到缓冲区可访问部分的开头。参见Narrowing and Widening章节)。

push-mark命令在光标通过(goto-char (point-min))表达式移动到缓冲区开头之前所在的位置设置了一个标记。因此,如果你想回到原来的位置,可以通过键入C-x C-x实现。

这就是函数定义的所有内容!

当你阅读此类代码并遇到不熟悉的函数,如goto-char时,可以通过使用describe-function命令来了解其功能。要使用此命令,请键入C-h f,然后输入函数名并按RET键。describe-function命令会在Help窗口中打印出函数的文档字符串。例如,goto-char的文档为:

将光标位置(point)设置为POSITION,一个数字或标记。
缓冲区开头位置为(point-min),结尾位置为(point-max)。

该函数的一个参数是要跳转到的目标位置。

(describe-function命令会提供光标下方或之前的符号作为提示,因此你可以通过将光标定位在函数正上方或之后,然后键入C-h f RET来节省输入时间。)

end-of-buffer函数的定义与beginning-of-buffer定义的编写方式相同,只不过函数体包含了(goto-char (point-max))表达式,替代了(goto-char (point-min)),以便将光标移动到缓冲区的末尾。

4.3 mark-whole-buffer函数的定义

mark-whole-buffer函数的理解难度并不比简化版的simplified-beginning-of-buffer函数高。然而,在本节中,我们将查看完整版的函数,而非简化的版本。

尽管mark-whole-buffer函数不像beginning-of-buffer函数那样常用,但它仍然非常有用:通过将光标置于缓冲区开头,并在缓冲区末尾放置标记,它能够将整个缓冲区标记为一个区域。通常,这个命令绑定到C-x h快捷键上。

mark-whole-buffer函数概述

在GNU Emacs 22中,完整函数的代码如下所示:

(defun mark-whole-buffer ()
  "将光标置于缓冲区起始位置,并在缓冲区末尾设置标记。
在Lisp程序中,你可能不应该使用这个函数;
通常情况下,Lisp函数调用任何会使用或设置标记的子程序都是错误的做法。"
  (interactive)
  (push-mark (point))
  (push-mark (point-max) nil t)
  (goto-char (point-min)))

与所有其他函数一样,mark-whole-buffer函数遵循函数定义的模板。该模板的形式如下:

(defun 函数名 (参数列表)
  "文档字符串……"
  (交互式表达式……)
  函数体……)

下面是函数的工作原理:函数名为mark-whole-buffer;后面是一个空的参数列表‘()’,表示该函数不需要任何参数。接下来是函数的文档描述。

下一行是一个(interactive)表达式,它告诉Emacs该函数将会以交互方式使用。这部分细节与上一节所描述的简化版beginning-of-buffer函数类似。

函数的具体工作过程为:首先通过(push-mark (point))在当前光标位置设置一个临时标记;接着使用(push-mark (point-max) nil t)在缓冲区末尾设置另一个标记(并将其加入到mark环中);最后(goto-char (point-min))将光标移动到缓冲区开头。这样就完成了整个缓冲区内容的选择操作。

4.3.1 mark-whole-buffer函数的主体部分

mark-whole-buffer函数的主体由三行代码组成:

(push-mark (point))
(push-mark (point-max) nil t)
(goto-char (point-min))

其中第一行表达式是(push-mark (point))。

这一行的作用与简化版beginning-of-buffer函数主体中的第一行(即(push-mark))完全相同。在这两种情况下,Lisp解释器都会在光标当前位置设置一个标记。

我不知道为什么mark-whole-buffer中的表达式写为(push-mark (point)),而beginning-of-buffer中写为(push-mark)。可能是编写这段代码的人并不知道push-mark的参数是可选的,如果未向push-mark传递参数,默认情况下该函数会自动在point的位置设置标记。或者可能是为了与下一行代码结构保持一致而这样编写的。无论如何,这一行代码使得Emacs确定了point的位置并在那里设置了一个标记。

在早期版本的GNU Emacs中,mark-whole-buffer的下一行是(push-mark (point-max))。这个表达式在缓冲区编号最高的位置设置一个标记,这将是缓冲区的末尾(或在缓冲区被窄化的情况下,是缓冲区可访问部分的末尾。关于窄化的更多信息,请参阅Narrowing和Widening章节)。设置了这个标记后,之前在point处设置的标记不再有效,但Emacs会像记住其他最近的标记一样记住其位置,这意味着如果你愿意,可以通过连续两次键入C-u C-SPC回到那个位置。

在GNU Emacs 22中,(point-max)的部分稍微复杂一些。这一行代码如下所示:

(push-mark (point-max) nil t)

这个表达式的工作方式几乎与以前相同,它会在缓冲区可以达到的最大编号位置设置一个标记。然而,在这个版本中,push-mark有两个额外的参数。push-mark的第二个参数是nil,这告诉函数在设置标记时应显示一条“Mark set”消息。第三个参数是t,这指示当临时标记模式开启时,push-mark应该激活标记。临时标记模式会高亮当前活动区域,通常会被关闭。

最后,函数的最后一行是(goto-char (point-min)))。它与beginning-of-buffer中的写法完全相同。该表达式将光标移动到缓冲区的最小点,即缓冲区的开头(或缓冲区可访问部分的开头)。因此,point位于缓冲区的开始位置,而mark则设置在缓冲区的结束位置。因此,整个缓冲区就成为了选定的区域。

4.4 append-to-buffer命令的定义

append-to-buffer命令比mark-whole-buffer命令更为复杂。它的功能是从当前缓冲区中复制指定区域(即光标point与标记mark之间的部分)到另一个指定的缓冲区。

append-to-buffer命令概述

append-to-buffer命令使用insert-buffer-substring函数来复制区域。正如其名所示,insert-buffer-substring函数从一个缓冲区中取出子字符串,并将其插入到另一个缓冲区。

append-to-buffer命令的大部分内容都是为了设置insert-buffer-substring正常工作的条件:代码必须指定文本将要复制到的目标缓冲区、文本来源和目标窗口以及要复制的区域。

以下是该函数的一种可能实现:

(defun append-to-buffer (buffer start end)
  "将指定区域内的文本追加到指定缓冲区。
在插入目标缓冲区之前的位置进行插入。

当从程序调用时,请提供三个参数:
BUFFER(或缓冲区名称)、START和END。
START和END用于指定当前缓冲区中待复制的部分。"
  (interactive
   (list (read-buffer "追加到缓冲区: " (other-buffer
                                            (current-buffer) t))
         (region-beginning) (region-end)))
  (let ((oldbuf (current-buffer)))
    (save-excursion
      (let* ((append-to (get-buffer-create buffer))
             (windows (get-buffer-window-list append-to t t))
             point)
        (set-buffer append-to)
        (setq point (point))
        (barf-if-buffer-read-only)
        (insert-buffer-substring oldbuf start end)
        (dolist (window windows)
          (when (= (window-point window) point)
            (set-window-point window (point))))))))

该函数可以通过将其视为一系列填充了具体内容的模板来进行理解。

最外层的模板是函数定义。在这个函数中,它看起来像这样(其中一些部分已经填入):

(defun append-to-buffer (buffer start end)
  "文档说明…"
  (interactive …)
  函数体…)

函数的第一行包含函数名及其三个参数。这些参数分别是文本将被复制到的目标缓冲区,以及当前缓冲区中待复制区域的起始和结束位置。

函数的下一部分是文档说明,这部分清晰且完整。按照惯例,三个参数都以大写字母书写,以便于你轻松注意到它们。更妙的是,它们在文档中描述的顺序与参数列表中的顺序一致。

请注意,文档说明区分了缓冲区和其名称。(函数可以处理两者。)

4.4.1 append-to-buffer的交互式表达式

由于append-to-buffer函数将被用于交互式操作,因此该函数必须包含一个交互式表达式。(有关interactive的回顾,请参阅“使函数具有交互性”章节。)

这个表达式的读取如下:

(interactive
 (list (read-buffer
        "追加到缓冲区: "
        (other-buffer (current-buffer) t))
       (region-beginning)
       (region-end)))

这个表达式并非如前所述那样用字母表示各个部分。相反,它以这些部分开始构建一个列表:

列表的第一部分是一个用于读取缓冲区名称并将其作为字符串返回的表达式,即read-buffer。该函数需要一个提示作为其第一个参数,“追加到缓冲区: ”。其第二个参数告诉命令在用户未指定任何内容时应提供什么值。

在这个案例中,第二个参数是一个包含other-buffer函数、一个例外情况和代表真(true)的't'的表达式。

other-buffer的第一个参数是另一个函数current-buffer,但并不会直接返回它的结果。第二个参数是代表真(true)的符号t。这指示other-buffer可以显示可见的缓冲区(但在本例中,它不会显示当前缓冲区,这是合理的做法)。

这个表达式看起来像这样:

(other-buffer (current-buffer) t)

列表表达式的第二项和第三项分别是(region-beginning)和(region-end)。这两个函数指定了要追加文本的起始和结束位置。

最初,该命令使用了字母'B'和'r'。整个交互式表达式看起来像这样:

(interactive "B追加到缓冲区: \nr")

但是这样做后,默认切换到的缓冲区变成了不可见的,这不是所期望的结果。

(提示与第二个参数之间用换行符'\n'隔开,并在其后跟随了一个'r',它告诉Emacs将函数参数列表中buffer符号之后的两个参数(即start和end)绑定到point和mark的值上。这个参数工作得很好。)

4.4.2 append-to-buffer函数的主体部分

append-to-buffer函数的主体部分以let开始。

正如我们之前所见(参见let),let表达式的目的在于创建并初始化一个或多个变量,这些变量仅在let表达式的主体内使用。这意味着此类变量不会与let表达式外部同名的任何变量混淆。

我们可以展示一个带有let表达式概要的append-to-buffer模板,来了解它如何融入整个函数中:

(defun append-to-buffer (buffer start end)
  "文档字符串……"
  (interactive …)
  (let ((变量 值))
        函数体…))

let表达式包含三个要素:

符号let;
varlist,其中包含,在本例中是一个单个的两元素列表,(变量 值);
let表达式的主体部分。
在append-to-buffer函数中,varlist看起来像这样:

(oldbuf (current-buffer))

在这个let表达式的一部分中,单个变量oldbuf被绑定到(current-buffer)表达式返回的值上。变量oldbuf用于跟踪您正在操作且将从中复制内容的工作缓冲区。

varlist的元素或元素组被一对括号包围,以便Lisp解释器能够区分varlist与let的主体部分。因此,varlist内部的两元素列表被一组外围括号包围。代码行如下所示:

(let ((oldbuf (current-buffer)))
  … )

如果未意识到oldbuf前面的第一个括号标记了varlist的边界,而第二个括号标志着两元素列表(oldbuf (current-buffer))的起始位置,那么oldbuf前的两个括号可能会让你感到惊讶。

4.4.3 append-to-buffer中的save-excursion

append-to-buffer函数中let表达式的主体部分包含了一个save-excursion表达式。

save-excursion函数会保存point的位置,并在save-excursion主体内所有表达式执行完毕后将point恢复到原来的位置。此外,save-excursion还会跟踪原始缓冲区,并将其恢复原状。这就是save-excursion在append-to-buffer中使用的方式。

顺便提一下,值得注意的是,Lisp函数通常按照格式化规范编写,其中多行展开的所有内容都比第一个符号向右缩进更多。在这个函数定义中,let的缩进比defun更多,而save-excursion的缩进又比let更多,如下所示:

(defun …
  …
  …
  (let…
    (save-excursion
      …

这种格式化约定使得我们可以很容易地看出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
  第一个主体表达式
  第二个主体表达式
   …
  最后一个主体表达式)

在这个函数中,save-excursion的主体仅包含一个表达式,即let表达式。你已经了解了let函数。而let函数有所不同。它允许Emacs按顺序逐个设置varlist中的每个变量,以便varlist后部的变量可以利用前面已由Emacs设置的变量值。

观察append-to-buffer中的let*表达式:

(let* ((append-to (get-buffer-create buffer))
       (windows (get-buffer-window-list append-to t t))
       point)
  主体代码...)

我们看到append-to被绑定到(get-buffer-create buffer)返回的值上。下一行中,append-to作为参数传递给get-buffer-window-list;如果使用let表达式,则无法实现这一点。请注意,point自动绑定为nil,这与在let语句中处理方式相同。

现在让我们关注let*表达式主体中的set-buffer和insert-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函数从其第一个参数指定的缓冲区中复制字符串,并将其插入当前缓冲区中。在此例中,insert-buffer-substring的参数是通过let创建并绑定的变量的值,即oldbuf的值,在调用append-to-buffer命令时,oldbuf是当时的当前缓冲区。

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

简化表示,主体的工作流程如下所示:

(let (将-oldbuf-绑定到-current-buffer-的值上)
  (save-excursion                  ; 记录缓冲区信息
    切换缓冲区
    从-oldbuf-向当前缓冲区插入子字符串)

  完成后切换回原始缓冲区
完成后让-oldbuf-的局部意义消失

总结来说,append-to-buffer的工作原理如下:它将当前缓冲区的值保存在名为oldbuf的变量中。获取新缓冲区(如有必要则创建),并将Emacs的焦点切换到该新缓冲区上。通过oldbuf的值,它将旧缓冲区中的文本区域插入到新缓冲区中;然后利用save-excursion,将焦点带回原始缓冲区。

通过对append-to-buffer的分析,你已经探索了一个相当复杂的函数。它展示了如何使用let和save-excursion,以及如何切换到其他缓冲区并返回原缓冲区。许多函数定义都会采用这种方式使用let、save-excursion和set-buffer。

4.5 复习总结

以下是对本章讨论的各种函数的简要总结。

  • describe-function

  • describe-variable
    这两个函数用于打印函数或变量的文档说明。通常分别绑定到C-h f和C-h v快捷键上。

  • xref-find-definitions
    该函数用于查找包含指定函数或变量源代码的文件,并切换至该缓冲区,将光标定位在目标项的起始位置。通常绑定到M-.(即META键后跟一个句点)快捷键上。

  • save-excursion
    该函数用于保存point的位置,并在save-excursion的参数被求值之后恢复其值。同时,它还会记住当前缓冲区并在操作完成后返回到该缓冲区。

  • push-mark
    在指定位置设置标记,并在标记环上记录先前标记的值。即使向缓冲区添加或删除文本,该标记仍会保持其相对位置。

  • goto-char
    将point设置到由参数值所指定的位置,参数可以是一个数字、一个标记或者一个返回位置编号的表达式,例如(point-min)。

  • insert-buffer-substring
    从作为函数参数传递的缓冲区中复制一段文本区域,并将其插入到当前缓冲区中。

  • mark-whole-buffer
    将整个缓冲区标记为一个区域。通常绑定到C-x h快捷键。

  • let*
    声明一系列变量并赋予它们初始值;然后在let*主体内评估其余表达式。列表中后续变量的绑定值可以使用前面已经初始化过的变量值。

  • set-buffer
    使Emacs将注意力切换到另一个缓冲区,但不改变当前显示的窗口。当程序而非人类需要对不同缓冲区进行操作时使用此函数。

  • get-buffer-create

  • get-buffer
    根据名称查找缓冲区,如果不存在同名缓冲区,则创建一个新的。get-buffer函数在找不到指定名称的缓冲区时会返回nil。

4.6 练习

(备注:通义千问自己做出了回答。)

  1. 编写你自己的简化版end-of-buffer函数定义;然后进行测试,以验证它是否有效运行。

    示例:

    (defun simplified-end-of-buffer ()
      "将光标移动到当前缓冲区的末尾。"
      (interactive)
      (goto-char (point-max)))
    
  2. 使用if和get-buffer编写一个函数,该函数会打印一条消息告诉你指定的缓冲区是否存在。

    示例:

    (defun buffer-exists-p (buffer-name)
      "检查BUFFER-NAME所代表的缓冲区是否存在,并打印相应消息。
      如果缓冲区存在,则返回t,否则返回nil并打印消息。"
      (let ((buffer (get-buffer buffer-name)))
        (if buffer
            (message "%s缓冲区存在." buffer-name)
          (message "%s缓冲区不存在." buffer-name)
          nil)))
    
  3. 利用xref-find-definitions找到copy-to-buffer函数的源代码。

    要执行此操作,请在Emacs中调用M-.(即按下Meta键后按句点)并输入copy-to-buffer函数名,Emacs将会自动定位到该函数的定义位置。

posted on 2024-02-09 08:00  马儿慢些走  阅读(79)  评论(0)    收藏  举报

导航