Emacs:朋友还是敌人?

作者:Matt Welsh

如果你像我一样,你会觉得 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-fC-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-upscroll-down 指的是 Emacs 函数,用于在文档中按页滚动?除了阅读文档之外,你还可以使用 Emacs 函数 describe-key (你可以用 C-h k 调用它) 来找到答案。调用此函数并随后输入按键序列将告诉你该按键序列映射到的 Emacs 函数名称。如果你知道 Emacs 按键 C-vM-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-vM-v (之前按页滚动) 分别移动到缓冲区的开头和结尾。第三个命令使 C-p 粘贴先前删除或复制的文本区域,第四个命令使 C-u 执行撤消命令。(我倾向于经常使用 Emacs 的撤消功能,并且发现 C-x uC-_ 太过笨拙。)最后一个命令使 C-r 切换覆盖模式。

以这种方式重新绑定按键有一些注意事项。首先,它会破坏按键以前的定义。在许多情况下,这很好;只要你没有使用以前定义的按键。然而,上述命令中的最后一个重新绑定了 C-r,它用于调用向后增量搜索;这是一个非常有用的功能。在重新绑定按键之前,你应该确保它以前的定义对你来说并不重要。

多按键序列

现在,涉及多个按键的绑定怎么样?例如,我喜欢使用按键序列 C-d C-d 来删除一行(有点像它的 vi 对应物),以及 C-d g 来删除从光标位置到缓冲区末尾的文本。global-set-key 的第二个参数可以包含多个按键,但问题是 C-d 已经有了含义。我们想使用 C-d 作为命令 C-d C-dC-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 gC-d C-d 绑定新的含义。然而,这些函数绑定到了神秘的函数 my-nuke-to-endmy-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-keydescribe-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) 进行比较。如果它们相等,那么我们正在尝试删除缓冲区的最后一行,并希望删除之前的终止换行符。我们向后移动一行 (使用参数为 -1forward-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

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-foregroundset-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 吧。

获取 Emacs LISP 手册

Matt Welsh (mdw@sunsite.unc.edu) 是康奈尔大学机器人与视觉实验室的程序员。他的空闲时间用于自酿虚拟啤酒和演奏布鲁斯音乐。

加载 Disqus 评论