ywsrock/nvim-md-preview

github github
programming-languages-supportmarkdown-and-latex
stars 2
issues 1
subscribers 0
forks 0
CREATED

UPDATED


nvim-md-preview

The Markdown preview that follows your cursor, and lets you click back. One small Rust binary. No Node.js, no Deno, works offline.

CI Release Neovim License: MIT

日本語

Typing in Neovim updates the preview, moving the cursor scrolls it, clicking the preview moves the cursor, and :MdPreviewDiff shows the changes

Why nvim-md-preview

  • Your cursor drives the preview, and the preview drives your cursor. The preview scrolls block by block as you move, and clicking any paragraph puts the cursor on that line in Neovim.
  • Nothing to install but one binary. About 2.5 MB of Rust with Mermaid, KaTeX, highlight.js and fonts built in. No Node.js or Deno, no network needed once installed.
  • It goes beyond previewing. Export to PDF, DOCX, HTML or EPUB, review your changes against Git, and present the document as slides, all from the same window.

Requires Neovim 0.9 or later. Vim is not supported.

Quick start

With lazy.nvim:

{
  "ywsrock/nvim-md-preview",
  build = ":MdPreviewInstall",
  config = function()
    require("md_preview").setup({})
  end,
}

Open a Markdown file, or any text file, and run :MarkdownPreview. That's it. The plugin only defines its commands at startup; the rest loads when you use them.

" vim-plug: then run :MdPreviewInstall once
Plug 'ywsrock/nvim-md-preview'
-- packer.nvim
use { "ywsrock/nvim-md-preview", run = ":MdPreviewInstall" }

:MdPreviewInstall downloads the binary that matches the plugin version (plus pandoc and Typst for export) in parallel with progress on the command line. If you update the plugin without a build hook, the preview warns when the binary is out of date; run :MdPreviewInstall again. Something wrong? Run :checkhealth md_preview.

How it compares

nvim-md-preview markdown-preview.nvim peek.nvim
Runtime Single Rust binary Node.js app (prebuilt bundle or build with yarn) Deno
Works in Vim No Yes No
Preview follows the cursor Block by block Yes Yes
Click the preview to jump in Neovim Yes No No
Export to PDF / DOCX / HTML / EPUB Built in Browser print No
Git diff of the document Yes No No
Slides from the same file Yes No No

In-buffer renderers such as render-markdown.nvim and markview.nvim solve a different problem. Use them while editing and nvim-md-preview when you want the real page, diagrams and math, or an exported file. They work well together.

This table reflects our understanding as of October 2026. If something is wrong, please open an issue.

Highlights

Two-way sync

The preview keeps the block under your cursor in view, using source-line markers instead of guessing from the scroll ratio. Click a paragraph in the preview and Neovim moves there. Local .md links and [[wiki links]] open in Neovim too.

Everything renders, offline

Mermaid diagrams, KaTeX math and syntax highlighting for 190 languages are built into the binary, so nothing is fetched from the network.

PlantUML blocks (```plantuml or ```puml) are drawn with your local plantuml command when it is installed. To use a PlantUML server instead, set plantuml_server; diagram sources are sent to that server only when you set it.

Tables and math. GitHub-flavored tables with alignment, inline and display math.

Code, callouts and tasks. GitHub alerts and Obsidian callouts, including foldable ones, task lists and footnotes.

See what changed

Changes against HEAD are shown as soon as you edit a file in a Git repository, unsaved edits included. :MdPreviewDiff main compares with a branch instead, :MdPreviewDiff staged with the index, and :MdPreviewDiff off hides it. Changed words are underlined or struck through in place, tables and math show the previous version in a collapsed box, removed paragraphs stay visible, and ↑ / ↓ in the header jump from change to change. The markers beside the scrollbar show where the changes are.

Any file, not just Markdown

Markdown, vimwiki, Quarto, R Markdown and MDX files are rendered as documents. Every other text file is shown as highlighted code with line numbers, and scroll sync, click-to-jump, search and the Git diff work the same way. Help, file explorers, terminals and other special buffers are skipped, and so are binary files and files over 2 MB. Limit the file types with filetypes, for example { "markdown", "vimwiki" }.

