Skip to content

Latest commit

 

History

History
302 lines (227 loc) · 22.8 KB

1.org

File metadata and controls

302 lines (227 loc) · 22.8 KB

自定义Emacs

在本章:

Backspace和Delete Lisp 按键和字符串 C-h绑定到什么 C-h应该绑定到什么 执行Lisp表达式 Apropos

本章将介绍基本的Emacs定制化,并且在这一过程中教给你一些Emacs Lisp。最简单、最常见的定制之一就是将一个按键的命令复制到另一个按键上。可能你不喜欢Emacs的两次按键(C-x C-s)来保存文件,因为其他的编辑器通常都只是简单的C-s。或者你可能只是想按C-x,却不小心按成C-x C-c,也就是退出Emacs,而你希望按下C-x C-c不要造成这么酷炫的效果。或者也可能,就像下面的例子所展示的,你可能希望对Emacs提供给你的键位做出一些自己的修改。

Backspace 和 Delete

想象你想要输入“Lisp”但却输成了“List”。要改正你的拼写,你是按下BACKSPACE键呢还是DELETE键?

这当然由你的键盘而定,但是我要问的并不仅仅是一个按键上标记了什么的问题。有时按键上标记的是“Backspace”,有时它又被标记为“Delete”,有时又是“Erase”,有时是一个向左的箭头或是别的什么图案。对于Emacs来说,按键上标记着什么并不重要,它在乎的是按下这个键时所触发的数字代码。“向左移动并且删掉前面的字符”可能会产生一个ASCII的“backspace”编码(十进制8,通常表示为BS)或者一个ASCII的“delete”编码(十进制127,通常表示为DEL)。

在Emacs的默认配置中,认为只有DEL表示“向左移动并且删掉前面的字符”。如果你的BACKSPACE/DELETE/ERASE键产生了一个BS,那么它将不会按照你所希望的那样执行。

更糟的是当你按下BACKSPACE/DELETE/ERASE键,它却产生了一个BS时。Emacs认为既然BS并不用来左移并删除前面的字符,那么它就可以用来触发另一个方法。结果是,BS现在触发的方法和按下C-h时触发的一样。如果你们那里并不需要C-h来执行左移并删除前面的字符,那么C-h更好的选择是作为一个Help键,而这也是Emacs的选择。不幸的是,这意味着如果你的BACKSPACE/DELETE/ERASE触发BS的话,那么按下它将不会触发backspace,delete或者erase;它将触发Emacs的在线帮助。

当Emacs的初学者想要修正一个拼写错误时,他们不止一次的被Emacs做出了热烈的欢迎。突然一个新的Emacs窗口–帮助窗口–弹了出来,提示不幸的用户来选择一些帮助命令。帮助的内容如此的冗长使得用户更加的瞠目结舌。用户恐慌的按下一大堆的C-g(终止当前的操作),伴随着一大堆的终端错误铃声提示。怪不得许多聪明善良的用户选择继续使用安全无害的vi,而不是成为Emacs的疯狂传道者。我每当想起这件事就很伤心,特别是这一情形很容易被修复。Emacs启动时,它将读取并且执行你的根目录下的.emacs文件。它使用Emacs Lisp作为语言,而读完本书你会发现,只要编写一些Emacs Lisp放到.emacs里,Emacs几乎没有什么是你不能改变的。我们要关注的第一件事就是向.emacs中添加一些代码来把BS和DEL都指定为“向前退格并且删掉一个字符”,而将帮助命令移动到其他键上去。首先我们需要看一下.emacs文件所使用的语言,Lisp。

Lisp

