缓冲区
缓冲区 buffer 是用来 保存要编辑文本的对象 :
- 通常缓冲区都是和文件相关联的,但是也有很多缓冲区没有对应的文件
- emacs 可以同时打开多个文件,也就是说能同时有很多个缓冲区存在
- 但是在任何时候都只有一个缓冲区称为当前缓冲区 current buffer
即使在 lisp 编程中也是如此。许多编辑和移动的命令只能针对当前缓冲区
缓冲区的名字
emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个 缓冲区对象 或一个 字符串作为缓冲区名 查找对应的缓冲区
一般函数中如果参数是 BUFFER-OR-NAME 则是能同时接受缓冲区对象和缓冲区名的函数,否则只能接受一种参数 有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区 所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件
- 要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的:
- 如果不指定参数,则返回当前缓冲区的名字
- 否则返回指定缓冲区的名字
- 更改一个缓冲区的名字用 rename-buffer ,这是一个命令,所以可以用 M-x 调用来修改当前缓冲区的名字
- 如果指定的名字与现有的缓冲区冲突,则会产生一个错误
- 可以使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 <序号> 的方式使名字变得不同
- 也可以用 generate-new-buffer-name 来产生一个唯一的缓冲区名
当前缓冲区
当前缓冲区可以用 current-buffer 函数得到。当前缓冲区 不一定是显示在屏幕上的那个缓冲区 ,可以用 set-buffer 来指定当前缓冲区
但是需要注意的是,当命令返回到命令循环时,光标所在的缓冲区 会自动成为当前缓冲区 这也是单独在 *scratch* 中执行 set-buffer 后并不能改变当前缓冲区,而必须使用 progn 语句同时执行多个语句才能改变当前缓冲区的原因
(set-buffer "*Messages*") ; => #<buffer *Messages*> (message (buffer-name)) ; => "*scratch*" (progn (set-buffer "*Messages*") (message (buffer-name))) ; "*Messages*"
但是不能依赖命令循环来把当前缓冲区设置成使用 set-buffer 之前的。因为这个命令很可以会被另一个程序员来调用。也不能直接用 set-buffer 设置成原来的缓冲区,比如
(let (buffer-read-only (obuf (current-buffer))) (set-buffer ...) ... (set-buffer obuf))
因为 set-buffer 不能处理错误或退出情况
正确的作法是使用 save-current-buffer 、 with-current-buffer 和 save-excursion 等方法:
- save-current-buffer 能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区
- 如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区
- with-current-buffer 使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区
(with-current-buffer BUFFER-OR-NAME body)
相当于:
(save-current-buffer (set-buffer BUFFER-OR-NAME) body)
lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句
save-excursion 与 save-current-buffer 不同之处在于,它不仅保存当前缓冲区,还保存了 当前的位置和 mark 。在 scratch 缓冲区中运行下面两个语句就能看出它们的差别了
(save-current-buffer (set-buffer "*scratch*") (goto-char (point-min)) (set-buffer "*Messages*")) (save-excursion (set-buffer "*scratch*") (goto-char (point-min)) (set-buffer "*Messages*"))
创建和关闭
- 产生一个缓冲区必须给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数: get-buffer-create 和 generate-new-buffer ,这两个函数的差别:
- 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 xxxx 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 函数会产生一个错误
- 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 来跳到缓冲区的开头和末尾,这其实是不对的 因为这两个命令还做了其它事情,比如设置标记等等
按词 移动使用 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 。这两个函数参数如下
- BOUND: 指定查找的范围,默认是 point-max(对于 re-search-forward)或 point-min(对于 re-search-backward)
- NOERROR: 是当查找失败后是否要产生一个错误,一般来说在 elisp 里都是自己进行错误处理,所以这个一般设置为 t,这样在查找成功后返回区配的位置,失败后会返回 nil
- COUNT: 是指定查找匹配的次数
(re-search-forward REGEXP &optional BOUND NOERROR COUNT) (re-search-backward REGEXP &optional BOUND NOERROR COUNT)
- 替换 一般都是在查找之后进行,也是使用 replace-match 函数
- 和字符串的替换不同的是不需要指定替换的对象