English | 中文
Preview Markdown inside Neovim: single-window reading (:MdView) or side-by-side (:MdSideView).
Pure Lua parse + read-only preview buffer; code highlighting, TOC, tables, images rendered with block characters, link jumps, optional terminal HD overlays.
- Demo: testdata/demo.md
- Screenshots: testdata/screenshots/
| Area | Description |
|---|---|
| Single / side | :MdView source ⇄ preview; :MdSideView paired view + scroll sync / _ cursor mark |
| Style | Headings (bold + distinct H1–H6 colors, optional auto numbers), bold, italic, code, strike, ==mark== (preview + editor yellow + conceal ==), links |
| Lists / quotes / HR / tables | GFM tables; column width by content (may be narrower than window); images in cells; escaped | |
| Code blocks | Border, language, line numbers, gray bg, fold after 10 lines (Enter anywhere in block / mouse on fold line only; incremental), c / yc / [Copy], TS→syntax→plain |
| Incremental preview | Edits re-render dirty AST segments only (paragraph / table / code / …); <details> fold + L UI strings also skip full re-render |
| Heading fold | Preview: ▼/▸ triangle only. Source: ATX # folds (zc/zo/zM/zR; source_heading_fold) |
| TOC | Top of preview; t in preview / <leader>toc in editor (configurable) opens TOC float |
| Links | Blue + underline in preview; Enter / Ctrl+LeftMouse; #heading / #1. heading anchors; md → target preview; Ctrl-o back (no extmark url required on Neovim 0.9) |
| Images | Block chars in preview (no Vim fold — not collapsed to … (N lines)); gi/Enter float; editor Enter/Ctrl-click on ; gh page HD; o system open |
| HTML | <details> / <summary>, <img>, <font color/style> (color / bold / italic; preview + editor) |
| Layout | Soft-wrap to preview width; reflow on resize |
From ① minimal to ③ full. Use your local paths.
Neovim 0.9+ only — no Python, no Tree-sitter, no config.
You get: headings/lists/tables/code (plain or syntax), TOC, links, side sync, etc.
You do not get: block-character images, float HD, Tree-sitter code highlight.
call plug#begin()
" mdview subfolder only
Plug '/path/to/nvimplugins/mdview'
" or whole-repo (network): Plug 'cfwang123/nvimplugins' then :PlugInstall
call plug#end()vim.opt.rtp:prepend("/path/to/nvimplugins/mdview")
-- ensure plugin/mdview.lua is sourced at startup (or packadd)No setup() required:
:e /path/to/nvimplugins/mdview/testdata/demo.md
:MdSideView
" or
:MdViewDefault global maps (if free):
| Key | Command |
|---|---|
<leader>mv |
:MdView |
<leader>ms |
:MdSideView |
<leader>toc |
TOC float from editor/preview (keys.toc) |
Disable default keys:
require("mdview").setup({
keys = { view = false, side = false, toc = false },
})On top of ①:
| Dependency | Role |
|---|---|
termguicolors |
Colors / block-character images look right |
| Python 3 + Pillow | Truecolor block images, float base + HD encode |
| Tree-sitter + language parsers | Code block highlighting (else syntax / plain) |
pip install Pillow
# or: python -m pip install PillowTree-sitter example:
require("nvim-treesitter.configs").setup({
ensure_installed = { "lua", "python", "javascript", "bash", "markdown" },
highlight = { enable = true },
})Install parsers as needed (:TSInstall lua python, etc.).
require("mdview").setup({
split_direction = "right",
width = 0.45,
code_fold_lines = 10,
code_highlight = "auto", -- treesitter → syntax → plain
sync_scroll = true,
sync_cursor_block = true,
keys = {
view = "<leader>mv",
side = "<leader>ms",
toc = "<leader>toc", -- TOC float from editor (false to disable)
},
image = {
mode = "thumb", -- block-character images in preview
python = "python",
open_with = "float",
float_hd = "never", -- tier ②: blocks only in float first
float_scale = "fit",
},
})Then you get:
- Preview images as block characters,
gifloat (blocks) - Code highlight (clearer with TS)
- Side-source cursor → preview
_mark
On top of ②:
| Dependency | Role |
|---|---|
| WezTerm / Kitty / Ghostty | Graphics protocol; float HD, gh temporary in-page HD |
| Truecolor terminal | Better blocks + HD look |
| Python 3 + Pillow | Block-character images (thumb.py) |
- WezTerm: recommended; float /
ghvia iTerm protocol overlay - Kitty / Ghostty: Kitty protocol
- Alacritty etc.: usually no graphics protocol → blocks only, no pixel HD
- Inside tmux: HD off by default; set
image.hd_tmux = true/graphics_tmux = true - SSH: HD off by default; set
hd_ssh/graphics_ssh
require("mdview").setup({
split_direction = "right",
width = 0.45,
heading_conceal = true,
heading_fold = true, -- preview: collapse section under heading (▼/▸)
source_heading_fold = nil, -- source buffer ATX folds; nil = follow heading_fold
source_details_fold = true, -- source <details> body folds (default collapsed)
list_bullets = { "●", "○" },
toc = true,
toc_min_level = 1,
toc_max_level = 3,
code_fold_lines = 10,
code_highlight = "auto",
code_line_numbers = true,
sync_scroll = true,
sync_cursor_block = true,
sync_reverse = true,
keys = {
view = "<leader>mv",
side = "<leader>ms",
toc = "<leader>toc",
},
image = {
mode = "thumb",
max_width = 60, -- max thumb columns; 0/nil = full preview width
max_height = 0,
max_images = 0, -- 0=unlimited block thumbs; >0 = only first N in preview
backend = "auto",
python = "python",
open_with = "float",
float_scale = "fit", -- fit contain | fill stretch
-- preview: no auto HD (scroll glitches); use gh temporarily
hd = "never",
float_hd = "always",
hd_tmux = false,
hd_ssh = false,
},
html = {
img = true,
details = true,
details_default_open = false,
},
})Capability matrix:
| Capability | ① min | ② rec | ③ full |
|---|---|---|---|
| Source/preview, TOC, tables, links, folds | ✓ | ✓ | ✓ |
| Tree-sitter code | — | ✓ | ✓ |
| Block-character images / float blocks | — | ✓ | ✓ |
Float pixel HD (gi) |
— | optional | ✓ |
Temporary in-page HD (gh) |
— | — | ✓ |
Side _ cursor, cross-file preview jumps |
✓ | ✓ | ✓ |
:e /path/to/nvimplugins/mdview/testdata/demo.md
:MdSideView
" in preview: ? help · t TOC · c copy code · gi image · gh in-page HDpython -c "from PIL import Image; print('Pillow OK')"| Command | Action |
|---|---|
:MdView |
Toggle source / preview in one window |
:MdSideView |
Toggle side preview (one preview window per tab) |
:MdSideView open / close |
Explicit open/close |
:MdViewRefresh |
Force re-render |
:MdViewSync |
Sync side preview to source cursor |
:MdViewToc |
Toggle TOC float (editor or preview) |
:MdViewPasteImage |
Save clipboard image as images/yyyyMMddHHmmss.png and insert  |
With side open, switching to another markdown buffer in the same tab follows that file; non-md keeps the current preview.
| Key | Action |
|---|---|
q |
Close preview / back to source |
r |
Force full refresh preview |
| (auto) | Reload source + re-render when the md file changes on disk and the buffer is not modified (watch_external) |
<CR> |
TOC / heading fold / code fold (anywhere in block) / details / image / md link → target preview |
| Mouse | Heading fold only on ▼/▸; code fold only on the gray fold line |
za |
Toggle smallest fold under cursor (heading / code / details) |
zM |
Collapse all headings |
zR |
Expand all headings |
gi |
Image float |
gh |
Temporary in-page HD (cleared on scroll / focus / resize) |
o |
System-open image |
c / yc |
Copy code block under cursor |
gs |
Jump to source line |
go |
Top TOC in document |
t |
TOC float (preview only; editor uses <leader>toc) |
<C-o> |
Back: in-doc jump → previous md preview |
L |
Toggle Chinese / English UI (hint bar, help, Copy labels; persisted) |
? |
Help float |
Works without opening preview (auto-mapped on markdown buffers once the plugin loads):
| Key | Action |
|---|---|
<CR> |
On  → image float; on [text](url) → jump (# anchor / external md source / web) |
Ctrl+LeftMouse |
Same |
<C-o> |
Jump back (Vim jumplist; in-doc anchors and external md) |
<CR> elsewhere |
Default Vim behavior |
zc / zo / za |
Close / open / toggle folds (headings + <details>) |
zM / zR |
Collapse / expand all folds |
| (auto) | Non-cursor lines: show  as 🖼 name (empty alt → image; full syntax on the cursor line). Disable with source_image_conceal = false. Task-list [ ] / [x] stay visible (not treated as shortcut-link brackets). |
"+p / "+P |
Smart paste (recommended): clipboard image → images/yyyyMMddHHmmss.png + ; otherwise normal text paste |
Ctrl-Shift-v / Shift-Insert |
Same (Shift-Insert also in insert mode) |
:MdViewPasteImage |
Image-only paste (notify if clipboard has no image) |
- Save the markdown file first (directory is required)
- Copy a screenshot / image to the clipboard
- In normal mode press
"+p(or:MdViewPasteImage/Ctrl-Shift-v) - Creates
<md-dir>/images/, writesyyyyMMddHHmmss.png(suffix_2… on collision), inserts e.g.
Requires Python3 + Pillow (same as block thumbnails).
| Mapping | Image paste? | Notes |
|---|---|---|
nmap Q "+p |
yes | recursive → hits mdview's p intercept |
nnoremap Q "+p |
no | non-recursive → builtin p, skips plugin |
| bind Lua | yes | best with nnoremap |
" A: recursive
nmap Q "+p
" B: noremap → call plugin (recommended)
nnoremap Q <Cmd>lua require('mdview').smart_clipboard_paste()<CR>require("mdview").setup({
paste_image = {
enable = true,
dir = "images",
alt = "image", -- 
intercept_clipboard_put = true, -- intercept p/P when register is +/*
keys = {
insert = { "<C-S-v>", "<S-Insert>" },
normal = { "<C-S-v>" },
},
},
})All defaults: lua/mdview/config.lua.
- In preview: block characters only by default (
hd = "never"); useghfor temporary HD - float /
gi: blocks + optional pixel HD (float_hd = "always") - Paste: clipboard image →
images/yyyyMMddHHmmss.png+ Markdown link - Needs Pillow; HD also needs a graphics-protocol terminal
mdview/
plugin/mdview.lua
lua/mdview/
scripts/thumb.py
scripts/gfx_prepare.py
testdata/demo.md
testdata/screenshots/
README.md
README.zh.md