From 56d110a74ed5e514ca1f31e0bc05ee322ce89fa2 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:07:00 -0500 Subject: [PATCH] fix(presets): correct error parsers for real compiler output Problem: all three built-in error parsers were broken against real compiler output. Typst set source to the relative file path, overriding the provider name. LaTeX errors go to stdout but the parser only received stderr. Pandoc's pattern matched "Error at" but not the real "Error parsing YAML metadata at" format, and single-line parsing missed multiline messages. Solution: pass combined stdout+stderr to error_parser so LaTeX stdout errors are visible. Remove source = file from the Typst parser so diagnostic.lua defaults it to the provider name. Rewrite the Pandoc parser with line-based lookahead: match (line N, column N) regardless of prefix text, skip YAML parse exception lines when looking ahead for the human-readable message. Rename stderr param to output throughout diagnostic.lua, presets.lua, and init.lua annotations. --- lua/preview/compiler.lua | 3 +- lua/preview/diagnostic.lua | 8 ++-- lua/preview/init.lua | 2 +- lua/preview/presets.lua | 93 ++++++++++++++++++++------------------ spec/presets_spec.lua | 74 ++++++++++++++++++------------ 5 files changed, 101 insertions(+), 79 deletions(-) diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 6e3f5cf..a247f2e 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -106,7 +106,8 @@ function M.compile(bufnr, name, provider, ctx) else log.dbg('compilation failed for buffer %d (exit code %d)', bufnr, result.code) if provider.error_parser then - diagnostic.set(bufnr, name, provider.error_parser, result.stderr or '', ctx) + local output = (result.stdout or '') .. (result.stderr or '') + diagnostic.set(bufnr, name, provider.error_parser, output, ctx) end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileFailed', diff --git a/lua/preview/diagnostic.lua b/lua/preview/diagnostic.lua index d81ec64..abd4105 100644 --- a/lua/preview/diagnostic.lua +++ b/lua/preview/diagnostic.lua @@ -12,11 +12,11 @@ end ---@param bufnr integer ---@param name string ----@param error_parser fun(stderr: string, ctx: preview.Context): preview.Diagnostic[] ----@param stderr string +---@param error_parser fun(output: string, ctx: preview.Context): preview.Diagnostic[] +---@param output string ---@param ctx preview.Context -function M.set(bufnr, name, error_parser, stderr, ctx) - local ok, diagnostics = pcall(error_parser, stderr, ctx) +function M.set(bufnr, name, error_parser, output, ctx) + local ok, diagnostics = pcall(error_parser, output, ctx) if not ok then log.dbg('error_parser for "%s" failed: %s', name, diagnostics) return diff --git a/lua/preview/init.lua b/lua/preview/init.lua index f6d0006..d122e61 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -5,7 +5,7 @@ ---@field cwd? string|fun(ctx: preview.Context): string ---@field env? table ---@field output? string|fun(ctx: preview.Context): string ----@field error_parser? fun(stderr: string, ctx: preview.Context): preview.Diagnostic[] +---@field error_parser? fun(output: string, ctx: preview.Context): preview.Diagnostic[] ---@field clean? string[]|fun(ctx: preview.Context): string[] ---@field open? boolean|string[] diff --git a/lua/preview/presets.lua b/lua/preview/presets.lua index 196114b..8b9faab 100644 --- a/lua/preview/presets.lua +++ b/lua/preview/presets.lua @@ -1,11 +1,11 @@ local M = {} ----@param stderr string +---@param output string ---@return preview.Diagnostic[] -local function parse_typst(stderr) +local function parse_typst(output) local diagnostics = {} - for line in stderr:gmatch('[^\r\n]+') do - local file, lnum, col, severity, msg = line:match('^(.+):(%d+):(%d+): (%w+): (.+)$') + for line in output:gmatch('[^\r\n]+') do + local _, lnum, col, severity, msg = line:match('^(.+):(%d+):(%d+): (%w+): (.+)$') if lnum then local sev = vim.diagnostic.severity.ERROR if severity == 'warning' then @@ -16,18 +16,17 @@ local function parse_typst(stderr) col = tonumber(col) - 1, message = msg, severity = sev, - source = file, }) end end return diagnostics end ----@param stderr string +---@param output string ---@return preview.Diagnostic[] -local function parse_latexmk(stderr) +local function parse_latexmk(output) local diagnostics = {} - for line in stderr:gmatch('[^\r\n]+') do + for line in output:gmatch('[^\r\n]+') do local _, lnum, msg = line:match('^%.?/?(.+%.tex):(%d+): (.+)$') if lnum then table.insert(diagnostics, { @@ -51,41 +50,45 @@ local function parse_latexmk(stderr) return diagnostics end ----@param stderr string +---@param output string ---@return preview.Diagnostic[] -local function parse_pandoc(stderr) +local function parse_pandoc(output) local diagnostics = {} - for line in stderr:gmatch('[^\r\n]+') do - local lnum, col, msg = line:match('Error at .+ %(line (%d+), column (%d+)%): (.+)$') + local lines = vim.split(output, '\n') + local i = 1 + while i <= #lines do + local line = lines[i] + local lnum, col, msg = line:match('%(line (%d+), column (%d+)%):%s*(.*)$') if lnum then - table.insert(diagnostics, { - lnum = tonumber(lnum) - 1, - col = tonumber(col) - 1, - message = msg, - severity = vim.diagnostic.severity.ERROR, - }) - else - local ylnum, ycol, ymsg = - line:match('YAML parse exception at line (%d+), column (%d+)[,:]%s*(.+)$') - if ylnum then - table.insert(diagnostics, { - lnum = tonumber(ylnum) - 1, - col = tonumber(ycol) - 1, - message = ymsg, - severity = vim.diagnostic.severity.ERROR, - }) - else - local errmsg = line:match('^pandoc: (.+)$') - if errmsg and not errmsg:match('^Error at') then - table.insert(diagnostics, { - lnum = 0, - col = 0, - message = errmsg, - severity = vim.diagnostic.severity.ERROR, - }) + if msg == '' then + for j = i + 1, math.min(i + 2, #lines) do + local next_line = lines[j]:match('^%s*(.+)$') + if next_line and not next_line:match('^YAML parse exception') then + msg = next_line + break + end end end + if msg ~= '' then + table.insert(diagnostics, { + lnum = tonumber(lnum) - 1, + col = tonumber(col) - 1, + message = msg, + severity = vim.diagnostic.severity.ERROR, + }) + end + else + local errmsg = line:match('^pandoc: (.+)$') + if errmsg then + table.insert(diagnostics, { + lnum = 0, + col = 0, + message = errmsg, + severity = vim.diagnostic.severity.ERROR, + }) + end end + i = i + 1 end return diagnostics end @@ -100,8 +103,8 @@ M.typst = { output = function(ctx) return (ctx.file:gsub('%.typ$', '.pdf')) end, - error_parser = function(stderr) - return parse_typst(stderr) + error_parser = function(output) + return parse_typst(output) end, open = true, } @@ -121,8 +124,8 @@ M.latex = { output = function(ctx) return (ctx.file:gsub('%.tex$', '.pdf')) end, - error_parser = function(stderr) - return parse_latexmk(stderr) + error_parser = function(output) + return parse_latexmk(output) end, clean = function(ctx) return { 'latexmk', '-c', ctx.file } @@ -141,8 +144,8 @@ M.markdown = { output = function(ctx) return (ctx.file:gsub('%.md$', '.html')) end, - error_parser = function(stderr) - return parse_pandoc(stderr) + error_parser = function(output) + return parse_pandoc(output) end, clean = function(ctx) return { 'rm', '-f', (ctx.file:gsub('%.md$', '.html')) } @@ -171,8 +174,8 @@ M.github = { output = function(ctx) return (ctx.file:gsub('%.md$', '.html')) end, - error_parser = function(stderr) - return parse_pandoc(stderr) + error_parser = function(output) + return parse_pandoc(output) end, clean = function(ctx) return { 'rm', '-f', (ctx.file:gsub('%.md$', '.html')) } diff --git a/spec/presets_spec.lua b/spec/presets_spec.lua index 33f3a91..904a4f4 100644 --- a/spec/presets_spec.lua +++ b/spec/presets_spec.lua @@ -49,7 +49,7 @@ describe('presets', function() assert.are.equal(22, diagnostics[1].col) assert.are.equal('unexpected token', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) - assert.are.equal('main.typ', diagnostics[1].source) + assert.is_nil(diagnostics[1].source) assert.are.equal(11, diagnostics[2].lnum) assert.are.equal(0, diagnostics[2].col) assert.are.equal('unused variable', diagnostics[2].message) @@ -105,14 +105,14 @@ describe('presets', function() assert.is_true(presets.latex.open) end) - it('parses file-line-error format from stderr', function() - local stderr = table.concat({ + it('parses file-line-error format from output', function() + local output = table.concat({ './document.tex:10: Undefined control sequence.', 'l.10 \\badcommand', 'Collected error summary (may duplicate other messages):', " pdflatex: Command for 'pdflatex' gave return code 256", }, '\n') - local diagnostics = presets.latex.error_parser(stderr, tex_ctx) + local diagnostics = presets.latex.error_parser(output, tex_ctx) assert.is_table(diagnostics) assert.is_true(#diagnostics > 0) assert.are.equal(9, diagnostics[1].lnum) @@ -122,12 +122,12 @@ describe('presets', function() end) it('parses collected error summary', function() - local stderr = table.concat({ + local output = table.concat({ 'Latexmk: Errors, so I did not complete making targets', 'Collected error summary (may duplicate other messages):', " pdflatex: Command for 'pdflatex' gave return code 256", }, '\n') - local diagnostics = presets.latex.error_parser(stderr, tex_ctx) + local diagnostics = presets.latex.error_parser(output, tex_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) @@ -185,9 +185,24 @@ describe('presets', function() assert.is_true(presets.markdown.open) end) - it('parses pandoc parse errors from stderr', function() - local stderr = 'Error at "source" (line 75, column 1): unexpected end of input' - local diagnostics = presets.markdown.error_parser(stderr, md_ctx) + it('parses YAML metadata errors with multiline message', function() + local output = table.concat({ + 'Error parsing YAML metadata at "/tmp/test.md" (line 1, column 1):', + 'YAML parse exception at line 1, column 9:', + 'mapping values are not allowed in this context', + }, '\n') + local diagnostics = presets.markdown.error_parser(output, md_ctx) + assert.is_table(diagnostics) + assert.are.equal(1, #diagnostics) + assert.are.equal(0, diagnostics[1].lnum) + assert.are.equal(0, diagnostics[1].col) + assert.are.equal('mapping values are not allowed in this context', diagnostics[1].message) + assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) + end) + + it('parses Error at format', function() + local output = 'Error at "source" (line 75, column 1): unexpected end of input' + local diagnostics = presets.markdown.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(74, diagnostics[1].lnum) @@ -196,20 +211,9 @@ describe('presets', function() assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) - it('parses YAML parse exceptions from stderr', function() - local stderr = - 'YAML parse exception at line 3, column 2, while scanning a block scalar: did not find expected comment or line break' - local diagnostics = presets.markdown.error_parser(stderr, md_ctx) - assert.is_table(diagnostics) - assert.are.equal(1, #diagnostics) - assert.are.equal(2, diagnostics[1].lnum) - assert.are.equal(1, diagnostics[1].col) - assert.is_string(diagnostics[1].message) - end) - - it('parses generic pandoc errors from stderr', function() - local stderr = 'pandoc: Could not find data file templates/default.html5' - local diagnostics = presets.markdown.error_parser(stderr, md_ctx) + it('parses generic pandoc errors', function() + local output = 'pandoc: Could not find data file templates/default.html5' + local diagnostics = presets.markdown.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) @@ -217,7 +221,7 @@ describe('presets', function() assert.are.equal('Could not find data file templates/default.html5', diagnostics[1].message) end) - it('returns empty table for clean stderr', function() + it('returns empty table for clean output', function() local diagnostics = presets.markdown.error_parser('', md_ctx) assert.are.same({}, diagnostics) end) @@ -284,9 +288,23 @@ describe('presets', function() assert.is_true(presets.github.open) end) - it('parses pandoc parse errors from stderr', function() - local stderr = 'Error at "document.md" (line 12, column 5): unexpected "}" expecting letter' - local diagnostics = presets.github.error_parser(stderr, md_ctx) + it('parses YAML metadata errors with multiline message', function() + local output = table.concat({ + 'Error parsing YAML metadata at "/tmp/test.md" (line 1, column 1):', + 'YAML parse exception at line 1, column 9:', + 'mapping values are not allowed in this context', + }, '\n') + local diagnostics = presets.github.error_parser(output, md_ctx) + assert.is_table(diagnostics) + assert.are.equal(1, #diagnostics) + assert.are.equal(0, diagnostics[1].lnum) + assert.are.equal(0, diagnostics[1].col) + assert.are.equal('mapping values are not allowed in this context', diagnostics[1].message) + end) + + it('parses Error at format', function() + local output = 'Error at "document.md" (line 12, column 5): unexpected "}" expecting letter' + local diagnostics = presets.github.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(11, diagnostics[1].lnum) @@ -295,7 +313,7 @@ describe('presets', function() assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) - it('returns empty table for clean stderr', function() + it('returns empty table for clean output', function() local diagnostics = presets.github.error_parser('', md_ctx) assert.are.same({}, diagnostics) end)