自从1950年以来已经产生了很多种不同形式的Lisp。最初它应用于人工智能,它很胜任这份工作,因为它允许符号运算,可以将代码作为数据处理,可以简化复杂数据结构的构建。但是Lisp可以做的要比一门AI语言多得多。它应用于非常广的问题处理上,这经常被计算机科学家忽视,而Emacs用户却知道的很清楚。Lisp不同于其他编程语言的特性有:

  • 括号前缀表示法 Lisp中的所有表达式和方法都由括号括起来[1],方法名通常在参数之前。所以在其他语言中你通常会这么写:
    x + y
        

    而在Lisp中,你会这么写:

    (+ x y)
        

    “前缀表示法”表示运算符在运算对象的前面。当运算符在运算对象中间时,这称为“中缀表示法”。

    虽然与通常的习惯不同,但前缀表示法相对于中缀有一些好处。在中缀语言中,要将5个变量加起来需要4个加号:

    a + b + c + d + e
        

    Lisp更简洁:

    (+ a b c d e)
        

    而且,不会产生运算符的优先级问题。例如,

    3 + 4 * 5
        

    的结果是35还是23?这需要知道*是否比+具有更高的优先级。而在Lisp中,就不会有这种疑惑:

    (+ 3 (* 4 5)) ; 结果是23
    (* (+ 3 4) 5) ; 结果是35
        

    (Lisp中的注释使用分号,作用到行尾。)最后,中缀语言需要在方法中使用逗号分隔参数:

    foo(3 + 4, 5 + 6)
        

    Lisp不需要额外的语法:

    (foo (+ 3 4) (+ 5 6))
        
  • List数据类型 Lisp有一种内建的数据类型称为列表(list)。列表是一种用括号括起来的,不包含或者包含着其他Lisp对象的Lisp对象。下面是一些列表:
    (hello there) ;包含着两个“符号”的列表
    (1 2 "xyz") ;两个数字和一个字符串
    () ;空列表
        

    列表可以作为值赋给其他变量,作为参数传递给方法以及作为返回值传递,使用cons和append这种方法来进行组合,使用car和cdr来进行拆分。后面我们将会更详细地叙述这些知识。

  • 垃圾回收 Lisp是一种垃圾回收的语言,这意味着Lisp会自动的回收你的程序里的数据结构所使用的内存。与之相反的,比如C语言,程序员必须显式的使用malloc来分配内存,然后显式的使用free来释放内存。(在非垃圾回收语言里,malloc/free这种语句非常容易出错。过早的释放内存是全世界程序错误中最大的原因之一,而忘记释放内存则会造成内存的泄露。)

    除了所有这些垃圾回收机制所具有的有点,它也有一个缺点:Emacs会不时的停下正在做的所有事情,向用户显示“Garbage collecting…”。用户要等到垃圾回收结束才能继续使用Emacs[2]。这通常只会持续不到1s,但是却可能非常频繁。后面我们将会学到如何减少垃圾回收发生的实用技巧。

表达式(expression)通常表示Lisp代码中的任何一部分或者任何Lisp数据结构。所有Lisp表达式,不管是代码还是数据,都可以被Emacs中内建的Lisp解释器执行。对一个变量求值的结果就是访问之前储存在变量中的Lisp对象。就像我们下面将要看到的,用来执行Lisp函数的方式就是对一个列表求值。

自从Lisp发明以来已经产生了许多Lisp方言,它们之间各有不同。MacLisp, Scheme和Common Lisp是其中较为有名的。Emacs Lisp和它们都不一样。这本书只关注Emacs Lisp。

按键和字符串

本章的目的是使所有触发BS的键同触发DEL的键能一样的工作。当然这将导致C-h不再触发帮助命令。你需要选择其他的键来使用帮助;我自己的方式是使用META-question-mark。

META键

META键的工作方式和CONTROL键以及SHIFT键一样,都是需要在按下其他键的同时按着它。这种键被称为修饰键(modifiers)。虽然不是所有键盘都有META键。有时ALT键起着同样的作用,但是也不是所有键盘都有ALT键。无论如何,你都不是必须使用META或者ALT。单次按键META-x总是可以使用两键序列ESC x来替代。(注意ESC不是修饰键–你需要先按下ESC,松开手,再按下x键。)

将按键绑定到命令上

在Emacs里,每个按键都触发一条命令或者是一个触发命令的多键序列的一部分。就像我们将要看到的,命令是一种特殊的Lisp函数。使一个按键触发类似帮助这种命令的行为被称为绑定。我们需要执行一些Lisp代码来将按键绑定到命令上。global-set-key是一个用于做这件事的函数。

下面介绍如何调用global-set-key。记住在Lisp里函数调用就是简单的一个括起来的列表。第一个元素是函数名称,剩下的元素全是参数。函数global-set-key使用两个参数:要绑定的按键序列,以及要绑定的命令。

(global-set-key keysequence command)

需要注意Emacs Lisp是区分大小写的。

我们选择的按键序列是META-question-mark。这在Emacs Lisp中如何表示呢?

字符串表示按键

