SyncTeX(前方・後方検索)

18 ページの記事を組んでみると、PDF は 76,974 バイト、その傍らに落ちる .synctex.gz は 159,347 バイト——説明される側の 2 倍以上ありました。この肥大した地図が SyncTeX です。やっていることは一つだけで、LaTeX ソースのどの行が、どのページのどの矩形になったかを覚えておく。それだけのために PDF の倍の容量を使います。このページでは、-synctex=1 が実際に何を書き出すのか、その中身を解凍して覗き、synctex viewsynctex edit を手で叩いて前方検索と逆検索の両方向を動かし、なぜクリックした語ではなく行に飛ぶのかまで見ていきます。SyncTeX が効かないときの切り分けも最後に置きました。

-synctex=1 は結局なにを作るのか

-synctex=1 を付けると、エンジンは PDF のほかにもう一つ、PDF と同じ場所・同じ基本名の main.synctex.gz を書き出します。付けなければ何も書き出されません——SyncTeX の設定でいちばん多い取りこぼしがこれです。値は真偽ではなくビットで、man synctex にそのまま書いてあります。0 か指定なしで無し、正なら gzip 圧縮、負なら圧縮しない素のテキスト2 のビットが立つと圧縮したまま .gz を付けない名前になり、4 は pdfTeX のフォーム対応、8 はより強い圧縮。全部載せなら -synctex=15 です。LuaTeX だけはハイフン二つの --synctex=1 を使います。この仕組みは TeX Live にも MiKTeX にも同じように入っており、pdfLaTeX・XeLaTeX・LuaLaTeX のどれからでも同じ形の地図が得られます。

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!

この 2 のビットには小さな罠があります。-synctex=2 で出てくるファイルの名前は main.synctex なのに、file で見ると中身は gzip のままです。拡張子を信じて less で開くとバイナリが出てきて、SyncTeX ファイルが壊れたと勘違いします。中を読みたいだけなら素直に -synctex=-1 を使ってください。なお、コマンドラインを触れない環境(ボタン一つで組む GUI など)では、ソース先頭の TeX プリミティブ \synctex=1 でも有効化できます。ただしこちらは 負の値を書いても圧縮版しか出ません——\synctex=-1 と書いても、手元の TeX Live 2024 では main.synctex.gz が出てきました。素のテキストが欲しければコマンドライン一択です。

指定出るファイル中身
(none)何も書かれない。逆検索も前方検索も動かない
-synctex=0指定なしと同じ。明示的に無効化するとき
-synctex=1main.synctex.gzgzip 圧縮。実務ではこれ
-synctex=-1main.synctex素のテキスト。中を読んで調べるとき
-synctex=2main.synctex名前は非圧縮ふうだが中身は gzip。紛らわしい
-synctex=15main.synctexビット 1+2+4+8。フォーム対応と強圧縮も込み

.synctex.gz を解凍して中身を読む

中身は行指向のテキストで、gunzip -c main.synctex.gz を通せばそのまま読めます。構造は前文(preamble)・本体(content)・後文(postamble)・追記(post scriptum)の 4 部。前文にはバージョンと Input: の表 があり、TeX が開いたファイルすべてに 1 から番号(タグ)が振られます。本文の main.tex だけでなく article.clssize10.clo.stymain.aux も並びます——地図がここまで太る理由の半分はこれです。続く Magnification Unit X Offset Y Offset が座標系の定義で、Unit:1 は数値がすべて sp(scaled point、1pt の 65536 分の 1) であることを意味します。X Offset:4736287 はちょうど 1 インチ、TeX が伝統的に紙の左上から取る余白です。

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

