feat: rename

This commit is contained in:
Barrett Ruth 2026-03-02 21:23:40 -05:00
parent e1d7abf58e
commit 942438f817
No known key found for this signature in database
GPG key ID: A6C96C9349D2FC81
21 changed files with 660 additions and 305 deletions

View file

@ -1,4 +1,4 @@
# render.nvim
# preview.nvim
Async document compilation for Neovim.
@ -10,32 +10,47 @@ your own providers.
- Async compilation via `vim.system()`
- Compiler errors as native `vim.diagnostic`
- User events for extensibility (`RenderCompileStarted`, `RenderCompileSuccess`,
`RenderCompileFailed`)
- User events for extensibility (`PreviewCompileStarted`, `PreviewCompileSuccess`,
`PreviewCompileFailed`)
- `:checkhealth` integration
- Zero dependencies beyond Neovim 0.10.0+
- Zero dependencies beyond Neovim 0.11.0+
## Requirements
- Neovim >= 0.10.0
- Neovim >= 0.11.0
- A compiler binary for each provider you configure
## Installation
```lua
-- lazy.nvim
{ 'barrettruth/render.nvim' }
{ 'barrettruth/preview.nvim' }
```
```vim
" luarocks
:Rocks install render.nvim
:Rocks install preview.nvim
```
## Configuration
Use built-in presets for common tools:
```lua
vim.g.render = {
local presets = require('preview.presets')
vim.g.preview = {
providers = {
typst = presets.typst,
tex = presets.latex,
markdown = presets.markdown,
},
}
```
Or define providers manually:
```lua
vim.g.preview = {
providers = {
typst = {
cmd = { 'typst', 'compile' },
@ -46,19 +61,10 @@ vim.g.render = {
return ctx.file:gsub('%.typ$', '.pdf')
end,
},
latexmk = {
cmd = { 'latexmk' },
args = { '-pdf', '-interaction=nonstopmode' },
clean = { 'latexmk', '-c' },
},
},
providers_by_ft = {
typst = 'typst',
tex = 'latexmk',
},
}
```
## Documentation
See `:help render.nvim` for full documentation.
See `:help preview.nvim` for full documentation.

View file

