mkdnflow.nvim

jakewvincentGPL-3.0更新放缓

在 Neovim 里流畅跳转和管理 Markdown 笔记,支持链接跟随、笔记本切换和 wiki 式导航,适合用纯文本做知识库的人。

Fluent navigation and management of markdown notebooks

v2.22.3发布于 2026-07-03 17:18 UTC838star43fork同步于 2026-11-07 07:33 UTC

怎么上手

由 README 提炼,执行前请确认
require('lazy').setup({ { 'jakewvincent/mkdnflow.nvim', ft = { 'markdown', 'rmd' }, config = function() require('mkdnflow').setup({}) end } })
硬件要求按 README 说明

README 未说明;仅要求 Linux、macOS 或 Windows 上的 Neovim >= 0.9.5(已在 0.9.5、0.10.x 和 stable 测试)

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

Neovim 里的 Markdown 笔记库导航与管理插件,让链接跳转、表格和待办操作更顺手

在 Neovim 里流畅跳转和管理 Markdown 笔记,支持链接跟随、笔记本切换和 wiki 式导航,适合用纯文本做知识库的人。

核心能力

  • 跟随链接在笔记间跳转,支持浏览器式前进后退
  • 创建与删除链接,兼容 wiki、引用式、图片和脚注等多种写法
  • 创建、扩展和格式化表格,可粘贴分隔数据或导入文件
  • 自动续写列表并更新有序列表编号,支持待办状态切换与排序
  • 按章节折叠并显示对象数量、行数与字数等折叠提示

容易踩的坑

