Skip to content
NoetherVim is alpha. Breaking changes land without deprecation shims. These docs track main.

One LaTeX setup

This is an account of how my LaTeX setup works, after about a decade of writing mathematics in it. It was not encoded into what NoetherVim does to your documents. The latex bundle assumes as little as it can get away with, and everything below is something you can add yourself if you so choose.

It is written since some of the bundle’s snippets are only worth having if you adopt the conventions they were built against, and a worked setup is easier to understand.

The usual amsthm declaration gives you numbered environments:

\newtheorem{theorem}{Theorem}[section]
\newtheorem{defn}[theorem]{Definition}

which you then use as \begin{defn} ... \end{defn}, and refer to with a \label you have to remember to write.

The variant here takes two arguments instead:

\begin{defn}{Group Action}{grpAction}
A group action of $G$ on $X$ is ...
\end{defn}

The first is the title shown in the rendered output, the second is the label. One declaration site, so a definition can never be labelled and named inconsistently, and \cref{df:grpAction} reads as a sentence rather than as “Definition 3.7”.

That form comes from tcolorbox rather than from anything hand-rolled: \newtcbtheorem declares environments taking exactly a title and a label, and gives the label a per-environment prefix, which is where the df: comes from.

Typing :defn group action writes both from what you typed: the name is title-cased and the label is a compact form of it. So the label is derived rather than invented, and stays predictable across a book.

The cost is that it is not standard: a co-author’s \begin{defn} will not compile against your preamble, and yours will not compile against theirs. For a solo thesis or a book that is a fair trade. For a joint paper it is not, which is the main reason this is a page rather than a default.

mathpkgs writes the package block. The theorem declarations are not among what any snippet writes, since thmset produces the standard one-argument \newtheorem form; the declarations below are the ones the :defn, :prop and :lem snippets expect.

The theorem snippets also emit \index{...} alongside the environment. For a document long enough to need an index (lecture notes, a book) the entry is only ever written when the definition is, so the index cannot drift out of step with the text. For a paper it is noise, and you would want the snippets without it.

Figures live in one folder, referenced relatively

Section titled “Figures live in one folder, referenced relatively”
project/
├── main.tex
├── sections/
│ └── chapter-3.tex
└── images/
└── commutative-square.pdf

Source files sit one level down, so figure paths are ../images/.... The FIG snippet works this out rather than assuming it: it offers whichever of figure_folders actually exists, beside the document or one level up, and the offered path is still an editable field. Name your own folders with figure_folders if neither default matches.

Figures are compiled separately as standalone documents where they are expensive: TikZ diagrams that would otherwise be rebuilt on every run of the main document. standalone writes that skeleton.

Bibliographies: shared for books, per-document for papers

Section titled “Bibliographies: shared for books, per-document for papers”

Two patterns, and they want different things:

  • A book or lecture-note series shares one bibliography across every chapter, so a citation added in chapter 8 is available in chapter 2 and the numbering is consistent. One .bib at the project root.
  • A paper carries its own, so it can be handed to a journal as a self-contained directory.

The zotero bundle inserts a citation and appends the entry to whichever .bib it resolves for the current document. The resolution is a function you can replace, which is how the shared-bibliography case is expressed: point every file in the project at the same one.

-- lua/user/plugins/zotero.lua
return {
{ "Chiarandini/snacks-zotero.nvim",
opts = {
ft = {
tex = {
locate_bib = function()
-- Walk up for a project marker; fall back to the plugin's
-- per-document search when there is none.
local root = vim.fs.find(".project-root", { upward = true })[1]
return root and (vim.fs.dirname(root) .. "/references.bib")
or require("snacks_zotero.bib").locate_tex_bib()
end,
},
},
} },
}

Compilation is on request, and the PDF follows the cursor

Section titled “Compilation is on request, and the PDF follows the cursor”

VimTeX compiles with latexmk once per request rather than continuously (continuous = 0, against VimTeX’s own default of 1), so a large project does not rebuild on every write. <LocalLeader>ll runs a build, <LocalLeader>lv jumps the viewer to the cursor, and <LocalLeader>lf makes the viewer follow the cursor as you move, so the PDF behaves like a second view of the document rather than something you check on.

That last one is off until asked for, per buffer: a forward search is a viewer round-trip, and paying it on every cursor movement all session is a cost most editing does not want.

The cost of building on request is that the PDF drifts behind the source, and SyncTeX resolves against the line numbers it recorded at the last build. So <LocalLeader>lv checks first, and says how far behind the PDF is rather than jumping somewhere plausible and wrong.

The writing profile hard-wraps at textwidth, so a paragraph is several lines in the file and one in the output. The reason is version control: a one-line paragraph produces a one-line diff no matter which word changed, and a review of “what changed in this proof” becomes unreadable.

The cost is that a search for a phrase can fail when the wrap falls between two of its words. That is what the wrapsearch bundle is for, and it is listed here because the two settings only make sense together.

