Emacs 的更多内置功能
摘要
一篇博客文章,介绍了Emacs中不太为人所知但实用的内置功能,延续了一个旨在提高原版Emacs功能可发现性的系列。
<p>Karthik 是一位非常出色的 Emacs 博主。他非常擅长识别模式并传达如何有效使用它们,我发现他的内容对 Emacs 新手和老手都很有帮助。</p>
<p>这篇文章是他关于 Emacs 内置功能系列的一部分(其他文章在文中附有链接)。快去看看吧!我使用 Emacs 已有约10年,每一篇‘内置功能’文章都让我学到新东西。考虑到 Elisp 以及第三方定义自己扩展的便捷性,Emacs 的深度就合情合理了。令人惊叹的是,Emacs 中内置了如此多的功能。我发现自己用 Emacs 内置功能替换了许多第三方包,体验非常愉快。</p>
<p><a href="https://lobste.rs/s/wwbl1n/even_more_batteries_included_with_emacs">评论</a></p>
查看缓存全文
缓存时间: 2026/06/15 00:47
# Emacs 的更多内置功能
来源:https://karthinks.com/software/even-more-batteries-included-with-emacs/
Emacs 的功能存在可发现性难题,而我们正通过一次次的演示逐步攻克它。自从我写完上一篇这类文章以来,又发现了更多令人惊讶且实用的功能,因此是时候再次发布一份“内置功能丰富”的报告了。
**注意**
这是系列文章的第三篇,重点介绍 Emacs 中包含的有用但鲜为人知的功能。第 1 部分和第 2 部分:
- [Batteries included with Emacs](https://karthinks.com/software/batteries-included-with-emacs/)
- [More batteries included with emacs](https://karthinks.com/software/more-batteries-included-with-emacs/)
“鲜为人知”是一个主观判断。大致来说,这意味着在撰写本文时,我在过去二十年断断续续参与在线 Emacs 讨论的过程中,看到这些功能被提及的次数少于五次——而且往往从未被提及。过去几个条目中涵盖的一些功能如今已广为人知并经常被推荐。对此我不敢居功。如果你是 Emacs 新手,**请勿从此处开始**。这不是入门指南。你最好先掌握基本的 Emacs 概念,并坚持使用最广泛推荐的包。当你体验过 Emacs 中类似于“为什么直到 1990 年才有人想到给行李箱装轮子?”这样的想法之后,本系列可能会更有帮助。
我的经验法则是:如果你还不知道 `undo-in-region`,那么还有大量唾手可得的果实等待采摘,等到那些果实摘完后,你再来读这篇文章也不迟!资深 Emacs 用户往往使用一些相对小众的 Emacs 功能,但根据我的经验,每个用户使用的小众功能子集都不同。所以,如果你已经身经百战,我保证下面仍然会有让你惊喜的内容!
规则与以前相同:
- **不引入包,仅限原生 Emacs**
- **没有陡峭的学习曲线。每个功能在五分钟内学会,否则拉倒。**
- **没有噱头**。没有 `doctor`、`tetris`、`snake`、`dunnet`、`zone`、`butterfly`……是的,我们知道 `dissociated-press`。让我们继续。
- **只讲增量**。不涉及 Flymake、doc-view、outline-minor-mode、Gnus 或 EWW 等常见包。不涉及 Emacs 自动弹出或通用谷歌搜索能轻易找到的内容。
- 假设你使用的是现代 Emacs,28.1 及以上版本。
另外,如果你是 Emacs 新手并且还在阅读:请参考以下术语对照表:
| Emacs 术语 | 现代说法 |
|---|---|
| `M-x` | Alt + x |
| `C-x` | Ctrl + x |
| Frame | Emacs 窗口 |
| Windows | 分屏/窗格 |
| Buffer | 连续的文本/数据块 |
| Point | 缓冲区中的光标位置 |
| Active Region | 文本选区 |
| Region | 文本选区(未高亮) |
| Face | 字体、颜色和显示属性 |
不好意思,行了吧?我们开始:
## 词典工具提示模式
开启 `dictionary-tooltip-mode`,当鼠标悬停在单词上时,可以在工具提示中看到词义:
[](https://karthinks.com/img/dictionary-tooltip-demo-01.mp4)
当然,`tooltip-mode` 也需要启用,但它是默认开启的。如果你设置了本地词典,它会优先使用本地词典。请注意,Emacs 的词典也可以查找当代行话和俚语,通常是通过维基词典。
## 使用通配符的 `find-file` 和 `dired`
两个最常用的 Emacs 命令有一个令人惊讶的鲜为人知的实用功能:你可以在交互使用 `find-file` 和 `dired` 时使用通配符。
- 使用 `find-file`(`C-x C-f`)查找文件时,可以使用像 `*foo*.txt` 这样的通配符一次打开多个文件。
- 使用 Dired 打开目录时,可以通过指定文件名通配符来生成特定文件的定制列表。
以下是一个演示,两个功能都用于清理一些(非常)旧的 TeX 编译产物,然后一次打开一堆 LaTeX 文件:
[](https://karthinks.com/img/dired-wildcard-demo-01.mp4)
逐步说明:
- 使用“两级”通配符 `*/_region_*` 运行 Dired:查找所有名称中包含 `"_region_"` 的文件,但仅限于子目录中。
- Dired 生成这些文件的列表。(这些是 AucTeX 创建的临时文件。)
- 全选它们(使用 `dired-toggle-marks`,绑定到 `t`)并删除。
- 使用通配符运行 `find-file`,打开所有子目录中的 TeX 文件。
- 检查缓冲区列表,发现多个 TeX 文件已打开。(用于查看打开缓冲区列表的命令是 `consult-buffer`,补全由 `Corfu` 显示。)
从函数签名可以看出,在编程调用时这是可能的。但意识到在交互使用时也具备这一能力,需要通读完整的文档字符串,而没人有那个时间!实际上,Dired 的通配符功能已被诸如 `consult-find` 之类的现代工作流所取代(通过 `embark-export` 导出为 Dired 缓冲区),但它是开箱即用的。
## 菜单式文件名查找 (`M-x ffap-menu`)
你可能熟悉 Emacs 的“在光标处查找文件”功能 `M-x ffap`,它会检查光标是否位于有效的文件路径上并提议打开它。与之相伴的是 `ffap-menu`,一个鲜为人知但同样方便的命令。`ffap-menu` 会扫描整个缓冲区,查找任何看起来像文件路径或 URL 的内容,并将它们全部呈现给你:
[](https://karthinks.com/img/ffap-menu-demo-01.mp4)
由于它提供了一个 `completing-read` 接口,这开启了一个小小的可能性宇宙:你可以将(可能经过筛选的)补全列表导出到缓冲区,复制或打开全部或任意子集,或者通过 Embark 立即对其进行操作。
### 附注:列出带属性的链接
许多 Emacs 应用(如 EWW)将 URL 作为文本属性而非纯文本链接包含在内,而 `ffap-menu` 会遗漏它们。受 `ffap-menu` 启发,我使用了一个自制版本,也能获取此类链接。
[](https://karthinks.com/img/search-occur-browse-url-01.mp4)
逐步说明:
- 从 EWW 显示的一个维基百科页面开始,左侧有 imenu。
- 调用 `my/search-occur-browse-url`,这是受 `ffap-menu` 启发的自定义命令。
- 滚动浏览页面链接列表,并滚动浏览页面本身。
增强版本:搜索缓冲区中的所有 URL
```elisp
(defun my/search-occur-browse-url (&optional use-generic-p)
"Point browser at a URL in the buffer using completion.
Which web browser to use depends on the value of the variable `browse-url-browser-function'.
Also see `my/search-occur-url'."
(interactive "P")
(let ((match nil)
(match-data nil)
(context (lambda (beg &optional shrp)
(let* ((before (string-replace "\n" ""
(buffer-substring-no-properties beg
(max (line-beginning-position)
(- beg 30)))))
(link (string-replace "\n" ""
(buffer-substring-no-properties beg (point))))
(after (buffer-substring-no-properties (point)
(min (line-end-position)
(+ (point) 30)))))
(concat (propertize " " 'display '(space :align-to 65))
(propertize (concat "..." before) 'face 'shadow)
(if shrp (propertize link 'face '(:inherit shadow :weight bold :underline t))
link)
(propertize (concat after "...") 'face 'shadow))))))
(save-excursion
(goto-char (point-min))
(while (search-forward-regexp my/search-url-regexp nil t)
(push (cons (match-string-no-properties 0)
(funcall context (match-beginning 0)))
match-data))
(goto-char (point-min))
(while (setq match (text-property-search-forward 'shr-url nil nil))
(push (cons (prop-match-value match)
(funcall context (prop-match-beginning match) 'shrp))
match-data)))
(let* ((completion-extra-properties
`(:annotation-function
,(lambda (cand)
(concat " " (cdr (assoc cand match-data))))))
(url (completing-read "Browse URL: " match-data nil t)))
(if use-generic-p
(browse-url-generic url)
(browse-url url)))))
```
## 比较窗口 (`M-x compare-windows`)
Emacs 中有多得数不清的命令用于比较缓冲区和文件:有 `diff`、`diff-buffers`、`diff-backup`、`diff-buffer-with-file`、`dired-diff`、`vc-diff`,以及一整个 `ediff-`、`ediff-merge-` 和 `ediff-directories-` 命令的星系。我数到大约二十二个就数不过来了,而且大部分都记不住。但我最喜欢的差异比较命令是轻量级的 `compare-windows`,它以一种与上下文无关的方式做一件非常简单明了的事:它从两个窗口各自的光标位置开始比较文本,在遇到第一个不匹配处停下并报告。这两个窗口是当前活动窗口和 `other-window` 会选中的那个。功能显然不如 Ediff 或 diff 强大,但运行起来却要容易和快得多。
[](https://karthinks.com/img/compare-windows-demo.mp4)
逐步说明:
1. 在两个窗口中将光标移动到要比较的文本的开头。
2. `M-x compare-windows`
3. 就这样。它会将光标移动到第一个不匹配处并报告。
`compare-windows` 只关心两个窗口中的实际文本,而不关心这些文本的来源。缓冲区类型、修改状态、文件、版本控制状态——统统无关!你甚至可以通过在同一个缓冲区中显示两个窗口,来比较缓冲区中的一段文本与同一缓冲区中稍远位置的另一段文本。
以一种愚蠢但有效的方式,它甚至可以用来比较目录内容,包括文件属性:
[](https://karthinks.com/img/compare-windows-demo-02.mp4)
逐步说明:
1. 两个包含一些相似文件的目录。
2. 在两个窗口中将光标放在同一个文件上。
3. `M-x compare-windows`
4. 光标停在第一个报告的不匹配处,这里是一个文件的修改时间。
是的,你可以通过前缀参数调用它来忽略空白差异。当你发现自己需要在两种视图之间玩“找不同”游戏时,`compare-windows` 就是你的选择。它是我最常用的“差异比较”命令。
## 使用 Dired 比较目录 (`M-x dired-compare-directories`)
但是说到比较目录,Dired(当然)提供了一种不那么 hacky 的方法。在 Dired 中运行 `M-x dired-compare-directories` 会提示输入要比较的目录,并标记两个 Dired 列表中文件名不同的所有文件。这涵盖了最常见的用例,可能就是你需要的全部了。但我们刚才已经用基本的 `compare-windows` 做过了。`dired-compare-directories` 是真正的文件级比较,因此你可以提供涉及任何文件属性的自定义匹配谓词,比如修改时间或大小。例如:
- 你可以用 `(> mtime2 mtime1)` 标记一个文件的最新修改版本,
- 或者用 `(/= size1 size2)` 标记名称相同但大小不同的文件。
在这个例子中,`dired-compare-directories` 标记了 (i) 两个列表中不共有的文件和 (ii) 修改时间不同的文件:
**每个季节都有一个 Ediff**
如果你想要更交互式/规定性的操作,还有 `ediff-directories`,因为每个场合都有一个 Ediff 命令。
## 高亮缓冲区更改 (`M-x highlight-changes-mode`)
既然我们还在讨论发现差异,`highlight-changes-mode` 是一个方便的方法来强调对文件的更改,也是 diff 命令(如 `diff-buffer-with-file`)的“实时”替代方案:
[](https://karthinks.com/img/highlight-changes-mode-demo-01.mp4)
逐步说明:
- 运行下面的代码块,将 `highlight-changes-mode` 与 `save-buffer` 同步。现在更改会在下次保存前高亮显示。
- 做一些更改。注意添加/更改的文本颜色不同。
- 保存缓冲区,同时清除高亮。
- 重复最后两步几次。
`highlight-changes` 的可视化仅由模式本身决定,更改从模式开启时高亮直到模式关闭。一般来说,这不是我们想要的。我们想要的是高亮**未保存的更改**。(有 `M-x highlight-compare-with-file`,但它不人性化到几乎不可用的地步。)
我们可以通过一些技巧来做到这一点,或者干脆加几个钩子:
```elisp
(defun highlight-changes-mode-turn-off ()
(and highlight-changes-mode
(highlight-changes-mode -1)))
(defun highlight-changes-auto ()
(when (buffer-file-name)
(highlight-changes-mode-turn-on)
(add-hook 'after-save-hook #'highlight-changes-mode-turn-on nil t)
(add-hook 'before-save-hook #'highlight-changes-mode-turn-off nil t)))
(add-hook 'text-mode-hook #'highlight-changes-auto)
```
现在 text-mode 缓冲区中的所有更改都会自动高亮。
### 将 `highlight-unsaved` 作为独立功能
`highlight-changes` 的可视化可以进行自定义以使其更微妙,但你可能仍然不希望它一直开启。上面的钩子逻辑可以很容易地转变为一个独立的次要模式:
```elisp
(require 'hilit-chg)
(defun highlight-changes-mode-turn-off ()
(and highlight-changes-mode
(highlight-changes-mode -1)))
(define-minor-mode highlight-unsaved-mode
"Highlight all changes until the buffer is saved."
:lighter "H"
(cond
((not (buffer-file-name))
(user-error "Highlight-until-save-mode is only meant for use in file-visiting buffers"))
(highlight-until-save-mode
(highlight-changes-mode 1)
(add-hook 'after-save-hook #'highlight-changes-mode-turn-on nil t)
(add-hook 'before-save-hook #'highlight-changes-mode-turn-off nil t))
(t
(highlight-changes-mode -1)
(remove-hook 'after-save-hook #'highlight-changes-mode-turn-on t)
(remove-hook 'before-save-hook #'highlight-changes-mode-turn-off t))))
```
---
最后,`highlight-changes-mode` 提供了一个辅助功能:你可以使用 `highlight-changes-next-change` 和 `highlight-changes-previous-change` 跳转到缓冲区中的上一个和下一个更改。由于这是更改跟踪的独立结果,你可以只使用此导航,并通过 `M-x highlight-changes-remove-highlight` 关闭更改可视化。
## 真正有用的文件备份(`vc-diff` 变体)
最后一次离题,以结束发现和比较更改的主题。这需要绕道到 Emacs 备份文件的话题,并且正在突破五分钟的限制和内置功能的理念,请耐心听我说。
### 我们来做备份
默认情况下,Emacs 会对你编辑和保存的任何文件进行定期备份。这个备份系统通常只在“烦人且应该禁用(通过 `make-backup-files`)”的上下文中被提及。如果你想要真正的备份,你可以直接使用版本控制,对吧?如果你担心敏感文件被复制到磁盘上的其他地方,我很同情你。但除此之外,我认为这主要是人体工程学的问题。
- Emacs 用备份文件弄乱你的工作目录,并且
- 不便于你浏览和使用备份文件。
改变前者是一个用户选项(例如,参见 `backup-directory-alist`、`kept-old-versions` 和 `kept-new-versions`),但后者完全是缺少用户界面的问题。外部包 [backup-walker](https://github.com/lewang/backup-walker) 以及其他一些包提供了这种“时间机器”界面。但有一个更简单、令人满意的修复方法,同时能解决另一个问题。
### VC(直到不版本控制)
Emacs 内置的 VC 包提供了一个用于查看版本控制文件过去版本的界面:
- `vc-diff`(`C-x v =`):将文件与其直接上一个版本进行比较,或者在使用前缀参数时与指定版本进行比较。
- `vc-ediff`:对文件的上一个版本或指定版本运行 Ediff。
- `vc-revision-other-window`(`C-x v ~`):在新窗口中显示文件的旧版本。
相似文章
推荐一下……理解 Emacs 的模式
文章解释了 Emacs 的架构模式,重点介绍了通用缓冲区数据模型和增量补全读取(ICR),并将其比作玫瑰的根与花瓣。文章强调了 Emacs 如何统一界面并通过 Elisp 实现可扩展性。
Emacs 写作机
一篇关于将旧 ThinkPad 改造为专用写作设备的博客文章,运行 Debian 和 Emacs,灵感来自 Veronica Explains 的 writerdeck 概念,包含配置和通过 Git 同步文件的技巧。
我可以推荐 Emacs 的创新 UI——eww 吗?
文章推荐使用 Emacs 的 eww 网页浏览器,强调其缺少 JavaScript 的特性改善了许多网站的体验,并指出 Emacs 提供了独特的 UI 创新,如逐张图片调整大小和键盘导航。
被低估的内置功能:Grand Unified Debugger
本文重点介绍了Emacs内置的Grand Unified Debugger(GUD)的功能,强调了其多调试器支持和图形化GDB界面,包括通过点击边缘设置断点以及单步执行代码等功能。
了解 GNU Emacs 中的 Embark(一点)和一些“傻”Embark 技巧
对 GNU Emacs 中 Embark 包的探索,涵盖基本理解和一些使用它的“傻”技巧。