fix: lifecycle cleanup and defensive runtime checks (#29)

* fix(commands): register VimLeavePre to call stop_all

Problem: spawned compiler processes and watching autocmds were never
cleaned up when Neovim exited, leaving orphaned processes running.

Solution: register a VimLeavePre autocmd in commands setup that calls
compiler.stop_all(), which kills active processes, unwatches all
buffers, and stops the reload server.

* fix(compiler): replace BufWipeout with BufUnload

Problem: cleanup autocmds used BufWipeout, which only fires for
:bwipeout. The common :bdelete path (used by most buffer managers
and nvim_buf_delete) fires BufUnload but not BufWipeout, so processes
and watches leaked on normal buffer deletion.

Solution: switch all three cleanup autocmds from BufWipeout to
BufUnload, which fires for both :bdelete and :bwipeout.

* fix(init): guard against unnamed buffer in public API

Problem: calling compile/toggle/clean/open on an unsaved scratch
buffer passed an empty string as ctx.file, producing nonsensical
output paths like ".pdf" and silently passing empty strings to
compiler binaries.

Solution: add an early return with a WARN notification in compile,
toggle, clean, and open when the buffer has no file name.

* fix(compiler): add fs_stat check to one-shot open path

Problem: the long-running process path already guarded opens with
vim.uv.fs_stat(), but the one-shot compile path and M.open() did not.
Compilation can exit 0 and produce no output, and output files can be
externally deleted between compile and open.

Solution: add the same fs_stat guard to the one-shot open branch and
to M.open() before attempting to launch the viewer.

* fix(compiler): check executable before spawning process

Problem: if a configured binary was missing or not in PATH, vim.system
would fail silently or with a cryptic OS error. The user had no
actionable feedback without running :checkhealth.

Solution: check vim.fn.executable() at the start of M.compile() and
notify with an ERROR-level message pointing to :checkhealth preview
if the binary is not found.

* fix(compiler): reformat one-shot open condition for line length

Problem: the added fs_stat condition exceeded stylua's line length
limit on the one-shot open guard.

Solution: split the boolean condition across multiple lines to match
the project's stylua formatting rules.
This commit is contained in:
Barrett Ruth 2026-03-04 14:02:16 -05:00 committed by GitHub
parent 75b855438a
commit c94df7c5d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 143 additions and 4 deletions

View file

@ -58,6 +58,14 @@ end
function M.compile(bufnr, name, provider, ctx, opts)
opts = opts or {}
if vim.fn.executable(provider.cmd[1]) ~= 1 then
vim.notify(
'[preview.nvim]: "' .. provider.cmd[1] .. '" is not executable (run :checkhealth preview)',
vim.log.levels.ERROR
)
return
end
if vim.bo[bufnr].modified then
vim.cmd('silent! update')
end
@ -170,7 +178,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true }
vim.api.nvim_create_autocmd('BufWipeout', {
vim.api.nvim_create_autocmd('BufUnload', {
buffer = bufnr,
once = true,
callback = function()
@ -230,7 +238,12 @@ function M.compile(bufnr, name, provider, ctx, opts)
r.inject(output_file)
r.broadcast()
end
if provider.open and not opened[bufnr] and output_file ~= '' then
if
provider.open
and not opened[bufnr]
and output_file ~= ''
and vim.uv.fs_stat(output_file)
then
if provider.open == true then
vim.ui.open(output_file)
elseif type(provider.open) == 'table' then
@ -279,7 +292,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
active[bufnr] = { obj = obj, provider = name, output_file = output_file }
vim.api.nvim_create_autocmd('BufWipeout', {
vim.api.nvim_create_autocmd('BufUnload', {
buffer = bufnr,
once = true,
callback = function()
@ -374,7 +387,7 @@ function M.toggle(bufnr, name, provider, ctx_builder)
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', {
vim.api.nvim_create_autocmd('BufUnload', {
buffer = bufnr,
once = true,
callback = function()
@ -452,6 +465,10 @@ function M.open(bufnr, open_config)
log.dbg('no last output file for buffer %d', bufnr)
return false
end
if not vim.uv.fs_stat(output) then
log.dbg('output file no longer exists for buffer %d: %s', bufnr, output)
return false
end
if type(open_config) == 'table' then
local open_cmd = vim.list_extend({}, open_config)
table.insert(open_cmd, output)