Build the heading scan around fenced block state
The cleanest route is to scan for both Markdown headings and triple-backtick lines in one pass, then keep a fenced state while you walk the matches. matchbufline() fits that pattern in Neovim 0.10 and Vim 9.1.0099, because it can return the lines you care about without dragging the whole buffer through a slower loop.
A practical filter looks for two things only: lines that start with # and lines that start with . When a fence line appears, flip the fenced flag. While that flag is set, ignore headings completely. That stops headings inside examples from polluting the outline, which is the whole point.
Use matchbufline() to catch headings and fence lines in one pass
The search pattern can be blunt. Headings and fence markers are both easy to spot from the start of the line, so there is no need to get clever. One pass through the matches is enough to build the list and keep the code small.
Heading level can come from the first space in the line. Count the # characters before it, then use that to indent the picker entry. It is crude, but it works for normal Markdown headings and keeps the logic obvious.
Toggle fenced mode only on triple backticks and ignore headings while it is set
Fence handling is deliberately narrow here. Toggle state only when the line begins with triple backticks, then skip any heading matches until the next fence line appears. That matches the common case and avoids trying to understand every Markdown fence variant under the sun.
That narrowness has a cost. It will miss uncommon fence forms and odd Markdown edge cases. For a navigation helper, that is usually acceptable, because the bad result is obvious: a heading appears where it should not, and you can see why.
Feed the outline picker without losing the current section
Once the list exists, the picker should show enough context to make the jump predictable. A line number on its own is fine for machines and useless for humans. Add indentation and the heading title, then let fzf do the sorting and preview work it is good at.
The current section should stay near the top of the list when the picker opens. Preselecting the nearest heading to the cursor avoids a pointless scroll through the entire outline just to get back to where you already were. It feels minor until the list has a few hundred headings and you notice how tedious the alternative gets.
Format each entry with line number, indent, and title for fzf
A simple entry format keeps the sink function easy. Store the line number, a tab, then the indented title. fzf can split on the tab and display only the title part, while the line number stays available for the jump.
That makes the picker readable without extra parsing tricks. The line number stays hidden until selection time, and the title keeps its original shape. It is a boring format, which is usually a good sign.
Preselect the heading nearest the cursor before opening the list
Track the heading whose line number is closest to the cursor before calling fzf. Then pass that index through load:pos() so the picker opens at the current section instead of at the top.
That small bit of state saves time in long files. It also makes the mapping feel anchored to the buffer, not to a detached list of headings that forgot where you were.
Keep the mapping small, and make the failure cases obvious
The command should stay tied to Markdown buffers. If the filetype is wrong, return a clear message and stop. If no headings are found, say that too. Silent failure is a nuisance in a navigation tool, because the user has no clue whether the buffer was empty, unsupported, or just badly formed.
A short mapping is enough: one command, one key, one job. In practice, that usually means a normal mode mapping such as <leader>o calling the outline function. Anything longer starts to feel like ceremony.
Gate the command to Markdown buffers and return a clear message when nothing matches
Check &filetype before doing any work. If it is not markdown, stop with a warning message such as Not a markdown file. After the scan, if the outline list is empty, return No headings found.
Those two checks cover the main failure modes without guessing. They also stop the function from opening a picker full of nothing, which is a special kind of waste of time.
Note the Neovim 0.10 and Vim 9.1.0099 floor, then verify the jump lands cleanly
This approach depends on matchbufline(), so the minimum versions matter. Neovim 0.10 or newer and Vim 9.1.0099 or newer are the sensible floor. Older builds will not have the function, so the outline scan will simply fail before it starts.
After selection, jump to the chosen line and centre it with normal! zz. That keeps the target heading in view instead of shoving it to the top or bottom of the screen like an afterthought.