@ -1,83 +1,80 @@
*render.nvim.txt* Async document compilation for Neovim
*preview.nvim.txt* Async document compilation for Neovim
Author: Raphael
License: MIT
==============================================================================
INTRODUCTION *render.nvim*
INTRODUCTION *preview.nvim*
render.nvim is an extensible framework for compiling documents asynchronously
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. render.nvim is purely an orchestration framework.
their compiler commands. preview.nvim is purely an orchestration framework.
==============================================================================
REQUIREMENTS *render.nvim-requirements*
REQUIREMENTS *preview.nvim-requirements*
- Neovim >= 0.10.0
- Neovim >= 0.11.0
- A compiler binary for each configured provider (e.g. `typst`, `latexmk`)
==============================================================================
INSTALLATION *render.nvim-installation*
INSTALLATION *preview.nvim-installation*
With luarocks (recommended):
>
:Rocks install render.nvim
:Rocks install preview.nvim
<
With lazy.nvim:
>lua
{
'barrettruth/render.nvim',
'barrettruth/preview.nvim',
}
<
==============================================================================
CONFIGURATION *render.nvim-configuration*
CONFIGURATION *preview.nvim-configuration*
Configure via the `vim.g.render` global table before the plugin loads.
Configure via the `vim.g.preview` global table before the plugin loads.
*render.Config*
*preview.Config*
Fields:~
`debug` boolean|string Enable debug logging. A string value
is treated as a log file path.
Default: `false`
`providers` table Provider configurations keyed by name.
Default: `{}`
`providers` table Provider configurations keyed by
filetype. Default: `{}`
`providers_by_ft` table Maps filetypes to provider names.
Default: `{}`
*render.ProviderConfig*
*preview.ProviderConfig*
Provider fields:~
`cmd` string[] The compiler command (required).
`args` string[]|function Additional arguments. If a function,
receives a |render.Context| and returns
receives a |preview.Context| and returns
a string[].
`cwd` string|function Working directory. If a function,
receives a |render.Context|. Default:
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 |render.Context|.
receives a |preview.Context|.
`error_parser` function Receives (stderr, |render.Context|)
`error_parser` function Receives (stderr, |preview.Context|)
and returns vim.Diagnostic[].
`clean` string[]|function Command to remove build artifacts.
If a function, receives a
|render.Context|.
|preview.Context|.
*render.Context*
*preview.Context*
Context fields:~
`bufnr` integer Buffer number.
@ -87,7 +84,7 @@ Context fields:~
Example:~
>lua
vim.g.render = {
vim.g.preview = {
providers = {
typst = {
cmd = { 'typst', 'compile' },
@ -110,74 +107,117 @@ Example:~
return diagnostics
end,
},
latexmk = {
tex = {
cmd = { 'latexmk' },
args = { '-pdf', '-interaction=nonstopmode' },
clean = { 'latexmk', '-c' },
},
},
providers_by_ft = {
typst = 'typst',
tex = 'latexmk',
}
<
==============================================================================
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 → PDF
Example:~
>lua
local presets = require('preview.presets')
vim.g.preview = {
providers = {
typst = presets.typst,
tex = presets.latex,
markdown = presets.markdown,
},
}
<
Override individual fields with `vim.tbl_deep_extend`:
>lua
local presets = require('preview.presets')
vim.g.preview = {
providers = {
typst = vim.tbl_deep_extend('force', presets.typst, {
env = { TYPST_FONT_PATHS = '/usr/share/fonts' },
}),
},
}
<
==============================================================================
COMMANDS *render.nvim-commands*
COMMANDS *preview.nvim-commands*
:Render [subcommand] *:Render*
: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.
`status` Echo compilation status (idle or compiling).
`watch` Toggle auto-compile on save for the current buffer.
`status` Echo compilation status (idle, compiling, watching).
==============================================================================
API *render.nvim-api*
API *preview.nvim-api*
render.compile({bufnr?}) *render.compile()*
preview.compile({bufnr?}) *preview.compile()*
Compile the document in the given buffer (default: current).
render.stop({bufnr?}) *render.stop()*
preview.stop({bufnr?}) *preview.stop()*
Kill the active compilation process for the buffer.
render.clean({bufnr?}) *render.clean()*
preview.clean({bufnr?}) *preview.clean()*
Run the provider's clean command for the buffer.
render.status({bufnr?}) *render.status()*
Returns a |render.Status| table.
preview.watch({bufnr?}) *preview.watch()*
Toggle watch mode for the buffer. When enabled, the buffer is
automatically compiled on each save (`BufWritePost`). Call again
to stop watching.
*render.Status*
preview.status({bufnr?}) *preview.status()*
Returns a |preview.Status| table.
*preview.Status*
Status fields:~
`compiling` boolean Whether compilation is active.
`watching` boolean Whether watch mode is active.
`provider` string? Name of the active provider.
`output_file` string? Path to the output file.
render.get_config() *render.get_config()*
Returns the resolved |render.Config|.
preview.get_config() *preview.get_config()*
Returns the resolved |preview.Config|.
==============================================================================
EVENTS *render.nvim-events*
EVENTS *preview.nvim-events*
render.nvim fires User autocmds with structured data:
preview.nvim fires User autocmds with structured data:
`RenderCompileStarted` Compilation began.
`PreviewCompileStarted` Compilation began.
data: `{ bufnr, provider }`
`RenderCompileSuccess` Compilation succeeded (exit code 0).
`PreviewCompileSuccess` Compilation succeeded (exit code 0).
data: `{ bufnr, provider, output }`
`RenderCompileFailed` Compilation failed (non-zero exit).
`PreviewCompileFailed` Compilation failed (non-zero exit).
data: `{ bufnr, provider, code, stderr }`
`PreviewWatchStarted` Watch mode enabled for a buffer.
data: `{ bufnr, provider }`
`PreviewWatchStopped` Watch mode disabled for a buffer.
data: `{ bufnr }`
Example:~
>lua
vim.api.nvim_create_autocmd('User', {
pattern = 'RenderCompileSuccess',
pattern = 'PreviewCompileSuccess',
callback = function(args)
local data = args.data
vim.notify('Compiled ' .. data.output .. ' with ' .. data.provider)
@ -186,13 +226,13 @@ Example:~
<
==============================================================================
HEALTH *render.nvim-health*
HEALTH *preview.nvim-health*
Run `:checkhealth render` to verify:
Run `:checkhealth preview` to verify:
- Neovim version >= 0.10.0
- Neovim version >= 0.11.0
- Each configured provider's binary is executable
- Filetype-to-provider mappings are valid
- Each configured provider's filetype mapping is valid
==============================================================================
vim:tw=78:ts=8:ft=help:norl:

View file

@ -1,5 +1,5 @@
{
description = "render.nvim async document compilation for Neovim";
description = "preview.nvim async document compilation for Neovim";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

54
lua/preview/commands.lua Normal file
View file

@ -0,0 +1,54 @@
local M = {}
local subcommands = { 'compile', 'stop', 'clean', 'watch', 'status' }
---@param args string
local function dispatch(args)
local subcmd = args ~= '' and args or 'compile'
if subcmd == 'compile' then
require('preview').compile()
elseif subcmd == 'stop' then
require('preview').stop()
elseif subcmd == 'clean' then
require('preview').clean()
elseif subcmd == 'watch' then
require('preview').watch()
elseif subcmd == 'status' then
local s = require('preview').status()
local parts = {}
if s.compiling then
table.insert(parts, 'compiling with "' .. s.provider .. '"')
else
table.insert(parts, 'idle')
end
if s.watching then
table.insert(parts, 'watching')
end
vim.notify('[preview.nvim] ' .. table.concat(parts, ', '), vim.log.levels.INFO)
else
vim.notify('[preview.nvim] unknown subcommand: ' .. subcmd, vim.log.levels.ERROR)
end
end
---@param lead string
---@return string[]
local function complete(lead)
return vim.tbl_filter(function(s)
return s:find(lead, 1, true) == 1
end, subcommands)
end
function M.setup()
vim.api.nvim_create_user_command('Preview', function(opts)
dispatch(opts.args)
end, {
nargs = '?',
complete = function(lead)
return complete(lead)
end,
desc = 'Compile, stop, clean, watch, or check status of document preview',
})
end
return M

View file

@ -1,13 +1,16 @@
local M = {}
local diagnostic = require('render.diagnostic')
local log = require('render.log')
local diagnostic = require('preview.diagnostic')
local log = require('preview.log')
---@type table<integer, render.Process>
---@type table<integer, preview.Process>
local active = {}
---@param val string[]|fun(ctx: render.Context): string[]
---@param ctx render.Context
---@type table<integer, integer>
local watching = {}
---@param val string[]|fun(ctx: preview.Context): string[]
---@param ctx preview.Context
---@return string[]
local function eval_list(val, ctx)
if type(val) == 'function' then
@ -16,8 +19,8 @@ local function eval_list(val, ctx)
return val
end
---@param val string|fun(ctx: render.Context): string
---@param ctx render.Context
---@param val string|fun(ctx: preview.Context): string
---@param ctx preview.Context
---@return string
local function eval_string(val, ctx)
if type(val) == 'function' then
@ -28,8 +31,8 @@ end
---@param bufnr integer
---@param name string
---@param provider render.ProviderConfig
---@param ctx render.Context
---@param provider preview.ProviderConfig
---@param ctx preview.Context
function M.compile(bufnr, name, provider, ctx)
if vim.bo[bufnr].modified then
vim.cmd('silent! update')
@ -70,7 +73,7 @@ function M.compile(bufnr, name, provider, ctx)
log.dbg('compilation succeeded for buffer %d', bufnr)
diagnostic.clear(bufnr)
vim.api.nvim_exec_autocmds('User', {
pattern = 'RenderCompileSuccess',
pattern = 'PreviewCompileSuccess',
data = { bufnr = bufnr, provider = name, output = output_file },
})
else
@ -79,7 +82,7 @@ function M.compile(bufnr, name, provider, ctx)
diagnostic.set(bufnr, name, provider.error_parser, result.stderr or '', ctx)
end
vim.api.nvim_exec_autocmds('User', {
pattern = 'RenderCompileFailed',
pattern = 'PreviewCompileFailed',
data = {
bufnr = bufnr,
provider = name,
@ -102,7 +105,7 @@ function M.compile(bufnr, name, provider, ctx)
})
vim.api.nvim_exec_autocmds('User', {
pattern = 'RenderCompileStarted',
pattern = 'PreviewCompileStarted',
data = { bufnr = bufnr, provider = name },
})
end
@ -134,15 +137,69 @@ function M.stop_all()
for bufnr, _ in pairs(active) do
M.stop(bufnr)
end
for bufnr, _ in pairs(watching) do
M.unwatch(bufnr)
end
end
---@param bufnr integer
---@param name string
---@param provider render.ProviderConfig
---@param ctx render.Context
---@param provider preview.ProviderConfig
---@param ctx_builder fun(bufnr: integer): preview.Context
function M.watch(bufnr, name, provider, ctx_builder)
if watching[bufnr] then
M.unwatch(bufnr)
return
end
local au_id = vim.api.nvim_create_autocmd('BufWritePost', {
buffer = bufnr,
callback = function()
local ctx = ctx_builder(bufnr)
M.compile(bufnr, name, provider, ctx)
end,
})
watching[bufnr] = au_id
log.dbg('watching buffer %d with provider "%s"', bufnr, name)
vim.api.nvim_create_autocmd('BufWipeout', {
buffer = bufnr,
once = true,
callback = function()
M.unwatch(bufnr)
end,
})
vim.api.nvim_exec_autocmds('User', {
pattern = 'PreviewWatchStarted',
data = { bufnr = bufnr, provider = name },
})
end
---@param bufnr integer
function M.unwatch(bufnr)
local au_id = watching[bufnr]
if not au_id then
return
end
vim.api.nvim_del_autocmd(au_id)
watching[bufnr] = nil
log.dbg('unwatched buffer %d', bufnr)
vim.api.nvim_exec_autocmds('User', {
pattern = 'PreviewWatchStopped',
data = { bufnr = bufnr },
})
end
---@param bufnr integer
---@param name string
---@param provider preview.ProviderConfig
---@param ctx preview.Context
function M.clean(bufnr, name, provider, ctx)
if not provider.clean then
vim.notify('[render.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
@ -160,27 +217,33 @@ 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('[render.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('[render.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)
)
end
---@param bufnr integer
---@return render.Status
---@return preview.Status
function M.status(bufnr)
local proc = active[bufnr]
if proc then
return { compiling = true, provider = proc.provider, output_file = proc.output_file }
return {
compiling = true,
watching = watching[bufnr] ~= nil,
provider = proc.provider,
output_file = proc.output_file,
}
end
return { compiling = false }
return { compiling = false, watching = watching[bufnr] ~= nil }
end
M._test = {
active = active,
watching = watching,
}
return M

View file

@ -1,8 +1,8 @@
local M = {}
local log = require('render.log')
local log = require('preview.log')
local ns = vim.api.nvim_create_namespace('render')
local ns = vim.api.nvim_create_namespace('preview')
---@param bufnr integer
function M.clear(bufnr)
@ -12,9 +12,9 @@ end
---@param bufnr integer
---@param name string
---@param error_parser fun(stderr: string, ctx: render.Context): render.Diagnostic[]
---@param error_parser fun(stderr: string, ctx: preview.Context): preview.Diagnostic[]
---@param stderr string
---@param ctx render.Context
---@param ctx preview.Context
function M.set(bufnr, name, error_parser, stderr, ctx)
local ok, diagnostics = pcall(error_parser, stderr, ctx)
if not ok then

31
lua/preview/health.lua Normal file
View file

@ -0,0 +1,31 @@
local M = {}
function M.check()
vim.health.start('preview.nvim')
if vim.fn.has('nvim-0.11.0') == 1 then
vim.health.ok('Neovim 0.11.0+ detected')
else
vim.health.error('preview.nvim requires Neovim 0.11.0+')
end
local config = require('preview').get_config()
local provider_count = vim.tbl_count(config.providers)
if provider_count == 0 then
vim.health.warn('no providers configured')
else
vim.health.ok(provider_count .. ' provider(s) configured')
end
for ft, provider in pairs(config.providers) do
local bin = provider.cmd[1]
if vim.fn.executable(bin) == 1 then
vim.health.ok('filetype "' .. ft .. '": ' .. bin .. ' found')
else
vim.health.error('filetype "' .. ft .. '": ' .. bin .. ' not found')
end
end
end
return M

View file

@ -1,24 +1,23 @@
---@class render.ProviderConfig
---@class preview.ProviderConfig
---@field cmd string[]
---@field args? string[]|fun(ctx: render.Context): string[]
---@field cwd? string|fun(ctx: render.Context): string
---@field args? string[]|fun(ctx: preview.Context): string[]
---@field cwd? string|fun(ctx: preview.Context): string
---@field env? table<string, string>
---@field output? string|fun(ctx: render.Context): string
---@field error_parser? fun(stderr: string, ctx: render.Context): render.Diagnostic[]
---@field clean? string[]|fun(ctx: render.Context): string[]
---@field output? string|fun(ctx: preview.Context): string
---@field error_parser? fun(stderr: string, ctx: preview.Context): preview.Diagnostic[]
---@field clean? string[]|fun(ctx: preview.Context): string[]
---@class render.Config
---@class preview.Config
---@field debug boolean|string
---@field providers table<string, render.ProviderConfig>
---@field providers_by_ft table<string, string>
---@field providers table<string, preview.ProviderConfig>
---@class render.Context
---@class preview.Context
---@field bufnr integer
---@field file string
---@field root string
---@field ft string
---@class render.Diagnostic
---@class preview.Diagnostic
---@field lnum integer
---@field col integer
---@field message string
@ -27,30 +26,30 @@
---@field end_col? integer
---@field source? string
---@class render.Process
---@class preview.Process
---@field obj table
---@field provider string
---@field output_file string
---@class render
---@class preview
---@field compile fun(bufnr?: integer)
---@field stop fun(bufnr?: integer)
---@field clean fun(bufnr?: integer)
---@field status fun(bufnr?: integer): render.Status
---@field get_config fun(): render.Config
---@field watch fun(bufnr?: integer)
---@field status fun(bufnr?: integer): preview.Status
---@field get_config fun(): preview.Config
local M = {}
local compiler = require('render.compiler')
local log = require('render.log')
local compiler = require('preview.compiler')
local log = require('preview.log')
---@type render.Config
---@type preview.Config
local default_config = {
debug = false,
providers = {},
providers_by_ft = {},
}
---@type render.Config
---@type preview.Config
local config = vim.deepcopy(default_config)
local initialized = false
@ -61,17 +60,14 @@ local function init()
end
initialized = true
local opts = vim.g.render or {}
local opts = vim.g.preview or {}
vim.validate('render config', opts, 'table')
vim.validate('preview config', opts, 'table')
if opts.debug ~= nil then
vim.validate('render config.debug', opts.debug, { 'boolean', 'string' })
vim.validate('preview config.debug', opts.debug, { 'boolean', 'string' })
end
if opts.providers ~= nil then
vim.validate('render config.providers', opts.providers, 'table')
end
if opts.providers_by_ft ~= nil then
vim.validate('render config.providers_by_ft', opts.providers_by_ft, 'table')
vim.validate('preview config.providers', opts.providers, 'table')
end
config = vim.tbl_deep_extend('force', default_config, opts)
@ -79,7 +75,7 @@ local function init()
log.dbg('initialized with %d providers', vim.tbl_count(config.providers))
end
---@return render.Config
---@return preview.Config
function M.get_config()
init()
return config
@ -91,20 +87,15 @@ function M.resolve_provider(bufnr)
init()
bufnr = bufnr or vim.api.nvim_get_current_buf()
local ft = vim.bo[bufnr].filetype
local name = config.providers_by_ft[ft]
if not name then
log.dbg('no provider mapped for filetype: %s', ft)
if not config.providers[ft] then
log.dbg('no provider configured for filetype: %s', ft)
return nil
end
if not config.providers[name] then
log.dbg('provider "%s" mapped for ft "%s" but not configured', name, ft)
return nil
end
return name
return ft
end
---@param bufnr? integer
---@return render.Context
---@return preview.Context
function M.build_context(bufnr)
init()
bufnr = bufnr or vim.api.nvim_get_current_buf()
@ -124,7 +115,7 @@ function M.compile(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[render.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]
@ -145,7 +136,7 @@ function M.clean(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[render.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]
@ -153,13 +144,27 @@ function M.clean(bufnr)
compiler.clean(bufnr, name, provider, ctx)
end
---@class render.Status
---@param bufnr? integer
function M.watch(bufnr)
init()
bufnr = bufnr or vim.api.nvim_get_current_buf()
local name = M.resolve_provider(bufnr)
if not name then
vim.notify('[preview.nvim] no provider configured for this filetype', vim.log.levels.WARN)
return
end
local provider = config.providers[name]
compiler.watch(bufnr, name, provider, M.build_context)
end
---@class preview.Status
---@field compiling boolean
---@field watching boolean
---@field provider? string
---@field output_file? string
---@param bufnr? integer
---@return render.Status
---@return preview.Status
function M.status(bufnr)
init()
bufnr = bufnr or vim.api.nvim_get_current_buf()

View file

@ -20,7 +20,7 @@ function M.dbg(msg, ...)
if not enabled then
return
end
local formatted = '[render.nvim]: ' .. string.format(msg, ...)
local formatted = '[preview.nvim]: ' .. string.format(msg, ...)
if log_file then
local f = io.open(log_file, 'a')
if f then

40
lua/preview/presets.lua Normal file
View file

@ -0,0 +1,40 @@
local M = {}
---@type preview.ProviderConfig
M.typst = {
cmd = { 'typst', 'compile' },
args = function(ctx)
return { ctx.file }
end,
output = function(ctx)
return ctx.file:gsub('%.typ$', '.pdf')
end,
}
---@type preview.ProviderConfig
M.latex = {
cmd = { 'latexmk' },
args = function(ctx)
return { '-pdf', '-interaction=nonstopmode', ctx.file }
end,
output = function(ctx)
return ctx.file:gsub('%.tex$', '.pdf')
end,
clean = function(ctx)
return { 'latexmk', '-c', ctx.file }
end,
}
---@type preview.ProviderConfig
M.markdown = {
cmd = { 'pandoc' },
args = function(ctx)
local output = ctx.file:gsub('%.md$', '.pdf')
return { ctx.file, '-o', output }
end,
output = function(ctx)
return ctx.file:gsub('%.md$', '.pdf')
end,
}
return M

View file

@ -1,47 +0,0 @@
local M = {}
local subcommands = { 'compile', 'stop', 'clean', 'status' }
---@param args string
local function dispatch(args)
local subcmd = args ~= '' and args or 'compile'
if subcmd == 'compile' then
require('render').compile()
elseif subcmd == 'stop' then
require('render').stop()
elseif subcmd == 'clean' then
require('render').clean()
elseif subcmd == 'status' then
local s = require('render').status()
if s.compiling then
vim.notify('[render.nvim] compiling with "' .. s.provider .. '"', vim.log.levels.INFO)
else
vim.notify('[render.nvim] idle', vim.log.levels.INFO)
end
else
vim.notify('[render.nvim] unknown subcommand: ' .. subcmd, vim.log.levels.ERROR)
end
end
---@param lead string
---@return string[]
local function complete(lead)
return vim.tbl_filter(function(s)
return s:find(lead, 1, true) == 1
end, subcommands)
end
function M.setup()
vim.api.nvim_create_user_command('Render', function(opts)
dispatch(opts.args)
end, {
nargs = '?',
complete = function(lead)
return complete(lead)
end,
desc = 'Compile, stop, clean, or check status of document rendering',
})
end
return M

View file

@ -1,42 +0,0 @@
local M = {}
function M.check()
vim.health.start('render.nvim')
if vim.fn.has('nvim-0.10.0') == 1 then
vim.health.ok('Neovim 0.10.0+ detected')
else
vim.health.error('render.nvim requires Neovim 0.10.0+')
end
local config = require('render').get_config()
local provider_count = vim.tbl_count(config.providers)
if provider_count == 0 then
vim.health.warn('no providers configured')
else
vim.health.ok(provider_count .. ' provider(s) configured')
end
for name, provider in pairs(config.providers) do
local bin = provider.cmd[1]
if vim.fn.executable(bin) == 1 then
vim.health.ok('provider "' .. name .. '": ' .. bin .. ' found')
else
vim.health.error('provider "' .. name .. '": ' .. bin .. ' not found')
end
end
local ft_count = vim.tbl_count(config.providers_by_ft)
if ft_count > 0 then
for ft, name in pairs(config.providers_by_ft) do
if config.providers[name] then
vim.health.ok('filetype "' .. ft .. '" -> provider "' .. name .. '"')
else
vim.health.error('filetype "' .. ft .. '" maps to unknown provider "' .. name .. '"')
end
end
end
end
return M

12
plugin/preview.lua Normal file
View file

@ -0,0 +1,12 @@
if vim.g.loaded_preview then
return
end
vim.g.loaded_preview = 1
require('preview.commands').setup()
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
require('preview.compiler').stop_all()
end,
})

View file

@ -1,12 +0,0 @@
if vim.g.loaded_render then
return
end
vim.g.loaded_render = 1
require('render.commands').setup()
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
require('render.compiler').stop_all()
end,
})

View file

@ -1,14 +1,14 @@
rockspec_format = '3.0'
package = 'render.nvim'
package = 'preview.nvim'
version = 'scm-1'
source = {
url = 'git+https://github.com/barrettruth/render.nvim.git',
url = 'git+https://github.com/barrettruth/preview.nvim.git',
}
description = {
summary = 'Async document compilation for Neovim',
homepage = 'https://github.com/barrettruth/render.nvim',
homepage = 'https://github.com/barrettruth/preview.nvim',
license = 'MIT',
}

View file

@ -6,32 +6,39 @@ describe('commands', function()
end)
describe('setup', function()
it('creates the :Render command', function()
require('render.commands').setup()
it('creates the :Preview command', function()
require('preview.commands').setup()
local cmds = vim.api.nvim_get_commands({})
assert.is_not_nil(cmds.Render)
assert.is_not_nil(cmds.Preview)
end)
end)
describe('dispatch', function()
it('does not error on :Render with no provider', function()
require('render.commands').setup()
it('does not error on :Preview with no provider', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Render compile')
vim.cmd('Preview compile')
end)
end)
it('does not error on :Render stop', function()
require('render.commands').setup()
it('does not error on :Preview stop', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Render stop')
vim.cmd('Preview stop')
end)
end)
it('does not error on :Render status', function()
require('render.commands').setup()
it('does not error on :Preview status', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Render status')
vim.cmd('Preview status')
end)
end)
it('does not error on :Preview watch with no provider', function()
require('preview.commands').setup()
assert.has_no.errors(function()
vim.cmd('Preview watch')
end)
end)
end)

View file

@ -5,19 +5,19 @@ describe('compiler', function()
before_each(function()
helpers.reset_config()
compiler = require('render.compiler')
compiler = require('preview.compiler')
end)
describe('compile', 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/render_test.txt')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test.txt')
vim.bo[bufnr].modified = false
local provider = { cmd = { 'echo', 'ok' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/render_test.txt',
file = '/tmp/preview_test.txt',
root = '/tmp',
ft = 'text',
}
@ -35,14 +35,14 @@ describe('compiler', function()
helpers.delete_buffer(bufnr)
end)
it('fires RenderCompileStarted event', function()
it('fires PreviewCompileStarted event', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/render_test_event.txt')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_event.txt')
vim.bo[bufnr].modified = false
local fired = false
vim.api.nvim_create_autocmd('User', {
pattern = 'RenderCompileStarted',
pattern = 'PreviewCompileStarted',
once = true,
callback = function()
fired = true
@ -52,7 +52,7 @@ describe('compiler', function()
local provider = { cmd = { 'echo', 'ok' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/render_test_event.txt',
file = '/tmp/preview_test_event.txt',
root = '/tmp',
ft = 'text',
}
@ -67,14 +67,14 @@ describe('compiler', function()
helpers.delete_buffer(bufnr)
end)
it('fires RenderCompileSuccess on exit code 0', function()
it('fires PreviewCompileSuccess on exit code 0', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/render_test_success.txt')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_success.txt')
vim.bo[bufnr].modified = false
local succeeded = false
vim.api.nvim_create_autocmd('User', {
pattern = 'RenderCompileSuccess',
pattern = 'PreviewCompileSuccess',
once = true,
callback = function()
succeeded = true
@ -84,7 +84,7 @@ describe('compiler', function()
local provider = { cmd = { 'true' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/render_test_success.txt',
file = '/tmp/preview_test_success.txt',
root = '/tmp',
ft = 'text',
}
@ -99,14 +99,14 @@ describe('compiler', function()
helpers.delete_buffer(bufnr)
end)
it('fires RenderCompileFailed on non-zero exit', function()
it('fires PreviewCompileFailed on non-zero exit', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/render_test_fail.txt')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_fail.txt')
vim.bo[bufnr].modified = false
local failed = false
vim.api.nvim_create_autocmd('User', {
pattern = 'RenderCompileFailed',
pattern = 'PreviewCompileFailed',
once = true,
callback = function()
failed = true
@ -116,7 +116,7 @@ describe('compiler', function()
local provider = { cmd = { 'false' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/render_test_fail.txt',
file = '/tmp/preview_test_fail.txt',
root = '/tmp',
ft = 'text',
}
@ -148,13 +148,13 @@ describe('compiler', function()
it('returns compiling during active process', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/render_test_status.txt')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_status.txt')
vim.bo[bufnr].modified = false
local provider = { cmd = { 'sleep', '10' } }
local ctx = {
bufnr = bufnr,
file = '/tmp/render_test_status.txt',
file = '/tmp/preview_test_status.txt',
root = '/tmp',
ft = 'text',
}
@ -173,4 +173,133 @@ describe('compiler', function()
helpers.delete_buffer(bufnr)
end)
end)
describe('watch', 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')
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return { bufnr = b, file = '/tmp/preview_test_watch.txt', root = '/tmp', ft = 'text' }
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
assert.is_not_nil(compiler._test.watching[bufnr])
helpers.delete_buffer(bufnr)
end)
it('fires PreviewWatchStarted event', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch_event.txt')
local fired = false
vim.api.nvim_create_autocmd('User', {
pattern = 'PreviewWatchStarted',
once = true,
callback = function()
fired = true
end,
})
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return { bufnr = b, file = '/tmp/preview_test_watch_event.txt', root = '/tmp', ft = 'text' }
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
assert.is_true(fired)
compiler.unwatch(bufnr)
helpers.delete_buffer(bufnr)
end)
it('toggles off when called again', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch_toggle.txt')
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return { bufnr = b, file = '/tmp/preview_test_watch_toggle.txt', root = '/tmp', ft = 'text' }
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
assert.is_not_nil(compiler._test.watching[bufnr])
compiler.watch(bufnr, 'echo', provider, ctx_builder)
assert.is_nil(compiler._test.watching[bufnr])
helpers.delete_buffer(bufnr)
end)
it('fires PreviewWatchStopped on unwatch', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch_stop.txt')
local stopped = false
vim.api.nvim_create_autocmd('User', {
pattern = 'PreviewWatchStopped',
once = true,
callback = function()
stopped = true
end,
})
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return { bufnr = b, file = '/tmp/preview_test_watch_stop.txt', root = '/tmp', ft = 'text' }
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
compiler.unwatch(bufnr)
assert.is_true(stopped)
assert.is_nil(compiler._test.watching[bufnr])
helpers.delete_buffer(bufnr)
end)
it('stop_all clears watches', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch_stopall.txt')
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return {
bufnr = b,
file = '/tmp/preview_test_watch_stopall.txt',
root = '/tmp',
ft = 'text',
}
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
assert.is_not_nil(compiler._test.watching[bufnr])
compiler.stop_all()
assert.is_nil(compiler._test.watching[bufnr])
helpers.delete_buffer(bufnr)
end)
it('status includes watching state', function()
local bufnr = helpers.create_buffer({ 'hello' }, 'text')
vim.api.nvim_buf_set_name(bufnr, '/tmp/preview_test_watch_status.txt')
local s = compiler.status(bufnr)
assert.is_false(s.watching)
local provider = { cmd = { 'echo', 'ok' } }
local ctx_builder = function(b)
return { bufnr = b, file = '/tmp/preview_test_watch_status.txt', root = '/tmp', ft = 'text' }
end
compiler.watch(bufnr, 'echo', provider, ctx_builder)
s = compiler.status(bufnr)
assert.is_true(s.watching)
compiler.unwatch(bufnr)
helpers.delete_buffer(bufnr)
end)
end)
end)

View file

@ -5,7 +5,7 @@ describe('diagnostic', function()
before_each(function()
helpers.reset_config()
diagnostic = require('render.diagnostic')
diagnostic = require('preview.diagnostic')
end)
describe('clear', function()

View file

@ -20,8 +20,8 @@ function M.delete_buffer(bufnr)
end
function M.reset_config(opts)
vim.g.render = opts
require('render')._test.reset()
vim.g.preview = opts
require('preview')._test.reset()
end
return M

View file

@ -1,30 +1,29 @@
local helpers = require('spec.helpers')
describe('render', function()
local render
describe('preview', function()
local preview
before_each(function()
helpers.reset_config()
render = require('render')
preview = require('preview')
end)
describe('config', function()
it('accepts nil config', function()
assert.has_no.errors(function()
render.get_config()
preview.get_config()
end)
end)
it('applies default values', function()
local config = render.get_config()
local config = preview.get_config()
assert.is_false(config.debug)
assert.are.same({}, config.providers)
assert.are.same({}, config.providers_by_ft)
end)
it('merges user config with defaults', function()
helpers.reset_config({ debug = true })
local config = require('render').get_config()
local config = require('preview').get_config()
assert.is_true(config.debug)
assert.are.same({}, config.providers)
end)
@ -37,13 +36,9 @@ describe('render', function()
args = { '%s' },
},
},
providers_by_ft = {
typst = 'typst',
},
})
local config = require('render').get_config()
local config = require('preview').get_config()
assert.is_not_nil(config.providers.typst)
assert.are.equal('typst', config.providers_by_ft.typst)
end)
end)
@ -53,34 +48,20 @@ describe('render', function()
providers = {
typst = { cmd = { 'typst', 'compile' } },
},
providers_by_ft = {
typst = 'typst',
},
})
render = require('render')
preview = require('preview')
end)
it('returns provider name for mapped filetype', function()
it('returns filetype when provider exists', function()
local bufnr = helpers.create_buffer({}, 'typst')
local name = render.resolve_provider(bufnr)
local name = preview.resolve_provider(bufnr)
assert.are.equal('typst', name)
helpers.delete_buffer(bufnr)
end)
it('returns nil for unmapped filetype', function()
it('returns nil for unconfigured filetype', function()
local bufnr = helpers.create_buffer({}, 'lua')
local name = render.resolve_provider(bufnr)
assert.is_nil(name)
helpers.delete_buffer(bufnr)
end)
it('returns nil when provider name maps to missing config', function()
helpers.reset_config({
providers = {},
providers_by_ft = { typst = 'typst' },
})
local bufnr = helpers.create_buffer({}, 'typst')
local name = require('render').resolve_provider(bufnr)
local name = preview.resolve_provider(bufnr)
assert.is_nil(name)
helpers.delete_buffer(bufnr)
end)
@ -89,7 +70,7 @@ describe('render', function()
describe('build_context', function()
it('builds context from buffer', function()
local bufnr = helpers.create_buffer({}, 'typst')
local ctx = render.build_context(bufnr)
local ctx = preview.build_context(bufnr)
assert.are.equal(bufnr, ctx.bufnr)
assert.are.equal('typst', ctx.ft)
assert.is_string(ctx.file)
@ -101,7 +82,7 @@ describe('render', function()
describe('status', function()
it('returns idle when nothing is compiling', function()
local bufnr = helpers.create_buffer({})
local s = render.status(bufnr)
local s = preview.status(bufnr)
assert.is_false(s.compiling)
assert.is_nil(s.provider)
helpers.delete_buffer(bufnr)

88
spec/presets_spec.lua Normal file
View file

@ -0,0 +1,88 @@
describe('presets', function()
local presets
before_each(function()
presets = require('preview.presets')
end)
local ctx = {
bufnr = 1,
file = '/tmp/document.typ',
root = '/tmp',
ft = 'typst',
}
describe('typst', function()
it('has cmd', function()
assert.are.same({ 'typst', 'compile' }, presets.typst.cmd)
end)
it('returns args with file path', function()
local args = presets.typst.args(ctx)
assert.is_table(args)
assert.are.same({ '/tmp/document.typ' }, args)
end)
it('returns pdf output path', function()
local output = presets.typst.output(ctx)
assert.is_string(output)
assert.are.equal('/tmp/document.pdf', output)
end)
end)
describe('latex', function()
local tex_ctx = {
bufnr = 1,
file = '/tmp/document.tex',
root = '/tmp',
ft = 'tex',
}
it('has cmd', function()
assert.are.same({ 'latexmk' }, presets.latex.cmd)
end)
it('returns args with pdf flag and file path', function()
local args = presets.latex.args(tex_ctx)
assert.is_table(args)
assert.are.same({ '-pdf', '-interaction=nonstopmode', '/tmp/document.tex' }, args)
end)
it('returns pdf output path', function()
local output = presets.latex.output(tex_ctx)
assert.is_string(output)
assert.are.equal('/tmp/document.pdf', output)
end)
it('returns clean command', function()
local clean = presets.latex.clean(tex_ctx)
assert.is_table(clean)
assert.are.same({ 'latexmk', '-c', '/tmp/document.tex' }, clean)
end)
end)
describe('markdown', function()
local md_ctx = {
bufnr = 1,
file = '/tmp/document.md',
root = '/tmp',
ft = 'markdown',
}
it('has cmd', function()
assert.are.same({ 'pandoc' }, presets.markdown.cmd)
end)
it('returns args with file and output flag', function()
local args = presets.markdown.args(md_ctx)
assert.is_table(args)
assert.are.same({ '/tmp/document.md', '-o', '/tmp/document.pdf' }, args)
end)
it('returns pdf output path', function()
local output = presets.markdown.output(md_ctx)
assert.is_string(output)
assert.are.equal('/tmp/document.pdf', output)
end)
end)
end)