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.
302 lines
7.3 KiB
Lua
302 lines
7.3 KiB
Lua
local M = {}
|
|
|
|
local diagnostic = require('preview.diagnostic')
|
|
local log = require('preview.log')
|
|
|
|
---@type table<integer, preview.Process>
|
|
local active = {}
|
|
|
|
---@type table<integer, integer>
|
|
local watching = {}
|
|
|
|
---@type table<integer, true>
|
|
local opened = {}
|
|
|
|
---@type table<integer, string>
|
|
local last_output = {}
|
|
|
|
local debounce_timers = {}
|
|
|
|
local DEBOUNCE_MS = 500
|
|
|
|
---@param val string[]|fun(ctx: preview.Context): string[]
|
|
---@param ctx preview.Context
|
|
---@return string[]
|
|
local function eval_list(val, ctx)
|
|
if type(val) == 'function' then
|
|
return val(ctx)
|
|
end
|
|
return val
|
|
end
|
|
|
|
---@param val string|fun(ctx: preview.Context): string
|
|
---@param ctx preview.Context
|
|
---@return string
|
|
local function eval_string(val, ctx)
|
|
if type(val) == 'function' then
|
|
return val(ctx)
|
|
end
|
|
return val
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param name string
|
|
---@param provider preview.ProviderConfig
|
|
---@param ctx preview.Context
|
|
function M.compile(bufnr, name, provider, ctx)
|
|
if vim.bo[bufnr].modified then
|
|
vim.cmd('silent! update')
|
|
end
|
|
|
|
if active[bufnr] then
|
|
log.dbg('killing existing process for buffer %d before recompile', bufnr)
|
|
M.stop(bufnr)
|
|
end
|
|
|
|
local cmd = vim.list_extend({}, provider.cmd)
|
|
if provider.args then
|
|
vim.list_extend(cmd, eval_list(provider.args, ctx))
|
|
end
|
|
|
|
local cwd = ctx.root
|
|
if provider.cwd then
|
|
cwd = eval_string(provider.cwd, ctx)
|
|
end
|
|
|
|
local output_file = ''
|
|
if provider.output then
|
|
output_file = eval_string(provider.output, ctx)
|
|
end
|
|
|
|
if output_file ~= '' then
|
|
last_output[bufnr] = output_file
|
|
end
|
|
|
|
log.dbg('compiling buffer %d with provider "%s": %s', bufnr, name, table.concat(cmd, ' '))
|
|
|
|
local obj = vim.system(
|
|
cmd,
|
|
{
|
|
cwd = cwd,
|
|
env = provider.env,
|
|
},
|
|
vim.schedule_wrap(function(result)
|
|
active[bufnr] = nil
|
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
|
return
|
|
end
|
|
|
|
if result.code == 0 then
|
|
log.dbg('compilation succeeded for buffer %d', bufnr)
|
|
diagnostic.clear(bufnr)
|
|
vim.api.nvim_exec_autocmds('User', {
|
|
pattern = 'PreviewCompileSuccess',
|
|
data = { bufnr = bufnr, provider = name, output = output_file },
|
|
})
|
|
if provider.open and not opened[bufnr] and output_file ~= '' then
|
|
if provider.open == true then
|
|
vim.ui.open(output_file)
|
|
elseif type(provider.open) == 'table' then
|
|
local open_cmd = vim.list_extend({}, provider.open)
|
|
table.insert(open_cmd, output_file)
|
|
vim.system(open_cmd)
|
|
end
|
|
opened[bufnr] = true
|
|
end
|
|
else
|
|
log.dbg('compilation failed for buffer %d (exit code %d)', bufnr, result.code)
|
|
if provider.error_parser then
|
|
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',
|
|
data = {
|
|
bufnr = bufnr,
|
|
provider = name,
|
|
code = result.code,
|
|
stderr = result.stderr or '',
|
|
},
|
|
})
|
|
end
|
|
end)
|
|
)
|
|
|
|
active[bufnr] = { obj = obj, provider = name, output_file = output_file }
|
|
|
|
vim.api.nvim_create_autocmd('BufWipeout', {
|
|
buffer = bufnr,
|
|
once = true,
|
|
callback = function()
|
|
M.stop(bufnr)
|
|
last_output[bufnr] = nil
|
|
end,
|
|
})
|
|
|
|
vim.api.nvim_exec_autocmds('User', {
|
|
pattern = 'PreviewCompileStarted',
|
|
data = { bufnr = bufnr, provider = name },
|
|
})
|
|
end
|
|
|
|
---@param bufnr integer
|
|
function M.stop(bufnr)
|
|
local proc = active[bufnr]
|
|
if not proc then
|
|
return
|
|
end
|
|
log.dbg('stopping process for buffer %d', bufnr)
|
|
---@type fun(self: table, signal: string|integer)
|
|
local kill = proc.obj.kill
|
|
kill(proc.obj, 'sigterm')
|
|
|
|
local timer = vim.uv.new_timer()
|
|
if timer then
|
|
timer:start(5000, 0, function()
|
|
timer:close()
|
|
if active[bufnr] and active[bufnr].obj == proc.obj then
|
|
kill(proc.obj, 'sigkill')
|
|
active[bufnr] = nil
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
function M.stop_all()
|
|
for bufnr, _ in pairs(active) do
|
|
M.stop(bufnr)
|
|
end
|
|
for bufnr, _ in pairs(watching) do
|
|
M.unwatch(bufnr)
|
|
end
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param name string
|
|
---@param provider preview.ProviderConfig
|
|
---@param ctx_builder fun(bufnr: integer): preview.Context
|
|
function M.toggle(bufnr, name, provider, ctx_builder)
|
|
if watching[bufnr] then
|
|
M.unwatch(bufnr)
|
|
return
|
|
end
|
|
|
|
local au_id = vim.api.nvim_create_autocmd('BufWritePost', {
|
|
buffer = bufnr,
|
|
callback = function()
|
|
if debounce_timers[bufnr] then
|
|
debounce_timers[bufnr]:stop()
|
|
else
|
|
debounce_timers[bufnr] = vim.uv.new_timer()
|
|
end
|
|
debounce_timers[bufnr]:start(
|
|
DEBOUNCE_MS,
|
|
0,
|
|
vim.schedule_wrap(function()
|
|
local ctx = ctx_builder(bufnr)
|
|
M.compile(bufnr, name, provider, ctx)
|
|
end)
|
|
)
|
|
end,
|
|
})
|
|
|
|
watching[bufnr] = au_id
|
|
log.dbg('watching buffer %d with provider "%s"', bufnr, name)
|
|
|
|
vim.api.nvim_create_autocmd('BufWipeout', {
|
|
buffer = bufnr,
|
|
once = true,
|
|
callback = function()
|
|
M.unwatch(bufnr)
|
|
opened[bufnr] = nil
|
|
end,
|
|
})
|
|
|
|
M.compile(bufnr, name, provider, ctx_builder(bufnr))
|
|
end
|
|
|
|
---@param bufnr integer
|
|
function M.unwatch(bufnr)
|
|
local au_id = watching[bufnr]
|
|
if not au_id then
|
|
return
|
|
end
|
|
vim.api.nvim_del_autocmd(au_id)
|
|
if debounce_timers[bufnr] then
|
|
debounce_timers[bufnr]:stop()
|
|
debounce_timers[bufnr]:close()
|
|
debounce_timers[bufnr] = nil
|
|
end
|
|
watching[bufnr] = nil
|
|
log.dbg('unwatched buffer %d', bufnr)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@param name string
|
|
---@param provider preview.ProviderConfig
|
|
---@param ctx preview.Context
|
|
function M.clean(bufnr, name, provider, ctx)
|
|
if not provider.clean then
|
|
vim.notify('[preview.nvim] provider "' .. name .. '" has no clean command', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
|
|
local cmd = eval_list(provider.clean, ctx)
|
|
local cwd = ctx.root
|
|
if provider.cwd then
|
|
cwd = eval_string(provider.cwd, ctx)
|
|
end
|
|
|
|
log.dbg('cleaning buffer %d with provider "%s": %s', bufnr, name, table.concat(cmd, ' '))
|
|
|
|
vim.system(
|
|
cmd,
|
|
{ cwd = cwd },
|
|
vim.schedule_wrap(function(result)
|
|
if result.code == 0 then
|
|
log.dbg('clean succeeded for buffer %d', bufnr)
|
|
vim.notify('[preview.nvim] clean complete', vim.log.levels.INFO)
|
|
else
|
|
log.dbg('clean failed for buffer %d (exit code %d)', bufnr, result.code)
|
|
vim.notify('[preview.nvim] clean failed: ' .. (result.stderr or ''), vim.log.levels.ERROR)
|
|
end
|
|
end)
|
|
)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return boolean
|
|
function M.open(bufnr)
|
|
local output = last_output[bufnr]
|
|
if not output then
|
|
log.dbg('no last output file for buffer %d', bufnr)
|
|
return false
|
|
end
|
|
vim.ui.open(output)
|
|
return true
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return preview.Status
|
|
function M.status(bufnr)
|
|
local proc = active[bufnr]
|
|
if proc then
|
|
return {
|
|
compiling = true,
|
|
watching = watching[bufnr] ~= nil,
|
|
provider = proc.provider,
|
|
output_file = proc.output_file,
|
|
}
|
|
end
|
|
return { compiling = false, watching = watching[bufnr] ~= nil }
|
|
end
|
|
|
|
M._test = {
|
|
active = active,
|
|
watching = watching,
|
|
opened = opened,
|
|
last_output = last_output,
|
|
debounce_timers = debounce_timers,
|
|
}
|
|
|
|
return M
|