SyncTeX (Vorwärts-/Rückwärtssuche)

Ein achtzehnseitiger Artikel ergibt ein PDF von 76.974 Byte – die daneben liegende .synctex.gz misst 159.347 Byte, also mehr als das Doppelte dessen, was sie beschreibt. Diese aufgeblähte Landkarte ist SyncTeX, und sie erledigt genau eine Aufgabe: sich zu merken, welche Zeile der LaTeX-Quelle zu welchem Rechteck auf welcher Seite wurde. Diese Seite öffnet die Datei, führt beide Richtungen von Hand mit synctex view und synctex edit aus, erklärt, warum ein Klick auf einer Zeile statt auf dem angezielten Wort landet, und schließt mit der Prüfliste für den Fall, dass die Vorwärtssuche gar nichts tut.

Was -synctex=1 tatsächlich erzeugt

Mit -synctex=1 schreibt die Engine neben dem PDF eine zusätzliche Datei mit gleichem Basisnamen: main.synctex.gz. Ohne die Option entsteht überhaupt nichts – das ist das mit Abstand häufigste Versäumnis bei der Einrichtung. Der Wert ist kein Wahrheitswert, sondern eine Bitmenge, wie man synctex unmissverständlich festhält: 0 oder fehlend heißt keine Datei, ein positiver Wert bedeutet gzip, ein negativer Wert unkomprimierten Klartext, das Bit 2 behält die Kompression, lässt aber .gz im Namen weg, 4 schaltet die Form-Unterstützung für pdfTeX ein und 8 komprimiert stärker. Alles zusammen ergibt -synctex=15. Einzig LuaTeX besteht auf der Langform --synctex=1. Die Mechanik steckt in TeX Live wie in MiKTeX gleichermaßen, und pdfLaTeX, XeLaTeX und LuaLaTeX liefern dieselbe Art von Landkarte.

terminal
pdflatex -synctex=1  main.tex     # writes main.synctex.gz
xelatex  -synctex=1  main.tex
lualatex --synctex=1 main.tex     # LuaTeX wants two dashes

pdflatex -synctex=-1 main.tex     # writes main.synctex, plain text
pdflatex -synctex=2  main.tex     # writes main.synctex -- still gzip inside!

Im Bit 2 steckt eine kleine Falle. Die von -synctex=2 erzeugte Datei heißt main.synctex, doch file meldet gzip-Daten darin. Wer der Endung traut und sie mit less öffnet, sieht Binärrauschen und hält die SyncTeX-Datei für beschädigt. Zum bloßen Lesen also lieber -synctex=-1. Wo die Kommandozeile nicht erreichbar ist – etwa in einer GUI, die auf Knopfdruck übersetzt –, aktiviert auch das TeX-Primitiv \synctex=1 am Anfang der Quelle die Aufzeichnung. Dieser Weg liefert allerdings ausschließlich die komprimierte Form: Auch \synctex=-1 erzeugte hier unter TeX Live 2024 weiterhin main.synctex.gz. Für Klartext führt kein Weg an der Kommandozeile vorbei.

WertGeschriebene DateiInhalt
(none)nichts wird geschrieben; keine Richtung funktioniert
-synctex=0wie ohne Option; die ausdrückliche Abschaltung
-synctex=1main.synctex.gzgzip-komprimiert; die Wahl für den Alltag
-synctex=-1main.synctexKlartext; die Form zum Nachsehen
-synctex=2main.synctexName wirkt unkomprimiert, Inhalt ist gzip – verwirrend
-synctex=15main.synctexBits 1+2+4+8: samt Form-Unterstützung und stärkerer Kompression

Die .synctex.gz auspacken und hineinsehen

Der Inhalt ist zeilenorientierter Text; gunzip -c main.synctex.gz macht ihn unmittelbar lesbar. Es gibt vier Abschnitte: Preamble, Content, Postamble und Post Scriptum. Die Preamble enthält die Version und die Input:-Tabelle, die jede von TeX geöffnete Datei ab 1 durchnummeriert. Nicht nur die eigene main.tex, sondern auch article.cls, size10.clo, jede .sty und main.aux bekommen eine Marke – darin liegt die halbe Erklärung für den Umfang der Landkarte. Danach definieren Magnification, Unit, X Offset und Y Offset das Koordinatensystem: Unit:1 heißt, alle folgenden Zahlen stehen in sp (scaled points, ein 65536stel Punkt), und X Offset:4736287 ist genau ein Zoll – der Rand, den TeX seit jeher von der linken oberen Papierecke nimmt.