Find your way in long documents

Open the table of contents with t and search with / or Ctrl+F. The current section is highlighted as you scroll.

Present and print

Slides. Press p to present the document, split on --- or on headings. The slide follows your cursor. :MdPreviewExport slides saves them as a PDF, one slide per page.

Print preview. See the document on an A4 sheet with page-break guides, then print from the browser.

Export

:MdPreviewExport pdf writes a PDF next to your file that looks exactly like the preview: diagrams, math, callouts and highlighted code included. It prints the preview with the Chrome, Edge, Brave or Chromium already on your machine, in the background, with paper size, margins, font, a table of contents and numbered headings from your settings. DOCX, HTML and EPUB go through pandoc. Unsaved edits are included.

[!NOTE] Without one of those browsers, PDFs are made with Typst instead (:checkhealth md_preview tells you which one is used). Typst output supports custom templates, but does not render Mermaid diagrams, math or callouts.

Recipes

{
  "ywsrock/nvim-md-preview",
  lazy = false, -- keep the commands available in every buffer (keys alone would lazy-load)
  build = ":MdPreviewInstall",
  config = function() require("md_preview").setup({}) end,
  keys = {
    { "<leader>mp", "<cmd>MarkdownPreviewToggle<cr>", desc = "Markdown: preview" },
    { "<leader>md", "<cmd>MdPreviewDiff<cr>", desc = "Markdown: Git diff" },
    {
      "<leader>me",
      function()
        vim.ui.select({ "pdf", "docx", "html", "epub" }, { prompt = "Export as" }, function(format)
          if format then vim.cmd("MdPreviewExport " .. format) end
        end)
      end,
      desc = "Markdown: export",
    },
  },
}

Start it with the first Markdown file you open. With follow_buffer (on by default), the preview then switches to whichever buffer you are in, if it can be previewed.

vim.api.nvim_create_autocmd("FileType", {
  pattern = "markdown",
  once = true,
  command = "MarkdownPreview",
})

[[Page]] and [[Page|label]] links and Obsidian callouts (> [!info] Title, foldable > [!todo]-) work out of the box. Clicking a wiki link opens the note in Neovim, matching names regardless of case, spaces, - and _. Add your own look with custom_css = "~/notes/preview.css".

Check out the branch and run :MdPreviewDiff main. Every changed paragraph is marked against main, with ↑ / ↓ in the preview header to walk through them. :MdPreviewDiff staged shows what you are about to commit, and :MdPreviewDiff off hides it again.

Browser-made PDFs (the default) use the preview's styles, so custom_css with a print rule is all you need:

/* ~/.config/nvim/preview.css */
@media print {
  #content { font-family: "Inter", sans-serif; }
  #content h1 { color: #0b3d91; }
}
require("md_preview").setup({
  custom_css = "~/.config/nvim/preview.css",
  export = { paper = "a4", toc = true, numbering = true },
})

For full control over the page, use Typst and a template:

require("md_preview").setup({
  export = { pdf_engine = "typst", template = "~/.config/nvim/report.typ" },
})
// ~/.config/nvim/report.typ
#let template(doc) = {
  set page(header: align(right)[Internal])
  set text(font: "Inter", size: 10pt)
  doc
}

Then :MdPreviewExport pdf. Relative images are resolved from the document's folder.

Set host = "0.0.0.0". Each time the preview starts, Neovim shows LAN URLs with a one-time token. Open one on your tablet and it follows your cursor like the desktop browser does.

Usage

Command
:MarkdownPreview / :MdPreview Start the preview
:MarkdownPreviewStop / :MdPreviewStop Stop it
:MarkdownPreviewToggle Start or stop
:MdPreviewExport {format} [path] Export the buffer: pdf, slides (PDF, one slide per page), docx, html, epub, markdown. Saved next to the file unless path is given
:MdPreviewDiff [base] Toggle the Git diff, or compare with base (HEAD, staged, a branch, tag or commit). off hides it
:MdPreviewInfo [copy] Show the preview URL, the file, and which PDF and PlantUML tools are used. copy copies the URL
:MdPreviewInstall Download or update the binary, pandoc and Typst

