Writing GNU Emacs Extensions ch4 要点
(defcustom xxx "%x" "*Format for \\[insert-date]")
这样可以通过set-variable定制xxx变量的内容,此外还能通过describe-variable获得格式化的doc-string内容。
(defcustom insert-time-format "%X" "*Format for \\[insert-time]") (defcustom insert-date-format "%x" "*Format for \\[insert-date]") (defun insert-time () "Insert the current time according to insert-time-format." (interactive "*");;如果是只读的buffer,报错 (insert (format-time-string insert-time-format (current-time)))) (defun insert-date () "Insert the current date according to insert-date-format" (interactive "*") (insert (format-time-string insert-date-format (current-time))))
光光是插入时间,几乎很没用。如果能够在文件写入系统时为文件记录时间就好了。
首先,我们需要在每次文件保存时调用我们的更新写入时间戳的代码,就像第二章所讲的那样,最好的方式是在一个hook变量里添加一个函数。为了找到合适的hook变量,使用M-xapropos RET hook RET,我们找到了四个hook变量:after-save-hook,local-write-file-hooks,write-contents-hooks和write-file-hooks。
我们可以立即抛弃after-save-hook,因为顾名思义,我们并不想在文件保存后再执行我们的代码。
剩下的候选项的差别很微妙:
-
write-file-hooks
- 任何buffer的每一次保存都会执行代码。 local-write-file-hooks
- 一种本地buffer的write-file-hooks。与write-file-hooks 关注每个buffer的变化情况不同,local-write-file-hooks只针对单独的buffer,所 以,如果你想在保存一个lisp文件时执行一种函数,而在保存txt文件时执行另外一个 函数,那么就该使用local-write-file-hooks。 write-contents-hooks
- 就像local-write-file-hooks那样,这个hooks是针对本地 buffer的,而且在每次buffer保存的时候执行代码。然而,write-contents-hooks中 的函数针对的是buffer的内容,而其他两个hook是针对的是文件是否已编辑。在实践 中,这个意思就是如果你改变了buffer的major mode,那么也就意味着你改变了buffer内容 的理解方式,然后write-contents-hooks返回nil但是local-write-file-hooks并不。 此外,如果你改变Emacs查看的文件,比如键入 set-visited-file-name ,然后 local-write-file-hooks会返回nil,而write-contents-hooks并不会。
于是乎,我们排除了write-file-hooks,此外,我们也排除了write-contents-hooks,因为我们想我们选择的hook对major mode改变的现象免疫。那么就剩下local-write-file-hooks了。
好了,下面得写我们的函数了。这个函数的基本能力应该是能够定位时间戳,删除它,然后写个新的时间戳,最直截了当的方式是让我们的时间戳有个很显著的特征:
WRITESTAMP((12:19pm 7 Jul 96))
那么更新函数可以写成:
(add-hook 'local-write-file-hooks 'update-writestamps) (defun update-writestamp () (interactive) (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (while (search-forward "WRITESTAMP((" nil t) (let ((start (point))) (search-forward "))") (delete-region start (- (point) 2)) (goto-char start) (insert-date)))))) nil)
tips:save-excursion保存光标的移动,save-restriction保存buffer的变窄变宽,save-match-data保存搜索的结果(不想因为我们的函数修改了全局的匹配的数据)。widen是撤销任何变窄的影响,使整个buffer都能在处理范围内。(search-forward"WRITESTAMP((" nil t")这里nil表名search的范围是没有边界的(到buffer的末尾),后面会细讲,t的意思是如果没有找到,那么search-forward应该简单地返回nil。(如果没有t,然后还找不到匹配项的话,search-forward会发出错误信号,强制退出当前的命令),如果搜索成功,point会移动到匹配项的后一个字符。search-forward就会返回那个位置。update-writestamp函数的返回值是save-excursion的返回值,为nil。Lisp函数的返回值是函数体的最后一个表达式的值,这里我们强制update-writestamp的返回值为nil,是因为在local-write-file-hooks中的函数返回值是特殊处理的。一般情况下,一个hook变量中的函数返回值是多少是不需要介意的,但是在local-write-file-hooks中的函数值中,一个非nil的返回值意味着,"这个hook函数已经完成了所有的事情了",也就是说如果hook函数返回非nil,在hook变量中的其余函数都不会被调用了,并且,Emacs也不会在hook函数运行结束后保存buffer到文件中。
一般化Writestamps
上面的函数确实让writestamps运行了,但是仍有一些问题,首先,"WRITESTAMP(("和"))"并不是所有的用户都喜欢,其次,用户可能不想用insert-date。
这些问题好解决,我们可以用三个新的变量:
(defvar writestamp-format "%C") (defvar writestamp-prefix "WRITESTAMP((") (defvar writestamp-suffix "))") (defun update-writestamp () (interactive) (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (while (search-forward writestamp-prefix nil t) (let ((start (point))) (search-forward writestamp-suffix) (delete-region start (match-beginning 0));;search完毕后,匹配第一个 (goto-char start) (insert (format-time-string writestamp-format (current-time)))))))) nil)
接下来讲讲正则表达式,可以参见http://dsec.pku.edu.cn/~rli/WiKi/EmacsRegexp.html
既然使用了正则表达式,那么就有使用了正则表达式搜索:re-search-forward,那么就有:
(re-search-forward (concat "^" writestamp-prefix) ... ) (re-search-forward (concat writestam-suffix "$") ... )
但是上述表达是有点问题的,因为可能用户的设定的writestamp-prefix或者writestamp-suffix可能也有关于正则的特殊字符,比如writestamp-suffix为'.',那么上述表达式实际上就是搜索行末为除了换行符以外的所有字符。所以我们得把用户设定的"魔幻字符"变为"朴素字符"。比如使用(regexp-quote ".")会返回"\\."。
所以新的update-writestamp函数为:
(defun update-writestamp () "Find writestamps and replace them with the current time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (while (re-search-forward (concat "^" (regexp-quote writestamp-prefix)) nil t) ...)))) nil)
下面我们要完成这个update-writestamp函数,那么re-search-forward之后,我们需要知道当前行以writestamp-suffix结尾。但是我们不能简单地写成:
(re-search-forward (concat (regexp-quote writestamp-suffix) "$"))
因为它能匹配很多行,而我们唯一感兴趣的是前缀匹配的当前行。一种做法是限制在当前行搜索,search-forward和re-search-forward的第二个可选参数就是干这个事情的,如果不是nil,那它就表示buffer的搜索截止位置,所以新的update-writestamps可以写成:
(defun update-writestamp () "Find writestamps and replace them with the current time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (while (re-search-forward (concat "^" (regexp-quote writestamp-prefix)) nil t) (let ((start (point))) (if (re-search-forward (concat (regexp-quote writestamp-suffix) "$") (save-excursion (end-of-line) (point)) t) (progn (delete-region start (match-beginning 0)) (goto-char start) (insert (format-time-string writestamp-format (current-time)))))))))) nil)
正则表达式的威力
实际上,找一个前缀,然后检查当前行是否能匹配后缀,并替换文本的这一系列操作可以减少到两个表达式:
(re-search-forward (concat "^" (regexp-quote wrx) "\\(.*\\)" (regexp-quote writestamp-suffix) "$")) (replace-match (format-time-string writestamp-format (current-time)) t t nil 1)
第二个表达式是replace-match,它能够根据上一轮的搜索结果决定替换一些还是所有的结果:
(replace-match new-string preserve-case literal base-string subexpression)
第一个参数是一个要插入的新的字符串,剩下的参数都是可选的:
-
preserve-case
- 我们设置为t,为了让replace-match保持大小写敏感。 literal
- 设置为t,意味着逐字对待new-string,如果为nil,replace-match会使用特 殊的语法解释new-string(参见describe-function replace-match)。 base-string
- 置为nil,意味着"修改当前buffer",如果该值为一个字符串,那么 replace-match会在这个串里进行替换,而不是整个buffer。 subexpression
- 置为1,意味着替换第一个,而不是所有的匹配的\\( \\)。
所以,update-writestamps被修改为:
(defun update-writestamps () "Find writestamps and replace them with the current time." (save-excursion (save-restriction (save-match-data (widen) (goto-char (point-min)) (let ((regexp (concat "^" (regexp-quote writestamp-prefix) "\\(.*\\) " (regexp-quote writestamp-suffix) "$"))) (while (re-search-forward regexp nil t) (replace-match (format-time-string writestamp-format (current-time)) t t nil 1)))))) nil)
(add-hook 'after-change-functions remember-change-time nil t)
nil为占位符,t表示只改变当前本地buffer的一份after-change-functions的拷贝。
(defvar last-change-time nil) (make-variable-buffer-local 'last-change-time)
使last-change-time称为本地buffer的变量。
(defun foo (a b &rest c) ................)
&rest参数使得所有的后继变量都存在变量c里。比如调用(foo 1 2 3 4),那么a为1,b为2,c为(3 4)列表。