Title: Markdown outline in (neo)vim with fzf
Date: 2026-07-01 02:30

I'm currently somehow or other writing a book, meaning that I'm spending a
significant amount of time with markdown files open in my text editor, which
happens to be (neo)vim. It's one of the least worst editors around, and I've
been using it for more than 15 years I think. Unfortunately it doesn't have a
great scripting language: vimscript is dreadful to read and equally dreadful to
write, like an organic growth of a Perl tumor on top of a JavaScript teratoma
on a perpetually dying swamp creature.

Anyway, the book currently has a bit less than 500 headings of various levels spread
between ~20 chapters, making it non-trivial to keep them all in my head,
meaning that I often wonders in what section the paragraph I'm writing is. So I
took it upon myself to get dirty, and wrote a small snippet of vimscript to
show me the current file's outline and my current position in it. It doesn't go
out of its way to handle technically-valid-but-nobody-should-do-that markdown
and is reasonably fast, taking around 7ms on a 20k lines/400 headings file on
my machine. It should be compatible with neovim ≥ 0.10 and vim ≥ 9.1.0099, as
the only *modern* construct it uses is the
[`matchbufline`](https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef)
function.

```vimscript
function! MarkdownOutline() abort
  if &filetype !=# 'markdown'
    echohl WarningMsg | echo 'Not a markdown file' | echohl None | return
  endif

  let l:cur_line = line('.')                                                 
  let l:cur_idx = 0                                                          
  let l:entries = []                                                         
  let l:is_fenced = 0                                                        
  for l:match in matchbufline(bufnr(''), '\v^%(#+ |```)', 1, '$')            
    let l:text = l:match.text                                                
    if l:text[0] ==# '`'                                                     
      let l:is_fenced = !l:is_fenced                                         
    elseif !l:is_fenced                                                      
      let l:line = getline(l:match.lnum)                                     
      let l:level = stridx(l:line, ' ')                                      
      let l:title = l:line[l:level :]                                        
      call add(l:entries, l:match.lnum . "\t" . repeat(' ', l:level - 1) . l:title)  
      if l:match.lnum <= l:cur_line                                          
        let l:cur_idx = len(l:entries)                                       
      endif                                                                  
    endif                                                                    
  endfor                                                                     

  if empty(l:entries)
    echohl WarningMsg | echo 'No headings found' | echohl None | return
  endif

  let l:options = ['--prompt', 'Outline> ', '--delimiter', "\t",
        \          '--with-nth', '2..', '--layout', 'reverse']
  if l:cur_idx > 0
    let l:options += ['--bind', 'load:pos(' . l:cur_idx . ')']
  endif

  call fzf#run(fzf#wrap('markdown-outline', {
        \ 'source':  l:entries,
        \ 'sink':    {l -> execute(matchstr(l, '^\d\+') . ' | normal! zz')},
        \ 'options': l:options,
        \ }))
endfunction

nnoremap <silent> <leader>o :call MarkdownOutline()<CR>
```

It looks like this:

![screenshot of the script in action]({static}/images/ouline_vim_md.png)

It depends on [fzf](https://github.com/junegunn/fzf), to both show/filter the
outline and quickly jump to whichever I want. Fortunately, it is packaged in
most distributions, along with its thin vimscript wrapper that can be loaded
like this:

```vimscript
for plugin_path in [
                        \ '/usr/share/doc/fzf/examples/plugin/fzf.vim',
                        \ '/usr/share/nvim/runtime/plugin/fzf.vim',
                        \ '/usr/share/vim/vimfiles/plugin/fzf.vim',
                        \ '/usr/share/nvim/site/plugin/fzf.vim',
                        \ ]
  if filereadable(plugin_path)
    exec "source" . fnameescape(plugin_path)
    nnoremap <C-p> :FZF<Cr>
    break
  endif
endfor
```

Now, I'm sure that there are plugins on GitHub doing all of this, but I'm
averse to the idea of my editor downloading random code from the internet
and executing it, sometimes as root.
