← 文章 / 编程开发
Hacker News 4小时前 · 2026-08-28 19:15:08 · 3 阅读

Emacs 31:Markdown-ts-mode 非官方指南

Cover Image for An unofficial guide to markdown-ts-mode on Emacs 31

Emacs 31 已经发布,里面有不少新鲜功能,等着我们上手体验。

你可能已经听说了新的 markdown-ts-mode,于是决定试试看。但你也许会发现:在 Emacs 31 中,它仍被标记为实验性模式。这到底意味着什么?现在应该用它吗?它已经准备好了吗?还是说,这只是一个模式的雏形?

不妨把本文当作一份快速指南:帮助你启用并运行这个模式,也帮你找到这些问题的答案。

功能完善到什么程度了?

它毕竟还是实验性模式,对吧?你需要主动启用它,这通常意味着目前还不能保证所有功能都稳定运行,并且仍需要更多测试和反馈。

不过,别被“实验性”这个标签误导了。它并不代表这个模式的功能还很不成熟。正如你接下来会看到的,markdown-ts-mode 的功能其实非常丰富:它已经覆盖了 https://commonmark.org/ 的全部规范,以及 https://github.github.com/gfm/ 的大部分内容。此外,它还提供了一些额外功能,例如为 elisp 等非 ts-mode 模式提供代码块支持、目录工具,以及与 pandocgfm 等外部转换器的接口。

在深入了解之前,你可能需要先弄清楚如何启用这个模式。Tree-sitter 的配置有些棘手;如果这是你第一次接触 Tree-sitter,就更需要一份快速“安装指南”了。接下来我们就从这里开始。

它在哪里?需要单独安装这个模式吗?

“实验性”意味着 Emacs 默认不会启用它。因此,你直接打开 .md 文件,或用 M-x markdown-ts-mode RET 调用它时,它不会自动出现。你需要先加载这个库。

在 Emacs 中,几乎所有事情都有不止一种做法。我很喜欢 use-package,所以通常会用它来整理 init 文件。下面是我建议的初始配置:

(use-package markdown-ts-mode
  :ensure nil
  :mode ("\\.md\\'" "\\.mdx\\'" "\\.markdown\\'")
  :config
  (require 'markdown-ts-mode-x))

如果你不使用 use-package,也可以这样配置:

(autoload 'markdown-ts-mode "markdown-ts-mode" nil t)

(dolist (re '("\\.md\\'" "\\.mdx\\'" "\\.markdown\\'"))
  (add-to-list 'auto-mode-alist (cons re 'markdown-ts-mode)))

(with-eval-after-load 'markdown-ts-mode
  (require 'markdown-ts-mode-x))

现在,主模式和包含额外实用功能的 x 库都会加载,你可以直接用它打开 Markdown 文件。

如果你想在不改动现有配置的情况下试用,可以按下面的步骤操作:

  1. 将上面的内容保存到类似 testing.el 的文件中。
  2. 使用 emacs -Q --load 'testing.el' 启动 emacs

这样,一个已经配置好测试环境的纯净 Emacs 会话就准备好了。本指南接下来的内容都会以此为基础。

重要完全不需要下载这个包,也不必将其加入包管理器。现在已经非常老旧且停止维护的 MELPA 仓库,在 Emacs 31 及更高版本上会拒绝安装,而且功能也十分有限。如果你用的是这个包,就不是在使用新版内置的 markdown-ts-mode。明白了吗?我们继续。

打开第一个 Markdown 文件

为了让你“看到我看到的内容”,我们需要借助一些图片。如果这是你第一次使用基于 tree-sitter 的模式,先提醒一下:tree-sitter 虽然优秀、快速且功能丰富,但也有一套需要处理的问题;遇到异常时,可能还需要具备一定的调试能力。这里我会尽量覆盖其中一部分,当然也肯定会遗漏一些。

本指南使用的是这个 测试文件

这个文件所在的仓库就是我们的实验室。记住,那里没有任何代码,所有代码都在 Emacs 自身中。

现在打开 test.md 文件。

重要:这一步可能会出现多种情况。如果你的系统已经安装了 markdown 语法,文件会直接打开。不过,也可能像我这里一样,看到下面的提示:

emacs_markdown_31_demo step 01

这说明 Emacs 没有在系统中找到 markdown 的语法文件。本例中,它应该位于 ~/.emacs.d/tree-sitter/(使用 emacs -Q ... 启动 Emacs 时的默认位置)。Emacs 会提示是否安装,也就是从 markdown-ts-mode 源码中预先定义的仓库下载并编译。输入 y 开始安装。Emacs 会克隆语法仓库、完成编译,然后继续安装第二个语法。没错,markdown 使用两个语法:一个用于主体解析,另一个用于解析行内内容。我会输入 y,允许 Emacs 安装第二个语法。

安装成功!

你应该会看到:

emacs_markdown_31_demo step 02

如果没有看到,出了问题时可以检查以下几点:

  1. Emacs 是否在编译时启用了 tree-sitter?执行 M-: (featurep 'treesit) RET,确认返回值是否为 t

  2. 是否安装了用于“编译”语法的工具,例如 makegcc 等?

  3. Tree-sitter 需要安装发行版提供的软件包,通常名为 tree-sitter-cli,它会提供 tree-sitter 可执行文件。你可以运行 tree-sitter --version 检查是否已安装。

这是所有 tree-sitter 模式都会遇到的常见问题。很多人不愿意自行编译语法,而是选择从可信来源获取已经编译好的文件,比如自己的发行版仓库,或包含数百个预编译语法的软件包。这里就不展开了;获取语法的方法很多,本指南只介绍“自行编译”这一种。

看,我刚才其实故意误导了你。我说你应该看到那个界面,但实际上,“你看到的是否和我一样”应该是下面这样:

emacs_markdown_31_demo step 03

完整文件可以在这里找到,其中包含多个默认主题,方便你对照检查配置是否完整。

那么,究竟发生了什么?

这也是 markdown-ts-mode 如此特别的原因之一。

这个模式不只能处理 markdown,还支持所有其他可用的 -ts-mode!记住这一点,我们稍后会讲到代码块。现在,先来了解几个基础概念。

在你的 test.md 文件中,有一段特殊的头部信息。markdown 文件通常会以 tomlyaml 作为头部。

就是下面这一小段:

---
title: The Official 'markdown-ts-mode.el' Feature Test File
author: Rahul Martim Juliato
date: 2026-03-18
version: 0.1.0
parsers needed: markdown, markdown-inline, yaml, toml, html, c, javascript, python, ruby, rust
---

要让这段内容也能进行语法高亮(也就是由 Emacs 着色显示),还需要一些东西。你能猜到缺了什么吗?如果你的答案是“我们需要一个 YAML grammar!”,恭喜你答对了!

-ts-mode 中,如果某些内容没有正确进行语法高亮,通常是因为缺少对应的 grammar。而 markdown-ts-mode 设计为支持所有可用的 ts-mode,自然也不例外。

运行我们熟悉的 M-x treesit-install-language-grammar RET yaml,安装 yaml grammar。

现在你看到的画面可能和我一样:

emacs_markdown_31_demo step 04

输入 y 确认。嗯,看起来这次是 yaml-ts-mode 在尝试向 treesit-install 注册其首选 grammar 时出了问题,因为这里没有提供任何候选项。我们当然可以手动填写。不过先检查一下别的地方。打开 yaml-ts-mode.el,就能从源代码中看到它所需要的 grammar:

;; from yaml-ts-mode.el
(add-to-list
 'treesit-language-source-alist
 '(yaml "https://github.com/tree-sitter-grammars/tree-sitter-yaml"
		:commit "b733d3f5f5005890f324333dd57e1f0badec5c87")
 t)

太好了!直接执行这段代码,然后再次尝试安装 grammar 即可。或者,也可以像我这次一样,把源地址 https://github.com/tree-sitter-grammars/tree-sitter-yaml 手动填入已经启动的交互会话中:

emacs_markdown_31_demo step 05

接下来只需按默认选项连续输入 RET RET RET...,直到库安装完成。

安装完成后,重新加载 markdown-ts-mode,也可以使用 C-x x g,或重新打开当前文件。

像这样通过访问源代码来完成安装其实比较少见。大多数 -ts-mode 都会自动推荐用于编译的代码仓库。这里刚好遇到了这种情况,正好可以借此说明具体操作。

接下来怎么办?每当遇到一个没有进行字体化的代码块,都需要执行一次 M-x treesit-install-language-grammar。如果愿意,也可以在测试文件中使用 C-x x f 强制进行字体化,这样文件中缺失的每个语法都会逐一提示安装。

现在,整个文档应该已经像这里一样完成字体化了。效果与前一张图相同:

emacs_markdown_31_demo step 03

关于语法的一点说明

-ts-mode 的能力取决于其背后的 tree-sitter 语法。也就是说,每个 -ts-mode 都必须持续跟进对应 grammar 的改进,因为任何希望使用 tree-sitter 解析该语言的编辑器或程序,共享的都是这套语法。

这也意味着,在处理某些限制和功能时,我们最终会依赖这套语法。Emacs 中几乎所有 -ts-mode 的代码里,都能看到关于局限性的说明,以及对某些不明显的处理方式及其原因的解释。

Emacs mode 的作者和维护者通常会在 mode 的注释或代码中,注明建议使用的语法版本和对应的 SHA commit,这一点与前面看到的 yaml 建议相同。维护 ts-mode 的一部分工作,就是跟进新版本语法的变化。我们会尽力使用最新版本进行更新,但经过测试、能够按预期工作的版本,仍以 mode 源文件中指定的版本为准。

所以我认为,直接在 Emacs 中交互式地自行编译,是确保使用体验顺畅的最佳方式。

对于 markdown-ts-mode,我们使用的是 https://github.com/tree-sitter-grammars/tree-sitter-markdown 提供的语法,因为它目前最完整、维护最好,也被代码编辑器和其他程序广泛采用。当然,这并不意味着它没有 bug 或局限。我们会尽力绕开这些限制,也会向该语法项目和 tree-sitter 核心库反馈问题。

终于可以打开 Markdown 文件了!

恭喜!接下来呢?这些步骤需要重复多少次?只需在第一次使用某个 -ts-mode 时执行一次;如果你已经通过其他方式安装了语法,则一次都不需要。

现在来看看 markdown-ts-mode 已经提供了哪些功能。

快速了解 markdown-ts-mode 的功能

为了方便快速发现各项功能,我们(顺便一提,这个 mode 由我和 Stéphane Marks 共同编写)提供了 easy-menu

你可以点击 mode-line 中的 Markdown 来访问它;如果启用了 menu-bar-mode,也可以从菜单栏打开;此外,在使用 markdown-ts-mode 的 buffer 中按 Ctrl + 右键(具体取决于 Emacs 如何映射你的操作系统输入)同样可以打开。

emacs_markdown_31_demo step 06

如果你想现在就停下来自己探索,这其实就是本指南的速览版(下文会提前剧透)。

编辑

学习这个 mode 最快的方法,就是把各种内容都输入一遍。下面来个速通:你要写什么,以及对应的快捷键。

标记(强调)

Markdown 是纯文本格式,所以你随时都可以手动输入这些标记:

想要的效果 输入内容
粗体 **bold**
粗体,另一种写法 __bold__
斜体 *italic*
斜体,另一种写法 _italic_
粗体 + 斜体 ***both***
删除线 ~~gone~~
行内代码 `code`

也可以交给 mode 处理:按下 C-c C-x C-fmarkdown-ts-emphasize),再按一个快捷键:

  • b 加粗,B 使用下划线加粗
  • i 斜体,I 使用下划线表示斜体
  • a 加粗并倾斜
  • s 删除线
  • c 行内代码
  • SPC 移除光标处的强调格式

如果选中了一个区域,格式会包裹整个区域。没有选区时,则包裹光标所在的单词;如果光标处没有单词,就插入一对标记,并将光标放在中间。

emacs_markdown_31_demo step 07

提示:C-c C-x RETmarkdown-ts-toggle-hide-markup)可以隐藏标记本身,因此 **bold** 会显示为 bold。这对编辑时阅读非常方便,和默认的 org-mode 类似。

emacs_markdown_31_demo step 08

另一个提示:即使在列表和引用中,M-q 也能正确填充段落。

标题

直接输入 ###……最多到 ###### 即可。Setext 标题(下方使用 ===--- 的下划线式标题)同样支持识别。

无需重新输入井号即可提升或降低标题级别:

  • M-<left> 提升级别(markdown-ts-promote
  • M-<right> 降低级别(markdown-ts-demote

还可以移动整个章节,包括正文和子章节:

  • M-<up>markdown-ts-move-subtree-up
  • M-<down>markdown-ts-move-subtree-down

在标题上按 TAB 可以循环切换其可见性(大纲折叠)。该 mode 兼容 outline-minor-mode,因此折叠功能开箱即用。在标题上按 S-TAB,则可以循环切换所有标题的可见性。

emacs_markdown_31_demo step 09

重要:你应该已经注意到,这个模式会尽量参考 org-mode 的设计,让熟悉它的 Emacs 用户更容易适应 markdown。如果你不喜欢这些快捷键,也可以自行定制。

列表(列表项和复选框)

输入 - item+ item* item1. item

  • M-RET 插入新的列表项(markdown-ts-insert-list-item
  • RET 会智能处理:markdown-ts-newline 会自动延续当前列表
  • M-<left> / M-<right> 提升或降低列表项层级
  • C-c C-r 重新编号有序列表(markdown-ts-renumber-list
  • C-c C-c 切换任务复选框状态(markdown-ts-toggle-checkbox
  • M-q 会在列表项内正确填充文本

任务列表使用 GFM 格式:

- [ ] not done
- [x] done

原始模式:

emacs_markdown_31_demo step 10

隐藏标记后:

emacs_markdown_31_demo step 11

注意,按下 C-c C-x RET 后看到的项目符号和复选框只是显示效果,缓冲区中实际保存的仍然是 -[x]。相关设置包括 markdown-ts-unordered-list-markermarkdown-ts-checked-checkboxmarkdown-ts-unchecked-checkbox

按下 C-c C-,markdown-ts-insert-structure),再按一个键:

  • ` 插入围栏代码块,并提示输入语言
  • ~ 插入使用波浪号围栏的代码块
  • q 插入引用块
  • d 插入分隔线(主题分隔符)
  • t 插入表格

如果当前选中了一个区域,命令会将该区域包裹起来,而不是插入空块。

emacs_markdown_31_demo step 12

隐藏标记后:

emacs_markdown_31_demo step 13

代码块

这算是它的看家本领。带有语言标签的围栏代码块,会使用对应语言的 mode 进行语法高亮:

```python
def hello():
	return "world"
```

如果代码没有颜色,通常说明缺少对应的 grammar,和前面 yaml 标题遇到的情况一样。

比语法高亮更实用的是:把 point 放进代码块后,你就会进入 markdown-ts-code-block-in-context-mode(mode-line 中显示较短的 [code])。在该模式下:

  • TAB 按对应语言的规则缩进
  • RET 按对应语言的规则换行并缩进
  • M-q 按对应语言的规则进行填充
  • M-. 通过 xref 跳转到定义

使用 C-c C-v nC-c C-v p 移动到下一个或上一个代码块。

非 tree-sitter mode 同样支持,包括 elisp。相关配置项有: markdown-ts-code-block-modesmarkdown-ts-default-code-block-modemarkdown-ts-fontify-code-blocks-natively

下面是原始效果:

emacs_markdown_31_demo step 14

隐藏标记后:

emacs_markdown_31_demo step 15

表格

使用 C-c C-,tM-x markdown-ts-table-insert-table 插入表格,随后指定要插入的行数和列数。

| Column 1 | Column 2 |
|----------|:---------|
| a        |        1 |

在表格中时,你会进入 markdown-ts-in-table-mode(mode-line 中显示 [table]),此时快捷键会切换为:

  • TAB / S-TAB:移动到下一个或上一个单元格(同时格式化表格)
  • RET / S-RET:移动到下一行或上一行
  • M-RET:在下方插入一行
  • M-<up> / M-<down>:移动行
  • M-<left> / M-<right>:移动列
  • M-S-<up> 在上方插入行,M-S-<down> 删除行
  • M-S-<right> 在左侧插入列,M-S-<left> 删除列
  • C-c C-c 对齐整张表格
  • C-c C-t a 设置列对齐方式(左对齐、居中、右对齐)
  • C-c C-t t 转置表格

此外,还可以通过菜单克隆行和列、导入选区中的 CSV/TSV 数据,以及导出表格为 CSV/TSV。

emacs_markdown_31_demo step 16

注意: 目前表格功能仍有一些限制,主要是语法解析方式所致,因此输入时可能会遇到部分内容未应用语法高亮。不过,只要符合 GFM 规范,正常使用应该没有问题。

链接和图片

链接支持常见的 [text](url)[text][ref] 格式。像 [intro](#intro) 这样的片段链接可以点击,跳转到缓冲区中的对应标题;默认使用 GitHub 风格的 slug。

图片会以内联方式显示。C-c C-x C-v 可切换图片显示状态(markdown-ts-toggle-inline-images)。图片的显示宽度,以及是否获取远程 URL,可分别通过 markdown-ts-image-max-widthmarkdown-ts-display-remote-inline-images 设置。

Markdown: emacs_markdown_31_demo step 17

执行 C-c C-x C-v 后: emacs_markdown_31_demo step 18

执行 C-c C-x RET 后: emacs_markdown_31_demo step 19

移动和跳转

  • TAB 循环折叠光标所在位置的内容
  • C-c C-n / C-c C-p 跳转到下一个 / 上一个标题
  • C-c C-u 跳转到父级标题
  • C-c C-f / C-c C-b 跳转到同级的下一个 / 上一个标题
  • M-x imenu 通过补全跳转到任意标题或命名代码块
  • C-c C-v n / C-c C-v p:跳转到下一个 / 上一个代码块

markdown-ts-default-folding 决定文件打开时的显示方式:展开全部内容,或折叠内容。

markdown-ts-view-mode

M-x markdown-ts-view-mode 会进入只读模式,并支持使用单键导航:npufbTAB。阅读 README 时很方便,不用担心误操作修改内容。

emacs_markdown_31_demo step 20

其他功能

下面介绍的功能都位于 markdown-ts-mode-x.el 中,这也是我们在设置过程中加载它的原因。

目录

目录由 HTML 注释包围,因此无论在哪里渲染,都能保留下来:

<!-- markdown-ts-toc: -->
<!-- markdown-ts-toc-end: -->
  • M-x markdown-ts-toc-insert-template:插入这些标记,可选择基础模板或完整模板(完整模板会列出每个参数及其默认值)
  • M-x markdown-ts-toc-generate:生成目录;每次调用都会重新生成
  • M-x markdown-ts-toc-clear:清空目录;markdown-ts-toc-clear-and-remove 还会删除这些标记
  • M-x markdown-ts-toc-update-before-save-mode:保存时自动重新生成目录

参数直接写在起始注释中,包括 min-depthmax-depthcandidatesfromstyleindentno-linkrelative-depthignore。同一个缓冲区可以包含多个使用不同参数的目录。目录条目不只可以来自标题,列表项、setext 标题和命名代码块也可以作为候选内容。

原始显示:

emacs_markdown_31_demo step 21

隐藏标记后:

emacs_markdown_31_demo step 22

导出

M-x markdown-ts-convert用于转换当前缓冲区,markdown-ts-convert-file用于转换文件。除非设置了markdown-ts-default-converter,否则系统会询问输出格式和转换器。开箱即用的选项包括:

  • 通过pandoc转换为 PDF
  • 通过pandoccmarkcmark-gfmmarkdownmarkdown.pl转换为 HTML

加上前缀参数后,转换结果会直接显示出来,默认使用eww。如果想改用浏览器打开,请参阅markdown-ts-convert-display-function。这算是一种不太完整的“实时”预览。修改内容后目前还不会自动重新转换,也许未来会支持。

下面是使用eww的示例;为了演示效果,窗口是手动拆分的:

emacs_markdown_31_demo step 23

随手查阅规范

需要和别人争论规范时,可以运行M-x markdown-ts-browse-commonmark-specM-x markdown-ts-browse-gfm-spec打开对应的规范文档。

尝试使用egloteldoc

这项功能目前仍处于“实验中的实验”阶段,因此出了问题不要怪eglot的作者。请将 bug 报告提交给markdown-ts-mode

设置以下选项:

(setopt eglot-documentation-renderer #'markdown-ts-view-mode)

Eglot 就会尝试使用markdown-ts-mode渲染文档(通常是 LSP 服务器提供的 Markdown 文档)。

emacs_markdown_31_demo step 24

这部分还有一些细节需要打磨,实际效果可能有所不同。不过,还是希望你能帮忙测试。

试试各种选项

运行M-x customize-group RET markdown-ts RET,逐项查看设置。下面这些选项尤其值得先了解:

  • 显示相关的markdown-ts选项:隐藏标记、折叠号、省略号、项目符号、复选框、主题分隔线和硬换行字符、内嵌图片,以及打开文件时是否折叠内容
  • 代码块相关选项:markdown-ts-code-block-modesmarkdown-ts-default-code-block-modemarkdown-ts-enable-code-block-context-mode
  • 表格:markdown-ts-enable-table-modemarkdown-ts-table-auto-alignmarkdown-ts-table-default-column-width
  • 使用 markdown-ts-convert 导出
  • 使用 markdown-ts-toc 生成目录

每种 Markdown 元素都有对应的 Face,也都可以自定义。

如何提供帮助

最好的帮助方式就是直接使用它。拿你的 Markdown 文件试试,体验各种功能,看看哪些地方需要改进,或者哪些功能会出问题。

如果发现某些功能没有按预期工作,请在 Emacs 中使用 M-x report-emacs-bug RET 提交 bug。只要条件允许,请附上一个能够复现问题的最小示例。对于涉及字体化、tree-sitter 语法、表格、代码块,或与其他 mode 交互的问题,这一点尤其有帮助。

我们还在不断打磨各种细节,因此非常欢迎 bug 报告、反馈以及真实场景下的测试。

我发现了一个 bug,是因为 markdown-ts-mode 本身有问题吗?

使用 markdown-ts-mode 时遇到的某些意外情况,可能源于 mode 本身,也可能来自语法、tree-sitter 在 Emacs 中的集成方式,或者整个 tree-sitter 生态。提前了解这一点,有助于认识到这类问题的调试并不容易。

语法是共享的外部资源

语法并不是专为 Emacs 编写的。同一个 tree-sitter-markdown 还被其他编辑器和工具使用,因此对它的任何修改都需要在所有使用者之间协商。这对整个生态来说是好事,但也意味着我们希望看到的修复可能需要一段时间才能合并,甚至可能永远不会以我们理想的方式落地。遇到这种情况时,我们会尽可能在 mode 内部绕开问题,同时向上游报告。

所以,如果你发现某个问题看起来像是 mode 的 bug,最后却发现答案是“语法就是这样解析的”,现在你就知道这个答案从何而来了。即便如此,也请照样报告;我们宁可收到两次反馈,也不愿一次都收不到。

构建语法也有一些特殊之处。并不是所有语法都能只靠 make 和 C 编译器完成构建:有些语法是根据 JavaScript 定义生成的,因此构建过程中需要使用 tree-sitter CLI,有时还需要安装 Node.js。这也是预编译语法包和各发行版软件包如此受欢迎的重要原因之一。前面提过,我仍然更喜欢在 Emacs 中交互式地编译它们;不过现在你应该明白了,为什么发行版可能会额外拉取一大堆依赖。

间接缓冲区

这一点需要特别提醒,因为它常常让人感到意外:tree-sitter 和间接缓冲区并不

原始来源: Hacker News

评论 (0)