使用自动补全提升我的 Emacs 体验

Martin Fowler: 2024 年 1 月 25 日

我使用 Emacs 已经很多年了,用它来写网站内容、写书,以及大部分的编程工作。(例外情况是使用 IntellJ IDEA 来写 Java 代码,以及使用 RStudio 来写 R 代码。)因此,我很高兴看到 Emacs 在过去几年里取得了很大的进步,不再像以前那样停滞不前。对我来说,Emacs 体验最大的改进之一是使用正则表达式来进行自动补全列表。

许多 Emacs 命令会生成一些选项供用户选择。例如,我想访问(打开)一个文件,我输入查找文件的快捷键组合,Emacs 就会在迷你缓冲区(一个与命令交互的特殊区域)中弹出一个候选文件列表。这些文件列表可能很长,尤其是我要查找当前项目中的所有文件时。

为了指定我想要的文件,我可以输入一些文本来过滤列表。例如,如果我想打开文件 articles/simple/2024-emacs-completion.md,我可以输入 emacs。我不需要精确地匹配到这个文件,只要过滤到一个足够小的列表就可以了。

我发现有一种特定的正则表达式构建器最有用,它使用空格来分隔正则表达式。这样我就可以输入 articles emacs 来获取包含“articles”和“emacs”的文件路径列表。它本质上将字符串“articles emacs”转换为正则表达式 \\(articles\\).*\\(emacs\\)。更棒的是,这种匹配器允许我以任何顺序输入正则表达式,因此“emacs articles”也会匹配。这样一来,当第一个正则表达式弹出过滤后的列表后,我就可以使用第二个正则表达式来选择我想要的文件,即使区分的正则表达式出现在我的初始搜索之前。

安装这种自动补全匹配器对我的 Emacs 使用体验产生了显著的影响,因为它让过滤大型列表变得轻而易举。其中最显著的影响之一是它改变了我使用 M-x 的方式,M-x 是一个快捷键组合,可以调出一个包含所有交互式 Emacs 函数的列表。使用正则表达式匹配器来过滤列表,我可以使用函数名来调用 Emacs 命令,只需几个按键。这样我就不用记住键盘快捷键了。有了它,我就可以通过 M-x 来调用那些我使用频率较低的命令。我并不经常列出所有打开的缓冲区,所以与其尝试记住它的快捷键组合,我只需输入 M-x ibibuffer 就会很快弹出来。这得益于我用于 M-x 的命令 (counsel-M-x) 在正则表达式中插入了一个“^”作为第一个字符,它将第一个正则表达式锚定到行的开头。由于我将所有自己编写的函数都以 mf- 为前缀,因此我可以轻松地找到自己的函数,即使它们有很长的名字。我写了一个命令来从 URL 中删除域名,我把它叫做 mf-url-remove-domain,可以用 M-x mf url 来调用它。

Emacs 中有很多包可以实现这种匹配功能,多到让人感到困惑。我目前使用的是 Ivy。默认情况下,它使用空格分隔的正则表达式匹配器,但它不支持任何顺序。为了按照我的喜好进行配置,我使用了

(setq ivy-re-builders-alist '((t . ivy--regex-ignore-order)))

Ivy 是一个名为 counsel 的包的一部分,该包包含各种命令,可以增强这种选择功能。

Ivy 不是唯一可以实现这种功能的工具。事实上,我发现 Emacs 中的自动补全工具的世界非常令人困惑:有很多工具,它们之间存在重叠和交互,我并不真正理解。这个领域的工具包括 HelmcompanyVerticoConsult。Mastering Emacs 有一篇关于 理解迷你缓冲区自动补全 的文章,但它没有解释它所谈论的机制是如何与 Ivy 的功能相结合的,我也没有花时间去弄清楚这一切。

总的来说,我强烈推荐 Mastering Emacs 这本书,它可以帮助你学习如何使用这个强大的工具。Emacs 有如此多的功能,即使像我这样使用 Emacs 已经几十年的用户,也发现这本书让我有了“原来它还能这样用”的感慨。

对于那些好奇的人,这里是我 Emacs 配置的相关部分

(use-package ivy
  :demand t
  :diminish ivy-mode
  :config
  (ivy-mode 1)
  (counsel-mode 1)
  (setq ivy-use-virtual-buffers t)
  (setq ivy-use-selectable-prompt t)
  (setq ivy-ignore-buffers '(\\` " "\\`\\*magit"))
  (setq ivy-re-builders-alist '(
                                (t . ivy--regex-ignore-order)
                                ))
  (setq ivy-height 10)
  (setq counsel-find-file-at-point t)
  (setq ivy-count-format "(%d/%d) "))

(use-package counsel
  :bind (
         ("C-x C-b" . ivy-switch-buffer)
         ("C-x b" . ivy-switch-buffer)
         ("M-r" . counsel-ag)
         ("C-x C-d" . counsel-dired)
         ("C-x d" . counsel-dired)
         )
  :diminish
  :config
  (global-set-key [remap org-set-tags-command] #'counsel-org-tag))

(use-package swiper
  :bind(("M-C-s" . swiper)))

(use-package ivy-hydra)