论坛首页 综合技术论坛

Programming in Emacs Lisp笔记(五)一些更复杂的函数

浏览 3262 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-27  

一些更复杂的函数

copy-to-buffer的函数定义

这个函数拷贝文本到缓冲区,但它不是追加到第二个缓冲区,而是替换第二个缓冲区之前的文本。copy-to-buffer函数与append-to-buffer代码很类似,但它使用了erase-buffer和二个save-excursion。

该函数的函数体如下:

...
(interactive "BCopy to buffer: \nr")
(let ((oldbuf (current-buffer)))
(save-excursion
(set-buffer (get-buffer-create buffer))
(erase-buffer)
(save-excursion
(insert-buffer-substring oldbuf start end)))))
代码与append-to-buffer类似:不同处在于,改变buffer后append-to-buffer添加文本到缓冲区;而copy-to- buffer函数先删除缓冲区的内容。在删除之前的缓冲区的内容后,第二次使用了save-excursion,并且插入了新的文本。

为什么需要执行save-excursion两次?

单独提取copy-to-buffer函数体如下:

(let (bind-oldbuf-to-value-of-current-buffer)
(save-excursion ; First use of save-excursion.
change-buffer
(erase-buffer)
(save-excursion ; Second use of save-excursion.
insert-substring-from-oldbuf-into-buffer)))

第一个save-excursion让Emacs返回被复制文本的缓冲区。很清楚,这与append-to-buffer函数中的使用是一致的。为 什么要使用第二个save-excursion呢?原因在于insert-buffer-substring总是将point设置在被插入的区块 (region)的结束位置。第二个save-excursion将使用Emacs将point设置在被插入区块的开始位置。多数情况下,用户喜欢看到 point停留在被插入文本的开始位置。(copy-to-buffer函数将返回用户最初所在的缓冲区,当用户切换到拷贝的目标缓冲区时,point停 留在缓冲区开始的位置)。

insert-buffer的函数定义

与append-to-buffer和copy-to-buffer相反,这个命令拷贝另一个缓冲区到当前缓冲区。

insert-buffer的代码

(defun insert-buffer (buffer)
"Insert after point the contents of BUFFER.
Puts mark after the inserted text.
BUFFER may be a buffer or a buffer name."
(interactive "*bInsert buffer: ")
(or (bufferp buffer)
(setq buffer (get-buffer buffer)))
(let (start end newmark)
(save-excursion
(save-excursion
(set-buffer buffer)
(setq start (point-min) end (point-max)))
(insert-buffer-substring buffer start end)
(setq newmark (point)))
(push-mark newmark)))

insert-buffer中的交互

insert-buffer中的interactive有两个部分,*号和bInsert buffer:

只读缓冲区

星号用于只读缓冲区。如果insert-buffer是在一个只读缓冲区上被调用,提示信息将在回显区显示提示不允许插入到当前的缓冲区。星号不需要使用\n与下一个参数分隔。

交互表达式b

交互表达式的第二个参数是小写b开头的(append-to-buffer中是大写的B)。小写b告诉Lisp解释器,insert-buffer 需要一个已存在的缓冲区或者已存在的缓冲区名称作为参数。(大写的B可以使用一个不存在的缓冲区)Emacs将提示输入缓冲区名称,并提供了默认的缓冲 区,输入时可以使用自动完成功能。如果缓冲区不存在,将给出"No match"的提示。

insert-buffer函数体

insert-buffer函数有两个主要部分:or语句和let语句。or语句用于确保参数buffer参数不仅仅只是被绑定到缓冲区的名字上。let语句包含复制其它缓冲区到当前缓冲区的代码。

