Emacs折腾日记(十四)——buffer操作
教程 中的下一节应该是正则表达式。但是我觉得目前来说正则表达式对我来说不是重点,而且正则表达式我还是比较了解,没必要专门去学习,在使用的时候看看相应的章节就好。况且现在有AI这个利器,在处理正则表达式应该问题不大。所以这里就略过这节,直接进入后面的学习
截止到前面的一些文章,我觉得应该已经涉及到了emacs lisp中的语法要点,现在去看一些emacs配置中的代码不太会一头雾水了。离攒自己的配置又进了一步。期间我想过是不是可以跳过教程后面的内容直接进入配置的过程呢?仔细考虑了一下,我觉得还是有必要跟着教程深入了解一下操作emacs对象的一些API。就像我之前学习C/C++编程一样,如果只学语法部分,最多也就能写写基于链表等数据结构的黑框框的信息管理系统。如果想要写点带界面的或者带网络功能的或者稍微复杂点的程序就离不开操作系统,网络编程,数据库等等知识。emacs的学习可能也是这样,现在也只能写点算术运算或者say-hello 这样的玩具。想要跟emacs结合起来,真正流畅的操作emacs,还需要学一些emacs自身的知识。
缓冲区名称
在学习vim的时候已经很详细的了解过什么是缓冲区,以及缓冲区与文件有什么区别。在这里我想就没有必要再谈论了,如果有读者不太清楚这方面的内容,欢迎阅读我博客中关于vim缓冲区的部分。
emacs中缓冲区的概念与vim基本没什么区别。唯一的区别可能是emacs中一些内置的缓冲区与vim的不太一样。
emacs 里的所有缓冲区都有一个不重复的名字。所以和缓冲区相关的函数通常都是可以接受一个缓冲区对象或一个字符串作为缓冲区名查找对应的缓冲区。有一个习惯是名字以空格开头的缓冲区是临时的,用户不需要关心的缓冲区。所以现在一般显示缓冲区列表的命令都不会显示这样的变量,除非这个缓冲区关联一个文件。
要得到缓冲区的名字,可以用 buffer-name 函数,它的参数是可选的,如果不指定参数,则返回当前缓冲区的名字,否则返回指定缓冲区的名字。更改一个缓冲区的名字用 rename-buffer,这是一个命令,所以你可以用 M-x 调用来修改当前缓冲区的名字。如果你指定的名字与现有的缓冲区冲突,则会产生一个错误,除非你使用第二个可选参数以产生一个不相同的名字,通常是在名字后加上 <序号> 的方式使名字变得不同。你也可以用 generate-new-buffer-name
来产生一个唯一的缓冲区名。它需要传入一个参数,表示buffer的名称,如果当前buffer有名称与指定名称冲突,它会在你提供的名称后面加一些后缀,否则就采用传入的名称
;; scratch buffer 中
(buffer-name) ;; ==> *scratch*
;; 此时 *scratch* 已经被重命名成了 scratch
(rename-buffer (generate-new-buffer-name "scratch")) ;; ==> scratch
当前缓冲区
可以使用 current-buffer
来获取当前缓冲区,需要注意的是当前缓冲区不一定是显示在当前屏幕上的那个缓冲区。这个跟工作目录有点像,当前目录并不一定就是程序所在的目录或者当前打开的文件所在的目录。
我们可以使用 set-buffer
来设置当前缓冲区,但是前面我们说过当前缓冲区并不一定是显示在屏幕上的那个缓冲区,即使修改当前缓冲区,也不会改变当前窗口上显示的缓冲区
(set-buffer "*Messages*") ;; ==> #<buffer *Messages*>,但是屏幕上显示的缓冲区没有变化
如果要切换当前屏幕显示的缓冲区需要配置窗口相关的函数,例如我们可以使用 switch-to-buffer
(switch-to-buffer "*Messages*")
前面提到 set-buffer
可以改变当前缓冲区,但是我们调用 buffer-name
获取当前缓冲区的名称时得到还是 scratch buffer
(set-buffer "*Messages*") ;; ==> #<buffer *Messages*>
(buffer-name) ;; ==> #<buffer *scratch*>
(buffer-name) ;; ==> "*scratch*"
这是因为我们如果采用 C-x C-e
来分别执行,这就相当于在命令行执行命令一样,每次语句结束之后,emacs会重新刷新上下文环境。而 buffer-name获取的是当前上下文环境中的当前缓冲区名称。上下文环境随着上一条语句的结束而更新,这就导致了当前缓冲区变化。这个过程可以描述为如下的过程
[主进程环境]
│
├── [逐行执行L1] → 创建临时子环境 → 执行set-buffer → 销毁子环境
└── [逐行执行L2] → 创建新子环境 → 读取buffer-name → 返回主环境值
所以如果要得到正确的结果,就是一次性执行完这两条语句,按照我当前的知识储备有三种办法:
第一个办法就是使用 eval-buffer
,从messages buffer 中获取输出信息
第二个办法,使用 progn
将两条语句包含起来
第三个办法就是将它包装成一个函数或者宏来执行
(progn
(set-buffer "*Messages*")
(buffer-name)) ;; ==> "*Messages*"
但是我们不能仅仅依靠这种包裹代码的方式来实现切换buffer的效果。因为这个命令很可以会被另一个程序员来调用。你也不能直接用 set-buffer 设置成原来的缓冲区,因为set-buffer不能处理错误或退出情况。正确的作法是使用 save-current-buffer
、with-current-buffer
和 save-excursion
等方法
save-current-buffer
能保存当前缓冲区,执行其中的表达式,最后恢复为原来的缓冲区。如果原来的缓冲区被关闭了,则使用最后使用的那个当前缓冲区作为语句返回后的当前缓冲区。lisp 中很多以 with 开头的宏,这些宏通常是在不改变当前状态下,临时用另一个变量代替现有变量执行语句。比如 with-current-buffer
使用另一个缓冲区作为当前缓冲区,语句执行结束后恢复成执行之前的那个缓冲区
save-excursion
与 save-current-buffer
不同之处在于,它不仅保存当前缓冲区,还保存了当前的位置和 mark
。在 scratch 缓冲区中运行下面两个语句就能看出它们的差别了
(save-current-buffer
(set-buffer "*scratch*")
(goto-char (point-min))
(save-excursion
(set-buffer "*scratch*")
(goto-char (point-min))
上面两段代码,都是先保存当前缓冲区,然后使用 set-buffer 保证当前缓冲区是 scratch buffer,接着调用goto-char移动鼠标光标到buffer最开始的位置。随着代码块的结束,会自动切换回对应的buffer。但是因为 save-excursion
会额外保存当前位置和 mark
,所以我们发现第一段代码光标位置跑到缓冲区最开始的位置,而第二段代码光标位置不变
在对比一下它们与 with-current-buffer
的区别,with-current-buffer
调用时已经帮我们使用 set-buffer
设置好了当前缓冲区,而且也会保存当前缓冲区,在结束之后也会还原当前缓冲区。但是它使用的是 save-current-buffer
。我们可以使用 C-h C-f
来查看并找到它的源代码
(defmacro with-current-buffer (buffer-or-name &rest body)
(declare (indent 1) (debug t))
`(save-current-buffer
(set-buffer ,buffer-or-name)
,@body))
我们发现它其实就是用 save-current-buffer
做了一次封装。如果我们对上面的测试代码稍加修改使用 with-current-buffer 实现,例如
(with-current-buffer "*Messages*"
(goto-char (point-min)))
执行之后我们发现,它的光标位置也改变了。
创建和关闭缓冲区
产生一个缓冲区必须用给这个缓冲区一个名字,所以两个能产生新缓冲区的函数都是以一个字符串为参数:get-buffer-create 和 generate-new-buffer。这两个函数的差别在于前者如果给定名字的缓冲区已经存在,则返回这个缓冲区对象,否则新建一个缓冲区,名字为参数字符串,而后者在给定名字的缓冲区存在时,会使用加上后缀 (N 是一个整数,从2开始) 的名字创建新的缓冲区。
(get-buffer-create "temp")
(with-current-buffer "temp"
(insert "this is temp buffer"))
(switch-to-buffer "temp")
上面的代码,我们先创建一个新的temp buffer,并且切换到这个buffer,然后在这个buffer中调用insert函数,插入一段话。最后可以让窗口显示这个buffer来验证结果
关闭一个缓冲区可以用 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 195 in *scratch*>
point 函数其实返回一个整数,表示当前光标在哪个位置,既然这里只用传入位置就可以正确的将foo这个标签绑定到对应的缓冲区,这里set-marker 应该是以当前缓冲区作为标签的缓冲区,我们可以使用下面的代码来验证
(with-current-buffer "*Messages*"
(set-marker foo (point))) ;; ==> #<marker at 477 in *Messages*>
也可以用 point-marker
直接得到 point
处的标记。或者用 copy-marker
复制一个标记或者直接用位置生成一个标记
(point-marker) ;; ==> #<marker at 211 in *scratch*>
(copy-marker 20) ;; ==> #<marker at 20 in *scratch*>
(copy-marker foo) ;; ==> #<marker at 195 in *scratch*>
如果要得一个标记的内容,可以用 marker-position
,marker-buffer
(marker-position foo) ;; ==> 195
(marker-buffer foo) ;; ==> #<buffer *scratch*>
位置就是一个整数,而标记在一般情况下都是以整数的形式使用,所以很多接受整数运算的函数也可以接受标记为参数。比如加减乘。
(goto-char (+ (marker-position foo) 10)
例如上面的代码我们移动光标到第205个字符的位置。
和缓冲区相关的变量,有的可以用变量得到,比如缓冲区关联的文件名,有的只能用函数来得到,比如 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
函数。
按单个字符位置来移动的函数主要使用 goto-char
和 forward-char
、backward-char
。前者是按缓冲区的绝对位置移动,而后者是按 point
的偏移位置移动比如
(goto-char (point-min)) ; 跳到缓冲区开始位置
(forward-char 10) ; 向前移动 10 个字符
(forward-char -10) ; 向后移动 10 个字符
(backward-char 10) ; 向后移动 10 个字符
(backward-char -10) ; 向前移动 10 个字符
按词移动使用 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
函数得到。
ting-at-point 可以获取光标处的很多类型的内容,它需要传入一个符号作为类型,例如 'word、'symbol、'url。不同的类型包含有不同的文本属性,第二个参数表示是否去除文本属性。如果当前位置有内容,则返回内容,否则返回nil
(defun show-current-word ()
(interactive)
(let ((word (thing-at-point 'word t))) ;; 'word 类型,t 表示去除文本属性
(if word
(message "当前单词: %s" word)
(message "光标位置没有单词"))))
修改缓冲区的内容
要修改缓冲区的内容,最常见的就是插入、删除、查找、替换了。下面就分别介绍这几种操作。
插入文本最常用的命令是 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
函数。和字符串的替换不同的是不需要指定替换的对象了。