From 3a3a0783e832e0a40ef4159b59f368a62a6eece0 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Wed, 4 Mar 2026 13:53:01 -0500 Subject: [PATCH] feat(presets): add tectonic preset Adds a tectonic preset for the modern Rust-based LaTeX engine, which auto-downloads packages and requires no TeX installation. Reuses parse_latexmk since tectonic emits the same file:line: message diagnostic format. --- doc/preview.nvim.txt | 1 + lua/preview/presets.lua | 16 +++++++++++++ spec/presets_spec.lua | 51 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/doc/preview.nvim.txt b/doc/preview.nvim.txt index be89cde..a3021e4 100644 --- a/doc/preview.nvim.txt +++ b/doc/preview.nvim.txt @@ -158,6 +158,7 @@ Import them from `preview.presets`: `presets.typst` typst compile → PDF `presets.latex` latexmk -pdf → PDF (with clean support) `presets.pdflatex` pdflatex → PDF (single pass, no latexmk) + `presets.tectonic` tectonic → PDF (Rust-based LaTeX engine) `presets.markdown` pandoc → HTML (standalone, embedded) `presets.github` pandoc → HTML (GitHub-styled, `-f gfm` input) diff --git a/lua/preview/presets.lua b/lua/preview/presets.lua index 88e84e8..1d573e1 100644 --- a/lua/preview/presets.lua +++ b/lua/preview/presets.lua @@ -153,6 +153,22 @@ M.pdflatex = { open = true, } +---@type preview.ProviderConfig +M.tectonic = { + ft = 'tex', + cmd = { 'tectonic' }, + args = function(ctx) + return { ctx.file } + end, + output = function(ctx) + return (ctx.file:gsub('%.tex$', '.pdf')) + end, + error_parser = function(output) + return parse_latexmk(output) + end, + open = true, +} + ---@type preview.ProviderConfig M.markdown = { ft = 'markdown', diff --git a/spec/presets_spec.lua b/spec/presets_spec.lua index 8e9e89b..c4394c5 100644 --- a/spec/presets_spec.lua +++ b/spec/presets_spec.lua @@ -212,6 +212,57 @@ describe('presets', function() end) end) + describe('tectonic', function() + local tex_ctx = { + bufnr = 1, + file = '/tmp/document.tex', + root = '/tmp', + ft = 'tex', + } + + it('has ft', function() + assert.are.equal('tex', presets.tectonic.ft) + end) + + it('has cmd', function() + assert.are.same({ 'tectonic' }, presets.tectonic.cmd) + end) + + it('returns args with file path', function() + assert.are.same({ '/tmp/document.tex' }, presets.tectonic.args(tex_ctx)) + end) + + it('returns pdf output path', function() + assert.are.equal('/tmp/document.pdf', presets.tectonic.output(tex_ctx)) + end) + + it('has open enabled', function() + assert.is_true(presets.tectonic.open) + end) + + it('has no clean command', function() + assert.is_nil(presets.tectonic.clean) + end) + + it('has no reload', function() + assert.is_nil(presets.tectonic.reload) + end) + + it('parses file-line-error format', function() + local output = './document.tex:5: Missing $ inserted.' + local diagnostics = presets.tectonic.error_parser(output, tex_ctx) + assert.are.equal(1, #diagnostics) + assert.are.equal(4, diagnostics[1].lnum) + assert.are.equal(0, diagnostics[1].col) + assert.are.equal('Missing $ inserted.', diagnostics[1].message) + assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) + end) + + it('returns empty table for clean output', function() + assert.are.same({}, presets.tectonic.error_parser('', tex_ctx)) + end) + end) + describe('markdown', function() local md_ctx = { bufnr = 1,