Open a .tex file in a freshly installed Visual Studio Code and it is already coloured, with no extension at all — because VS Code ships a LaTeX grammar in the box, and that grammar was carved out of the LaTeX Workshop extension. Colour, though, is the whole of what VS Code knows about LaTeX. Building, showing the PDF, jumping between source and PDF: all of that is LaTeX Workshop, and the typesetting itself is done by the TeX distribution on your machine, launched as a child process. This page covers the two-layer model that describes a build — tools and recipes — why the default recipe runs a different engine than a great many documents need, and the built-in PDF viewer that turns SyncTeX into a ctrl-click.
What VS Code knows about LaTeX on its own
A bare VS Code registers three language ids and a grammar for each, and nothing more: tex for .sty and .cls, latex for .tex, bibtex for .bib. No build command, no PDF viewer, no completion, no jumping to a \ref. Those grammar files come from a repository called jlelong/vscode-latex-basics, whose README states that the files "were originally part of" LaTeX Workshop; VS Code has shipped them since its January 2022 release. The colours you see the moment you open a .tex file are therefore already the extension's work, months before you install the extension.
Everything else is supplied by LaTeX Workshop (by James Yu; Marketplace id James-Yu.latex-workshop) — everything except TeX. The extension launches executables such as latexmk, pdflatex and biber as child processes and reads their output back, so it can never be healthier than the distribution behind it: TeX Live, MiKTeX or MacTeX. One diagnostic rule follows from that. Build the same project once in a terminal before you touch a single setting. If latexmk fails there, nothing in settings.json will rescue you; and if it succeeds there while the extension still reports that the command cannot be found, the suspect is the environment VS Code inherited, not the extension.
Installing it is unremarkable — the Extensions view (Ctrl/Cmd+Shift+X), search for “LaTeX Workshop.” What arrives with it is very nearly the whole working surface: build commands, the PDF preview, completion, navigation from a \ref or \cite to its target, an outline of the document, and a project file tree assembled by following \input and \include — which is also the list of files the automatic build watches. If you edited your PATH to install TeX, restart VS Code, ideally by logging out and back in, so the new environment is picked up. Then check from a terminal that the distribution answers at all.
# 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 latexmkOnce the terminal build is green, whatever is left lives on the VS Code side and reduces to three questions: which recipe runs, which file is the root, and where the PDF appears. The rest of this page is those three.
Tools and recipes: reading latex-workshop.latex.recipes
A build is written in two layers. A tool (latex-workshop.latex.tools) defines one command to launch: a name, a command (the executable) and an args array. A recipe (latex-workshop.latex.recipes) is an ordered list of tool names. latexmk is a recipe of one tool; pdflatex -> bibtex -> pdflatex * 2 is a recipe of four. The split exists because the same executable is wanted with different arguments in different situations: the shipped tools include latexmk, lualatexmk, xelatexmk, latexmk_rconly, pdflatex, bibtex and tectonic, and recipes recombine them rather than duplicating the command definitions.
{
"name": "latexmk",
"command": "latexmk",
"args": [
"-synctex=1",
"-interaction=nonstopmode",
"-file-line-error",
"-pdf",
"-outdir=%OUTDIR%",
"%DOC%"
],
"env": {}
}Reading the arguments one by one shows the design. -synctex=1 asks for the SyncTeX map discussed below; -interaction=nonstopmode runs to the end instead of stopping at an error to wait for input; -file-line-error prints errors in the form main.tex:42: Undefined control sequence, and it is that last flag that lets the extension jump from its Problems panel straight to the line. -pdf tells latexmk to produce the PDF directly with pdfLaTeX — the flag that causes trouble further down this page. The %…% tokens are placeholders the extension substitutes just before launching.
| Placeholder | Expands to |
|---|---|
%DOC% | path of the root file, without its extension |
%DOC_EXT% | path of the root file, with its extension |
%DOCFILE% | just the root file name, without its extension |
%DIR% | the directory holding the root file; the default outDir |
%OUTDIR% | the output directory set by latex-workshop.latex.outDir |
%TMPDIR% | a temporary directory for auxiliary files; keeps the source clean |
%WORKSPACE_FOLDER% | the path of the workspace currently open |
Which recipe runs is decided by latex-workshop.latex.recipe.default. Its default value is "first" — meaning the top entry of the list wins — and setting it to "lastUsed" makes the extension remember the recipe you last chose. A build is started with Ctrl+Alt+B (Cmd+Alt+B on Mac). To run a particular recipe once, use “LaTeX Workshop: Build with recipe” from the Command Palette; to pin one to a file, put %!LW recipe=latexmk (lualatex) on the first line. That last directive is disregarded when you pick a recipe by hand from the panel.
Why the default recipe runs a different engine than you want
The answer is in the tool definition above: -pdf is the argument that tells latexmk to make the PDF directly with pdfLaTeX. That one word accounts for a large share of “it builds in the terminal but not in VS Code” reports. If your preamble loads fontspec — anything using OpenType fonts, anything using unicode-math, most modern templates — the build halts with ! Fatal Package fontspec Error: The fontspec package requires either XeTeX or LuaTeX. If you type Japanese, Chinese or Korean straight into the document instead, you get ! LaTeX Error: Unicode character こ (U+3053) not set up for use with LaTeX. Neither message mentions VS Code, because VS Code is not the problem.
The fix is simply to choose another recipe, and there are four ways, in increasing order of permanence. Just this once: “Build with recipe” from the Command Palette. Just this file: %!LW recipe=… on the first line. From now on, whatever I last picked: set latex-workshop.latex.recipe.default to "lastUsed". Fixed for the whole project: reorder latex-workshop.latex.recipes in settings.json so the recipe you want sits first, since the default is "first". The shipped list already contains latexmk (lualatex), latexmk (xelatex) and latexmk (latexmkrc), so most of the time you are choosing, not writing.
Put the engine in .latexmkrc, not in settings.json
An engine choice written into editor settings never leaves the machine you wrote it on. Written into a .latexmkrc, it travels with the project — your co-author's TeXstudio, a CI container and a bare latexmk main.tex all reach the same result. The shipped recipe latexmk (latexmkrc) exists for exactly this: it runs latexmk %DOC% and adds no arguments of its own. Here is an upLaTeX + dvipdfmx setup, long the standard combination for Japanese papers:
$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;The pivotal line is $pdf_mode. 3 means “make a DVI, then turn it into a PDF with $dvipdf”; 1 is pdfLaTeX directly; 4 is LuaLaTeX. The index goes to upmendex, which can sort Japanese, and the bibliography to upbibtex. %S, %O, %D and %B are latexmk's own placeholders — source, extra options, output target, and base name without extension — a different family from the extension's %DOC%, so do not mix them up. Quietly important: $latex carries -synctex=1. Drop it and the click-to-jump described below stops working with no message anywhere.
If you would rather keep everything in settings.json and skip the .latexmkrc, write your own tool and recipe and put the recipe first. Here is a self-contained example for LuaLaTeX — which sets Japanese through luatexja and the ltjsclasses, so no DVI detour is needed:
{
"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"] }
]
}The three settings to decide first: output, auto-build, viewer
Settings live in settings.json. Open the Ctrl/Cmd+, settings screen and use “Open Settings (JSON)” at the top right; you can edit either the global user file or a .vscode/settings.json in the project. Anything you want co-authors or a build server to share must go in the latter. Of the several dozen settings, three are worth deciding up front:
latex-workshop.latex.outDir— where intermediate files and the PDF go. The default is%DIR%, next to the.tex. Setting%DIR%/outkeeps.aux,.logand.flsfrom littering the source folder and reduces.gitignoreto one line.latex-workshop.latex.autoBuild.run— what triggers an automatic build. The default isonFileChange, which watches dependencies on disk and therefore reacts to changes made outside the editor. The alternatives areonSave(only when you save) andnever(manual only). If you ever lose track of what caused a build,onSaveis the legible choice.latex-workshop.view.pdf.viewer— where the PDF appears:tab(default; a tab inside VS Code),browser(your default browser), orexternal(another program, treated as experimental). For frictionless SyncTeX,tab.
{
"latex-workshop.latex.outDir": "%DIR%/out",
"latex-workshop.latex.autoBuild.run": "onSave",
"latex-workshop.view.pdf.viewer": "tab",
"latex-workshop.latex.recipe.default": "lastUsed"
}Splitting the output directory has one trap. Changing outDir also changes where the extension looks for .aux and .fls. If that no longer matches where the build actually writes them, you end up with a PDF that exists but that the extension cannot find, and cross-references that never resolve. Keep the two in step — especially if your .latexmkrc also sets $out_dir. Cleaning up intermediates is the job of latex-workshop.latex.autoClean.run, but once everything lands in out/ you can simply delete the folder, so you often do not need it.
Building main.tex while you edit a chapter: % !TEX root
Put % !TEX root = ../main.tex on the first line of the child file. That alone makes a build start from the main document even when only the chapter is open. It works because LaTeX Workshop looks for the root in five stages, and this magic comment is the first: (1) % !TEX root; (2) does the open file itself contain \documentclass or \begin{document}; (3) scan the .tex files at the top of the workspace for one with a class declaration; (4) the subfiles package arrangement; (5) analysis of .fls files. The guess is often right — but in a thesis with dozens of chapter files, the fact that it is a guess is itself the hazard.
% !TEX root = ../main.tex
% !TEX program = lualatex
\section{Method}
% Building from inside this chapter still starts at main.tex.- Besides
% !TEX root, the extension also reads% !TEX program,% !TEX optionsand% !BIB program. To disable the whole family, setlatex-workshop.latex.build.enableMagicCommentstofalse. - Open the workspace at the project root that holds
main.tex. If you open only the chapter folder, stage (3) of the search never reaches the main document at all. - The path in
% !TEX rootis relative to the file that carries it, so moving a chapter into another folder means editing the line. - A recipe choice that disagrees with
% !TEX programis a good way to get lost. For team use, push the decision into.latexmkrcand standardise on thelatexmk (latexmkrc)recipe.
The built-in PDF viewer, and SyncTeX by ctrl-click
The PDF viewer you get with tab is a web page built around Mozilla's PDF.js, served by a small server the extension runs locally. That is why switching to browser gives you the very same viewer, and why the rendering does not vary with the operating system or an installed PDF reader. Only external is different: it hands the file to another program, which is why it is marked experimental — forward search with an external viewer has to be wired up separately through keys such as latex-workshop.view.pdf.external.synctex.command.
The thing to hold on to here is that SyncTeX is not a feature of the editor. What writes the map between source lines and positions in the PDF is the TeX engine, and its switch is -synctex=1. The extension can read a .synctex.gz only because the recipe passed that flag. Define your own tool and forget the flag, and the build succeeds, the PDF appears, and only click-to-jump quietly stops working — with no error anywhere. When jumping worked yesterday and does not today, suspect the recipe's arguments first.
Two gestures are enough. Forward search (source → PDF) jumps from the cursor to the matching place in the PDF: Ctrl+Alt+J, or Cmd+Alt+J on Mac; from the Command Palette it is “LaTeX Workshop: SyncTeX from cursor.” To jump automatically right after each build, set latex-workshop.synctex.afterBuild.enabled to true. Inverse search (PDF → source) is a Ctrl-click (Cmd-click on Mac) in the built-in viewer; the gesture is chosen by latex-workshop.view.pdf.internal.synctex.keybinding, either ctrl-click (default) or double-click. While you are at it: build is Ctrl+Alt+B and opening the PDF is Ctrl+Alt+V.
SyncTeX survives the DVI route too. Pass -synctex=1 to $latex as in the .latexmkrc above and the map upLaTeX writes travels through dvipdfmx all the way into the PDF; you do not need to go straight to PDF with pdfLaTeX to get jumping. The machinery itself — what is inside a .synctex.gz, and the fact that a negative value produces an uncompressed, readable text file instead — belongs to the SyncTeX page.