config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

pdf-sync.el (28440B)


      1 ;;; pdf-sync.el --- Use synctex to correlate LaTeX-Sources with PDF positions. -*- lexical-binding:t -*-
      2 ;; Copyright (C) 2013, 2014  Andreas Politz
      3 
      4 ;; Author: Andreas Politz <politza@fh-trier.de>
      5 ;; Keywords: files, doc-view, pdf
      6 
      7 ;; This program is free software; you can redistribute it and/or modify
      8 ;; it under the terms of the GNU General Public License as published by
      9 ;; the Free Software Foundation, either version 3 of the License, or
     10 ;; (at your option) any later version.
     11 
     12 ;; This program is distributed in the hope that it will be useful,
     13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15 ;; GNU General Public License for more details.
     16 
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     19 
     20 ;;; Commentary:
     21 ;;
     22 ;; The backward search uses a heuristic, which is pretty simple, but
     23 ;; effective: It extracts the text around the click-position in the
     24 ;; PDF, normalizes its whitespace, deletes certain notorious
     25 ;; characters and translates certain other characters into their latex
     26 ;; equivalents.  This transformed text is split into a series of
     27 ;; token.  A similar operation is performed on the source code around
     28 ;; the position synctex points at.  These two sequences of token are
     29 ;; aligned with a standard sequence alignment algorithm, resulting in
     30 ;; an alist of matched and unmatched tokens.  This is then used to
     31 ;; find the corresponding word from the PDF file in the LaTeX buffer.
     32 
     33 
     34 (require 'pdf-view)
     35 (require 'pdf-info)
     36 (require 'pdf-util)
     37 (require 'let-alist)
     38 
     39 ;;; Code:
     40 
     41 (defgroup pdf-sync nil
     42   "Jump from TeX sources to PDF pages and back."
     43   :group 'pdf-tools)
     44 
     45 (defcustom pdf-sync-forward-display-pdf-key "C-c C-g"
     46   "Key to jump from a TeX buffer to its PDF file.
     47 
     48 This key is added to `TeX-source-correlate-method', when
     49 command `pdf-sync-minor-mode' is activated and this map is defined."
     50   :type 'key-sequence)
     51 
     52 (make-obsolete-variable
     53  'pdf-sync-forward-display-pdf-key
     54  "Bound in Auctex's to C-c C-v, if TeX-source-correlate-mode is activate." "1.0")
     55 
     56 (defcustom pdf-sync-backward-hook nil
     57   "Hook ran after going to a source location.
     58 
     59 The hook is run in the TeX buffer."
     60   :type 'hook
     61   :options '(pdf-sync-backward-beginning-of-word))
     62 
     63 (defcustom pdf-sync-forward-hook nil
     64   "Hook ran after displaying the PDF buffer.
     65 
     66 The hook is run in the PDF's buffer."
     67   :type 'hook)
     68 
     69 (defcustom pdf-sync-forward-display-action nil
     70   "Display action used when displaying PDF buffers."
     71   :type 'display-buffer--action-custom-type)
     72 
     73 (defcustom pdf-sync-backward-display-action nil
     74   "Display action used when displaying TeX buffers."
     75   :type 'display-buffer--action-custom-type)
     76 
     77 (defcustom pdf-sync-locate-synctex-file-functions nil
     78   "A list of functions for locating the synctex database.
     79 
     80 Each function on this hook should accept a single argument: The
     81 absolute path of a PDF file.  It should return the absolute path
     82 of the corresponding synctex database or nil, if it was unable to
     83 locate it."
     84   :type 'hook)
     85 
     86 (defvar pdf-sync-minor-mode-map
     87   (let ((kmap (make-sparse-keymap)))
     88     (define-key kmap [double-mouse-1] #'pdf-sync-backward-search-mouse)
     89     (define-key kmap [C-mouse-1] #'pdf-sync-backward-search-mouse)
     90     kmap))
     91 
     92 (defcustom pdf-sync-backward-redirect-functions nil
     93   "List of functions which may redirect a backward search.
     94 
     95 Functions on this hook should accept three arguments, namely
     96 SOURCE, LINE and COLUMN, where SOURCE is the absolute filename of
     97 the source file and LINE and COLUMN denote the position in the
     98 file.  COLUMN may be negative, meaning unspecified.
     99 
    100 These functions should either return nil, if no redirection is
    101 necessary.  Or a list of the same structure, with some or all (or
    102 none) values modified.
    103 
    104 AUCTeX installs a function here which changes the backward search
    105 location for synthetic `TeX-region' files back to the equivalent
    106 position in the original tex file."
    107   :type '(repeat function))
    108 
    109 
    110 ;;;###autoload
    111 (define-minor-mode pdf-sync-minor-mode
    112   "Correlate a PDF position with the TeX file.
    113 \\<pdf-sync-minor-mode-map>
    114 This works via SyncTeX, which means the TeX sources need to have
    115 been compiled with `--synctex=1'.  In AUCTeX this can be done by
    116 setting `TeX-source-correlate-method' to `synctex' (before AUCTeX
    117 is loaded) and enabling `TeX-source-correlate-mode'.
    118 
    119 Then \\[pdf-sync-backward-search-mouse] in the PDF buffer will
    120 open the corresponding TeX location.
    121 
    122 If AUCTeX is your preferred tex-mode, this library arranges to
    123 bind `pdf-sync-forward-display-pdf-key' (the default is `C-c C-g')
    124 to `pdf-sync-forward-search' in `TeX-source-correlate-map'.  This
    125 function displays the PDF page corresponding to the current
    126 position in the TeX buffer.  This function only works together
    127 with AUCTeX."
    128   :group 'pdf-sync
    129   (pdf-util-assert-pdf-buffer))
    130 
    131 
    132 ;; * ================================================================== *
    133 ;; * Backward search (PDF -> TeX)
    134 ;; * ================================================================== *
    135 
    136 (defcustom pdf-sync-backward-use-heuristic t
    137   "Whether to apply a heuristic when backward searching.
    138 
    139 If nil, just go where Synctex tells us.  Otherwise try to find
    140 the exact location of the clicked-upon text in the PDF."
    141   :type 'boolean)
    142 
    143 (defcustom pdf-sync-backward-text-translations
    144   '((88 "X" "sum")
    145     (94 "textasciicircum")
    146     (126 "textasciitilde")
    147     (169 "copyright" "textcopyright")
    148     (172 "neg" "textlnot")
    149     (174 "textregistered" "textregistered")
    150     (176 "textdegree")
    151     (177 "pm" "textpm")
    152     (181 "upmu" "mu")
    153     (182 "mathparagraph" "textparagraph" "P" "textparagraph")
    154     (215 "times")
    155     (240 "eth" "dh")
    156     (915 "Upgamma" "Gamma")
    157     (920 "Uptheta" "Theta")
    158     (923 "Uplambda" "Lambda")
    159     (926 "Upxi" "Xi")
    160     (928 "Uppi" "Pi")
    161     (931 "Upsigma" "Sigma")
    162     (933 "Upupsilon" "Upsilon")
    163     (934 "Upphi" "Phi")
    164     (936 "Uppsi" "Psi")
    165     (945 "upalpha" "alpha")
    166     (946 "upbeta" "beta")
    167     (947 "upgamma" "gamma")
    168     (948 "updelta" "delta")
    169     (949 "upvarepsilon" "varepsilon")
    170     (950 "upzeta" "zeta")
    171     (951 "upeta" "eta")
    172     (952 "uptheta" "theta")
    173     (953 "upiota" "iota")
    174     (954 "upkappa" "varkappa" "kappa")
    175     (955 "uplambda" "lambda")
    176     (957 "upnu" "nu")
    177     (958 "upxi" "xi")
    178     (960 "uppi" "pi")
    179     (961 "upvarrho" "uprho" "rho")
    180     (962 "varsigma")
    181     (963 "upvarsigma" "upsigma" "sigma")
    182     (964 "uptau" "tau")
    183     (965 "upupsilon" "upsilon")
    184     (966 "upphi" "phi")
    185     (967 "upchi" "chi")
    186     (968 "uppsi" "psi")
    187     (969 "upomega" "omega")
    188     (977 "upvartheta" "vartheta")
    189     (981 "upvarphi" "varphi")
    190     (8224 "dagger")
    191     (8225 "ddagger")
    192     (8226 "bullet")
    193     (8486 "Upomega" "Omega")
    194     (8501 "aleph")
    195     (8592 "mapsfrom" "leftarrow")
    196     (8593 "uparrow")
    197     (8594 "to" "mapsto" "rightarrow")
    198     (8595 "downarrow")
    199     (8596 "leftrightarrow")
    200     (8656 "shortleftarrow" "Leftarrow")
    201     (8657 "Uparrow")
    202     (8658 "Mapsto" "rightrightarrows" "Rightarrow")
    203     (8659 "Downarrow")
    204     (8660 "Leftrightarrow")
    205     (8704 "forall")
    206     (8706 "partial")
    207     (8707 "exists")
    208     (8709 "varnothing" "emptyset")
    209     (8710 "Updelta" "Delta")
    210     (8711 "nabla")
    211     (8712 "in")
    212     (8722 "-")
    213     (8725 "setminus")
    214     (8727 "*")
    215     (8734 "infty")
    216     (8743 "wedge")
    217     (8744 "vee")
    218     (8745 "cap")
    219     (8746 "cup")
    220     (8756 "therefore")
    221     (8757 "because")
    222     (8764 "thicksim" "sim")
    223     (8776 "thickapprox" "approx")
    224     (8801 "equiv")
    225     (8804 "leq")
    226     (8805 "geq")
    227     (8810 "lll")
    228     (8811 "ggg")
    229     (8814 "nless")
    230     (8815 "ngtr")
    231     (8822 "lessgtr")
    232     (8823 "gtrless")
    233     (8826 "prec")
    234     (8832 "nprec")
    235     (8834 "subset")
    236     (8835 "supset")
    237     (8838 "subseteq")
    238     (8839 "supseteq")
    239     (8853 "oplus")
    240     (8855 "otimes")
    241     (8869 "bot" "perp")
    242     (9702 "circ")
    243     (9792 "female" "venus")
    244     (9793 "earth")
    245     (9794 "male" "mars")
    246     (9824 "spadesuit")
    247     (9827 "clubsuit")
    248     (9829 "heartsuit")
    249     (9830 "diamondsuit"))
    250   "Alist mapping PDF character to a list of LaTeX macro names.
    251 
    252 Adding a character here with its LaTeX equivalent names allows
    253 the heuristic backward search to find its location in the source
    254 file.  These strings should not match
    255 `pdf-sync-backward-source-flush-regexp'.
    256 
    257 Has no effect if `pdf-sync-backward-use-heuristic' is nil."
    258   :type '(alist :key-type character
    259                 :value-type (repeat string)))
    260 
    261 (defconst pdf-sync-backward-text-flush-regexp
    262   "[][.ยท{}|\\]\\|\\C.\\|-\n+"
    263   "Regexp of ignored text when backward searching.")
    264 
    265 (defconst pdf-sync-backward-source-flush-regexp
    266   "\\(?:\\\\\\(?:begin\\|end\\|\\(?:eq\\)?ref\\|label\\|cite\\){[^}]*}\\)\\|[][\\&{}$_]"
    267   "Regexp of ignored source when backward searching.")
    268 
    269 (defconst pdf-sync-backward-context-limit 64
    270   "Number of character to include in the backward search.")
    271 
    272 (defun pdf-sync-backward-search-mouse (ev)
    273   "Go to the source corresponding to position at event EV."
    274   (interactive "@e")
    275   (let* ((posn (event-start ev))
    276          (image (posn-image posn))
    277          (xy (posn-object-x-y posn)))
    278     (unless image
    279       (error "Outside of image area"))
    280     (pdf-sync-backward-search (car xy) (cdr xy))))
    281 
    282 (defun pdf-sync-backward-search (x y)
    283   "Go to the source corresponding to image coordinates X, Y.
    284 
    285 Try to find the exact position, if
    286 `pdf-sync-backward-use-heuristic' is non-nil."
    287   (cl-destructuring-bind (source finder)
    288       (pdf-sync-backward-correlate x y)
    289     (pop-to-buffer (or (find-buffer-visiting source)
    290                        (find-file-noselect source))
    291                    pdf-sync-backward-display-action)
    292     (push-mark)
    293     (funcall finder)
    294     (run-hooks 'pdf-sync-backward-hook)))
    295 
    296 (defun pdf-sync-backward-correlate (x y)
    297   "Find the source corresponding to image coordinates X, Y.
    298 
    299 Returns a list \(SOURCE FINDER\), where SOURCE is the name of the
    300 TeX file and FINDER a function of zero arguments which, when
    301 called in the buffer of the aforementioned file, will try to move
    302 point to the correct position."
    303 
    304   (pdf-util-assert-pdf-window)
    305   (let ((size (pdf-view-image-size))
    306         (page (pdf-view-current-page)))
    307     (setq x (/ x (float (car size)))
    308           y (/ y (float (cdr size))))
    309     (let-alist (pdf-info-synctex-backward-search page x y)
    310       (let ((data (list (expand-file-name .filename)
    311                         .line .column)))
    312         (cl-destructuring-bind (source line column)
    313             (or (save-selected-window
    314                   (apply #'run-hook-with-args-until-success
    315                     'pdf-sync-backward-redirect-functions data))
    316                 data)
    317           (list source
    318                 (if (not pdf-sync-backward-use-heuristic)
    319                     (lambda nil
    320                       (pdf-util-goto-position line column))
    321                   (let ((context (pdf-sync-backward--get-text-context page x y)))
    322                     (lambda nil
    323                       (pdf-sync-backward--find-position line column context))))))))))
    324 
    325 (defun pdf-sync-backward--find-position (line column context)
    326   (pdf-util-goto-position line column)
    327   (cl-destructuring-bind (windex chindex words)
    328       context
    329     (let* ((swords (pdf-sync-backward--get-source-context
    330                     nil (* 6 pdf-sync-backward-context-limit)))
    331            (similarity-fn (lambda (text source)
    332                             (if (if (consp text)
    333                                     (member source text)
    334                                   (equal text source))
    335                                 1024 -1024)))
    336            (alignment
    337             (pdf-util-seq-alignment
    338              words swords similarity-fn 'infix)))
    339       (setq alignment (cl-remove-if-not 'car (cdr alignment)))
    340       (cl-assert (< windex (length alignment)))
    341 
    342       (let ((word (cdr (nth windex alignment))))
    343         (unless word
    344           (setq chindex 0
    345                 word (cdr (nth (1+ windex) alignment))))
    346         (unless word
    347           (setq word (cdr (nth (1- windex) alignment))
    348                 chindex (length word)))
    349         (when word
    350           (cl-assert (get-text-property 0 'position word) t)
    351           (goto-char (get-text-property 0 'position word))
    352           (forward-char chindex))))))
    353 
    354 (defun pdf-sync-backward--get-source-context (&optional position limit)
    355   (save-excursion
    356     (when position (goto-char position))
    357     (goto-char (line-beginning-position))
    358     (let* ((region
    359             (cond
    360              ((eq limit 'line)
    361               (cons (line-beginning-position)
    362                     (line-end-position)))
    363 
    364              ;; Synctex usually jumps to the end macro, in case it
    365              ;; does not understand the environment.
    366              ((and (fboundp 'LaTeX-find-matching-begin)
    367                    (looking-at " *\\\\\\(end\\){"))
    368               (cons (or (ignore-errors
    369                           (save-excursion
    370                             (LaTeX-find-matching-begin)
    371                             (forward-line 1)
    372                             (point)))
    373                         (point))
    374                     (point)))
    375              ((and (fboundp 'LaTeX-find-matching-end)
    376                    (looking-at " *\\\\\\(begin\\){"))
    377               (goto-char (line-end-position))
    378               (cons (point)
    379                     (or (ignore-errors
    380                           (save-excursion
    381                             (LaTeX-find-matching-end)
    382                             (forward-line 0)
    383                             (point)))
    384                         (point))))
    385              (t (cons (point) (point)))))
    386            (begin (car region))
    387            (end (cdr region)))
    388       (when (numberp limit)
    389         (let ((delta (- limit (- end begin))))
    390           (when (> delta 0)
    391             (setq begin (max (point-min)
    392                              (- begin (/ delta 2)))
    393                   end (min (point-max)
    394                            (+ end (/ delta 2)))))))
    395       (let ((string (buffer-substring-no-properties begin end)))
    396         (dotimes (i (length string))
    397           (put-text-property i (1+ i) 'position (+ begin i) string))
    398         (nth 2 (pdf-sync-backward--tokenize
    399                 (pdf-sync-backward--source-strip-comments string)
    400                 nil
    401                 pdf-sync-backward-source-flush-regexp))))))
    402 
    403 (defun pdf-sync-backward--source-strip-comments (string)
    404   "Strip all standard LaTeX comments from string."
    405   (with-temp-buffer
    406     (save-excursion (insert string))
    407     (while (re-search-forward
    408             "^\\(?:[^\\\n]\\|\\(?:\\\\\\\\\\)\\)*\\(%.*\\)" nil t)
    409       (delete-region (match-beginning 1) (match-end 1)))
    410     (buffer-string)))
    411 
    412 (defun pdf-sync-backward--get-text-context (page x y)
    413   (cl-destructuring-bind (&optional char edges)
    414       (car (pdf-info-charlayout page (cons x y)))
    415     (when edges
    416       (setq x (nth 0 edges)
    417             y (nth 1 edges)))
    418     (let* ((prefix (pdf-info-gettext page (list 0 0 x y)))
    419            (suffix (pdf-info-gettext page (list x y 1 1)))
    420            (need-suffix-space-p (memq char '(?\s ?\n)))
    421            ;; Figure out whether we missed a space by matching the
    422            ;; prefix's suffix with the line's prefix.  Due to the text
    423            ;; extraction in poppler, spaces are only inserted in
    424            ;; between words.  This test may fail, if prefix and line
    425            ;; do not overlap, which may happen in various cases, but
    426            ;; we don't care.
    427            (need-prefix-space-p
    428             (and (not need-suffix-space-p)
    429                  (memq
    430                   (ignore-errors
    431                     (aref (pdf-info-gettext page (list x y x y) 'line)
    432                           (- (length prefix)
    433                              (or (cl-position ?\n prefix :from-end t)
    434                                  -1)
    435                              1)))
    436                   '(?\s ?\n)))))
    437       (setq prefix
    438             (concat
    439              (substring
    440               prefix (max 0 (min (1- (length prefix))
    441                                  (- (length prefix)
    442                                     pdf-sync-backward-context-limit))))
    443              (if need-prefix-space-p " "))
    444             suffix
    445             (concat
    446              (if need-suffix-space-p " ")
    447              (substring
    448               suffix 0 (max 0 (min (1- (length suffix))
    449                                    pdf-sync-backward-context-limit)))))
    450       (pdf-sync-backward--tokenize
    451        prefix suffix
    452        pdf-sync-backward-text-flush-regexp
    453        pdf-sync-backward-text-translations))))
    454 
    455 (defun pdf-sync-backward--tokenize (prefix &optional suffix flush-re translation)
    456   (with-temp-buffer
    457     (when prefix (insert prefix))
    458     (let* ((center (copy-marker (point)))
    459            (case-fold-search nil))
    460       (when suffix (insert suffix))
    461       (goto-char 1)
    462       ;; Delete ignored text.
    463       (when flush-re
    464         (save-excursion
    465           (while (re-search-forward flush-re nil t)
    466             (replace-match " " t t))))
    467       ;; Normalize whitespace.
    468       (save-excursion
    469         (while (re-search-forward "[ \t\f\n]+" nil t)
    470           (replace-match " " t t)))
    471       ;; Split words and non-words
    472       (save-excursion
    473         (while (re-search-forward "[^ ]\\b\\|[^ [:alnum:]]" nil t)
    474           (insert-before-markers " ")))
    475       ;; Replace character
    476       (let ((translate
    477              (lambda (string)
    478                (or (and (= (length string) 1)
    479                         (cdr (assq (aref string 0)
    480                                    translation)))
    481                    string)))
    482             words
    483             (windex -1)
    484             (chindex 0))
    485         (skip-chars-forward " ")
    486         (while (and (not (eobp))
    487                     (<= (point) center))
    488           (cl-incf windex)
    489           (skip-chars-forward "^ ")
    490           (skip-chars-forward " "))
    491         (goto-char center)
    492         (when (eq ?\s (char-after))
    493           (skip-chars-backward " "))
    494         (setq chindex (- (skip-chars-backward "^ ")))
    495         (setq words (split-string (buffer-string)))
    496         (when translation
    497           (setq words (mapcar translate words)))
    498         (list windex chindex words)))))
    499 
    500 (defun pdf-sync-backward-beginning-of-word ()
    501   "Maybe move to the beginning of the word.
    502 
    503 Don't move if already at the beginning, or if not at a word
    504 character.
    505 
    506 This function is meant to be put on `pdf-sync-backward-hook', when
    507 word-level searching is desired."
    508   (interactive)
    509   (unless (or (looking-at "\\b\\w")
    510               (not (looking-back "\\w" (1- (point)))))
    511     (backward-word)))
    512 
    513 ;; * ------------------------------------------------------------------ *
    514 ;; * Debugging backward search
    515 ;; * ------------------------------------------------------------------ *
    516 
    517 (defvar pdf-sync-backward-debug-trace nil)
    518 
    519 (defun pdf-sync-backward-debug-wrapper (fn-symbol fn &rest args)
    520   (cond
    521    ((eq fn-symbol 'pdf-sync-backward-search)
    522     (setq pdf-sync-backward-debug-trace nil)
    523     (apply fn args))
    524    (t
    525     (let ((retval (apply fn args)))
    526       (push `(,args . ,retval)
    527             pdf-sync-backward-debug-trace)
    528       retval))))
    529 
    530 (define-minor-mode pdf-sync-backward-debug-minor-mode
    531   "Aid in debugging the backward search."
    532   :group 'pdf-sync
    533   (let ((functions
    534          '(pdf-sync-backward-search
    535            pdf-sync-backward--tokenize
    536            pdf-util-seq-alignment)))
    537     (cond
    538      (pdf-sync-backward-debug-minor-mode
    539       (dolist (fn functions)
    540         (advice-add fn :around
    541                     (apply-partially #'pdf-sync-backward-debug-wrapper fn)
    542                     `((name . ,(format "%s-debug" fn))))))
    543      (t
    544       (dolist (fn functions)
    545         (advice-remove fn (format "%s-debug" fn)))))))
    546 
    547 (defun pdf-sync-backward-debug-explain ()
    548   "Explain the last backward search.
    549 
    550 Needs to have `pdf-sync-backward-debug-minor-mode' enabled."
    551 
    552   (interactive)
    553   (unless pdf-sync-backward-debug-trace
    554     (error "No last search or `pdf-sync-backward-debug-minor-mode' not enabled."))
    555 
    556   (with-current-buffer (get-buffer-create "*pdf-sync-backward trace*")
    557     (cl-destructuring-bind (text source alignment &rest ignored)
    558         (reverse pdf-sync-backward-debug-trace)
    559       (let* ((fill-column 68)
    560              (sep (format "\n%s\n" (make-string fill-column ?-)))
    561              (highlight '(:background "chartreuse" :foreground "black"))
    562              (or-sep "|")
    563              (inhibit-read-only t)
    564              (windex (nth 0 (cdr text)))
    565              (chindex (nth 1 (cdr text))))
    566         (erase-buffer)
    567         (font-lock-mode -1)
    568         (view-mode 1)
    569         (insert (propertize "Text Raw:" 'face 'font-lock-keyword-face))
    570         (insert sep)
    571         (insert (nth 0 (car text)))
    572         (insert (propertize "<|>" 'face highlight))
    573         (insert (nth 1 (car text)))
    574         (insert sep)
    575         (insert (propertize "Text Token:" 'face 'font-lock-keyword-face))
    576         (insert sep)
    577         (fill-region (point)
    578                      (progn
    579                        (insert
    580                         (mapconcat (lambda (elt)
    581                                      (if (consp elt)
    582                                          (mapconcat #'identity elt or-sep)
    583                                        elt))
    584                                    (nth 2 (cdr text)) " "))
    585                        (point)))
    586         (insert sep)
    587 
    588         (insert (propertize "Source Raw:" 'face 'font-lock-keyword-face))
    589         (insert sep)
    590         (insert (nth 0 (car source)))
    591         (insert sep)
    592         (insert (propertize "Source Token:" 'face 'font-lock-keyword-face))
    593         (insert sep)
    594         (fill-region (point)
    595                      (progn (insert (mapconcat #'identity (nth 2 (cdr source)) " "))
    596                             (point)))
    597         (insert sep)
    598 
    599         (insert (propertize "Alignment:" 'face 'font-lock-keyword-face))
    600         (insert (format " (windex=%d, chindex=%d" windex chindex))
    601         (insert sep)
    602         (save-excursion (newline 2))
    603         (let ((column 0)
    604               (index 0))
    605           (dolist (a (cdr (cdr alignment)))
    606             (let* ((source (cdr a))
    607                    (text (if (consp (car a))
    608                              (mapconcat #'identity (car a) or-sep)
    609                            (car a)))
    610                    (extend (max (length text)
    611                                 (length source))))
    612               (when (and (not (bolp))
    613                          (> (+ column extend)
    614                             fill-column))
    615                 (forward-line 2)
    616                 (newline 3)
    617                 (forward-line -2)
    618                 (setq column 0))
    619               (when text
    620                 (insert (propertize text 'face
    621                                     (if (= index windex)
    622                                         highlight
    623                                       (if source 'match
    624                                         'lazy-highlight)))))
    625               (move-to-column (+ column extend) t)
    626               (insert " ")
    627               (save-excursion
    628                 (forward-line)
    629                 (move-to-column column t)
    630                 (when source
    631                   (insert (propertize source 'face (if text
    632                                                        'match
    633                                                      'lazy-highlight))))
    634                 (move-to-column (+ column extend) t)
    635                 (insert " "))
    636               (cl-incf column (+ 1 extend))
    637               (when text (cl-incf index)))))
    638         (goto-char (point-max))
    639         (insert sep)
    640         (goto-char 1)
    641         (pop-to-buffer (current-buffer))))))
    642 
    643 
    644 ;; * ================================================================== *
    645 ;; * Forward search (TeX -> PDF)
    646 ;; * ================================================================== *
    647 
    648 (defun pdf-sync-forward-search (&optional line column)
    649   "Display the PDF location corresponding to LINE, COLUMN."
    650   (interactive)
    651   (cl-destructuring-bind (pdf page _x1 y1 _x2 _y2)
    652       (pdf-sync-forward-correlate line column)
    653     (let ((buffer (or (find-buffer-visiting pdf)
    654                       (find-file-noselect pdf))))
    655       (with-selected-window (display-buffer
    656                              buffer pdf-sync-forward-display-action)
    657         (pdf-util-assert-pdf-window)
    658         (when page
    659 	  (pdf-view-goto-page page)
    660 	  (when y1
    661 	    (let ((top (* y1 (cdr (pdf-view-image-size)))))
    662 	      (pdf-util-tooltip-arrow (round top))))))
    663       (with-current-buffer buffer
    664         (run-hooks 'pdf-sync-forward-hook)))))
    665 
    666 (defun pdf-sync-forward-correlate (&optional line column)
    667   "Find the PDF location corresponding to LINE, COLUMN.
    668 
    669 Returns a list \(PDF PAGE X1 Y1 X2 Y2\), where PAGE, X1, Y1, X2
    670 and Y2 may be nil, if the destination could not be found."
    671   (unless (fboundp 'TeX-master-file)
    672     (error "This function works only with AUCTeX"))
    673   (unless line (setq line (line-number-at-pos)))
    674   (unless column (setq column (current-column)))
    675 
    676   (let* ((pdf (expand-file-name
    677                (with-no-warnings (TeX-master-file "pdf"))))
    678          (sfilename (pdf-sync-synctex-file-name
    679                      (buffer-file-name) pdf)))
    680     (cons pdf
    681 	  (condition-case error
    682 	      (let-alist (pdf-info-synctex-forward-search
    683 			  (or sfilename
    684 			      (buffer-file-name))
    685 			  line column pdf)
    686 		(cons .page .edges))
    687 	    (error
    688 	     (message "%s" (error-message-string error))
    689 	     (list nil nil nil nil nil))))))
    690 
    691 
    692 
    693 ;; * ================================================================== *
    694 ;; * Dealing with synctex files.
    695 ;; * ================================================================== *
    696 
    697 (defun pdf-sync-locate-synctex-file (pdffile)
    698   "Locate the synctex database corresponding to PDFFILE.
    699 
    700 Returns either the absolute path of the database or nil.
    701 
    702 See also `pdf-sync-locate-synctex-file-functions'."
    703   (cl-check-type pdffile string)
    704   (setq pdffile (expand-file-name pdffile))
    705   (or (run-hook-with-args-until-success
    706        'pdf-sync-locate-synctex-file-functions pdffile)
    707       (pdf-sync-locate-synctex-file-default pdffile)))
    708 
    709 (defun pdf-sync-locate-synctex-file-default (pdffile)
    710   "The default function for locating a synctex database for PDFFILE.
    711 
    712 See also `pdf-sync-locate-synctex-file'."
    713   (let ((default-directory
    714           (file-name-directory pdffile))
    715         (basename (file-name-sans-extension
    716                    (file-name-nondirectory pdffile))))
    717     (cl-labels ((file-if-exists-p (file)
    718                   (and (file-exists-p file)
    719                        file)))
    720       (or (file-if-exists-p
    721            (expand-file-name (concat basename ".synctex.gz")))
    722           (file-if-exists-p
    723            (expand-file-name (concat basename ".synctex")))
    724           ;; Some pdftex quote the basename.
    725           (file-if-exists-p
    726            (expand-file-name (concat "\"" basename "\"" ".synctex.gz")))
    727           (file-if-exists-p
    728            (expand-file-name (concat "\"" basename "\"" ".synctex")))))))
    729 
    730 (defun pdf-sync-synctex-file-name (filename pdffile)
    731   "Find SyncTeX filename corresponding to FILENAME in the context of PDFFILE.
    732 
    733 This function consults the synctex.gz database of PDFFILE and
    734 searches for a filename, which is `file-equal-p' to FILENAME.
    735 The first such filename is returned, or nil if none was found."
    736 
    737   (when (file-exists-p filename)
    738     (setq filename (expand-file-name filename))
    739     (let* ((synctex (pdf-sync-locate-synctex-file pdffile))
    740            (basename (file-name-nondirectory filename))
    741            (regexp (format "^ *Input *: *[^:\n]+ *:\\(.*%s\\)$"
    742                            (regexp-quote basename)))
    743            (jka-compr-verbose nil))
    744       (when (and synctex
    745                  (file-readable-p synctex))
    746         (with-current-buffer (find-file-noselect synctex :nowarn)
    747           (unless (or (verify-visited-file-modtime)
    748                       (buffer-modified-p))
    749             (revert-buffer :ignore-auto :noconfirm)
    750             (goto-char (point-min)))
    751           ;; Keep point in front of the found filename. It will
    752           ;; probably be queried for again next time.
    753           (let ((beg (point))
    754                 (end (point-max)))
    755             (catch 'found
    756               (dotimes (_x 2)
    757                 (while (re-search-forward regexp end t)
    758                   (let ((syncname (match-string-no-properties 1)))
    759                     (when (and (file-exists-p syncname)
    760                                (file-equal-p filename syncname))
    761                       (goto-char (line-beginning-position))
    762                       (throw 'found syncname))))
    763                 (setq end beg
    764                       beg (point-min))
    765                 (goto-char beg)))))))))
    766 
    767 (provide 'pdf-sync)
    768 ;;; pdf-sync.el ends here