在 Vim 里写 LaTeX 的人,并不是为了 LaTeX 才选择 Vim 的。他们本来就住在 Vim 或 Neovim 里,只希望 LaTeX 能自己找上门来。裸装的 Vim 其实认得 .tex——它自带语法文件和文件类型插件——但那份 ftplugin/tex.vim 的头部至今写着 Last Change: Wed 19 Apr 2006。没有编译,没有 PDF,没有 \ref 补全,也没有章节列表。把这一切带过来的是 Karl Yngve Lervåg 写的 vimtex。本页讲 vimtex 比裸 Vim 多出什么、\ll 背后常驻的 latexmk 编译、让你用 Vim 自己的语法去编辑环境与命令的文本对象,以及 Vim 与 Neovim 真正不同的地方。
裸装 Vim 对 LaTeX 究竟知道多少
裸装 Vim 对 .tex 的了解,就是配色加上三个小技巧:[d 可以跳到 \newcommand 或 \def 的定义处;gf 与 [i 会把 \include{...} 里的内容当作文件名;启用 matchit 之后,% 能在 \begin{...} 与 \end{...} 之间来回跳。整套实现只有 ftplugin/tex.vim 的四十来行,今天的 Vim 9.1 仍原样附带,署名 Benji Fisher,标着 Version: 1.4 / Last Change: Wed 19 Apr 2006。语法文件 syntax/tex.vim 倒是长到了 Version 121,但它开头写着:这份运行时文件正在寻找新的维护者。
裸装 Vim 还带着一份编译器定义 compiler/tex.vim。执行 :compiler tex 后,makeprg 变成 latex -interaction=nonstopmode,errorformat 也被填入一大串读取 LaTeX 日志的模式,于是一句 :make 就能把错误排进 quickfix 列表。也就是说,裸 Vim 已经足以完成「写、:make、跳到出错行」。它做不到的是:打开 PDF、在源码与 PDF 之间往返、补全 \ref 或 \cite、以及沿着文档结构导航。vimtex 把这些全部接了过来,并且对文件类型 tex 直接接管 Vim 内置的 TeX 插件。
" What bare Vim gives you, without any plugin at all.
packadd! matchit " % jumps between \begin{...} and \end{...}
compiler tex " :make runs latex and fills the quickfix list新建的 .tex 为何变成 plaintex,vimtex 为何没反应
答案很简单:Vim 是靠读文件内容来猜的。 Vim 9.1 的 autoload/dist/ft.vim 里的 FTtex() 先看首行的 %&格式,再从第一行非注释行起扫描一千行,寻找 \documentclass、\usepackage、\begin{、\newcommand 或 \renewcommand。若一个都没找到,就退回默认值;而在未设置 g:tex_flavor 时,这个默认值是 plain——于是文件类型成了 plaintex 而不是 tex。刚新建的空文件,或者还没写 \documentclass 的片段,正属于这种情况。
有意思的是,关于这件事最常见的建议——在 vimrc 里写 let g:tex_flavor = 'latex'——在 vimtex 这里是反过来的。vimtex 用自带的 ftdetect/tex.vim 直接覆盖了文件类型检测,并替你把 g:tex_flavor 设成 latex。它的文档也明说了原因:正是为了避免 .tex 被默认识别成 plaintex 这种出人意料的行为。所以只要装了 vimtex,就不必自己写 g:tex_flavor;反过来,把它设成 latex 以外的值,才是拒绝 vimtex 接管的办法——那才是这个选项真正有用的场合。
vimtex 安装,以及为何不能延迟加载
直说:不要延迟加载 vimtex。 原因是机制上的,不是口味问题。反向搜索(从 PDF 回到源码)依赖 :VimtexInverseSearch 这个全局命令,阅读器是从编辑器外部去调用它的;插件主体还没加载,这条命令就不存在。何况 vimtex 本身是文件类型插件,又用了 autoload 机制,本来就只在需要时才加载——让插件管理器再延迟一次已无收益。在 lazy.nvim 里写 lazy = false,在 vim-plug 里不要加 for。
还有两个前提。其一是编辑器版本。自 2026 年 7 月发布的 vimtex 2.18 起,它要求 Vim 9.2 或 Neovim 0.12.4,更旧的版本根本不会加载。若必须停留在旧编辑器上,正确做法是把插件固定在 v2.17 标签;let g:vimtex_version_check = 0 可以让检查闭嘴。其二是 filetype plugin on 与 syntax enable。缺了前者,vimtex 压根不加载;缺了后者,所有依赖语法信息的功能——判断数学区域、i$ 文本对象——都会失效。即便在 Neovim 里偏向 Tree-sitter,也最好别关掉 Vim 自身的语法功能。
call plug#begin()
Plug 'lervag/vimtex'
" Pin an older tag if you are stuck on Vim < 9.2:
" Plug 'lervag/vimtex', { 'tag': 'v2.17' }
call plug#end()
filetype plugin indent on " required (indent is optional)
syntax enable " required for math zones, i$ and friends
set encoding=utf-8 " needed in Vim, not in Neovim
let maplocalleader = ' ' " Space as <localleader>; default is backslash
let g:vimtex_view_method = 'zathura'在 Neovim 里用 Lua 写同样的内容,配置放进 init,好让它们在插件主体加载之前生效。maplocalleader 是几乎所有 vimtex 命令的入口——默认前缀由 g:vimtex_mappings_prefix 决定,其默认值是 <localleader>l,而 <localleader> 本身默认是反斜杠——所以明确写出来能省去日后的困惑。保持反斜杠就敲 \ll;改成空格,就是空格接 ll。
return {
"lervag/vimtex",
lazy = false, -- never lazy-load: it breaks :VimtexInverseSearch
init = function()
vim.g.maplocalleader = " "
vim.g.vimtex_view_method = "zathura" -- "skim" on macOS
vim.g.vimtex_compiler_method = "latexmk"
end,
}\ll:latexmk 常驻,每次保存 PDF 都跟上
按一次 \ll,latexmk 就以常驻模式跑起来;再按一次则停下。它之所以是开关式的,是因为默认的编译器设置 g:vimtex_compiler_latexmk 打开了 continuous,底层用的是 latexmk 的连续监视。此后每次保存都会触发重新编译,阅读器里的 PDF 会自己跟上。只想跑一次就用 \lS(:VimtexCompileSS);想停下就按 \lk(要停掉全部项目则是 \lK)。
| 按键 | 命令 | 作用 |
|---|---|---|
\ll | :VimtexCompile | 开始或停止常驻编译(开关式) |
\lS | :VimtexCompileSS | 单次编译,与 CI 里的一次性运行相同 |
\lv | :VimtexView | 打开 PDF 并正向搜索到光标处 |
\lt | :VimtexTocOpen | 打开目录缓冲区(\lT 为开关) |
\le | :VimtexErrors | 把错误与警告排进 quickfix 窗口 |
\lo | :VimtexCompileOutput | 显示编译器的原始输出 |
\lc | :VimtexClean | 清理辅助文件(\lC 连输出一起删) |
\li | :VimtexInfo | 显示识别到的根文件、宏包等状态 |
\ls | :VimtexToggleMain | 切换当前文件是否被当作根文件 |
默认传给 latexmk 的选项有四个:-verbose、-file-line-error、-synctex=1、-interaction=nonstopmode。由于 -synctex=1 一开始就在,后文前向与反向搜索所需的同步数据无需你做任何配置就会被写出。错误处理方面,g:vimtex_quickfix_mode 默认为 2——quickfix 窗口会自动打开,但不抢焦点——正适合一边写一边用余光看错误。想换掉编译器本身,就把 g:vimtex_compiler_method 设为 latexmk(默认)、latexrun、tectonic、arara、texpresso 或 generic 之一。
当 \ll 调用了另一个引擎:-pdf 与 $pdf_mode 的冲突
在 .latexmkrc 里写了 $pdf_mode = 3,构建却仍不走 DVI 路线——原因是 vimtex 每次都会往命令行上加一个引擎标志。引擎取自对照表 g:vimtex_compiler_latexmk_engines,其默认键 _ 对应 -pdf。命令行上的 -pdf 优先级高于 .latexmkrc 里的 $pdf_mode,于是配置文件被悄悄覆盖。vimtex 确实会从 $pdf_mode 推断引擎,但正如文档写明的,只支持 1(pdfLaTeX)、4(LuaLaTeX)、5(XeLaTeX) 三个值,不含走 DVI 的 3。
正确的做法是在主文件开头写一行 TeX 程序指令。右侧必须是上面那张对照表里的键,所以 LuaLaTeX 写 lualatex,走 DVI 则写 pdfdvi(对应 -pdfdvi)。然后把 upLaTeX 与 dvipdfmx 的实际调用放进 .latexmkrc——这正是日文论文的经典组合。latexmk 的配置本身归自动构建那一页管,细节请看那里。
% In the main .tex file, first line: pick the key, not the binary name.
% !TeX program = pdfdvi
# .latexmkrc -- upLaTeX and dvipdfmx do the actual work
$latex = 'uplatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
$bibtex = 'upbibtex %O %B';
$biber = 'biber --bblencoding=utf8 -u -U --output_safechars %O %S';
$makeindex = 'upmendex %O -o %D %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;同时把 -synctex=1 传给 $latex 是关键,这样即便经过 DVI,同步数据也能一路带到 PDF。若不用 latexmk,情况就不同了:切到 Tectonic 只要把 g:vimtex_compiler_method 设成 tectonic,既不用选引擎,也不需要 .latexmkrc。
文本对象:用 Vim 自己的语法编辑 \begin{...}
这是在 Vim 里写 LaTeX 无可替代的唯一理由。Vim 的编辑建立在「动词 + 对象」的语法上:d(删除)或 c(更改)配上 iw(单词内部)或 ap(整个段落)。vimtex 把 LaTeX 自身的结构加进了这套词汇。dae 会连 \begin{...} 到 \end{...} 一起删掉,cie 只替换其中的内容,ci$ 则只重写行内公式的内部。要删掉一个三十行的 align 环境,再也不用数行数了。
| 对象 | 匹配范围 | 典型用法 |
|---|---|---|
ie / ae | 环境(不含最外层的 document) | dae 连环境一起删;cie 只换内容 |
i$ / a$ | 数学环境($...$、\[...\]) | ci$ 只重写公式内部 |
ic / ac | 命令及其参数 | dac 把整个 \textbf{...} 去掉 |
id / ad | 成对的定界符 | LaTeX 版的 ci(,也能抓住 \left(...\right) |
iP / aP | 一节(section) | daP 一次移动或删除整节 |
im / am | 单个 \item | 正好抓住列表中的一项 |
此外还有一组用来改写结构的三件套:ds 去掉外层,cs 更换外层,ts 切换。dse 剥掉环境只留内容,cse 把 itemize 换成 enumerate(候选由补全给出),dsc/csc 对命令做同样的事,dsd/csd 则针对定界符。tse 切换环境,tss 切换环境的带星形式,tsc 切换命令的星号,tsd 在 (...) 与 \left(...\right) 之间往返。移动方面,% 在配对之间跳,]]/[[ 前往下一个与上一个节首,][/[] 前往节尾,]m/[m 跳环境,]n/[n 跳公式。把光标放在某个命令上按 K,就会打开该宏包的文档。
在长文档里走动:目录缓冲区与 \ref 补全
按下 \lt,整份文档的目录会作为一个普通缓冲区打开。既然是普通缓冲区,就可以用 / 搜索、用 j/k 走动、按 Enter 跳到那一节。即便文档拆成多个文件,vimtex 也会从根文件沿着 \input 与 \include 走下去,于是得到一份横跨各章的完整目录。默认是宽 50 列的分割窗口,外观与行为通过 g:vimtex_toc_config 调整。当前编辑的文件是否算作根文件,可用 \ls 切换;\li 则能看到 vimtex 认定的根文件是哪一个。
补全搭在 Vim 自己的机制上。在 tex 缓冲区里,omnifunc 会被自动设为 vimtex#complete#omnifunc(g:vimtex_complete_enabled 默认开启),因此在插入模式按 Ctrl-X Ctrl-O 就会弹出候选。紧接 \cite{ 时列出 .bib 与 \bibitem 里的文献键;\ref{ 之后列出文档中的 \label;\usepackage{ 之后列出本机的 .sty;\includegraphics{ 之后列出文件名。每次都敲那组键很累,所以实际使用中会把 omnifunc 接到补全引擎上——Neovim 用 nvim-cmp 的 omni 源,Vim 与 Neovim 通用则用 coc.nvim 的 coc-omni 扩展。分工始终不变:候选由 vimtex 生成,何时以何种方式呈现由补全引擎决定。
local cmp = require("cmp")
cmp.setup({
sources = cmp.config.sources({
{ name = "omni" }, -- pulls vimtex candidates through omnifunc
}),
})Vim 与 Neovim 真正的差别
就编辑功能而言,文本对象、目录、补全在两者中完全一样。差别只在一点:外部能不能反过来叫住编辑器。反向搜索需要阅读器回头找编辑器,在 Vim 里这条通路是 +clientserver 特性。vimtex 的文档明确写着:在 Windows 或 gVim 下服务器会自动启动,但在 Linux 或 macOS 的终端里运行的 Vim 不会。Neovim 根本没有 clientserver,改用 MessagePack-RPC;vimtex 对两者的处理方式相同,地址都落在 v:servername 里。
而且「终端里的 Vim 只要把服务器启起来就行」也未必成立。macOS 自带的 /usr/bin/vim 报告的是 -clientserver——该特性在编译时就被去掉了,因此 remote_startserver() 根本不存在,写下面这段代码也毫无作用。此时的选择是:换一个带 +clientserver 的构建,例如 MacVim 或 Homebrew 的 Vim;或者转投 Neovim。反过来说,本节的实务结论正是:Neovim 这一侧什么都不用配。还有两处小差异:Vim 需要把 encoding 设为 utf-8,Neovim 不需要;所要求的编辑器版本也分别写作 Vim 9.2 与 Neovim 0.12.4。
" Vim only, and only in a build that has +clientserver.
if empty(v:servername) && exists('*remote_startserver')
call remote_startserver('VIM')
endif选择阅读器与反向搜索的命令行
这里最容易被误解的是 g:vimtex_view_method 的默认值。它并不会按平台聪明地挑选:在任何操作系统上默认都是 general,也就是退回到通用启动方式——Linux 上是 xdg-open,macOS 上是 open,Windows 上是 SumatraPDF 之类。这个通用阅读器由 g:vimtex_view_general_viewer 指定。真正提供的专用方式是 zathura、zathura_simple、skim、mupdf、galley 以及 sioyek;并不存在 sumatrapdf 这个值。在 Windows 上使用 SumatraPDF 是走 general 这条路。SyncTeX 本身的原理另有专页,这里只看配置。
正向搜索(源码 → PDF)只要按 \lv,几乎不用配置。麻烦的是反向搜索(PDF → 源码),你得告诉阅读器「被点击时执行这条命令」。这条命令的内容就是 VimtexInverseSearch <行号> <文件>。唯一要留神的是,表示行号与文件名的占位符在各阅读器里写法不同:zathura 用 %{line} 与 %{input},Skim 用 %line 与 %file,SumatraPDF 用 %l 与 %f。只要设了 g:vimtex_view_method = 'zathura',vimtex 就会带 -x 启动 zathura 并替你把这条命令传过去,因此在多数环境下什么都不用写,Ctrl 点击就能回到源码。
# Linux: zathura. Ctrl-click in the PDF jumps back to the source.
set synctex true
set synctex-editor-command "nvim --headless -c 'VimtexInverseSearch %{line} %{input}'"macOS 的 Skim:打开偏好设置的同步标签页,把预设改为 Custom,并登记命令与参数;随后 Cmd-Shift-点击即可反向搜索。Windows 的 SumatraPDF:在设置中的反向搜索命令行一栏填入同样形状的一行;反向搜索是双击。若用 gVim,请把 nvim --headless 换成 vim -v --not-a-term -T dumb。
# macOS, Skim: Preferences > Sync > Preset: Custom
Command: nvim
Arguments: --headless -c "VimtexInverseSearch %line '%file'"
# Windows, SumatraPDF: Settings > Options > inverse search command-line
cmd /c start /min "" nvim --headless -c "VimtexInverseSearch %l '%f'"最先要固定下来的四个动作
vimtex 不是必须在动笔之前先掌握的工具。用 \ll 启动常驻编译、保存、用 \lv 看 PDF 里的对应位置、用 \le 只看错误——这四下进了手指,其余功能等到需要那天再一个个加就够了。文本对象同样如此:先从 dae 与 cse 两个开始,再扩展到 ci$、tsd,很快就能看清自己每天真正在做哪些操作。
在把文档拆成多个文件之前,有一件事值得先确认一次:从章节文件里按 \ll,产出的还是同一份 PDF 吗? 用 \li 看 vimtex 认定的根文件,如果不对,就用 \ls 切换,或者用 % !TeX root = main.tex 指向主文件。先把这里定下来,之后再加补全引擎与代码片段时,构建这一侧就不会再晃动。