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
function.
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:

It depends on 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:
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.