Compare commits

..

3 commits

Author SHA1 Message Date
20f44f44a2
test: update unnamed buffer tests for tmpfile fallback
Problem: the unnamed buffer guard tests expected a "no file name"
warning that no longer exists after the tmpfile fallback change.

Solution: update assertions to expect the downstream messages that
unnamed buffers now reach ("no provider configured", "no output file").
2026-03-13 08:22:03 -04:00
c1734eec8f
fix: fall back to /tmp for buffers without a backing file
Problem: markdown and gfm presets fail when the buffer has no file on
disk (e.g. unnamed buffer with `ft=markdown`, or a named buffer whose
path doesn't exist yet) because `build_context` passes a nonexistent
path to pandoc and `compile` guards reject empty buffer names.

Solution: `build_context` now detects missing files and redirects
`ctx.file` to `/tmp/{bufnr}-{name}`. `compile` writes buffer contents
to that temp path via `vim.fn.writefile` instead of `:silent! update`.
2026-03-13 08:19:57 -04:00
Barrett Ruth
272153a158
fix(compiler): use BufDelete instead of BufUnload for cleanup (#59)
Problem: `:e` (edit/reload) fires `BufUnload`, which tears down the
entire preview — process, watchers, and viewer — even though the user
is just reloading the current buffer.

Solution: Switch the cleanup autocmd from `BufUnload` to `BufDelete`,
which only fires on `:bdelete` and `:bwipeout`. Guard `nvim_del_autocmd`
calls with `pcall` in cleanup paths since buffer-local autocmds may
already be gone when the buffer is wiped before `stop_watching` runs.

Closes #57
2026-03-12 16:52:26 -04:00
3 changed files with 55 additions and 39 deletions

View file

@ -16,7 +16,7 @@ local log = require('preview.log')
---@field has_errors? boolean
---@field debounce? uv.uv_timer_t
---@field bwp_autocmd? integer
---@field unload_autocmd? integer
---@field cleanup_autocmd? integer
---@type table<integer, preview.BufState>
local state = {}
@ -189,7 +189,7 @@ local function stop_watching(bufnr, s)
close_viewer(bufnr)
s.viewer_open = nil
if s.bwp_autocmd then
vim.api.nvim_del_autocmd(s.bwp_autocmd)
pcall(vim.api.nvim_del_autocmd, s.bwp_autocmd)
s.bwp_autocmd = nil
end
if s.debounce then
@ -215,8 +215,14 @@ function M.compile(bufnr, name, provider, ctx, opts)
return
end
if vim.bo[bufnr].modified then
vim.cmd('silent! update')
local buf_file = vim.api.nvim_buf_get_name(bufnr)
if buf_file ~= '' and buf_file == ctx.file then
if vim.bo[bufnr].modified then
vim.cmd('silent! update')
end
else
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
vim.fn.writefile(lines, ctx.file)
end
local s = get_state(bufnr)
@ -508,8 +514,8 @@ end
function M.stop_all()
for bufnr, s in pairs(state) do
stop_watching(bufnr, s)
if s.unload_autocmd then
vim.api.nvim_del_autocmd(s.unload_autocmd)
if s.cleanup_autocmd then
pcall(vim.api.nvim_del_autocmd, s.cleanup_autocmd)
end
state[bufnr] = nil
end
@ -541,10 +547,10 @@ function M.toggle(bufnr, name, provider, ctx_builder)
log.dbg('toggle on for buffer %d', bufnr)
s.watching = true
if s.unload_autocmd then
vim.api.nvim_del_autocmd(s.unload_autocmd)
if s.cleanup_autocmd then
vim.api.nvim_del_autocmd(s.cleanup_autocmd)
end
s.unload_autocmd = vim.api.nvim_create_autocmd('BufUnload', {
s.cleanup_autocmd = vim.api.nvim_create_autocmd('BufDelete', {
buffer = bufnr,
once = true,
callback = function()

View file

@ -139,27 +139,49 @@ function M.resolve_provider(bufnr)
return ft
end
local ft_ext = {
markdown = 'md',
typst = 'typ',
tex = 'tex',
asciidoc = 'adoc',
plantuml = 'puml',
mermaid = 'mmd',
quarto = 'qmd',
}
---@param bufnr? integer
---@return preview.Context
function M.build_context(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local file = vim.api.nvim_buf_get_name(bufnr)
local root = vim.fs.root(bufnr, { '.git' }) or vim.fn.fnamemodify(file, ':h')
local ft = vim.bo[bufnr].filetype
local root
if file ~= '' and vim.uv.fs_stat(file) then
root = vim.fs.root(bufnr, { '.git' }) or vim.fn.fnamemodify(file, ':h')
else
local basename
if file ~= '' then
basename = string.format('%d-%s', bufnr, vim.fn.fnamemodify(file, ':t'))
else
local ext = ft_ext[ft] or ft
basename = string.format('preview-%d.%s', bufnr, ext)
end
file = '/tmp/' .. basename
root = '/tmp'
end
return {
bufnr = bufnr,
file = file,
root = root,
ft = vim.bo[bufnr].filetype,
ft = ft,
}
end
---@param bufnr? integer
function M.compile(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_name(bufnr) == '' then
vim.notify('[preview.nvim]: buffer has no file name', vim.log.levels.WARN)
return
end
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[preview.nvim]: no provider configured for this filetype', vim.log.levels.WARN)
@ -179,10 +201,6 @@ end
---@param bufnr? integer
function M.clean(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_name(bufnr) == '' then
vim.notify('[preview.nvim]: buffer has no file name', vim.log.levels.WARN)
return
end
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[preview.nvim]: no provider configured for this filetype', vim.log.levels.WARN)
@ -196,10 +214,6 @@ end
---@param bufnr? integer
function M.toggle(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_name(bufnr) == '' then
vim.notify('[preview.nvim]: buffer has no file name', vim.log.levels.WARN)
return
end
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[preview.nvim]: no provider configured for this filetype', vim.log.levels.WARN)
@ -212,10 +226,6 @@ end
---@param bufnr? integer
function M.open(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_name(bufnr) == '' then
vim.notify('[preview.nvim]: buffer has no file name', vim.log.levels.WARN)
return
end
local name = M.resolve_provider(bufnr)
local open_config = name and config.providers[name] and config.providers[name].open
if not compiler.open(bufnr, open_config) then

View file

@ -126,43 +126,43 @@ describe('preview', function()
return msg
end
it('compile warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
it('compile falls through to provider check on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'lua')
local msg = capture_notify(function()
preview.compile(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
assert.is_truthy(msg:find('no provider configured'))
helpers.delete_buffer(bufnr)
end)
it('toggle warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
it('toggle falls through to provider check on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'lua')
local msg = capture_notify(function()
preview.toggle(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
assert.is_truthy(msg:find('no provider configured'))
helpers.delete_buffer(bufnr)
end)
it('clean warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
it('clean falls through to provider check on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'lua')
local msg = capture_notify(function()
preview.clean(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
assert.is_truthy(msg:find('no provider configured'))
helpers.delete_buffer(bufnr)
end)
it('open warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
it('open warns no output on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'lua')
local msg = capture_notify(function()
preview.open(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
assert.is_truthy(msg:find('no output file'))
helpers.delete_buffer(bufnr)
end)
end)