diff --git a/lua/preview/commands.lua b/lua/preview/commands.lua index ad63c97..f82c726 100644 --- a/lua/preview/commands.lua +++ b/lua/preview/commands.lua @@ -27,9 +27,9 @@ local function dispatch(args) if s.watching then table.insert(parts, 'watching') end - vim.notify('[preview.nvim] ' .. table.concat(parts, ', '), vim.log.levels.INFO) + vim.notify('[preview.nvim]: ' .. table.concat(parts, ', '), vim.log.levels.INFO) else - vim.notify('[preview.nvim] unknown subcommand: ' .. subcmd, vim.log.levels.ERROR) + vim.notify('[preview.nvim]: unknown subcommand: ' .. subcmd, vim.log.levels.ERROR) end end diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 6e3f5cf..0643ccd 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -86,9 +86,18 @@ function M.compile(bufnr, name, provider, ctx) return end + local errors_mode = provider.errors + if errors_mode == nil then + errors_mode = 'diagnostic' + end + if result.code == 0 then log.dbg('compilation succeeded for buffer %d', bufnr) - diagnostic.clear(bufnr) + if errors_mode == 'diagnostic' then + diagnostic.clear(bufnr) + elseif errors_mode == 'quickfix' then + vim.fn.setqflist({}, 'r') + end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileSuccess', data = { bufnr = bufnr, provider = name, output = output_file }, @@ -105,8 +114,27 @@ function M.compile(bufnr, name, provider, ctx) end 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) + if provider.error_parser and errors_mode then + local output = (result.stdout or '') .. (result.stderr or '') + if errors_mode == 'diagnostic' then + diagnostic.set(bufnr, name, provider.error_parser, output, ctx) + elseif errors_mode == 'quickfix' then + local ok, diagnostics = pcall(provider.error_parser, output, ctx) + if ok and diagnostics and #diagnostics > 0 then + local items = {} + for _, d in ipairs(diagnostics) do + table.insert(items, { + bufnr = bufnr, + lnum = d.lnum + 1, + col = d.col + 1, + text = d.message, + type = d.severity == vim.diagnostic.severity.WARN and 'W' or 'E', + }) + end + vim.fn.setqflist(items, 'r') + vim.cmd('copen') + end + end end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileFailed', @@ -177,6 +205,7 @@ end function M.toggle(bufnr, name, provider, ctx_builder) if watching[bufnr] then M.unwatch(bufnr) + vim.notify('[preview.nvim]: watching stopped', vim.log.levels.INFO) return end @@ -201,6 +230,7 @@ function M.toggle(bufnr, name, provider, ctx_builder) watching[bufnr] = au_id log.dbg('watching buffer %d with provider "%s"', bufnr, name) + vim.notify('[preview.nvim]: watching with "' .. name .. '"', vim.log.levels.INFO) vim.api.nvim_create_autocmd('BufWipeout', { buffer = bufnr, 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..4a44a33 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -5,7 +5,8 @@ ---@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 errors? false|'diagnostic'|'quickfix' ---@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/compiler_spec.lua b/spec/compiler_spec.lua index 2046ef2..2189347 100644 --- a/spec/compiler_spec.lua +++ b/spec/compiler_spec.lua @@ -132,6 +132,108 @@ describe('compiler', function() end) end) + describe('errors mode', function() + it('errors = false suppresses error parser', function() + local bufnr = helpers.create_buffer({ 'hello' }, 'text') + vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_errors_false.txt') + vim.bo[bufnr].modified = false + + local parser_called = false + local provider = { + cmd = { 'false' }, + errors = false, + error_parser = function() + parser_called = true + return {} + end, + } + local ctx = { + bufnr = bufnr, + file = '/tmp/preview_test_errors_false.txt', + root = '/tmp', + ft = 'text', + } + + compiler.compile(bufnr, 'falsecmd', provider, ctx) + + vim.wait(2000, function() + return compiler._test.active[bufnr] == nil + end, 50) + + assert.is_false(parser_called) + helpers.delete_buffer(bufnr) + end) + + it('errors = quickfix populates quickfix list', function() + local bufnr = helpers.create_buffer({ 'hello' }, 'text') + vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_errors_qf.txt') + vim.bo[bufnr].modified = false + + local provider = { + cmd = { 'sh', '-c', 'echo "line 1 error" >&2; exit 1' }, + errors = 'quickfix', + error_parser = function() + return { + { lnum = 0, col = 0, message = 'test error', severity = vim.diagnostic.severity.ERROR }, + } + end, + } + local ctx = { + bufnr = bufnr, + file = '/tmp/preview_test_errors_qf.txt', + root = '/tmp', + ft = 'text', + } + + vim.fn.setqflist({}, 'r') + compiler.compile(bufnr, 'qfcmd', provider, ctx) + + vim.wait(2000, function() + return compiler._test.active[bufnr] == nil + end, 50) + + local qflist = vim.fn.getqflist() + assert.are.equal(1, #qflist) + assert.are.equal('test error', qflist[1].text) + assert.are.equal(1, qflist[1].lnum) + + vim.fn.setqflist({}, 'r') + helpers.delete_buffer(bufnr) + end) + + it('errors = quickfix clears quickfix on success', function() + local bufnr = helpers.create_buffer({ 'hello' }, 'text') + vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_errors_qf_clear.txt') + vim.bo[bufnr].modified = false + + vim.fn.setqflist({ { text = 'old error', lnum = 1 } }, 'r') + assert.are.equal(1, #vim.fn.getqflist()) + + local provider = { + cmd = { 'true' }, + errors = 'quickfix', + error_parser = function() + return {} + end, + } + local ctx = { + bufnr = bufnr, + file = '/tmp/preview_test_errors_qf_clear.txt', + root = '/tmp', + ft = 'text', + } + + compiler.compile(bufnr, 'truecmd', provider, ctx) + + vim.wait(2000, function() + return compiler._test.active[bufnr] == nil + end, 50) + + assert.are.equal(0, #vim.fn.getqflist()) + helpers.delete_buffer(bufnr) + end) + end) + describe('stop', function() it('does nothing when no process is active', function() assert.has_no.errors(function() 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)