LaTeX Project Manager
A workspace-wide assistant for long LaTeX documents, built for VS Code. It reads your
.bib bibliographies and .tex sources and gives you three things that are hard to keep
straight by hand in a thesis-sized project:
- Citation statistics — which references you actually cite, how often, and exactly where.
- Glossary usage statistics — which acronyms and glossary entries you defined, how often
each is used, where, and which are used but never declared.
- Automatic labels — a
\label{...} generated for each heading, figure, table and
listing, kept in sync with it, with every \ref in the workspace updated when it changes.
It is designed for writing long, sensitive documents (theses, papers, reports): it runs
completely offline, ships zero runtime dependencies, and never makes a network
request or emits telemetry.
Renamed in 2.0. This extension was previously LaTeX Citation Statistics
(ActiveClientMods.latex-citation-statistics). Citations are unchanged and still the
first view; the glossary and label features are new. See Upgrading from
1.x for the settings that moved.
Features
Citations
- Search bar with Match Case, Match Whole Word, and Regex. A search box at the top of
the view filters your sources as you type, with the same three toggles as VS Code's own
Search panel — Match Case (
Aa), Match Whole Word (ab), and Use Regular
Expression (.*). Search runs over each entry's key, title, author, and year (and over
undefined keys), entirely in-memory and offline.
- Filter and sort menu. A filter button narrows the list to all, used only,
unused only, or undefined only, and a sort button orders sources by citation
count, author, title, year, or key (ascending or descending). Your filter, sort, and
search toggles persist across restarts.
- Live citation counts. Each source shows how many times it is cited across all
.tex
files, ordered by the sort you pick (most-cited first by default).
- Overview at a glance. An Overview node summarises your bibliography: total
sources, used vs. unused, and total citation occurrences. The same headline numbers
appear next to the view title, and undefined keys raise a badge on the activity-bar icon.
- Unused references stand out. Sources with zero citations are marked
unused with a
distinct warning icon, so dead bibliography entries are easy to spot and prune.
- Expand to every occurrence. Expanding a source reveals every individual citation
instance as
file:line, each with a preview of the source line.
- Group by source or file. A Group by menu chooses how the occurrence tree is
outlined across two dimensions — source (bibkey) and file: Source, Source, then file,
File, then source, and File. Only these four outlines are offered, so every grouping
is valid by construction.
- Jump to the exact spot. Clicking an occurrence opens the
.tex file and places the
cursor precisely on the citation key — correct line and column, with the key selected.
Clicking an unused source jumps to its definition in the .bib file instead.
- Undefined citations. Keys you cite but that don't exist in any
.bib file are
collected under a separate Undefined citations node.
Glossary
- A second sidebar view listing every
\newacronym, \newglossaryentry,
\newabbreviation, and friend declared anywhere in your workspace — with the same search
box, filter, sort, and grouping controls as the Citations view.
- Live usage counts across the whole
\gls family: \gls, \Gls, \GLS, \glspl,
\glsdisp, \glslink, \glstext, \acrshort, \acrlongpl, \ac/\acp, the
glossaries-extra \glsxtr… commands, and so on. Starred and optional-argument variants
(\gls*[format=textbf]{key}) are all counted.
- Unused entries stand out, so a glossary that has drifted ahead of the text is obvious.
- Undefined entries — a
\gls{key} with no declaration is a hard LaTeX build failure
(Glossary entry 'key' has not been defined), so those are collected in their own
section and reported in the Problems panel.
Automatic labels
Labels are generated for headings and for floats — figures, tables, listings and
algorithms — with a prefix per kind:
| Construct |
Prefix |
Example |
\part |
part: |
\label{part:background} |
\chapter |
chap: |
\label{chap:methods} |
\section |
sec: |
\label{sec:introduction} |
\subsection |
subsec: |
\label{subsec:setup} |
\subsubsection |
subsubsec: |
\label{subsubsec:detail} |
\paragraph / \subparagraph |
par: / subpar: |
\label{par:aside} |
figure |
fig: |
\label{fig:test-architecture} |
table, longtable |
tab: |
\label{tab:measured-results} |
lstlisting, listing |
lst: |
\label{lst:parser-loop} |
algorithm |
alg: |
\label{alg:search} |
Every prefix is configurable via latex-project-manager.labels.prefixes.
Headings are named after their title. Type one, and a matching label appears below it:
\section{Über den Bären}
\label{sec:ueber-den-baeren}
The slug is lower-cased, hyphen-separated, and stripped of everything else. German umlauts
and eszett fold to two letters (ä→ae, ö→oe, ü→ue, ß→ss), other accents
reduce to their base letter (é→e, ñ→n), and LaTeX markup is resolved first — so
\"{U}ber and Über produce the same label rather than depending on how you typed it.
\textbf{Bold} keeps its text, \footnote{…} does not.
Floats are named after the file they include. A figure that pulls in
figures/test-architecture.pdf — or figures/test-architecture.tex via \input — is
labelled fig:test-architecture, whatever its caption says:
\begin{figure}
\includegraphics[width=\textwidth]{figures/test-architecture.pdf}
\caption{An overview of how the parts fit together}
\label{fig:test-architecture}
\end{figure}
Directories and the extension are stripped, so ./img/deep/test-architecture.png,
figures/test-architecture and figures/test-architecture.tex all give the same name. The
filename wins over the caption because it is stable and unique, whereas a caption is prose
that gets reworded. A float that includes nothing falls back to its caption
(\caption{Measured Results} → tab:measured-results).
The label is inserted after the \caption, since that is where LaTeX numbers a float —
a label placed before it would pick up the previous number.
Two more rules that apply to everything:
- Renames follow the source. Retitle a section, or point a figure at a different file,
and the label is rewritten — along with every
\ref, \cref, \Cref, \autoref,
\pageref, \eqref, \nameref, \vref, \crefrange, \hyperref[…] and friend that
points at it, anywhere in the workspace, as a single undoable edit. One Ctrl+Z
reverses the whole thing, exactly like VS Code's own F2 rename.
- Only labels it generated are ever touched. A label you wrote by hand is never
renamed, never removed, and never treated as a collision it may resolve by force. A
\begin{lstlisting}[label={lst:x}] that declares its label in the option list counts as
hand-written too, and is left alone.
- An empty
\label{} is filled in, not duplicated. Clear a label's name and the next
apply writes the generated name into the braces you already typed, rather than adding a
second \label above them.
Choosing when labels get written
latex-project-manager.labels.mode controls this:
| Mode |
Behaviour |
onType (default) |
Writes shortly after you stop typing. |
onSave |
Writes when the file is saved. |
manual |
Writes nothing on its own. Pending changes are listed in the Labels view; applying them takes a click and a confirmation. |
off |
Never writes. The commands still work from the Command Palette. |
In the automatic modes, generation is restricted to the lines your edit touched, so opening
a document can never produce a hundred-line unrequested diff. Generate and Update Labels
in This File does a whole document deliberately, in one undoable edit;
Review and Apply Label Changes… does the same but shows the confirmation first.
The Labels view
The third view in the LaTeX Project sidebar shows two things:
- Pending label changes for the file you are editing — every label that would be added,
every empty
\label{} that would be named, and every rename with the number of
references it would update. Each row names the file and line it applies to, and an
Apply row at the bottom runs the change with a confirmation.
- Problems, workspace-wide — every
\ref (or \cref, \autoref, …) that resolves to
no label, and every label defined more than once. A reference whose label this extension
generated is marked restorable, meaning the target is most likely commented out rather
than gone. Clicking a row jumps to it.
The same problems appear in the Problems panel; the view exists so that a broken reference
in a file you do not currently have open is still visible.
Label lifecycle: what happens when the link breaks
Renaming a label safely requires answering one question — is this label mine? — and
nothing in the LaTeX source records the answer. Matching targets by position breaks the
moment a line is inserted above; matching by text similarity guesses, and a wrong guess
silently rewrites references across the workspace.
So the extension remembers, keyed on the label name itself. That works because the
label physically travels with its target: cut a section, paste it into another file, and
\label{sec:foo} goes with it. The name is a stable identity that survives edits, moves,
and restarts — which is what makes re-linking work with no fuzzy matching at all.
Each generated label is in one of three states:
| State |
Meaning |
| managed |
The target exists and carries this label. Its name is kept in sync. |
| orphaned |
The label is not currently visible in the parsed source. A tombstone, not a deletion. |
| released |
You overruled the extension for this label. It is never touched again. |
Nothing is changed, and nothing is lost. Comments are blanked before parsing, so a
commented-out \section is invisible — which makes it look exactly like a deleted one, and
also exactly like a file that is momentarily unreadable or lost to a branch switch. Those
cases cannot be told apart from a single parse, so the extension does the only safe thing:
The label's record becomes an orphaned tombstone. It is not deleted.
The \label itself is left in place if you only commented out the heading. Removing
it would break every reference pointing at it.
The \refs are left exactly as they are. Stripping them is the one thing the
extension must not do: \ref{…} sits inside prose ("as shown in Section \ref{sec:x}"),
so removing it leaves a hole in a sentence — unrecoverable from your side, because you no
longer know what it pointed at. LaTeX's own behaviour is the model: an unresolved
reference typesets as ?? and warns. The extension says the same thing earlier, in the
Problems panel, and because it has a tombstone it can say it usefully:
Reference to "sec:results" does not resolve. This extension generated that label for
the \section "Results and Discussion" in chapters/03.tex. It is most likely
commented out or deleted — restore it and the link repairs itself.
Uncomment it and everything reconnects by itself. The label reappears with the same
name, the tombstone flips straight back to managed, the diagnostic clears, and syncing
resumes — no prompt, no heuristics. If you also retitled the heading while it was
commented out, the pending rename is applied on its return.
The same holds for a deleted section restored from git, and for a whole file that
disappears and comes back.
You undo one of its renames
Without special handling this would loop forever: the undo is itself a document change,
which triggers another pass, which sees the same mismatch and renames again. You could not
win.
The extension detects it by the plan repeating itself. A rename that succeeds leaves the
label holding the new name, so the next pass plans nothing; seeing the identical
from → to pair proposed again shortly after applying it means the edit was reverted —
there is no other way to reach that state. The response is to release the label
permanently, not to skip once: an undo is a clear statement that the extension's opinion is
unwanted here, and a one-shot skip would restart the loop on the next keystroke.
Retitling a heading back to its earlier wording is not affected — that produces the
reversed pair, which is a different rename, and is applied normally.
released is deliberately a one-way door, so an undo can never be second-guessed. Resume
Automatic Label Sync… in the Command Palette is your way back through it.
Other cases
| Situation |
Behaviour |
| You edit a generated label by hand |
It stops being the extension's. Left alone from then on. |
| Two targets produce the same name |
First keeps sec:intro, second gets sec:intro-2. Deleting the first does not renumber the second — that would break its references for no benefit. |
| A generated slug collides with a hand-written label |
The generated one yields and takes the -2 suffix. |
| A label is defined twice |
Renaming is ambiguous, so auto-sync pauses for that label and the duplicate is reported instead. |
\section*{...} (unnumbered) |
Not labelled by default — a \ref to an unnumbered heading typesets as ??. Enable labels.includeStarred if you use \phantomsection. figure* and table* are labelled, since they are still numbered. |
A \ref inside a comment |
Not rewritten. A commented reference is inert, and is treated the same way as a commented \cite. |
How it works
Parsing
.bib files — Entry types, keys, titles, authors, and years are extracted with a
brace-depth-aware scan, so nested braces in titles (e.g. {The {LaTeX} Companion}) are
handled correctly. @comment, @string, and @preamble blocks are ignored.
.tex files — All three features share one low-level scanner
(src/parsers/latexScanner.ts) that solves the problems
they have in common: blanking comments without shifting any offset (so every reported
column stays exact), matching braces through arbitrary nesting, and skipping the *,
[opt] and (note) arguments between a command and its first mandatory group.
\cite[p.~5]{key}, \gls*[format=textbf]{key} and \cref{sec:a,sec:b} are the same
scanning problem wearing different names.
- Citations: the whole
\...cite... family, including biblatex multicite
commands (\cites[S.~12-22]{key1}[S.~1]{key2} yields two citations), multi-key groups
split with per-key columns, and \citestyle/\nocite{*} correctly ignored.
- Glossary: declarations are read positionally (
\newacronym{key}{short}{long}) or
as a brace-aware key=value list (\newglossaryentry{k}{name={x}, description={a, b}}
— a comma inside braces does not split the list). \glsaddall is never mistaken for
\glsadd.
- Labels: headings, float environments,
\labels, and the whole \ref family in one
pass. A heading claims a label only when it sits inside the heading braces, or after the
closing brace with nothing but whitespace between — deliberately stopping at the first
real character, so a heading can never claim a figure's label further down. A float
claims only labels inside its own \begin/\end, matched by depth.
- One read, three indexes. Citations, glossary entries and labels all come out of the
same
.tex files, so there is exactly one workspace scan on activation, one debounce per
document, and one file-watcher pair. On a keystroke the buffer already in memory is
parsed once and fanned out. Running these as separate extensions would double the
discovery, the watchers, and the per-keystroke work.
- Incremental cache. State is stored per file. Editing one document re-parses that
document and merges its slice into the global indexes — the extension never rescans the
workspace on a keystroke.
- Debounced updates, 250 ms by default.
- Scoped consistently. Discovery, watching, and live edits are all limited to files
inside the workspace, so the index is a faithful picture of the project rather than of
whichever stray files happen to be open.
Getting started
- Open a folder (or multi-root workspace) containing your
.tex sources.
- Click the LaTeX Project icon in the activity bar.
- Browse Citations, Glossary and Labels; expand rows to see every occurrence,
and click one to jump to it.
- Type a new
\section{…} and watch the label appear beneath it.
| Control |
What it does |
| Search box |
Filters as you type — by key, title, author, year (citations) or key, name, short, long, description (glossary) |
Aa / ab / .* |
Match Case · Match Whole Word · Use Regular Expression |
| Group by |
Source/Entry · Source/Entry, then file · File, then source/entry · File |
| Filter |
All · Used · Unused · Undefined (glossary adds: Acronyms · Entries) |
| Sort |
Citation/usage count · Author · Title · Year · Key · Name · Kind |
Search, filter, sort, and grouping choices persist across restarts. Use Refresh in a
view's title bar to force a full re-scan.
Extension settings
| Setting |
Type |
Default |
Description |
latex-project-manager.debounceDelay |
number |
250 |
Milliseconds before re-parsing a document after you stop typing. |
latex-project-manager.citations.showOverview |
boolean |
true |
Show the Overview node in the Citations view. |
latex-project-manager.glossary.showOverview |
boolean |
true |
Show the Overview node in the Glossary view. |
latex-project-manager.glossary.diagnostics |
boolean |
true |
Report gls uses whose entry is declared nowhere in the Problems panel. |
latex-project-manager.labels.mode |
onType · onSave · manual · off |
onType |
When labels are written. manual lists pending changes in the Labels view and applies only after you confirm. |
latex-project-manager.labels.autoInsert |
boolean |
true |
Insert a \label under a heading you just typed. |
latex-project-manager.labels.syncReferences |
boolean |
true |
Rewrite every reference when a generated label is renamed. Off: only the \label changes, and the stale references are reported instead. |
latex-project-manager.labels.includeStarred |
boolean |
false |
Also label unnumbered \section*{…} headings. |
latex-project-manager.labels.maxLength |
number |
60 |
Slug length cap, excluding the prefix. Truncates on a word boundary. 0 disables. |
latex-project-manager.labels.diagnostics |
boolean |
true |
Report unresolvable references and duplicate labels in the Problems panel. |
latex-project-manager.labels.prefixes |
object |
sec, chap, fig, tab, lst, … |
Label prefix per sectioning command and float environment. |
Commands
| Command |
ID |
Notes |
| Refresh LaTeX Project Index |
latex-project-manager.refresh |
View title bar and Command Palette. Full re-scan. |
| Generate and Update Labels in This File |
latex-project-manager.generateLabels |
Adds and updates every label in the active .tex, in one undoable edit. |
| Review and Apply Label Changes… |
latex-project-manager.applyLabels |
Same, but lists the changes and waits for confirmation. Bound to the Apply row in the Labels view. |
| Resume Automatic Label Sync… |
latex-project-manager.resumeLabelSync |
Multi-select the labels you released, to bring them back under auto-sync. |
| Go to Usage · Go to Bib Definition · Copy Citation Key |
…goToUsage, …goToBibDefinition, …copyCitationKey |
Driven from the Citations view. |
| Go to Glossary Definition · Copy Glossary Key |
…goToGlossaryDefinition, …copyGlossaryKey |
Driven from the Glossary view. |
Upgrading from 1.x
The Marketplace treats the rename as a new extension ID
(ActiveClientMods.latex-project-manager), so install it alongside and remove the old one.
Command and setting IDs moved into the new namespace:
| 1.x |
2.0 |
latex-citation-statistics.debounceDelay |
latex-project-manager.debounceDelay |
latex-citation-statistics.showOverview |
latex-project-manager.citations.showOverview |
latex-citation-statistics.refresh |
latex-project-manager.refresh |
latex-citation-statistics.goToUsage |
latex-project-manager.goToUsage |
latex-citation-statistics.goToBibDefinition |
latex-project-manager.goToBibDefinition |
latex-citation-statistics.copyCitationKey |
latex-project-manager.copyCitationKey |
Citation behaviour is otherwise unchanged. If you would rather not have labels written for
you, set latex-project-manager.labels.mode to off — the two statistics views are
entirely read-only and unaffected.
Privacy & security
This extension is built for confidential, unpublished documents:
- Offline only — it uses solely the native VS Code API and Node.js. No external APIs.
- No network requests of any kind.
- No telemetry.
- Zero runtime dependencies — the shipped bundle contains only the extension's own
code, keeping the supply-chain surface effectively empty.
Requirements
- VS Code
^1.125.0.
- No other dependencies or configuration.
Known limitations
- Parsing is intentionally regex/brace-scan based rather than a full LaTeX grammar. Custom
citation macros are detected only if their command name contains
cite; custom glossary
or sectioning macros defined with \newcommand are not followed.
- Glossary entries pulled in via
\loadglsentries or a generated .glsdefs file are not
visible, which is why an undefined entry is reported as a warning rather than an error.
- BibTeX
@string abbreviations are not expanded when deriving titles.
Development
This project uses Bun for all package management and scripts.
bun install # install dev dependencies
bun run compile # type-check, lint, and bundle with esbuild
bun run watch # incremental rebuild while developing
bun run test # compile, lint, bundle, and run the full test suite
Press F5 in VS Code to launch the Extension Development Host with the extension
loaded.
Testing
The suite runs inside a real VS Code instance via @vscode/test-cli and covers:
- Unit tests for the shared scanner, all four parsers, the slug rules (including the
umlaut/accent ordering that makes
\"{U}ber and Über agree), float include-name
extraction, the managed-label state machine, the incremental indexes, and the view models.
- Lifecycle tests for the label planner: retitling, deletion, commenting out,
restoration, collisions, duplicates, and hand-written labels.
- Malformed-input tests for the synchronizer: emptied
label{} slots, unterminated
commands, CRLF documents, files with no trailing newline, duplicated generated labels,
and repeated applies (which must be idempotent).
- Integration tests for the synchronizer against real documents — cross-file reference
rewriting, single-step undo, and undo detection — plus the views and navigation commands.
- An end-to-end test driving the whole manager against a fixtures workspace, verifying
that one scan populates all three indexes and that live (unsaved) edits flow through
debounce into each of them as a delta.
Continuous integration
A GitHub Actions workflow (.github/workflows/ci.yml) runs on
every push and pull request. It installs with Bun, audits production dependencies,
type-checks, lints, and runs the full test suite headlessly (via xvfb).
Release notes
See CHANGELOG.md for the full history.
| |