terminal
$ gunzip -c main.synctex.gz        # abridged; ... marks omitted lines
SyncTeX Version:1
Input:1:/home/you/doc/./main.tex
Input:2:/usr/local/texlive/2024/texmf-dist/tex/latex/base/article.cls
Input:3:/usr/local/texlive/2024/texmf-dist/tex/latex/base/size10.clo
...
Input:10:/home/you/doc/./chap.tex
Output:pdf
Magnification:1000
Unit:1
X Offset:4736287
Y Offset:4736287
Content:
!962
{1
[1,17:4736286,46220574:26673152,41484288,0
(1,4:8799518,8865054:22609920,655359,0
x1,4:9330359,8865054
k1,4:31409438,8865054:11586343
...
)
]
}1

Der Content-Abschnitt ist eine Aufzeichnung verschachtelter Boxen. {1}1 ist ein Sheet, also eine Seite; eckige Klammern [] sind eine vertikale, runde () eine horizontale Box. Jeder Öffner hat die Form Marke,Zeile:x,y:Breite,Höhe,Tiefe, sodass (1,4:8799518,8865054:22609920,655359,0 besagt: „eine horizontale Box, entstanden in Zeile 4 der Marke 1, also main.tex.“ Im obigen Beispiel stand in Zeile 4 von main.tex genau \section{Forward and inverse}. Das erste Zeichen einer Zeile nennt den Satztyp: x die aktuelle Position, k ein Kern, g Glue, $ Mathematik, f eine pdfTeX-Formularreferenz, v und h leere vertikale und horizontale Boxen, ! ein Byte-Offset zum Anspringen mitten in der Datei.

Jede Seite in dieser Feinheit festzuhalten lässt die Datei anschwellen. Beim eingangs erwähnten achtzehnseitigen Artikel maß die komprimierte Karte 159.347 Byte, die entpackte 638.962 Byte – über das Achtfache des PDF, verteilt auf 24.717 Zeilen. Deshalb ist .synctex.gz kein Ergebnis, sondern eine jederzeit neu erzeugbare Arbeitsdatei: ab in die .gitignore, und zusätzlich in latexmks @generated_exts, damit das Aufräumen sie mitnimmt. Die Handbuchseite synctex(5) ist an einem verwandten Punkt deutlich: Die Struktur sei nicht als öffentlich zu betrachten, und außer dem Befehl synctex und der Bibliothek synctex_parser müsse sie niemand auswerten. Zum Verstehen eines Problems darin zu lesen ist in Ordnung; ein Werkzeug dauerhaft darauf zu gründen nicht.

perl
# .latexmkrc -- keep -synctex=1 on every build, and clean the map up afterwards
$pdf_mode  = 1;
$pdflatex  = 'pdflatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
@generated_exts = (@generated_exts, 'synctex.gz');

Vorwärts- und Rückwärtssuche von Hand ausführen

Vorwärtssuche (Quelle → PDF) ist synctex view, Rückwärtssuche (PDF → Quelle) ist synctex edit. Was Editor und Betrachter hinter ihren Schaltflächen aufrufen, sind entweder diese beiden oder ein Äquivalent. Streikt also die Rückwärtssuche in LaTeX, trennt der direkte Aufruf sofort die beiden möglichen Ursachen: eine fehlerhafte Landkarte oder ein fehlerhaftes Zusammenspiel von Editor und Betrachter. Die Vorwärtssuche nimmt -i Zeile:Spalte:Datei und -o pdf und antwortet mit Seitenzahl und Rechteck.

terminal
$ synctex view -i 5:1:main.tex -o main.pdf
This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:main.pdf
Page:1
x:153.195526
y:156.585541
h:133.768356
v:158.522720
W:343.711060
H:8.855677
before:
offset:-1
middle:
after:
SyncTeX result end

Das Paar x und y ist der anzuzeigende Punkt; h, v, W, H sind linke Kante, Grundlinie, Breite und Höhe des hervorzuhebenden Rechtecks. Einheit sind PDF-Punkte, v:158.52 bedeutet also 158,52 pt unterhalb des Seitenkopfs. Der Betrachter nimmt diese Zahlen, scrollt und lässt ein Band von W mal H kurz aufleuchten. Die Gegenrichtung wirft schlicht eine Koordinate zurück.

terminal
$ synctex edit -o 1:153.19:156.58:main.pdf
This is SyncTeX command line utility, version 1.5
SyncTeX result begin
Output:main.pdf
Input:/home/you/doc/./main.tex
Line:5
Column:-1
Offset:0
Context:
SyncTeX result end

Das Argument lautet -o Seite:x:y:pdf, zurück kommen ein absoluter Dateipfad und eine Zeilennummer. Der Betrachter setzt dieses Input: und Line: in den Befehl ein, der den Editor startet. Auffällig ist Column:-1. Das Format könnte eine Spalte darstellen, doch die Engines schreiben keine, weshalb die Rückwärtssuche praktisch stets nur zeilengenau ist. Deshalb setzt der Editor den Cursor an den Zeilenanfang – das ist kein Konfigurationsfehler.

Warum der Sprung auf einer Zeile landet und nicht auf dem angeklickten Wort

Weil die Einheit der Zuordnung eine gesetzte Box ist. TeX macht aus einem Absatz eine einzige lange horizontale Liste und zerschneidet sie erst ganz am Schluss in Zeilen. SyncTeX merkt sich die entstandenen Boxen und die Quellzeile, die jede erzeugt hat – keine Wörter, keine Zeichen. Misst man nach, springt die Asymmetrie ins Auge: Stehen zwölf kurze Wörter auf zwölf aufeinanderfolgenden Zeilen ohne Leerzeile dazwischen, fallen sie zu nur zwei Zeilenboxen zusammen. Fragt man die Vorwärtssuche der Reihe nach zu den Quellzeilen 5 bis 12, antwortet jede mit derselben Koordinate.

terminal
# twelve short source lines 5..16, no blank line between them
$ for l in 5 6 7 8 9 10 11 12 13 14 15 16; do
>   printf "src %-3s " $l; synctex view -i $l:1:res.tex -o res.pdf | grep "^v:"
> done
src 5   v:230.405960
src 6   v:230.405960
src 7   v:230.405960
src 8   v:230.405960
src 9   v:230.405960
src 10  v:230.405960
src 11  v:230.405960
src 12  v:230.405960
src 13  v:242.361130
src 14  v:242.361130
src 15  v:242.361130
src 16  v:242.361130

Interessant ist, dass die Gegenrichtung etwas cleverer ist. Führt man synctex edit von links nach rechts über dieselbe Zeilenbox, liefert es je nach horizontaler Position unterschiedliche Quellzeilen – und oft mehrere Kandidaten pro Punkt, von denen der Betrachter üblicherweise den ersten nimmt. Die Vorwärtssuche ist also grob, die Rückwärtssuche fein. Umgekehrt: In einem Absatz, in dem eine einzige lange Quellzeile auf acht gesetzte Zeilen umbrach, lieferte jeder Klick auf eine dieser acht dieselbe Zeile 3 – weil es schlicht nur eine Quellzeile zu merken gab. Ein Wort danebenzuliegen innerhalb eines TikZ-Bildes, in der Expansion eines verwickelten Makros oder im Inneren einer Tabelle ist dieselbe Geschichte der Box-Granularität, kein Fehler.

terminal
# same typeset line (baseline v=230.4), scanning left to right
$ for x in 135 185 235 310 360 435 460; do
>   printf "x=%-4s " $x; synctex edit -o 1:$x:229:res.pdf | grep "^Line:" | tr "\n" " "; echo
> done
x=135  Line:5
x=185  Line:5
x=235  Line:6 Line:7
x=310  Line:7 Line:8
x=360  Line:9 Line:10
x=435  Line:10 Line:11
x=460  Line:11 Line:12

Daraus folgt eine praktische Konsequenz. Schreibt man die Quelle als eine einzige riesige Zeile, sinkt die Auflösung von SyncTeX für den ganzen Absatz auf einen einzigen Punkt. Umbrüche nach Sätzen oder wenigstens an Satzgliedgrenzen lassen die Rückwärtssuche wieder treffen. Die Schreibweise, die Diffs in der Versionsverwaltung lesbar macht, und die, die SyncTeX präzise macht, sind zufällig dieselbe.

Warum die Zeilennummern bei \input-Dateien verrutschen

Kurz gesagt: \input ist selbst keine Ursache für Verschiebungen. Jeder Satz trägt neben der Zeilennummer eine Marke, und die Marke verweist in die Input:-Tabelle. Eine eingebundene Datei bekommt ihre eigene Marke, und ihre Zeilennummern sind die Zeilennummern innerhalb dieser Datei. Gemessen: Ein Klick in ein per \input{chap} eingezogenes Kapitel lieferte chap.tex als Input: und die dortige Zeilennummer als Line:. Auch bei zwanzig verketteten Kapiteln wird nichts aufaddiert.

Zwei echte Ursachen gibt es. Die erste ist eine veraltete Landkarte. Eine .synctex.gz ist die Momentaufnahme eines Laufs: Fügt man oben in chap.tex drei Zeilen ein und sucht rückwärts, ohne neu zu übersetzen, antwortet die Karte weiterhin Line:3, obwohl der Text längst in Zeile 6 steht. Entspricht der Versatz genau der Zahl der eingefügten Zeilen, ist es mit hoher Wahrscheinlichkeit das. Die zweite ist der absolute Pfad. In Input: landet der volle Pfad zum Zeitpunkt der Übersetzung; ein verschobenes Projekt, ein Zugriff über einen Symlink oder ein Lauf im Container bei Ansicht außerhalb schicken den Editor an einen Pfad, den es nicht mehr gibt. Öffnet sich die falsche Datei – oder gar keine – statt der falschen Zeile, ist das der Verdächtige.

Der Rückwärtssuch-Befehl je Betrachter und die Platzhalter

Die Rückwärtssuche wird auf der Betrachterseite eingestellt. Man übergibt eine Vorlage: Wird geklickt, setze diese Zeilennummer und diesen Dateinamen ein und führe diesen Befehl aus. Lästig ist, dass die Platzhalter-Schreibweise je Betrachter abweicht. zathura nutzt geschweifte Klammern – %{line} und %{input}; Skim %line und %file; SumatraPDF und Okular %l und %f. Anderswo abgeschriebene Einstellungen scheitern meist genau daran: Der Befehl stimmt, nur die Platzhalter passen nicht.

BetrachterPlattformPlatzhalter für Zeile und Datei
zathuraLinux / BSD%{line} und %{input}, über set synctex-editor-command
SkimmacOS%line und %file, unter Preferences ▸ Sync ▸ Preset: Custom
SumatraPDFWindows%l und %f, im Feld inverse search unter Settings ▸ Options
OkularLinux / Windows%l und %f, unter Einstellungen ▸ Editor (für Kile kile --line %l)
Adobe Acrobat / Readerallekeine SyncTeX-Unterstützung; Rückwärtssuche ist schlicht nicht möglich
ini
# zathura (~/.config/zathura/zathurarc)
set synctex true
set synctex-editor-command "nvim --headless -c 'VimtexInverseSearch %{line} %{input}'"

# Skim  -- Preferences > Sync > Preset: Custom
Command:   nvim
Arguments: --headless -c "VimtexInverseSearch %line '%file'"

# SumatraPDF -- Settings > Options > inverse search command-line
cmd /c start /min "" nvim --headless -c "VimtexInverseSearch %l '%f'"

# Okular -- Settings > Configure Okular > Editor
kile --line %l

Die Vorwärtssuche von der Editorseite auszulösen ist unkompliziert: In TeXShop mit Skim ist es Cmd-Klick im PDF, für die Gegenrichtung Umschalt-Cmd-Klick. TeXstudio nutzt Strg-Klick oder die Menüpunkte „Gehe zu PDF“ und „Zur Quelle springen“. VS Code mit LaTeX Workshop nimmt Strg/Cmd+Alt+J. Eine macOS-typische Falle gehört hier benannt: Das mit macOS ausgelieferte /usr/bin/vim ist -clientserver gebaut, es gibt also gar keinen Kanal, über den etwas von außen den Editor zurückrufen könnte, und das übliche Rückwärtssuch-Schnipsel bewirkt stillschweigend nichts. Abhilfe schaffen MacVim, Homebrews Vim oder Neovim.

Was auf dem DVI-Weg geschieht (pLaTeX / upLaTeX → dvipdfmx)

Vorab das Ergebnis: Mit den Voreinstellungen ist nichts zu tun, und die Koordinaten stimmen mit dem direkten PDF-Weg überein. -synctex=1 gehört an die Engine (platex oder uplatex), nicht an den Konverter. Die Engine schreibt die .synctex.gz neben die DVI, und ihre Preamble meldet Output:dvi statt Output:pdf. Ein anschließender Lauf von dvipdfmx rührt diese Karte nicht an – ein Vergleich der Datei vor und nach dem Lauf mit cmp ergab hier Byte-Gleichheit, und dvipdfmx besitzt ohnehin keine -synctex-Option. Nebenbei: Das dvipdfmx aus TeX Live 2024 ist ein Symlink auf xdvipdfmx, also dieselbe Binärdatei wie der Konverter für XeTeX.

terminal
# the same source, both routes, same forward-search question
$ pdflatex -synctex=1 cmp.tex && synctex view -i 3:1:cmp.tex -o cmp.pdf
h:133.768356   v:137.554138

$ latex -synctex=1 cmp.tex && dvipdfmx cmp.dvi
$ synctex view -i 3:1:cmp.tex -o cmp.pdf
h:133.768372   v:137.554153

# the two agree to about 2e-5 pt -- nothing needs reconciling

Wozu dient dann synctex update? Genau zu dem, was sein Handbuch sagt – die SyncTeX-Datei zu aktualisieren, nachdem ein dvi/xdv-nach-pdf-Filter gelaufen ist – und nötig ist es nur, wenn dieser Umwandlung eine Vergrößerung oder ein Versatz mitgegeben wurde. An -m, -x und -y übergibt man dieselben Werte wie an den Filter. Interessant ist die Umsetzung: synctex update schreibt die Karte nicht um. Ein Lauf mit -x 20mm und ein byteweiser Vergleich davor und danach zeigten, dass lediglich hinter der abschließenden Zeile Post scriptum: ein gzip-Block angehängt wird. Entpackt enthält er genau eine Zeile: X Offset:20mm. Der vierte Abschnitt des Formats ist also die Stelle, an der ein nachgelagerter Konverter eine Korrektur des Koordinatensystems anheften kann wie einen Notizzettel. Im Alltag erledigen ptex2pdf oder latexmk diese Kette, und das Thema kommt nie auf.

Wenn SyncTeX nichts tut: was der Reihe nach zu prüfen ist

Zuerst prüfen, ob eine .synctex.gz im selben Ordner wie das PDF liegt. Fehlt sie, enthält der Build kein -synctex=1. Leicht zu übersehen ist dabei der Build-Befehl, den der Editor mitbringt. Kiles mitgeliefertes Werkzeug PDFLaTeX etwa führt -synctex=1 nicht in seinen Standardoptionen, und genau dieses Fehlen ist die häufigste Ursache für „ich habe alles eingestellt, und nichts synchronisiert“. Ein Häkchen bei SyncTeX in den Editoreinstellungen ändert nicht immer den Befehl, der tatsächlich läuft.

  • Gibt es eine Karte? Mit ls nach der .synctex.gz sehen. Fehlt sie, -synctex=1 in den Build-Befehl aufnehmen – und die Voreinstellungen des Editors für verdächtig halten.
  • Wurden PDF und Karte getrennt? -output-directory ist unproblematisch, denn beide landen gemeinsam im Ausgabeordner; kopiert man aber nur das PDF heraus, bleibt die Karte zurück und nichts passiert. Hier gemessen: Nach dem Kopieren allein von main.pdf aus build/ kehrte synctex view stumm zurück.
  • Ist die Karte veraltet? Nach dem Speichern neu übersetzt? Entspricht der Versatz der Zahl der eben eingefügten Zeilen, ist der Fall klar. Läuft latexmk mit -pvc, sodass jedes Speichern neu übersetzt, verschwindet dieser Fehler nahezu vollständig.
  • Wurde tatsächlich dieses Dokument gesetzt? Übersetzt man eine Kapiteldatei für sich, beschreibt die Karte das PDF dieses Kapitels, nicht das des Buches. Prüfen, ob die Einstellung für Masterdatei beziehungsweise Wurzeldokument im Editor dorthin zeigt, wo man sie vermutet.
  • Unterstützt der Betrachter SyncTeX? Adobe Acrobat/Reader kann Rückwärtssuche überhaupt nicht. Umsteigen auf Skim (macOS), SumatraPDF (Windows) oder Okular und zathura (Linux).
  • Stimmen die Platzhalter? %{line}, %line und %l zu verwechseln ist gerade deshalb schwer zu bemerken, weil der Rest des Befehls stimmt.
  • Das Problem an der Kommandozeile aufteilen. synctex view und synctex edit direkt aufrufen. Antworten sie richtig, ist die Karte gesund und der Fehler liegt im Zusammenspiel von Editor und Betrachter. Zu beachten: synctex liefert auch ohne Fund den Rückgabewert 0, ein Skript muss also die Ausgabe prüfen, nicht den Status.

Zum Schluss die Schleife, die aus SyncTeX statt einer Einstellung eine Korrekturgewohnheit macht. PDF lesen, ein störendes Wort anklicken, in der Quelle landen, ändern, speichern, neu übersetzen und per Vorwärtssuche zur gerade korrigierten Stelle zurückkehren. Läuft diese Schleife rund, sinkt die Zeit, in der man eine lange Quelle nach der zu ändernden Stelle durchsucht, auf null. Der Name, den Jérôme Laurens seinem Werk gab – Synchronize TeXnology –, klingt großspurig, doch was er tatsächlich einbringt, ist genau dies: nie mehr suchen zu müssen.