VS Code (LaTeX Workshop)

在一个刚装好、没有任何扩展的 Visual Studio Code 里打开 .tex,代码就已经有了配色。因为 VS Code 自带一份 LaTeX 语法定义,而这份语法正是从 LaTeX Workshop 扩展里剥离出来的。不过,配色也就是 VS Code 对 LaTeX 所知的全部。编译、显示 PDF、在源码与 PDF 之间双向跳转,全都由 LaTeX Workshop 负责,真正排版的则是你装在本机上的 TeX 发行版——扩展只是把它当作子进程启动。本页讲三件事:描述一次构建的「工具与配方」两层结构;默认配方为何会给大量文档调用另一个引擎;以及内置 PDF 阅读器与 SyncTeX。

VS Code 自己知道多少 LaTeX

一个未加任何扩展的 VS Code 只注册了三个语言 ID 及各自的语法:.sty.cls 对应 tex.tex 对应 latex.bib 对应 bibtex。仅此而已——没有构建命令,没有 PDF 阅读器,没有补全,也不能跳到 \ref 的定义。这些语法文件出自 jlelong/vscode-latex-basics 仓库,其 README 明确写着这些文件「原本属于」LaTeX Workshop;VS Code 从 2022 年 1 月的版本起随包发布。也就是说,你打开 .tex 时看到的配色,早在安装扩展之前就已经是这个扩展的成果。

其余一切都由 LaTeX Workshop 承担(作者 James Yu,市场 ID 为 James-Yu.latex-workshop)——唯独不包括 TeX 本身。扩展只是把 latexmkpdflatexbiber 这类可执行文件作为子进程启动,再读回它们的输出,因此它的健康状况不可能超过背后的发行版:TeX Live、MiKTeX 或 MacTeX。由此可以得出一条排查原则:在改动任何一项设置之前,先在终端里把同一个项目编译一遍。 如果 latexmk 在终端就失败,改再多 settings.json 也无济于事;反之,终端能通而扩展仍报「找不到命令」,那么可疑的是 VS Code 继承到的环境变量,而不是扩展。

安装本身平平无奇——扩展视图 (Ctrl/Cmd+Shift+X),搜索「LaTeX Workshop」即可。但随之而来的几乎就是全部工作面:构建命令、PDF 预览、补全、从 \ref\cite 跳到目标、文档大纲,以及沿着 \input\include 组装出的项目文件树——自动构建监视的也正是这份清单。如果你为安装 TeX 改过 PATH,请重启 VS Code,最好注销后重新登录,让新环境被读到。然后先在终端确认发行版是否有回应。

terminal
# does the TeX distribution answer at all?
latexmk --version

# does the project build outside the editor?
latexmk -pdf main.tex

# with a .latexmkrc that already picks the engine, no flags are needed
latexmk main.tex

# is the extension looking at the same PATH you are?
which latexmk

一旦终端能编译成功,剩下的问题就都在 VS Code 这一侧,且只剩三个:跑的是哪个配方哪个文件是根文件PDF 显示在哪里。本页余下的内容就是这三件事。

工具与配方:读懂 latex-workshop.latex.recipes

一次构建分两层书写。工具(latex-workshop.latex.tools) 定义「要启动的一条命令」,由 namecommand(可执行文件)和 args(参数数组)组成。配方(latex-workshop.latex.recipes) 则是「按顺序排列的工具名列表」。latexmk 是只含一个工具的配方,pdflatex -> bibtex -> pdflatex * 2 是含四个工具的配方。之所以分成两层,是因为同一个可执行文件常常要以不同参数反复使用:内置工具包括 latexmklualatexmkxelatexmklatexmk_rconlypdflatexbibtextectonic 等,配方只是把它们重新组合,而不是重复定义命令。

terminal
{
  "name": "latexmk",
  "command": "latexmk",
  "args": [
    "-synctex=1",
    "-interaction=nonstopmode",
    "-file-line-error",
    "-pdf",
    "-outdir=%OUTDIR%",
    "%DOC%"
  ],
  "env": {}
}

逐个读参数,就能看出这套设计。-synctex=1 要求输出下文所说的 SyncTeX 对照表;-interaction=nonstopmode 让编译遇错也不停下来等输入,一直跑到结束;-file-line-error 把错误打成 main.tex:42: Undefined control sequence 的形式,正是这一项让扩展能从「问题」面板直接跳到那一行。-pdf 则是告诉 latexmk「用 pdfLaTeX 直接生成 PDF」——这个开关会在本页后面制造麻烦。形如 %…% 的记号是占位符,扩展在启动前替换掉。

占位符展开为
%DOC%根文件的路径(不含扩展名)
%DOC_EXT%根文件的路径(含扩展名)
%DOCFILE%仅根文件名(不含扩展名)
%DIR%根文件所在目录;outDir 的默认值
%OUTDIR%latex-workshop.latex.outDir 指定的输出目录
%TMPDIR%存放中间文件的临时目录,可保持源目录整洁
%WORKSPACE_FOLDER%当前打开的工作区路径

