Skip to content

selimacerbas/markdown-preview.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

markdown-preview.nvim

Note: This repository was previously known as mermaid-playground.nvim. It has been renamed and rewritten to support full Markdown preview alongside first-class Mermaid diagram support.

Live Markdown preview for Neovim with first-class Mermaid diagram support.

  • Renders your entire .md file in the browser — headings, tables, code blocks, everything
  • Mermaid diagrams render inline as interactive SVGs (click to expand, zoom, pan, export)
  • Instant updates via Server-Sent Events (no polling) with scroll sync — browser follows your cursor
  • LaTeX math — inline $...$ and display $$...$$ rendered via KaTeX
  • Syntax highlighting for code blocks (highlight.js)
  • Dark / Light theme toggle with colored heading accents
  • Optional Rust-powered rendering — use mermaid-rs-renderer for ~400x faster mermaid diagrams
  • Zero external dependencies — no npm, no Node.js, just Neovim + your browser
  • Powered by live-server.nvim (pure Lua HTTP server)

Quick start

Install (lazy.nvim)

{
  "selimacerbas/markdown-preview.nvim",
  dependencies = { "selimacerbas/live-server.nvim" },
  config = function()
    require("markdown_preview").setup({
      -- all optional; sane defaults shown
      instance_mode = "takeover",  -- "takeover" (one tab) or "multi" (tab per instance)
      port = 0,                    -- 0 = auto (8421 for takeover, OS-assigned for multi)
      open_browser = true,
      debounce_ms = 300,
    })
  end,
}

No prereqs. No npm install. Just install and go.

Use it

Open any Markdown file, then:

  • Start preview: :MarkdownPreview
  • Edit freely — the browser updates instantly as you type
  • Force refresh: :MarkdownPreviewRefresh
  • Stop: :MarkdownPreviewStop

The first start opens your browser. Subsequent updates reuse the same tab.

.mmd / .mermaid files are fully supported — the entire file is rendered as a diagram.