本体は入れ子の箱の記録です。{1}1 が 1 ページぶんの「シート」、角括弧 [] が縦の箱、丸括弧 () が横の箱。それぞれの見出しは タグ,行:x,y:幅,高さ,深さ という形をしていて、たとえば (1,4:8799518,8865054:22609920,655359,0 は「タグ 1(= main.tex)の 4 行目から生まれた横の箱」を意味します。上の例で main.tex の 4 行目は \section{Forward and inverse} でした。行の頭の 1 文字が記録の種類で、x は現在位置、k はカーン、g はグルー、$ は数式、f は pdfTeX のフォーム参照、vh は中身のない縦箱・横箱、! は途中から読み始めるためのバイト位置です。

これだけの粒度で全ページを記録するので、ファイルはよく太ります。冒頭の 18 ページの記事では、圧縮後で 159,347 バイト、解凍すると 638,962 バイト——PDF 本体の 8 倍以上、行数にして 24,717 行でした。だから .synctex.gz は成果物ではなく再生成できる作業ファイルです。.gitignore に入れ、latexmk の @generated_exts にも足して掃除の対象にしておくのが定石です。ちなみに synctex(5) の man ページは、この形式は公開仕様とみなすべきではなく、synctex コマンドと synctex_parser ライブラリ以外が解析する必要はない、とはっきり断っています。読むのは構いませんが、自作ツールで恒久的に依存する対象ではありません。

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');

前方検索と逆検索を、コマンドラインで動かしてみる

前方検索(forward search、原稿 → PDF)synctex view逆検索(inverse search、PDF → 原稿)synctex edit です。エディタとビューアがボタンの裏で呼んでいるのはこの 2 つか、その同等品で、LaTeX の逆検索がうまくいかないときは、まずこの 2 つを直接叩けば「地図が悪いのか、エディタとビューアの連携が悪いのか」を一発で切り分けられます。前方検索は -i 行:桁:ファイル-o PDF を渡すと、ページ番号と矩形を返します。

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

xy は「ここを見せろ」という点、h v W H はハイライトすべき矩形の左端・ベースライン・幅・高さです。単位は PDF のポイント(bp)で、v:158.52 はページ上端から 158.52pt 下という意味。ビューアはこの数値を受け取ってスクロールし、W × H の帯を一瞬光らせています。逆方向はその座標を投げ返すだけです。

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

引数は -o ページ:x:y:PDF の形で、返るのは ファイルの絶対パスと行番号 です。ビューアはこの Input:Line: を、エディタを起動するコマンドに埋め込んで呼びます。ここで目を引くのが Column:-1。SyncTeX は桁を記録できる形式を持っていますが、実際のエンジンは桁を書き出さないので、逆検索は事実上つねに「行まで」です。エディタがカーソルを行頭に置くのはこのためで、設定の不備ではありません。

なぜクリックした語ではなく「行」に飛ぶのか

対応づけの最小単位が 組版の箱 だからです。TeX は段落を一本の長い横並びにしてから、最後にまとめて行に切ります。SyncTeX が覚えているのは切り上がった箱と、その箱を作ったソース行だけで、語や文字は覚えていません。この非対称は実測するとよく見えます。空行なしで 12 行に分けて短い語を書いた原稿を組むと、12 行が たった 2 行の箱 に収まりました。前方検索を 5 行目から 12 行目まで順に叩くと、返る座標が全部同じになります。

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

面白いのは、逆方向はもう少し賢い ことです。同じ行の帯を左から右へなぞって synctex edit を叩くと、水平位置に応じて別々のソース行が返ってきます。しかも一点につき候補が複数返ることがあり、ビューアは先頭を採るのがふつうです。つまり「前方検索は粗く、逆検索は細かい」。逆に、1 行の長いソース行が 8 行に折り返された段落では、どの折り返し行をクリックしても同じ行番号 3 が返りました——覚えているソース行がそもそも 1 本しかないからです。TikZ の図中、複雑なマクロの展開結果、表組みの内部で狙いから一語ずれるのも、すべてこの箱の粒度の話で、不具合ではありません。

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

ここから実務的な結論が一つ出ます。ソースを 1 行に長々と書くと、SyncTeX の分解能はその段落まるごと 1 点に落ちます。 逆に 1 文 1 行、あるいは節目で改行しておくと、逆検索がぐっと当たるようになります。版管理の差分が読みやすくなる書き方と、SyncTeX が効きやすい書き方は、たまたま同じです。

\input した子ファイルで行番号がずれる理由

結論から言うと、\input そのものはずれの原因ではありません。 各記録は行番号だけでなくタグを持っていて、タグは Input: の表を指します。子ファイルには子ファイルのタグが付き、行番号もその子ファイルの中の行番号です。実測でも、\input{chap} した章の中をクリックすると Input:chap.tex が、Line: にその中での行番号が返りました。章を 20 本つないでも番号は足し算されません。

本当の原因は二つあります。一つは 地図の古さ.synctex.gz は組んだ瞬間の写真なので、chap.tex の先頭に 3 行足して再コンパイルしないまま逆検索すると、地図はいまだに Line:3 を返します——本文はすでに 6 行目に移っているのに。ずれ幅が「足した行数ぴったり」なら、ほぼ確実にこれです。もう一つは 絶対パスInput: に書かれるのは組んだときのフルパスで、プロジェクトを移動したり、シンボリックリンク越しに開いたり、コンテナの中で組んで外で開いたりすると、ビューアは存在しないパスへエディタを飛ばそうとします。行がずれるのではなく、まったく別のファイルが開く(あるいは何も開かない)ときは、こちらを疑ってください。

ビューアごとの逆検索コマンドと、置換記号の違い

逆検索の設定は ビューア側 に書きます。「クリックされたら、この行番号とこのファイル名を埋めて、このコマンドを実行せよ」というテンプレートを渡す形です。ここで面倒なのは、置換記号の書式がビューアごとに違う こと。zathura は波括弧つきの %{line}%{input}、Skim は %line%file、SumatraPDF と Okular は %l%f です。他所からコピーした設定が動かない原因の大半はこれで、コマンド自体は合っているのに置換記号だけが噛み合っていません。

ビューア主な OS行と ファイルの置換記号
zathuraLinux / BSD%{line}%{input}。設定は set synctex-editor-command
SkimmacOS%line%file。Preferences ▸ Sync ▸ Preset: Custom
SumatraPDFWindows%l%f。Settings ▸ Options の inverse search 欄
OkularLinux / Windows%l%f。設定 ▸ エディタで指定(Kile なら kile --line %l
Adobe Acrobat / ReaderすべてSyncTeX 非対応。逆検索は原理的に使えない
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

エディタ側から見た 前方検索 の起こし方は素直で、TeXShop と Skim なら PDF 上で Cmd + クリック、逆向きは Shift + Cmd + クリック。TeXstudio は Ctrl + クリック、または「PDF へ移動」「ソースへ移動」。VS Code の LaTeX Workshop は Ctrl/Cmd+Alt+J です。ここで一つ、macOS 特有の落とし穴を挙げておきます。macOS 同梱の /usr/bin/vim-clientserver でビルドされている ので、外部から呼び返す口が最初から存在せず、よくある逆検索用の設定を書いても静かに何も起きません。MacVim か Homebrew の Vim、あるいは Neovim に移るのが解決です。

DVI 経由(pLaTeX・upLaTeX → dvipdfmx)ではどうなるか

結論を先に書くと、既定の設定なら何もしなくてよく、座標は直接 PDF を吐く経路と一致します。 -synctex=1 は変換器ではなく エンジン側platex / uplatex)に渡します。エンジンは DVI を書くと同時に .synctex.gz も書き、前文の Output:pdf ではなく dvi になります。その後 dvipdfmx を走らせても、この地図には指一本触れません——手元で dvipdfmx の前後を cmp にかけたところ、バイト単位で同一でした。そもそも dvipdfmx-synctex オプションはありません。ちなみに TeX Live 2024 の dvipdfmxxdvipdfmx へのシンボリックリンクで、実体は 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

では synctex update は何のためにあるのか。man の言葉どおり「DVI/XDV → PDF のフィルタをかけた後に SyncTeX ファイルを更新する」ためで、必要になるのは 変換に倍率やオフセットを指定したとき だけです。-m / -x / -y にはフィルタへ渡したのと同じ値を与えます。面白いのはその実装で、synctex update は地図の中身を書き換えません。実際に -x 20mm を付けて走らせ、前後をバイト比較したところ、ファイル末尾の Post scriptum: の後ろに gzip の塊が 追記 されているだけでした。解凍すると中身は X Offset:20mm の 1 行。つまり形式の第 4 部「追記」は、後段の変換器が座標系の訂正を付箋のように貼るための場所なのです。ふだんは ptex2pdf や latexmk がこの一連を面倒みてくれるので、意識する場面はまずありません。

SyncTeX が効かないときに、上から順に見るところ

まず .synctex.gz が PDF と同じフォルダにあるかを見ます。無ければビルドに -synctex=1 が入っていません。ここで見落としがちなのが、エディタが用意している既定のビルド設定 です。たとえば Kile が同梱している PDFLaTeX ツールの既定オプションには -synctex=1 が入っておらず、「設定したのに何も同期しない」の最大の原因になっています。エディタの GUI で SyncTeX の項目を有効にしただけでは、実際に走るコマンドが変わっていないことがある、と覚えておいてください。

  • 地図があるか。 ls.synctex.gz を確認する。無ければビルドコマンドに -synctex=1 を足す(エディタの既定設定は疑ってかかる)。
  • PDF と地図が離れていないか。 -output-directory を使うと両方まとめて出力先に落ちるので問題ありませんが、PDF だけをコピーして持ち出すと地図が付いてこず、何も起きません。 手元の実測でも、build/ から main.pdf だけをコピーした先では synctex view が無言で終わりました。
  • 地図が古くないか。 保存したあと再ビルドしたか。ずれ幅が「編集で足した行数」と一致するなら確定です。 latexmk の -pvc で保存のたびに組み直しておけば、この失敗はまず起きません。
  • 組んだのは本当にその文書か。 章ファイルだけを単独でコンパイルすると、地図はその章だけの PDF を説明します。エディタの「マスターファイル」「ルート文書」の指定が思ったところを指しているか確かめてください。
  • ビューアが SyncTeX に対応しているか。 Adobe Acrobat / Reader では逆検索は原理的に不可能です。Skim(macOS)、SumatraPDF(Windows)、Okular・zathura(Linux)に替えます。
  • 置換記号が合っているか。 %{line} / %line / %l の取り違えは、コマンドが正しいだけに気づきにくい失敗です。
  • コマンドラインで切り分ける。 synctex viewsynctex edit を直に叩く。ここで正しい答えが返るなら地図は健全で、問題はエディタとビューアの連携側にあります。なお synctex は結果が見つからなくても終了コード 0 を返すので、スクリプトで判定するときは出力の中身を見る必要があります。

最後に、SyncTeX を「設定項目」ではなく 校正の作法 として使うための一周を書いておきます。PDF を眺めて気になった語をクリックし、原稿へ戻り、直し、保存して再ビルドし、前方検索で直した箇所へ戻る。この一周が滑らかに回るなら、長い文書のどこを直せばよいか探し回る時間はゼロになります。SyncTeX を考えた ジェローム・ローラン(Jérôme Laurens) が名前に選んだ Synchronize TeXnology という言葉は、いささか大げさに響きますが、実際に得られるのはこの「探さなくてよい」という一点です。