Compare commits

..

4 commits

Author SHA1 Message Date
c12891f7ea
style(compiler): reformat long condition for stylua 2026-03-04 00:20:10 -05:00
Barrett Ruth
88dc47b643
Merge branch 'main' into fix/compiler-longrunning-open 2026-03-04 00:17:17 -05:00
Barrett Ruth
a5ebccc292
Merge branch 'main' into fix/compiler-longrunning-open 2026-03-03 18:00:06 -05:00
02958158c0
fix(compiler): check output exists before opening from long-running process
Problem: for long-running processes (typst watch), M.compile() calls
the opener immediately after vim.system() returns, before the process
has produced any output. On first run the output file does not exist
yet, so the opener is called on a nonexistent path.

Solution: guard the open block with vim.uv.fs_stat so it only fires
if the output file already exists at spawn time.
2026-03-03 17:50:36 -05:00
16 changed files with 340 additions and 1086 deletions

View file

@ -4,6 +4,5 @@
"diagnostics.globals": ["vim", "jit"],
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
"workspace.checkThirdParty": false,
"workspace.ignoreDir": [".direnv"],
"completion.callSnippet": "Replace"
}

View file

@ -1 +0,0 @@
.direnv/

View file

@ -1,19 +1,17 @@
# preview.nvim
**Universal document previewer for Neovim**
**Async document compilation for Neovim**
An extensible framework for compiling and previewing _any_ documents (LaTeX,
Typst, Markdown, etc.)—diagnostics included.
<video src="https://github.com/user-attachments/assets/3b4fbc31-c1c4-4429-a9dc-a68d6185ab2e" width="100%" controls></video>
An extensible framework for compiling documents (LaTeX, Typst, Markdown, etc.)
asynchronously with error diagnostics.
## Features
- Async compilation via `vim.system()`
- Built-in presets for Typst, LaTeX (latexmk, pdflatex, tectonic), Markdown,
GitHub-flavored Markdown, AsciiDoc, PlantUML, Mermaid, and Quarto
- Compiler errors via `vim.diagnostic` or quickfix
- Previewer auto-close on buffer deletion
- Built-in presets for Typst, LaTeX, Markdown, and GitHub-flavored Markdown
- Compiler errors as native `vim.diagnostic`
- User events for extensibility (`PreviewCompileStarted`,
`PreviewCompileSuccess`, `PreviewCompileFailed`)
## Requirements
@ -21,18 +19,8 @@ Typst, Markdown, etc.)&mdash;diagnostics included.
## Installation
With lazy.nvim:
```lua
{
'barrettruth/preview.nvim',
init = function()
vim.g.preview = { typst = true, latex = true }
end,
}
```
Or via [luarocks](https://luarocks.org/modules/barrettruth/preview.nvim):
Install with your package manager of choice or via
[luarocks](https://luarocks.org/modules/barrettruth/preview.nvim):
```
luarocks install preview.nvim
@ -49,35 +37,35 @@ luarocks install preview.nvim
**Q: How do I define a custom provider?**
```lua
vim.g.preview = {
rst = {
cmd = { 'rst2html' },
require('preview').setup({
typst = {
cmd = { 'typst', 'compile' },
args = function(ctx)
return { ctx.file, ctx.output }
return { ctx.file }
end,
output = function(ctx)
return ctx.file:gsub('%.rst$', '.html')
return ctx.file:gsub('%.typ$', '.pdf')
end,
},
}
})
```
**Q: How do I override a preset?**
```lua
vim.g.preview = {
require('preview').setup({
typst = { env = { TYPST_FONT_PATHS = '/usr/share/fonts' } },
}
})
```
**Q: How do I automatically open the output file?**
Set `open = true` on your provider (all built-in presets have this enabled) to
open the output with `vim.ui.open()` after the first successful compilation in
toggle/watch mode. For a specific application, pass a command table:
open the output with `vim.ui.open()` after the first successful compilation. For
a specific application, pass a command table:
```lua
vim.g.preview = {
require('preview').setup({
typst = { open = { 'sioyek', '--new-instance' } },
}
})
```

244
doc/preview.nvim.txt Normal file
View file

@ -0,0 +1,244 @@
*preview.nvim.txt* Async document compilation for Neovim
Author: Barrett Ruth <br.barrettruth@gmail.com>
License: MIT
==============================================================================
INTRODUCTION *preview.nvim*
preview.nvim is an extensible framework for compiling documents asynchronously
in Neovim. It provides a unified interface for any compilation workflow —
LaTeX, Typst, Markdown, or anything else with a CLI compiler.
The plugin ships with zero provider defaults. Users must explicitly configure
their compiler commands. preview.nvim is purely an orchestration framework.
==============================================================================
REQUIREMENTS *preview.nvim-requirements*
- Neovim >= 0.11.0
- A compiler binary for each configured provider (e.g. `typst`, `latexmk`)
==============================================================================
SETUP *preview.nvim-setup*
Load preview.nvim with your package manager. For example, with lazy.nvim: >lua
{
'barrettruth/preview.nvim',
}
<
Call |preview.setup()| to configure providers before use.
==============================================================================
CONFIGURATION *preview.nvim-configuration*
Configure via `require('preview').setup()`.
*preview.setup()*
setup({opts?})
`opts` is a table where keys are preset names or filetypes. For each
key `k` with value `v` (excluding `debug`):
- If `k` is a preset name and `v` is `true`, the preset is registered
as-is under its filetype.
- If `k` is a preset name and `v` is a table, it is deep-merged with
the preset and registered under the preset's filetype.
- If `k` is not a preset name and `v` is a table, it is registered
directly as a custom provider keyed by filetype `k`.
- If `v` is `false`, the entry is skipped (no-op).
See |preview.nvim-presets| for available preset names.
Fields:~
`debug` boolean|string Enable debug logging. A string value
is treated as a log file path.
Default: `false`
*preview.ProviderConfig*
Provider fields:~
`cmd` string[] The compiler command (required).
`args` string[]|function Additional arguments. If a function,
receives a |preview.Context| and returns
a string[].
`cwd` string|function Working directory. If a function,
receives a |preview.Context|. Default:
git root or file directory.
`env` table Environment variables.
`output` string|function Output file path. If a function,
receives a |preview.Context|.
`error_parser` function Receives (output, |preview.Context|)
and returns vim.Diagnostic[].
`errors` false|'diagnostic'|'quickfix'
How parse errors are reported.
`false` suppresses error handling.
`'quickfix'` populates the quickfix
list and opens it. Default:
`'diagnostic'`.
`clean` string[]|function Command to remove build artifacts.
If a function, receives a
|preview.Context|.
`open` boolean|string[] Open the output file after the first
successful compilation. `true` uses
|vim.ui.open()|. A string[] is run as
a command with the output path appended.
*preview.Context*
Context fields:~
`bufnr` integer Buffer number.
`file` string Absolute file path.
`root` string Project root (git root or file directory).
`ft` string Filetype.
`output` string? Resolved output file path (set after `output`
is evaluated, available to `args` functions).
Example enabling presets:~
>lua
require('preview').setup({ typst = true, latex = true, github = true })
<
Example overriding a preset field:~
>lua
require('preview').setup({
typst = { open = { 'sioyek', '--new-instance' } },
})
<
Example with a fully custom provider (key is not a preset name):~
>lua
require('preview').setup({
rst = {
cmd = { 'rst2html' },
args = function(ctx)
return { ctx.file }
end,
output = function(ctx)
return ctx.file:gsub('%.rst$', '.html')
end,
},
})
<
==============================================================================
PRESETS *preview.nvim-presets*
preview.nvim ships with pre-built provider configurations for common tools.
Import them from `preview.presets`:
`presets.typst` typst compile → PDF
`presets.latex` latexmk -pdf → PDF (with clean support)
`presets.markdown` pandoc → HTML (standalone, embedded)
`presets.github` pandoc → HTML (GitHub-styled, `-f gfm` input)
Enable presets with `preset_name = true`:
>lua
require('preview').setup({ typst = true, latex = true, github = true })
<
Override individual fields by passing a table instead of `true`:
>lua
require('preview').setup({
typst = { env = { TYPST_FONT_PATHS = '/usr/share/fonts' } },
})
<
==============================================================================
COMMANDS *preview.nvim-commands*
:Preview [subcommand] *:Preview*
Subcommands:~
`compile` Compile the current buffer (default if omitted).
`stop` Kill active compilation for the current buffer.
`clean` Run the provider's clean command.
`toggle` Toggle auto-compile on save for the current buffer.
`open` Open the last compiled output without recompiling.
`status` Echo compilation status (idle, compiling, watching).
==============================================================================
API *preview.nvim-api*
preview.compile({bufnr?}) *preview.compile()*
Compile the document in the given buffer (default: current).
preview.stop({bufnr?}) *preview.stop()*
Kill the active compilation process for the buffer.
preview.clean({bufnr?}) *preview.clean()*
Run the provider's clean command for the buffer.
preview.toggle({bufnr?}) *preview.toggle()*
Toggle auto-compile for the buffer. When enabled, the buffer is
immediately compiled and automatically recompiled on each save
(`BufWritePost`). Call again to stop.
preview.open({bufnr?}) *preview.open()*
Open the last compiled output for the buffer without recompiling.
preview.status({bufnr?}) *preview.status()*
Returns a |preview.Status| table.
preview.statusline({bufnr?}) *preview.statusline()*
Returns a short status string for statusline integration:
`'compiling'`, `'watching'`, or `''` (idle).
*preview.Status*
Status fields:~
`compiling` boolean Whether compilation is active.
`watching` boolean Whether auto-compile is active.
`provider` string? Name of the active provider.
`output_file` string? Path to the output file.
preview.get_config() *preview.get_config()*
Returns the resolved |preview.Config|.
==============================================================================
EVENTS *preview.nvim-events*
preview.nvim fires User autocmds with structured data:
`PreviewCompileStarted` Compilation began.
data: `{ bufnr, provider }`
`PreviewCompileSuccess` Compilation succeeded (exit code 0).
data: `{ bufnr, provider, output }`
`PreviewCompileFailed` Compilation failed (non-zero exit).
data: `{ bufnr, provider, code, stderr }`
Example:~
>lua
vim.api.nvim_create_autocmd('User', {
pattern = 'PreviewCompileSuccess',
callback = function(args)
local data = args.data
vim.notify('Compiled ' .. data.output .. ' with ' .. data.provider)
end,
})
<
==============================================================================
HEALTH *preview.nvim-health*
Run `:checkhealth preview` to verify:
- Neovim version >= 0.11.0
- Each configured provider's binary is executable
- Each configured provider's opener binary (if any) is executable
- Each configured provider's filetype mapping is valid
==============================================================================
vim:tw=78:ts=8:ft=help:norl:

View file

@ -1,276 +0,0 @@
*preview.txt* Async document compilation for Neovim
Author: Barrett Ruth <br.barrettruth@gmail.com>
License: MIT
==============================================================================
INTRODUCTION *preview.nvim*
preview.nvim is an extensible framework for compiling documents asynchronously
in Neovim. It provides a unified interface for any compilation workflow —
LaTeX, Typst, Markdown, or anything else with a CLI compiler.
The plugin ships with opt-in presets for common tools (Typst, LaTeX, Pandoc,
AsciiDoc, PlantUML, Mermaid, Quarto) and supports fully custom providers.
See |preview-presets|.
==============================================================================
CONTENTS *preview-contents*
1. Introduction ............................................. |preview.nvim|
2. Requirements ..................................... |preview-requirements|
3. Install ............................................... |preview-install|
4. Configuration ........................................... |preview-config|
5. Presets ............................................... |preview-presets|
6. Commands ............................................. |preview-commands|
7. Lua API ................................................... |preview-api|
8. Events ............................................... |preview-events|
9. Health ............................................... |preview-health|
==============================================================================
REQUIREMENTS *preview-requirements*
- Neovim >= 0.11.0
- A compiler binary for each configured provider (e.g. `typst`, `latexmk`)
==============================================================================
INSTALL *preview-install*
Install with lazy.nvim: >lua
{ 'barrettruth/preview.nvim' }
<
No `setup()` call is needed. The plugin loads automatically when
|vim.g.preview| is set. See |preview-config|.
==============================================================================
CONFIGURATION *preview-config*
Configure by setting |vim.g.preview| to a table where keys are preset names
or filetypes. For each key `k` with value `v` (excluding `debug`):
- If `k` is a preset name and `v` is `true`, the preset is registered
as-is under its filetype.
- If `k` is a preset name and `v` is a table, it is deep-merged with
the preset and registered under the preset's filetype.
- If `k` is not a preset name and `v` is a table, it is registered
directly as a custom provider keyed by filetype `k`.
- If `v` is `false`, the entry is skipped (no-op).
See |preview-presets| for available preset names.
*preview.ProviderConfig*
Provider fields: ~
{cmd} (string[]) Compiler command (required).
{args} (string[]|function) Additional arguments. If a function,
receives a |preview.Context| and
returns a string[].
{cwd} (string|function) Working directory. If a function,
receives a |preview.Context|.
Default: git root or file directory.
{env} (table) Environment variables.
{output} (string|function) Output file path. If a function,
receives a |preview.Context|.
{error_parser} (function) Receives (output, |preview.Context|)
and returns vim.Diagnostic[].
{errors} (false|'diagnostic'|'quickfix')
How parse errors are reported.
`false` suppresses error handling.
`'quickfix'` populates the quickfix
list and opens it.
Default: `'diagnostic'`.
{clean} (string[]|function) Command to remove build artifacts.
If a function, receives a
|preview.Context|.
{open} (boolean|string[]) Open the output file after the first
successful compilation in toggle/watch
mode. `true` uses |vim.ui.open()|. A
string[] is run as a command with the
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 the output after recompilation.
`true` uses a built-in SSE server for
HTML files. A string[] is run as a
command. If a function, receives a
|preview.Context| and returns a
string[].
{detach} (boolean) When `true`, the viewer process opened
via a string[] `open` command is not
sent SIGTERM when the buffer is
deleted. Has no effect when `open` is
`true`. Default: `false`.
*preview.Context*
Context fields: ~
{bufnr} (integer) Buffer number.
{file} (string) Absolute file path.
{root} (string) Project root (git root or file directory).
{ft} (string) Filetype.
{output} (string?) Resolved output file path (set after `output`
is evaluated, available to `args` functions).
Global options: ~
{debug} (boolean|string) Enable debug logging. A string value is treated
as a log file path. Default: `false`.
Example enabling presets: >lua
vim.g.preview = { typst = true, latex = true, github = true }
<
Example overriding a preset field: >lua
vim.g.preview = {
typst = { open = { 'sioyek', '--new-instance' } },
}
<
Example overriding the output path (e.g. latexmk `$out_dir`): >lua
vim.g.preview = {
latex = {
output = function(ctx)
return 'build/' .. vim.fn.fnamemodify(ctx.file, ':t:r') .. '.pdf'
end,
},
}
<
Example with a fully custom provider (key is not a preset name): >lua
vim.g.preview = {
rst = {
cmd = { 'rst2html' },
args = function(ctx)
return { ctx.file }
end,
output = function(ctx)
return ctx.file:gsub('%.rst$', '.html')
end,
},
}
<
==============================================================================
PRESETS *preview-presets*
Built-in provider configurations. Enable with `preset_name = true` or
override individual fields by passing a table instead: >lua
vim.g.preview = { typst = true, latex = true, github = true }
<
`typst` typst compile → PDF
`latex` latexmk -pdf → PDF (with clean support)
`pdflatex` pdflatex → PDF (single pass, no latexmk)
`tectonic` tectonic → PDF (Rust-based LaTeX engine)
`markdown` pandoc → HTML (standalone, embedded)
`github` pandoc → HTML (GitHub-styled, `-f gfm` input)
`asciidoctor` asciidoctor → HTML (AsciiDoc with SSE reload)
`plantuml` plantuml → SVG (UML diagrams, `.puml`)
`mermaid` mmdc → SVG (Mermaid diagrams, `.mmd`)
`quarto` quarto render → HTML (scientific publishing)
==============================================================================
COMMANDS *preview-commands*
:Preview [subcommand] *:Preview*
Subcommands: ~
`toggle` Toggle auto-compile on save (default if omitted).
`compile` One-shot compile of the current buffer.
`clean` Run the provider's clean command.
`open` Open the last compiled output without recompiling.
`status` Echo compilation status (idle, compiling, watching).
==============================================================================
LUA API *preview-api*
preview.toggle({bufnr?}) *preview.toggle()*
Toggle auto-compile for the buffer. When enabled, the buffer is
immediately compiled and automatically recompiled on each save
(`BufWritePost`). Call again to stop.
preview.compile({bufnr?}) *preview.compile()*
One-shot compile the document in the given buffer (default: current).
preview.stop({bufnr?}) *preview.stop()*
Kill the active compilation process for the buffer. Programmatic
escape hatch — not exposed as a subcommand.
preview.clean({bufnr?}) *preview.clean()*
Run the provider's clean command for the buffer.
preview.open({bufnr?}) *preview.open()*
Open the last compiled output for the buffer without recompiling.
preview.status({bufnr?}) *preview.status()*
Returns a |preview.Status| table.
preview.statusline({bufnr?}) *preview.statusline()*
Returns a short status string for statusline integration:
`'compiling'`, `'watching'`, or `''` (idle).
preview.get_config() *preview.get_config()*
Returns the resolved |preview.Config|.
*preview.Status*
Status fields: ~
{compiling} (boolean) Whether compilation is active.
{watching} (boolean) Whether auto-compile is active.
{provider} (string?) Name of the active provider.
{output_file} (string?) Path to the output file.
==============================================================================
EVENTS *preview-events*
preview.nvim fires User autocmds with structured data:
`PreviewCompileStarted` Compilation began.
data: `{ bufnr, provider }`
`PreviewCompileSuccess` Compilation succeeded (exit code 0).
data: `{ bufnr, provider, output }`
`PreviewCompileFailed` Compilation failed (non-zero exit).
data: `{ bufnr, provider, code, stderr }`
Example: >lua
vim.api.nvim_create_autocmd('User', {
pattern = 'PreviewCompileSuccess',
callback = function(args)
local data = args.data
vim.notify('Compiled ' .. data.output .. ' with ' .. data.provider)
end,
})
<
==============================================================================
HEALTH *preview-health*
Run |:checkhealth| preview to verify your setup: >vim
:checkhealth preview
<
Checks: ~
- Neovim version >= 0.11.0
- Each configured provider's binary is executable
- Each configured provider's opener binary (if any) is executable
==============================================================================
vim:tw=78:ts=8:ft=help:norl:

View file

@ -13,15 +13,12 @@
...
}:
let
forEachSystem =
f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
forEachSystem = f: nixpkgs.lib.genAttrs (import systems) (system: f nixpkgs.legacyPackages.${system});
in
{
formatter = forEachSystem (pkgs: pkgs.nixfmt-tree);
devShells = forEachSystem (pkgs:
let
devTools = [
devShells = forEachSystem (pkgs: {
default = pkgs.mkShell {
packages = [
(pkgs.luajit.withPackages (
ps: with ps; [
busted
@ -33,23 +30,7 @@
pkgs.selene
pkgs.lua-language-server
];
in
{
default = pkgs.mkShell {
packages = devTools;
};
presets = pkgs.mkShell {
packages = devTools ++ [
pkgs.typst
pkgs.texliveMedium
pkgs.tectonic
pkgs.pandoc
pkgs.asciidoctor
pkgs.quarto
pkgs.plantuml
pkgs.mermaid-cli
];
};
});
};
});
};
}

View file

@ -1,14 +1,17 @@
local M = {}
local handlers = {
compile = function()
require('preview').compile()
build = function()
require('preview').build()
end,
stop = function()
require('preview').stop()
end,
clean = function()
require('preview').clean()
end,
toggle = function()
require('preview').toggle()
watch = function()
require('preview').watch()
end,
open = function()
require('preview').open()
@ -30,7 +33,7 @@ local handlers = {
---@param args string
local function dispatch(args)
local subcmd = args ~= '' and args or 'toggle'
local subcmd = args ~= '' and args or 'build'
local handler = handlers[subcmd]
if handler then
handler()
@ -55,13 +58,7 @@ function M.setup()
complete = function(lead)
return complete(lead)
end,
desc = 'Toggle, compile, clean, open, or check status of document preview',
})
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
require('preview.compiler').stop_all()
end,
desc = 'Build, stop, clean, watch, open, or check status of document preview',
})
end

View file

@ -15,49 +15,10 @@ local opened = {}
---@type table<integer, string>
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_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 ctx preview.Context
---@return string[]
@ -94,17 +55,7 @@ end
---@param name string
---@param provider preview.ProviderConfig
---@param ctx preview.Context
function M.compile(bufnr, name, provider, ctx, opts)
opts = opts or {}
if vim.fn.executable(provider.cmd[1]) ~= 1 then
vim.notify(
'[preview.nvim]: "' .. provider.cmd[1] .. '" is not executable (run :checkhealth preview)',
vim.log.levels.ERROR
)
return
end
function M.compile(bufnr, name, provider, ctx)
if vim.bo[bufnr].modified then
vim.cmd('silent! update')
end
@ -130,10 +81,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
last_output[bufnr] = output_file
end
local reload_cmd
if not opts.oneshot then
reload_cmd = resolve_reload_cmd(provider, resolved_ctx)
end
local reload_cmd = resolve_reload_cmd(provider, resolved_ctx)
if reload_cmd then
log.dbg(
@ -143,47 +91,12 @@ function M.compile(bufnr, name, provider, ctx, opts)
table.concat(reload_cmd, ' ')
)
local stderr_acc = {}
local obj
obj = vim.system(
reload_cmd,
{
cwd = cwd,
env = provider.env,
stderr = vim.schedule_wrap(function(_err, data)
if not data or not vim.api.nvim_buf_is_valid(bufnr) then
return
end
stderr_acc[#stderr_acc + 1] = data
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)
if active[bufnr] and active[bufnr].obj == obj then
@ -217,9 +130,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
})
end
vim.fn.setqflist(items, 'r')
local win = vim.fn.win_getid()
vim.cmd.cwindow()
vim.fn.win_gotoid(win)
vim.cmd('copen')
end
end
end
@ -236,64 +147,29 @@ function M.compile(bufnr, name, provider, ctx, opts)
end)
)
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')
local out_name = vim.fn.fnamemodify(output_file, ':t')
stop_open_watcher(bufnr)
local watcher = vim.uv.new_fs_event()
if watcher then
open_watchers[bufnr] = watcher
watcher:start(
out_dir,
{},
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)
stderr_acc = {}
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)
)
if
provider.open
and not opened[bufnr]
and output_file ~= ''
and vim.uv.fs_stat(output_file)
then
if provider.open == true then
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
end
active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true }
vim.api.nvim_create_autocmd('BufUnload', {
vim.api.nvim_create_autocmd('BufWipeout', {
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,
})
@ -338,7 +214,6 @@ function M.compile(bufnr, name, provider, ctx, opts)
diagnostic.clear(bufnr)
elseif errors_mode == 'quickfix' then
vim.fn.setqflist({}, 'r')
vim.cmd.cwindow()
end
vim.api.nvim_exec_autocmds('User', {
pattern = 'PreviewCompileSuccess',
@ -350,14 +225,14 @@ function M.compile(bufnr, name, provider, ctx, opts)
r.inject(output_file)
r.broadcast()
end
if
provider.open
and not opts.oneshot
and not opened[bufnr]
and output_file ~= ''
and vim.uv.fs_stat(output_file)
then
do_open(bufnr, output_file, provider.open)
if provider.open and not opened[bufnr] and output_file ~= '' then
if provider.open == true then
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
end
else
@ -380,9 +255,7 @@ function M.compile(bufnr, name, provider, ctx, opts)
})
end
vim.fn.setqflist(items, 'r')
local win = vim.fn.win_getid()
vim.cmd.cwindow()
vim.fn.win_gotoid(win)
vim.cmd('copen')
end
end
end
@ -401,14 +274,11 @@ function M.compile(bufnr, name, provider, ctx, opts)
active[bufnr] = { obj = obj, provider = name, output_file = output_file }
vim.api.nvim_create_autocmd('BufUnload', {
vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
once = true,
callback = function()
M.stop(bufnr)
if not provider.detach then
close_viewer(bufnr)
end
last_output[bufnr] = nil
end,
})
@ -449,12 +319,6 @@ function M.stop_all()
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
@ -505,15 +369,11 @@ function M.toggle(bufnr, name, provider, ctx_builder)
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', {
vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
once = true,
callback = function()
M.unwatch(bufnr)
stop_open_watcher(bufnr)
if not provider.detach then
close_viewer(bufnr)
end
opened[bufnr] = nil
end,
})
@ -543,10 +403,7 @@ end
---@param ctx preview.Context
function M.clean(bufnr, name, provider, ctx)
if not provider.clean then
vim.notify(
'[preview.nvim]: provider "' .. name .. '" has no clean command',
vim.log.levels.WARN
)
vim.notify('[preview.nvim] provider "' .. name .. '" has no clean command', vim.log.levels.WARN)
return
end
@ -570,10 +427,10 @@ function M.clean(bufnr, name, provider, ctx)
vim.schedule_wrap(function(result)
if result.code == 0 then
log.dbg('clean succeeded for buffer %d', bufnr)
vim.notify('[preview.nvim]: clean complete', vim.log.levels.INFO)
vim.notify('[preview.nvim] clean complete', vim.log.levels.INFO)
else
log.dbg('clean failed for buffer %d (exit code %d)', bufnr, result.code)
vim.notify('[preview.nvim]: clean failed: ' .. (result.stderr or ''), vim.log.levels.ERROR)
vim.notify('[preview.nvim] clean failed: ' .. (result.stderr or ''), vim.log.levels.ERROR)
end
end)
)
@ -581,17 +438,13 @@ end
---@param bufnr integer
---@return boolean
function M.open(bufnr, open_config)
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
if not vim.uv.fs_stat(output) then
log.dbg('output file no longer exists for buffer %d: %s', bufnr, output)
return false
end
do_open(bufnr, output, open_config)
vim.ui.open(output)
return true
end
@ -616,8 +469,6 @@ M._test = {
opened = opened,
last_output = last_output,
debounce_timers = debounce_timers,
viewer_procs = viewer_procs,
open_watchers = open_watchers,
}
return M

View file

@ -10,7 +10,6 @@
---@field clean? string[]|fun(ctx: preview.Context): string[]
---@field open? boolean|string[]
---@field reload? boolean|string[]|fun(ctx: preview.Context): string[]
---@field detach? boolean
---@class preview.Config
---@field debug boolean|string
@ -40,10 +39,10 @@
---@class preview
---@field setup fun(opts?: table)
---@field compile fun(bufnr?: integer)
---@field build fun(bufnr?: integer)
---@field stop fun(bufnr?: integer)
---@field clean fun(bufnr?: integer)
---@field toggle fun(bufnr?: integer)
---@field watch fun(bufnr?: integer)
---@field open fun(bufnr?: integer)
---@field status fun(bufnr?: integer): preview.Status
---@field statusline fun(bufnr?: integer): string
@ -102,13 +101,6 @@ function M.setup(opts)
end, 'false, "diagnostic", or "quickfix"')
vim.validate(prefix .. '.open', provider.open, { 'boolean', 'table' }, true)
vim.validate(prefix .. '.reload', provider.reload, { 'boolean', 'table', 'function' }, true)
vim.validate(prefix .. '.detach', provider.detach, 'boolean', true)
end
if providers['plantuml'] then
vim.filetype.add({
extension = { puml = 'plantuml', pu = 'plantuml' },
})
end
config = vim.tbl_deep_extend('force', default_config, {
@ -152,20 +144,16 @@ function M.build_context(bufnr)
end
---@param bufnr? integer
function M.compile(bufnr)
function M.build(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)
vim.notify('[preview.nvim] no provider configured for this filetype', vim.log.levels.WARN)
return
end
local ctx = M.build_context(bufnr)
local provider = config.providers[name]
compiler.compile(bufnr, name, provider, ctx, { oneshot = true })
compiler.compile(bufnr, name, provider, ctx)
end
---@param bufnr? integer
@ -177,13 +165,9 @@ 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)
vim.notify('[preview.nvim] no provider configured for this filetype', vim.log.levels.WARN)
return
end
local ctx = M.build_context(bufnr)
@ -192,15 +176,11 @@ function M.clean(bufnr)
end
---@param bufnr? integer
function M.toggle(bufnr)
function M.watch(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)
vim.notify('[preview.nvim] no provider configured for this filetype', vim.log.levels.WARN)
return
end
local provider = config.providers[name]
@ -210,14 +190,8 @@ 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
vim.notify('[preview.nvim]: no output file available for this buffer', vim.log.levels.WARN)
if not compiler.open(bufnr) then
vim.notify('[preview.nvim] no output file available for this buffer', vim.log.levels.WARN)
end
end
@ -254,8 +228,4 @@ M._test = {
end,
}
if vim.g.preview then
M.setup(vim.g.preview)
end
return M

View file

@ -93,28 +93,6 @@ local function parse_pandoc(output)
return diagnostics
end
---@param output string
---@return preview.Diagnostic[]
local function parse_asciidoctor(output)
local diagnostics = {}
for line in output:gmatch('[^\r\n]+') do
local severity, _, lnum, msg = line:match('^asciidoctor: (%u+): (.+): line (%d+): (.+)$')
if lnum then
local sev = vim.diagnostic.severity.ERROR
if severity == 'WARNING' then
sev = vim.diagnostic.severity.WARN
end
table.insert(diagnostics, {
lnum = tonumber(lnum) - 1,
col = 0,
message = msg,
severity = sev,
})
end
end
return diagnostics
end
---@type preview.ProviderConfig
M.typst = {
ft = 'typst',
@ -128,12 +106,9 @@ M.typst = {
error_parser = function(output)
return parse_typst(output)
end,
clean = function(ctx)
return { 'rm', '-f', (ctx.file:gsub('%.typ$', '.pdf')) }
end,
open = true,
reload = function(ctx)
return { 'typst', 'watch', '--diagnostic-format', 'short', ctx.file }
return { 'typst', 'watch', ctx.file }
end,
}
@ -162,45 +137,6 @@ M.latex = {
open = true,
}
---@type preview.ProviderConfig
M.pdflatex = {
ft = 'tex',
cmd = { 'pdflatex' },
args = function(ctx)
return { '-interaction=nonstopmode', '-file-line-error', '-synctex=1', ctx.file }
end,
output = function(ctx)
return (ctx.file:gsub('%.tex$', '.pdf'))
end,
error_parser = function(output)
return parse_latexmk(output)
end,
clean = function(ctx)
local base = ctx.file:gsub('%.tex$', '')
return { 'rm', '-f', base .. '.pdf', base .. '.aux', base .. '.log', base .. '.synctex.gz' }
end,
open = true,
}
---@type preview.ProviderConfig
M.tectonic = {
ft = 'tex',
cmd = { 'tectonic' },
args = function(ctx)
return { ctx.file }
end,
output = function(ctx)
return (ctx.file:gsub('%.tex$', '.pdf'))
end,
error_parser = function(output)
return parse_latexmk(output)
end,
clean = function(ctx)
return { 'rm', '-f', (ctx.file:gsub('%.tex$', '.pdf')) }
end,
open = true,
}
---@type preview.ProviderConfig
M.markdown = {
ft = 'markdown',
@ -251,104 +187,4 @@ M.github = {
reload = true,
}
---@type preview.ProviderConfig
M.asciidoctor = {
ft = 'asciidoc',
cmd = { 'asciidoctor' },
args = function(ctx)
return { '--failure-level', 'ERROR', ctx.file, '-o', ctx.output }
end,
output = function(ctx)
return (ctx.file:gsub('%.adoc$', '.html'))
end,
error_parser = function(output)
return parse_asciidoctor(output)
end,
clean = function(ctx)
return { 'rm', '-f', (ctx.file:gsub('%.adoc$', '.html')) }
end,
open = true,
reload = true,
}
---@type preview.ProviderConfig
M.plantuml = {
ft = 'plantuml',
cmd = { 'plantuml' },
args = function(ctx)
return { '-tsvg', ctx.file }
end,
output = function(ctx)
return (ctx.file:gsub('%.puml$', '.svg'))
end,
error_parser = function(output)
local diagnostics = {}
for line in output:gmatch('[^\r\n]+') do
local lnum = line:match('^Error line (%d+) in file:')
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('%.puml$', '.svg')) }
end,
open = true,
}
---@type preview.ProviderConfig
M.mermaid = {
ft = 'mermaid',
cmd = { 'mmdc' },
args = function(ctx)
return { '-i', ctx.file, '-o', ctx.output }
end,
output = function(ctx)
return (ctx.file:gsub('%.mmd$', '.svg'))
end,
error_parser = function(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')) }
end,
open = true,
}
---@type preview.ProviderConfig
M.quarto = {
ft = 'quarto',
cmd = { 'quarto' },
args = function(ctx)
return { 'render', ctx.file, '--to', 'html', '--embed-resources' }
end,
output = function(ctx)
return (ctx.file:gsub('%.qmd$', '.html'))
end,
clean = function(ctx)
local base = ctx.file:gsub('%.qmd$', '')
return { 'rm', '-rf', base .. '.html', base .. '_files' }
end,
open = true,
reload = true,
}
return M

View file

@ -1,10 +0,0 @@
#!/bin/sh
set -eu
nix develop --command stylua --check .
git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet
nix develop --command prettier --check .
nix fmt
git diff --exit-code -- '*.nix'
nix develop --command lua-language-server --check lua/ --configpath "$(pwd)/.luarc.json" --checklevel=Warning
nix develop --command busted

View file

@ -11,26 +11,20 @@ describe('commands', function()
local cmds = vim.api.nvim_get_commands({})
assert.is_not_nil(cmds.Preview)
end)
it('registers VimLeavePre autocmd', function()
require('preview.commands').setup()
local aus = vim.api.nvim_get_autocmds({ event = 'VimLeavePre' })
local found = false
for _, au in ipairs(aus) do
if au.callback then
found = true
break
end
end
assert.is_true(found)
end)
end)
describe('dispatch', function()
it('does not error on :Preview compile with no provider', function()
it('does not error on :Preview with no provider', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Preview compile')
vim.cmd('Preview build')
end)
end)
it('does not error on :Preview stop', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Preview stop')
end)
end)
@ -48,10 +42,10 @@ describe('commands', function()
end)
end)
it('does not error on :Preview toggle with no provider', function()
it('does not error on :Preview watch with no provider', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Preview toggle')
vim.cmd('Preview watch')
end)
end)
end)

View file

@ -99,35 +99,6 @@ describe('compiler', function()
helpers.delete_buffer(bufnr)
end)
it('notifies and returns when binary is not executable', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_nobin.txt')
vim.bo[bufnr].modified = false
local notified = false
local orig = vim.notify
vim.notify = function(msg)
if msg:find('not executable') then
notified = true
end
end
local provider = { cmd = { 'totally_nonexistent_binary_xyz_preview' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/preview_test_nobin.txt',
root = '/tmp',
ft = 'text',
}
compiler.compile(bufnr, 'nobin', provider, ctx)
vim.notify = orig
assert.is_true(notified)
assert.is_nil(compiler._test.active[bufnr])
helpers.delete_buffer(bufnr)
end)
it('fires PreviewCompileFailed on non-zero exit', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_fail.txt')

View file

@ -108,62 +108,4 @@ describe('preview', function()
helpers.delete_buffer(bufnr)
end)
end)
describe('unnamed buffer guard', function()
before_each(function()
helpers.reset_config({ typst = true })
preview = require('preview')
end)
local function capture_notify(fn)
local msg = nil
local orig = vim.notify
vim.notify = function(m)
msg = m
end
fn()
vim.notify = orig
return msg
end
it('compile warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
local msg = capture_notify(function()
preview.compile(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
helpers.delete_buffer(bufnr)
end)
it('toggle warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
local msg = capture_notify(function()
preview.toggle(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
helpers.delete_buffer(bufnr)
end)
it('clean warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
local msg = capture_notify(function()
preview.clean(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
helpers.delete_buffer(bufnr)
end)
it('open warns on unnamed buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
local msg = capture_notify(function()
preview.open(bufnr)
end)
assert.is_not_nil(msg)
assert.is_truthy(msg:find('no file name'))
helpers.delete_buffer(bufnr)
end)
end)
end)

View file

@ -33,10 +33,6 @@ describe('presets', function()
assert.are.equal('/tmp/document.pdf', output)
end)
it('returns clean command', function()
assert.are.same({ 'rm', '-f', '/tmp/document.pdf' }, presets.typst.clean(ctx))
end)
it('has open enabled', function()
assert.is_true(presets.typst.open)
end)
@ -50,9 +46,7 @@ describe('presets', function()
assert.is_table(result)
assert.are.equal('typst', result[1])
assert.are.equal('watch', result[2])
assert.are.equal('--diagnostic-format', result[3])
assert.are.equal('short', result[4])
assert.are.equal(ctx.file, result[5])
assert.are.equal(ctx.file, result[3])
end)
it('parses errors from stderr', function()
@ -163,120 +157,6 @@ describe('presets', function()
end)
end)
describe('pdflatex', function()
local tex_ctx = {
bufnr = 1,
file = '/tmp/document.tex',
root = '/tmp',
ft = 'tex',
}
it('has ft', function()
assert.are.equal('tex', presets.pdflatex.ft)
end)
it('has cmd', function()
assert.are.same({ 'pdflatex' }, presets.pdflatex.cmd)
end)
it('returns args with flags and file path', function()
local args = presets.pdflatex.args(tex_ctx)
assert.are.same(
{ '-interaction=nonstopmode', '-file-line-error', '-synctex=1', '/tmp/document.tex' },
args
)
end)
it('returns pdf output path', function()
assert.are.equal('/tmp/document.pdf', presets.pdflatex.output(tex_ctx))
end)
it('has open enabled', function()
assert.is_true(presets.pdflatex.open)
end)
it('returns clean command removing pdf and aux files', function()
local clean = presets.pdflatex.clean(tex_ctx)
assert.are.same({
'rm',
'-f',
'/tmp/document.pdf',
'/tmp/document.aux',
'/tmp/document.log',
'/tmp/document.synctex.gz',
}, clean)
end)
it('has no reload', function()
assert.is_nil(presets.pdflatex.reload)
end)
it('parses file-line-error format', function()
local output = './document.tex:10: Undefined control sequence.'
local diagnostics = presets.pdflatex.error_parser(output, tex_ctx)
assert.are.equal(1, #diagnostics)
assert.are.equal(9, diagnostics[1].lnum)
assert.are.equal(0, diagnostics[1].col)
assert.are.equal('Undefined control sequence.', diagnostics[1].message)
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
end)
it('returns empty table for clean output', function()
assert.are.same({}, presets.pdflatex.error_parser('', tex_ctx))
end)
end)
describe('tectonic', function()
local tex_ctx = {
bufnr = 1,
file = '/tmp/document.tex',
root = '/tmp',
ft = 'tex',
}
it('has ft', function()
assert.are.equal('tex', presets.tectonic.ft)
end)
it('has cmd', function()
assert.are.same({ 'tectonic' }, presets.tectonic.cmd)
end)
it('returns args with file path', function()
assert.are.same({ '/tmp/document.tex' }, presets.tectonic.args(tex_ctx))
end)
it('returns pdf output path', function()
assert.are.equal('/tmp/document.pdf', presets.tectonic.output(tex_ctx))
end)
it('has open enabled', function()
assert.is_true(presets.tectonic.open)
end)
it('returns clean command removing pdf', function()
assert.are.same({ 'rm', '-f', '/tmp/document.pdf' }, presets.tectonic.clean(tex_ctx))
end)
it('has no reload', function()
assert.is_nil(presets.tectonic.reload)
end)
it('parses file-line-error format', function()
local output = './document.tex:5: Missing $ inserted.'
local diagnostics = presets.tectonic.error_parser(output, tex_ctx)
assert.are.equal(1, #diagnostics)
assert.are.equal(4, diagnostics[1].lnum)
assert.are.equal(0, diagnostics[1].col)
assert.are.equal('Missing $ inserted.', diagnostics[1].message)
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
end)
it('returns empty table for clean output', function()
assert.are.same({}, presets.tectonic.error_parser('', tex_ctx))
end)
end)
describe('markdown', function()
local md_ctx = {
bufnr = 1,
@ -461,116 +341,4 @@ describe('presets', function()
assert.are.same({}, diagnostics)
end)
end)
describe('asciidoctor', function()
local adoc_ctx = {
bufnr = 1,
file = '/tmp/document.adoc',
root = '/tmp',
ft = 'asciidoc',
output = '/tmp/document.html',
}
it('has ft', function()
assert.are.equal('asciidoc', presets.asciidoctor.ft)
end)
it('has cmd', function()
assert.are.same({ 'asciidoctor' }, presets.asciidoctor.cmd)
end)
it('returns args with file and output', function()
assert.are.same(
{ '--failure-level', 'ERROR', '/tmp/document.adoc', '-o', '/tmp/document.html' },
presets.asciidoctor.args(adoc_ctx)
)
end)
it('returns html output path', function()
assert.are.equal('/tmp/document.html', presets.asciidoctor.output(adoc_ctx))
end)
it('returns clean command', function()
assert.are.same({ 'rm', '-f', '/tmp/document.html' }, presets.asciidoctor.clean(adoc_ctx))
end)
it('has open enabled', function()
assert.is_true(presets.asciidoctor.open)
end)
it('has reload enabled for SSE', function()
assert.is_true(presets.asciidoctor.reload)
end)
it('parses error messages', function()
local output =
'asciidoctor: ERROR: document.adoc: line 8: invalid part, must have at least one section'
local diagnostics = presets.asciidoctor.error_parser(output, adoc_ctx)
assert.are.equal(1, #diagnostics)
assert.are.equal(7, diagnostics[1].lnum)
assert.are.equal(0, diagnostics[1].col)
assert.are.equal('invalid part, must have at least one section', diagnostics[1].message)
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
end)
it('parses warning messages', function()
local output = 'asciidoctor: WARNING: document.adoc: line 52: section title out of sequence'
local diagnostics = presets.asciidoctor.error_parser(output, adoc_ctx)
assert.are.equal(1, #diagnostics)
assert.are.equal(51, diagnostics[1].lnum)
assert.are.equal(vim.diagnostic.severity.WARN, diagnostics[1].severity)
end)
it('returns empty table for clean output', function()
assert.are.same({}, presets.asciidoctor.error_parser('', adoc_ctx))
end)
end)
describe('quarto', function()
local qmd_ctx = {
bufnr = 1,
file = '/tmp/document.qmd',
root = '/tmp',
ft = 'quarto',
output = '/tmp/document.html',
}
it('has ft', function()
assert.are.equal('quarto', presets.quarto.ft)
end)
it('has cmd', function()
assert.are.same({ 'quarto' }, presets.quarto.cmd)
end)
it('returns args with render subcommand and html format', function()
assert.are.same(
{ 'render', '/tmp/document.qmd', '--to', 'html', '--embed-resources' },
presets.quarto.args(qmd_ctx)
)
end)
it('returns html output path', function()
assert.are.equal('/tmp/document.html', presets.quarto.output(qmd_ctx))
end)
it('returns clean command removing html and _files directory', function()
assert.are.same(
{ 'rm', '-rf', '/tmp/document.html', '/tmp/document_files' },
presets.quarto.clean(qmd_ctx)
)
end)
it('has open enabled', function()
assert.is_true(presets.quarto.open)
end)
it('has reload enabled for SSE', function()
assert.is_true(presets.quarto.reload)
end)
it('has no error_parser', function()
assert.is_nil(presets.quarto.error_parser)
end)
end)
end)

View file

@ -13,13 +13,13 @@ describe('reload', function()
describe('inject', function()
it('injects script before </body>', function()
local path = os.tmpname()
local f = assert(io.open(path, 'w'))
local f = io.open(path, 'w')
f:write('<html><body><p>hello</p></body></html>')
f:close()
reload.inject(path)
local fr = assert(io.open(path, 'r'))
local fr = io.open(path, 'r')
local content = fr:read('*a')
fr:close()
os.remove(path)
@ -33,13 +33,13 @@ describe('reload', function()
it('appends script when no </body>', function()
local path = os.tmpname()
local f = assert(io.open(path, 'w'))
local f = io.open(path, 'w')
f:write('<html><p>hello</p></html>')
f:close()
reload.inject(path)
local fr = assert(io.open(path, 'r'))
local fr = io.open(path, 'r')
local content = fr:read('*a')
fr:close()
os.remove(path)