For other non-markdown files, place your cursor inside a fenced ```mermaid block — the plugin extracts and previews just that diagram.


Commands

Command Description
:MarkdownPreview Start preview
:MarkdownPreviewRefresh Force refresh
:MarkdownPreviewStop Stop preview

No keymaps are set by default — map them however you like. Suggested:

vim.keymap.set("n", "<leader>mps", "<cmd>MarkdownPreview<cr>", { desc = "Markdown: Start preview" })
vim.keymap.set("n", "<leader>mpS", "<cmd>MarkdownPreviewStop<cr>", { desc = "Markdown: Stop preview" })
vim.keymap.set("n", "<leader>mpr", "<cmd>MarkdownPreviewRefresh<cr>", { desc = "Markdown: Refresh preview" })

Browser UI

The preview opens a polished browser app with:

  • Full Markdown rendering — GitHub-flavored styling with colored heading borders, lists, tables, blockquotes, code, images, links, horizontal rules
  • Syntax-highlighted code blocks — powered by highlight.js, with language badges
  • Interactive Mermaid diagrams — rendered inline as SVGs:
    • Hover a diagram to reveal the expand button
    • Click to open a fullscreen overlay with zoom, pan, fit-to-width/height, and SVG export
  • Dark / Light theme toggle (sun/moon icon in header)
  • Live connection indicator — green dot when SSE is connected
  • Per-diagram error handling — if one mermaid block is invalid, only that block shows an error; the rest of the page renders fine
  • LaTeX math rendering$E = mc^2$ inline and $$\int_0^\infty$$ display math via KaTeX, plus \begin{equation} environments
  • Scroll sync — browser follows your cursor position with line-level precision
  • Iconify auto-detection — icon packs like logos:google-cloud are loaded on demand

Configuration

require("markdown_preview").setup({
  instance_mode = "takeover",           -- "takeover" or "multi" (see below)
  port = 0,                             -- 0 = auto (8421 for takeover, OS-assigned for multi)
  open_browser = true,                  -- auto-open browser on start

  content_name = "content.md",          -- workspace content file
  index_name = "index.html",            -- workspace HTML file
  workspace_dir = nil,                  -- nil = auto (shared for takeover, per-buffer for multi)

  overwrite_index_on_start = true,      -- copy plugin's index.html on every start

  auto_refresh = true,                  -- auto-update on buffer changes
  auto_refresh_events = {               -- which events trigger refresh
    "InsertLeave", "TextChanged", "TextChangedI", "BufWritePost"
  },
  debounce_ms = 300,                    -- debounce interval
  notify_on_refresh = false,            -- show notification on refresh

  mermaid_renderer = "js",              -- "js" (browser mermaid.js) or "rust" (mmdr CLI, ~400x faster)

  scroll_sync = true,                   -- browser follows cursor position
})

Instance modes

Takeover (default) — all Neovim instances share a single workspace and browser tab. The first instance to run :MarkdownPreview becomes the primary (starts the server on port 8421). Subsequent instances become secondaries — they write content to the shared workspace, and the server's file watcher pushes a reload to the browser. Scroll sync works across instances via HTTP event injection.

Multi — each instance gets its own server on an OS-assigned port and its own browser tab. Use this for side-by-side previews of different files.

require("markdown_preview").setup({ instance_mode = "multi" })

Example

graph LR
    A[Neovim Buffer] -->|write| B[content.md]
    A -.->|optional: mmdr| B
    B -->|fs watch| C[live-server.nvim]
    C -->|SSE| D[Browser]
    D --> E[markdown-it]
    D --> F[mermaid.js]
    D --> G[highlight.js]
    E --> H[Rendered Preview]
    F --> H
    G --> H
Loading

How it works

Neovim buffer
    |
    |  (autocmd: debounced write)
    v
workspace/content.md
    |
    |  (live-server.nvim detects change)
    v
SSE event --> Browser
    |
    |  markdown-it --> HTML
    |  mermaid.js  --> inline SVG diagrams
    |  highlight.js --> syntax highlighting
    |  morphdom    --> efficient DOM diffing
    v
Rendered preview (scroll preserved, no flicker)
  • Rust renderer (mermaid_renderer = "rust"): mermaid fences are pre-rendered to SVG via the mmdr CLI before writing to content.md — the browser receives ready-made SVGs with no mermaid.js overhead. Failed blocks fall back to browser-side rendering automatically.
  • Markdown files: The entire buffer is written to content.md
  • Mermaid files (.mmd, .mermaid): The entire buffer is wrapped in a mermaid code fence
  • Other files: The mermaid block under the cursor is extracted (via Tree-sitter or regex fallback) and wrapped in a code fence
  • SSE (Server-Sent Events) from live-server.nvim push updates instantly — no polling
  • morphdom diffs the DOM efficiently, preserving scroll position and interactive state
  • Takeover mode shares a single workspace (~/.cache/nvim/markdown-preview/shared/) and browser tab across all Neovim instances via a lock file
  • Multi mode uses per-buffer workspaces under ~/.cache/nvim/markdown-preview/<hash>/ with independent servers

Dependencies

  • Neovim 0.9+
  • live-server.nvim — pure Lua HTTP server (no npm)
  • Tree-sitter with the Markdown parser (recommended for mermaid block extraction)
  • mermaid-rs-renderer (optional) — cargo install mermaid-rs-renderer for ~400x faster mermaid rendering. Set mermaid_renderer = "rust" in config to enable.

Browser-side libraries are loaded from CDN (cached by your browser):


Troubleshooting

Browser shows nothing or "Loading..."

  • Make sure live-server.nvim is installed and loadable: :lua require("live_server")
  • Check the port isn't in use: change port in config

Mermaid diagram not rendering

  • The diagram syntax must be valid Mermaid — check the error chip on the diagram block
  • Invalid diagrams show the last good render + error message

Port conflict

  • In takeover mode, stop the other instance first or change the port: port = 9999
  • In multi mode, ports are auto-assigned — conflicts shouldn't happen

Stale lock file (takeover mode)

  • If Neovim crashes, the lock file may persist. The next :MarkdownPreview detects the dead server and automatically takes over

Project structure

markdown-preview.nvim/
├─ plugin/markdown-preview.lua       -- commands
├─ lua/markdown_preview/
│  ├─ init.lua                       -- main logic (server, refresh, workspace, instance modes)
│  ├─ util.lua                       -- fs helpers, workspace resolution
│  ├─ ts.lua                         -- Tree-sitter mermaid extractor + fallback
│  ├─ lock.lua                       -- lock file management (takeover mode coordination)
│  └─ remote.lua                     -- HTTP event injection (secondary scroll sync)
└─ assets/
   └─ index.html                     -- browser preview app

Thanks

PRs and ideas welcome!