Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
e03f51ca04
ci: format 2026-03-04 15:48:13 -05:00
96df25b7be
docs: document viewer auto-close behaviour and limitations in open field 2026-03-04 15:45:56 -05:00
f6ce8a1544
fix(compiler): defer open until successful compile, close viewer on :bd
Problem: For long-running providers (e.g. `typst watch`), the viewer
was opened immediately on toggle start by checking if the output file
existed on disk. A stale PDF from a prior session satisfied that check,
so a failed compile still opened the viewer. Additionally, viewer
processes spawned via a table `open` command were untracked, so `:bd`
killed the compiler but left the viewer running.

Solution: Replace the immediate open with a `vim.uv.new_fs_event`
directory watcher that fires only when the output file's `mtime`
advances past its pre-compile value, proving the current session wrote
it. Add `viewer_procs` and `open_watchers` tables with `close_viewer`
and `stop_open_watcher` helpers; all `BufUnload` paths and `stop_all`
now tear down both. Extract `do_open` to deduplicate the open branching
logic across three call sites.
2026-03-04 15:45:52 -05:00
2 changed files with 93 additions and 29 deletions

View file

@ -93,7 +93,12 @@ Provider fields:~
successful compilation in toggle/watch successful compilation in toggle/watch
mode. `true` uses |vim.ui.open()|. A mode. `true` uses |vim.ui.open()|. A
string[] is run as a command with the string[] is run as a command with the
output path appended. output path appended. When a string[]
is used the viewer process is tracked
and sent SIGTERM when the buffer is
deleted. `true` and single-instance
apps (e.g. Chrome) do not support
auto-close.
`reload` boolean|string[]|function `reload` boolean|string[]|function
Reload the output after recompilation. Reload the output after recompilation.

View file

@ -15,10 +15,49 @@ local opened = {}
---@type table<integer, string> ---@type table<integer, string>
local last_output = {} local last_output = {}
---@type table<integer, table>
local viewer_procs = {}
---@type table<integer, uv.uv_fs_event_t>
local open_watchers = {}
local debounce_timers = {} local debounce_timers = {}
local DEBOUNCE_MS = 500 local DEBOUNCE_MS = 500
---@param bufnr integer
local function stop_open_watcher(bufnr)
local w = open_watchers[bufnr]
if w then
w:stop()
w:close()
open_watchers[bufnr] = nil
end
end
---@param bufnr integer
local function close_viewer(bufnr)
local obj = viewer_procs[bufnr]
if obj then
local kill = obj.kill
kill(obj, 'sigterm')
viewer_procs[bufnr] = nil
end
end
---@param bufnr integer
---@param output_file string
---@param open_config boolean|string[]
local function do_open(bufnr, output_file, open_config)
if open_config == true then
vim.ui.open(output_file)
elseif type(open_config) == 'table' then
local open_cmd = vim.list_extend({}, open_config)
table.insert(open_cmd, output_file)
viewer_procs[bufnr] = vim.system(open_cmd)
end
end
---@param val string[]|fun(ctx: preview.Context): string[] ---@param val string[]|fun(ctx: preview.Context): string[]
---@param ctx preview.Context ---@param ctx preview.Context
---@return string[] ---@return string[]
@ -160,21 +199,40 @@ function M.compile(bufnr, name, provider, ctx, opts)
end) end)
) )
if if provider.open and not opts.oneshot and not opened[bufnr] and output_file ~= '' then
provider.open local pre_stat = vim.uv.fs_stat(output_file)
and not opts.oneshot local pre_mtime = pre_stat and pre_stat.mtime.sec or 0
and not opened[bufnr] local out_dir = vim.fn.fnamemodify(output_file, ':h')
and output_file ~= '' local out_name = vim.fn.fnamemodify(output_file, ':t')
and vim.uv.fs_stat(output_file) stop_open_watcher(bufnr)
then local watcher = vim.uv.new_fs_event()
if provider.open == true then if watcher then
vim.ui.open(output_file) open_watchers[bufnr] = watcher
elseif type(provider.open) == 'table' then watcher:start(
local open_cmd = vim.list_extend({}, provider.open) out_dir,
table.insert(open_cmd, output_file) {},
vim.system(open_cmd) vim.schedule_wrap(function(err, filename, _events)
if err or vim.fn.fnamemodify(filename or '', ':t') ~= out_name then
return
end
if opened[bufnr] then
stop_open_watcher(bufnr)
return
end
if not vim.api.nvim_buf_is_valid(bufnr) then
stop_open_watcher(bufnr)
return
end
local new_stat = vim.uv.fs_stat(output_file)
if not (new_stat and new_stat.mtime.sec > pre_mtime) then
return
end
stop_open_watcher(bufnr)
do_open(bufnr, output_file, provider.open)
opened[bufnr] = true
end)
)
end end
opened[bufnr] = true
end end
active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true } active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true }
@ -184,6 +242,8 @@ function M.compile(bufnr, name, provider, ctx, opts)
once = true, once = true,
callback = function() callback = function()
M.stop(bufnr) M.stop(bufnr)
stop_open_watcher(bufnr)
close_viewer(bufnr)
last_output[bufnr] = nil last_output[bufnr] = nil
end, end,
}) })
@ -246,13 +306,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
and output_file ~= '' and output_file ~= ''
and vim.uv.fs_stat(output_file) and vim.uv.fs_stat(output_file)
then then
if provider.open == true then do_open(bufnr, output_file, provider.open)
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 opened[bufnr] = true
end end
else else
@ -299,6 +353,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
once = true, once = true,
callback = function() callback = function()
M.stop(bufnr) M.stop(bufnr)
close_viewer(bufnr)
last_output[bufnr] = nil last_output[bufnr] = nil
end, end,
}) })
@ -339,6 +394,12 @@ function M.stop_all()
for bufnr, _ in pairs(watching) do for bufnr, _ in pairs(watching) do
M.unwatch(bufnr) M.unwatch(bufnr)
end end
for bufnr, _ in pairs(open_watchers) do
stop_open_watcher(bufnr)
end
for bufnr, _ in pairs(viewer_procs) do
close_viewer(bufnr)
end
require('preview.reload').stop() require('preview.reload').stop()
end end
@ -394,6 +455,8 @@ function M.toggle(bufnr, name, provider, ctx_builder)
once = true, once = true,
callback = function() callback = function()
M.unwatch(bufnr) M.unwatch(bufnr)
stop_open_watcher(bufnr)
close_viewer(bufnr)
opened[bufnr] = nil opened[bufnr] = nil
end, end,
}) })
@ -471,13 +534,7 @@ function M.open(bufnr, open_config)
log.dbg('output file no longer exists for buffer %d: %s', bufnr, output) log.dbg('output file no longer exists for buffer %d: %s', bufnr, output)
return false return false
end end
if type(open_config) == 'table' then do_open(bufnr, output, open_config)
local open_cmd = vim.list_extend({}, open_config)
table.insert(open_cmd, output)
vim.system(open_cmd)
else
vim.ui.open(output)
end
return true return true
end end
@ -502,6 +559,8 @@ M._test = {
opened = opened, opened = opened,
last_output = last_output, last_output = last_output,
debounce_timers = debounce_timers, debounce_timers = debounce_timers,
viewer_procs = viewer_procs,
open_watchers = open_watchers,
} }
return M return M