Emacs:朋友还是敌人?
如果你像我一样,你会觉得 Emacs 有点令人生畏。嗯,如果你像我一样,你多年来一直是 vi 的忠实用户,并且一直想开始使用 Emacs,但却很难做到。每次你尝试切换时,你会发现自己在每说三句话后就按下 <esc> 键,这(在 Emacs 中)会产生一些随机的,通常是不 желаемых, 结果。有一次,我被抛进了臭名昭著的“医生”模式,这是一个模拟精神病医生的简单人工智能程序。在那里,我花了整整十分钟告诉 Emacs 我对它的看法。(回应是,“也许你可以尝试少骂人一点。”)
使用 Emacs 的 vi 模拟模式之一也没有多大帮助;它们似乎都不能正确处理 даже basic vi 命令。如果你倾向于使用不太知名的 vi 功能,那你 просто 倒霉了。(有人试过用 nnnzwww^ 更改窗口大小吗?我不这么认为。)
尽管这似乎不可能,但你可以在你的日常生活中找到 Emacs 的位置。如果你有磁盘空间和内存(这绝非微不足道的细节),我强烈建议研究一下 Emacs;即使你认为你对 vi 非常满意,它也能让你的生活更轻松。
然而,无论你是否习惯了 vi,许多系统上的默认 Emacs 配置都远非令人满意。虽然许多用户学会了忍受“开箱即用”的 Emacs,但我从未对这种方法感到满意。相反,你可以让 Emacs 几乎做任何你想让它做的事情。所以,让我们来看看一些我发现特别有用的自定义设置。
这里讨论的所有自定义设置都涉及到编辑 Emacs 配置文件,默认情况下是 ~/.emacs。这个文件是用 Emacs LISP (Elisp) 编写的,Emacs 几乎对所有事情都使用这种内部 LISP 引擎。例如,按键映射到 Emacs LISP 命令。你可以修改为每个按键序列执行的命令,甚至编写自己的 Elisp 函数来绑定到按键。
如果你不是 LISP 程序员,也不要害怕。这里描述的大多数 LISP 形式都非常简单,不需要人工智能学位就能理解。
我假设你至少尝试过使用 Emacs,并且知道如何进入和退出它,熟悉 Info 文档系统等等。如果不是,启动 Emacs 并输入 Ctrl-h (C-h),然后输入 t,这将把你带入教程。(从那里开始,你就得靠自己了。)以下自定义设置在 Emacs 19.24 下工作。当你读到这篇文章时,很可能已经有更新的版本可用了,某些东西可能已经发生了变化。
在我们自定义 Emacs 之前,我们需要一个自定义文件。默认情况下,这是你主目录中的 .emacs。(稍后,我们将把 .emacs 的内容移动到另一个文件,所以系好安全带。)
我们的首要任务是重新绑定几个按键以执行更合理的操作。诚然,其中几个按键绑定是面向 vi 的,但仍然应该有意义。首先,我喜欢用 C-f 和 C-b 在文档中前后翻页,就像在 vi 中一样。为了重新绑定这些按键,我们会在我们的 .emacs 文件中包含以下几行
; Modify meaning of C-f and C-b (global-set-key "\C-f" 'scroll-up) (global-set-key "\C-b" 'scroll-down)
正如你所看到的,Elisp 中的注释以分号开头,并延伸到行尾。在这里,我们调用函数 global-set-key 两次;一次用于 C-f,另一次用于 C-b。在 LISP 中,表达式用括号括起来。每个列表中的第一个项目是要调用的函数名称,后面跟着任何参数。global-set-key 的第一个参数是一个字符串常量,表示要绑定的按键序列。第二个参数是要绑定到的 Emacs LISP 函数的名称。函数名称用单引号括起来,以便引用函数名称本身,而不是它指向的函数。
还剩下一个问题:我们是如何知道 scroll-up 和 scroll-down 指的是 Emacs 函数,用于在文档中按页滚动?除了阅读文档之外,你还可以使用 Emacs 函数 describe-key (你可以用 C-h k 调用它) 来找到答案。调用此函数并随后输入按键序列将告诉你该按键序列映射到的 Emacs 函数名称。如果你知道 Emacs 按键 C-v 和 M-v (Meta-v,其中“meta”通常是 <esc> 键) 在文档中前后滚动,你可以使用 C-h k 来确定这些按键的函数名称。例如,序列 C-h k C-v 将显示绑定到 C-v 的函数名称。
以下是我经常使用的一些其他按键重新绑定
(global-set-key "\C-v" 'end-of-buffer) (global-set-key "\M-v" 'beginning-of-buffer) (global-set-key "\C-p" 'yank) (global-set-key "\C-u" 'undo) (global-set-key "\C-r" 'overwrite-mode)
前两个命令重新绑定 C-v 和 M-v (之前按页滚动) 分别移动到缓冲区的开头和结尾。第三个命令使 C-p 粘贴先前删除或复制的文本区域,第四个命令使 C-u 执行撤消命令。(我倾向于经常使用 Emacs 的撤消功能,并且发现 C-x u 或 C-_ 太过笨拙。)最后一个命令使 C-r 切换覆盖模式。
以这种方式重新绑定按键有一些注意事项。首先,它会破坏按键以前的定义。在许多情况下,这很好;只要你没有使用以前定义的按键。然而,上述命令中的最后一个重新绑定了 C-r,它用于调用向后增量搜索;这是一个非常有用的功能。在重新绑定按键之前,你应该确保它以前的定义对你来说并不重要。
现在,涉及多个按键的绑定怎么样?例如,我喜欢使用按键序列 C-d C-d 来删除一行(有点像它的 vi 对应物),以及 C-d g 来删除从光标位置到缓冲区末尾的文本。global-set-key 的第二个参数可以包含多个按键,但问题是 C-d 已经有了含义。我们想使用 C-d 作为命令 C-d C-d 和 C-d g 的“前缀”键,所以首先我们必须取消绑定 C-d。这是我们做的
; Various keys for nuking text (global-unset-key "\C-d") (global-set-key "\C-d g" 'my-nuke-to-end) (global-set-key "\C-d\C-d" 'my-nuke-line)
我们 просто 使用 global-unset-key 来取消绑定 C-d,然后为 C-d g 和 C-d C-d 绑定新的含义。然而,这些函数绑定到了神秘的函数 my-nuke-to-end 和 my-nuke-line,你们当中精明的人会注意到这些不是标准的 Emacs 函数。我们必须定义它们。
定义 Emacs LISP 函数相当简单。当然,Emacs 函数可能非常强大和复杂,但在这种情况下,我们将使用函数来调用其他函数的短序列,这并不那么可怕。一般来说,如果你需要按键绑定来按顺序调用几个函数,你必须定义一个新函数来包装这个序列。
这是 my-nuke-to-end 的定义,它应该放在 .emacs 文件中相应的 global-set-key 调用之上,该调用使用了它。
(defun my-nuke-to-end () "Nuke text from here to end of buffer." (interactive "*") (kill-region (point) (point-max)))
defun 函数将要定义的函数名称、参数列表(这里为空)以及函数体作为参数。请注意,函数体的第一行是一个字符串常量,它是函数的简短描述。当使用 describe-key 或 describe-function 显示有关 my-nuke-to-end 的信息时,将显示此字符串。
函数体的第二行是对 interactive 函数的调用。这是绑定到按键的函数所必需的。它 просто 告诉 Emacs 如何交互式地执行该函数(也就是说,从按键序列调用时)。interactive 的参数“*”表示该函数不应在只读缓冲区内执行。如果你想了解详细信息,请查看 interactive 的文档。(请参阅侧边栏“获取 Emacs LISP 手册”,了解如何获取此文档的信息。)
函数体的最后一行使用 Emacs 内部函数 kill-region 删除文本区域。函数 point 和 point-max 分别返回光标的当前位置和缓冲区末尾的位置。kill-region 删除这两个位置之间的文本。
my-nuke-line 的定义稍微复杂一些,因为没有单个 Emacs 函数映射到此操作。这是它的定义
(defun my-nuke-line (arg) "Nuke a line." (interactive "*p") (beginning-of-line nil) (kill-line arg) (if (= (point) (point-max)) (progn (forward-line -1) (end-of-line nil) (kill-line arg) (beginning-of-line nil))))
首先,我们看到这个函数现在接受一个参数,我们称之为 arg。许多 Emacs 按键函数接受一个数字参数,你可以通过在按键前加上 C-u,然后加上一个数字来指定它。(也就是说,除非你已经重新绑定了 C-u,就像我们所做的那样。)这个数字参数改变了某些函数的行为。在这里,arg 被传递给 kill-line,在函数体的第 4 行和第 9 行中使用。
my-nuke-line 本质上是 kill-line 的包装器,但处理缓冲区中最后一行的特殊情况。在这种情况下,我们想删除最后一行之前的换行符,这将导致最后一行完全被剪掉(否则,Emacs 会删除该行,但在其位置留下一个空行)。在调用 interactive (使用 “*p” 参数,这将使 arg 转换为数字) 之后,beginning-of-line 将光标移动到 (惊喜!) 行的开头。然后调用 kill-line。请注意,kill-line 只删除从光标位置到行尾的文本;而不是整行。
接下来,我们将光标位置 (point) 与缓冲区末尾位置 (point-max) 进行比较。如果它们相等,那么我们正在尝试删除缓冲区的最后一行,并希望删除之前的终止换行符。我们向后移动一行 (使用参数为 -1 的 forward-line),移动到该行的末尾,删除该行剩余的部分 (只包含终止换行符),然后移回行首。所有这些操作导致缓冲区的最后一行被删除。
我确信那里有 Emacs 高手可以找到更好的方法来完成同样的事情;请原谅我。我已经被 vi 洗脑了。Emacs 的一个更好的特性是,有很多方法可以修改它的行为。
既然你已经上了一门 Emacs LISP 编程速成课程,让我们继续讨论一些更实际的东西。一旦你开始自定义 Emacs,你会注意到你的 .emacs 文件变得非常大,并且可能需要一段时间才能加载。你可能知道 Emacs 允许你字节编译 LISP 源文件以加快加载速度,所以让我们在我们的 .emacs 配置文件上利用这个特性。
第一步是创建一个目录,用于存放你的个人 Emacs LISP 文件。起初,这个目录只包含一个文件;也就是你的初始配置文件;但在以后的生活中,你可能希望编写单独的 Elisp 文件。我使用我的主目录中的 emacs 目录来实现此目的。
接下来,将你的 .emacs 文件复制到这个目录,并将其重命名为类似 startup.el 的名称。
现在,我们将 .emacs 的内容替换为一小段代码,该代码字节编译 emacs/startup.el 并加载它。但是,我们只想在 startup.el 比其编译后的对应文件 startup.elc 更新时才字节编译 startup.el。这是诀窍
(defun byte-compile-if-newer-and-load (file) "Byte compile file.el if newer than file.elc" (if (file-newer-than-file-p (concat file ".el") (concat file ".elc")) (byte-compile-file (concat file ".el"))) (load file)) (byte-compile-if-newer-and-load "~/emacs/startup")
我相信这显而易见,但为了解释:这段代码定义了一个新函数 byte-compile-if-newer-and-load(与 Emacs 对冗长函数名称的偏好保持一致),并在 ~/emacs/startup.el 上执行它。我们现在已经将所有 Emacs 配置文件代码移动到 startup.el,该文件在必要时会被字节编译。
Emacs 和 X Window 系统是两个好东西,它们在一起更棒。事实上,我第四千次开始使用 Emacs 的主要动机是拥有一个编辑器,它融合了 X 的许多优秀功能,例如基于鼠标的区域剪切和粘贴等等。Emacs 19 支持许多有用的基于 X 的功能,我将在这里介绍其中一些。
你可能想在使用 X 下的 Emacs 时做的第一件事是自定义颜色。我不是黑白色的粉丝;事实上,我更喜欢深色背景上的浅色字体。虽然你可以使用 X 资源数据库(例如,通过编辑 ~/.Xdefaults)来自定义 Emacs 的 X 特定属性,但这还不够灵活。相反,我们可以使用 Emacs 内部函数,例如 set-foreground-color。
例如,在你的 startup.el 文件中,你可能包含
(set-foreground-color "white") (set-background-color "dimgray") (set-cursor-color "red")
这将适当地设置这些颜色。
Emacs 还为面孔提供支持,最常用于 font-lock-mode 中。在这种模式下,当前缓冲区中的文本被“字体化”,例如,C 源代码注释以一种字体面孔显示,而 C 函数名称以另一种字体面孔显示。Emacs 的几种主要模式都支持字体锁定,包括 C 模式、Info、Emacs-Lisp 模式等等。每种模式都有不同的规则来确定如何字体化文本。
为了简单起见,我使用相同字体的面孔,但使用不同的颜色。例如,我将“粗体”面孔设置为浅蓝色,将“粗斜体”设置为令人不快的绿色阴影。每种主要模式对每个面孔都有不同的用途;例如,在 Info 中,粗体面孔用于突出显示节点名称,而在 C 模式中,粗斜体面孔用于函数名称。
Emacs 函数 set-face-foreground 和 set-face-background 用于设置与每个面孔对应的颜色。有关可用面孔及其当前显示参数的列表,请使用命令 M-x list-faces-display。
例如,我在 startup.el 中使用以下命令来配置面孔
(set-face-foreground 'bold "lightblue") (set-face-foreground 'bold-italic "olivedrab2") (set-face-foreground 'italic "lightsteelblue") (set-face-foreground 'modeline "white") (set-face-background 'modeline "black") (set-face-background 'highlight "blue") (set-face-underline-p 'bold nil) (set-face-underline-p 'underline nil)
模式行面孔(在 Emacs 文档中出于某种原因被称为 mode-line)用于模式行和菜单栏。此外,函数 set-face-underline-p 可用于指定特定面孔是否应加下划线。在这种情况下,我关闭了粗体和下划线面孔的下划线。(一个没有下划线的下划线面孔?嘿,这是 Emacs。一切皆有可能。)
为了使用所有这些精彩的面孔,你需要启用 font-lock-mode。你可能还希望启用 transient-mark-mode,这将使当前区域(光标位置和标记之间的文本)使用区域面孔突出显示。以下命令将启用此功能。
(transient-mark-mode 1) (font-lock-mode 1)
上述配置的一个问题是 font-lock-mode 不会自动为每种主要模式启用。在你的 startup.el 文件中包含 font-lock-mode 命令将在 Emacs 首次启动时启用此模式,但不会为你要创建的每个新缓冲区启用。一般来说,对于任何 Emacs 次要模式都是如此;每当你进入新的主要模式时,都必须启用次要模式。
我们想要做的是在每次进入某些模式时,例如 C 模式或 Emacs LISP 模式,都执行 font-lock-mode 命令。幸运的是,Emacs 提供了“钩子”,允许你在某些事件发生时执行函数。
让我们在每次进入 C 模式、Emacs LISP 模式或文本模式时启用几个次要模式
(defun my-enable-minor-modes () "Enables several minor modes." (interactive "") (transient-mark-mode 1) (font-lock-mode 1)) (add-hook 'c-mode-hook 'my-enable-minor-modes) (add-hook 'emacs-lisp-mode-hook 'my-enable-minor-modes) (add-hook 'text-mode-hook 'my-enable-minor-modes)
现在你应该发现,在进入这些主要模式中的任何一种时,相应的次要模式也会被启用。一般来说,每种主要模式都有一个入口钩子,名为 modename-hook。
我们可以使用 define-key 为特定模式设置绑定,而不是使用 global-set-key 为所有主要模式定义按键绑定。通过这种方式,我们可以根据你碰巧处于的主要模式来指定某些按键的行为。
例如,我更喜欢回车键相对于上面一行缩进下一行文本(如果上一行缩进了五个空格,下一行也应该缩进五个空格)。要在 C 模式、Emacs LISP 模式和缩进文本模式中启用此功能,请使用以下命令
(define-key indented-text-mode-map "\C-m" 'newline-and-indent) (define-key emacs-lisp-mode-map "\C-m" 'newline-and-indent) (define-key c-mode-map "\C-m" 'newline-and-indent)
每种模式都有一个与其关联的 modename-map,它指定了该模式的按键绑定。你可能已经猜到,newline-and-indent 是 Emacs 函数,它执行换行符,然后进行相对缩进。
编辑文件时,Emacs 通常根据文件名扩展名确定要使用的模式。如果我要编辑一个名为 clunker.c 的新文件,C 模式将用作默认模式。但是,当无法做出决定时,Emacs 使用基本模式。我更喜欢使用缩进文本模式,可以使用以下命令启用该模式
(setq default-major-mode 'indented-text-mode)
局部按键绑定可以用于比上面演示的更有趣的任务。例如,命令 M-x compile 将在当前目录中发出命令 make -k(默认情况下),从而编译你可能正在处理的任何代码。来自 make 命令的输出和错误消息显示在一个单独的窗口中。你可以从编译缓冲区中选择错误消息,在这种情况下,Emacs 将自动打开相应的源文件并跳转到包含错误的行。总而言之,这使得编辑、编译和调试程序更加高效;你几乎可以在 Emacs 中完成所有操作。
为了自动化保存当前源文件和发出 make 的过程,我们可以将按键序列 C-c C-c 绑定到一个新函数;让我们称之为 my-save-and-compile。代码如下所示
(defun my-save-and-compile () "Save current buffer and issue compile." (interactive "") (save-buffer 0) (compile "make -k")) (define-key c-mode-map "\C-c\C-c" 'my-save-and-compile)
save-buffer 命令用于保存当前源文件,而 compile 使用命令 make -k 发出。现在,只需两个简单的按键,你就可以将你的源文件发送到编译器,并等待错误消息的到来。如果没有 my-save-and-compile 函数,你必须手动保存源文件(使用 C-x C-s)并发出 M-x compile。
当然,要使用此功能,你必须在你源文件所在的目录中创建一个 Makefile。(make 命令在包含相关源文件的目录中发出。)创建 Makefile 是另一个问题。Linux Journal 的未来刊物将讨论这个主题,但与此同时,有很多关于 make 的信息来源。《Managing Projects with make》这本书来自 O'Reilly and Associates 是一个好的起点,GNU make Manual 也是如此,它涵盖了 Linux 系统上可用的 make 版本。
另请注意,compile 将首先提示你保存任何已修改的缓冲区。如果你一次只修改一个缓冲区,my-save-and-compile 会为你保存它。我们可以让 my-save-and-compile 保存所有已修改的缓冲区,但你可能不希望这种情况在你不知情的情况下自动发生。
正如我们提到的,compile 函数将打开一个新窗口,其中包含来自 make 命令的消息。从此缓冲区中,你可以选择错误消息,以便 Emacs 自动跳转到该消息,从而使你能够修复问题并继续前进。如果你在 X 下运行 Emacs,用鼠标第二个按钮单击错误消息会将你带到包含错误的行。否则,你可以将光标移动到错误消息(在编译缓冲区中),并使用 C-c C-c(不要被此按键序列的多种含义混淆)。或者,你可以使用函数 next-error 访问下一个错误消息。在我的 startup.el 中,我在 C 模式下将此函数绑定到 M-n(到现在为止,你应该知道如何操作)。
你可能希望配置一些小项目,以偏离 Emacs 的默认行为。我将在下面简要列出这些代码;除了上面讨论的概念之外,它们都不涉及新概念。注释应该充分描述这些自定义设置。
;; Allow M-j, M-k, M-h, M-l to move cursor, ;; similar to vi. (global-set-key "\M-j" 'next-line) (global-set-key "\M-k" 'previous-line) (global-set-key "\M-h" 'backward-char) (global-set-key "\M-l" 'forward-char) ;; Commonly used buffer commands, requiring ;; less use of CTRL ;; (For the ergonomically-minded.) (global-set-key "\C-xf" 'find-file) (global-set-key "\C-xs" 'save-buffer) ;; Open a line below the current one; as in "o" in vi (defun my-open-line () (interactive "*") (end-of-line nil) (insert ?\n)) (global-set-key "\C-o" 'my-open-line) ;; Make the current buffer the only visible one, ;; and recenter it. (defun my-recenter-frame () (interactive "") (delete-other-windows) (recenter)) (global-set-key "\C-l 'my-recenter-frame) ;; Save all buffers and kill Emacs, without prompting (defun my-save-buffers-kill-emacs (arg) (interactive "P") (save-buffers-kill-emacs t)) (global-set-key "\C-x\C-c" 'my-save-buffers-kill-emacs) ;; Preserve original save-buffers-kill-emacs, ;; in case we don't want ;; to save what we were doing (global-set-key "\C-x\C-x" 'save-buffers-kill-emacs) ;; Real Programmers don't use backup files (setq make-backup-files 'nil) ;; But Real Programmers do use RCS. Includes ;; rcsid[] definition in a C source file (defun my-c-insert-rcsid () (interactive "*") (insert "static char rcsid[] = \"@(#)$Header$\";")) (define-key c-mode-map "\C-c\C-x" 'my-c-insert-rcsid) ;; Finally, prevent next-line command from adding ;; newlines at the ;; end of the document. Instead, ring the bell when ;; at the end of ;; the buffer. (setq next-line-add-newlines 'nil)
我希望这次 Emacs 自定义世界的旋风之旅对您有所帮助,或者至少很有趣。我发现上述许多修改都非常宝贵。记住一句老话:有 Elisp,走遍天下都不怕。
话虽如此,暂时还是用回 vi 吧。
Matt Welsh (mdw@sunsite.unc.edu) 是康奈尔大学机器人与视觉实验室的程序员。他的空闲时间用于自酿虚拟啤酒和演奏布鲁斯音乐。