在Emacs Lisp中有一些不同的方式来表示一个按键序列。最简单的是直接使用字符串。在Lisp中,字符串是一些被引号括起来的字符序列。

"xyz" ; 三个字母的字符串

要在字符串中使用双引号,使用反斜杠(\):

"I said, \"Look out!\""

这表示如下字符串:

I said, "Look out!"

要在字符串中表示反斜杠需要使用另一个反斜杠对其转义。

普通的按键使用它所代表的字符来表示它。例如,按键q在Lisp中被字符串“q”所表示。而反斜杠\则写作“\”。

像META-question-mark这种特殊字符在字符串里使用特殊的标识符:“\M-?”来表示。虽然字符串里有四个字母,但Emacs会将此字符串读为META question-mark[3]。

在Emacs的术语中,M-x是META-x的简写,“\M-x”是字符串版本。CONTROL-x在Emacs文档中简写为C-x,在字符串中表示为“\C-x”。你也可以组合CONTROL和META键。CONTROL-META-x简写作C-M-x,字符串表示为“\C-\M-x”。顺便,”\C-\M-x”和”\M-\C-x”(META-CONTROL-x)等价。

(CONTROL-x在文档里有时也表示为^x,那么字符串就表示为”\^x”。)

现在我们知道了如何填写global-set-key的第一个参数:

(global-set-key "\M-?" command)

(另一种书写”\M-?”的方式是”\e?”。字符串“\e”表示escape,而M-x和Esc x等价。)

下面我们必须找出command需要填写什么。这个参数应该是我们希望M-?触发的帮助函数的名称,也就是当前C-h所触发的函数。在Lisp中,函数使用符号(symbols)来表示。符号就像其他语言中的函数名或者变量名,虽然Lisp在命名时比大多数语言都允许更宽泛的字符集。例如,合法的符号名包括let*以及up&down-p。

C-h绑定到什么

要找到帮助命令的符号,我们可以使用C-h b,这将会触发另一个名为describe-bindings的命令。这是帮助系统众多的命令之一。它会弹出一个列出所有有效键绑定的窗口。查找C-h,我们可以找到这一行:

C-h help-command

这告诉了我们help-command是指向帮助命令的符号。我们的Lisp示例即将完成了,但是我们不能只是写下

(global-set-key\M-?” help-command) ; 几乎对了!

这是错误的,因为符号只要出现在Lisp表达式里就会马上被解释执行。如果符号出现在列表的第一个位置时,那么它将作为函数的名称来执行。否则,它作为变量的值就要被展开。但是当我们运行global-set-key时,我们不需要help-command所包含的值,不管那是什么。我们需要的是help-command这个符号的本身。简而言之,我们希望在传递给global-set-key之前不要对符号进行求值。毕竟就我们所知,help-command并没有作为变量的值存在。

阻止符号(以及其他任何Lisp表达式)被求值的方法是在它的前面加一个单引号(’)进行引用(quoted)。就像这样:

