Elisp 操作对象:缓冲区

Table of Contents

缓冲区的名字

emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。下面的函数列表中如果参数是 BUFFER-OR-NAME 则是能同时接受缓冲区对象和缓冲区名的函数,否则只能接受一种参数。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。

要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 M-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 <序号> 的方式使名字变得不同。你也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名。

创建和关闭缓冲区

产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 <N>(N 是一个整数,从2开始) 的名字创建新的缓冲区。

关闭一个缓冲区可以用 kill-buffer。当关闭缓冲区时,如果要用户确认是否要关闭缓冲区,可以加到 kill-buffer-query-functions 里。如果要做一些善后处理,可以用 kill-buffer-hook。

通常一个接受缓冲区作为参数的函数都需要参数所指定的缓冲区是存在的。如果要确认一个缓冲区是否依然还存在可以使用 buffer-live-p。

要对所有缓冲区进行某个操作,可以用 buffer-list 获得所有缓冲区的列表。

如果你只是想使用一个临时的缓冲区,而不想先建一个缓冲区,使用结束后又需要关闭这个缓冲区,可以用 with-temp-buffer 这个宏。从这个宏的名字可以看出,它所做的事情是先新建一个临时缓冲区,并把这个缓冲区作为当前缓冲区,使用结束后,关闭这个缓冲区,并恢复之前的缓冲区为当前缓冲区。

在缓冲区内移动

在学会移动函数之前,先要理解两个概念:位置(position)和标记(mark)。位置是指某个字符在缓冲区内的下标,它从1开始。更准确的说位置是在两个字符之间,所以有在位置之前的字符和在位置之后的字符之说。但是通常我们说在某个位置的字符都是指在这个位置之后的字符。

标记和位置的区别在于位置会随文本插入和删除而改变位置。一个标记包含了缓冲区和位置两个信息。在插入和删除缓冲区里的文本时,所有的标记都会检查一遍,并重新设置位置。这对于含有大量标记的缓冲区处理是很花时间的,所以当你确认某个标记不用的话应该释放这个标记。

创建一个标记使用函数 make-marker。这样产生的标记不会指向任何地方。你需要用 set-marker 命令来设置标记的位置和缓冲区

(setq foo (make-marker))             ; => #<marker in no buffer>
(set-marker foo (point))             ; => #<marker at 3594 in *scratch*>

也可以用 point-marker 直接得到 point 处的标记。或者用 copy-marker 复制一个标记或者直接用位置生成一个标记

(point-marker)                       ; => #<marker at 3516 in *scratch*>
(copy-marker 20)                     ; => #<marker at 20 in *scratch*>
(copy-marker foo)                    ; => #<marker at 3502 in *scratch*>

如果要得一个标记的内容,可以用 marker-position,marker-buffer

(marker-position foo)                ; => 3502
(marker-buffer foo)                  ; => #<buffer *scratch*>

位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。

和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 point。point 是一个特殊的缓冲区位置,许多命令在这个位置进行文本插入。每个缓冲区都有一个 point 值,它总是比函数point-min 大,比另一个函数 point-max 返回值小。注意,point-min 的返回值不一定是 1,point-max 的返回值也不定是比缓冲区大小函数 buffer-size 的返回值大 1 的数,因为 emacs 可以把一个缓冲区缩小(narrow)到一个区域,这时 point-min 和 point-max 返回值就是这个区域的起点和终点位置。所以要得到 point 的范围,只能用这两个函数,而不能用 1 和 buffer-size 函数。

和 point 类似,有一个特殊的标记称为 “the mark”。它指定了一个区域的文本用于某些命令,比如 kill-region,indent-region。可以用 mark 函数返回当前 mark 的值。如果使用 transient-mark-mode,而且 mark-even-if-inactive值是 nil 的话,在 mark 没有激活时(也就是 mark-active 的值为 nil),调用 mark 函数会产生一个错误。如果传递一个参数 force 才能返回当前缓冲区 mark 的位置。mark-marker 能返回当前缓冲区的 mark,这不是 mark 的拷贝,所以设置它的值会改变当前 mark 的值。set-mark 可以设置 mark 的值,并激活 mark。每个缓冲区还维护一个mark-ring,这个列表里保存了 mark 的前一个值。当一个命令修改了 mark 的值时,通常要把旧的值放到 mark-ring 里。可以用 push-mark 和 pop-mark 加入或删除 mark-ring 里的元素。当缓冲区里 mark 存在且指向某个位置时,可以用 region-beginning 和 region-end 得到 point 和 mark 中较小的和较大的值。当然如果使用 transient-mark-mode 时,需要激活 mark,否则会产生一个错误。

按单个字符位置来移动的函数主要使用 goto-char 和 forward-char、backward-char。前者是按缓冲区的绝对位置移动,而后者是按 point 的偏移位置移动比如

(goto-char (point-min))                   ; 跳到缓冲区开始位置
(forward-char 10)                         ; 向前移动 10 个字符
(forward-char -10)                        ; 向后移动 10 个字符

可能有一些写 elisp 的人没有读文档或者贪图省事,就在写的 elisp 里直接用 beginning-of-buffer 和 end-of-buffer 来跳到缓冲区的开头和末尾,这其实是不对的。因为这两个命令还做了其它事情,比如设置标记等等。同样,还有一些函数都是不推荐在 elisp 中使用的,如果你准备写一个要发布 elisp,还是要注意一下。

按词移动使用 forward-word 和 backward-word。至于什么是词,这就要看语法表格的定义了。

按行移动使用 forward-line。没有 backward-line。forward-line 每次移动都是移动到行首的。所以,如果要移动到当前行的行首,使用 (forward-line 0)。如果不想移动就得到行首和行尾的位置,可以用 line-beginning-position 和 line-end-position。得到当前行的行号可以用 line-number-at-pos。需要注意的是这个行号是从当前状态下的行号,如果使用 narrow-to-region 或者用 widen 之后都有可能改变行号。

由于 point 只能在 point-min 和 point-max 之间,所以 point 位置测试有时是很重要的,特别是在循环条件测试里。常用的测试函数是 bobp(beginning of buffer predicate)和 eobp(end of buffer predicate)。对于行位置测试使用 bolp(beginning of line predicate)和 eolp(end of line predicate)。

缓冲区的内容

要得到整个缓冲区的文本,可以用 buffer-string 函数。如果只要一个区间的文本,使用 buffer-substring 函数。point 附近的字符可以用 char-after 和 char-before 得到。point 处的词可以用 current-word 得到,其它类型的文本,比如符号,数字,S 表达式等等,可以用 thing-at-point 函数得到。

修改缓冲区的内容

要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。

插入文本最常用的命令是 insert。它可以插入一个或者多个字符串到当前缓冲区的 point 后。也可以用 insert-char 插入单个字符。插入另一个缓冲区的一个区域使用 insert-buffer-substring。

删除一个或多个字符使用 delete-char 或 delete-backward-char。删除一个区间使用 delete-region。如果既要删除一个区间又要得到这部分的内容使用 delete-and-extract-region,它返回包含被删除部分的字符串。

最常用的查找函数是 re-search-forward 和 re-search-backward。这两个函数参数如下

(re-search-forward REGEXP &optional BOUND NOERROR COUNT)
(re-search-backward REGEXP &optional BOUND NOERROR COUNT)

其中 BOUND 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward),NOERROR 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil。COUNT 是指定查找匹配的次数。

替换一般都是在查找之后进行,也是使用 replace-match 函数。和字符串的替换不同的是不需要指定替换的对象了。