diff --git a/README.md b/README.md index 80a2650..6d896ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # preview.nvim -**Universal document previewer for Neovim** +**Universal previewer for Neovim** An extensible framework for compiling and previewing _any_ documents (LaTeX, Typst, Markdown, etc.)—diagnostics included. @@ -81,3 +81,8 @@ vim.g.preview = { typst = { open = { 'sioyek', '--new-instance' } }, } ``` + +**Q: How do I set up SyncTeX (forward/inverse search)?** + +See `:help preview-synctex` for full recipes covering Zathura, Sioyek, and +Okular. diff --git a/doc/preview.txt b/doc/preview.txt index 383a8f5..ca4610a 100644 --- a/doc/preview.txt +++ b/doc/preview.txt @@ -26,6 +26,7 @@ CONTENTS *preview-contents* 7. Lua API ................................................... |preview-api| 8. Events ............................................... |preview-events| 9. Health ............................................... |preview-health| + 10. SyncTeX ............................................. |preview-synctex| ============================================================================== REQUIREMENTS *preview-requirements* @@ -272,5 +273,116 @@ Checks: ~ - Each configured provider's binary is executable - Each configured provider's opener binary (if any) is executable +============================================================================== +SYNCTEX *preview-synctex* + +SyncTeX enables bidirectional navigation between LaTeX source and the +compiled PDF. The `latex` preset compiles with `-synctex=1` by default. + +Forward search (editor -> viewer) requires caching the output path. +Inverse search (viewer -> editor) requires a fixed Neovim server socket. + +The following configs leverage the below basic setup: ~ + +>lua + vim.fn.serverstart('/tmp/nvim-preview.sock') + + local synctex_pdf = {} + vim.api.nvim_create_autocmd('User', { + pattern = 'PreviewCompileSuccess', + callback = function(args) + synctex_pdf[args.data.bufnr] = args.data.output + end, + }) +< + +The recipes below bind `s` for forward search. To scroll the PDF +automatically on cursor movement, call the forward search function from a +|CursorMoved| or |CursorHold| autocmd instead. + +Viewer-specific recipes: ~ + + *preview-synctex-zathura* +Zathura ~ + +Inverse search: Ctrl+click. + +>lua + vim.keymap.set('n', 's', function() + local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] + if pdf then + vim.fn.jobstart({ + 'zathura', '--synctex-forward', + vim.fn.line('.') .. ':0:' .. vim.fn.expand('%:p'), pdf, + }) + end + end) + + vim.g.preview = { + latex = { + open = { + 'zathura', + '--synctex-editor-command', + 'nvim --server /tmp/nvim-preview.sock' + .. [[ --remote-expr "execute('b +%{line} %{input}')"]], + }, + }, + } +< + + *preview-synctex-sioyek* +Sioyek ~ + +Inverse search: right-click with synctex mode active. + +Add to `~/.config/sioyek/prefs_user.config`: > + inverse_search_command nvim --server /tmp/nvim-preview.sock --remote-expr "execute('b +%2 %1')" +< + +>lua + vim.keymap.set('n', 's', function() + local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] + if pdf then + vim.fn.jobstart({ + 'sioyek', + '--instance-name', 'preview', + '--forward-search-file', vim.fn.expand('%:p'), + '--forward-search-line', tostring(vim.fn.line('.')), + pdf, + }) + end + end) + + vim.g.preview = { + latex = { + open = { 'sioyek', '--instance-name', 'preview' }, + }, + } +< + + *preview-synctex-okular* +Okular ~ + +Inverse search (Shift+click): one-time GUI setup via +Settings -> Configure Okular -> Editor -> Custom Text Editor: > + nvim --server /tmp/nvim-preview.sock --remote-expr "execute('b +%l %f')" +< + +>lua + vim.keymap.set('n', 's', function() + local pdf = synctex_pdf[vim.api.nvim_get_current_buf()] + if pdf then + vim.fn.jobstart({ + 'okular', '--unique', + ('%s#src:%d:%s'):format(pdf, vim.fn.line('.'), vim.fn.expand('%:p')), + }) + end + end) + + vim.g.preview = { + latex = { open = { 'okular', '--unique' } }, + } +< + ============================================================================== vim:tw=78:ts=8:ft=help:norl: diff --git a/flake.nix b/flake.nix index 636f4d0..bb71950 100644 --- a/flake.nix +++ b/flake.nix @@ -19,7 +19,8 @@ { formatter = forEachSystem (pkgs: pkgs.nixfmt-tree); - devShells = forEachSystem (pkgs: + devShells = forEachSystem ( + pkgs: let devTools = [ (pkgs.luajit.withPackages ( @@ -33,6 +34,16 @@ pkgs.selene pkgs.lua-language-server ]; + okular-wrapped = pkgs.symlinkJoin { + name = "okular"; + paths = [ pkgs.kdePackages.okular ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/okular \ + --prefix XDG_DATA_DIRS : "${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}" \ + --prefix XDG_DATA_DIRS : "${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}" + ''; + }; in { default = pkgs.mkShell { @@ -48,8 +59,12 @@ pkgs.quarto pkgs.plantuml pkgs.mermaid-cli + pkgs.zathura + pkgs.sioyek + okular-wrapped ]; }; - }); + } + ); }; } diff --git a/lua/preview/presets.lua b/lua/preview/presets.lua index 1b5333e..d4b03cc 100644 --- a/lua/preview/presets.lua +++ b/lua/preview/presets.lua @@ -115,6 +115,24 @@ local function parse_asciidoctor(output) return diagnostics end +---@param output string +---@return preview.Diagnostic[] +local function parse_mermaid(output) + local lnum = output:match('Parse error on line (%d+)') + if not lnum then + return {} + end + local msg = output:match('(Expecting .+)') or 'parse error' + return { + { + lnum = tonumber(lnum) - 1, + col = 0, + message = msg, + severity = vim.diagnostic.severity.ERROR, + }, + } +end + ---@type preview.ProviderConfig M.typst = { ft = 'typst', @@ -313,19 +331,7 @@ M.mermaid = { 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 + return parse_mermaid(output) end, clean = function(ctx) return { 'rm', '-f', (ctx.file:gsub('%.mmd$', '.svg')) }