Referencing lines in fancyvrb/minted

I prefer to use the minted package for typesetting syntax-highlighted code in LaTeX. Occasionally it is necessary to refer to a particular line of code by its number in the accompanying text. Of course one can just hard-code the line numbers into the text, but that’s not very TeX-like. This becomes very error-prone when the code needs to be modified and all hard-coded line number references manually synchronized. So I want this to be automatic, just like numbering and referencing of sections, figures, etc.

Luckily, minted builds on the much older fancyvrb package, which provides basic support for referencing lines. For example, the desired line can be marked using \label using escapeinside:

\begin{minted}[linenos,escapeinside=||]{python}
print("foo")
print("bar")|\label{ln:bar}|
print("baz")
\end{minted}

Then elsewhere \ref{ln:bar} expands to “2”.

Problem

Unfortunately, that’s also where the support ends. In particular, fancyvrb is incompatible with two other often-used packages:

  1. The hyperref package makes \ref{ln:bar} a hyperlink, but it’s a link to the wrong place! The link doesn’t go to the specific line of code or not even the beginning of the minted environment it is in. Instead, it goes to whatever happens to be the previous hyperref anchor at the point of \label{ln:bar}, e.g. a previous section heading, figure caption, etc. If you’re lucky, this might at least be on the same page as the code, but it could also be many pages before.

  2. The cleveref package adds \cref and friends, which prefix the reference number with its kind, e.g. \cref{sec:introduction} might expand to “section 1” instead of just “1”. The incompatibility with fancyvrb is similar to the one with hyperref: \cref{ln:bar} expands to a complete reference to something before the code, e.g. “section 1”. Not only is the “section” prefix wrong, but the number isn’t even the line number “2”. (At least it’s self-consistent: the number is for whatever is being referenced instead.)

TeXnical details

Deep inside LaTeX, the problem stems from the fact that both hyperref and cleveref achieve their functionality by redefining \refstepcounter. However, fancyvrb does not use that standard command and instead defines its own version:

\def\FV@refstepcounter#1{
  \stepcounter{#1}
  \protected@edef\@currentlabel{\csname p@#1\endcsname\arabic{FancyVerbLine}}
}

It uses explicitly uses \arabic{FancyVerbLine} instead of \theFancyVerbLine, which the standard definition would use. As far as I can tell, it needs to do that because it defines the latter as:

\def\theFancyVerbLine{\rmfamily\tiny\arabic{FancyVerbLine}}

which is weird because it puts the number formatting (for in the code environment) into the counter value formatting itself, which no other sensible counter would do. And \FV@refstepcounter is explicitly there to not make the number tiny at \ref{ln:bar}.

Non-solutions

There have been some attempts at avoiding the incompatibilities:

  1. A reddit thread suggests to use |\phantomsection\label{ln:bar}| in the minted environment instead. The \phantomsection provided by hyperref inserts a fresh anchor which \ref{ln:bar} will link to. This comes with two problems:
    1. It’s outright annoying to have to manually insert \phantomsection before every \label in code.
    2. The anchor inserted by \phantomsection is not at the beginning of the line of code, but at the specific column where the escaped label is placed. One could put all code line labels at the beginning of lines, but that makes the code even less readable: at the end of lines the code at least maintains its indentation in the LaTeX sources.
  2. One file on GitHub uses \let\FV@refstepcounter\refstepcounter to make fancyvrb use the standard command and thus allow hyperref/cleveref to modify it as usual. In order to avoid the tiny numbers at \ref{ln:bar}, it goes on to patch fancyvrb in various places to move the \tiny to a more appropriate place by adding numberstyle customization option. This is morally the right approach, but unfortunately also comes with two problems:
    1. It breaks fancyvrb’s firstnumber option and causes the first two lines to have the same number. Seems like fancyvrb’s logic is very particular to its oddities.
    2. The resulting hyperref anchors are based on the FancyVerbLine counter, which isn’t globally unique. So pdflatex warns about duplicate destinations and all of them link into the first minted environment, not the one where the \label actually is.
  3. Another file on GitHub only patches \FV@refstepcounter to add \refstepcounter of an additional global counter, avoiding the last issue. But it’s also not well-behaved, at least with minted: extra empty space appears before minted environments. I believe this is because minted works in two phases:
    1. The contents of a minted environment are not typeset, but written to a file (to run Pygments on). While doing so, fancyvrb still steps the counter which inserts spurious hyperref anchors but nothing else is typeset yet.
    2. After running Pygments, its result is somehow input into some fancyvrb environment for actual typesetting. This is what actually displays the syntax-highlighted code along with the line numbers (which are produced by stepping the line counter again).
  4. A TeX StackExchange answer defines alternative \label and \ref commands to use just for lines of code, which isn’t entirely satisfactory (one doesn’t need separate commands for sections, figures, etc.). It bypasses the usual hyperref anchor mechanism and uses \hypertarget and \hyperlink to directly work with custom PDF destinations. It also tries to provide a hint for cleveref, but admits that it still produces a wrong reference.

Solution

After digging deep into the implementation of fancyvrb, minted, hyperref and cleveref to (try to) understand how they work, I managed to put together a solution which seems to achieve the desired functionality without any of the downsides listed above. I wrapped my solution into my new fancyvrbref package. I haven’t (yet) published it on CTAN, but you can just copy it into your project for the time being.

The solution is the following:

  1. For hyperref compatibility, the fancyvrb internal command for actually typesetting a line is patched to insert a globally unique anchor with FancyVerbLine* prefix. Since this isn’t in \FV@refstepcounter, it doesn’t screw up minted.
  2. For cleveref compatibility, the \FV@refstepcounter command is extended to also define \cref@currentlabel. This is a bit ad hoc but avoids modifying \theFancyVerbLine.

Let me know if you find this package useful or find any issues with it!