Synctex с обратным поиском для MacVim & zathura

29 июля 2022. Комментарии .

Запись обновлена в июле 2026.

Оглавление #

Цель #

Связать редактор и просмотрщик PDF в обе стороны:

Проблемы на macOS #

Forward-поиск завязан на D-Bus, которого в macOS нет. Чтобы zathura --synctex-forward нашёл уже открытое окно с нужным PDF, zathura общается с ним через freedesktop D-Bus session bus. В macOS такой шины по умолчанию нет, и попытка форварда падает:

1
2
error: Could not connect to session bus: Could not connect: Connection refused
error: Got no usable data from SyncTeX or D-Bus failed in some way.

Решение — поднять одну общую session-шину launchd-агентом org.freedesktop.dbus-session (plist поставляется с brew-пакетом dbus, который ставится как зависимость zathura). Launchd создаёт сокет сам и публикует его путь в переменной DBUS_LAUNCHD_SESSION_BUS_SOCKET для всего GUI-сеанса; демон стартует при первом обращении. Vim перед запуском zathura лишь узнаёт этот сокет через launchctl и прокидывает его в своё окружение — так все процессы zathura (и запущенные из vim, и открытые вручную) оказываются на одной шине.

В ранней версии этой записи vim поднимал собственную шину на фиксированном сокете. От этого я отказался: окна zathura, открытые не из vim, жили на другой шине (или вовсе без неё), forward их не видел и открывал дубли.

Ещё две мелочи, из-за которых плодятся лишние окна:

Установка #

Установите zathura с поддержкой synctex (tap переехал от zegervdv в организацию homebrew-zathura). Может потребоваться вот этот фикс.

1
2
3
4
5
6
7
8
brew tap homebrew-zathura/zathura
brew trust homebrew-zathura/zathura
brew install zathura --with-synctex
brew install zathura-pdf-poppler zathura-djvu zathura-ps
mkdir -p $(brew --prefix zathura)/lib/zathura
ln -s $(brew --prefix zathura-pdf-poppler)/libpdf-poppler.dylib $(brew --prefix zathura)/lib/zathura/libpdf-poppler.dylib
ln -s $(brew --prefix zathura-djvu)/libdjvu.dylib $(brew --prefix zathura)/lib/zathura/libdjvu.dylib
ln -s $(brew --prefix zathura-ps)/libps.dylib $(brew --prefix zathura)/lib/zathura/libps.dylib

Вместе с zathura придут dbus и glib (из неё используется утилита gdbus).

Загрузите launchd-агент session-шины — plist уже лежит в brew-пакете dbus:

1
2
cp /opt/homebrew/opt/dbus/org.freedesktop.dbus-session.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.freedesktop.dbus-session.plist

Проверка: launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET должен напечатать путь к сокету.

Также, разумеется, установите LaTeX, MacVim.

Реализация #

zathurarc #

~/.config/zathura/zathurarc:

~/.config/zathura/zathurarc
1
2
3
set dbus-service true
set synctex true
set synctex-editor-command "/Users/slisakov/.local/bin/mvim-synctex %{line} %{input}"

dbus-service true включает D-Bus-интерфейс окна (нужен для forward). Обратный поиск идёт через скрипт mvim-synctex — он ниже.

mvim-synctex #

~/.local/bin/mvim-synctex (не забудьте chmod +x):

~/.local/bin/mvim-synctex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/zsh
# Обратный synctex zathura -> MacVim.
# Ищет среди запущенных инстанций MacVim ту, где уже открыт файл, и переключается
# на неё (с переходом на строку). Если файл не открыт нигде — открывает НОВУЮ
# инстанцию (окно) MacVim, не трогая буферы существующих.
#
# Вызов из zathurarc:
# set synctex-editor-command "/Users/slisakov/.local/bin/mvim-synctex %{line} %{input}"

emulate -L zsh
line="$1"
file="$2"
MVIM=/Applications/MacVim.app/Contents/bin/mvim

abs="$(realpath "$file" 2>/dev/null)" # канонический путь (регистр/симлинки)
[[ -z "$abs" ]] && abs="${file:a}"

# vim-выражение: открыт ли файл в этом сервере?
# Только канонический полный путь (resolve убирает симлинки). Сравнение по
# имени файла давало бы ложные срабатывания на одноимённых файлах в других
# папках — и тогда :drop заменял бы вкладку в чужой инстанции.
expr='!empty(filter(getbufinfo(), {i,b -> resolve(fnamemodify(b.name,":p"))==#"'"$abs"'"}))'

# Глюк MacVim: --remote-expr печатает результат в stderr, приклеивая
# "Send expression failed." (stdout пустой). Поэтому берём 2>&1 и смотрим,
# начинается ли ответ с "1".
target=""
for s in ${(f)"$("$MVIM" --serverlist 2>/dev/null)"}; do
[[ -z "$s" ]] && continue
res="$("$MVIM" --servername "$s" --remote-expr "$expr" 2>&1)"
if [[ "$res" == 1* ]]; then
target="$s"
break
fi
done

if [[ -n "$target" ]]; then
# файл уже открыт в этой инстанции: :drop перейдёт на его вкладку/окно
exec "$MVIM" --servername "$target" --remote-silent "+$line" "$abs"
else
# нигде не открыт: новая инстанция MacVim (без --remote ничего не шлётся
# в существующие серверы, значит их вкладки не затрагиваются)
exec "$MVIM" "+$line" "$abs"
fi

.vimrc #

~/.vimrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
" --- zathura synctex (macOS) ---------------------------------------------
" forward-search zathura ищет открытое окно через D-Bus session bus. В macOS
" эту шину поднимает launchd-агент org.freedesktop.dbus-session (общий для ВСЕХ
" приложений: Finder, терминал, vim). Здесь только указываем vim'у на её сокет,
" чтобы запущенная из vim zathura была на той же шине, что и открытая вручную,
" — иначе forward не найдёт окно и откроет новое.
function! s:EnsureSyncBus() abort
let l:sock = trim(system('launchctl getenv DBUS_LAUNCHD_SESSION_BUS_SOCKET'))
if empty(l:sock)
let l:uid = trim(system('id -u'))
let l:cmd = 'launchctl print gui/' . l:uid
\ . " | awk '/DBUS_LAUNCHD_SESSION_BUS_SOCKET =>/ {print $3; exit}'"
let l:sock = trim(system(l:cmd))
endif
if empty(l:sock)
echoerr 'SyncTeX: D-Bus session socket not found; refusing to open a duplicate PDF'
return 0
endif
let $DBUS_LAUNCHD_SESSION_BUS_SOCKET = l:sock
let $DBUS_SESSION_BUS_ADDRESS = 'unix:path=' . l:sock
return 1
endfunction

" Канонический путь к текущему .tex (realpath: единый регистр/симлинки).
" Важно: open и forward должны давать zathura ОДНУ И ТУ ЖЕ строку пути к PDF,
" иначе zathura (matching чувствителен к строке) не узнаёт уже открытый файл
" и открывает новое окно.
function! s:CanonTex() abort
let l:t = trim(system('realpath ' . shellescape(expand('%:p'))))
return (v:shell_error == 0 && !empty(l:t)) ? l:t : expand('%:p')
endfunction

" Список живых экземпляров Zathura в текущей D-Bus-сессии.
function! s:ZathuraServices() abort
let l:cmd = '/opt/homebrew/bin/gdbus call --session'
\ . ' --dest org.freedesktop.DBus'
\ . ' --object-path /org/freedesktop/DBus'
\ . ' --method org.freedesktop.DBus.ListNames'
\ . ' | /usr/bin/grep -oE '
\ . shellescape('org\.pwmt\.zathura\.PID-[0-9]+')
return uniq(sort(systemlist(l:cmd)))
endfunction

