Skip to main content

retex_engine/
synctex.rs

1//! SyncTeX output (`\synctex`, `-synctex`): the `<job>.synctex.gz` file
2//! with which editors and viewers map source lines to positions in the
3//! PDF and back.
4//!
5//! The format is described in `man 5 synctex`. What LuaTeX records, and
6//! where, comes from the files it writes (observed):
7//!
8//! * The file starts with a preamble (`SyncTeX Version:1`, the `Input:`
9//!   lines of the files read so far, `Output:pdf`, `Magnification:`,
10//!   `Unit:1` and zero offsets), then `Content:`. Each `\input` file gets
11//!   the next tag; its `Input:` line names it with the current directory
12//!   in front of a relative name (`/dir/./doc.tex`) and is written when
13//!   the file is opened (in the preamble if no page has been shipped
14//!   yet).
15//! * Each page is a sheet `{n` … `}n` holding the records of its boxes.
16//!   Positions are in scaled points from the top left corner of the page,
17//!   with the 1in origin (`\pdfhorigin`, `\hoffset`, …) included. A box
18//!   resource is written inside the sheet of the page (or form) that first
19//!   uses it, after its boxes, as a sheet numbered `{n+1` … `}n` (`{1` …
20//!   `}0` for an immediate one), with its bottom left corner at
21//!   (0, `\pageheight`).
22//! * In vertical lists only boxes are recorded: `[` for a vbox at its top
23//!   left corner, `(` for an hbox at its reference point, `v`/`h` for an
24//!   empty one at the bottom of the space it takes, all with their width,
25//!   height and depth. In horizontal lists: boxes (empty ones at their
26//!   start), glue (`g`, where it ends; rule leaders too, box leaders give
27//!   their boxes), rules (`r`, where they end, with their dimensions as
28//!   stored, running ones as −2³⁰), math nodes (`$`, where they are) and
29//!   some kerns (`k`, see [`Synctex::kern`]). A run of glyphs gives an `x`
30//!   record where it ends, with the tag and line of the last record or
31//!   kern: glyphs have no fields. Marks, inserts, adjusts, boundaries and
32//!   local paragraph nodes do not end a run; other nodes do.
33//! * Nodes have the tag and line of the input when they were made (boxes
34//!   when packaged, rules before their dimensions are scanned). Lua output
35//!   and `\scantokens` text have tag 0 and count their own lines. Glue,
36//!   kerns, rules and math nodes with tag or line 0 are not recorded and
37//!   change nothing.
38//! * `!n` records mark byte offsets (from the previous one); `Count:`
39//!   gives the number of records, `x` records not included.
40//!
41//! The fields of the nodes are kept in [`crate::nodes::SyncFields`] while
42//! SyncTeX is on.
43
44use std::io::Write;
45
46use crate::Engine;
47use crate::input::Level;
48use crate::nodes::NodeRef;
49use crate::primitives_gen::assign_int as ip;
50
51/// Tag and line.
52pub type Fields = (u32, i32);
53
54/// Where the SyncTeX data goes while the job runs.
55enum Sink {
56    Plain(Vec<u8>),
57    /// The compressor and the bytes not given to it yet (it is fed large
58    /// chunks: one write per record is slow).
59    Gz(flate2::write::GzEncoder<Vec<u8>>, Vec<u8>),
60}
61
62/// The file being written.
63struct Out {
64    sink: Sink,
65    /// Bytes written (uncompressed).
66    size: u64,
67    /// Offset of the last `!` record.
68    last_bang: u64,
69    /// Records counted for `Count:`.
70    count: u64,
71    /// Scratch line.
72    line: Vec<u8>,
73}
74
75impl Out {
76    fn emit(&mut self, counted: bool) {
77        self.size += self.line.len() as u64;
78        match &mut self.sink {
79            Sink::Plain(v) => v.extend_from_slice(&self.line),
80            Sink::Gz(g, pending) => {
81                pending.extend_from_slice(&self.line);
82                if pending.len() >= 1 << 16 {
83                    let _ = g.write_all(pending);
84                    pending.clear();
85                }
86            }
87        }
88        self.line.clear();
89        if counted {
90            self.count += 1;
91        }
92    }
93
94    fn bang(&mut self) {
95        let off = self.size - self.last_bang;
96        self.last_bang = self.size;
97        let _ = writeln!(self.line, "!{off}");
98        self.emit(true);
99    }
100}
101
102/// The SyncTeX state of a run.
103#[derive(Default)]
104pub struct Synctex {
105    /// The `-synctex` value given on the command line: 0 forbids SyncTeX,
106    /// a negative value asks for an uncompressed file.
107    pub option: Option<i32>,
108    /// The `Input:` names of the files read, by tag minus one.
109    pub files: Vec<Vec<u8>>,
110    /// The current directory, for the names of relative files.
111    cwd: Option<Vec<u8>>,
112    out: Option<Out>,
113    /// The directory the file goes to (the PDF's), if not the output
114    /// directory.
115    pub dir: Option<std::path::PathBuf>,
116    /// Whether a page is being shipped out (forms then get the next page
117    /// number).
118    in_page: bool,
119    // ----- the traversal (see `kern`) -----
120    /// Offset of the sheet's coordinates.
121    origin: (i32, i32),
122    /// Tag and line of the last record (the context of `x` records).
123    ctx: Fields,
124    /// The position recorded last, where kern records go.
125    last: (i32, i32),
126    /// Whether something was recorded in the current box since it opened.
127    seen: bool,
128    /// A kern not recorded yet, and its width.
129    pending: Option<(Fields, i32)>,
130    /// Whether glyphs were output since the last record.
131    run: bool,
132}
133
134impl Synctex {
135    /// The name of an input file in `Input:` lines.
136    fn input_name(&mut self, path: &std::path::Path) -> Vec<u8> {
137        let name = path.to_string_lossy().into_owned().into_bytes();
138        if path.is_absolute() {
139            return name;
140        }
141        let cwd = self.cwd.get_or_insert_with(|| {
142            std::env::current_dir()
143                .map(|d| d.to_string_lossy().into_owned().into_bytes())
144                .unwrap_or_default()
145        });
146        let mut full = cwd.clone();
147        full.push(b'/');
148        full.extend_from_slice(&name);
149        full
150    }
151
152    /// Registers an input file; returns its tag.
153    fn add_input(&mut self, path: &std::path::Path) -> u32 {
154        let name = self.input_name(path);
155        let tag = self.files.len() as u32 + 1;
156        if let Some(out) = self.out.as_mut() {
157            out.line
158                .extend_from_slice(format!("Input:{tag}:").as_bytes());
159            out.line.extend_from_slice(&name);
160            out.line.push(b'\n');
161            out.emit(false);
162        }
163        self.files.push(name);
164        tag
165    }
166
167    /// Opens the file (writes the preamble) if needed.
168    fn open(&mut self, pdf: bool, mag: i32) -> &mut Out {
169        let compressed = self.option.is_none_or(|n| n > 0);
170        let files = &self.files;
171        self.out.get_or_insert_with(|| {
172            let sink = if compressed {
173                Sink::Gz(
174                    flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()),
175                    Vec::new(),
176                )
177            } else {
178                Sink::Plain(Vec::new())
179            };
180            let mut out = Out {
181                sink,
182                size: 0,
183                last_bang: 0,
184                count: 0,
185                line: Vec::new(),
186            };
187            out.line.extend_from_slice(b"SyncTeX Version:1\n");
188            for (i, f) in files.iter().enumerate() {
189                out.line
190                    .extend_from_slice(format!("Input:{}:", i + 1).as_bytes());
191                out.line.extend_from_slice(f);
192                out.line.push(b'\n');
193            }
194            let _ = write!(
195                out.line,
196                "Output:{}\nMagnification:{mag}\nUnit:1\nX Offset:0\nY Offset:0\nContent:\n",
197                if pdf { "pdf" } else { "dvi" }
198            );
199            out.emit(false);
200            out
201        })
202    }
203
204    /// Starts sheet `n` with coordinates offset by `origin`.
205    fn begin_sheet(&mut self, n: u32, origin: (i32, i32), pdf: bool, mag: i32) {
206        let out = self.open(pdf, mag);
207        out.bang();
208        let _ = writeln!(out.line, "{{{n}");
209        out.emit(true);
210        self.origin = origin;
211        self.ctx = (0, 0);
212        self.last = (0, 0);
213        self.seen = false;
214        self.pending = None;
215        self.run = false;
216    }
217
218    /// Ends sheet `n`.
219    fn end_sheet(&mut self, n: u32) {
220        if let Some(out) = self.out.as_mut() {
221            out.bang();
222            let _ = writeln!(out.line, "}}{n}");
223            out.emit(true);
224        }
225    }
226
227    fn out(&mut self) -> &mut Out {
228        self.out.as_mut().expect("a sheet is open")
229    }
230
231    /// Writes a record `<c>tag,line:h,v` and the rest given.
232    fn record(&mut self, c: u8, f: Fields, h: i32, v: i32, rest: &[i32]) {
233        let (oh, ov) = self.origin;
234        let out = self.out();
235        out.line.push(c);
236        let _ = write!(out.line, "{},{}:{},{}", f.0, f.1, h + oh, v + ov);
237        for (i, x) in rest.iter().enumerate() {
238            out.line.push(if i == 0 { b':' } else { b',' });
239            let _ = write!(out.line, "{x}");
240        }
241        out.line.push(b'\n');
242        out.emit(c != b'x');
243    }
244
245    /// Writes the pending kern, unless the next record has the same fields
246    /// `next` (`None`: always).
247    fn flush_kern(&mut self, next: Option<Fields>) {
248        if let Some((f, w)) = self.pending.take()
249            && Some(f) != next
250        {
251            let (h, v) = self.last;
252            self.record(b'k', f, h, v, &[w]);
253        }
254    }
255
256    /// Ends a run of glyphs at `h`: an `x` record.
257    #[inline]
258    pub fn end_run(&mut self, h: i32, v: i32) {
259        if self.run {
260            self.run = false;
261            let f = self.ctx;
262            self.record(b'x', f, h, v, &[]);
263        }
264    }
265
266    /// A glyph.
267    #[inline]
268    pub fn glyph(&mut self) {
269        self.run = true;
270    }
271
272    /// A box with contents opens at (`h`, `v`): its reference point for
273    /// an hbox, its top left corner for a vbox.
274    pub fn box_open(&mut self, hlist: bool, f: Fields, h: i32, v: i32, size: [i32; 3]) {
275        self.record(if hlist { b'(' } else { b'[' }, f, h, v, &size);
276        self.ctx = f;
277        self.last = (h, v);
278        self.seen = false;
279        self.pending = None;
280    }
281
282    /// The box opened last closes; (`h`, `v`) is where its contents end:
283    /// the end of the baseline of an hbox, the bottom left corner of a
284    /// vbox.
285    pub fn box_close(&mut self, hlist: bool, f: Fields, h: i32, v: i32) {
286        let out = self.out();
287        out.line.push(if hlist { b')' } else { b']' });
288        out.line.push(b'\n');
289        out.emit(true);
290        self.ctx = f;
291        self.last = (h, v);
292        self.seen = true;
293        self.pending = None;
294    }
295
296    /// An empty box.
297    pub fn void_box(&mut self, hlist: bool, f: Fields, h: i32, v: i32, size: [i32; 3]) {
298        self.flush_kern(None);
299        self.record(if hlist { b'h' } else { b'v' }, f, h, v, &size);
300        self.ctx = f;
301        self.last = (h, v);
302        self.seen = true;
303    }
304
305    /// Glue ending at `h`.
306    pub fn glue(&mut self, f: Fields, h: i32, v: i32) {
307        if f.0 == 0 || f.1 == 0 {
308            return;
309        }
310        self.pending = None;
311        self.record(b'g', f, h, v, &[]);
312        self.ctx = f;
313        self.last = (h, v);
314        self.seen = true;
315    }
316
317    /// A rule ending at `h`, with its dimensions as stored.
318    pub fn rule(&mut self, f: Fields, h: i32, v: i32, size: [i32; 3]) {
319        if f.0 == 0 || f.1 == 0 {
320            return;
321        }
322        self.pending = None;
323        self.record(b'r', f, h, v, &size);
324        self.ctx = f;
325        self.last = (h, v);
326        self.seen = true;
327    }
328
329    /// A math node at `h`.
330    pub fn math(&mut self, f: Fields, h: i32, v: i32) {
331        if f.0 == 0 || f.1 == 0 {
332            return;
333        }
334        self.flush_kern(Some(f));
335        self.record(b'$', f, h, v, &[]);
336        self.ctx = f;
337        self.last = (h, v);
338        self.seen = true;
339    }
340
341    /// A kern of width `w`.
342    ///
343    /// Observed: kerns without fields (those TeX makes; copies get the
344    /// current ones) are not recorded. Another kern is recorded, at the
345    /// position recorded last (not where the kern is), if something was
346    /// recorded in its box before and its fields differ from those of the
347    /// last record; it is then preceded by the last kern not recorded, if
348    /// no other record came in between. A kern not recorded waits: a
349    /// following empty box, or math node with other fields, writes it too;
350    /// glue, rules and boxes drop it. Either way, the kern's fields become
351    /// the context of the following `x` records.
352    pub fn kern(&mut self, f: Fields, w: i32) {
353        if f.0 == 0 || f.1 == 0 {
354            return;
355        }
356        if self.seen && f != self.ctx {
357            self.flush_kern(Some(f));
358            let (h, v) = self.last;
359            self.record(b'k', f, h, v, &[w]);
360        } else {
361            self.pending = Some((f, w));
362        }
363        self.ctx = f;
364        self.seen = true;
365    }
366
367    /// Writes the postamble and returns the file's content.
368    fn finish(&mut self) -> Option<(Vec<u8>, bool)> {
369        let mut out = self.out.take()?;
370        out.bang();
371        let _ = write!(out.line, "Postamble:\nCount:{}\n", out.count);
372        out.emit(false);
373        out.bang();
374        out.line.extend_from_slice(b"Post scriptum:\n");
375        out.emit(false);
376        match out.sink {
377            Sink::Plain(v) => Some((v, false)),
378            Sink::Gz(mut g, pending) => {
379                let _ = g.write_all(&pending);
380                g.finish().ok().map(|v| (v, true))
381            }
382        }
383    }
384}
385
386impl Engine {
387    /// Whether SyncTeX may be used in this run (not `-synctex=0`).
388    fn synctex_allowed(&self) -> bool {
389        self.synctex.option != Some(0)
390    }
391
392    /// Called when the job starts and when `\synctex` changes: starts
393    /// keeping the fields of nodes once `\synctex` is nonzero.
394    pub fn synctex_changed(&mut self) {
395        if self.eqtb.int(ip::SYNCTEX) != 0 && self.synctex_allowed() && self.nodes.sync.is_none() {
396            self.nodes.enable_sync();
397            self.sync_input_changed();
398        }
399    }
400
401    /// Updates the current tag and line of new nodes after a change of
402    /// the input (a line read, a level pushed or popped).
403    #[inline]
404    pub(crate) fn sync_input_changed(&mut self) {
405        if let Some(s) = self.nodes.sync.as_deref_mut() {
406            s.cur = self
407                .input
408                .stack
409                .iter()
410                .rev()
411                .find_map(|l| match l {
412                    Level::Text(t) => Some(t.sync),
413                    Level::Tokens(_) => None,
414                })
415                .unwrap_or((0, 0));
416        }
417    }
418
419    /// Gives the file just opened by `\input` (the current text level) its
420    /// tag.
421    pub(crate) fn synctex_input_opened(&mut self, path: &std::path::Path) {
422        let tag = self.synctex.add_input(path);
423        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
424            t.sync = (tag, 0);
425        }
426    }
427
428    /// Whether the sheet about to be shipped out is recorded.
429    pub(crate) fn synctex_recording(&self) -> bool {
430        self.nodes.sync.is_some() && self.eqtb.int(ip::SYNCTEX) != 0
431    }
432
433    /// Starts recording a page, with the page's origin.
434    pub(crate) fn synctex_begin_page(&mut self, origin: (i32, i32)) {
435        let pdf = self.eqtb.int(ip::OUTPUTMODE) > 0;
436        let mag = self.eqtb.int(ip::MAG);
437        self.synctex
438            .begin_sheet(self.total_pages + 1, origin, pdf, mag);
439        self.synctex.in_page = true;
440    }
441
442    /// Ends the page.
443    pub(crate) fn synctex_end_page(&mut self) {
444        self.synctex.in_page = false;
445        self.synctex.end_sheet(self.total_pages + 1);
446    }
447
448    /// Starts recording a box resource of height plus depth `total`.
449    pub(crate) fn synctex_begin_form(&mut self, total: i32) {
450        let pdf = self.eqtb.int(ip::OUTPUTMODE) > 0;
451        let mag = self.eqtb.int(ip::MAG);
452        let n = self.total_pages + 1 + u32::from(self.synctex.in_page);
453        let page_height = self
454            .eqtb
455            .dimen(crate::primitives_gen::assign_dimen::PAGEHEIGHT);
456        self.synctex
457            .begin_sheet(n, (0, page_height - total), pdf, mag);
458    }
459
460    /// Ends a box resource.
461    pub(crate) fn synctex_end_form(&mut self) {
462        let n = self.total_pages + u32::from(self.synctex.in_page);
463        self.synctex.end_sheet(n);
464    }
465
466    /// The SyncTeX fields of node `p`.
467    #[inline]
468    pub(crate) fn sync_of(&self, p: NodeRef) -> Fields {
469        self.nodes.sync_fields(p)
470    }
471
472    /// Writes the SyncTeX file at the end of the job, next to the PDF, and
473    /// removes a stale one (as LuaTeX does); prints `SyncTeX written on
474    /// NAME.` on the terminal.
475    pub(crate) fn finish_synctex(&mut self) {
476        let job =
477            String::from_utf8_lossy(self.job_name.as_deref().unwrap_or(b"texput")).into_owned();
478        let dir = self
479            .synctex
480            .dir
481            .clone()
482            .or_else(|| self.files.output_dir.clone())
483            .unwrap_or_default();
484        let gz = dir.join(format!("{job}.synctex.gz"));
485        let plain = dir.join(format!("{job}.synctex"));
486        let result = if self.total_pages > 0 {
487            self.synctex.finish()
488        } else {
489            None
490        };
491        let Some((data, compressed)) = result else {
492            let _ = std::fs::remove_file(&gz);
493            let _ = std::fs::remove_file(&plain);
494            return;
495        };
496        let (path, other) = if compressed { (gz, plain) } else { (plain, gz) };
497        let _ = std::fs::remove_file(&other);
498        if std::fs::write(&path, &data).is_err() {
499            return;
500        }
501        let name = path
502            .file_name()
503            .map(|n| n.to_string_lossy().into_owned())
504            .unwrap_or_default();
505        let sel = self.printer.selector;
506        self.printer.selector = crate::print::Selector::TermOnly;
507        self.printer.print_nl("SyncTeX written on ");
508        self.printer.print_bytes(name.as_bytes());
509        self.printer.print_char('.' as u32);
510        self.printer.selector = sel;
511    }
512}