mytex-latex
Reference for writing, compiling, and debugging LaTeX with the myvimtex setup (`~/dotfiles/myvimtex`, a lazy.nvim plugin loaded by the daily driver) and for LaTeX authoring problems generally. Use whe
SKILL.mdclaude-skills/mytex-latex/SKILL.md
GitHubmytex — LaTeX authoring in Neovim
Overview
myvimtex (~/dotfiles/myvimtex) is a lazy.nvim plugin loaded by the
daily-driver config as a dir spec with import = 'latex.plugins'; every
spec inside is ft = 'tex', so opening any .tex file in plain nvim
gives the full LaTeX experience and non-tex sessions never load it.
mytex <file.tex> is an optional macOS wrapper that only adds the Skim
reverse-search socket (--listen /tmp/nvimsocket). Compiler: tectonic.
Viewer: Skim (SyncTeX forward/reverse search). Snippets: LuaSnip,
defined in luasnippets/tex.lua, expand via blink.cmp. Localleader is
space; the VimTeX mapping prefix is moved to <localleader>v (which-key
collision with kickstart's <leader>l), so VimTeX docs' \ll = Space vl
here.
Canonical, evolving docs live in the repo itself — README.md (install),
GUIDE.md (day-to-day usage + append-only snag log). Read GUIDE.md for
anything not covered below or if a fact here seems stale; it accumulates
new snags over time and this skill won't auto-update with it.
Compile / view / sync
| Keys | Action |
|---|---|
Space vl |
Compile (tectonic, single-shot — no watch mode; re-run after edits) |
Space ve |
Quickfix with compile errors (spans all chapter files) |
Space vo |
Raw compiler output |
Space vv |
Forward search — open Skim at current line |
| ⌘-click in Skim | Reverse search — jump nvim to that source line (only the most-recently-launched mytex session receives it) |
Space vc |
Clean aux files |
Space vi |
Session info (shows detected main file) |
:VimtexCountWords |
Word count |
Multi-file projects: open any file — VimTeX scans for the file that
\inputs/\includes it and treats that as the project root for compile,
TOC, and SyncTeX. To iterate fast on one chapter of an \include-based
project, add \includeonly{chaptername} to the main file while drafting,
remove it for full builds.
Navigating and editing structurally
| Keys | Action |
|---|---|
Space vt |
TOC window (project-wide outline); Enter jump+close, Space jump+keep-open, r refresh, L/I/T/C toggle label/include/todo/content layers, -/+ depth, f/F filter |
]] / [[ |
Next/prev section start (][/[] for ends) |
]m / [m |
Next/prev environment |
]n / [n |
Next/prev math zone |
% |
Bounce \begin/\end, $...$, delimiters |
gf on \input{}/\include{} |
Open that file |
K on a command |
Open its package docs |
Text objects (ie/ae environment, i$/a$ math zone, ic/ac
command, id/ad delimiters, iP/aP section) combine with
c/d/v/y — e.g. cie rewrites an environment's contents, daP
deletes a whole section.
Surround-style: cse/dse change/delete environment wrapper (edits
\begin+\end together), csc/dsc change/delete command
(\textbf{x}→\emph{x}), cs$/ds$ and csd/dsd for math/generic
delimiters. Toggles: tse starred environment (equation↔equation*),
tsc starred command, tsd (...)↔\left(...\right), tsf
a/b↔\frac{a}{b}.
Snippets (luasnippets/tex.lua)
| Trigger | Kind | Expands to |
|---|---|---|
beg |
completion (accept Enter, not Tab) |
\begin{env}...\end{env}, name mirrored via rep(1) |
fig |
completion | figure block (includegraphics, caption, fig: label) |
item |
completion | empty itemize block |
itemN (e.g. item3) |
regex-trigger, auto-expand | itemize block pre-populated with N \item lines |
it |
completion | \item |
;a / ;b / ;t |
auto-expand, no accept key | \alpha / \beta / \theta |
vim.g.vimtex_imaps_enabled = 0 so VimTeX's own insert mappings don't
fight LuaSnip's. Accept completion-menu snippets with Enter, not Tab
(this config's blink.cmp uses preset = 'enter'); if a literal buffer word
matching the trigger already exists, blink.cmp may rank the buffer-word
match above the snippet — arrow down (<C-j>/<Down>) to the snippet
entry first.
Adding a new snippet: a plain templated block goes in snippets
(completion, needs an accept key), built with fmt([[...]], { i(1, 'placeholder'), ... }) — e.g. fig itself:
s('fig', fmt(
[[
\begin{{figure}}[htbp]
\centering
\includegraphics[width=\linewidth]{{{}}}
\caption{{{}}}
\label{{fig:{}}}
\end{{figure}}]],
{ i(1, 'path'), i(2, 'caption'), i(3, 'label') }
))
A word-shaped trigger like eq or thm belongs here, not in
autosnippets — autosnippets fire the instant the trigger is typed,
with no accept step and no chance to back out, so a bare word trigger
there would fire mid-prose the moment you typed it as an ordinary word.
autosnippets is only safe for triggers that can't occur as normal text
— either a leading-sigil trigger (;a, ;b, ;t — the ; guarantees it
never appears in prose) or a regex trigger distinctive enough not to
collide (item(%d+)).
Regex-triggered snippets whose content depends on the match (like
itemN) can't use blink.cmp's completion menu at all — an unset name
falls back to the raw Lua pattern (e.g. item(%d+)), which can't
fuzzy-match what you typed. Instead they belong in autosnippets with
regTrig = true, and build their body with a dynamic_node that reads
snip.captures[1]:
local function item_nodes(_, snip)
local n = tonumber(snip.captures[1]) or 1
local nodes = {}
for j = 1, n do
if j > 1 then table.insert(nodes, t { '', '\t' }) end
table.insert(nodes, t '\\item ')
table.insert(nodes, i(j))
end
return sn(nil, nodes)
end
-- in autosnippets:
s({ trig = 'item(%d+)', regTrig = true }, fmt(
[[\begin{{itemize}}\n\t{}\n\end{{itemize}}\n{}]],
{ d(1, item_nodes, {}), i(0) }
))
after/ftplugin/tex.lua hooks InsertCharPre → luasnip.expand_auto()
(LuaSnip doesn't call this itself) and guards against double-loading,
since the daily driver's lazy.nvim re-dispatches FileType once deferred
plugins finish loading.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
\alpha doesn't render as α |
(1) treesitter highlight overrides :syntax's conceal rules — needs highlight.disable = { 'latex' } in the daily driver; (2) conceallevel defaults to 0; (3) Greek conceal is math-mode only (texCmdGreek is contained in texClusterMath) |
Set conceallevel=2, concealcursor='' for tex buffers (leaving n/i out of concealcursor keeps the current line raw/unconcealed so you can edit it, while other lines stay concealed); write $\alpha$, not bare \alpha |
beg<Tab> does nothing / buffer-word offered instead of snippet |
blink.cmp preset = 'enter'; buffer-word source can outrank the snippet |
Accept with Enter; arrow to the snippet entry first if needed |
| ⌘-click in Skim opens a disconnected new nvim | Skim's reverse search shells out to nvr (neovim-remote — a CLI that sends commands to a running nvim's RPC socket), which falls back to /tmp/nvimsocket when Skim's subprocess has no $NVIM; nothing listening there |
mytex must launch nvim with --listen /tmp/nvimsocket so nvr's fallback address actually has a listener |
Space vv lands consistently a few lines below the cursor, or does nothing |
Missing/stale synctex: PDF was compiled without --synctex (CLI habit) or the last compile failed and the PDF is frozen — Skim silently falls back to text-matching against the stale PDF |
Fix the compile error, recompile via Space vl; verify .synctex.gz exists next to the PDF. See "How SyncTeX actually connects the pieces" below |
| tectonic halts: "Missing number, treated as zero", log points at an unrelated file | A line right after \\ starts with a bare [ — LaTeX parses \\[ as the optional vertical-space argument |
Brace it: {[}text{]}. Rule: never start a line after \\ with a bare [ |
| TOC shows TODOs/includes but no chapter/section outline | (1) content layer toggled off (C in TOC to re-enable); (2) \begin {document} has a space — VimTeX's preamble-end regex requires no space, so it never leaves preamble mode |
Press C; write \begin{document} with no space |
sh: kpsewhich: command not found in TOC |
VimTeX shells out to kpsewhich (TeX Live) for bib paths; tectonic doesn't ship it |
Cosmetic only, .bib is still found — ignore |
| tectonic halts on an error pdflatex would push through | tectonic stops at potentially-recoverable errors by default instead of emitting a broken PDF | Fix the error rather than forcing past, unless it's a legacy doc you deliberately want to force |
| TeX error message/context looks truncated | default \errorcontextlines is too small |
Add \errorcontextlines=200 to surface the real backtrace |
General LaTeX gotchas worth knowing regardless of editor: Undefined control sequence usually means a missing \usepackage or a typo in the
command name; Missing $ inserted means a math-only command (\alpha,
\frac, _, ^) was used in text mode — wrap it in $...$; prefer
\includegraphics + the graphicx package and biblatex/natbib +
hyperref (load hyperref last) as defaults when a document doesn't
specify its own package choices.
How SyncTeX actually connects the pieces (debugging theory)
Three layers, three contracts — knowing which one broke saves an hour:
- Engine (tectonic):
--synctexwrites<pdfname>.synctex.gz— a viewer-agnostic text database:Input:records (tag → source path), then box records(tag, line) → (page, x, y). Plain CLItectonic file.texdoes NOT pass--synctex; vimtex'sSpace vlalways does. - Viewer (Skim): not TeX-agnostic — it embeds the reference synctex
parser (same library as Zathura/Okular/Sumatra) and exposes TeX-aware
scripting (
go to TeX line N from file F). It finds the synctex file by basename next to the PDF. With synctex data, resolution is line-exact (verified: mid-paragraph, lists, later pages). Without it, Skim silently falls back to text-matching heuristics — no error, just wrong-ish positions. - Editor (vimtex): never reads synctex; it only sends Apple Events
(JXA
go({to: texLines[l-1], from: Path(file)}), afterrevert()to reload the PDF). Reverse search is the mirror: Skim queries(page,x,y) → (file,line)and shells out tonvrper itsSKTeXEditor*defaults.
Diagnostic rule: forward search landing consistently a few lines off
(stale-text heuristic match) or doing nothing (no match) means missing or
stale synctex — check that the last compile succeeded and that
.synctex.gz exists next to the PDF, before suspecting vimtex/Skim/mytex.
A CLI compile without --synctex silently poisons this for the whole
project. See GUIDE.md's 2026-07-22 snag entries for the full case study.
Setup / architecture gotchas (only relevant when the setup misbehaves)
- myvimtex is a normal lazy.nvim plugin (dotfiles submodule); there is no
separate config, lockfile, or data dir. The daily driver's
lazy-lock.jsonpins everything — vimtex is pinned toa5949d2because vimtex master requires nvim 0.12.4+;:Lazy updatebreaks the LaTeX layer until nvim is upgraded (:Lazy restore vimtexrecovers). - Isolation =
ft = 'tex'gating, not process isolation: tex plugins are in every nvim's spec but never load outside tex buffers.vim.g.vimtex_*globals ARE set in all sessions (lazy runsinithooks eagerly) — that's expected and inert. - Snippets load via LuaSnip's rtp scan (
from_lua.load()with no paths, which finds anyluasnippets/dir on the runtimepath) — there is no hardcoded snippet path anywhere.