Programming in Emacs Lisp笔记(八)剪切和存储文本


Programming in Emacs Lisp笔记(八)剪切和存储文本
2010年06月21日
  剪切和存储文本
  当使用'kill'命令剪切文本时,Emacs将它存储到一个列表中,可以用'yank'命令重新获取到。
  存储文本到列表
  当文本被剪切出缓冲区时,它将被存储到一个list中。文本块连续的存储在list中,这个列表看如下面的形式:  函数cons可以添加文本块到list,如:
  (cons "another piece"      '("a piece of text" "previous piece"))执行上面的语句,回显区将显示 
  ("another piece" "a piece of text" "previous piece")使用car和nthcdr函数,可以获取到list中任意的一个文本块。。例: 
  (car (nthcdr 1 '("another piece"                 "a piece of text"                 "previous piece")))     => "a piece of text"当然,Emacs中实际处理这些时更复杂一些。Emacs中编写的剪切函数能猜想出你需要的是list的哪个元素。
  包含这些文本块的list被称作kill ring。
  zap-to-char
  完整的zap-to-char实现
  这个函数将移除光标和指定的字符之间的文本。被移除的文本被放入kill ring中,可以用C-y(yank)获取到。如果命令带了数字前缀参数n(C-u),它将移除当前光标位置至遇到的第n个字符之间的文本。
  如果指定的字符不存在,zap-to-char将显示"Search failed"。
  为了决定要移除多少文本,zap-to-char使用了search函数。搜索在文本处理代码中使用得非常广泛。
  下面是zap-to-char在Emacs 19中的完整代码:
  (defun zap-to-char (arg char)  ; version 19 implementation  "Kill up to and including ARG'th occurrence of CHAR.Goes backward if ARG is negative; error if CHAR not found."  (interactive "*p\ncZap to char: ")  (kill-region (point)               (progn                 (search-forward                  (char-to-string char) nil nil arg)                 (point)))interactive语句
  zap-to-char的interactive语句如下:
  (interactive "*p\ncZap to char: ")引号中的部分"*p\ncZap to char: ",指定了3个不同的东西。第一,星号,如果当前缓冲区是只读缓冲区将产生一个错误信息。这意味着如果将zap-to-char用于只读缓冲将得到错误信息"Buffer is read-only"。 
  在Emacs21的实现中没有包含星号。函数与Emacs19中一样能工作,但在只读缓冲区中它不会移除文本,它将复制文本并将文本放到kill ring中。在这种情况下,两个版本中都将显示错误信息。
  在Emacs19中的实现也能从只读缓冲区中复制文本,这只是interactive的一个Bug。interactive的文档中说明了,星号将阻止zap-to-char函数对只读缓冲区做任何操作,这个函数不应该复制文本到kill ring中。
  在Emacs21中interactive的实现是正确的。因此星号不得不被移除。如果你在这个这个函数的定义中插入了星号,并重新执行函数定义,下次你再在只读缓冲区上运行zap-to-char时,将不能再复制文本到kill ring里。
  从这点来看,两个版本中的zap-to-char是一致的。
  "*p\ncZap to char: "中的第二个部分是p。这个部分与下一部分用\n分隔了。p表示参数应该是一个前缀参数'processed prefix',这个参数是用C-u加数字或者M-加数字传递的。如果调用时没有加参数,1将作为默认的参数值。
  "*p\ncZap to char: "中的第三个部分是"cZap to char: ",小写的c指定了参数必须是一个字符。c后面的字符串Zap to char: 是提示字符串。
  zap-to-char的函数体
  zap-to-char函数体包含kill当前光标位置至指定字符之间文本的代码。代码的第一部分如下:
  (kill-region (point) ...(point)是光标的当前位置 
  代码的下一个部分是一个progn语句。progn的body部分由search-forward和point组成。
  在学习完search-forward后,很容易懂progn。
  search-forward函数
  search-forward函数被用于定位字符(zapped-for-character)。如果查找成功, search-forward会将point设置在要查找的目标字符串的最后一个字符的后面。(zap-to-char中目标字符串只有一个字符)如果是 向后查找,则search-forward会将point设置在查找目标字符串第一个字符的前面。查找成功后,search-forward将返回t。
  在zap-to-char中,search-forward函数部分如下:
  (search-forward (char-to-string char) nil nil arg)search-forward函数包含四个参数: 
  第一个参数是要查询目标,必须是一个字符串,比如"z"。 
  传递给zap-to-char是一个字符。Lisp解释器对字符串和字符的处理是不同的。因为search-forward函数查询的是一个字符 串,传递给zap-to-char函数接收到的是一个字符,因此参数必须被转换为字符串,否则search-forward将报错。char-to- string用于处理这种转换。
  第二个参数限制查询的范围;它是一个缓冲区位置。在这里,可以查询到缓冲区的结束位置,因此第二个参数为nil。 
  第三个参数告诉函数如果查询失败该如何做:比如打印错误信息或者返回nil。第三个参数为nil将在查询失败时显示错误信息。 
  search-forward的第四个参数用于指定重复查询的次数。这个参数是可选,如果没有传递,则默认为1.如果参数是一个负数,查询将向后查询。 
  使用search-forward语句的模板:
  (search-forward "target-string"                limit-of-search                what-to-do-if-search-fails                repeat-count)progn
  progn是一个特殊的form。它使传递给它的参数依次被执行,并返回最后一个值。前面部分只是被执行,它们的返回值被丢弃。
  progn语句的模板:
  (progn  body...)zap-to-char中的progn语句做了两件事:将point设置到正确的位置;返回point的位置以便kill-region知道要操作的范围。 
  progn的第一个参数是search-forward。当search-forward找到了字符串,它会将point设置在查找目标字符串的最 后一个字符的后面。(这里目标字符串只有一个字符长)如果是向后查找,search-forward会将poing设置在查找目标的第一个字符的前面。 point的移动是side effect(单方面的,不影响界面)。
  progn的第二个参数是表达式(point)。这个表达式返回point的值,即search-forward设置的那个值。这个值被作为progn语句的返回值将作为kill-region的第二个参数传递给kill-region函数。
  zap-to-char的总结
  前面了解了search-forward和progn是如何工作的,我们可以看到整个zap-to-char函数是如何工作的。
  kill-region的第一个参数是执行zap-to-char命令时的光标位置。在progn的内部,查找函数将poing移动到要查找目标 (zapped-to-character)的后面。kill-region函数将这两个point中的第一个作为操作区域(region)的开始位置, 第二个参数作为结束位置,然后移除这个区域。
  progn是必需的,因为kill-region命令需要两个参数;如果把search-forward和point语句直接作为kill-region的参数将报错。progn语句是一个单独的参数,它的返回值将作为传递给kill-region的第二个参数。
  kill-region
  zap-to-char函数使用了kill-region函数。函数将从一个region中clip文本到kill ring中。
  在Emacs 21中这个函数使用了condition-case和copy-region-as-kill,这两个函数都将在后面解释,confition-case是一个特别重要的form。
  实际上,kill-region函数调用了condition-case,它需要3个参数。第一个参数不做什么,第二个参数包含了正常工作时需要执行的代码。第三个参数包含了出错时需要执行的代码。
  完整的kill-region定义
  下面将介绍condition-case。首先来看kill-region的完整定义:
  (defun kill-region (beg end)  "Kill between point and mark.The text is deleted but saved in the kill ring."  (interactive "r")  ;; 1. `condition-case' takes three arguments.  ;;    If the first argument is nil, as it is here,  ;;    information about the error signal is not  ;;    stored for use by another function.  (condition-case nil      ;; 2. The second argument to `condition-case'      ;;    tells the Lisp interpreter what to do when all goes well.      ;;    The `delete-and-extract-region' function usually does the      ;;    work.  If the beginning and ending of the region are both      ;;    the same, then the variable `string' will be empty, or nil      (let ((string (delete-and-extract-region beg end)))        ;; `when' is an `if' clause that cannot take an `else-part'.        ;; Emacs normally sets the value of `last-command' to the        ;; previous command.        ;; `kill-append' concatenates the new string and the old.        ;; `kill-new' inserts text into a new item in the kill ring.        (when string          (if (eq last-command 'kill-region)              ;; if true, prepend string              (kill-append string (Read only text copied to kill ring")       ;; or else, signal an error if the buffer is read-only;       (barf-if-buffer-read-only)       ;; and, in any case, signal that the text is read-only.       (signal 'text-read-only (list (current-buffer)))))))condition-case
  前面说过,当Emacs Lisp解释器在执行语句发生错误时,它将提供帮助信息,这被称为"signaling a error"。通常,程序将停止执行并显示错误信息。
  然而在一些复杂的情况下。程序不应该在出错的时候只是简单的停止程序执行。在kill-region函数中,一个典型的错误是,如果在只读缓冲区中 删除文本时,文本将不会被删除。因此kill-region函数包含了处理这种情况的代码。这些代码在kill-region函数中condition- case语句的内部。
  condition-case的模板如下:
  (condition-case  var  bodyform  error-handler...)如果没有发生错误,解释器将执行bodyform语句。 
  错误发生时,函数将产生错误信息,定义一个或者多个错误条件名称(condition name)。
  condition-case的第三个参数是一个错误处理器。一个错误处理器包含了两个部分,一个condition-name和一个body。如 果错误处理器的condition-name与发生错误时的condition-name匹配,错误处理器的body部分将执行。
  错误处理器中的错误条件名称(condition-name)可以是一个单一的condition name也可以是包含多个condition name的list。
  condition-case语句可以包含一个或多个错误处理器。当错误发生时,第一个被匹配的处理器被执行。
  最后,condition-case语句的第一个参数var,有时被绑定到包含错误信息的变量上。如果它为nil,比如在kill-region中,错误消息将被丢弃。
  简单来说,在kill-region函数中,condition-case的工作如下:
  If no errors, run only this code    but, if errors, run this other code.delete-and-extract-region
  一个condition-case语句有二个部分,一个是正常时执行的,但它有可能会产生错误。另一个部分用于出错时执行。
  先来看kill-region中正常运行的代码:
  (let ((string (delete-and-extract-region beg end)))  (when string    (if (eq last-command 'kill-region)        (kill-append string (不能移除时,它给出错误信号)
  这里的let语句将delete-and-extract-region的返回值赋给局部变量string中。这也就是从缓冲区中删除的文本。
  如果变量string指向了文本,那些文本就被添加到kill ring,如果变量值为nil则表示没有文本被删除。
  这里使用了when来检查变量string是否指向了文本块。when语句是程序员的一种简便写法。when语句是没有else部分的if语句。可以把when理解为if。
  技术上来说,when是一个Lisp宏。Lisp宏允许你定义新的控制结构和其它语言功能。它告诉解释器如何计算另一个Lisp语句的值,并返回计算的结果。这里的'另一个表达式'就是一个if表达式。C语言里也提供了宏。但这是不同的,但它们同样很有用。
  如果string变量有内容,另一个条件表达式被执行。这是一个包含了then部分和else部分的if语句。
  (if (eq last-command 'kill-region)    (kill-append string (不能恢复。
  与其它代码不同,delete-and-extract-region不是用Emacs Lisp编写的;它是用C编写的,这也是Emacs的一个基础系统。
  与其它Emacs原生函数一样,delete-and-extract=region是C宏,宏是一个代码模板。完整宏如下:
  DEFUN ("delete-and-extract-region", Fdelete_and_extract_region,       Sdelete_and_extract_region, 2, 2, 0,  "Delete the text between START and END and return it.")  (start, end)     Lisp_Object start, end;{  validate_region (&start, &end);  return del_range_1 (XINT (start), XINT (end), 1, 1);}DEFUN与Lisp中的defun是同样的用途。DEFUN后面括号中有七个部分: 
  第一个部分给出了Lisp函数的名称,delete-and-extract-region 
  第二部分是C函数的名称,Fdelete_and_extract_region。习惯上以F开头。因为C不能在函数名中使用连字符,因此用下划线替代了。 
  第三个部分是记录了供函数内部使用的信息的C常量结构。它的名称与C函数名一致但它以S开头。 
  第四和第五个部分指定了最小和最大的参数个数。这个函数需要2个参数。 
  第六部分与Lisp编写的函数中的交互式语句类似:一个字符后跟着可选的提示信息。两者不同之处在于Lisp没有参数时不需要写参数。在这个宏里需要写成0(null string)。 
  第七个部分是文档字符串与Lisp编写的函数中的相同。不同之处在于换行时,需要在\n后面添加一个反斜线并添加回车。 
  因此,goto-char的文档字符串的前两行如下:
  "Set point to POSITION, a number or marker.\n\      Beginning of buffer is position (point-min), end is (point-max).在C宏中,紧接在后面是正式的参数,和参数类型语句,接下来就是宏的'body'部分。delete-and-extract-region的'body'包含了两行: validate_region (&start, &end);return del_range_1 (XINT (start), XINT (end), 1, 1);第一个函数validate_region检查传递的区域起始位置和结束位置是否是在规定的范围内,检查参数类型是否正确。第二个函数del_range_1,执行删除文本的操作。 
  del_range_1是一个复杂的函数我们不深入研究。它修改缓冲区并执行其它操作。
  传递给del_range的两个参数XINT (start) and XINT (end)值得研究一下。
  C语言中,start和end是标记了被删除区域的开始位置和结束位置的两个整数。
  早期版本的Emacs中,这两个数字是32bits长,但这个代码运行比较慢。三个bit被用于指定类型信息,四个bit被用于处理内存;其它bits被作为'content'。
  XINT是一个C宏它从bits集合中解析出相关的数字;4个bits被丢弃。
  delete-and-extract-region命令看起来如下:
  del_range_1 (XINT (start), XINT (end), 1, 1);它删除start和end之间的region。
  从这点来看Emacs Lisp很简单;它隐藏了大量复杂的工作。
  使用用defvar初始化变量
  与delete-and-extract-region函数不同,copy-region-as-kill函数是用 Emacs Lisp编写的。它内部有两个函数kill-append和kill-new,复制缓冲区区域中的信息到变量kill-ring中。这节讨论kill- ring变量是如何被defvar创建和初始化的。
  在Emacs Lisp中kill-ring之类的变量是用defvar创建和初始化的。这个名称来源于"define variable"。
  defvar与setq设置变量类似。与setq不同的两点:第一,它只给未赋值的变量赋值,如果变量已经有值,defvar将不会覆盖已经存在的值。第二,defvar有一个文档字符串。
  (另一个特别的form是defcustom,被设计为可以让用户自定义。它比defvar有更多的功能。)
  查看变量的当前值
  可以使用describe-variable函数查看任何变量的当前值,通常可以用C-h v来调用。比如可以C-h v然后输入kill-ring将看到 当前kill ring的值,同时也能看到kill-ring的文档字符串:
  Documentation:List of killed text sequences.Since the kill ring is supposed to interact nicely with cut-and-pastefacilities offered by window systems, use of this variable shouldinteract nicely with `interprogram-cut-function' and`interprogram-paste-function'.  The functions `kill-new',`kill-append', and `current-kill' are supposed to implement thisinteraction; you may want to use them instead of manipulating the killring directly.kill ring是使用defvar按下面的方法定义的: 
  (defvar kill-ring nil  "List of killed text sequences....")这个变量定义中,变量初始化为nil。这意味着如果没有保存任何东西,使用yank时将不会获取到任何信息。文档字符串的写法与使用defun时的文档字 符串是一样的,文档字符串的第一行必须是一个完整的语句,因为一些命令,比如apropos只打印文档字符串的第一行。后面的行不应该使用缩进;否则如果 用C-h v(describe-variable)查看时将会混乱。 
  defvar时使用星号
  以前,Emacs使用defvar来定义希望被用户修改的变量和不希望被用户修改的变量。尽管你可以用defvar定义自定义变量,但是请使用defcustom来代替。
  当使用defvar设定变量时,可以在文档字符串的第一个位置添加*号来来区分变量是否为可以设值的变量。比如:
  (defvar shell-command-default-error-buffer nil  "*Buffer name for `shell-command' ... error output.... ")这表示你可以使用edit-options命令临时修改shell-command-default-error-buffer的值。 
  edit-options设置的值只在当前编辑会话中有用。新值并不会被保存。每次Emacs启动时它将读取原始值,除非你在.emacs文件中设定它。
  copy-region-as-kill
  这个函数从缓冲区中复制区域中的内容(使用kill-append或kill-new)并保存到kill-ring上。
  如果在调用kill-region后立即调用copy-region-as-kill,Emacs会将新的文本追加到前一个复制的文本中。这意味着 你使用yank时将得前面两次操作的所有文本。另一方面,如果在copy-region-as-kill之前执行了一些命令,则函数复制的文本块将不会放 在一起。
  完整的copy-region-as-kill函数定义
  下面是Emacs 21中copy-region-as-kill函数定义:
  (defun copy-region-as-kill (beg end)  "Save the region as if killed, but don't kill it.In Transient Mark mode, deactivate the mark.If `interprogram-cut-function' is non-nil, also savethe text for a window system cut and paste."  (interactive "r")  (if (eq last-command 'kill-region)      (kill-append (buffer-substring beg end) ( (length kill-ring) kill-ring-max)        (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))  (setq kill-ring-yank-pointer kill-ring)  (if interprogram-cut-function      (funcall interprogram-cut-function string (not replace))))先看下面的部分:
  (if (and replace kill-ring)      ;; then      (setcar kill-ring string)    ;; else    (setq kill-ring (cons string kill-ring))    (if (> (length kill-ring) kill-ring-max)        ;; avoid overly long kill ring        (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil)))  (setq kill-ring-yank-pointer kill-ring)  (if interprogram-cut-function      (funcall interprogram-cut-function string (not replace))))条件测试(and replace kill-ring),如果两个kill ring中有内容,并且replace变量为true则返回true。
  kill-append函数将replace设置为true;然后当kill ring至少有一个元素时,setcar语句被执行:
  (setcar kill-ring string)setcar函数将kill-ring的第一个元素修改为string的值。它替换了原来的元素。 
  如果kill ring为空,或者replace为false,则条件语句的else部分将执行:
  (setq kill-ring (cons string kill-ring))(if (> (length kill-ring) kill-ring-max)    (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil))语句先通过在原来的kill ring前添加新元素string,而构造了一个新的kill ring。然后执行了第二个if子句。第二个if子名防止了kill ring增长过大。 
  依次来看这两个语句。
  setq的这行将string添加到旧的kill ring组成的新list重新设置给kill-ring。
  第二个if子名,防止了kill ring增长得过长。
  (if (> (length kill-ring) kill-ring-max)    (setcdr (nthcdr (1- kill-ring-max) kill-ring) nil))这段代码检查kill ring的长度是否已经超过了允许的最大长度--kill-ring-max(默认为60)。如果kill ring过长,则将kill ring的最后一个元素设置为nil。执行这个操作使用了两个函数:nthcdr和setcdr。 
  setcdr设置list的CDR部分,setcar设置list的CAR部分。在这里,setcdr不会设置kill ring的CDR部分;nthcdr函数限制了设置CDR的位置。
  例:
  (setq trees '(maple oak pine birch))     => (maple oak pine birch)(setcdr (nthcdr 2 trees) nil)     => niltrees     => (maple oak pine)setcdr返回值为nil,是因为它设置的CDR是nil。 
  kill-new函数中的下一行语句是:
  (setq kill-ring-yank-pointer kill-ring)kill-ring-yank-pointer也是一个全局变量,它被设置为kill-ring。 
  尽管kill-ring-yank-pointer被称为pointer,实际上却是kill ring变量。但选用名字是为了帮助人们懂得这个变量起的作用。这个变量用于yank和yank-pop等函数。
  现在,回到函数的最前面的两行:
  (and (fboundp 'menu-bar-update-yank-menu)       (menu-bar-update-yank-menu string (and replace (car kill-ring))))这个语句第一个元素是函数and。 
  and将依次对每个参数求值只到某个参数返回值为nil,这种情况下and语句将返回nil;如果没有参数返回值为nil,返回值将是最后一个参数 的值。(这种情况下返回值不会为nil,在Emacs Lisp里可以作为true)。换言之,and语句只有在所有参数都返回true的情况下才返回true。
  在这里,语句测试了menu-bar-update-yank-menu是否是一个函数,如果是则调用它。如果测试的参数符号是一个函数定义而不是'is not void',则fboundp返回true,如果函数未定义则我们将得到错误信息。
  这个and和if语句效果如下:
  if the-menu-bar-function-exists  then execute-itmenu-bar-update-yank-menu函数允许用户使用'Select and Paste'菜单操作,并且可以在菜单上看到文本块。
  最后一个语句kill-new函数添加新的文本到窗口系统中,以便在不同的程序中进行复制粘贴操作。比如:在XWindow系统中x-select-text函数将文本存储在X系统操作的内存中,你可以在另一个程序中粘贴。
  语句结构如下:
  (if interprogram-cut-function      (funcall interprogram-cut-function string (not replace))))如果interprogram-cut-function存在,则Emacs执行funcall,它将第一个参数作为函数,并将其它参数传递给这个函数。
  回顾
  car 
  cdr 
  car返回list的第一个元素;cdr返回list中从第二个元素开始的list。
  例:
  (car '(1 2 3 4 5 6 7))     => 1(cdr '(1 2 3 4 5 6 7))     => (2 3 4 5 6 7)cons 
  cons将第一个参数添加到第二个参数前面。
  例:
  (cons 1 '(2 3 4))     => (1 2 3 4)nthcdr 
  返回对list求'n'次CDR的值。
  例:
  (nthcdr 3 '(1 2 3 4 5 6 7))     => (4 5 6 7)setcar 
  setcdr 
  setcar修改list中的第一个元素;setcdr修改list中第二个元素开始的list。
  例:
  (setq triple '(1 2 3))(setcar triple '37)triple     => (37 2 3)(setcdr triple '("foo" "bar"))triple     => (37 "foo" "bar")progn 
  依次执行各个参数并返回最后一个参数的值。 例: (progn 1 2 3 4)     => 4save-restriction 
  记录当前缓冲区的任何narrowing,在执行完它的参数后,恢复narrowing。
  search-forward 
  查找字符串,如果找到则将point设置到那个位置。
  它接收4个参数:
  要查找的字符串 
  可选参数,是一个缓冲区位置,它用于限制查询范围 
  可选参数,查询失败执行的代码,返回nil或者显示错误信息 
  查询的次数,如果为负数则向前查找 
  kill-region 
  delete-region 
  copy-region-as-kill 
  kill-region剪切point和mark间的文本到kill ring上,可以用yanking恢复。 
  delete-and-extract-region移除point和mark间的文本并丢弃。不能恢复。 
  copy-region-as-kill复制point和mark间的文本到kill ring,可以用yanking恢复。这个函数并不移除原来的文本。

猜你喜欢

转载自xluuv11o.iteye.com/blog/1362489