究竟跑哪个配方,由 latex-workshop.latex.recipe.default 决定。默认值是 "first"——意思是取列表里的第一项;改成 "lastUsed" 则会记住你上次选过的配方。启动构建的快捷键是 Ctrl+Alt+B(Mac 为 Cmd+Alt+B)。想临时挑一个配方运行,就用命令面板里的「LaTeX Workshop: Build with recipe」;想按文件固定,则在第一行写 %!LW recipe=latexmk (lualatex)。不过当你从面板里手动选择配方时,这条指令会被忽略。

默认配方为什么会调用另一个引擎

答案就在上面那段工具定义里:-pdf 是告诉 latexmk「用 pdfLaTeX 直接生成 PDF」的参数。仅这一个词,就解释了相当一部分「终端能编译、VS Code 不行」的报告。如果导言区加载了 fontspec——用 OpenType 字体的、用 unicode-math 的、以及大多数较新的模板——构建会停在 ! Fatal Package fontspec Error: The fontspec package requires either XeTeX or LuaTeX.。若是直接键入中文、日文或韩文,则会得到 ! LaTeX Error: Unicode character こ (U+3053) not set up for use with LaTeX.。两条消息都没有提到 VS Code,因为问题不在 VS Code。

修法只有一条——换一个配方,按持久程度从低到高有四种。只此一次:命令面板里的「Build with recipe」。只对这个文件:第一行写 %!LW recipe=…今后都用我上次选的:把 latex-workshop.latex.recipe.default 设为 "lastUsed"整个项目固定:在 settings.json 里重排 latex-workshop.latex.recipes,把想要的配方放到第一位(因为默认值是 "first")。内置配方列表里已经有 latexmk (lualatex)latexmk (xelatex)latexmk (latexmkrc),所以多数时候你只是在挑选,而不是在编写。

把引擎写进 .latexmkrc,而不是 settings.json

写在编辑器设置里的引擎选择,永远走不出你这台机器。写进 .latexmkrc,它就随项目一起走——合作者的 TeXstudio、CI 容器、以及一句光秃秃的 latexmk main.tex,结果都一样。内置配方 latexmk (latexmkrc) 正是为此准备的:它只执行 latexmk %DOC%,不额外添加任何参数。下面是 upLaTeX + dvipdfmx 的写法,这一组合长期是日文论文的定式:

latex
$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';
$pdf_mode = 3;
$max_repeat = 5;

关键在 $pdf_mode 这一行。3 表示「先生成 DVI,再用 $dvipdf 转成 PDF」;1 是 pdfLaTeX 直出;4 是 LuaLaTeX。索引交给能排序日文的 upmendex,文献交给 upbibtex%S%O%D%B 是 latexmk 自己的占位符,分别代表源文件、额外选项、输出目标和不含扩展名的基名——与扩展的 %DOC% 属于两套体系,切勿混用。还有一处不起眼但要紧:$latex 里带着 -synctex=1;一旦漏掉,下文的点击跳转就会悄无声息地失效。

反过来,如果你想不用 .latexmkrc、只在 settings.json 里解决,就自己写工具和配方,并把配方放在首位。下面是用 LuaLaTeX 排版的自足示例(LuaLaTeX 通过 luatexjaltjsclasses 系列排日文,无需绕道 dvipdfmx):

terminal
{
  "latex-workshop.latex.tools": [
    {
      "name": "lualatexmk",
      "command": "latexmk",
      "args": [
        "-synctex=1",
        "-interaction=nonstopmode",
        "-file-line-error",
        "-lualatex",
        "-outdir=%OUTDIR%",
        "%DOC%"
      ],
      "env": {}
    }
  ],
  "latex-workshop.latex.recipes": [
    { "name": "lualatexmk", "tools": ["lualatexmk"] }
  ]
}

最先要定的三个设置:输出目录、自动构建、PDF 显示位置

设置写在 settings.json 里。按 Ctrl/Cmd+, 打开设置界面,用右上角的「打开设置 (JSON)」,可以编辑全局用户文件,也可以编辑项目根目录下的 .vscode/settings.json凡是希望合作者或构建服务器共享的,一律放进后者。 几十项设置中,值得一开始就定下来的有三项:

  • latex-workshop.latex.outDir — 中间文件与 PDF 的输出目录。默认是 %DIR%,即与 .tex 同一位置。设为 %DIR%/out 可以让 .aux.log.fls 不再散落在源目录里,.gitignore 也只需一行。
  • latex-workshop.latex.autoBuild.run — 触发自动构建的时机。默认 onFileChange,在磁盘上监视依赖文件,因此编辑器之外的改动也会触发。另有 onSave(仅保存时)与 never(仅手动)。如果你常常搞不清是什么引发了构建,onSave 是更易读的选择。
  • latex-workshop.view.pdf.viewer — PDF 显示在哪里:tab(默认,VS Code 内的选项卡)、browser(默认浏览器)、external(外部程序,仍属实验性)。想顺畅使用 SyncTeX,就选 tab