(global-set-key "\M-?" 'help-command)

我们的Lisp例子现在完成了。如果你把它放到你的.emacs文件中,那么以后当你打开Emacs时M-?将会触发help-command。(马上我们将会学到如何立即触发Lisp表达式。)M-? b将会像C-h b一样触发describe-bindings(这时M-?和C-h都绑定到了help-command)。

顺便,为了说明引用和非引用的区别,下面两条表达式可以达成同样的效果:

(setq x 'help-command) ;  setq分配一个变量
(global-set-key "\M-?" x) ; 使用 x 的变量值

第一行使变量x保存符号help-command。第二行使用x的值–符号help-command–绑定给M-?。这个例子与上一个的唯一区别是你现在多使用了一个变量x。

符号并不是唯一可以被单引号前缀的;任何Lisp表达式都能被引用,包括列表,数字,字符串,以及其他我们后面将要学到的表达式。’expr是下面的简写:

(quote expr)

这在执行的时候会延缓求值(yield)。你可能已经注意到了符号help-command需要引用而字符串参数“\M-?”却不需要。这是因为在Lisp里,字符串是自解释的,当字符串被执行时,它返回的是它本身。所以对其进行引用是无害而多余的。数字,字符以及向量(vector)是其他自解释的Lisp表达式。

C-h应该绑定到什么?

既然我们已经将help-command绑定到M-?,下面我们需要给C-h绑定一些什么。使用前面所描述的同样的流程–也就是说,触发命令describe-bindings(使用C-h b或者M-? b)–我们发现DEL触发的命令是delete-backward-char。

所以我们可以这样写:

(global-set-key "\C-h" 'delete-backward-char)

现在DEL和C-h一样了。如果你把下面的命令放到.emacs里:

(global-set-key "\M-?" 'help-command)
(global-set-key "\C-h" 'delete-backward-char)

那么以后在Emacs里,BACKSPACE/DELETE/ERASE将会执行正确的事情,不管发出的是BS还是DEL。但是我们如何使他们马上产生效果呢?这需要显式执行(explicit evaluation)这两个表达式。

执行Lisp表达式

有几种方式来显式执行Lisp表达式。

  1. 你可以将Lisp表达式放到一个文件里,然后载入这个文件。假设你把表达式放到文件rebind.el里。(Emacs Lisp文件的后缀名是.el)。你可以键入M-x load-file RET rebind.el RET以使Emacs来执行文件的内容。如果你把这些内容放到了.emacs里,你可以使用同样的方法来载入它。但是在你使用了Emacs一段时间后,你的.emacs将会变得越来越大,它的载入将会变得很慢。因此,你不会希望为了一点点改动就重新载入整个文件。因此我们可以使用下一种选择。
  2. 你可以使用命令eval-last-sexp,这绑定到了[4]C-x C-e上。(sexp[5]是S表达式(S-expression)的简写,也就是符号表达式的简写,也就是Lisp表达式的另一种说法。)这个命令将执行光标左边的Lisp表达式。所以你要做的是将光标放到第一行的末尾:


    (global-set-key "\M-?" 'help-command) |
    (global-set-key "\C-h" 'delete-backward-char)
        

    

然后按下C-x C-e;然后移动到第二行尾:



    (global-set-key "\M-?" 'help-command)
    (global-set-key "\C-h" 'delete-backward-char) |
        

    

然后再次按下C-x C-e。执行global-set-key的结果–一个特别的符号nil(我们后面将会再次看到)–展示在了Emacs屏幕下方的消息区里。

  3. 你可以使用命令eval-expression,这绑定到了M-:[6]。这个命令在minibuffer(屏幕的底部)中提示你输入一个Lisp表达式,然后执行它并输出结果。Emacs的制作者认为eval-expression是少数一些对于初学者来说尝试使用会造成危险的命令之一。以我来看,这简直是胡说;不论如何,这个命令在初始时是被禁用的,所以当你尝试使用时,Emacs告诉你“You have typed M-:, invoking disabled command eval-expression.”。然后它会显示eval-expression的描述并且如下提示:
    You can now type
    Space to try the command just this once,
    but leave it disabled,
    Y to try it and enable it (no questions if you use it again),
    N to do nothing (command remains disabled).
        

    

如果你选择Y,Emacs将会把下面的表达式加入你的.emacs。



    (put 'eval-expression 'disabled nil)
        

    (put函数和属性列表(property list)有关,我们将会在第三章的符号属性中看到它)我的建议是你可以在获得这个提示之前就把它手动加入到.emacs里,这样你就不会被“disabled command”警告所困扰了。当然,当你把这条语句放到.emacs里之后,使用前面提到的eval-last-sexp使它马上生效是一个不错的想法。

  4. 你可以使用*scratch* buffer。这个buffer在Emacs启动的时候就会自动创建。它使用了Lisp 交互模式。在这个模式里,按下C-j来执行eval-print-last-sexp,它很像eval-last-sexp,除了它会将结果插入到光标所在的位置。Lisp交互模式的另一个特性是你可以使用M-TAB进行自动补全(触发lisp-complete-symbol)。Lisp交互模式在用来调试太长的Lisp表达式或者数据结构太复杂的时候特别有用。

不管你使用哪一种方法,执行global-set-key表达式的结果是产生了新的按键绑定。

Apropos

在结束第一个例子之前,让我们讨论一下Emacs的最重要的在线帮助特性,apropos。假设你同时拥有BS和DEL键,你希望BS删除光标前面的字符而DEL删除后面的。你现在知道了delete-backward-char用来完成前面的目的,但是你不知道什么命令完成后面的。你确信Emacs一定有这么一个命令。但是如何找到它呢?

答案是使用apropos命令,它允许你使用表达式来搜索所有已知的变量名和函数名。试试这么做[7]:

M-x apropos RET delete RET

返回值是一个列出了所有符合“delete”的变量和函数的buffer。如果我们在这个buffer里搜索“character”,然后翻到这一部分

backward-delete-char 
Command: Delete the previous N characters (following if N is negative). 
backward-delete-char-untabify 
Command: Delete characters backward, changing tabs into spaces. 
delete-backward-char 
Command: Delete the previous N characters (following if N is negative). 
delete-char 
Command: Delete the following N characters (previous if N is negative). 

而函数delete-char正是我们需要的。

(global-set-key "\C-?" 'delete-char)

(由于历史原因,DEL由CONTROL-question-mark来触发。)

你可以使用前置参数来执行apropos。在Emacs中,在执行命令前按下C-u将会向命令传递额外的参数。通常,C-u后面跟着一个数字;例如C-u 5 C-b表示“将光标向前移动5个字符”。有时额外的参数就是你按下的C-u本身。

当apropos使用了前置参数时,它不只显示所有符合搜索表达式的函数和变量,它还展示出列表中每个命令绑定的按键(这不是默认的,因为搜索按键绑定很慢)。使用C-u M-x apropos RET delete RET 然后搜索“character”,我们将会得到下面的信息:

backward-delete-char (not bound to any keys) 
Command: Delete the previous N characters (following if N is negative). 
backward-delete-char-untabify (not bound to any keys) 
Command: Delete characters backward, changing tabs into spaces. 
delete-backward-char C-h, DEL 
Command: Delete the previous N characters (following if N is negative). 
delete-char C-d 
Command: Delete the following N characters (previous if N is negative). 

这证实了现在C-h和DEL都会执行delete-backward-char,并且告诉了我们delete-char已经有了一个绑定:C-d。在我们执行

(global-set-key "\C-?" 'delete-char)

之后,如果我们再次执行apropos,我们将会得到

backward-delete-char (not bound to any keys) 
Command: Delete the previous N characters (following if N is negative). 
backward-delete-char-untabify (not bound to any keys) 
Command: Delete characters backward, changing tabs into spaces. 
delete-backward-char C-h 
Command: Delete the previous N characters (following if N is negative). 
delete-char C-d, DEL 
Command: Delete the following N characters (previous if N is negative). 

如果我们知道我们要搜索的对象是Emacs命令,而不是变量或者函数,我们可以使用command-apropos(M-? a)来缩小搜索范围。命令和其他Lisp函数的区别是命令特别用于交互式的触发,也就是说可以通过按键或者M-x触发。非命令的函数只能被其他Lisp代码调用或者被类似eval-epression和eval-last-sexp这样的命令来执行。我们将会在下一章看到更多的函数和命令的知识。

<<1-1>>[1]. 批评者通常认为Lisp的括号是它标志性的缺点。他们认为,Lisp是“Lots of Infernal Stupid Parentheses”的简写(实际上是“List Processing”的简写)。以我来看,这个更简单的符号使得Lisp比其他语言更易读,而我希望你也这么认为。

<<1-2>>[2]. Emacs使用了一种标记-清扫的垃圾回收设计,是最简单的垃圾回收实现方式之一。有一些其他的实现方式会更少打扰用户;例如,一种称为“incremental”的方式在执行时不会使Emacs当机。不幸的是,Emacs没有使用这些方式。

<<1-3>>[3]. 你可以使用length函数查看字符串的长度来确认这件事。如果你执行(length “\M-?”),结果为1。如何“执行”在本章的后面有介绍。

<<1-4>>[4]. 技术上说,我们应该说按键被绑定到了命令上,而不是命令绑定到了按键上。(说按键“绑定到”了命令上正确的表示了这个按键序列只能做一件事–触发这个命令。说命令“绑定”到了一个按键上则表示只有这个按键序列能够触发这个命令,而这并不是真的。)但是一般来说这种误用的“绑定到”并不会引起什么误会。

<<1-5>>[5]. 遗憾地读作“sex pee.”。

<<1-6>>[6]. 这个按键绑定是19.29新引入的。在之前的版本,eval-expression默认绑定到M-ESC。

<<1-7>>[7]. 所有的Emacs命令,不管它们绑定到了哪里(如果有的话),都可以通过M-x command-name RET来执行。自然,M-x自己也是一个绑定到按键上的命令,execute-extend-command,它会提示输入一个要执行的函数名。