The bundle’s LaTeX snippets divide into three, and two of them are switches:

  • On always: fractions, sectioning, \textbf, alignment, the environment wrappers, the preamble skeletons. Nothing on this page matters for these; they work in any document.
  • conventions: the theorem family, in the two-argument form with an \index entry. Off by default, because \begin{defn}{a}{b} does not compile against a preamble that has not declared it. The declarations are below.
  • acronyms: the prose shorthand. Off by default for a different reason; it has no dependency at all, it is simply one person’s vocabulary.
-- lua/user/plugins/noethervim-tex.lua
return {
{ "Chiarandini/NoetherVim-tex",
opts = { snippets = { conventions = true, acronyms = true } } },
}

The two-argument form is not a wrapper anyone has to write. It is tcolorbox’s own interface: \newtcbtheorem declares an environment taking a title and a label, and its final argument is a prefix that the label is given. So

\newtcbtheorem[number within=section]{defn}{Definition}{...}{df}

makes \begin{defn}{Group Action}{grpAction} set Definition 1.1: Group Action and write \label{df:grpAction}. Those prefixes are the same ones the label picker maps in its transformations table, which is why \cref and <Space>w agree about what a definition is called.

label type= names the cleveref type, so \crefname can be stated in the ordinary way rather than reaching for the internal counter.

\usepackage{amsmath, amssymb, amsthm, mathtools}
\usepackage[most]{tcolorbox}
\tcbuselibrary{theorems}
\usepackage[hidelinks]{hyperref}
\usepackage{cleveref}
% Theorem-like environments, all sharing the thm counter within a section.
% Last argument is the label prefix; `label type` is the cleveref type.
\newtcbtheorem[number within=section]{thm}{Theorem}%
{colback=green!5, colframe=green!35!black, fonttitle=\bfseries,
label type=theorem}{th}
\newtcbtheorem[number within=section, use counter from=thm]{lem}{Lemma}%
{colback=blue!5, colframe=black!35!black, fonttitle=\bfseries,
label type=lemma}{lm}
\newtcbtheorem[number within=section, use counter from=thm]{prop}{Proposition}%
{colback=white!5, colframe=white!35!black, fonttitle=\bfseries,
label type=proposition}{pr}
\newtcbtheorem[number within=section, use counter from=thm]{cor}{Corollary}%
{colback=blue!5, colframe=blue!35!black, fonttitle=\bfseries,
label type=corollary}{co}
\newtcbtheorem[number within=section, use counter from=thm]{defn}{Definition}%
{colback=red!5, colframe=red!35!black, fonttitle=\bfseries,
label type=definition}{df}
\newtcbtheorem[number within=section, use counter from=thm]{example}{Example}%
{colback=black!2, colframe=black!50, fonttitle=\bfseries,
label type=example}{ex}
% titledBox has no display name of its own: the title argument is the whole
% heading, set inline in bold.
\newtcbtheorem[number within=section, use counter from=thm]{titledBox}{}%
{label type=box, enhanced, sharp corners, colback=white, colframe=black,
attach title to upper, before upper={\textbf{\tcbtitletext}\quad},
fonttitle=\bfseries, separator sign={\quad}}{box}
% The theorem snippets close proofs with a capitalised Proof.
\newenvironment{Proof}{\begin{proof}}{\end{proof}}
\crefname{theorem}{theorem}{theorems}
\crefname{definition}{definition}{definitions}
\crefname{box}{box}{boxes}

:defn group action then writes \begin{defn}{Group Action}{grpAction}, and \cref{df:grpAction} reads definition 1.1. The :exercise snippet additionally wants the exercises package, which supplies the Exercise and Answer environments it writes.

The figure snippet is the case that used to be a trap and is now not a switch: FIG reads figure_folders and offers whichever of them exists beside your document or one level up, so it fits either layout instead of assuming this one.

<Space>es opens the snippet files, with the ones a plugin ships marked read-only and a route to start your own for the same filetype. Reading how one is built is the fastest way to write a different one.

Section titled “Navigating by structure, including your own environments”

]g and [g jump between theorem environments, ]p and ]x between proofs and examples. The names each one matches are a list rather than something fixed, because the names in a document belong to whoever wrote it. Out of the box ]g covers both spellings, so \begin{theorem} and \begin{thm} are both found.

Adding a motion is an entry in the same table. The exercises environment that :exercise writes has none by default, so:

-- lua/user/plugins/noethervim-tex.lua
return {
{ "Chiarandini/NoetherVim-tex",
opts = {
textobjects = {
exercise = { envs = { "Exercise" }, next = "]e", prev = "[e",
desc = "exercise" },
},
} },
}

Pick the keys to suit your own map rather than copying these two: ]e and [e are line-swap keys in some configurations, and ]c and [c, which would otherwise be the obvious pair for chapters, are Vim’s diff-mode change motions and gitsigns’ hunk motions. That is why chapter navigation ships unbound and is one table entry away:

textobjects = {
chapter = { node = "chapter", next = "]c", prev = "[c", desc = "chapter" },
}