From cfe101c6c4fb1e1bf24761881407b3f9798f0a7e Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:37:02 -0500 Subject: [PATCH] feat(commands): add :Preview open subcommand (#6) Problem: after closing a viewer, there was no way to re-open the last compiled output without recompiling. Solution: track the most recent output file per buffer in a `last_output` table that persists after compilation finishes. Add `compiler.open()`, `M.open()`, and wire it into the command dispatch. --- lua/preview/commands.lua | 6 ++++-- lua/preview/compiler.lua | 21 +++++++++++++++++++++ lua/preview/init.lua | 9 +++++++++ spec/commands_spec.lua | 7 +++++++ spec/compiler_spec.lua | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lua/preview/commands.lua b/lua/preview/commands.lua index 8e08236..ad63c97 100644 --- a/lua/preview/commands.lua +++ b/lua/preview/commands.lua @@ -1,6 +1,6 @@ local M = {} -local subcommands = { 'compile', 'stop', 'clean', 'toggle', 'status' } +local subcommands = { 'compile', 'stop', 'clean', 'toggle', 'open', 'status' } ---@param args string local function dispatch(args) @@ -14,6 +14,8 @@ local function dispatch(args) require('preview').clean() elseif subcmd == 'toggle' then require('preview').toggle() + elseif subcmd == 'open' then + require('preview').open() elseif subcmd == 'status' then local s = require('preview').status() local parts = {} @@ -47,7 +49,7 @@ function M.setup() complete = function(lead) return complete(lead) end, - desc = 'Compile, stop, clean, toggle, or check status of document preview', + desc = 'Compile, stop, clean, toggle, open, or check status of document preview', }) end diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index b0c7402..50ab0ba 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -12,6 +12,9 @@ local watching = {} ---@type table local opened = {} +---@type table +local last_output = {} + ---@param val string[]|fun(ctx: preview.Context): string[] ---@param ctx preview.Context ---@return string[] @@ -61,6 +64,10 @@ function M.compile(bufnr, name, provider, ctx) 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( @@ -117,6 +124,7 @@ function M.compile(bufnr, name, provider, ctx) once = true, callback = function() M.stop(bufnr) + last_output[bufnr] = nil end, }) @@ -235,6 +243,18 @@ function M.clean(bufnr, name, provider, ctx) ) 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) @@ -254,6 +274,7 @@ M._test = { active = active, watching = watching, opened = opened, + last_output = last_output, } return M diff --git a/lua/preview/init.lua b/lua/preview/init.lua index e2cf794..641da4a 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -39,6 +39,7 @@ ---@field stop fun(bufnr?: integer) ---@field clean fun(bufnr?: integer) ---@field toggle fun(bufnr?: integer) +---@field open fun(bufnr?: integer) ---@field status fun(bufnr?: integer): preview.Status ---@field get_config fun(): preview.Config local M = {} @@ -166,6 +167,14 @@ function M.toggle(bufnr) compiler.toggle(bufnr, name, provider, M.build_context) end +---@param bufnr? integer +function M.open(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + if not compiler.open(bufnr) then + vim.notify('[preview.nvim] no output file available for this buffer', vim.log.levels.WARN) + end +end + ---@class preview.Status ---@field compiling boolean ---@field watching boolean diff --git a/spec/commands_spec.lua b/spec/commands_spec.lua index be9a616..931174f 100644 --- a/spec/commands_spec.lua +++ b/spec/commands_spec.lua @@ -35,6 +35,13 @@ describe('commands', function() end) end) + it('does not error on :Preview open', function() + require('preview.commands').setup() + assert.has_no.errors(function() + vim.cmd('Preview open') + end) + end) + it('does not error on :Preview toggle with no provider', function() require('preview.commands').setup() assert.has_no.errors(function() diff --git a/spec/compiler_spec.lua b/spec/compiler_spec.lua index 6802dee..2046ef2 100644 --- a/spec/compiler_spec.lua +++ b/spec/compiler_spec.lua @@ -174,6 +174,41 @@ describe('compiler', function() end) end) + describe('open', function() + it('returns false when no output exists', function() + assert.is_false(compiler.open(999)) + end) + + it('returns true after compilation stores output', function() + local bufnr = helpers.create_buffer({ 'hello' }, 'text') + vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_open.txt') + vim.bo[bufnr].modified = false + + local provider = { + cmd = { 'true' }, + output = function() + return '/tmp/preview_test_open.pdf' + end, + } + local ctx = { + bufnr = bufnr, + file = '/tmp/preview_test_open.txt', + root = '/tmp', + ft = 'text', + } + + compiler.compile(bufnr, 'testprov', provider, ctx) + assert.is_not_nil(compiler._test.last_output[bufnr]) + assert.are.equal('/tmp/preview_test_open.pdf', compiler._test.last_output[bufnr]) + + vim.wait(2000, function() + return compiler._test.active[bufnr] == nil + end, 50) + + helpers.delete_buffer(bufnr) + end) + end) + describe('toggle', function() it('registers autocmd and tracks in watching table', function() local bufnr = helpers.create_buffer({ 'hello' }, 'text')