Build the outline from the current buffer without tripping over fenced code
matchbufline() gives a quick way to pull matching lines from the current buffer without looping through every line in Vimscript. For a Markdown outline, the useful matches are headings and triple-backtick fence markers. One pass is enough if you keep a simple state flag for whether the current line sits inside a fenced block.
That keeps the logic small. Every time a line appears, flip the fence state. When the state is open, ignore heading matches until the next fence marker closes it. This misses other Markdown fence styles, but triple backticks cover the common case cleanly.
Match headings and fences in one pass with matchbufline
The pattern can match both heading lines and fence markers, so the function only needs one list of hits to work through. Headings give you the section entries. Fence markers decide whether those headings count or get dropped.
Heading level can come from the position of the first space in the line. A short # heading sorts above a deeper ### heading without extra parsing. For an outline picker, that is enough. The entry only needs the line number, some indentation, and the heading text.
Skip headings inside code blocks and keep the current section in view
A heading inside a fenced block looks real to the editor and wrong to the reader. The fence flag stops that from polluting the outline. It also lets the picker find the current section from the cursor line before opening fzf, which is what makes the jump feel direct instead of random.
The current heading can be tracked while the scan runs. Once the cursor line sits between two headings, the last valid one is the active section. That line number becomes the preselection target later, so the picker opens on the place you are already reading.
Feed the matches into fzf and jump straight to the chosen section
fzf works well here because the outline is just a short list of entries, not a full buffer search. Each row can carry the line number in a hidden field, with the visible part showing the section text. The picker then stays out of the way and does one job: choose a heading and jump there.
The jump itself is simple. Pull the line number back out of the selected entry, execute the move, then run normal! zz to centre the heading in the window. That last step matters more than it sounds. Without it, the cursor lands on the line and the rest of the file keeps its old position, which feels slightly off.
Format each entry with line number, indent, and heading text
A compact entry format keeps fzf readable and makes the sink function easy to write. Line number first, a tab, then the display text. Indentation can reflect heading depth, so the list keeps the structure of the document instead of flattening every section into the same row.
--delimiter '\ ' and --with-nth 2.. are enough for this. fzf sees the line number, but only shows the human part. That keeps the interface tidy and avoids stuffing the picker with raw metadata.
Preselect the heading under the cursor and centre it with normal! zz
fzf can start on the current section if you pass the matching entry index into a load:pos bind. That is the difference between an outline and a proper navigation aid. The picker opens where you already are, and the list around it gives you nearby structure instead of a blind jump from the top.
Once a selection is made, execute can send the cursor straight to the chosen line. normal! zz follows to centre the result. It is a small touch, but it keeps repeated jumps usable when the file has a few dozen headings and the current view would otherwise drift all over the place.
Keep the mapping small, and handle the dead ends cleanly
A single normal-mode mapping is enough. nnoremap <silent> <leader>o :call MarkdownOutline()<CR> keeps the outline feature close without making it noisy. If fzf is already bound to something else, leave that alone and keep the outline on its own key.
The function should also stop cleanly when the buffer is not Markdown or when no headings are found. A blunt message is better than a half-working picker that opens empty. That leaves the failure obvious and saves a bit of head-scratching later, which is always a nice side effect in Vimscript.