(defun insert-buffer (buffer)
"documentation..."
(interactive "*bInsert buffer: ")
(or ...
...
(let (varlist)
body-of-let... )
要明白or如何确保参数buffer不只是被绑定到缓冲区名称上,先要清楚or函数。

在insert-buffer用if替代or

主要工作在于确保buffer变量值是一个缓冲区,而不是缓冲区的名字。如果变量值是名字,则需要获取对对应的缓冲区。

通过if来实现:如果没有获取到buffer就获取它。

这里使用了bufferp函数,这个函数检查参数是否为一个缓冲区(或者缓冲区的名字),我们可以如下编码:

(if (not (bufferp buffer))              ; if-part
(setq buffer (get-buffer buffer))) ; then-part
前面说过bufferp中的字符p是一个约定的函数描述,它意味着函数用于决定某些属性为true或false。这里bufferp就是用于检查参数是否为一个缓冲区。

not函数用于取逻辑值的反值。

当buffer参数不是一个缓冲区但它是一个缓冲区名称时,true-or-false-test返回true。这时(set q buffer (get-buffer buffer))被执行。语句使用get-buffer函数获取缓冲区名称所对应的缓冲区。setq将buffer绑定到缓冲区上。

函数体中的or

insert-buffer函数中使用or语句的目的在于确保buffer被绑定到缓冲区。上一节用if实现了这个功能。但在insert-buffer函数中实际使用的却是or函数。

or函数可以接收任何意数量的参数。它依次对每个参数求值并返回第一个结果不为nil的值。or并不会对第一个返回值不为nil的参数的后面的参数求值。

or语句如下:

(or (bufferp buffer)
(setq buffer (get-buffer buffer)))
该语句中or的第一个参数为(bufferp buffer)。如果buffer参数是一个缓冲区则返回true(一个非nil值)。在or语句中,这种情况下or将返回true,并且不执行后面的语句。

如果(bufferp buffer)返回值为nil,即buffer是一个缓冲区的名字,Lisp解释器将执行or语句的下一个元素:(setq buffer (get-buffer buffer))。这个语句将返回一个非nil值,这个值为绑定到buffer变量上的缓冲区而不是缓冲区的名字。

使用or的情况:

(or (holding-on-to-guest) (find-and-take-arm-of-guest))

insert-buffer中的let语句

确保了buffer变量绑定到缓冲区后,insert-buffer函数中接下来是一个let语句。它设置了3个局部变量start、end和newmark并初始化为nil。这些变量是let语句中的临时变量。

let语句体包含了两个save-excursion语句。内部的那个save-excursion如下:

(save-excursion
(set-buffer buffer)
(setq start (point-min) end (point-max)))
(set-buffer buffer)将将当前缓冲区设置为将要复制文本的缓冲区。在那个缓冲区中将start和end分别设置为缓冲区开始位置和结束位置。这里可以看到setq可以在一个语句中设置多个变量。第一个参数值设置为第二个参数,第三个参数值为第四个参数。

外部的那个save-excursion表达式结构如下:

(save-excursion
(inner-save-excursion-expression
(go-to-new-buffer-and-set-start-and-end)
(insert-buffer-substring buffer start end)
(setq newmark (point)))

insert-buffer-substring函数从原缓冲区中把start和end所定义的区域中的文本拷贝到buffer中。第二个缓冲区所 有内容都处于start和end之间,因此整个缓冲区都将被拷贝到当前编辑的缓冲区中。这时,point位于被插入的文本的结束位置,被保存到 newmark变量中。

在执行外部的save-excursion语句后,point和mark将回到原来的位置。

然而,合适的mark位置应该被设置在被插入的文本块的结束位置,而point应当被设置在这个文本块的开始位置。newmark记录了被插入文本 的结束位置。let语句的最后一行(push-mark newmark)语句将mark设置到了那个位置。(前一个mark仍然可以访问,它被保存在mark ring里面,可以用C-u C-<spc>回到那个位置)。同时,point被设置到被插入文本的开始位置,这也是函数调用前point所在的位置。</spc>

完整的beginning-of-buffer函数的定义

前面讨论过"简化版的beginning-of-buffer函数定义"。在那个版本中,调用的时候没有传递参数。 Emacs中的beginning-of-buffer将光标移到缓冲区开始位置,并将mark设置在之前光标所在位置。调用的时候可以传递1-10之间 的数字给这个命令,函数将把参数当作移动的百分比:整个缓冲区当作10份,C-u 7 M-<将跳转到整个缓冲区70%的位置。M-<将中跳转到缓冲区的开始位置。如果传递的参数大于10,则将移动到缓冲区的结束位置。

beginning-of-buffer的参数是可选的,可以不带参数调用。

可选参数

在调用需要参数的函数时,如果没有设置参数Lisp解释器将报错:Wrong number of arguments。

但Lisp提供了可选参数的功能:用&optional(&是这个关键字的一部分)关键字告诉解释器参数是可选的,如果参数跟在&optional的后面,则在调用函数时可以不传递这个参数

beginning-of-buffer函数定义的第一行:

(defun beginning-of-buffer (&optional arg)
整个函数看起来如下:
(defun beginning-of-buffer (&optional arg)
"documentation..."
(interactive "P")
(push-mark)
(goto-char
(if-there-is-an-argument
figure-out-where-to-go
else-go-to
(point-min))))

整个函数与simplified-beginning-of-buffer函数类似,除了interactive语句用了"P"参数和goto-char函数跟了一个用于在传递了参数时计算光标位置if-then-else语句。

"P" interactive语句告诉Emacs传递一个前缀参数,这个参数来自于按键前输入的数字。或者输入C-u时输入的数字。(如果不输入数字,C-u缺省为4)

if语句部分比较简单:如果参数arg的值不为nil,即调用beginning-of-buffer时有带参数,则true-or-false- test返回true,if语句的then部分将被执行。如果beginning-of-buffer调用时没有带参数则if语句将被执行。else部分 (goto-char (point-min))被执行。

带参数执行beginning-of-buffer

当带参数执行时,用于计算传递给goto-char的参数值的语句被执行。这个语句初看起来比较复杂。它内部包含了一个if语句和更多的数学计算。

(if (> (buffer-size) 10000)
;; Avoid overflow for large buffer sizes!
(* (prefix-numeric-value arg) (/ (buffer-size) 10))
(/
(+ 10
(*
(buffer-size) (prefix-numeric-value arg))) 10))
解开beginning-of-buffer

解开上面的条件语句如下:

(if (buffer-is-large
divide-buffer-size-by-10-and-multiply-by-arg
else-use-alternate-calculation
if语句检查缓冲区的大小。这样做主要是由于在由的Emacs 18版本中计算出来的数字不允许大于8百万,Emacs怕缓冲区太大,后面的计算结果超过上限而溢出。在Emacs 21中使用大数字,但这个代码没被改动。
缓冲区很大时的情况

在beginning-of-buffer中,内部的if语句为了检查缓冲区是否大于1000个字符使用了>函数和buffer-size函数。

(if (> (buffer-size) 10000)
如果超过了if语句的then部分将执行:
(*
(prefix-numeric-value arg)
(/ (buffer-size) 10))
语句使用*函数将两个参数相乘。

第一个参数(prefix-number-value arg)。当使用"P"作为interactive时,传递给函数的参数值是一个"raw prefix argument",不是一个数字(是一个包含了一个数字的列表)。为了执行数字运行,需要通过prefix-number-value来做转换。

第二个参数是(/ (buffer-size) 10)。这个语句将数值与十相除。这计算出了缓冲区中1/10有多少个字符。

在整个相乘的语句如下:

(* numeric-value-of-prefix-arg
number-of-characters-in-one-tenth-of-the-buffer)
如果传递的参数是7,计算出的位置就是缓冲区70%的位置。

缓冲区很大的时候,goto-char语句的情况:

(goto-char (* (prefix-numeric-value arg)
(/ (buffer-size) 10)))
缓冲区较小时的情况

如果缓冲区包含的字符数量小于10000,计算上有些不同。也许你会认为这没有必要,因为第一个计算方式(大于10000时的情况)也能工作。然而在小型缓冲区中,第一种方法不能将光标放在需要位置;第二种方法则工作得好一些:

(/ (+ 10 (* (buffer-size) (prefix-numeric-value arg))) 10))
格式化后看得更清楚一些:
(/
(+ 10
(*
(buffer-size)
(prefix-numeric-value arg)))
10))
看最内部的括号(prefix-numberic-value arg),它将raw argument转换为数字。然后将数字与缓冲区大小相乘。 <src lang="lisp"></src> 这个操作将得到一个大于缓冲区几倍的数字。然后用这个数字加上10最后再除以10得到一个大于百分比位置的值。

这个结果被传递给goto-char将光标移到那个点。

beginning-of-buffer的完整代码

(defun beginning-of-buffer (&optional arg)
"Move point to the beginning of the buffer;
leave mark at previous position.
With arg N, put point N/10 of the way
from the true beginning.
Don't use this in Lisp programs!
\(goto-char (point-min)) is faster
and does not set the mark."
(interactive "P")
(push-mark)
(goto-char
(if arg
(if (> (buffer-size) 10000)
;; Avoid overflow for large buffer sizes!
(* (prefix-numeric-value arg)
(/ (buffer-size) 10))
(/ (+ 10 (* (buffer-size)
(prefix-numeric-value arg)))
10))
(point-min)))
(if arg (forward-line 1)))

代码中的文档字符串中使用了一个语句:

\(goto-char (point-min))
语句第一个括号前的\告诉Lisp解释器应该打印这个表达式而不是对它求值。

beginning-of-buffer的最后一行代码:如果执行命令时带了参数,则移动point到下一行的起始位置。

(if arg (forward-line 1)))
这行代码将光标移动到了计算位置的下一行的起始位置。(这行代码并非必要的,只是为了看起来更好)

回顾

  • or

依次执行各个参数直到遇到一个返回值不为nil的值。如果没有返回值为nil的参数,则返回nil,否则返回第一个返回值不为nil的值。简单来说就是:返回参数中第一个为true的值。

  • and

依次执行各个参数,直到遇到返回值为nil时,返回nil;如果没有nil则返回最后一个参数的值。简单来说就是:如果所有参数都为true就返回true;

  • &optional

这个关键字用于标明函数定义中的参数是可选的参数。意味着调用函数时可以不传递这个参数。

  • prefix-numeric-value

将从(interactive "P")获取到的"raw prefix argument'转换为数字。

  • forward-line

将point移到下一行的行首,如果参数大于1,则向下移动多行。如果不能移动那么多行,则forward-line尽量移动到能到达的位置,并返回没有进行操作的多余次数。

  • erase-buffer

删除当前整个缓冲区中的内容。

  • bufferp

如果参数是一个缓冲区,则返回t,否则返回nil。

论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics