markdown.nvim · Editing · vimdoc
:help markdown.nvim
A self-contained Markdown toolkit for Neovim.
doc/markdown.nvim.txt — rendered from the plugin's own vimdoc
*markdown.nvim.txt* A self-contained Markdown toolkit for Neovim.
CONTENTS
1. Introduction .............. |markdown.nvim-intro| 2. Requirements .............. |markdown.nvim-requirements| 3. Installation .............. |markdown.nvim-installation| 4. Setup ..................... |markdown.nvim-setup| 5. Configuration ............. |markdown.nvim-config| 5.1 General options ....... |markdown.nvim-config-general| 5.2 blockquote_hl ......... |markdown.nvim-config-blockquote| 5.3 fenced_fix ............ |markdown.nvim-config-fenced-fix| 5.4 fenced_scope .......... |markdown.nvim-config-fenced-scope| 5.5 toc ................... |markdown.nvim-config-toc| 5.6 table .................. |markdown.nvim-config-table| 5.7 features (gating) ..... |markdown.nvim-config-features| 5.8 hover (link preview) .. |markdown.nvim-config-hover| 6. Keymaps ................... |markdown.nvim-keymaps| 6.1 Navigation ............ |markdown.nvim-keymaps-nav| 6.2 Heading shift ......... |markdown.nvim-keymaps-shift| 6.3 Folding ............... |markdown.nvim-keymaps-fold| 6.4 TOC ................... |markdown.nvim-keymaps-toc| 6.5 Cursor handler ........ |markdown.nvim-keymaps-handler| 6.6 Bold / link wrap ...... |markdown.nvim-keymaps-bold| 6.7 TableView ............. |markdown.nvim-keymaps-tableview| 6.8 Deleting a link's file |markdown.nvim-keymaps-delete| 7. Commands .................. |markdown.nvim-commands| 7.1 :Markdown ............. |:Markdown| 7.2 links ................. |:Markdown-links| 7.3 toc ................... |:Markdown-toc| 7.4 refs .................. |:Markdown-refs| 7.5 table ................. |:Markdown-table| 7.6 render / preview / mdview / export |:Markdown-render| 7.7 create ................ |:Markdown-create| 7.8 headline_spacing ...... |:Markdown-headline-spacing| 7.9 scope ................. |:Markdown-scope| 7.10 list .................. |:Markdown-list| 7.11 image ................. |:Markdown-image| 7.12 gaps .................. |:Markdown-gaps| 7.13 Buffer commands ....... |markdown.nvim-buf-commands| 7.14 :MDTable* (wrapping) .. |markdown.nvim-mdtable| 8. TableView ................. |markdown.nvim-tableview| 9. Fold expression ........... |markdown.nvim-foldexpr| 10. Lua API ................... |markdown.nvim-api| 11. Architecture .............. |markdown.nvim-architecture|
1. INTRODUCTION
markdown.nvim is a self-contained Markdown toolkit for Neovim. All features are FileType-scoped: keymaps and user commands are installed only for Markdown buffers, and nothing is registered globally beyond one :Markdown command and the :checkhealth handler. Included subsystems: * Heading navigation and level shifting (count-aware) * Custom fold expression for ATX and Setext headings * Table of Contents generator (GFM slug + duplicate suffix) * Reference sync: auto-update#anchorlinks + TOC on heading rename * Visual bold (**) toggle * Link wrap (<leader>[) — wrap word/selection in[]()* Headline spacing enforcer (blank-dash-blank between H2+ sections, including a closing separator after the final section) * Underline headings (:MarkdownNvimUnderlineHeadings): Setext-style=decoration below every ATX heading's text * Fenced-code and inline-code highlight override * Blockquote coloring via matchadd * Anchor / URL / image / file handler on double-click orma* Floating Markdown table browser with HTML export/import (round-trip) * GFM table stack: formatter, auto-format table mode, tableize, cell motions * Link diagnostics: dead relative-file links + duplicate heading anchors via vim.diagnostic * Unified:Markdowncommand: links, toc, table, render, preview, mdview, create, headline_spacing, gaps * Width-limited table wrapping (:MDTable*): wraps over-wide cells onto GFM-valid continuation rows, with unwrap, lint, CSV roundtrip, and more
2. REQUIREMENTS
* Neovim >= 0.9 * lib.nvim (https://github.com/StefanBartl/lib.nvim) — REQUIRED. Supplies the :Markdown/:TableView* command layer (lib.nvim.bindings.usercmd.composer) and core/table_mode.lua's buffer debouncing. No other external tools required. Every other feature uses built-in Neovim APIs.
3. INSTALLATION
lazy.nvim:
{
"StefanBartl/markdown.nvim",
dependencies = { "StefanBartl/lib.nvim" },
ft = { "markdown", "mdx", "md" },
config = function()
require("markdown").setup()
end,
}
Local development:
{
dir = "E:/repos/markdown.nvim",
ft = { "markdown", "mdx", "md" },
config = function()
require("markdown").setup()
end,
}
4. SETUP
Call setup() once during Neovim startup:
require("markdown").setup({
-- options (see |markdown.nvim-config|)
})
setup()is idempotent — calling it twice has no effect. On FileType events matching markdown / mdx / md, the plugin installs: * Buffer-local keymaps (see |markdown.nvim-keymaps|) * Buffer-local user commands (see |markdown.nvim-buf-commands|) * TableView keymaps and commands (see |markdown.nvim-tableview|) Already-open Markdown buffers are also covered: the plugin applies its keymaps and commands to every loaded buffer whosefiletypematches.
5. CONFIGURATION
All keys are optional. Unset keys use the defaults shown below. For copy-paste-readysetup()snippets covering common customizations (rather than the full reference below), seedocs/templates/in the repo — GitHub-only, not duplicated into this helpfile.
5.1 General options
require("markdown").setup({
features = {}, -- feature gating; see |markdown.nvim-config-features|
progress_style = "auto", -- indicator for :Markdown links show|sanitize cwd
map_double_asterisk = true,
map_wrap_link = true,
keep_inner_selection = true,
protect_h1 = false,
nav = { fences = true }, -- <C-p>/<C-f> also stop on fence delimiters
use_zf_override = true,
enable_autocmds = true,
enable_keymaps = true,
ft_only = true,
ensure_headline_spacing = true,
underline_headings = { char = "=" }, -- :MarkdownNvimUnderlineHeadings
keymaps = {}, -- per-binding disable/remap by id (see |markdown.nvim-keymaps|)
tableview = { style = "markdown" }, -- default float style: "markdown" | "box"
link_hl = { underline = false },
refs = {
mode = "save", -- "off" | "save" | "live"
debounce_ms = 2000,
update_toc = true,
orphans = "report", -- "report" | "ignore"
-- toc_header = "## Table of content", -- unset: falls back to toc.header
},
toc = {
header = "## Table of content", marker = "-",
min_level = 2, max_level = 4,
anchor_style = "gfm", anchor_separator = "-",
},
table = { header_align = "center", entry_align = "center" }, -- + col_overrides
links = {
picker = "hover_select", -- "hover_select"|"select"|"telescope"|"fzf"
diagnostics = { mode = "off" }, -- "off" | "save"
},
open = { external_extensions = { "png", "pdf", "mp4", ... } },
})
progress_styleIndicator style for the scope-wide operations that walk every*.mdfile under a directory —:Markdown links show cwdand `:Markdown links sanitize cwd`. Once that is more than 20 files they scan (and, forsanitize, rewrite) in chunks across event-loop ticks so the editor never freezes; a smaller tree stays synchronous. Provided by lib.nvim'slib.nvim.progress: "statusline" feeds its headless registry, the rest render directly. One of "auto" (default), "notify", "statusline", "fidget", "float", "kit".map_double_asteriskWhentrue, typing**in visual mode toggles bold on the selection. Set tofalseif you use a surround plugin that provides this.map_wrap_linkWhentrue,<leader>[wraps the word under the cursor (normal mode) or the visual selection in a Markdown link. The inner text is auto-classified: a URL or file path goes into the(target)part, plain text into the[text]part. See |markdown.nvim-keymaps-bold|.keep_inner_selectionAfter wrapping text with**, restore the visual selection to the inner text only (without the asterisks). Whenfalse, selects the full wrapped region.protect_h1Whentrue, heading shift operations refuse to move H1 further up (i.e., H1 cannot become plain text via<C-Left>). H2+ shifting is unaffected.nav.fencesWhentrue(default),<C-p>/<C-f>and[[/]]stop on a fenced block's opening ``lang line and on its closing`` line as well as on headings — a code block is the other landmark of a markdown file, and it used to take a separate motion to reach. Set tofalsefor the previous headings-only behaviour. The by-level hops{N}<leader><C-p>/{N}<leader><C-f>are headings-only either way, so heading-only navigation is always available. See |markdown.nvim-keymaps-nav|.use_zf_overrideWhentrue,zfis remapped to "toggle fold under cursor + center" in Markdown buffers, overriding the default Vimzfoperator.enable_autocmdsWhenfalse, no FileType autocmds are registered — you must callrequire("markdown.setup.keymaps").apply(bufnr)manually.enable_keymapsCurrently unused at runtime; all keymaps are controlled byenable_autocmds. Reserved for finer-grained control in future versions.ft_onlyReserved. Currently all features are ft-only by design.ensure_headline_spacingWhentrue(default), refreshing the TOC (<leader>tocor:Markdown toc) also enforces[blank]---[blank]spacing between consecutive H2+ sections and adds a closing---after the final section. Override per call with the--sep/--no-sepflags of:Markdown toc, or apply on demand with:Markdown headline_spacing(see |:Markdown-headline-spacing|).check_heading_gapsWhentrue(default), refreshing the TOC (<leader>tocor:Markdown toc) also checks for skipped heading levels (e.g. an H1 followed directly by an H3, with no H2 in between) and, when any are found, notifies and asks whether to fix them immediately. Override per call with the--check-gaps/--no-check-gapsflags of:Markdown toc, or run the check on demand with:Markdown gaps(see |:Markdown-gaps|).underline_headingschar(default"=") is the underline character drawn below each ATX heading's text by:MarkdownNvimUnderlineHeadings(see |:MarkdownNvimUnderlineHeadings|).linksOptions for:Markdown links show/check/sanitize(see |:Markdown-links|).
links = {
picker = "hover_select", -- "hover_select"|"select"|"telescope"|"fzf"
diagnostics = { mode = "off" }, -- "off" | "save"
sanitize_on_save = true, -- normalize link targets before a write
repair_env_prefix = true, -- './$VAR/x' -> '$VAR/x' (variable set)
cursor = { enable = true, startinsert = true, path_cursor = "end" },
}
picker:"hover_select"uses lib.nvim's floating hover_select;"select"falls back to|vim.ui.select()|;"telescope"/"fzf"use nvim-telescope/telescope.nvim / ibhagwan/fzf-lua (soft dependencies — a requested backend whose plugin isn't installed falls back tovim.ui.select()with a warning).diagnostics.mode: dead relative-file links and duplicate heading titles are surfaced via|vim.diagnostic|(namespace "markdown_links"). The manual:Markdown links checkcommand always works;mode = "save"also reruns it automatically on|BufWritePost|.sanitize_on_save(defaulttrue) runs:Markdown links sanitizeon the current buffer before every write. Set tofalseto only ever sanitize manually. Env-rooted targets ($VAR/x,${VAR}/x,%VAR%/x) never get a./prefix.repair_env_prefix(defaulttrue) also strips the./an older version wrote in front of one (./$VAR/x->$VAR/x), but only when the variable is set.cursor(after the link-wrap keymap): the cursor goes where the link still needs typing -- the empty title of[](url), else the path of[text]()-- and into insert mode.startinsert = falseonly skips insert mode;enable = falseleaves the cursor inside the link in normal mode.openControls how followed file targets open (see |markdown.nvim-keymaps-handler| and |:Markdown-links|).
open = { external_extensions = { "png", "pdf", "mp4", ... } }
Targets whose extension is inexternal_extensionsare launched with the system application (image viewer, PDF reader, media player, …). Every other (text-like) target opens in the current window via:edit. The default list covers common image, video, audio, office, archive and binary formats; extend or replace it to taste.image*markdown.nvim-config-image* Whatmidoes with an image target when an in-Neovim preview provider (images.nvim, snacks.nvim or image.nvim, all optional) is installed.
image = { preview = "ask" } -- "ask" | "preview" | "system"
With no provider installed every value behaves like "system". See
|markdown.nvim-image-preview| for the full behaviour.
link_hl *markdown.nvim-config-link-hl*
Inline-link highlight tweaks.
link_hl = { underline = false }
Neovim's markdown treesitter underlines link URLs/labels; a long URL then draws a full-width underline across the soft-wrapped screen line. Withunderline = false(default) that underline is stripped from themarkdown_inlinelink groups only (other filetypes are untouched). Set totrueto restore the built-in behaviour.tableview*markdown.nvim-config-tableview* Default float style for:Markdown table view toggle/ <leader>tvt.
tableview = { style = "markdown" } -- "markdown" | "box"
"markdown" is the aligned GFM table; "box" is a Unicode box-drawing "spreadsheet" grid. The explicitview markdown/view boxactions (and <leader>tvx) always override this default. See |:Markdown-table|.menu*markdown.nvim-config-menu* Context-aware entries for nvzone/menu (soft, opt-in). markdown.nvim does not depend on a menu plugin; it provides entries and a host composes them.
menu = { enable = true, fold = true, toc = true, refs = true }
The fold entries (Fold/Unfold Heading, Fold below H2 toggle, Unfold All) appear only when the cursor is on a heading; TOC and refs entries are always available. Get the entries fromrequire("markdown.integrations.menu").items()(inline list) or.submenu()(a single fly-out), and compose into your menu, e.g.require("menu").open(vim.list_extend(md.items(), require("menus.custom"))).integrations*markdown.nvim-config-integrations* Which hosts may drive this plugin.
integrations = { ui_menu = true }
ui_menu = falsekeeps ui.nvim's right-click menu (ui.menu) from composing the Markdown fly-out;items()/submenu()still work for other hosts.refs*markdown.nvim-config-refs* Keep in-document[text](#anchor)links and the TOC in sync when headings are renamed (see |:Markdown-refs|).
refs = {
mode = "save", -- "off" | "save" | "live"
debounce_ms = 2000,
update_toc = true,
orphans = "report", -- "report" | "ignore"
-- toc_header = "## Table of content", -- unset: falls back to toc.header
}
modegoverns only AUTOMATIC syncs; the manual:Markdown refscommands work regardless. "off" No automatic sync (manual commands only). "save" Reconcile on|BufWritePre|(default). "live" Reconcile after edits, debounced bydebounce_msmilliseconds (default 2000; 1500–3000 is a sane range) so it never runs on the hot path.update_tocrefreshes an existing TOC block during a sync (it never force-creates one).orphans = "report"surfaces links whose#anchormatches no heading;"ignore"skips that report.toc_headeris the TOC header line used to detect/refresh the block; unset by default, in which case it falls back totoc.header(see |markdown.nvim-config-toc|) so the two never drift apart unless you deliberately want refs to look for a DIFFERENT header than the one:Markdown tocitself generates.
5.2 blockquote_hl
blockquote_hl = {
marker_fg = "#6A9955", -- the > token; false = colorscheme-derived
text_fg = "#7EE787", -- text after >; false = colorscheme-derived
text_bg = "dimm", -- or a hex color or nil to disable
text_bold = true,
text_italic = false,
width = "block", -- "block" | "line" | "window" | <columns>
-- link = nil or a highlight group name (overrides all other fields)
},
Two separate highlight groups are created:MarkdownBlockquoteMarkerthe>tokenMarkdownBlockquoteTexteverything after>, plus padding up to the width set bywidth(below) Both are applied via a decoration provider (extmarks) at priority 110, which wins over Tree-sitter's@punctuation.special.markdown(priority 100). Being extmark-based (notmatchadd()), the text region's background can extend past the last character.widthsets how far the background reaches:"block"(default) as wide as the widest line of the contiguous>block — one box per quote; a blank line starts a new block"line"only behind each line's own text"window"to the window edge (hl_eol, the former behavior) <number> at least that many display columns (a longer line is not cut)marker_fg/text_fgdefault to a fixed VS Code-style green, independent of the active colorscheme — some themes'Comment/Stringgroups (the colorscheme-derived fallback below) are muted greys/blues that don't read as "quoted". Set either field to a hex color to override, or tofalseto opt back into colorscheme derivation: a markdown-specific highlight group first, thenComment/String, then this same hex as the last-resort fallback — re-derived on every|ColorScheme|event.text_bg = "dimm"(the default) derives a background frommarker_fgby mixing 20% of its color toward black, filled as far aswidthsays. DON'T WANT THIS? *markdown.nvim-config-blockquote-off* This VS Code-style coloring (green marker/text + dimmed whole-line background) is ON by default. You must say so explicitly if you don't want it — nothing about the plugin auto-detects "matches your colorscheme already" or similar. The three ways to back out, from least to most drastic: 1.text_bg = nil— keep the colors, drop the bg fill 2.marker_fg = false, text_fg = false— colorscheme-derived colors instead 3.blockquote_hl = { link = "Normal" }— fully flat, no special styling Ready-to-paste snippets for all three (plus custom-color and link-group examples) are in the repo underdocs/templates/blockquote-hl.md.
5.3 fenced_fix
fenced_fix = {
inline_base_hl = { "DiagnosticWarn", "Special", "Constant", "String" },
inline_style = { italic = false, bold = false },
delimiter_hl = "Comment",
},
inline_base_hlOrdered list of highlight groups tried in sequence. The first one that exists in the current colorscheme is used as the base color for inline backtick code spans.inline_styleAdditional style flags (bold, italic, underline, undercurl) applied on top of the base color.delimiter_hlHighlight group for the backtick delimiters (```). The fix also clears@markup.raw.block/@markup.fenced_code.blockso that injected language tokens use their own colors rather than a blanket single-color overlay. Re-applied automatically on:colorschemechanges.
5.4 fenced_scope
fenced_scope = {
enable = true,
langs = { "markdown", "md", "mdx", "ascii-markdown", "ascii-md" },
provider = "auto", -- "auto" | "color_my_ascii" | "builtin"
operations = {
toc = true, nav = true, jump = true, shift = true, fold = true,
},
},
Treat a markdown-family fenced block as its own document scope. When the cursor is inside such a block, the heading-aware operations act on the block's interior; when the cursor is outside, they act on the whole file but skip every fenced block's interior. On by default.enableMaster switch. When false, every operation reverts to its whole-buffer behavior (turning it off is a true no-op).langsFence tags that count as a markdown sub-document.providerFence-detection backend. "auto" uses color_my_ascii's fence API when installed, otherwise a small built-in scanner. color_my_ascii is a soft dependency; the feature works without it.operationsPer-operation opt-out: toc Generate/insert the TOC inside the block; outside, the outer TOC skips fenced interiors. nav Heading navigation stays within the block; outside, it jumps over fenced blocks. jump Anchor jump resolves within the block. shift Whole-"buffer" heading shift shifts only the block. fold Scope-aware'foldexpr': a#inside a NON-markdown fence (e.g. a shell comment) no longer opens a fold. Toggle at runtime with:Markdown scope(see |:Markdown-scope|). Nesting: to nest a fenced block inside a ```markdown block, the outer fence must be longer (CommonMark), e.g. open the outer block with ````markdown.
5.5 toc
toc = {
header = "## Table of content",
marker = "-", -- bullet prefix, e.g. "-" or "*"
min_level = 2,
max_level = 4,
anchor_style = "gfm", -- "gfm" | "keep-case"
anchor_separator = "-",
},
Defaults for<leader>toc/:Markdown toc(see |markdown.nvim-keymaps-toc| and |:Markdown-toc|).headerTOC header line to insert/detect. Shared withrefs.toc_headerwhen the latter is left unset (see |markdown.nvim-config-refs|).markerBullet prefix for every TOC entry.min_level/max_levelDefault heading-level range included.:Markdown toc [level](ormax=N) still overridesmax_levelper call.anchor_style"gfm"(default): lowercase, GitHub-style de-dup source slug."keep-case": same shape, original case preserved.anchor_separatorWord separator in generated anchors (default "-"). Shared bycore.tocandcore.slug.heading_anchors(), socore.refsandcore.link_diagnosticsproduce anchors that agree with the TOC.slug.gfm()itself always stays byte-for-byte the historical algorithm, regardless of this config.:Markdown tocalso acceptsmin=N,max=N, andmarker=Xas per-call overrides (in addition to the legacy bare-number shorthand formax=N).
5.6 table
table = {
header_align = "center", -- "left" | "center" | "right"
entry_align = "center",
-- col_overrides = { { col = 1, align = "left" }, { col = "Name", align = "left" } },
},
Defaults for:Markdown table format(see |:Markdown-table|); explicit command args (header=,cell=,skip=) always override these per call.col_overrides[].colmay be a 1-based column index or a header-cell name (case-insensitive); it also acceptsmax/minfor width-limited wrapping (see below).table.wrap/table.wrap_profilesconfigure the:MDTable*width-limited wrapping command family; see |markdown.nvim-mdtable| for the full option list, per-table directive syntax, and API hooks.
5.7 features (gating)
Reduce the plugin to a subset without unsetting each option.
require("markdown").setup({
features = {
-- disable = "all" -- turn every feature off
-- disable = { "tableview", "refs" } -- turn off just these
-- enable = { "table" } -- re-enable (after disable)
just_enable = { "table", "toc" }, -- ONLY these run
},
})
Precedence:just_enable(if set) wins — only the listed features stay on and everything else is off. Otherwise the resolver starts all-on, appliesdisable, then re-appliesenable. Unknown names emit a warning. Gateable feature names (also returned by `require("markdown.config") .features()`): keymaps fold hl link_hl fenced_fix fenced_scope tableview refs links toc table render preview mdview create headline_spacing scope list image underline_headings table_wrap hover A disabled:Markdownsubcommand drops out of completion and reports if invoked; disabled keymaps and autocmds are never installed. The legacyenable_keymaps/enable_autocmdsflags still work alongside this gating.
5.8 hover (link preview)
Rest the cursor on a link and a small float previews what it points at: an image, a PDF's first page, another file's section, a directory listing, an in-page anchor, a URL -- or, when the target does not exist, that fact, which is often the most useful answer of all.
require("markdown").setup({
hover = {
enabled = true,
trigger = { "CursorHold" }, -- add "mouse" to follow the pointer
delay_ms = 250,
max_lines = 20,
max_width = 80,
border = "rounded",
inline_images = true,
url = {
fetch = false,
timeout_ms = 2000,
},
},
})
Options:
enabled (boolean) Master switch. Default true.
trigger (string[]) "CursorHold" and/or "mouse".
Default { "CursorHold" }.
delay_ms (integer) Debounce before the float opens. Default 250.
max_lines (integer) Preview line cap, and the float's height cap.
Default 20.
max_width (integer) Float width cap in display columns. Default 80.
border (string) As |nvim_open_win()|. Default "rounded".
inline_images (boolean) Draw images / PDF pages into the float.
Default true.
url.fetch (boolean) Fetch <title>/<meta description> for http(s)
targets. Default FALSE -- see below.
url.timeout_ms (integer) Per-fetch timeout. Default 2000.
The "mouse" trigger additionally requires 'mousemoveevent', a global option
this plugin deliberately does not set for you. Without it, that trigger
simply never fires.
url.fetch is off by default on purpose: a hover that silently issues HTTP
requests would disclose every link you brush past to its host, and would
turn a link-heavy document into a request storm while scrolling. With it
off, URLs still preview -- parsed into host, path and decoded query,
entirely locally.
Drawing pictures into the float needs images.nvim specifically: it is the
only supported provider that can draw into a window it does not own. With
snacks.nvim or image.nvim you still get the metadata float, just without
the picture. PDF page rendering additionally needs pdfport.nvim, and runs
asynchronously -- move the cursor away and the render is discarded.
On demand, regardless of enabled:
require("markdown").hover() -- preview the link under the cursor
require("markdown").hover_hide() -- close it
vim.keymap.set("n", "K", require("markdown").hover, { buffer = true })
MappingKreplaces|vim.lsp.buf.hover()|in that buffer; keep the mapping buffer-local as shown. Full write-up:docs/hover.md.
6. KEYMAPS
All keymaps are buffer-local and installed only for Markdown filetypes. None of the keys are mapped globally. Per-binding control (recommended). Every default key has a stableid(see theeditinglist indocs/BINDINGS.md). Setkeymaps[id]to disable or remap a single binding; everything else keeps its default:
require("markdown").setup({
keymaps = {
jump_anchor = false, -- disable this binding
toc = "<leader>T", -- remap to a new key (same mode)
fold_toggle = { lhs = "<F2>" }, -- table form; may also override mode
},
})
enable_keymaps = falsedisables ALL default keys at once. The legacy flags (map_double_asterisk,map_wrap_link,use_zf_override) still work too. Free-form remapping: every action is also a plain function onrequire("markdown").actions, bindable by hand (no<Plug>layer):
local a = require("markdown").actions
vim.keymap.set("n", "<C-n>", a.next_heading, { desc = "Next heading" })
vim.keymap.set("n", "gO", a.toc, { desc = "Insert/refresh TOC" })
The full list of action names and ids lives indocs/BINDINGS.md. If which-key is installed, the<leader>tprefix is labelled "Markdown" automatically.
6.1 Navigation
Key Mode Action
<C-p> n/v/x Jump to previous heading or fence delimiter
[[ n Jump to previous heading or fence delimiter
<C-f> n/v/x Jump to next heading or fence delimiter
]] n Jump to next heading or fence delimiter
{N}<leader><C-p> n Jump to previous heading of level N
{N}<leader><C-f> n Jump to next heading of level N
With a count, the plain variants repeat N times (e.g., 3<C-f> skips three
landmarks forward). With the <leader> variants, the count sets the heading
level to match (e.g., 2<leader><C-f> finds the next ## heading).
The plain variants stop on a fenced block's opening ```lang line and on its
closing `` line as well as on headings; set nav = { fences = false }` for
headings only (see |markdown.nvim-config-general|). The <leader> variants
are headings-only regardless, which is what keeps heading-only navigation
available when fence stops are on.
6.2 Heading shift
Key Mode Action
<C-Right> n Increase heading level on current line <C-Left> n Decrease heading level on current line <C-Right> v / x Increase heading level on visual selection <C-Left> v / x Decrease heading level on visual selection <S-Right> n Increase all headings in buffer <S-Left> n Decrease all headings in buffer Notes: * A{count}prefix shifts by that many levels (e.g.2<C-Right>adds two#). Without a count, one level is used. * In normal mode (single line): plain text can be promoted to#. * In visual mode (multi-line): only existing headings are modified; plain text lines are skipped. * Lines inside fenced code blocks are never modified. * Ifprotect_h1 = true, H1 headings cannot be shifted further.
6.3 Folding
Key Mode Action
zf n Toggle fold under cursor + center (if use_zf_override) <localleader>f n Toggle fold under cursor + center zu n Unfold all, center zi n Fold previous heading then center zk n Toggle outline: fold below H2 (keep H1+H2) / unfold See also |markdown.nvim-foldexpr|.
6.4 TOC
Key Mode Action
{N}<leader>toc n Insert or refresh TOC; count N sets max heading level
The TOC header line and bullet marker come from config.toc (default
"## Table of content" / "-", see |markdown.nvim-config-toc|). Anchors use
GFM-compatible slugs by default (toc.anchor_style/anchor_separator opt
into other renderers' conventions). Duplicate headings receive numeric
suffixes (GitHub convention: slug, slug-1, slug-2, ...).
Placement rules:
1. If a ## Table of content block already exists, it is replaced in-place.
2. If a first-level heading (#) exists, the TOC is inserted before the
first following ## heading.
3. Otherwise the TOC is appended to the end of the file.
Spacing is normalized: exactly one blank line before the TOC header, one
blank line before the --- separator, and one blank line after it.
Every refresh also checks for skipped heading levels (see
check_heading_gaps, |markdown.nvim-config-general|); when gaps are found,
you are notified and asked whether to fix them right away.
6.5 Cursor handler
Key Mode Action
<2-LeftMouse> n Handle target under cursor <C-LeftMouse> n Handle target under cursor ma n Handle target under cursor mi n Open image under cursor mj n Jump to anchor under cursor Dispatch priority: 1. Markdown(#anchor)link inside a TOC block or list line. 2. HTML anchor link (<a href="#id">, etc.). 3. External file link with optional fragment (file.md#anchor). 4. Image (,<img src="...">). 5. URL (https?://...). 6. Local file ([text](path)). Opening rules: * URLs open in the default browser (via|vim.ui.open()|where available, falling back to a system command). * Internal#anchorlinks jump in-buffer. * File targets whose extension is inopen.external_extensions(images, PDFs, media, …) open with the system application; all other (text-like) files open in the current window via:edit. See |markdown.nvim-config-general| for the extension list. * PDFs additionally offer rendering into a Neovim buffer when pdfport.nvim is installed. * Images additionally offer an in-Neovim preview when images.nvim, snacks.nvim (Snacks.image) or image.nvim is installed — see |markdown.nvim-image-preview|. With none installed the system application is used directly, with no prompt. *markdown.nvim-image-preview*
Image preview (`mi`)
All three providers are soft dependencies: nothing is required, and
nothing changes for a setup that has none installed.
image.preview selects the behaviour when one IS installed:
require("markdown").setup({
image = {
preview = "ask", -- "ask" | "preview" | "system"
},
})
"ask" Prompt for "System app" vs. "Preview in Neovim". Default.
"preview" Always render in a floating window, no prompt.
"system" Always hand off to the system viewer, no prompt. This is the
behaviour from before the option existed.
The float closes with q or <Esc>. A remote image (an http(s)://
target) always goes to the system handler regardless of this setting —
there is no local file for a provider to read. If a preview fails (a
terminal with no image protocol, an unreadable file), the system viewer
is used instead, so mi always shows the image somewhere.
When several providers are installed, images.nvim is preferred: it is the
only one of the three that draws on native Windows Neovim in WezTerm
(snacks.nvim and image.nvim both speak only the Kitty graphics protocol,
which Neovim's own output layer never gets drawn there — see images.nvim's
README, "Why not snacks.image or image.nvim"). It renders via
images.browse.draw_in_window(), the same primitive images.nvim's own
:Image pickers preview uses. Without images.nvim, snacks.nvim is
preferred over image.nvim: it renders through its own buffer hook, so it
needs no explicit placement or teardown.
6.6 Bold / link wrap
Key Mode Action
** v Toggle **bold** on visual selection ** V Toggle **bold** on every selected line <leader>[ n Wrap word under cursor in a Markdown link <leader>[ v Wrap visual selection in a Markdown link Bold (**): If the selected text is already surrounded by**, the asterisks are removed. Otherwise**is added on both sides. Whether the inner text or the full wrapped text is re-selected is controlled bykeep_inner_selection(see |markdown.nvim-config-general|). Requiresmap_double_asterisk = true(default). Charwise (v) wraps exactly what is selected. Linewise (V) wraps the whole line, one span per selected line, since that is what selecting a line means. Indent, a blockquote's>, a list bullet or number, a task-list checkbox, an ATX heading's hashes and trailing hard-break spaces stay outside the wrap —**- item**is no longer a list item. A range whose every non-blank line is already bold unwraps instead. Link (<leader>[): The inner text is auto-classified: * URL or file path ->[](target), cursor placed inside[]* plain text ->[text](), cursor placed inside()* empty / blank ->[](), cursor placed inside[]A path separator, ascheme://prefix,mailto:, or aname.extshape marks the text as a target. Requiresmap_wrap_link = true(default).
6.7 TableView
Key Mode Action
<leader>tvt n Toggle floating table preview (aligned Markdown style)
<leader>tvx n Toggle floating table preview (box-drawing / spreadsheet)
<leader>tvs n Open table selector (list of all tables in buffer)
<leader>tvb n Export table at cursor as basic HTML, open in browser
<leader>tvc n Close TableView floating window
<leader>tvm n Toggle table mode (auto-format on edit)
]| n Jump to the next cell on the current table row
[| n Jump to the previous cell on the current table row
<leader>mtf n Format the table at the cursor -- identical to
:Markdown table format (id table_format)
Inside the floating preview itself (buffer-local to the popup, Normal mode
only — not active anywhere else):
Key Action
<M-Right> / <M-l> Widen the column under the cursor
<M-Left> / <M-h> Narrow the column under the cursor (floors at its
natural content width — never truncates)
<M-Up> / <M-k> Move the row under the cursor up (swap with the row
above)
<M-Down> / <M-j> Move the row under the cursor down (swap with the row
below)
:w Write the current row order back to the source
The h/j/k/l forms are a fallback: some terminals/multiplexers intercept
<M-Up>/<M-Down> (commonly for scrollback or pane navigation) before Neovim
sees them. Row-move never changes the row count; column-resize never
changes row order. A cursor on a border, separator, or (in a stacked
multi-table view) a "── Table i/N ──" label line is a no-op for all of them.
In a stacked view, each table's column widths and edits are tracked
independently.
:w writes the row order back to wherever the shown table(s) actually came
from: the source buffer (marks it modified, does not save it) if rendered
from a live buffer, or the file directly (writes to disk immediately) for
the %/cwd/path scopes reading files that aren't open as buffers. Column
widening is a reading aid only and is never written back — the written
table always uses natural, unpadded widths. A table with no known source is
skipped.
See |markdown.nvim-tableview| and |:Markdown-table| for details.
6.8 Deleting a link's file
Key Mode Action
DD n Delete the line, and the file its first link points at On a line whose first link resolves to a file that exists, a confirmation dialog names the resolved path and says how many other links point at the same file (core/file_refs.luascans every*.mdunder the cwd for that count, with a ripgrep prefilter). Answering yes deletes the file and then the line; answering no, or pressing <Esc>, leaves both alone. On any other line it is plaindd,v:countincluded -- a key that stands in forddhas to be at leastdd. "Any other line" covers a line with no link, a URL, amailto:, an in-document#anchor, a target that resolves to a directory, and a target that does not exist on disk (the last of those also says so: a dead link is worth knowing about). Two things worth knowing before binding it: *DDputs a mapping in front of the built-inD, which then waits'timeoutlen'for a second key. Only in Markdown buffers, but it is a real cost.keymaps.delete_link_file = falsedrops the binding;keymaps.delete_link_file = "<leader>dl"moves it. * The dialog islib.nvim'sui.kit.confirm. Without lib.nvim there is no way to ask, so the key falls back to plainddand warns -- it never deletes a file unasked. The buffer is re-read against the remembered line both after the reference scan and after the dialog is answered; if it moved in the meantime, nothing is deleted.
7. COMMANDS
7.1 :Markdown
:Markdown {subcommand} [args]
Global command dispatcher for all Markdown utilities. It accepts a range,
so visual selections are honoured by range-aware subcommands (e.g.
create fs). Tab-completion is available at every level: subcommands,
their actions, and their options.
Subcommands:
links Show or generate Markdown links |:Markdown-links|
toc Insert/refresh the TOC |:Markdown-toc|
refs Sync #anchor links + TOC |:Markdown-refs|
table view/format/new/mode/tableize/import |:Markdown-table|
render Toggle render-markdown.nvim |:Markdown-render|
preview Start/stop mdview.nvim |:Markdown-render|
mdview Open file via mdview.nvim |:Markdown-render|
export Export to PDF via pdfport.nvim |:Markdown-export|
create Create files for link targets |:Markdown-create|
headline_spacing Enforce section separators |:Markdown-headline-spacing|
scope Toggle fenced-block scope |:Markdown-scope|
list List document items in a picker |:Markdown-list|
image Delegate to images.nvim paste/screenshot |:Markdown-image|
gaps Check/fix skipped heading levels |:Markdown-gaps|
7.2 :Markdown links
:Markdown links show [%|cwd|<file>] Scan for links and open the chosen one through a picker. The source is the current buffer (%, the default), the current working directory (cwd), or a given file. Selecting an entry opens it: * URL -> default browser * #anchor -> in-buffer jump * file -> system application or:edit(see |markdown.nvim-config-general|,open). The picker backend is set by thelinks.pickeroption. When the scanned links include at least one image and bothsnacks.pickerand images.nvim are installed,showroutes through asnacks.pickerpicker instead, with a live image preview per image link (images.browse.draw_in_window()) —links.pickeris ignored in that case, since none of its backends support a per-item live preview (the same constraint that applies to pickers.nvim). Without both deps, or with no image link in the results,showbehaves exactly as above. :Markdown links create [-r] [--noignore] [--root <path>] <path> Generate Markdown-style links for all files under <path> and copy them to the system clipboard (+register). Arguments: <path> Path to a file or directory. -r / --recursive Recurse into sub-directories. --noignore Do not skip common directory names (.git, node_modules, build, dist, …). --root <path> Prepend this path to every generated link. Supports$ENV_VARexpansion. Example:
:Markdown links create -r --root $DOCS_ROOT ./docs
A bare path with no subcommand (:Markdown links ./docs) is treated ascreate ./docsfor backwards compatibility. :Markdown links check Flag dead relative-file links and duplicate heading titles in the current buffer via|vim.diagnostic|(namespace "markdown_links"; reusescore.link_scan+core.slug). Cross-filepath#anchorlinks only check that the file exists — validating an anchor inside another file is out of scope. Seelinks.diagnostics(|markdown.nvim-config-general|) to also run this automatically on save. :Markdown links sanitize [%|cwd|<file>] Normalize inline-link targets in the current buffer (%, the default), every*.mdfile under the working directory (cwd), or a given file: * Backslashes become forward slashes. * A bare relative path gets a./prefix. Examples:[t](doc.md)->[t](./doc.md);[t](.\doc\file.md)->[t](./doc/file.md). Left untouched: URLs (https://...), scheme targets (mailto:..., a Windows drive letterC:\...),#anchor-only links, absolute paths (/...), and~-relative paths. Already-./- or../-prefixed targets are kept (backslashes are still normalized). Runs automatically on the current buffer before every write, governed by thelinks.sanitize_on_saveoption (defaulttrue; see |markdown.nvim-config-general|).
7.3 :Markdown toc
:Markdown toc [level] [min=N] [max=N] [marker=X] [--sep | --no-sep]
[--check-gaps | --no-check-gaps]
Insert or refresh the Table of Contents (see |markdown.nvim-keymaps-toc|
for placement and slug rules), using config.toc for anything not given
here (see |markdown.nvim-config-toc|). A bare level is shorthand for
max=N (legacy). min=/max=/marker= override config.toc for this
call only; the anchor style/separator are config-only (no per-call
override, so core.refs always agrees with the TOC just generated).
By default the headline separators are applied afterwards, following the
ensure_headline_spacing option. Force the behaviour per call:
--sep always apply separators
--no-sep never apply separators
By default, skipped heading levels are also checked afterwards, following
the check_heading_gaps option. Force the behaviour per call:
--check-gaps always check for gaps
--no-check-gaps skip the gap check
7.4 :Markdown refs
:Markdown refs [sync] Reconcile now: detect heading renames since the baseline, propagate eachold-anchor -> new-anchorto every inline[text](#anchor)link and to an existing TOC block, then report orphaned anchor links. A bare `:Markdown refsis the same as:Markdown refs sync`. :Markdown refs check Dry run. List every#anchorlink with no matching heading in the|quickfix|list (open it with|:copen|). Changes nothing. :Markdown refs live [on|off|toggle] Enable/disable debounced live tracking for the current buffer at runtime, independent of therefs.modeconfig default. :Markdown refs baseline Re-snapshot the current heading anchors, resetting rename tracking. Useful after large restructuring when you do not want prior edits interpreted as renames. Heading identity is tracked with extmarks (which survive in-line edits) plus a positional-diff fallback (which catches whole-line replacements without inventing renames on structural add/delete). See |markdown.nvim-config-refs| for the automatic-trigger mode and the debounce.
7.5 :Markdown table
:Markdown table view [toggle|markdown|box|select|close|browser|browsernice] [scope] Render a table (or every table) in a nicely formatted preview (see |markdown.nvim-tableview|).toggleuses the configured default style (tableview.style, default "markdown");markdown/boxforce the aligned-Markdown or Unicode box-drawing "spreadsheet" style;browser/browserniceopen it as basic / GitHub-styled HTML in the browser.selectpicks a table from a list;closecloses the float (alsoq/<Esc>).toggle/markdown/boxaccept an optionalscope: (none) cursor ON a table -> preview just that table cursor OFF any table -> every table in the current buffer % every table in the current buffer, even with the cursor on one cwd every table in every *.md file under the working directory (recursive) <path> every table in that file, or — if <path> is a directory — every table in every *.md file under it (recursive) Multiple tables render stacked one after another, separated by a blank line and a label:── Table i/N (line L) ──for same-buffer tables, or── path:line (Table i/N) ──when a table came from a file on disk rather than the current buffer. Tab-completion on the scope argument offers%,cwd, and path completion. Same via the buffer-local commands directly::TableViewToggle,:TableViewToggle %,:TableViewBox cwd,:TableViewToggle ./docs, … Inside the floating preview (Normal mode, buffer-local to the popup): <M-Right>/<M-l> widen the column under the cursor <M-Left>/<M-h> narrow it (floors at its natural content width) <M-Up>/<M-k> move the row under the cursor up <M-Down>/<M-j> move the row under the cursor down :w write the current row order back to the source Column widening is never written back (:wuses natural, unpadded widths). A cursor on a border, separator, or multi-table label line is a no-op. See |markdown.nvim-keymaps-tableview|. :Markdown table format [options] Run the self-contained GFM formatter on the table at the cursor (or in the given scope): pad columns to equal width and normalize separator rows. Defaults for alignment/column-overrides come fromconfig.table(see |markdown.nvim-config-table|) when the command args don't set them. :Markdown table new [cols] [rows] Insert an empty GFM table template with the given dimensions (defaults apply when omitted). :Markdown table mode [on|off|toggle] Per-buffer table mode: a focused, dependency-free take on vim-table-mode. While on, the table under the cursor is re-aligned automatically after each edit (debounced, on InsertLeave / TextChanged), reusing theformatalignment. Also mapped to <leader>tvm. :Markdown table tableize [format] Convert delimited text into a GFM table. Operates on the command's range (:'<,'>or:N,M) or the current line. The separator is auto-detected (tab, comma, or runs of 2+ spaces) or named explicitly: auto auto-detect (default) csv / comma a single comma tsv / tab a tab psv / pipe a single | scsv / semicolon a single ; colon a single : space a single space spaces a run of 2+ whitespace ";" / ":" / … any bare or quoted literal delimiter Single-character separators honor RFC-4180 double quoting: a field wrapped in "..." may contain the delimiter ("Smith, John",42-> two cells), and "" inside is a literal quote. Consecutive separators produce empty cells, so leading separators map to leading empty columns. A literal space must be quoted::Markdown table tableize " "(Neovim splits unquoted spaces). The result is alignment-formatted. Cell motions]|/[|jump to the next / previous cell on the current row. The whole table surface lives under thetablefeature and stays available when onlytableviewis enabled (see |markdown.nvim-config-features|). :Markdown table import [clipboard|PATH] Parse an HTML<table>into a GFM table — round-trips with the TableView "open in browser" export (|markdown.nvim-tableview|). The first row (<th>or<td>) becomes the header; tags inside cells are stripped and HTML entities (&,<,>,",'/', ) unescaped. Source of the HTML: clipboard the"+"register PATH a file on disk (none) the command's range if any (:'<,'>Markdown table importreplaces the selected HTML in place), otherwise the whole current buffer (inserted below the cursor).
7.6 :Markdown render / preview / mdview / export
:Markdown render [on|off|toggle] Enable/disable render-markdown.nvim for the current buffer. The plugin is an optional host dependency; the command warns gracefully when it is not installed. :Markdown preview [start|stop|toggle] Start/stop mdview.nvim for the current buffer via:MDView start/`:MDView stop`. Once running, mdview.nvim follows buffer switches and drives scroll sync itself (browser.behavior, default "reuse"); no separate refresh step is needed here. An optional host dependency. :Markdown mdview [path] Openpath(default: the current buffer's file) directly in the browser via mdview.nvim's:MDView start, which starts a session or — if one is already running — pushes the file and re-opens the preview surface for it. mdview.nvim is an optional host dependency; the command warns gracefully when it is not installed or loaded (e.g. omitted as a dependency of markdown.nvim and not installed standalone).:checkhealth markdownreports whether it was detected. *:Markdown-export* :Markdown export [pdf] [path] Export the current buffer/file (orpath) to PDF by delegating to pdfport.nvim'screate()— pandoc plus a PDF engine, all owned by pdfport.
7.7 :Markdown create
:Markdown create fs Walk the Markdown-link targets in the range (visual selection) or, without a range, the whole buffer, and create the corresponding filesystem entries. A trailing/denotes a directory; otherwise a file is created together with its parent directories. URLs,mailto:and#anchorsare skipped and existing paths are left untouched. The result reports created / already existing / failed counts.
7.8 :Markdown headline_spacing
:Markdown headline_spacing Enforce[blank]---[blank]spacing between consecutive H2+ sections in the current buffer and add a closing---after the final section. Idempotent. This is the on-demand form of the behaviour described underensure_headline_spacing(see |markdown.nvim-config-general|).
7.9 :Markdown scope
:Markdown scope [on|off|toggle|status] Control the fenced-block scope feature at runtime, overriding thefenced_scope.enableconfig (see |markdown.nvim-config-fenced-scope|). With no argument, toggles.statusreports the current state.
7.10 :Markdown list
:Markdown list [headings] [%|cwd|<file>] List the document's items in a picker and jump to the chosen one. The only option so far isheadings(also the default). The scope vocabulary is the same as |:Markdown-links|: the current buffer (%, the default), every*.mdbelow the current working directory (cwd), or a given file. Entries are indented by heading level and labelled with their source location. Picking one opens the file if it is not the current buffer and moves the cursor to the heading; the jump is undoable with|CTRL-O|. Headings inside YAML frontmatter and inside fenced code blocks are skipped, so what is listed matches what |:Markdown-toc| would generate. Setext headings (underlined with===/---) are not recognized, also matching the TOC generator. The picker backend is set by thelist.pickeroption (same values and fallback behavior aslinks.picker, see|markdown.nvim-config-links|).
7.11 :Markdown image
:Markdown image [paste|screenshot] Thin delegators to images.nvim (https://github.com/StefanBartl/images.nvim), an optional host, soft dependency — not a reimplementation. Default sub ispaste. paste Same as |:Image| paste: clipboard image -> file next to the document + inserted link. screenshot Same as |:Image| screenshot: interactive screenshot, skipping the clipboard step. Without images.nvim installed, both report a warning instead of an error. Exists purely for discoverability from:Markdown <Tab>— markdown.nvim and images.nvim are scoped to the same buffers by default (markdown, vimwiki, norg, text), so the coupling is natural even though the actual logic lives entirely in images.nvim.
7.12 :Markdown gaps
:Markdown gaps Check the current buffer for skipped heading levels (e.g. an H1 followed directly by an H3, with no H2 in between) and report them. When gaps are found, you are asked whether to fix them immediately (each offending heading is renumbered to close the gap). This is the on-demand form of the behaviour described undercheck_heading_gaps(see |markdown.nvim-config-general|);<leader>toc/:Markdown tocalso run it automatically unless disabled.
7.13 Buffer-local commands
These commands are available only in Markdown buffers (installed via FileType
autocmd):
*:OpenWithSystemApplication*
:OpenWithSystemApplication
Identical to pressing ma. Dispatches to the cursor-action handler:
opens anchors, images, URLs or local files under the cursor.
*:MarkdownNvimUnderlineHeadings*
:MarkdownNvimUnderlineHeadings
Inserts (or corrects) a line of = below every ATX heading's text in the
buffer, matching its length — a purely visual, Setext-style decoration.
The ATX # marker is left untouched, and this applies at every heading
level (not just H1/H2, unlike real Setext syntax). Idempotent: a
correctly-sized underline already in place is left alone; fenced code
interiors are skipped. Underline character is underline_headings.char
(see |markdown.nvim-config-general|), default "=". Disable via
features.disable = { "underline_headings" }
(see |markdown.nvim-config-features|).
*:TableViewToggle*
:TableViewToggle
Toggle the floating table preview for the table at the cursor.
*:TableViewSelect*
:TableViewSelect
Open a floating selection list with all pipe-tables found in the buffer.
Press <Enter> to preview a table; q or <Esc> to close.
*:TableViewClose*
:TableViewClose
Close the persistent floating TableView window.
*:TableViewOpenBrowser*
:TableViewOpenBrowser
Export the table at the cursor as a basic HTML file and open it in the
system default browser.
*:TableViewOpenBrowserNice*
:TableViewOpenBrowserNice
Export the table at the cursor as a styled HTML file (dark header, zebra
rows, rounded card) and open it in the system default browser.
7.14 :MDTable* (width-limited wrapping)
A separate, opt-in command family (buffer-local, featuretable_wrap, on by default) -- not nested under |:Markdown-table|.table_fmt's formatter aligns columns to their natural, unbounded width; these commands add a width CAP: cells exceeding a column's planned width wrap onto GFM-valid continuation rows of the same logical row (every physical row keeps the same pipe count). Off by default -- nothing changes until one of these commands runs, ortable.wrap.enabled = true. Continuation rows carry a↳gutter sign (virtual text only; the buffer text stays clean GFM). :MDTableWrap Wrap the table at the cursor; falls back to every table in the buffer when the cursor isn't inside one. :MDTableUnwrap Merge the table at the cursor's continuation rows back into one physical row each. Detected structurally (no in-buffer marker is written): a non-separator row with at most one non-empty cell, directly following another row of the same table, is treated as a continuation. One caveat: a genuine data row matching that same shape is indistinguishable and gets merged too. :MDTableWrapVisual[!] (range) Wrap tables in the visual selection.!unwraps first, for a clean recompute instead of re-wrapping an already-wrapped table in place. :MDTableWrapVisible[!] Wrap tables intersecting the visible window range (line("w0")..line("w$")).!unwraps first. :MDTableReflowHeader Reflow only the header + separator of the table at the cursor; body rows (and any existing continuation rows) are left untouched. :MDTableFoldRow :MDTableFoldAll Fold the continuation block under the cursor / every continuation block in the buffer. Extends the heading |markdown.nvim-foldexpr| rather than a separate manual-fold pass: continuation rows fold one level deeper than their heading section once either command has run in the buffer. :MDTableProfile {compact|docs|wide} Load a named preset fromtable.wrap_profilesas this buffer's wrap opts, then re-wrap the table at the cursor. :MDTableCol {inc|dec} [n] Widen/narrow the column under the cursor byn(default 1), taking the width from (or giving it to) the neighboring column so the row's total width is unchanged. (Vim command names can't contain+/-, henceinc/decrather than the roadmap note's originalCol+/Col-.) :MDTableAlign {cycle|left|center|right} Cycle (or set) the alignment of the column under the cursor. :MDTableFlavor {github|loose}github: strict GFM -- minimum 3-dash separator, spaced style.loose: no forced minimum. Re-wraps the table at the cursor. :MDTableLint Flags unequal cell counts, missing separator lines, and empty header cells via|vim.diagnostic|(namespacemarkdown_table). :MDTableFixMissingSeparator Inserts a separator line after every table block in the buffer missing one. :MDTableDebug Prints the resolved column-width plan for the table at the cursor: avail width, pipes, padding, sum, and per-column width/natural/min/max/mode. :MDTableToCSV [path] Exports the table at the cursor as RFC-4180 CSV, topathor the+register. :MDTableFromCSV [path] Inserts a GFM table below the cursor, parsed from CSV (pathor the+register). Configuration (table.wrap,table.wrap_profiles; see |markdown.nvim-config-table|):enabled,auto,min,max,pad,join,soft_break_chars,continuation_marker,flavor,auto_resize,resize_debounce_ms,selective_reflow. A per-table directive comment immediately above a table overrides these for that one table:
<!-- mdwrap: auto=false max=40 min=12 pad=1 join=br -->
| Name | Description |
API hooks: `require("markdown.core.table_wrap").on("before_reflow"|
"after_reflow", function(bounds, opts) end) -- bounds` is
{ start_line, end_line } (1-indexed, inclusive); errors inside a hook are
caught and ignored.
8. TABLEVIEW
The TableView subsystem parses GFM pipe-style tables from the current buffer and displays them in a persistent floating window. Table format supported:
| Column A | Column B | Column C |
|:---------|:--------:|---------:|
| left | center | right |
Alignment markers (:---,:---:,---:) are respected in the rendered output. Column widths are computed from screen-DISPLAY width (vim.fn.strdisplaywidth), not byte length, so cells containing multi-byte UTF-8 (umlauts, em dashes, curly quotes, arrows, …) still keep every|/│divider lined up across rows.require("markdown.tableview.renderer").validate_alignment(lines)checks a rendered table's lines and reports (ok,err) —errnames the first line whose divider count or display column doesn't match the reference row. Use it to verify a rendering programmatically (e.g. in a test) instead of eyeballing a screenshot for drifted columns. The floating window reuses the same buffer for successive renders. It is closed by:TableViewClose, by pressingqor<Esc>in the selection list, or when the buffer is wiped. With the cursor on a table,:TableViewToggle(and:TableViewMarkdown/:TableViewBox) preview just that one. Off any table, or with an explicit scope (%,cwd, or a file/directory path), they preview every matching table instead, stacked one after another and separated by a blank line plus a label. See |:Markdown-table|. Browser export creates a temporary.htmlfile invim.fn.tempname()and opens it with the system application (xdg-open / open / cmd start).
9. FOLD EXPRESSION
To enable fold support, add to your config (or use an ftplugin):
vim.opt_local.foldmethod = "expr"
vim.opt_local.foldexpr = "v:lua.require('markdown').foldexpr(v:lnum)"
vim.opt_local.foldenable = true
The expression recognizes: * ATX headings (#,##, …) — foldlevel equals the heading depth. * Setext H2 underlines (---) — treated as level 2. * All other lines return=(inherit from previous line). Helper functions:require("markdown.core.fold").toggle_under_cursor()Toggle fold under cursor and center the view.require("markdown.core.fold").unfold_all_center()Open all folds (zR) and center the view.require("markdown.core.fold_levels").fold_h2_plus()Toggle an outline view: fold everything below H2 (H3+,foldlevel=2), keeping H1 and H2 open; running it again unfolds all.require("markdown.core.fold_levels").fold_levels({2,3,4})Fold the specified heading levels.require("markdown.core.fold_prev").fold_prev_heading_then_center()Move to the previous heading, close its fold, and center.
10. LUA API
After callingsetup(), the following functions are exported from the top-level module: *markdown.foldexpr()*require("markdown").foldexpr(lnum)Fold expression function. Pass it tofoldexpr(see |markdown.nvim-foldexpr|). *markdown.goto_prev_heading()*require("markdown").goto_prev_heading()Jump to the previous H2+ heading. Respectsvim.v.count1. *markdown.goto_next_heading()*require("markdown").goto_next_heading()Jump to the next H2+ heading. Respectsvim.v.count1. *markdown.toggle_visual_bold()*require("markdown").toggle_visual_bold()Toggle**bold**on the current visual selection. *markdown.update_toc()*require("markdown").update_toc([header [, opts]])Insert or refresh the TOC in the current buffer.headerString: the TOC heading line (default"## Table of content").optsTable with optionalmin_levelandmax_level(integers 1-6). *markdown.handle_cursor_action()*require("markdown").handle_cursor_action()Dispatch the cursor-action handler (anchor / image / URL / file). Sub-modules can be imported individually, e.g.:
local headings = require("markdown.core.headings")
headings.shift_range(srow, erow, delta)
local toc = require("markdown.core.toc")
toc.update_markdown_toc("## Contents", { max_level = 3 })
local hs = require("markdown.core.headline_spacing")
hs.apply_headl_separators(0, { notify = false })
local gaps = require("markdown.core.heading_gaps")
gaps.check(0, { silent_ok = true })
local renderer = require("markdown.tableview.renderer")
local ok, err = renderer.validate_alignment(rendered_lines)
local diag = require("markdown.core.link_diagnostics")
diag.check(0) -- populate vim.diagnostic for buffer 0 (namespace "markdown_links")
local tf = require("markdown.core.table_fmt")
local rows = tf.parse_html_table(html_string)
local gfm_lines = tf.rows_to_gfm(rows, {})
11. ARCHITECTURE
lua/markdown/
init.lua setup() + public Lua facade
config.lua merged defaults
util/
notify.lua vim.notify wrapper
clipboard.lua setreg("+") helper
ignore.lua default directory ignore list
picker.lua hover_select / vim.ui.select / telescope / fzf abstraction
core/
headings.lua navigation + level shifting
fold.lua foldexpr, toggle, unfold
fold_levels.lua fold by heading level
fold_prev.lua fold previous heading
toc.lua TOC generator (GFM slugs, de-dup)
heading_gaps.lua detect/fix skipped heading levels
wrap.lua visual bold toggle
wrap_link.lua <leader>[ wrap word/selection in a link
link_scan.lua collect links from a line/buffer
heading_scan.lua collect ATX headings from lines/buffer/file
link_diagnostics.lua dead links / duplicate anchors (vim.diagnostic)
link_sanitize.lua normalize link-target paths (./, forward slashes)
slug.lua anchor slug algorithm (gfm/keep-case) + anchor map
table_fmt.lua GFM table formatter + HTML->GFM import (self-contained)
headline_spacing/
init.lua blank-dash-blank enforcer (+ final closer)
anchor/
is_anchor_line.lua
is_html_anchor_line.lua
is_html_extern_anchor_line.lua
is_inside_toc_block.lua
jump.lua jump to #anchor under cursor
handler/
init.lua cursor-action dispatcher
image.lua open image (system viewer)
url.lua open URL (system browser)
file.lua open file (system viewer / :edit)
fenced_fix/
init.lua fenced-code + inline-code HL override
hl_options/
init.lua orchestrator (ColorScheme re-apply)
hl_groups/
blockquote.lua decoration-provider blockquote coloring
tableview/
init.lua setup (autocmds)
autocmds.lua FileType autocmd
mappings.lua buffer-local keymaps
commands.lua buffer-local user commands
parser.lua pipe-table parser
renderer.lua floating window renderer (single + stacked all-tables view)
views/
browser_basic.lua basic HTML export
browser_niceified.lua styled HTML export
table_selector.lua pick-a-table floating UI
commands/
init.lua :Markdown dispatcher + nested completion
links.lua :Markdown links show|create|sanitize
markdown_links.lua directory-to-link generator (links create)
toc.lua :Markdown toc (TOC + separators)
table.lua :Markdown table view|format|new
render.lua :Markdown render (render-markdown.nvim)
preview.lua :Markdown preview (mdview.nvim)
mdview.lua :Markdown mdview (mdview.nvim)
create.lua :Markdown create fs
image.lua :Markdown image paste|screenshot (images.nvim)
setup/
keymaps.lua buffer-local keymap installer
autocmds.lua FileType autocmd driver
usercmds/
init.lua buffer-local user-command installer
plugin/
markdown.lua guard (vim.g.loaded_markdown)