Compare commits
20 commits
fix/asciid
...
build/nix-
| Author | SHA1 | Date | |
|---|---|---|---|
| e76ace674f | |||
|
|
9fe68dd159 | ||
| 6f090fdcf3 | |||
|
|
31dcf9c91f | ||
|
|
23aa8acc55 | ||
|
|
837c97cd09 | ||
|
|
7895b67c21 | ||
|
|
f1aed82f42 | ||
|
|
bb9ca987e1 | ||
| 8107f8c0ac | |||
|
|
cf8fd02e6d | ||
|
|
3e6ba580e4 | ||
|
|
c8e3a88434 | ||
|
|
239f8a4769 | ||
| df0765a27f | |||
| 934ef03728 | |||
| e8f93fb47e | |||
|
|
7a11f39341 | ||
|
|
68e2e82232 | ||
|
|
d4e7d8c2fd |
9 changed files with 584 additions and 331 deletions
54
README.md
54
README.md
|
|
@ -1,17 +1,19 @@
|
|||
# preview.nvim
|
||||
|
||||
**Async document compilation for Neovim**
|
||||
**Universal document previewer for Neovim**
|
||||
|
||||
An extensible framework for compiling documents (LaTeX, Typst, Markdown, etc.)
|
||||
asynchronously with error diagnostics.
|
||||
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>
|
||||
|
||||
## Features
|
||||
|
||||
- Async compilation via `vim.system()`
|
||||
- Built-in presets for Typst, LaTeX, Markdown, and GitHub-flavored Markdown
|
||||
- Compiler errors as native `vim.diagnostic`
|
||||
- User events for extensibility (`PreviewCompileStarted`,
|
||||
`PreviewCompileSuccess`, `PreviewCompileFailed`)
|
||||
- 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
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -19,8 +21,18 @@ asynchronously with error diagnostics.
|
|||
|
||||
## Installation
|
||||
|
||||
Install with your package manager of choice or via
|
||||
[luarocks](https://luarocks.org/modules/barrettruth/preview.nvim):
|
||||
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):
|
||||
|
||||
```
|
||||
luarocks install preview.nvim
|
||||
|
|
@ -37,35 +49,35 @@ luarocks install preview.nvim
|
|||
**Q: How do I define a custom provider?**
|
||||
|
||||
```lua
|
||||
require('preview').setup({
|
||||
typst = {
|
||||
cmd = { 'typst', 'compile' },
|
||||
vim.g.preview = {
|
||||
rst = {
|
||||
cmd = { 'rst2html' },
|
||||
args = function(ctx)
|
||||
return { ctx.file }
|
||||
return { ctx.file, ctx.output }
|
||||
end,
|
||||
output = function(ctx)
|
||||
return ctx.file:gsub('%.typ$', '.pdf')
|
||||
return ctx.file:gsub('%.rst$', '.html')
|
||||
end,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Q: How do I override a preset?**
|
||||
|
||||
```lua
|
||||
require('preview').setup({
|
||||
vim.g.preview = {
|
||||
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. For
|
||||
a specific application, pass a command table:
|
||||
open the output with `vim.ui.open()` after the first successful compilation in
|
||||
toggle/watch mode. For a specific application, pass a command table:
|
||||
|
||||
```lua
|
||||
require('preview').setup({
|
||||
vim.g.preview = {
|
||||
typst = { open = { 'sioyek', '--new-instance' } },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,267 +0,0 @@
|
|||
*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 opt-in presets for common tools (Typst, LaTeX, Pandoc)
|
||||
and supports fully custom providers. See |preview.nvim-presets|.
|
||||
|
||||
==============================================================================
|
||||
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.
|
||||
|
||||
`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[].
|
||||
|
||||
*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 overriding the output path (e.g. latexmk `$out_dir`):~
|
||||
>lua
|
||||
require('preview').setup({
|
||||
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
|
||||
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.pdflatex` pdflatex → PDF (single pass, no latexmk)
|
||||
`presets.tectonic` tectonic → PDF (Rust-based LaTeX engine)
|
||||
`presets.markdown` pandoc → HTML (standalone, embedded)
|
||||
`presets.github` pandoc → HTML (GitHub-styled, `-f gfm` input)
|
||||
`presets.asciidoctor` asciidoctor → HTML (AsciiDoc with SSE reload)
|
||||
`presets.quarto` quarto render → HTML (scientific publishing)
|
||||
|
||||
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:~
|
||||
|
||||
`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).
|
||||
|
||||
==============================================================================
|
||||
API *preview.nvim-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.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:
|
||||
276
doc/preview.txt
Normal file
276
doc/preview.txt
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
*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:
|
||||
26
flake.nix
26
flake.nix
|
|
@ -19,9 +19,9 @@
|
|||
{
|
||||
formatter = forEachSystem (pkgs: pkgs.nixfmt-tree);
|
||||
|
||||
devShells = forEachSystem (pkgs: {
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
devShells = forEachSystem (pkgs:
|
||||
let
|
||||
devTools = [
|
||||
(pkgs.luajit.withPackages (
|
||||
ps: with ps; [
|
||||
busted
|
||||
|
|
@ -33,7 +33,23 @@
|
|||
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
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,49 @@ 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[]
|
||||
|
|
@ -104,12 +143,47 @@ 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
|
||||
|
|
@ -143,7 +217,9 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
})
|
||||
end
|
||||
vim.fn.setqflist(items, 'r')
|
||||
vim.cmd('copen')
|
||||
local win = vim.fn.win_getid()
|
||||
vim.cmd.cwindow()
|
||||
vim.fn.win_gotoid(win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -160,20 +236,51 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
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)
|
||||
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)
|
||||
)
|
||||
end
|
||||
opened[bufnr] = true
|
||||
end
|
||||
|
||||
active[bufnr] = { obj = obj, provider = name, output_file = output_file, is_reload = true }
|
||||
|
|
@ -183,6 +290,10 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
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,
|
||||
})
|
||||
|
|
@ -227,6 +338,7 @@ 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',
|
||||
|
|
@ -240,17 +352,12 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
end
|
||||
if
|
||||
provider.open
|
||||
and not opts.oneshot
|
||||
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
|
||||
do_open(bufnr, output_file, provider.open)
|
||||
opened[bufnr] = true
|
||||
end
|
||||
else
|
||||
|
|
@ -273,7 +380,9 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
})
|
||||
end
|
||||
vim.fn.setqflist(items, 'r')
|
||||
vim.cmd('copen')
|
||||
local win = vim.fn.win_getid()
|
||||
vim.cmd.cwindow()
|
||||
vim.fn.win_gotoid(win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -297,6 +406,9 @@ function M.compile(bufnr, name, provider, ctx, opts)
|
|||
once = true,
|
||||
callback = function()
|
||||
M.stop(bufnr)
|
||||
if not provider.detach then
|
||||
close_viewer(bufnr)
|
||||
end
|
||||
last_output[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
|
|
@ -337,6 +449,12 @@ 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
|
||||
|
||||
|
|
@ -392,6 +510,10 @@ function M.toggle(bufnr, name, provider, ctx_builder)
|
|||
once = true,
|
||||
callback = function()
|
||||
M.unwatch(bufnr)
|
||||
stop_open_watcher(bufnr)
|
||||
if not provider.detach then
|
||||
close_viewer(bufnr)
|
||||
end
|
||||
opened[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
|
|
@ -469,13 +591,7 @@ function M.open(bufnr, open_config)
|
|||
log.dbg('output file no longer exists for buffer %d: %s', bufnr, output)
|
||||
return false
|
||||
end
|
||||
if type(open_config) == 'table' then
|
||||
local open_cmd = vim.list_extend({}, open_config)
|
||||
table.insert(open_cmd, output)
|
||||
vim.system(open_cmd)
|
||||
else
|
||||
vim.ui.open(output)
|
||||
end
|
||||
do_open(bufnr, output, open_config)
|
||||
return true
|
||||
end
|
||||
|
||||
|
|
@ -500,6 +616,8 @@ M._test = {
|
|||
opened = opened,
|
||||
last_output = last_output,
|
||||
debounce_timers = debounce_timers,
|
||||
viewer_procs = viewer_procs,
|
||||
open_watchers = open_watchers,
|
||||
}
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
---@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
|
||||
|
|
@ -101,6 +102,13 @@ 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, {
|
||||
|
|
@ -246,4 +254,8 @@ M._test = {
|
|||
end,
|
||||
}
|
||||
|
||||
if vim.g.preview then
|
||||
M.setup(vim.g.preview)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -128,9 +128,12 @@ 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', ctx.file }
|
||||
return { 'typst', 'watch', '--diagnostic-format', 'short', ctx.file }
|
||||
end,
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +175,10 @@ M.pdflatex = {
|
|||
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,
|
||||
}
|
||||
|
||||
|
|
@ -188,6 +195,9 @@ M.tectonic = {
|
|||
error_parser = function(output)
|
||||
return parse_latexmk(output)
|
||||
end,
|
||||
clean = function(ctx)
|
||||
return { 'rm', '-f', (ctx.file:gsub('%.tex$', '.pdf')) }
|
||||
end,
|
||||
open = true,
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +256,7 @@ M.asciidoctor = {
|
|||
ft = 'asciidoc',
|
||||
cmd = { 'asciidoctor' },
|
||||
args = function(ctx)
|
||||
return { ctx.file, '-o', ctx.output }
|
||||
return { '--failure-level', 'ERROR', ctx.file, '-o', ctx.output }
|
||||
end,
|
||||
output = function(ctx)
|
||||
return (ctx.file:gsub('%.adoc$', '.html'))
|
||||
|
|
@ -261,6 +271,68 @@ M.asciidoctor = {
|
|||
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',
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ 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 .luarc.json --checklevel=Warning
|
||||
nix develop --command lua-language-server --check lua/ --configpath "$(pwd)/.luarc.json" --checklevel=Warning
|
||||
nix develop --command busted
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ 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)
|
||||
|
|
@ -46,7 +50,9 @@ describe('presets', function()
|
|||
assert.is_table(result)
|
||||
assert.are.equal('typst', result[1])
|
||||
assert.are.equal('watch', result[2])
|
||||
assert.are.equal(ctx.file, result[3])
|
||||
assert.are.equal('--diagnostic-format', result[3])
|
||||
assert.are.equal('short', result[4])
|
||||
assert.are.equal(ctx.file, result[5])
|
||||
end)
|
||||
|
||||
it('parses errors from stderr', function()
|
||||
|
|
@ -189,8 +195,16 @@ describe('presets', function()
|
|||
assert.is_true(presets.pdflatex.open)
|
||||
end)
|
||||
|
||||
it('has no clean command', function()
|
||||
assert.is_nil(presets.pdflatex.clean)
|
||||
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()
|
||||
|
|
@ -240,8 +254,8 @@ describe('presets', function()
|
|||
assert.is_true(presets.tectonic.open)
|
||||
end)
|
||||
|
||||
it('has no clean command', function()
|
||||
assert.is_nil(presets.tectonic.clean)
|
||||
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()
|
||||
|
|
@ -467,7 +481,7 @@ describe('presets', function()
|
|||
|
||||
it('returns args with file and output', function()
|
||||
assert.are.same(
|
||||
{ '/tmp/document.adoc', '-o', '/tmp/document.html' },
|
||||
{ '--failure-level', 'ERROR', '/tmp/document.adoc', '-o', '/tmp/document.html' },
|
||||
presets.asciidoctor.args(adoc_ctx)
|
||||
)
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue