diff --git a/README.md b/README.md index 6d896ac..80a2650 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # preview.nvim -**Universal previewer for Neovim** +**Universal document previewer for Neovim** An extensible framework for compiling and previewing _any_ documents (LaTeX, Typst, Markdown, etc.)—diagnostics included. @@ -81,8 +81,3 @@ vim.g.preview = { typst = { open = { 'sioyek', '--new-instance' } }, } ``` - -**Q: How do I set up SyncTeX (forward/inverse search)?** - -See `:help preview-synctex` for full recipes covering Zathura, Sioyek, and -Okular. diff --git a/doc/preview.txt b/doc/preview.txt index 6bf5171..383a8f5 100644 --- a/doc/preview.txt +++ b/doc/preview.txt @@ -26,7 +26,6 @@ CONTENTS *preview-contents* 7. Lua API ................................................... |preview-api| 8. Events ............................................... |preview-events| 9. Health ............................................... |preview-health| - 10. SyncTeX ............................................. |preview-synctex| ============================================================================== REQUIREMENTS *preview-requirements* @@ -69,10 +68,6 @@ Provider fields: ~ receives a |preview.Context| and returns a string[]. - {extra_args} (string[]|function) Appended to {args} after evaluation. - Useful for adding flags to a preset - without replacing its defaults. - {cwd} (string|function) Working directory. If a function, receives a |preview.Context|. Default: git root or file directory. @@ -277,116 +272,5 @@ Checks: ~ - Each configured provider's binary is executable - Each configured provider's opener binary (if any) is executable -============================================================================== -SYNCTEX *preview-synctex* - -SyncTeX enables bidirectional navigation between LaTeX source and the -compiled PDF. The `latex` preset compiles with `-synctex=1` by default. - -Forward search (editor -> viewer) requires caching the output path. -Inverse search (viewer -> editor) requires a fixed Neovim server socket. - -The following configs leverage the below basic setup: ~ - ->lua - vim.fn.serverstart('/tmp/nvim-preview.sock') - - local synctex_pdf = {} - vim.api.nvim_create_autocmd('User', { - pattern = 'PreviewCompileSuccess', - callback = function(args) - synctex_pdf[args.data.bufnr] = args.data.output - end, - }) -< - -The recipes below bind `s` for forward search. To scroll the PDF -automatically on cursor movement, call the forward search function from a -|CursorMoved| or |CursorHold| autocmd instead. - -Viewer-specific recipes: ~ - - *preview-synctex-zathura* -Zathura ~ - -Inverse search: Ctrl+click. - ->lua - vim.keymap.set('n', 's', function() - local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] - if pdf then - vim.fn.jobstart({ - 'zathura', '--synctex-forward', - vim.fn.line('.') .. ':0:' .. vim.fn.expand('%:p'), pdf, - }) - end - end) - - vim.g.preview = { - latex = { - open = { - 'zathura', - '--synctex-editor-command', - 'nvim --server /tmp/nvim-preview.sock' - .. [[ --remote-expr "execute('b +%{line} %{input}')"]], - }, - }, - } -< - - *preview-synctex-sioyek* -Sioyek ~ - -Inverse search: right-click with synctex mode active. - -Add to `~/.config/sioyek/prefs_user.config`: > - inverse_search_command nvim --server /tmp/nvim-preview.sock --remote-expr "execute('b +%2 %1')" -< - ->lua - vim.keymap.set('n', 's', function() - local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] - if pdf then - vim.fn.jobstart({ - 'sioyek', - '--instance-name', 'preview', - '--forward-search-file', vim.fn.expand('%:p'), - '--forward-search-line', tostring(vim.fn.line('.')), - pdf, - }) - end - end) - - vim.g.preview = { - latex = { - open = { 'sioyek', '--instance-name', 'preview' }, - }, - } -< - - *preview-synctex-okular* -Okular ~ - -Inverse search (Shift+click): one-time GUI setup via -Settings -> Configure Okular -> Editor -> Custom Text Editor: > - nvim --server /tmp/nvim-preview.sock --remote-expr "execute('b +%l %f')" -< - ->lua - vim.keymap.set('n', 's', function() - local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] - if pdf then - vim.fn.jobstart({ - 'okular', '--unique', - ('%s#src:%d:%s'):format(pdf, vim.fn.line('.'), vim.fn.expand('%:p')), - }) - end - end) - - vim.g.preview = { - latex = { open = { 'okular', '--unique' } }, - } -< - ============================================================================== vim:tw=78:ts=8:ft=help:norl: diff --git a/flake.nix b/flake.nix index 539840f..636f4d0 100644 --- a/flake.nix +++ b/flake.nix @@ -19,8 +19,7 @@ { formatter = forEachSystem (pkgs: pkgs.nixfmt-tree); - devShells = forEachSystem ( - pkgs: + devShells = forEachSystem (pkgs: let devTools = [ (pkgs.luajit.withPackages ( @@ -49,11 +48,8 @@ pkgs.quarto pkgs.plantuml pkgs.mermaid-cli - pkgs.zathura - pkgs.sioyek ]; }; - } - ); + }); }; } diff --git a/lua/preview/compiler.lua b/lua/preview/compiler.lua index 5480d57..a193ad2 100644 --- a/lua/preview/compiler.lua +++ b/lua/preview/compiler.lua @@ -3,102 +3,45 @@ local M = {} local diagnostic = require('preview.diagnostic') local log = require('preview.log') ----@class preview.BufState ----@field watching boolean ----@field process? table ----@field is_reload? boolean ----@field provider? string ----@field output? string ----@field viewer? table ----@field viewer_open? boolean ----@field open_watcher? uv.uv_fs_event_t ----@field debounce? uv.uv_timer_t ----@field bwp_autocmd? integer ----@field unload_autocmd? integer +---@type table +local active = {} ----@type table -local state = {} +---@type table +local watching = {} + +---@type table +local opened = {} + +---@type table +local last_output = {} + +---@type table +local viewer_procs = {} + +---@type table +local open_watchers = {} + +local debounce_timers = {} local DEBOUNCE_MS = 500 ----@param bufnr integer ----@return preview.BufState -local function get_state(bufnr) - if not state[bufnr] then - state[bufnr] = { watching = false } - end - return state[bufnr] -end - ---@param bufnr integer local function stop_open_watcher(bufnr) - local s = state[bufnr] - if not (s and s.open_watcher) then - return + local w = open_watchers[bufnr] + if w then + w:stop() + w:close() + open_watchers[bufnr] = nil end - s.open_watcher:stop() - s.open_watcher:close() - s.open_watcher = nil end ---@param bufnr integer local function close_viewer(bufnr) - local s = state[bufnr] - if not (s and s.viewer) then - return - end - s.viewer:kill('sigterm') - s.viewer = nil -end - ----@param bufnr integer ----@param name string ----@param provider preview.ProviderConfig ----@param ctx preview.Context ----@param output string -local function handle_errors(bufnr, name, provider, ctx, output) - local errors_mode = provider.errors - if errors_mode == nil then - errors_mode = 'diagnostic' - end - if not (provider.error_parser and errors_mode) then - return - end - if errors_mode == 'diagnostic' then - diagnostic.set(bufnr, name, provider.error_parser, output, ctx) - elseif errors_mode == 'quickfix' then - local ok, diags = pcall(provider.error_parser, output, ctx) - if ok and diags and #diags > 0 then - local items = {} - for _, d in ipairs(diags) do - table.insert(items, { - bufnr = bufnr, - lnum = d.lnum + 1, - col = d.col + 1, - text = d.message, - type = d.severity == vim.diagnostic.severity.WARN and 'W' or 'E', - }) - end - vim.fn.setqflist(items, 'r') - local win = vim.fn.win_getid() - vim.cmd.cwindow() - vim.fn.win_gotoid(win) - end - end -end - ----@param bufnr integer ----@param provider preview.ProviderConfig -local function clear_errors(bufnr, provider) - local errors_mode = provider.errors - if errors_mode == nil then - errors_mode = 'diagnostic' - end - if errors_mode == 'diagnostic' then - diagnostic.clear(bufnr) - elseif errors_mode == 'quickfix' then - vim.fn.setqflist({}, 'r') - vim.cmd.cwindow() + local obj = viewer_procs[bufnr] + if obj then + local kill = obj.kill + kill(obj, 'sigterm') + viewer_procs[bufnr] = nil end end @@ -111,23 +54,7 @@ local function do_open(bufnr, output_file, open_config) elseif type(open_config) == 'table' then local open_cmd = vim.list_extend({}, open_config) table.insert(open_cmd, output_file) - log.dbg('opening viewer for buffer %d: %s', bufnr, table.concat(open_cmd, ' ')) - local proc - proc = vim.system( - open_cmd, - {}, - vim.schedule_wrap(function() - local s = state[bufnr] - if s and s.viewer == proc then - log.dbg('viewer exited for buffer %d, resetting viewer_open', bufnr) - s.viewer = nil - s.viewer_open = nil - else - log.dbg('viewer exited for buffer %d (stale proc, ignoring)', bufnr) - end - end) - ) - get_state(bufnr).viewer = proc + viewer_procs[bufnr] = vim.system(open_cmd) end end @@ -163,30 +90,10 @@ local function resolve_reload_cmd(provider, ctx) return nil end ----@param bufnr integer ----@param s preview.BufState -local function stop_watching(bufnr, s) - s.watching = false - M.stop(bufnr) - stop_open_watcher(bufnr) - close_viewer(bufnr) - s.viewer_open = nil - if s.bwp_autocmd then - vim.api.nvim_del_autocmd(s.bwp_autocmd) - s.bwp_autocmd = nil - end - if s.debounce then - s.debounce:stop() - s.debounce:close() - s.debounce = nil - end -end - ---@param bufnr integer ---@param name string ---@param provider preview.ProviderConfig ---@param ctx preview.Context ----@param opts? {oneshot?: boolean} function M.compile(bufnr, name, provider, ctx, opts) opts = opts or {} @@ -202,9 +109,7 @@ function M.compile(bufnr, name, provider, ctx, opts) vim.cmd('silent! update') end - local s = get_state(bufnr) - - if s.process then + if active[bufnr] then log.dbg('killing existing process for buffer %d before recompile', bufnr) M.stop(bufnr) end @@ -222,7 +127,7 @@ function M.compile(bufnr, name, provider, ctx, opts) end if output_file ~= '' then - s.output = output_file + last_output[bufnr] = output_file end local reload_cmd @@ -250,20 +155,74 @@ function M.compile(bufnr, name, provider, ctx, opts) return end stderr_acc[#stderr_acc + 1] = data - handle_errors(bufnr, name, provider, ctx, table.concat(stderr_acc)) + local errors_mode = provider.errors + if errors_mode == nil then + errors_mode = 'diagnostic' + end + if provider.error_parser and errors_mode then + local output = table.concat(stderr_acc) + if errors_mode == 'diagnostic' then + diagnostic.set(bufnr, name, provider.error_parser, output, ctx) + elseif errors_mode == 'quickfix' then + local ok, diags = pcall(provider.error_parser, output, ctx) + if ok and diags and #diags > 0 then + local items = {} + for _, d in ipairs(diags) do + table.insert(items, { + bufnr = bufnr, + lnum = d.lnum + 1, + col = d.col + 1, + text = d.message, + type = d.severity == vim.diagnostic.severity.WARN and 'W' or 'E', + }) + end + vim.fn.setqflist(items, 'r') + local win = vim.fn.win_getid() + vim.cmd.cwindow() + vim.fn.win_gotoid(win) + end + end + end end), }, vim.schedule_wrap(function(result) - local cs = state[bufnr] - if cs and cs.process == obj then - cs.process = nil + if active[bufnr] and active[bufnr].obj == obj then + active[bufnr] = nil end if not vim.api.nvim_buf_is_valid(bufnr) then return end + if result.code ~= 0 then log.dbg('long-running process failed for buffer %d (exit code %d)', bufnr, result.code) - handle_errors(bufnr, name, provider, ctx, (result.stdout or '') .. (result.stderr or '')) + local errors_mode = provider.errors + if errors_mode == nil then + errors_mode = 'diagnostic' + end + if provider.error_parser and errors_mode then + local output = (result.stdout or '') .. (result.stderr or '') + if errors_mode == 'diagnostic' then + diagnostic.set(bufnr, name, provider.error_parser, output, ctx) + elseif errors_mode == 'quickfix' then + local ok, diagnostics = pcall(provider.error_parser, output, ctx) + if ok and diagnostics and #diagnostics > 0 then + local items = {} + for _, d in ipairs(diagnostics) do + table.insert(items, { + bufnr = bufnr, + lnum = d.lnum + 1, + col = d.col + 1, + text = d.message, + type = d.severity == vim.diagnostic.severity.WARN and 'W' or 'E', + }) + end + vim.fn.setqflist(items, 'r') + local win = vim.fn.win_getid() + vim.cmd.cwindow() + vim.fn.win_gotoid(win) + end + end + end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileFailed', data = { @@ -277,7 +236,7 @@ function M.compile(bufnr, name, provider, ctx, opts) end) ) - if provider.open and not opts.oneshot and not s.viewer_open and output_file ~= '' then + if provider.open and not opts.oneshot and not opened[bufnr] and output_file ~= '' then local pre_stat = vim.uv.fs_stat(output_file) local pre_mtime = pre_stat and pre_stat.mtime.sec or 0 local out_dir = vim.fn.fnamemodify(output_file, ':h') @@ -285,7 +244,7 @@ function M.compile(bufnr, name, provider, ctx, opts) stop_open_watcher(bufnr) local watcher = vim.uv.new_fs_event() if watcher then - s.open_watcher = watcher + open_watchers[bufnr] = watcher watcher:start( out_dir, {}, @@ -293,12 +252,8 @@ function M.compile(bufnr, name, provider, ctx, opts) if err or vim.fn.fnamemodify(filename or '', ':t') ~= out_name then return end - local cs = state[bufnr] - if not cs then - return - end - if cs.viewer_open then - log.dbg('watcher fired for buffer %d but viewer already open', bufnr) + if opened[bufnr] then + stop_open_watcher(bufnr) return end if not vim.api.nvim_buf_is_valid(bufnr) then @@ -307,27 +262,41 @@ function M.compile(bufnr, name, provider, ctx, opts) end local new_stat = vim.uv.fs_stat(output_file) if not (new_stat and new_stat.mtime.sec > pre_mtime) then - log.dbg( - 'watcher fired for buffer %d but mtime not newer (%d <= %d)', - bufnr, - new_stat and new_stat.mtime.sec or 0, - pre_mtime - ) return end - log.dbg('watcher opening viewer for buffer %d', bufnr) - cs.viewer_open = true + stop_open_watcher(bufnr) stderr_acc = {} - clear_errors(bufnr, provider) + local errors_mode = provider.errors + if errors_mode == nil then + errors_mode = 'diagnostic' + end + if errors_mode == 'diagnostic' then + diagnostic.clear(bufnr) + elseif errors_mode == 'quickfix' then + vim.fn.setqflist({}, 'r') + vim.cmd.cwindow() + end do_open(bufnr, output_file, provider.open) + opened[bufnr] = true end) ) end end - s.process = obj - s.provider = name - s.is_reload = true + active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true } + + vim.api.nvim_create_autocmd('BufUnload', { + buffer = bufnr, + once = true, + callback = function() + M.stop(bufnr) + stop_open_watcher(bufnr) + if not provider.detach then + close_viewer(bufnr) + end + last_output[bufnr] = nil + end, + }) vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileStarted', @@ -340,27 +309,37 @@ function M.compile(bufnr, name, provider, ctx, opts) if provider.args then vim.list_extend(cmd, eval_list(provider.args, resolved_ctx)) end - if provider.extra_args then - vim.list_extend(cmd, eval_list(provider.extra_args, resolved_ctx)) - end log.dbg('compiling buffer %d with provider "%s": %s', bufnr, name, table.concat(cmd, ' ')) local obj obj = vim.system( cmd, - { cwd = cwd, env = provider.env }, + { + cwd = cwd, + env = provider.env, + }, vim.schedule_wrap(function(result) - local cs = state[bufnr] - if cs and cs.process == obj then - cs.process = nil + if active[bufnr] and active[bufnr].obj == obj then + active[bufnr] = nil end if not vim.api.nvim_buf_is_valid(bufnr) then return end + + local errors_mode = provider.errors + if errors_mode == nil then + errors_mode = 'diagnostic' + end + if result.code == 0 then log.dbg('compilation succeeded for buffer %d', bufnr) - clear_errors(bufnr, provider) + if errors_mode == 'diagnostic' then + diagnostic.clear(bufnr) + elseif errors_mode == 'quickfix' then + vim.fn.setqflist({}, 'r') + vim.cmd.cwindow() + end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileSuccess', data = { bufnr = bufnr, provider = name, output = output_file }, @@ -371,21 +350,42 @@ function M.compile(bufnr, name, provider, ctx, opts) r.inject(output_file) r.broadcast() end - cs = state[bufnr] if provider.open and not opts.oneshot - and cs - and not cs.viewer_open + and not opened[bufnr] and output_file ~= '' and vim.uv.fs_stat(output_file) then - cs.viewer_open = true do_open(bufnr, output_file, provider.open) + opened[bufnr] = true end else log.dbg('compilation failed for buffer %d (exit code %d)', bufnr, result.code) - handle_errors(bufnr, name, provider, ctx, (result.stdout or '') .. (result.stderr or '')) + if provider.error_parser and errors_mode then + local output = (result.stdout or '') .. (result.stderr or '') + if errors_mode == 'diagnostic' then + diagnostic.set(bufnr, name, provider.error_parser, output, ctx) + elseif errors_mode == 'quickfix' then + local ok, diagnostics = pcall(provider.error_parser, output, ctx) + if ok and diagnostics and #diagnostics > 0 then + local items = {} + for _, d in ipairs(diagnostics) do + table.insert(items, { + bufnr = bufnr, + lnum = d.lnum + 1, + col = d.col + 1, + text = d.message, + type = d.severity == vim.diagnostic.severity.WARN and 'W' or 'E', + }) + end + vim.fn.setqflist(items, 'r') + local win = vim.fn.win_getid() + vim.cmd.cwindow() + vim.fn.win_gotoid(win) + end + end + end vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileFailed', data = { @@ -399,9 +399,19 @@ function M.compile(bufnr, name, provider, ctx, opts) end) ) - s.process = obj - s.provider = name - s.is_reload = false + active[bufnr] = { obj = obj, provider = name, output_file = output_file } + + vim.api.nvim_create_autocmd('BufUnload', { + buffer = bufnr, + once = true, + callback = function() + M.stop(bufnr) + if not provider.detach then + close_viewer(bufnr) + end + last_output[bufnr] = nil + end, + }) vim.api.nvim_exec_autocmds('User', { pattern = 'PreviewCompileStarted', @@ -411,37 +421,39 @@ end ---@param bufnr integer function M.stop(bufnr) - local s = state[bufnr] - if not s then - return - end - local obj = s.process - if not obj then + local proc = active[bufnr] + if not proc then return end log.dbg('stopping process for buffer %d', bufnr) - obj:kill('sigterm') + ---@type fun(self: table, signal: string|integer) + local kill = proc.obj.kill + kill(proc.obj, 'sigterm') local timer = vim.uv.new_timer() if timer then timer:start(5000, 0, function() timer:close() - local cs = state[bufnr] - if cs and cs.process == obj then - obj:kill('sigkill') - cs.process = nil + if active[bufnr] and active[bufnr].obj == proc.obj then + kill(proc.obj, 'sigkill') + active[bufnr] = nil end end) end 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) - end - state[bufnr] = nil + for bufnr, _ in pairs(active) do + M.stop(bufnr) + end + for bufnr, _ in pairs(watching) do + M.unwatch(bufnr) + 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() end @@ -452,77 +464,76 @@ end ---@param ctx_builder fun(bufnr: integer): preview.Context function M.toggle(bufnr, name, provider, ctx_builder) local is_longrunning = type(provider.reload) == 'table' or type(provider.reload) == 'function' - local s = get_state(bufnr) - if s.watching then - local output = s.output - if not s.viewer_open and provider.open and output and vim.uv.fs_stat(output) then - log.dbg('toggle reopen viewer for buffer %d', bufnr) - s.viewer_open = true - do_open(bufnr, output, provider.open) - else - log.dbg('toggle off for buffer %d', bufnr) - stop_watching(bufnr, s) + if is_longrunning then + if active[bufnr] then + M.stop(bufnr) vim.notify('[preview.nvim]: watching stopped', vim.log.levels.INFO) + else + M.compile(bufnr, name, provider, ctx_builder(bufnr)) + vim.notify('[preview.nvim]: watching with "' .. name .. '"', vim.log.levels.INFO) end return end - 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 watching[bufnr] then + M.unwatch(bufnr) + vim.notify('[preview.nvim]: watching stopped', vim.log.levels.INFO) + return end - s.unload_autocmd = vim.api.nvim_create_autocmd('BufUnload', { + + local au_id = vim.api.nvim_create_autocmd('BufWritePost', { + buffer = bufnr, + callback = function() + if debounce_timers[bufnr] then + debounce_timers[bufnr]:stop() + else + debounce_timers[bufnr] = vim.uv.new_timer() + end + debounce_timers[bufnr]:start( + DEBOUNCE_MS, + 0, + vim.schedule_wrap(function() + local ctx = ctx_builder(bufnr) + M.compile(bufnr, name, provider, ctx) + end) + ) + end, + }) + + watching[bufnr] = au_id + 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('BufUnload', { buffer = bufnr, once = true, callback = function() - M.stop(bufnr) + M.unwatch(bufnr) stop_open_watcher(bufnr) if not provider.detach then close_viewer(bufnr) end - state[bufnr] = nil + opened[bufnr] = nil end, }) - if not is_longrunning then - s.bwp_autocmd = vim.api.nvim_create_autocmd('BufWritePost', { - buffer = bufnr, - callback = function() - local ds = state[bufnr] - if not ds then - return - end - if ds.debounce then - ds.debounce:stop() - else - ds.debounce = vim.uv.new_timer() - end - ds.debounce:start( - DEBOUNCE_MS, - 0, - vim.schedule_wrap(function() - M.compile(bufnr, name, provider, ctx_builder(bufnr)) - end) - ) - end, - }) - log.dbg('watching buffer %d with provider "%s"', bufnr, name) - end - - vim.notify('[preview.nvim]: watching with "' .. name .. '"', vim.log.levels.INFO) M.compile(bufnr, name, provider, ctx_builder(bufnr)) end ---@param bufnr integer function M.unwatch(bufnr) - local s = state[bufnr] - if not s then + local au_id = watching[bufnr] + if not au_id then return end - stop_watching(bufnr, s) + vim.api.nvim_del_autocmd(au_id) + if debounce_timers[bufnr] then + debounce_timers[bufnr]:stop() + debounce_timers[bufnr]:close() + debounce_timers[bufnr] = nil + end + watching[bufnr] = nil log.dbg('unwatched buffer %d', bufnr) end @@ -571,8 +582,7 @@ end ---@param bufnr integer ---@return boolean function M.open(bufnr, open_config) - local s = state[bufnr] - local output = s and s.output + local output = last_output[bufnr] if not output then log.dbg('no last output file for buffer %d', bufnr) return false @@ -588,20 +598,26 @@ end ---@param bufnr integer ---@return preview.Status function M.status(bufnr) - local s = state[bufnr] - if not s then - return { compiling = false, watching = false } + local proc = active[bufnr] + if proc then + return { + compiling = not proc.is_reload, + watching = watching[bufnr] ~= nil or proc.is_reload == true, + provider = proc.provider, + output_file = proc.output_file, + } end - return { - compiling = s.process ~= nil and not s.is_reload, - watching = s.watching, - provider = s.provider, - output_file = s.output, - } + return { compiling = false, watching = watching[bufnr] ~= nil } end M._test = { - state = state, + active = active, + watching = watching, + opened = opened, + last_output = last_output, + debounce_timers = debounce_timers, + viewer_procs = viewer_procs, + open_watchers = open_watchers, } return M diff --git a/lua/preview/init.lua b/lua/preview/init.lua index 489679f..7cb982b 100644 --- a/lua/preview/init.lua +++ b/lua/preview/init.lua @@ -2,7 +2,6 @@ ---@field ft? string ---@field cmd string[] ---@field args? string[]|fun(ctx: preview.Context): string[] ----@field extra_args? string[]|fun(ctx: preview.Context): string[] ---@field cwd? string|fun(ctx: preview.Context): string ---@field env? table ---@field output? string|fun(ctx: preview.Context): string @@ -95,7 +94,6 @@ function M.setup(opts) vim.validate(prefix .. '.cmd', provider.cmd, 'table') vim.validate(prefix .. '.cmd[1]', provider.cmd[1], 'string') vim.validate(prefix .. '.args', provider.args, { 'table', 'function' }, true) - vim.validate(prefix .. '.extra_args', provider.extra_args, { 'table', 'function' }, true) vim.validate(prefix .. '.cwd', provider.cwd, { 'string', 'function' }, true) vim.validate(prefix .. '.output', provider.output, { 'string', 'function' }, true) vim.validate(prefix .. '.error_parser', provider.error_parser, 'function', true) diff --git a/lua/preview/presets.lua b/lua/preview/presets.lua index d4b03cc..1b5333e 100644 --- a/lua/preview/presets.lua +++ b/lua/preview/presets.lua @@ -115,24 +115,6 @@ local function parse_asciidoctor(output) return diagnostics end ----@param output string ----@return preview.Diagnostic[] -local function parse_mermaid(output) - local lnum = output:match('Parse error on line (%d+)') - if not lnum then - return {} - end - local msg = output:match('(Expecting .+)') or 'parse error' - return { - { - lnum = tonumber(lnum) - 1, - col = 0, - message = msg, - severity = vim.diagnostic.severity.ERROR, - }, - } -end - ---@type preview.ProviderConfig M.typst = { ft = 'typst', @@ -331,7 +313,19 @@ M.mermaid = { return (ctx.file:gsub('%.mmd$', '.svg')) end, error_parser = function(output) - return parse_mermaid(output) + local diagnostics = {} + for line in output:gmatch('[^\r\n]+') do + local lnum = line:match('^%s*Parse error on line (%d+)') + if lnum then + table.insert(diagnostics, { + lnum = tonumber(lnum) - 1, + col = 0, + message = line, + severity = vim.diagnostic.severity.ERROR, + }) + end + end + return diagnostics end, clean = function(ctx) return { 'rm', '-f', (ctx.file:gsub('%.mmd$', '.svg')) } diff --git a/spec/compiler_spec.lua b/spec/compiler_spec.lua index 5b12cfb..cd1dd9f 100644 --- a/spec/compiler_spec.lua +++ b/spec/compiler_spec.lua @@ -8,13 +8,8 @@ describe('compiler', function() compiler = require('preview.compiler') end) - local function process_done(bufnr) - local s = compiler._test.state[bufnr] - return not s or s.process == nil - end - describe('compile', function() - it('spawns a process and tracks it in state', function() + it('spawns a process and tracks it in active table', function() local bufnr = helpers.create_buffer({ 'hello' }, 'text') vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test.txt') vim.bo[bufnr].modified = false @@ -28,16 +23,15 @@ describe('compiler', function() } compiler.compile(bufnr, 'echo', provider, ctx) - local s = compiler._test.state[bufnr] - assert.is_not_nil(s) - assert.is_not_nil(s.process) - assert.are.equal('echo', s.provider) + local active = compiler._test.active + assert.is_not_nil(active[bufnr]) + assert.are.equal('echo', active[bufnr].provider) vim.wait(2000, function() - return process_done(bufnr) + return active[bufnr] == nil end, 50) - assert.is_nil(compiler._test.state[bufnr].process) + assert.is_nil(active[bufnr]) helpers.delete_buffer(bufnr) end) @@ -67,7 +61,7 @@ describe('compiler', function() assert.is_true(fired) vim.wait(2000, function() - return process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) helpers.delete_buffer(bufnr) @@ -130,7 +124,7 @@ describe('compiler', function() vim.notify = orig assert.is_true(notified) - assert.is_true(process_done(bufnr)) + assert.is_nil(compiler._test.active[bufnr]) helpers.delete_buffer(bufnr) end) @@ -192,7 +186,7 @@ describe('compiler', function() compiler.compile(bufnr, 'falsecmd', provider, ctx) vim.wait(2000, function() - return process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) assert.is_false(parser_called) @@ -224,7 +218,7 @@ describe('compiler', function() compiler.compile(bufnr, 'qfcmd', provider, ctx) vim.wait(2000, function() - return process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) local qflist = vim.fn.getqflist() @@ -261,7 +255,7 @@ describe('compiler', function() compiler.compile(bufnr, 'truecmd', provider, ctx) vim.wait(2000, function() - return process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) assert.are.equal(0, #vim.fn.getqflist()) @@ -304,7 +298,7 @@ describe('compiler', function() compiler.stop(bufnr) vim.wait(2000, function() - return process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) helpers.delete_buffer(bufnr) @@ -335,12 +329,11 @@ describe('compiler', function() } compiler.compile(bufnr, 'testprov', provider, ctx) - local s = compiler._test.state[bufnr] - assert.is_not_nil(s) - assert.are.equal('/tmp/preview_test_open.pdf', s.output) + 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 process_done(bufnr) + return compiler._test.active[bufnr] == nil end, 50) helpers.delete_buffer(bufnr) @@ -348,7 +341,7 @@ describe('compiler', function() end) describe('toggle', function() - it('starts watching and sets watching flag', function() + it('registers autocmd and tracks in watching table', function() local bufnr = helpers.create_buffer({ 'hello' }, 'text') vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch.txt') @@ -358,7 +351,7 @@ describe('compiler', function() end compiler.toggle(bufnr, 'echo', provider, ctx_builder) - assert.is_true(compiler.status(bufnr).watching) + assert.is_not_nil(compiler._test.watching[bufnr]) helpers.delete_buffer(bufnr) end) @@ -373,10 +366,10 @@ describe('compiler', function() end compiler.toggle(bufnr, 'echo', provider, ctx_builder) - assert.is_true(compiler.status(bufnr).watching) + assert.is_not_nil(compiler._test.watching[bufnr]) compiler.toggle(bufnr, 'echo', provider, ctx_builder) - assert.is_false(compiler.status(bufnr).watching) + assert.is_nil(compiler._test.watching[bufnr]) helpers.delete_buffer(bufnr) end) @@ -396,10 +389,10 @@ describe('compiler', function() end compiler.toggle(bufnr, 'echo', provider, ctx_builder) - assert.is_true(compiler.status(bufnr).watching) + assert.is_not_nil(compiler._test.watching[bufnr]) compiler.stop_all() - assert.is_false(compiler.status(bufnr).watching) + assert.is_nil(compiler._test.watching[bufnr]) helpers.delete_buffer(bufnr) end)