From 272153a158248265dd044944345f21321600eb5e Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:52:26 -0400 Subject: [PATCH 1/3] fix(compiler): use `BufDelete` instead of `BufUnload` for cleanup (#59) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lua/preview/compiler.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 90f7367..672b02f 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -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 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 @@ -508,8 +508,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 +541,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() From c1734eec8f9cd57d9585549a14dd314034c64ded Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 13 Mar 2026 08:19:57 -0400 Subject: [PATCH 2/3] 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`. --- lua/preview/compiler.lua | 10 +++++++-- lua/preview/init.lua | 46 ++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 672b02f..c3ffbfa 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -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) diff --git a/lua/preview/init.lua b/lua/preview/init.lua index 489679f..5383623 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -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 From 20f44f44a25e41c356fceb1df74e9c6a5149c8fa Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 13 Mar 2026 08:20:48 -0400 Subject: [PATCH 3/3] 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"). --- spec/init_spec.lua | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/init_spec.lua b/spec/init_spec.lua index f68438c..55c2391 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -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)