老实说,以前在 Neovim 里写 LaTeX 最让我头疼的不是编译,而是语法高亮和代码跳转。Vim 传统的正则语法虽然能用,但面对层层嵌套的\begin{...} \end{...}、数学模式里的$...$、以及各种自定义宏,经常会出现高亮错位、上下文识别不准确的问题,修改一处往往牵连整篇文档。后来我把树形解析器(tree-sitter)的 LaTeX 支持彻底配好之后,编辑体验几乎是质的提升——高亮变成结构化的,选中一个equation环境不用再手数行号,折叠也能做到真正“懂” LaTeX 的语义。这篇就围绕 Neovim 配置 tree-sitter 对 LaTeX 的支持,把从环境准备、核心配置到协作避坑、性能调优的完整路线理一遍,适合那些已经在用 Neovim 但还没吃透 tree-sitter LaTeX 能力的人参考。
1. 为什么 LaTeX 编辑需要树形解析器:一个经常被忽略的刚需
1.1 正则高亮的边界:嵌套环境与数学模式
很多人的 LaTeX 写作环境一开始都是“Vim + Vimtex”,这没有问题,Vimtex 到今天依然是这个领域绕不开的插件。但它的语法高亮底层是 Vim 的正则引擎,正则处理简单场景很顺手,一旦遇到深层嵌套就力不从心。最典型的例子是:\begin{align}里面再套一个\begin{aligned},外层环境还没闭合,里层环境的配色就已经开始乱了;或者一段文字里既有行内公式\( ... \),又有粗体和斜体混排,正则要同时处理“数学模式”和“强调模式”的优先级,很容易把$符号之后的整节文字都染成数学颜色。
这不是 Vimtex 的缺陷,而是正则模型的天花板——它对“层”的概念天然不敏感,只能靠一堆 look-behind、look-ahead 和分组来硬撑。而 tree-sitter 用的是增量解析器,它会把 LaTeX 源码解析成一棵具体的语法树,每个\begin和对应的\end在树里就是明确配对的两个节点,数学模式、注释、宏、参数都是独立类型。有了这棵树,高亮就变成了“给节点上色”的问题,而不是“猜文本含义”的问题。
1.2 tree-sitter 在 LaTeX 场景节省的精力
我用 tree-sitter 的 LaTeX parser 之后,感受最明显的是三点:第一,高亮不再错位,改代码结构的时候不会出现“颜色漂移”;第二,可以按语法单元做文本对象,比如直接“选中整个环境”“选中命令的某个参数”,而不需要自定义一堆正则快捷键;第三,折叠和跳转更像 IDE,折叠目标是\begin{...}环境或\section级别,跳转可以按解析树移动,而不是靠搜索花括号。
另外还有一个不容易察觉但很重要的点:tree-sitter 是增量解析,也就是文件被修改后只重新解析改动影响的那一小块范围。LaTeX 文档动不动几千行,如果每次都全量重新高亮,磨擦力会很明显。树形解析器基本能保持输入跟渲染的同步,这个舒服程度用一段时间就回不去了。
1.3 适用人群与前置知识
如果你满足以下任一条件,这篇配置文就能直接帮到你:正在 Neovim 中写学位论文或期刊文章,文档超过几十个\section,需要频繁在大段文字和数学环境之间切换;或者你已经在用 Vimtex,但对高亮效果不满意,想尝试 tree-sitter 作为补充;又或者你在别的编辑器里被 LaTeX 的实时渲染惯坏了,想在终端环境下找回结构化的编辑体验。前置知识不需要太多,会基本的 Neovim 配置、能用一种插件管理器(下面示例用 lazy.nvim),基本就够了。
2. nvim-treesitter 安装与 LaTeX parser 环境准备
2.1 Neovim 版本和依赖准备
tree-sitter 从 Neovim 0.9 开始就成了内置功能,不再需要单独装运行时库。但我还是建议直接用 Neovim 0.10 以上版本,最好升级到 0.11 稳定版。原因不是 0.9 不能用,而是后面很多查询(query)文件和高亮组设计是逐步向新版本靠拢的,旧版本会遇到一些 API 兼容问题,排查起来很麻烦。检查版本:
nvim --version如果你系统中版本太旧,建议不要走系统包管理器,直接从官方发布页拿编译好的二进制,或者用你熟悉的版本管理器装一个 stable 版本。Neovim 本身依赖的libuv、tree-sitter运行时都是自带的,不需要额外装什么。
但 tree-sitter 的 parser 本身是以动态库(.so)形式加载的,需要编译。所以系统里至少要有一组可用的 C 编译工具链。Debian/Ubuntu 系:
sudo apt install build-essential git unzipFedora/RHEL 系:
sudo dnf groupinstall "Development Tools"macOS 就直接用 Xcode Command Line Tools:
xcode-select --install如果选择从源码手动编译 parser,还可能需要tree-sitter-cli,但在最新的 nvim-treesitter 插件里,TSInstall命令一般会帮你处理好编译流程,先把编译链准备好即可。
2.2 安装 nvim-treesitter 并用 lazy.nvim 配置
nvim-treesitter 是 Neovim 社区维护的核心插件,LaTeX 的语法支持主要靠它的安装和查询体系。我用 lazy.nvim 作为示范,因为这现在是主流选择,其他插件管理器原理一样,只是声明方式不同:
{ "nvim-treesitter/nvim-treesitter", version = "*", -- 或者固定到某个 commit,保证稳定 build = ":TSUpdate", event = { "BufReadPost", "BufNewFile" }, main = "nvim-treesitter.configs", opts = { ensure_installed = { "latex", "bibtex" }, highlight = { enable = true, -- 如果想让 Vimtex 的照片语法负责某些部分,可在这里指定关闭 -- additional_vim_regex_highlighting = false, }, indent = { enable = true, disable = { "latex" }, -- LaTeX 缩进我们一般交给 vimtex 或手工控制 }, }, }这段配置里,ensure_installed指定 parser 列表。latex管.tex文件的解析,bibtex管参考文献库。version = "*"在某些场景有争议,因为 nvim-treesitter 的更新频率不算低,为了保证查询文件与 parser 版本匹配,建议要么固定到一个稳定的 commit,要么定期:TSUpdate统一升级,不要长期停在老版本然后怪高亮失效。
2.3 检查 LaTeX parser 是否真正生效
配置写好后,打开一个.tex文件,执行:
:TSModuleInfo latex旧版本插件可能还是:TSInstallInfo latex,都能看到 LaTeX parser 的安装状态。如果显示没有安装,就执行:
:TSInstall latex安装完成后,最重要的验证手段是打开 tree-sitter 的解析树面板。Neovim 0.10+ 可以用:
:InspectTree或者安装 nvim-treesitter 自带的 playground 命令:
:TSPlaygroundToggle把光标放到\begin{equation}上,应该能看到text.environment一类的节点名在面板里高亮。能看到这棵树,说明 tree-sitter 已经真正接管了 LaTeX 的语法分析,接下来的高亮、文本对象、折叠才有意义。
3. 核心配置:高亮、文本对象、折叠如何各司其职
3.1 高亮配置:ensure_installed 与 additional_vim_regex_highlighting
高亮是 tree-sitter 最直观的收益,但也是坑最多的地方。很多人配置完发现 latex 文件的高亮“没变化”,原因多半是 nvim-treesitter 的highlight.enable虽然开了,但 Vim 自身的 LaTeX 语法(runtimepath里自带的tex.vim)也在工作,两组颜色互相覆盖,最后呈现的效果就乱了。
我最终采用的方案是:对 LaTeX 明确关闭 Vim 原生的正则高亮,只保留 tree-sitter 的高亮。在 nvim-treesitter 配置里这样写:
highlight = { enable = true, additional_vim_regex_highlighting = false, },如果你只想关掉 LaTeX 语言、保留其他语言的正则高亮,也可以写成表的形式。这样做的好处是颜色分布完全由 tree-sitter 的 query 文件决定,Neovim 的高亮组体系会让 \LaTeX 命令、环境名、参数、数学符号各归其位,不会有灰色阴影覆盖彩色内容的违和感。
不过要记住一个前提:tree-sitter 只负责语法高亮,它不会替你解决编译问题,也不提供查看 PDF、快速补全一类的功能。所以 LaTeX 场景的正确姿态是让 tree-sitter 管结构化显示,让 Vimtex 管编译、查看、补全和目录导航,两者搭配而不是互相替代。
3.2 用 treesitter-textobjects 实现环境/参数/列表的快速选中
这是我觉得 tree-sitter LaTeX 支持里最“香”的一块,也是很多人没玩过的高级操作。配合 nvim-treesitter-textobjects 插件,可以直接按 LaTeX 语法单元定义文本对象:把光标放在一个\begin{itemize}环境内部,按一个键就能选中整个 itemize 环境;按另一个键只选中当前 item;光标在\frac{a}{b}上,则可以直接选中某个花括号参数。
插件安装后的配置示例如下:
{ "nvim-treesitter/nvim-treesitter-textobjects", dependencies = { "nvim-treesitter/nvim-treesitter" }, config = function() require("nvim-treesitter.configs").setup({ textobjects = { select = { enable = true, lookahead = true, keymaps = { ["ae"] = "@latex.environment.outer", ["ie"] = "@latex.environment.inner", ["a$"] = "@latex.arg.outer", ["i$"] = "@latex.arg.inner", ["ai"] = "@latex.item.outer", ["ii"] = "@latex.item.inner", }, }, move = { enable = true, goto_next_start = { ["]e"] = "@latex.environment.outer", ["]i"] = "@latex.item.outer", }, goto_previous_start = { ["[e"] = "@latex.environment.outer", ["[i"] = "@latex.item.outer", }, }, }, }) end, }这里的键位逻辑参考了社区里常见的映射习惯,ae表示 around environment,ie表示 inner environment,a$表示 around argument,i$表示 inner argument。用起来的效果是:在\begin{quote}里按vae,整个 quote 环境被选中;按vii或vai可以在 itemize 的当前条目范围里切换。这比我以前用vit选中到标签页那种粗糙方式要精确得多,尤其适合在长表格、长证明环境中微调内容。
3.3 折叠方案:treesitter foldexpr 与 vimtex 折叠的分工
折叠是我必须提醒你谨慎处理的部分。tree-sitter 提供了vim.treesitter.foldexpr(),理论上可以按语法树折叠 LaTeX 环境:
vim.wo.foldmethod = "expr" vim.wo.foldexpr = "v:lua.vim.treesitter.foldexpr()"这个方法对代码类语言很友好,但 LaTeX 用下来,问题很明显:它把每个\begin环境都当成一个折叠层级,结果文档里大量的equation、itemize、center会把折叠结构撑得很碎,反而看不清章节层级。真正写论文的时候,你最需要的是按\section、\subsection、\chapter折叠,或者至少按“顶层环境”折叠。
所以我在 LaTeX 场景下建议把折叠交给 Vimtex:
vim.g.vimtex_fold_enabled = 1Vimtex 对 LaTeX 的折叠语义理解更准确——它知道哪些环境值得折叠、哪些不值得,而且能保留section标题作为折叠文本。如果你非要用 tree-sitter 的折叠,请把disable = { "latex" }从 nvim-treesitter 的 indent 配置里放开,但foldmethod不要一股脑全局设置为expr,最好在 ftplugin 里针对.tex文件用 Vimtex 的折叠,其他代码文件用 tree-sitter 折叠。
4. 与 Vimtex、LaTeX 工具链协作的边界处理
4.1 谁负责高亮、谁负责补全、谁负责编译
我先给出一张我实际使用的分工表,避免你在不同插件之间来回折腾:
| 功能 | 负责人 | 原因 |
|---|---|---|
| 语法高亮 | tree-sitter | 结构化、增量解析、颜色稳定 |
| 环境选中 / 文本对象 | tree-sitter-textobjects | 按语法树操作,精准定位 |
| 编译与查看 PDF | Vimtex | 与 latexmk、Zathura/Skim 集成成熟 |
| 补全 / 引文 / 标签 | Vimtex + 补全引擎 | 依赖 LaTeX 项目结构而非纯语法 |
| 折叠 | Vimtex | 语义化折叠章节和环境,优于通用 foldexpr |
| 目录树导航 | Vimtex\ll或VimtexToc | 基于 TOC 快速跳转 |
这个分工不是绝对的,但遵循一个原则:凡是“需要理解文档结构”的功能,优先交给 Vimtex;凡是“需要精确解析语法树”的功能,优先交给 tree-sitter。两者不打架,反而互补。
4.2 capture 组如何自定义:queries/latex/highlights.scm
tree-sitter 的 LaTeX 高亮不是写死的,而是由 query 文件驱动。如果你想调整某些节点颜色,不建议直接改插件的默认文件,那会在升级时被覆盖。我更推荐在 Neovim 的配置目录里放自己的补充 query:
-- ~/.config/nvim/queries/latex/highlights.scm ; 这里可以追加你自己的高亮规则,比如把某个命令染成你喜欢的颜色 ; 实际 capture 名称以当前 parser 版本和 nvim-treesitter 自带的 highlights.scm 为准 (generic_command command: (command_name) @function.macro)写完后,用:InspectTree查看光标下节点的类型名,再用:Inspect查看当前高亮组来源。这是排查自定义高亮不生效的黄金组合。比如你可以发现\begin命令会被解析成text.environment或相关节点,然后决定在哪个层级覆盖颜色。我不建议一次性大改,先从一两个你最在意的节点开始,比如\section的颜色或数学环境名称的字体,逐步建立自己的高亮风格。
4.3 数学模式、verbatim 环境等特殊区域的显示策略
tree-sitter 把verbatim环境里的内容解析为 verbatim 节点,通常不做宏高亮,这符合语义,但也带来一个麻烦:如果你在\begin{minted}或\begin{lstlisting}里写代码,期望它带代码高亮,纯 tree-sitter LaTeX 支持是给不了你的。解决方案是 Vimtex 的vimtex_syntax_enabled里针对这些环境做特殊处理,或者直接让 Vim 原生语法为这些区块服务——也就是在additional_vim_regex_highlighting里只允许特定区域返回正则高亮。
数学模式又是另一个话题。tree-sitter 会把$...$、\[ ... \]、equation/align中的内容解析为 math 节点,高亮组通常链路到@text.math或@text.math.environment。如果你觉得公式里的变量没有斜体或颜色区分,可以在自己的highlights.scm里把@text.math链接到一个更明显的高亮组。但要注意别用力过猛,导致公式里到处是彩色,那反而干扰阅读。
5. 常见坑的完整排查链路
5.1 Parser 编译失败的排查链路
很多刚入门的人卡在第一步::TSInstall latex一直失败。排查顺序建议是这样:
- 先看错误日志:执行
:messages或:checkhealth nvim-treesitter,它会告诉你缺什么依赖。最常见的坑是系统里没有make和 C 编译器,parser 编译不过。 - 如果是网络原因导致无法下载 parser 源码,先确认能正常访问 GitHub(这一步在正常网络条件下基本没问题),再检查是否被本地代理规则干扰。这里不讨论任何代理工具,只强调保持系统网络通畅即可。
- 如果你手动 clone 了 nvim-treesitter,注意插件目录下有没有
parser目录,正常情况下安装成功后会生成对应语言的.so文件。手动清理一次 plugin 目录并重新:TSUpdate往往能解决半残状态。
最近版本的 nvim-treesitter 对预编译产物支持也还可以,但那个依赖 GitHub Releases 的 CDN,版本变动时偶尔会有二进制不匹配,我就是遇到过latex.so下载成功但加载报段错误的情况。解决办法很简单:卸载 parser 重建:
:TSUninstall latex :TSInstall latex如果还不行,就把version = "*"去掉,固定到一个稳定 commit,然后重新:TSUpdate,这能排除插件主分支和 parser release 不同步的问题。
5.2 高亮不生效/颜色不对的排查链路
高亮不生效,首先确认不是termguicolors的问题。Neovim 里 tree-sitter 高亮依赖真彩色,如果你没有设置:
vim.opt.termguicolors = true那很多@text.*高亮组会退化成终端 256 色,视觉上非常平淡。确认之后还没变化,就依次查:
- 打开
.tex文件,执行:TSModuleInfo latex,确认 parser 已加载。 - 执行
:Inspect,看光标处的单词来自哪个语法源。如果显示tex.vim而不是 treesitter,说明 Vim 原生语法在优先接管,把additional_vim_regex_highlighting关掉或针对latex关掉即可。 - 如果
:Inspect显示来自 treesitter 但颜色还是不对,那就检查你的 colorscheme 是否定义了@text.latex.*一类的高亮组。很多主题对 LaTeX 的数学环境支持不够全,需要你手动 link 到已有高亮组,比如:
vim.api.nvim_set_hl(0, "@text.math", { link = "Special" }) vim.api.nvim_set_hl(0, "@text.environment.name", { link = "Identifier" })这种做法比改 query 文件更轻量,而且不会因为插件升级而覆盖。
5.3 新老 parser 的 capture 变化
tree-sitter-latex 这个 parser 经过一次较大的重构,capture 名称也变过。如果你在网上搜教程,会发现有人写@function.macro、@include,有人写@text.macro、@text.reference,其实它们对应不同时期的 parser/query 版本。遇到旧教程里的 capture 不生效,不要急着怀疑配置,先到 nvim-treesitter 的安装目录里打开:
~/.local/share/nvim/lazy/nvim-treesitter/queries/latex/highlights.scm看一眼当前版本实际使用的 capture 名,然后照着改你自己的自定义 query。这个思路适用于所有 tree-sitter 语言,不只 LaTeX。
5.4 大型文档性能问题的优化手段
LaTeX 写书或写大论文时,一个.tex文件可能上万行。tree-sitter 虽然是增量解析,但第一次打开文件还是要全量构建语法树,几百毫秒到一两秒的卡顿都可能出现。我目前的优化策略:
- 对超过一定大小的
.tex文件,延迟启动 tree-sitter,比如打开后等用户空下来再开始解析:
vim.api.nvim_create_autocmd("BufReadPost", { pattern = "*.tex", callback = function() local buf = vim.api.nvim_get_current_buf() local size = vim.api.nvim_buf_line_count(buf) if size > 5000 then vim.defer_fn(function() vim.treesitter.start(buf, "latex") end, 300) end end, })- 不要开太多同时需要解析的窗口,特别是分屏一边写
.tex一边写.bib时,两个 buffer 都在构建语法树,压力会叠加。 - 关掉 LaTeX 的 tree-sitter 缩进(我在第一节配置里就
disable = { "latex" }),因为缩进计算在某些复杂宏环境里开销不小,而且 LaTeX 也不像代码那样依赖缩进表达层级。
实测下来,几千行的普通论文完全不需要担心,几万行的书稿用上面延迟启动的方式,体验还在可接受范围内。
6. 实测性能对比与最终配置参考
6.1 大文档、嵌套环境的实际表现
我拿一篇带 TikZ 插图、大量数学公式和长表格的论文做了对比测试。同样一份 2000 行的.tex,纯 Vim 原生语法高亮在快速滚动时偶尔会出现渲染卡顿,而且\begin{align*}内部的多行公式高亮偶尔会整体断掉。开启 tree-sitter 之后,滚动明显更顺滑,增量编辑时高亮也不会闪。TikZ 里那些层层嵌套的scope、node参数,tree-sitter 也能稳定解析成节点,虽然它不理解 TikZ 语义,但至少不会因为方括号配对错乱导致后续整段高亮崩溃。
嵌套环境方面,我故意构造了一个五层嵌套的\begin{equation}套\begin{aligned}套\begin{array}套\begin{minipage}套\begin{center}的结构,tree-sitter 的高亮依然稳定,InspectTree里的层级清清楚楚。这一点是正则方案做不到的。
6.2 一份可直接抄作业的完整配置快照
最后给出一份我当前在用的精简配置快照,它把 nvim-treesitter、textobjects 和 Vimtex 的协作关系理顺了,你可以在此基础上改成自己的键位:
-- lazy.nvim 插件声明片段 { "nvim-treesitter/nvim-treesitter", version = "*", build = ":TSUpdate", event = { "BufReadPost", "BufNewFile" }, main = "nvim-treesitter.configs", opts = { ensure_installed = { "latex", "bibtex" }, highlight = { enable = true, additional_vim_regex_highlighting = false, }, indent = { enable = true, disable = { "latex" }, }, }, }, { "nvim-treesitter/nvim-treesitter-textobjects", dependencies = { "nvim-treesitter/nvim-treesitter" }, config = function() require("nvim-treesitter.configs").setup({ textobjects = { select = { enable = true, lookahead = true, keymaps = { ["ae"] = "@latex.environment.outer", ["ie"] = "@latex.environment.inner", ["a$"] = "@latex.arg.outer", ["i$"] = "@latex.arg.inner", ["ai"] = "@latex.item.outer", ["ii"] = "@latex.item.inner", }, }, }, }) end, }, { "lervag/vimtex", lazy = false, -- 打开 tex 文件时自动启动 config = function() vim.g.vimtex_fold_enabled = 1 vim.g.vimtex_view_method = "zathura" -- 按你的 PDF 阅读器调整 vim.g.vimtex_compiler_method = "latexmk" end, },配置好之后,打开一个.tex文件,试试vae选中整个环境、]e跳到下一个环境开头,再配合\ll编译、\lv查看 PDF,整个 LaTeX 写作链路就完整了。
6.3 我的个人使用习惯与建议
如果让我从零再配一次,我会先确认 Neovim 版本和编译链,再一次性把latex和bibtexparser 装好,然后花十分钟熟悉InspectTree这个工具,因为后面的所有自定义高亮都离不开它。tree-sitter 的 LaTeX 支持不是那种装上就完事的插件,它值得你花一点时间把 capture 名称、文本对象、与 Vimtex 的分工都摸一遍,这部分的收益会持续体现在每一篇文档的编辑过程里。
一个小技巧收尾:把:InspectTree绑成一个顺手快捷键,比如;t,写 LaTeX 遇到高亮不对或想精确选中某段内容时随手看一眼语法树,比盲改 query 快得多。我在调整公式环境和列表环境时,基本都要靠这棵“树”来定位,配合 textobjects,整套编辑节奏比早期只靠正则高亮时利落太多了。