emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
blob e0ef4a30f4b6c8aea25162a1e6a00e138f222577 9436 bytes (raw)
name: contrib/lisp/ox-synctex.el 	 # note: path name is non-authoritative(*)

  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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
 
;;; ox-synctex.el --- Synctex functionality for org LaTeX export

;; Copyright (C) 2013 Aaron Ecay

;; Author: Aaron Ecay <aaronecay@gmail.com>

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This code provides synctex support for org mode export to latex.
;; To activate, execute (ox-synctex-activate).  To deactivate,
;; (ox-synctex-deactivate)

;; TODOs:
;; - support multi-file documents through #+include and friends
;; - do something so that clicks on a minted source code block go to
;;   the .org file and not the .pyg intermediate
;; - ...

;;; Code:

(require 'org-element)
(require 'ox-latex)
(require 'dired) ;; for `dired-touch-program'

;;;; Internal functions and variable

(defvar ox-synctex--concordance nil
  "The concordance resulting from the last export operation.")

(defun ox-synctex--read-concordance (concordance src-line)
  "Get the output line number from CONCORDANCE for input line SRC-LINE."
  ;; TODO: not robust against malformed concordances
  (while (and (caadr concordance)
              (<= (caadr concordance) src-line))
    (setq concordance (cdr concordance)))
  (cdar concordance))

(defun ox-synctex--propertize-buffer ()
  "Put line-number text properties on a buffer.

Each line gets a org-line-num-pre property, which is its line
number in the buffer.  When export operations change the buffer,
the text property will still reflect the original state of affairs."
  (save-restriction
    (widen)
    (while (= 0 (forward-line 1))
      (put-text-property (point) (point-at-eol)
			 'ox-synctex-line-num
			 (line-number-at-pos)))))

(defun ox-synctex--line-number-at-pos (pos)
  "Return the buffer line number at POS, widening if necessary.

This function first looks for text properties set by
`ox-synctex--propertize-buffer' which allow it to return an
accurate line number in a buffer copy modified during export.  It
falls back to the usual method of calculating line numbers if no
text properties are found."
  (or (get-text-property pos 'ox-synctex-line-num)
      (save-excursion
        (widen)
        (line-number-at-pos pos))))

(defun ox-synctex--add-line-to-element (element)
  "Add begin and end line numbers to an element as returned by `org-element'."
  (let* ((plist (cadr element))
	 (beg (plist-get plist :begin))
	 (end (plist-get plist :end)))
    (and beg (plist-put plist :begin-line (ox-synctex--line-number-at-pos beg)))
    (and end (plist-put plist :end-line (ox-synctex--line-number-at-pos end)))
    element))


(defun ox-synctex--propertize-string (data string)
  "Add line number text properties to STRING, based on DATA.

The function works by copying the properties added by
`ox-synctex--add-line-to-element' to the string.  This will allow
the construction of a concordance from the exported string."
  (let ((len (length string)))
    (when (> len 1)
      (put-text-property 0 1 'org-line-num
                         (org-element-property :begin-line data)
                         string)
      (put-text-property (1- len) len 'org-line-num
                         (org-element-property :end-line data)
                         string)))
  string)

(defun ox-synctex--build-concordance ()
  "Build a concordance, based on text properties added by
`ox-synctex--propertize-string' and accumulated in in an export
result buffer.

Has the form ((OUT-LINE . IN-LINE) ...)"
  (save-excursion
    (let ((res '())
          next)
      (goto-char (point-min))
      (while (setq next (next-single-property-change (point) 'org-line-num))
        (goto-char next)
	(let ((ln (get-text-property (point) 'org-line-num)))
	  ;; TODO: `ln' should never be nil, but sometimes it is.  For
	  ;; now, we hack around that with this `when'.
	  (when ln
	    (setq res (cons (cons (line-number-at-pos) ln)
			    res))))
        (forward-char 1))
      (setq res (nreverse res))
      (setq next res)
      (while (cdr next)
        (if (equal (caar next) (caadr next))
            (setcdr next (cddr next))
          (setq next (cdr next))))
      res)))

(defun ox-synctex--patch-synctex (file)
  "Patch the synctex file resulting from the last export
operation, using the information stored in
`ox-synctex--concordance'."
  (let* ((file-base (file-name-nondirectory
		     (replace-regexp-in-string "\\.tex\\'" "." file)))
	 (synctex-file (concat file-base "synctex.gz")))
    (cond
     ((not ox-synctex--concordance)
      (message "ox-synctex: no concordance, not patching."))
     ((not (file-exists-p synctex-file))
      (message "ox-synctex: no synctex file found, not patching."))
     (t
      (message "ox-synctex: patching synctex file")
      (let* ((conc ox-synctex--concordance)
	     (buf (find-file-noselect synctex-file)))
	(with-current-buffer buf
	  (let ((max-index 0)
		the-index extra-path new-index)
	    (goto-char (point-min))
	    (while (re-search-forward "^Input:\\([0-9]+\\):" nil t)
	      (setq max-index (max max-index (string-to-number (match-string 1)))))
	    (setq new-index (number-to-string (1+ max-index)))
	    (goto-char (point-min))
	    (when (re-search-forward (concat "^Input:\\([0-9]+\\):\\(.*\\)"
					     (regexp-quote file-base) "tex$")
				     nil t)
	      (setq the-index (string-to-number (match-string 1)))
	      (setq extra-path (match-string 2))
	      (goto-char (line-end-position))
	      (insert (format "\nInput:%s:%s%sorg" new-index extra-path file-base)))
	    (goto-char (point-min))
	    (while (re-search-forward (format "^[vhxkgr$[()]\\(%s\\),\\([0-9]+\\):"
					      the-index)
				      nil t)
	      (let ((new-line (ox-synctex--read-concordance
			       ox-synctex--concordance
			       (string-to-number (match-string 2)))))
		(when new-line
		  (replace-match new-index nil t nil 1)
		  (replace-match (int-to-string new-line)
				 nil t nil 2))))
	    (save-buffer)))
	(kill-buffer buf))))))

;;;; Hooks and advice

(defun ox-synctex--before-processing-hook (&rest ignore)
  (ox-synctex--propertize-buffer))

(defconst ox-synctex--parsers-to-patch
  (append org-element-greater-elements org-element-all-elements))

;;; Patch all `org-element' parsers to add line number info to their
;;; return values.
(dolist (parser ox-synctex--parsers-to-patch)
  (let ((parser-fn (intern (format "org-element-%s-parser"
				   (symbol-name parser)))))
    (eval `(defadvice ,parser-fn (around ox-synctex)
	     "Advice added by `ox-synctex'."
	     ad-do-it
	     (setq ad-return-value (ox-synctex--add-line-to-element
				    ad-return-value))))))

;;; Patch element->string conversion to carry through the line numbers
;;; added above
(defadvice org-export-transcoder (around ox-synctex)
  ad-do-it
  (when (and ad-return-value
	     (org-export-derived-backend-p
	      (plist-get (ad-get-arg 1) :back-end)
	      'latex))
    (setq ad-return-value
	  `(lambda (data contents &optional info)
	     (ox-synctex--propertize-string
	      data
	      (if info
		  (,ad-return-value data contents info)
		;; The plain text transcoder takes only 2 arguments;
		;; here contents is really info.  I couldn't find a
		;; better way to inspect the arity of elisp
		;; functions...?
		(,ad-return-value data contents)))))))

;;; Patch to build the concordance once we have the export result.  We
;;; need to hack around the fact that the original function strips
;;; text properties from its return value which we need.
(defadvice org-export-as (around ox-synctex)
  (cl-letf (((symbol-function 'org-no-properties)
	     (lambda (s &optional _restricted) s)))
    ad-do-it)
  (when (org-export-derived-backend-p (ad-get-arg 0) 'latex)
    (message "ox-synctex: patching org-export-as return")
    (with-temp-buffer
      (insert ad-return-value)
      (setq ox-synctex--concordance (ox-synctex--build-concordance))))
  (setq ad-return-value (org-no-properties ad-return-value)))

;;; Actually do the patching after compilation
(defadvice org-latex-compile (around ox-synctex)
  (message "ox-synctex: active during latex compile")
  ad-do-it
  (ox-synctex--patch-synctex (ad-get-arg 0))
  ;; Some PDF viewers (eg evince) don't notice changes to the synctex
  ;; file, so we need to poke them to reload the pdf after we've
  ;; finished changing it.
  (call-process dired-touch-program nil nil nil
		(replace-regexp-in-string "\\.tex\\'" "." (ad-get-arg 0)))
  (message "ox-synctex: done, hoorah!"))

;;;; User-facing functions

(defun ox-synctex-activate ()
  (interactive)
  (add-hook 'org-export-before-processing-hook
            #'ox-synctex--before-processing-hook)
  (ad-activate-regexp "ox-synctex"))

(defun ox-synctex-deactivate ()
  (interactive)
  (remove-hook 'org-export-before-processing-hook
	       #'ox-synctex--before-processing-hook)
  (ad-deactivate-regexp "ox-synctex"))

(provide 'ox-synctex)

;; Local Variables:
;; lexical-binding: t
;; End:

;;; ox-synctex.el ends here

debug log:

solving e0ef4a3 ...
found e0ef4a3 in https://list.orgmode.org/orgmode/87zjprbbhl.fsf@gmail.com/

applying [1/1] https://list.orgmode.org/orgmode/87zjprbbhl.fsf@gmail.com/
diff --git a/contrib/lisp/ox-synctex.el b/contrib/lisp/ox-synctex.el
new file mode 100644
index 0000000..e0ef4a3

Checking patch contrib/lisp/ox-synctex.el...
Applied patch contrib/lisp/ox-synctex.el cleanly.

index at:
100644 e0ef4a30f4b6c8aea25162a1e6a00e138f222577	contrib/lisp/ox-synctex.el

(*) Git path names are given by the tree(s) the blob belongs to.
    Blobs themselves have no identifier aside from the hash of its contents.^

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).