" Найти экземпляр, в котором уже открыт этот PDF. Сравниваем realpath,
" поэтому разные написания пути и симлинки не создают второе окно.
function! s:ZathuraServiceFor(pdf) abort
let l:target = trim(system('realpath ' . shellescape(a:pdf)))
if empty(l:target)
let l:target = a:pdf
endif
for l:service in s:ZathuraServices()
let l:cmd = '/opt/homebrew/bin/gdbus call --session --dest '
\ . shellescape(l:service)
\ . ' --object-path /org/pwmt/zathura'
\ . ' --method org.freedesktop.DBus.Properties.Get'
\ . ' org.pwmt.zathura filename'
let l:info = system(l:cmd)
if v:shell_error != 0
continue
endif
let l:filename = matchstr(l:info, "<'\\zs.\\{-}\\ze'>")
if empty(l:filename)
continue
endif
let l:registered = trim(system('realpath ' . shellescape(l:filename)))
if (empty(l:registered) ? l:filename : l:registered) ==# l:target
return l:service
endif
endfor
return ''
endfunction

" Открыть превью PDF (под нужной шиной, без дублей окон)
function! ZathuraOpen() abort
if !s:EnsureSyncBus()
return
endif
let l:pdf = fnamemodify(s:CanonTex(), ':r') . '.pdf'
if filereadable(l:pdf) && empty(s:ZathuraServiceFor(l:pdf))
call job_start(['/opt/homebrew/bin/zathura', l:pdf],
\ {'stoponexit': ''})
endif
endfunction

" Forward search: позиция курсора в vim -> подсветка в zathura
function! SyncTexForward() abort
if !s:EnsureSyncBus()
return
endif
let l:tex = s:CanonTex()
let l:pos = line('.') . ':' . col('.') . ':' . l:tex
let l:pdf = fnamemodify(l:tex, ':r') . '.pdf'
let l:service = s:ZathuraServiceFor(l:pdf)
if !empty(l:service)
" Обращаемся прямо к открытому окну: новый процесс Zathura не запускается.
let l:args = ['/opt/homebrew/bin/gdbus', 'call', '--session',
\ '--dest', l:service,
\ '--object-path', '/org/pwmt/zathura',
\ '--method', 'org.pwmt.zathura.SynctexView',
\ l:tex, string(line('.')), string(col('.'))]
" Не блокируем Vim, пока Zathura обрабатывает переход и подсветку.
call job_start(l:args, {'stoponexit': ''})
return
endif
" Нужный PDF действительно не открыт — только тогда создаём окно.
call job_start(['/opt/homebrew/bin/zathura', '--synctex-forward',
\ l:pos, l:pdf], {'stoponexit': ''})
endfunction

" Компиляция, открытие превью, forward
map <F11> <ESC>:w<CR>:!pdflatex -synctex=1 '%'<CR><CR>
set macmeta " чтобы сработало Option+8
nnoremap <silent> <M-8> :call ZathuraOpen()<CR>
inoremap <silent> <M-8> <C-o>:call ZathuraOpen()<CR>
nnoremap <silent> <C-CR> :call SyncTexForward()<CR>
inoremap <silent> <C-CR> <C-o>:call SyncTexForward()<CR>

Несколько пояснений, почему именно так:

Порядок работы #

  1. Открыть .tex в MacVim (mvim file.tex).
  2. F11 — скомпилировать с -synctex=1 (для надёжных ссылок прогнать дважды); под xelatex у меня такая же привязка на F12.
  3. Alt+8 — открыть превью в zathura (под общей D-Bus-шиной).
  4. Ctrl+Enter на нужной строке — zathura подсвечивает соответствующее место (forward). Если pdf не был открыт, то он откроется и по Ctrl+Enter.
  5. Ctrl+левый клик в zathura — переход на нужную строку в той инстанции MacVim, где файл открыт; если он не открыт нигде — откроется новая инстанция (backward).

Окна zathura, открытые до загрузки launchd-агента (или до внесения правок), к шине не подключены — перезапустите их, иначе forward-поиск их не увидит.