In the preview: t table of contents, / or Ctrl+F search, p slides, click to jump. The toolbar buttons do the same. If these keys clash with a browser extension such as Vimium, set browser_keys = false (see FAQ).

Full reference: :help md_preview.

Configuration

Every option is optional. Call setup() with only what you want to change:

require("md_preview").setup({
  theme = "light",
  git_diff_base = "main",
  export = { paper = "us-letter", toc = true },
})

General

Option Default Description
filetypes { "*" } File types (Neovim filetype) that can be previewed. "*" means every file. Markdown, vimwiki, Quarto, R Markdown, pandoc and MDX are rendered as documents; other files are shown as highlighted code. Special buffers (help, file explorers, terminals) are never previewed.
max_file_size 2097152 (2 MB) Files larger than this, in bytes, are not previewed. Binary files are never previewed.
port 9876 Port of the preview server. If it is taken, the next free port is used.
host "127.0.0.1" Address to listen on. "0.0.0.0" lets other devices on your LAN open the preview; a token is then required (see "Follow along on a tablet" in Recipes).
auto_open true Open the browser when the preview starts.
browser_app false Open the preview in an app window without tabs or an address bar (--app of Chrome, Edge, Brave or Chromium), handy next to the editor. Uses export.browser_path, or finds one of those browsers (on WSL, the Windows one). Without one, the default browser is used.
binary Downloaded copy in stdpath("data")/nvim-md-preview/ Path to the preview binary. Set it when you build from source.
log_file "" File to append a log to: start and stop, the port, the tools used, export results and errors from the binary. Document contents are never written, and the LAN token is masked. Empty writes nothing. Useful for bug reports.

Preview

Option Default Description
scroll_sync true Scroll the preview to the block under the cursor.
cursor_highlight true Outline the block under the cursor in the preview: the paragraph, heading, list item, table or code block, or the line in code files. Works with scroll_sync = false too. Style it with .cursor-block in custom_css.
follow_buffer true Switch the preview when you enter another buffer that can be previewed (see filetypes). Special buffers such as help, file explorers and terminals are skipped.
debounce_ms 80 Milliseconds to wait after typing before the preview updates.
theme "auto" "auto" follows the OS, or "dark" / "light". A theme chosen with the button in the preview is remembered and takes precedence.
custom_css "" Path to a CSS file loaded after the built-in styles, so it can override them.
wiki_links true Render [[Page]] and [[Page|label]] as links that open in Neovim.
task_toggle true Click a task list checkbox (- [ ]) in the preview to toggle it in the Neovim buffer, as one change you can undo. If the preview is behind the buffer (for example, right after typing), nothing is changed and you are asked to try again. Not available in LAN mode.
browser_keys true Keyboard shortcuts in the preview (t, /, Ctrl+F, p). Set false if they clash with a browser extension such as Vimium (see FAQ).

Git diff

Option Default Description
git_diff true Show changes against git_diff_base in the preview. Toggle with :MdPreviewDiff. Files outside a Git repository show nothing.
git_diff_base "HEAD" What to compare against: "HEAD", "staged" (the index), or a branch, tag or commit. :MdPreviewDiff <base> changes it for the session.
git_diff_colors true Use the editor's diff colors (GitSignsAdd, Added, DiffAdd, ...). Colors that are hard to read on the preview's theme fall back to green, yellow and red.

PlantUML

Option Default Description
plantuml_path "" The plantuml command or a plantuml.jar (run with java -jar) used for ```plantuml and ```puml blocks. Empty uses plantuml on PATH. Without it, the blocks are shown as code.
plantuml_server "" A PlantUML server URL, for example "https://www.plantuml.com/plantuml". When set, the browser loads the diagrams from this server instead of running plantuml, so the diagram sources leave your machine. Empty never uses a server.

Export (export = { ... })

Option Default Description
formats { "pdf", "slides", "docx", "html", "epub", "markdown" } Formats offered in the preview's Export menu and in :MdPreviewExport completion. {} hides the Export button.
pdf_engine "browser" How PDFs are made. "browser" prints the preview with Chrome, Edge, Brave or Chromium, so the PDF looks like the preview; without one of them, Typst is used. "typst" always uses Typst (supports template, but not Mermaid, math or callouts).
browser_path "" Browser used for PDFs. Empty looks for Chrome, Edge, Brave and Chromium in their usual places and on your PATH.
docx_engine "pandoc" "pandoc" makes :MdPreviewInstall download pandoc for DOCX, HTML and EPUB. Any other value skips it.
pandoc_path "" Path to pandoc. Empty uses the downloaded copy, then pandoc on your PATH.
typst_path "" Path to Typst. Empty uses the downloaded copy, then typst on your PATH.
paper "a4" PDF paper size. Any Typst paper name, such as "us-letter" or "a5".
margin "2cm" PDF page margin.
font "" PDF body font. Empty uses Noto Serif CJK JP with fallbacks.
font_size 11 PDF body font size in points.
toc false Add a table of contents (PDF, DOCX, HTML, EPUB).
numbering false Number headings (PDF, DOCX, HTML, EPUB).
slide_size "16:9" Page shape of :MdPreviewExport slides: "16:9" (13.33 x 7.5 in) or "4:3" (10 x 7.5 in). Slides are printed with Chrome, Edge, Brave or Chromium, whatever pdf_engine is; a slide too long for one page is scaled down to fit.
template "" Typst file that exports a template function, applied to the whole PDF when pdf_engine = "typst" (see Recipes).

Vimscript

Without setup(), these variables are read when the plugin loads:

Variable Same as
g:md_preview_port port
g:md_preview_auto_open auto_open (0 turns it off)
-- Markdown preview (nvim-md-preview)
require("md_preview").setup({
  -- General
  filetypes = { "*" },              -- file types that can be previewed ("*" means all)
  max_file_size = 2 * 1024 * 1024,  -- larger files are not previewed (bytes)
  binary = vim.fn.stdpath("data") .. "/nvim-md-preview/nvim-md-preview",  -- path to the binary
  port = 9876,                      -- server port (the next free one if taken)
  host = "127.0.0.1",               -- address to listen on ("0.0.0.0" for the LAN)
  auto_open = true,                 -- open the browser when the preview starts
  browser_app = false,              -- open in an app window without tabs or an address bar
  log_file = "",                    -- file to write a log to (empty: none)

  -- Preview
  scroll_sync = true,               -- scroll the preview with the cursor
  cursor_highlight = true,          -- outline the block under the cursor
  follow_buffer = true,             -- switch when you enter another previewable buffer
  debounce_ms = 80,                 -- wait after typing before updating (ms)
  theme = "auto",                   -- "auto" (follow the OS) | "dark" | "light"
  custom_css = "",                  -- extra CSS file
  wiki_links = true,                -- render [[Page]] as links that open in Neovim
  task_toggle = true,               -- click a checkbox to toggle it in the buffer
  browser_keys = true,              -- keys in the preview (t, /, Ctrl+F, p)

  -- Git diff
  git_diff = true,                  -- show changes in the preview
  git_diff_base = "HEAD",           -- compare with "HEAD" | "staged" | a branch, tag or commit
  git_diff_colors = true,           -- use the editor's diff colors

  -- PlantUML
  plantuml_path = "",               -- plantuml command or plantuml.jar (empty: from PATH)
  plantuml_server = "",             -- PlantUML server URL (used only when set)

  -- Export
  export = {
    formats = { "pdf", "slides", "docx", "html", "epub", "markdown" },  -- formats in the Export menu
    pdf_engine = "browser",         -- how PDFs are made: "browser" | "typst"
    browser_path = "",              -- browser for printing PDFs (empty: found automatically)
    docx_engine = "pandoc",         -- converter for DOCX, HTML and EPUB
    pandoc_path = "",               -- path to pandoc (empty: the downloaded one)
    typst_path = "",                -- path to Typst (empty: the downloaded one)
    paper = "a4",                   -- paper size
    margin = "2cm",                 -- page margin
    font = "",                      -- body font (empty: default)
    font_size = 11,                 -- body font size (pt)
    toc = false,                    -- add a table of contents
    numbering = false,              -- number headings
    slide_size = "16:9",            -- page shape of the slides PDF: "16:9" | "4:3"
    template = "",                  -- Typst template (with pdf_engine = "typst")
  },
})

Compatibility with markdown-preview.nvim

nvim-md-preview uses the same command names as markdown-preview.nvim: :MarkdownPreview, :MarkdownPreviewStop and :MarkdownPreviewToggle. The table shows the equivalent of each markdown-preview.nvim setting:

markdown-preview.nvim nvim-md-preview
build = "cd app && npx --yes yarn install" build = ":MdPreviewInstall"
g:mkdp_port port
g:mkdp_theme theme
g:mkdp_markdown_css custom_css
g:mkdp_open_to_the_world = 1 host = "0.0.0.0" (protected by a token)
g:mkdp_preview_options.disable_sync_scroll = 1 scroll_sync = false
g:mkdp_auto_start = 1 The auto-start recipe
g:mkdp_browser Not supported yet. Set auto_open = false and open the URL yourself

FAQ

The keyboard shortcuts in the preview do nothing, or open a new tab instead

Browser extensions that add Vim-style keys, such as Vimium, Vimium C, Tridactyl and Surfingkeys, use the same keys as the preview:

Key Preview Vim-style extensions
t Table of contents Open a new tab (Vimium, Tridactyl)
/ Search Find in page (Vimium), quick find (Firefox)
p Presentation mode Open the clipboard URL (Vimium, Tridactyl)
f (in slides) Fullscreen Link hints

The extension usually receives the key first, so the preview never sees it. You can do either of these:

  • Turn off the preview's shortcuts and use the toolbar buttons instead:

    require("md_preview").setup({ browser_keys = false })
    
  • Turn off the extension on the preview page so the preview's keys work:

    • Vimium / Vimium C: in the options, add an excluded URL rule for http://127.0.0.1:*/*. To keep the extension and only pass some keys through, put them in "Keys", for example t / p.
    • Tridactyl: run :blacklistadd 127.0.0.1.
    • Surfingkeys: add settings.blocklistPattern = /127\.0\.0\.1/; to your settings.

    If you changed port or use LAN mode, adjust the address accordingly.

I use Neovim on both Windows and WSL. Which preview does the browser show?

Each Neovim runs its own preview server. WSL and Windows have separate localhost networks, but the browser runs on Windows, so in WSL the server skips ports that are in use on Windows (checked with netstat.exe; see :checkhealth md_preview). If a URL still reaches a preview started by another Neovim, the page says "Another preview is using this port" instead of showing the other document. Set a different port in that case.

Does it send my documents anywhere?

No. The server listens on 127.0.0.1 only, and everything it needs is inside the binary. See Security. The only exception is plantuml_server: if you set it, PlantUML diagram sources are sent to that server.

How it works

Neovim ──msgpack-rpc (stdin/stdout)──▶ Rust binary ──WebSocket──▶ Browser
   ▲                                     │ pulldown-cmark → HTML           │
   └──────────── click / open link ──────┴──────────────────────────────────┘

The Lua side sends the buffer (debounced) and the cursor line. The binary converts Markdown to HTML with source-line markers, serves the page and assets on 127.0.0.1, and pushes updates over a WebSocket. Clicks in the preview travel back the same way.

Security

The server binds to 127.0.0.1 unless you set host. It rejects requests whose Host is not a loopback name (DNS rebinding) and any cross-origin request or WebSocket, so other websites open in your browser cannot read your files or control Neovim. /assets only serves files inside the document's directory. In LAN mode every request needs the session token.

Versions up to v0.1.0 allowed other websites to read local files while a preview was running. Update with :MdPreviewInstall.

Contributing

Bug reports, ideas and pull requests are welcome. See CONTRIBUTING.md for building from source, tests and the release flow.

git clone https://github.com/ywsrock/nvim-md-preview
cd nvim-md-preview
bash download-libs.sh && cargo build --release

Acknowledgements

License

MIT