From e0f84b173393201239511d627a021b79f080fa1f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 14:07:28 -0500 Subject: [PATCH] feat(compiler): add configurable error output modes Problem: all parse errors went to vim.diagnostic with no way to silence them or route them to the quickfix list. Users wanting quickfix-style error navigation had no option. Solution: add an errors field to ProviderConfig accepting false, 'diagnostic' (default), or 'quickfix'. false suppresses error handling entirely. 'quickfix' converts parsed diagnostics to qflist items (1-indexed), calls setqflist, and opens the window. On success, 'quickfix' mode clears the qflist the same way 'diagnostic' mode clears vim.diagnostic. --- lua/preview/compiler.lua | 33 +++++++++++-- lua/preview/init.lua | 1 + spec/compiler_spec.lua | 102 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 052c4f0..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,9 +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 + if provider.error_parser and errors_mode then local output = (result.stdout or '') .. (result.stderr or '') - diagnostic.set(bufnr, name, provider.error_parser, output, ctx) + 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', diff --git a/lua/preview/init.lua b/lua/preview/init.lua index d122e61..4a44a33 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -6,6 +6,7 @@ ---@field env? table ---@field output? string|fun(ctx: preview.Context): string ---@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/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()