terminal
{
  "latex-workshop.latex.outDir": "%DIR%/out",
  "latex-workshop.latex.autoBuild.run": "onSave",
  "latex-workshop.view.pdf.viewer": "tab",
  "latex-workshop.latex.recipe.default": "lastUsed"
}

分离输出目录有一个陷阱:改动 outDir 的同时,也改变了扩展去哪里寻找 .aux.fls。一旦这个位置与实际构建写出的位置不一致,就会出现 PDF 明明生成了扩展却找不到、交叉引用始终解析不了的状况。请让两者保持一致——尤其当你的 .latexmkrc 里也设了 $out_dir 时。中间文件的清理由 latex-workshop.latex.autoClean.run 负责,不过既然都归到了 out/,直接删掉整个文件夹即可,多数情况下用不上它。

一边编辑章节文件一边构建 main.tex% !TEX root

在子文件的第一行写上 % !TEX root = ../main.tex。仅此一句,即使只打开了这一章,构建也会从主文档开始。它之所以奏效,是因为 LaTeX Workshop 按五个步骤寻找根文件,而这条魔术注释正是第一步:(1) % !TEX root;(2) 当前打开的文件自身是否含有 \documentclass\begin{document};(3) 逐一查看工作区顶层的 .tex,寻找带类声明的那个;(4) subfiles 宏包的组织方式;(5) 分析 .fls 文件。放着不管往往也能猜对——但在有几十个章节文件的学位论文里,「靠猜」本身就是事故的源头。

latex
% !TEX root = ../main.tex
% !TEX program = lualatex

\section{Method}
% Building from inside this chapter still starts at main.tex.
  • % !TEX root 外,扩展还会读取 % !TEX program% !TEX options% !BIB program。若要整体关闭,把 latex-workshop.latex.build.enableMagicComments 设为 false
  • 请在包含 main.tex 的项目根目录打开工作区。若只打开章节文件夹,第 (3) 步的搜索根本够不到主文档。
  • % !TEX root 中的路径是相对于写下它的那个文件的,因此把章节移到别的文件夹就必须改这一行。
  • 配方的选择与 % !TEX program 的指定互相打架时最容易迷路。团队协作时,把决定推到 .latexmkrc 里,配方统一用 latexmk (latexmkrc) 更稳妥。

内置 PDF 阅读器,以及用 Ctrl+点击实现的 SyncTeX

选择 tab 时打开的 PDF 阅读器,本体是一个内嵌 Mozilla PDF.js 的网页,由扩展在本地启动的小服务器提供。这也是为什么切到 browser 会得到一模一样的阅读器,以及为什么渲染结果不随操作系统或已装 PDF 阅读器而变。只有 external 不同:它只是把文件交给另一个程序,因此被标为实验性——外部阅读器的正向搜索需要另行通过 latex-workshop.view.pdf.external.synctex.command 之类的键来配置。

这里要记住的一点是:SyncTeX 并不是编辑器的功能。 写出源码行与 PDF 位置对照表的是 TeX 引擎,其开关就是 -synctex=1。扩展之所以能读到 .synctex.gz,仅仅因为配方传了这个参数。自己定义工具时若漏掉 -synctex=1,构建照样成功、PDF 照样生成,唯独点击跳转会悄悄失效,而且哪里都不报错。若发现「昨天还能跳」,请先怀疑配方的参数。

记住两个操作就够了。正向搜索(源码 → PDF):从光标位置跳到 PDF 中对应的位置,快捷键 Ctrl+Alt+J(Mac 为 Cmd+Alt+J);命令面板里叫「LaTeX Workshop: SyncTeX from cursor」。想在每次构建后自动跳转,把 latex-workshop.synctex.afterBuild.enabled 设为 true反向搜索(PDF → 源码):在内置阅读器中 Ctrl+点击(Mac 为 Cmd+点击);这个手势由 latex-workshop.view.pdf.internal.synctex.keybinding 决定,可取 ctrl-click(默认)或 double-click。顺带一提:构建是 Ctrl+Alt+B,打开 PDF 是 Ctrl+Alt+V

走 DVI 路线的日文构建同样保得住 SyncTeX。像上面的 .latexmkrc 那样给 $latex-synctex=1,upLaTeX 写下的对照信息就会经由 dvipdfmx 一路送进 PDF;并不是「非得 pdfLaTeX 直出才能跳转」。至于 SyncTeX 本身的机制——.synctex.gz 里装了什么、传负数会得到未压缩的可读文本文件——留给 SyncTeX 那一页。