必须调用 require('mkdnflow').setup() 初始化,否则插件不会自行激活;默认只对 markdown 和 rmd 文件类型生效,其他文件类型需在 filetypes 中另行配置;yaml 和 completion 模块默认关闭,需要手动开启

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v2.22.3
  1. v2.22.3最新正式版2026-07-03 · 2026-07-03 17:18 UTC

    2.22.3 (2026-03-25)

    Bug Fixes

    • cursor: polish same-level heading navigation from PR #336 (f207326)
  2. v2.22.22026-02-27 · 2026-02-27 00:37 UTC
    查看更新说明

    2.22.2 (2026-02-27)

    Bug Fixes

    • paths: guard against nil dir when activating on non-file buffers (60bdca9)
    • to_do: make match ID tracking window-local (bc5fc64)
  3. v2.22.12026-02-25 · 2026-02-25 15:03 UTC
    查看更新说明

    2.22.1 (2026-02-25)

    Bug Fixes

    • validate: allow 'current' as a valid path_resolution.primary value (af7f645), closes #331
  4. v2.22.02026-02-25 · 2026-02-25 00:48 UTC
    查看更新说明

    2.22.0 (2026-02-25)

    Features

    • to_do: add create_on_toggle option to disable checkbox creation (0174ac1), closes #321
    • to_do: return boolean from toggle_to_do (6be3f78)
  5. v2.21.12026-02-25 · 2026-02-25 00:20 UTC
    查看更新说明

    2.21.1 (2026-02-25)

    Bug Fixes

    • tests: ensure compatibility with Neovim 0.9.5 (6598a00)

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    Contents

    1. 🚀 Introduction
    2. ✨ Features
    3. 💾 Installation
    4. ⚙️ Configuration
    5. 🛠️ Commands & mappings
    6. 📚 API
    7. 🤝 Contributing
    8. 🔢 Version information
    9. 🔗 Related projects

    🚀 Introduction

    Mkdnflow is designed for the fluent navigation and management of markdown documents and document collections (notebooks, wikis, etc). It features numerous convenience functions that make it easier to work within raw markdown documents or document collections: link and reference handling (🔗 Link and reference handling), navigation (🧭 Navigation), table support (📊 Table support), list (📝 List support) and to-do list (✅ To-do list support) support, file management (📁 File management), section folding (🪗 Folding), and more. Use it for notetaking, personal knowledge management, static website building, and more. Most features are highly tweakable (⚙️ Configuration).

    ✨ Features

    🧭 Navigation

    Within-buffer navigation
    • Jump to links
    • Jump to section headings
    Within-notebook navigation
    • Link following
      • Open markdown and other text filetypes in the current window
      • Open other filetypes and URLs with your system's default application
    • Browser-like 'Back' and 'Forward' functionality
    • Table of contents window

    🔗 Link and reference handling

    • Link creation from a visual selection or the text under the cursor
    • Link destruction
    • Follow links to local paths and other Markdown files
    • Follow external links (open using default application)
    • Follow .bib-based references
      • Open url or doi field in the default browser
      • Open documents specified in file field
    • Implicit filetype extensions
    • Support for various link types
      • Standard Markdown links ([my page](https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/my_page.md))
      • Wiki links (direct [[my page]] or piped [[my_page.md|my page]])
      • Automatic links (<https://my.page>)
      • Reference-style links ([my page][1] with [1]: my_page.md)
      • Image links (![alt text](https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/image.png)) — opened in system viewer
      • Citations (@citekey or Pandoc-style [@citekey])
      • Footnotes ([^1] with [^1]: Footnote text)

    📊 Table support

    • Table creation
    • Table extension (add rows and columns)
    • Table formatting
    • Pandoc grid table support
    • Column alignment (left, right, center)
    • Paste delimited data as a table
    • Import delimited file into a new table

    📝 List support

    • Automatic list extension
    • Sensible auto-indentation and auto-dedentation
    • Ordered list number updating

    ✅ To-do list support

    • Toggle to-do item status
    • Status propagation
    • To-do list sorting
    • Create to-do items from plain ordered or unordered list items
    • Customizable highlighting for to-do status markers and content

    📁 File management

    • Simultaneous link and file renaming
    • As-needed directory creation

    🪗 Folding

    • Section folding and fold toggling
    • Helpful indicators for folded section contents
      • Section heading level
      • Counts of Markdown objects (tables, lists, code blocks, etc.)
      • Line and word counts
    • YAML block folding

    🔮 Completion

    • Path completion
    • Completion of bibliography items

    🧩 YAML block parsing

    • Specify a bibliography file in YAML front matter

    🖌️ Visual enhancements

    • Conceal markdown and wiki link syntax
    • Extended link highlighting
      • Automatic links
      • Wiki links

    💾 Installation

    Requirements:

    • Linux, macOS, or Windows
    • Neovim >= 0.9.5 (tested on 0.9.5, 0.10.x, and stable)

    Install Mkdnflow using your preferred package manager for Neovim. Once installed, Mkdnflow is configured and initialized using a setup function.

    require('lazy').setup({
        -- Your other plugins
        {
            'jakewvincent/mkdnflow.nvim',
            ft = { 'markdown', 'rmd' },  -- Add custom filetypes here if configured
            config = function()
                require('mkdnflow').setup({
                    -- Your config
                })
            end
        }
        -- Your other plugins
    })
    
    " Vim-Plug
    Plug 'jakewvincent/mkdnflow.nvim'
    
    " Include the setup function somewhere else in your init.vim file, or the
    " plugin won't activate itself:
    lua << EOF
    require('mkdnflow').setup({
        -- Config goes here; leave blank for defaults
    })
    EOF
    

    ⚙️ Configuration

    ⚡ Quick start

    Mkdnflow is configured and initialized using a setup function. To use the default settings, pass no arguments or an empty table to the setup function:

    {
        'jakewvincent/mkdnflow.nvim',
        config = function()
            require('mkdnflow').setup({})
        end
    }
    

    🔧 Advanced configuration and sample recipes

    Most features are highly configurable. Study the default config first and read the documentation for the configuration options below or in the help files.

    {
        modules = {
            bib = true,
            buffers = true,
            conceal = true,
            cursor = true,
            folds = true,
            foldtext = true,
            links = true,
            lists = true,
            maps = true,
            paths = true,
            tables = true,
            templates = true,
            to_do = true,
            yaml = false,
            completion = false,
        },
        create_dirs = true,
        silent = false,
        wrap = false,
        on_attach = false,
        path_resolution = {
            primary = 'first',
            fallback = 'current',
            root_marker = false,
            sync_cwd = false,
            update_on_navigate = true,
        },
        filetypes = {
            markdown = true,
            rmd = true,
        },
        foldtext = {
            object_count = true,
            object_count_icon_set = 'emoji',
            object_count_opts = function()
                return require('mkdnflow').foldtext.default_count_opts()
            end,
            line_count = true,
            line_percentage = true,
            word_count = false,
            title_transformer = function()
                return require('mkdnflow').foldtext.default_title_transformer
            end,
            fill_chars = {
                left_edge = '⢾⣿⣿',
                right_edge = '⣿⣿⡷',
                item_separator = ' · ',
                section_separator = ' ⣹⣿⣏ ',
                left_inside = ' ⣹',
                right_inside = '⣏ ',
                middle = '⣿',
            },
        },
        bib = {
            default_path = nil,
            find_in_root = true,
        },
        cursor = {
            jump_patterns = nil,
        },
        links = {
            style = 'markdown',
            compact = false,
            conceal = false,
            search_range = 0,
            implicit_extension = nil,
            transform_on_follow = false,
            transform_on_create = function(text)
                text = text:gsub('[ /]', '-')
                text = text:lower()
                text = os.date('%Y-%m-%d_') .. text
                return text
            end,
            transform_scope = 'path',
            auto_create = true,
            on_create_new = false,
        },
        new_file_template = {
            enabled = false,
            placeholders = {
                before = {
                    title = 'link_title',
                    date = 'os_date',
                },
                after = {},
            },
            template = '# {{ title }}',
        },
        to_do = {
            highlight = false,
            statuses = {
                not_started = {
                    marker = ' ',
                    highlight = {
                        marker = { link = 'Conceal' },
                        content = { link = 'Conceal' },
                    },
                    sort = { section = 2, position = 'top' },
                    propagate = {
                        up = function(host_list)
                            local no_items_started = true
                            for _, item in ipairs(host_list.items) do
                                if item.status.name ~= 'not_started' then
                                    no_items_started = false
                                end
                            end
                            if no_items_started then
                                return 'not_started'
                            else
                                return 'in_progress'
                            end
                        end,
                        down = function(child_list)
                            local target_statuses = {}
                            for _ = 1, #child_list.items, 1 do
                                table.insert(target_statuses, 'not_started')
                            end
                            return target_statuses
                        end,
                    },
                },
                in_progress = {
                    marker = '-',
                    highlight = {
                        marker = { link = 'WarningMsg' },
                        content = { bold = true },
                    },
                    sort = { section = 1, position = 'bottom' },
                    propagate = {
                        up = function(host_list)
                            return 'in_progress'
                        end,
                        down = function(child_list) end,
                    },
                },
                complete = {
                    marker = { 'X', 'x' },
                    highlight = {
                        marker = { link = 'String' },
                        content = { link = 'Conceal' },
                    },
                    sort = { section = 3, position = 'top' },
                    propagate = {
                        up = function(host_list)
                            local all_items_complete = true
                            for _, item in ipairs(host_list.items) do
                                if item.status.name ~= 'complete' then
                                    all_items_complete = false
                                end
                            end
                            if all_items_complete then
                                return 'complete'
                            else
                                return 'in_progress'
                            end
                        end,
                        down = function(child_list)
                            local target_statuses = {}
                            for _ = 1, #child_list.items, 1 do
                                table.insert(target_statuses, 'complete')
                            end
                            return target_statuses
                        end,
                    },
                },
            },
            status_order = { 'not_started', 'in_progress', 'complete' },
            status_propagation = {
                up = true,
                down = true,
            },
            sort = {
                on_status_change = false,
                recursive = false,
                cursor_behavior = {
                    track = true,
                },
            },
        },
        tables = {
            type = 'pipe',
            trim_whitespace = true,
            format_on_move = true,
            auto_extend_rows = false,
            auto_extend_cols = false,
            style = {
                cell_padding = 1,
                separator_padding = 1,
                outer_pipes = true,
                apply_alignment = true,
            },
        },
        yaml = {
            bib = { override = false },
        },
        mappings = {
            MkdnEnter = { { 'n', 'v' }, '<CR>' },
            MkdnGoBack = { 'n', '<BS>' },
            MkdnGoForward = { 'n', '<Del>' },
            MkdnMoveSource = { 'n', '<F2>' },
            MkdnNextLink = { 'n', '<Tab>' },
            MkdnPrevLink = { 'n', '<S-Tab>' },
            MkdnFollowLink = false,
            MkdnDestroyLink = { 'n', '<M-CR>' },
            MkdnTagSpan = { 'v', '<M-CR>' },
            MkdnYankAnchorLink = { 'n', 'yaa' },
            MkdnYankFileAnchorLink = { 'n', 'yfa' },
            MkdnNextHeading = { 'n', ']]' },
            MkdnPrevHeading = { 'n', '[[' },
            MkdnNextHeadingSame = { 'n', '][' },
            MkdnPrevHeadingSame = { 'n', '[]' },
            MkdnIncreaseHeading = { { 'n', 'v' }, '+' },
            MkdnDecreaseHeading = { { 'n', 'v' }, '-' },
            MkdnIncreaseHeadingOp = { { 'n', 'v' }, 'g+' },
            MkdnDecreaseHeadingOp = { { 'n', 'v' }, 'g-' },
            MkdnToggleToDo = { { 'n', 'v' }, '<C-Space>' },
            MkdnNewListItem = false,
            MkdnNewListItemBelowInsert = { 'n', 'o' },
            MkdnNewListItemAboveInsert = { 'n', 'O' },
            MkdnExtendList = false,
            MkdnUpdateNumbering = { 'n', '<leader>nn' },
            MkdnTableNextCell = { 'i', '<Tab>' },
            MkdnTablePrevCell = { 'i', '<S-Tab>' },
            MkdnTableNextRow = false,
            MkdnTablePrevRow = { 'i', '<M-CR>' },
            MkdnTableNewRowBelow = { 'n', '<leader>ir' },
            MkdnTableNewRowAbove = { 'n', '<leader>iR' },
            MkdnTableNewColAfter = { 'n', '<leader>ic' },
            MkdnTableNewColBefore = { 'n', '<leader>iC' },
            MkdnTableDeleteRow = { 'n', '<leader>dr' },
            MkdnTableDeleteCol = { 'n', '<leader>dc' },
            MkdnFoldSection = { 'n', '<leader>f' },
            MkdnUnfoldSection = { 'n', '<leader>F' },
            MkdnTab = false,
            MkdnSTab = false,
            MkdnIndentListItem = { 'i', '<C-t>' },
            MkdnDedentListItem = { 'i', '<C-d>' },
            MkdnCreateLink = false,
            MkdnCreateLinkFromClipboard = { { 'n', 'v' }, '<leader>p' },
        },
    }
    
    🎨 Configuration options
    modules
    require('mkdnflow').setup({
        modules = {
            bib = true,
            buffers = true,
            conceal = true,
            cursor = true,
            folds = true,
            foldtext = true,
            links = true,
            lists = true,
            maps = true,
            paths = true,
            tables = true,
            to_do = true,
            yaml = false,
            cmp = false,
        notebook = true,
        }
    })
    
    OptionTypeDescription
    modules.bibbooleantrue (default): bib module is enabled (required for parsing .bib files and following citations).false: Disable bib module functionality.
    modules.buffersbooleantrue (default): buffers module is enabled (required for backward and forward navigation through buffers).false: Suppress buffers keybindings. Note: This is a core module and is always loaded internally because other modules depend on it. Setting this to false only disables its keybindings.
    modules.concealbooleantrue (default): conceal module is enabled (required if you wish to enable link concealing. This does not automatically enable conceal behavior; see links.conceal.)false: Disable conceal module functionality.
    modules.cursorbooleantrue (default): cursor module is enabled (required for cursor movements: jumping to links, headings, etc.).false: Suppress cursor keybindings. Note: This is a core module and is always loaded internally because other modules depend on it. Setting this to false only disables its keybindings.
    modules.foldsbooleantrue (default): folds module is enabled (required for section folding).false: Disable folds module functionality.
    modules.foldtextbooleantrue (default): foldtext module is enabled (required for prettified foldtext).false: Disable foldtext module functionality.
    modules.linksbooleantrue (default): links module is enabled (required for creating, destroying, and following links).false: Suppress links keybindings. Note: This is a core module and is always loaded internally because other modules depend on it. Setting this to false only disables its keybindings.
    modules.listsbooleantrue (default): lists module is enabled (required for working in and manipulating lists, etc.).false: Disable lists module functionality.
    modules.to_dobooleantrue (default): to_do module is enabled (required for manipulating to-do statuses/lists, toggling to-do items, to-do list sorting, etc.)false: Disable to_do module functionality.
    modules.pathsbooleantrue (default): paths module is enabled (required for link interpretation, link following, etc.).false: Suppress paths keybindings. Note: This is a core module and is always loaded internally because other modules depend on it. Setting this to false only disables its keybindings.
    modules.tablesbooleantrue (default): tables module is enabled (required for table management, navigation, formatting, etc.).false: Disable tables module functionality.
    modules.templatesbooleantrue (default): templates module is enabled (required for new-file template formatting and injection when following links to non-existent files).false: Disable templates module functionality. Template injection will be skipped even if new_file_template.enabled is true.
    modules.yamlbooleantrue: yaml module is enabled (required for parsing yaml headers).false (default): Disable yaml module functionality.
    modules.completionbooleantrue: Completion module is enabled. Automatically registers with nvim-cmp if installed; blink.cmp users should also set this to true (see completion setup below).false (default): Disable completion module functionality.
    modules.notebookbooleantrue (default): notebook module is enabled. Provides shared cross-file primitives for scanning notebook files, headings, and links. This module has zero cost at load time (no autocommands, keymaps, or side effects) and serves as infrastructure for other features.false: Disable notebook module functionality.
    modules.backlinksbooleantrue (default): backlinks module is enabled. Provides a side panel showing all files in the notebook that link to the current file. Requires the notebook module.false: Disable backlinks module functionality.
    create_dirs
    require('mkdnflow').setup({
        create_dirs = true,
    })
    
    OptionTypeDescription
    create_dirsbooleantrue (default): Directories referenced in a link will be (recursively) created if they do not exist.false: No action will be taken when directories referenced in a link do not exist. Neovim will open a new file, but you will get an error when you attempt to write the file.
    path_resolution
    require('mkdnflow').setup({
        path_resolution = {
            primary = 'first',
            fallback = 'current',
            root_marker = false,
            sync_cwd = false,
            update_on_navigate = false,
        },
    })
    
    OptionTypeDescription
    path_resolution.primarystring'first' (default): Links will be interpreted relative to the first-opened file (when the current instance of Neovim was started).'current': Links will always be interpreted relative to the current file.'root': Links will be always interpreted relative to the root directory of the current notebook (requires path_resolution.root_marker to be specified).Previously named perspective.priority.
    path_resolution.fallbackstring'first': Links will be resolved relative to the first-opened file.'current' (default): Links will be resolved relative to the current file.The fallback strategy is used when the primary strategy cannot resolve a path (e.g. when primary is 'root' but no root marker is found). Since 'root' cannot fall back to itself, only 'first' and 'current' are valid here.
    path_resolution.root_markerstring | booleanfalse (default): The plugin does not look for the notebook root.string: The name of a file (not a full path) by which a notebook's root directory can be identified. For instance, '.root' or 'index.md'.Previously named perspective.root_tell.
    path_resolution.sync_cwdbooleantrue: Changes in path resolution will be reflected in the nvim working directory. (In other words, the working directory will sync with the plugin's path resolution.) This helps ensure (at least) that path completions (if using a completion plugin with support for paths) will be accurate and usable.false (default): Neovim's working directory will not be affected by Mkdnflow.Previously named perspective.nvim_wd_heel.
    path_resolution.update_on_navigatebooleantrue: Path resolution will be updated when following a link to a file in a separate notebook/wiki (or navigating backwards to a file in another notebook/wiki).false (default): Path resolution will be not updated when following a link to a file in a separate notebook/wiki. (Links in the file in the separate notebook/wiki will be interpreted relative to the original notebook/wiki.)Previously named perspective.update.
    filetypes
    require('mkdnflow').setup({
        filetypes = {
            markdown = true,
            rmd = true,
        },
    })
    
    OptionTypeDescription
    filetypes.markdownbooleantrue (default): The plugin activates for files with the markdown filetype (includes .md, .markdown, .mkd, .mkdn, .mdwn, .mdown extensions).false: The plugin does not activate for markdown files.
    filetypes.rmdbooleantrue (default): The plugin activates for files with the rmd filetype (.rmd extension).false: The plugin does not activate for R Markdown files.
    filetypes.<name>boolean | stringCustom filetype/extension configuration:- true: If Neovim recognizes this as an extension (e.g., md), the detected filetype is used. Otherwise, the extension is auto-registered as its own filetype (e.g., wiki = true registers .wiki files as filetype wiki).- 'filetype' (string): Register this extension as the specified filetype. Example: txt = 'markdown' makes .txt files activate mkdnflow and be treated as markdown.- false: Disable for this filetype/extension.Note: Old extension-based configs (e.g., md = true) are automatically migrated to filetype-based configs (e.g., markdown = true).Examples:lua<br>filetypes = {<br> markdown = true, -- Standard (default)<br> rmd = true, -- Standard (default)<br> wiki = true, -- Auto-register .wiki as 'wiki' filetype<br> txt = 'markdown', -- Treat .txt files as markdown<br>}<br>

    [!NOTE] Mkdnflow activates based on Neovim's filetype detection, not file extensions. This means files with modelines like vim: ft=markdown will activate the plugin regardless of their extension.

    Lazy loading note: If you use a plugin manager with lazy loading (e.g., ft = { 'markdown' }) and configure custom extensions like wiki = true, you must add those filetypes to your lazy loading configuration (e.g., ft = { 'markdown', 'wiki' }), since the plugin won't load until it sees a matching filetype.

    wrap
    require('mkdnflow').setup({
        wrap = false,
    })
    
    OptionTypeDescription
    wrapbooleantrue: When jumping to next/previous links or headings, the cursor will continue searching at the beginning/end of the file.false (default): When jumping to next/previous links or headings, the cursor will stop searching at the end/beginning of the file.
    bib
    require('mkdnflow').setup({
        bib = {
            default_path = nil,
            find_in_root = true,
        },
    })
    
    OptionTypeDescription
    bib.default_pathstring | nilnil (default): No default/fallback bib file will be used to search for citation keys.string: A path to a default .bib file to look for citation keys in when attempting to follow a reference. The path need not be in the root directory of the notebook.
    bib.find_in_rootbooleantrue (default): When path_resolution.primary is also set to root (and a root directory was found), the plugin will search for bib files to reference in the notebook's top-level directory. If bib.default_path is also specified, the default path will be added to the list of bib files found in the top-level directory so that it will also be searched.false: The notebook's root directory will not be searched for bib files.
    silent
    require('mkdnflow').setup({
        silent = false,
    })
    
    OptionTypeDescription
    silentbooleantrue: The plugin will not display any messages in the console except compatibility warnings related to your config.false (default): The plugin will display messages to the console.
    cursor
    require('mkdnflow').setup({
        cursor = {
            jump_patterns = nil,
        },
    })
    
    OptionTypeDescription
    cursor.jump_patternstable | nilnil (default): Cursor jumping (MkdnNextLink/MkdnPrevLink) uses detection-based link finding, which reuses the same link detection system as followLink(). All recognized link types (markdown links, wiki links, auto links, reference links, citations, footnote refs with definitions) are automatically jumped to based on the configured links.style. Links inside fenced code blocks and inline code spans are skipped.table: A table of additional Lua regex patterns to jump to alongside detected links. Useful for non-link targets like TODO markers (e.g. { 'TODO', 'FIXME' }).{} (empty table): No extra patterns; only detected links are jumped to (same as nil).
    cursor.yank_registerstring'"' (default): Anchor links are yanked to the unnamed register.'+': Yank to the system clipboard.'<any register>': Yank to any valid Vim register (e.g. 'a', '*', '0').
    links
    require('mkdnflow').setup({
        links = {
            style = 'markdown',
            compact = false,
            conceal = false,
            search_range = 0,
            implicit_extension = nil,
            transform_on_follow = false,
            transform_on_create = function(text)
                text = text:gsub(" ", "-")
                text = text:lower()
                text = os.date('%Y-%m-%d_') .. text
                return(text)
            end,
            auto_create = true,
            on_create_new = false,
        },
    })
    
    OptionTypeDescription
    links.stylestring'markdown' (default): Links will be expected in the standard markdown format: [<title>](https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/<https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/source>)'wiki': Links will be expected in the unofficial wiki-link style, specifically the title-after-pipe format: [[<source>|<title>]].This sets the default style for link creation. To create links in both styles, you can override the style per-call via :MkdnCreateLink wiki (or markdown), or via the Lua API: createLink({ style = 'wiki' }). See 'custom-mappings-on-attach' for an example.
    links.compactbooleantrue: Wiki-style links will be created with the source and name being the same (e.g. [[Link]] will display as "Link" and go to a file named "Link.md").false (default): Wiki-style links will be created with separate name and source (e.g. [[link-to-source|Link]] will display as "Link" and go to a file named "link-to-source.md").Previously named links.name_is_source.
    links.concealbooleantrue: Link sources and delimiters will be concealed (depending on which link style is selected).false (default): Link sources and delimiters will not be concealed by mkdnflow.
    links.ref_hintbooleantrue: When the cursor is on a reference-style link ([text][ref] or [label]), the resolved URL is shown as virtual text at the end of the line. When the cursor is on a reference definition line ([ref]: url), a usage count is shown instead. The virtual text uses the MkdnflowRefHint highlight group, which is linked to Comment by default. To customize its appearance, override the highlight group, e.g. vim.api.nvim_set_hl(0, 'MkdnflowRefHint', { fg = '#88aaff', italic = true }).false (default): No virtual text hints for reference links.
    links.search_rangeintegerWhen following or jumping to links, consider n lines before and after a given line (useful if you ever permit links to be interrupted by a hard line break). Default: 0.Previously named links.context.
    links.implicit_extensionstringA string that instructs the plugin (a) how to interpret links to files that do not have an extension, and (b) how to create new links from the text under the cursor or text selection.nil (default): Extensions will be explicit when a link is created and must be explicit in any notebook link.'<any extension>' (e.g. 'md'): Links without an extension (e.g. [Homepage](https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/index)) will be interpreted with the implicit extension (e.g. index.md), and new links will be created without an extension.
    links.transform_on_createfun(string): string | booleanfalse: No transformations are applied to the text to be turned into the name of the link source/path.fun(string): string (default): A function that transforms the text to be inserted as the source/path of a link when a link is created. Anchor links are not currently customizable. For an example, see the sample recipes beneath this table.Previously named links.transform_explicit.
    links.transform_scopestringControls which part of the link text is passed to transform_on_create when the text contains /.'path' (default): The entire text (including directory components) is passed to transform_on_create. For example, work/with/dirs is transformed as a single string, and the default transform produces 2026-02-15_work-with-dirs.md.'filename': Only the filename portion (after the last /) is passed to transform_on_create; the directory prefix is preserved. For example, work/with/dirs becomes work/with/2026-02-15_dirs.md.If the text contains no /, both modes behave identically.Can be overridden per-call via the Lua API: createLink({ transform_scope = 'filename' }), or via command argument: :MkdnCreateLink filename.
    links.transform_on_followfun(string): string | booleanfalse (default): Do not perform any transformations on the link's source when following.fun(string): string: A function that transforms the path of a link immediately before interpretation. It does not transform the actual text in the buffer but can be used to modify link interpretation. For an example, see the sample recipe below.Previously named links.transform_implicit.
    links.auto_createbooleantrue (default): Try to create a link from the text under the cursor if there is no link under the cursor to follow.false: Do nothing if trying to follow a link and a link can't be found under the cursor.Previously named links.create_on_follow_failure.
    links.on_create_newfalse | fun(string, string|nil): string|nilA callback invoked when following a link to a file that does not yet exist,allowing file creation to be delegated to an external tool (e.g. zk,Obsidian CLI, a custom script). This callback is only invoked when the targetfile does not yet exist. Following a link to an existing file bypasses thiscallback entirely.The function receives two arguments: the full resolved path (with extension)that mkdnflow would create, and the link's display text (which may be nil).It should return a string (file path for mkdnflow to open) or nil (if thecallback handled everything). If a path is returned and the file exists there,mkdnflow opens it directly (skipping template injection). If the file does notexist at the returned path, mkdnflow runs its normal creation flow.false (default): Use mkdnflow's built-in file creation.
    links.edit_dirsboolean | fun(path: string)Controls what happens when you follow a link that points to a directoryrather than a file. This is useful when your notebook has subdirectoriesthat you link to (e.g. [recipes](https://github.com/jakewvincent/mkdnflow.nvim/blob/HEAD/recipes/)) and you want your preferredfile browser plugin to handle them instead of the default filename prompt.false (default): Prompt the user to type a filename within the directory.true: Run :edit on the directory, which triggers whateverdirectory-browsing plugin you have installed (netrw, oil.nvim, etc.).fun(path: string): Call the supplied function with the absolutedirectory path. The return value is ignored. This is useful for filebrowsers that don't hook into :edit and need their own API call.In all non-default modes, the buffer stack is updated before thedirectory is opened, so MkdnGoBack will return you to the previousfile.Example:lua<br>-- Open directories with mini.files<br>links = {<br> edit_dirs = require('mini.files').open,<br>}<br>
    links.uri_handlerstable<string, fun(uri: string, scheme: string, path: string, anchor: string|nil)|'system'>A table of custom URI scheme handlers, keyed by scheme name (without ://).When following a link whose path starts with <scheme>://, the registeredhandler is called instead of the normal path classification. This is usefulfor custom protocols like phd://, zotero://, obsidian://, etc.Each value can be:- A function fun(uri, scheme, path, anchor) receiving the full URI (with anchor reattached), the scheme name, the path (without anchor), and the anchor (e.g. '#section') or nil. The function has full control over what happens when the link is followed.- The string 'system' to open the URI with the OS default handler (same as how http:// URLs are opened).Standard schemes like http and https can also be overridden byregistering a handler for them.{} (default): No custom URI scheme handlers are registered.Example:lua<br>links = {<br> uri_handlers = {<br> phd = function(uri, scheme, path, anchor)<br> vim.fn.jobstart({ 'xdg-open', uri }, { detach = true })<br> end,<br> zotero = 'system',<br> },<br>}<br>
    require('mkdnflow').setup({
        links = {
            -- If you want all link paths to be explicitly prefixed with the year
            -- and for the path to be converted to uppercase:
            transform_on_create = function(input)
                return(string.upper(os.date('%Y-')..input))
            end,
            -- Link paths that match a date pattern can be opened in a `journals`
            -- subdirectory of your notebook, and all others can be opened in a
            -- `pages` subdirectory:
            transform_on_follow = function(input)
                if input:match('%d%d%d%d%-%d%d%-%d%d') then
                    return('journals/'..input)
                else
                    return('pages/'..input)
                end
            end
        }
    })
    

    Delegate new-file creation to zk:

    require('mkdnflow').setup({
        links = {
            on_create_new = function(path, title)
                -- Let zk create the note with its own templates and ID scheme.
                -- `zk new` prints the created path to stdout.
                local dir = vim.fn.fnamemodify(path, ':h')
                local cmd = { 'zk', 'new', '--no-input', '--print-path', dir }
                if title then
                    table.insert(cmd, '--title')
                    table.insert(cmd, title)
                end
                local result = vim.fn.system(cmd)
                local new_path = vim.trim(result)
                if vim.v.shell_error ~= 0 then
                    vim.notify('zk new failed: ' .. result, vim.log.levels.ERROR)
                    return nil
                end
                return new_path  -- mkdnflow opens the zk-created file
            end,
        },
    })
    
    new_file_template
    require('mkdnflow').setup({
        new_file_template = {
            enabled = false,
            placeholders = {
                before = { title = 'link_title', date = 'os_date' },
                after = {},
            },
            template = '# {{ title }}',
        },
    })
    
    OptionTypeDescription
    new_file_template.enabledbooleantrue: Use the new-file template when opening a new file by following a link.false (default): Don't use the new-file template when opening a new file by following a link.Previously named new_file_template.use_template.
    new_file_template.placeholderstable<string, string|fun(ctx: table): string>A flat table whose keys are placeholder names mapped to one of:- A function fun(ctx): string — called with a context table containing all resolved built-in values. Available fields in ctx: - link_title — the display text of the link under the cursor, or '' - os_date — today's date as YYYY-MM-DD - source_file — basename of the source file (e.g. 'index.md') - filename — stem of the new file being created, without extension (e.g. 'my-page') - heading_context — text of the nearest heading above the cursor (without # prefix), or ''- A magic string shorthand — one of 'link_title' or 'os_date', which resolves to the corresponding ctx value.- A plain string literal — used as-is (e.g., author = 'Jake').All placeholders are resolved in a single pass before the new buffer is opened.Default: { title = 'link_title', date = 'os_date' }Deprecated: The old nested format { before = {...}, after = {} } is auto-migrated to this flat format. placeholders.after entries are merged in but all placeholders now resolve before the buffer switch.
    new_file_template.templatestringA string, optionally containing placeholder names, that will be inserted into a new file. Default: '# {{ title }}'
    to_do
    require('mkdnflow').setup({
        to_do = {
            highlight = false,
            statuses = {
                not_started = { marker = ' ', ... },
                in_progress = { marker = '-', ... },
                complete = { marker = { 'X', 'x' }, ... },
            },
            status_order = { 'not_started', 'in_progress', 'complete' },
            status_propagation = { up = true, down = true },
            sort = {
                on_status_change = false,
                recursive = false,
                cursor_behavior = { track = true },
            },
        },
    })
    
    OptionTypeDescription
    to_do.create_on_togglebooleantrue (default): When toggling a line that is not already a to-do item, convert it into one (adding a checkbox to plain list items or wrapping plain text in - [ ] ).false: Only rotate the status of existing to-do items. Lines without a checkbox are left unchanged. This is useful when mapping MkdnToggleToDo to a key like <CR> alongside other context-sensitive actions — the command becomes a no-op on non-to-do lines, allowing subsequent actions to fire instead.
    to_do.highlightbooleantrue: Apply highlighting to to-do status markers and/or content (as defined in to_do.statuses[*].highlight).false (default): Do not apply any highlighting.
    to_do.status_propagation.upbooleantrue (default): Update ancestor statuses (recursively) when a descendant status is changed or when a new to-do item is added to a list (e.g. a completed parent reverts to in-progress when a new uncompleted child is added). Updated according to logic provided in to_do.statuses[*].propagate.up.false: Ancestor statuses are not affected by descendant status changes or new item creation.
    to_do.status_propagation.downbooleantrue (default): Update descendant statuses (recursively) when an ancestor's status is changed. Updated according to logic provided in to_do.statuses[*].propagate.down.false: Descendant statuses are not affected by ancestor status changes.
    to_do.sort_on_status_changebooleantrue: Sort a to-do list when an item's status is changed.false (default): Leave all to-do items in their current position when an item's status is changed.Note: This will not apply if the to-do item's status is changed manually (i.e. by typing or pasting in the status marker).
    to_do.sort.recursivebooleantrue: sort_on_status_change applies recursively, sorting the host list of each successive parent until the root of the list is reached.false (default): sort_on_status_change only applies at the current to-do list level (not to the host list of the parent to-do item).
    to_do.sort.cursor_behavior.trackbooleantrue (default): Move the cursor so that it remains on the same to-do item, even after a to-do list sort relocates the item.false: The cursor remains on its current line number, even if the to-do item is relocated by sorting.
    to_do.statusestable (dict-like)A dictionary of to-do statuses, keyed by status name (e.g. 'not_started', 'in_progress', 'complete'). Each value is a table with the options described in the following rows. An arbitrary number of statuses can be defined. Users can partially override individual statuses — for example, overriding just the highlight of not_started while keeping all other defaults. See default statuses in the settings table.
    to_do.statuses.<name>.markerstring | tableThe marker symbol to use for the status. The marker's string width must be 1.When provided as a string (e.g. ' '), the string is used as both the recognized and written marker.When provided as a table (e.g. { 'X', 'x' }), the first element is the primary marker — the one written to the buffer when the status is set. Any additional elements are legacy markers that will be recognized as belonging to this status when read from a file, but will be replaced with the primary marker on the next toggle. This is useful for accepting alternate symbols from other tools or conventions without losing compatibility.
    to_do.statuses.<name>.highlight.markertable (highlight definition)A table of highlight definitions to apply to a status marker, including brackets. See the {val} parameter of :h nvim_set_hl for possible options.
    to_do.statuses.<name>.highlight.contenttable (highlight definition)A table of highlight definitions to apply to the to-do item content (everything following the status marker). See the {val} parameter of :h nvim_set_hl for possible options.
    to_do.statuses.<name>.sort.sectionintegerThe integer should represent the linear section of the list in which items of this status should be placed when sorted. A section refers to a segment of a to-do list. If you want items with the 'in_progress' status to be first in the list, you would set this option to 1 for the status.Note: Sections are not visually delineated in any way other than items with the same section number occurring on adjacent lines in the list.
    to_do.statuses.<name>.sort.positionstringWhere in its assigned section a to-do item should be placed:'top': Place a sorted item at the top of its corresponding section.'bottom': Place a sorted item at the bottom of its corresponding section.'relative': Maintain the current relative order of the sorted item whose status was just changed (vs. other list items).
    to_do.statuses.<name>.propagate.upfun(to_do_list): string | nilA function that accepts a to-do list instance and returns a valid to-do status name. The list passed in is the list that hosts the to-do item whose status was just changed. The return value should be the desired value of the parent. Return nil to leave the parent's status as is.
    to_do.statuses.<name>.propagate.downfun(to_do_list): string[]A function that accepts a to-do list instance and returns a list of valid to-do status names. The list passed in will be the child list of the to-do item whose status was just changed. Return nil or an empty table to leave the children's status as is.
    to_do.status_ordertable (array-like)An ordered list of status names that controls the toggle rotation order. When a to-do item is toggled, it cycles through the statuses in this order. A status defined in to_do.statuses but absent from status_order is still recognized when reading files, but excluded from toggle rotation (useful for statuses that should only be set via propagation).Replaces the deprecated to_do.statuses[*].skip_on_toggle option.

    [!WARNING] The following to-do configuration options are deprecated. Please use the to_do.statuses dict and to_do.status_order instead. Continued support for these options is temporarily provided by a compatibility layer that will be removed in the near future.

    • to_do.symbols - A list of markers representing to-do completion statuses
    • to_do.not_started - Which marker represents a not-yet-started to-do
    • to_do.in_progress - Which marker represents an in-progress to-do
    • to_do.complete - Which marker represents a complete to-do
    • to_do.update_parents - Whether parent to-dos' statuses should be updated
    • to_do.statuses (array form) - The old array-of-tables format for statuses
    • to_do.statuses[*].skip_on_toggle - Use status_order membership instead
    • to_do.statuses[*].exclude_from_rotation - Use status_order membership instead
    foldtext
    require('mkdnflow').setup({
        foldtext = {
            object_count = true,
            object_count_icon_set = 'emoji',
            object_count_opts = function()
                return require('mkdnflow').foldtext.default_count_opts()
            end,
            line_count = true,
            line_percentage = true,
            word_count = false,
            title_transformer = function()
                return require('mkdnflow').foldtext.default_title_transformer
            end,
            fill_chars = {
                left_edge = '⢾⣿⣿',
                right_edge = '⣿⣿⡷',
                item_separator = ' · ',
                section_separator = ' ⣹⣿⣏ ',
                left_inside = ' ⣹',
                right_inside = '⣏ ',
                middle = '⣿',
            },
        },
    })
    
    OptionTypeDescription
    foldtext.object_countbooleantrue (default): Show a count of all objects inside of a folded section.false: Do not show a count of any objects inside of a folded section.
    foldtext.object_count_icon_setstring | table'emoji' (default): Use pre-defined emojis as icons for counted objects.'plain': Use pre-defined plaintext UTF-8 characters as icons for counted objects.'nerdfont': Use pre-defined nerdfont characters as icons for counted objects. Requires a nerdfont.table<string, string>: Use custom mapping of object names to icons.
    foldtext.object_count_optsfun(): table<string, table>A function that returns the options table defining the final attributes of the objects to be counted, including icons and counting methods. The pre-defined object types are tbl, ul, ol, todo, img, fncblk, sec, par, and link.
    foldtext.line_countbooleantrue (default): Show a count of the lines contained in the folded section.false: Don't show a line count.
    foldtext.line_percentagebooleantrue (default): Show the percentage of document (buffer) lines contained in the folded section.false: Don't show the percentage.
    foldtext.word_countbooleantrue: Show a count of the paragraph words in the folded section, ignoring words inside of other objects.false (default): Don't show a word count.
    foldtext.title_transformerfun(): fun(string): stringA function that returns another function. The inner function accepts a string (the section heading text) and returns a potentially modified string.
    foldtext.fill_chars.left_edgestringThe character(s) to use at the very left edge of the foldtext. Default: '⢾⣿⣿'.
    foldtext.fill_chars.right_edgestringThe character(s) to use at the very right edge of the foldtext. Default: '⣿⣿⡷'
    foldtext.fill_chars.item_separatorstringThe character(s) used to separate the items within a section. Default: ' · '
    foldtext.fill_chars.section_separatorstringThe character(s) used to separate adjacent sections. Default: ' ⣹⣿⣏ '
    foldtext.fill_chars.left_insidestringThe character(s) used at the internal left edge of fill characters. Default: ' ⣹'
    foldtext.fill_chars.right_insidestringThe character(s) used at the internal right edge of fill characters. Default: '⣏ '
    foldtext.fill_chars.middlestringThe character used to fill empty space in the foldtext line. Default: '⣿'
    -- SAMPLE FOLDTEXT CONFIGURATION RECIPE WITH COMMENTS
    require('mkdnflow').setup({
        foldtext = {
            title_transformer = function()
                local function my_title_transformer(text)
                    local updated_title = text:gsub('%b{}', '')
                    updated_title = updated_title:gsub('^%s*', '')
                    updated_title = updated_title:gsub('%s*$', '')
                    updated_title = updated_title:gsub('^######', '░░░░░▓')
                    updated_title = updated_title:gsub('^#####', '░░░░▓▓')
                    updated_title = updated_title:gsub('^####', '░░░▓▓▓')
                    updated_title = updated_title:gsub('^###', '░░▓▓▓▓')
                    updated_title = updated_title:gsub('^##', '░▓▓▓▓▓')
                    updated_title = updated_title:gsub('^#', '▓▓▓▓▓▓')
                    return updated_title
                end
                return my_title_transformer
            end,
            object_count_icon_set = 'nerdfont',
            object_count_opts = function()
                local opts = {
                    link = false,
                    blockquote = {
                        icon = ' ',
                        count_method = {
                            pattern = { '^>.+$' },
                            tally = 'blocks',
                        }
                    },
                    fncblk = { icon = ' ' }
                }
                return opts
            end,
            line_count = false,
            word_count = true,
            fill_chars = {
                left_edge = '╾─🖿 ─',
                right_edge = '──╼',
                item_separator = ' · ',
                section_separator = ' // ',
                left_inside = ' ┝',
                right_inside = '┥ ',
                middle = '─',
            },
        },
    })
    

    The above recipe will produce foldtext like the following (for an h3-level section heading called My section):

    tables
    require('mkdnflow').setup({
        tables = {
            type = 'pipe',
            trim_whitespace = true,
            format_on_move = true,
            auto_extend_rows = false,
            auto_extend_cols = false,
            style = {
                cell_padding = 1,
                separator_padding = 1,
                outer_pipes = true,
                apply_alignment = true,
            },
        },
    })
    
    OptionTypeDescription
    tables.typestring'pipe' (default): New tables are created in pipe format (| cell | cell | with | --- | --- | separator).'grid': New tables are created in pandoc grid format with +---+ border lines between rows and +===+ header separators. Grid tables support native multiline cells.Regardless of this setting, existing tables are auto-detected and handled in their native format when formatting, navigating, or editing.
    tables.trim_whitespacebooleantrue (default): Trim extra whitespace from the end of a table cell when a table is formatted.false: Leave whitespace at the end of a table cell when formatting.
    tables.format_on_movebooleantrue (default): Format the table each time the cursor is moved to the next/previous cell/row using the plugin's API.false: Don't format the table when the cursor is moved.
    tables.auto_extend_rowsbooleantrue: Add another row when attempting to jump to the next row and the row doesn't exist.false (default): Leave the table when attempting to jump to the next row and the row doesn't exist.
    tables.auto_extend_colsbooleantrue: Add another column when attempting to jump to the next column and the column doesn't exist.false (default): Go to the first cell of the next row when attempting to jump to the next column and the column doesn't exist.
    tables.style.cell_paddinginteger1 (default): Use one space as padding at the beginning and end of each cell.<n>: Use <n> spaces as cell padding.
    tables.style.separator_paddinginteger1 (default): Use one space as padding in the separator row.<n>: Use <n> spaces as padding in the separator row.
    tables.style.outer_pipesbooleantrue (default): Include outer pipes when formatting a table.false: Do not use outer pipes when formatting a table.
    tables.style.apply_alignmentbooleantrue (default): Apply the cell alignment indicated in the separator row when formatting the table.false: Always visually left-align cell contents when formatting a table.Previously named tables.style.mimic_alignment.
    yaml
    require('mkdnflow').setup({
        yaml = {
            bib = { override = false },
        },
    })
    
    OptionTypeDescription
    yaml.bib.overridebooleantrue: A bib path specified in a markdown file's yaml header should be the only source considered for bib references in that file.false (default): All known bib paths will be considered, whether specified in the yaml header or in your configuration settings.
    mappings
    require('mkdnflow').setup({
        mappings = {
            MkdnEnter = { { 'n', 'v' }, '<CR>' },
            MkdnGoBack = { 'n', '<BS>' },
            MkdnGoForward = { 'n', '<Del>' },
            MkdnMoveSource = { 'n', '<F2>' },
            MkdnNextLink = { 'n', '<Tab>' },
            MkdnPrevLink = { 'n', '<S-Tab>' },
            MkdnFollowLink = false,
            MkdnDestroyLink = { 'n', '<M-CR>' },
            MkdnTagSpan = { 'v', '<M-CR>' },
            MkdnYankAnchorLink = { 'n', 'yaa' },
            MkdnYankFileAnchorLink = { 'n', 'yfa' },
            MkdnNextHeading = { 'n', ']]' },
            MkdnPrevHeading = { 'n', '[[' },
            MkdnNextHeadingSame = { 'n', '][' },
            MkdnPrevHeadingSame = { 'n', '[]' },
            MkdnIncreaseHeading = { { 'n', 'v' }, '+' },
            MkdnDecreaseHeading = { { 'n', 'v' }, '-' },
            MkdnIncreaseHeadingOp = { { 'n', 'v' }, 'g+' },
            MkdnDecreaseHeadingOp = { { 'n', 'v' }, 'g-' },
            MkdnToggleToDo = { { 'n', 'v' }, '<C-Space>' },
            MkdnNewListItem = false,
            MkdnNewListItemBelowInsert = { 'n', 'o' },
            MkdnNewListItemAboveInsert = { 'n', 'O' },
            MkdnExtendList = false,
            MkdnUpdateNumbering = { 'n', '<leader>nn' },
            MkdnTableNextCell = { 'i', '<Tab>' },
            MkdnTablePrevCell = { 'i', '<S-Tab>' },