Vim Markdown Folding?

Viewed 20149

I just realized that VIM 7.3 has built-in support for highlighting Markdown files. Excellent. However, it doesn't fold on the headings.

Can any offer suggestions on how to get this working?


Alternatively, I'm using Markdown only as a way to get simple structured text. If there is a better alternative format, please also suggest. But not sure I dig TVO or VimOutliner.

13 Answers

When I use markdown I only use the hash-style headings with space separating hashes and text. This makes the folding task a lot simpler.

I'm pretty new to Vim, so use the following at your own risk. I added the following code to my vimrc and it folds headings based on number of hashes, and it retains the syntax colouring.

function! MarkdownLevel()
    if getline(v:lnum) =~ '^# .*$'
        return ">1"
    endif
    if getline(v:lnum) =~ '^## .*$'
        return ">2"
    endif
    if getline(v:lnum) =~ '^### .*$'
        return ">3"
    endif
    if getline(v:lnum) =~ '^#### .*$'
        return ">4"
    endif
    if getline(v:lnum) =~ '^##### .*$'
        return ">5"
    endif
    if getline(v:lnum) =~ '^###### .*$'
        return ">6"
    endif
    return "=" 
endfunction
au BufEnter *.md setlocal foldexpr=MarkdownLevel()  
au BufEnter *.md setlocal foldmethod=expr     

There is a vim-markdown plugin at https://github.com/plasticboy/vim-markdown .

The code related to folding from there appears to be:

" fold region for headings
syn region mkdHeaderFold
    \ start="^\s*\z(#\+\)"
    \ skip="^\s*\z1#\+"
    \ end="^\(\s*#\)\@="
    \ fold contains=TOP

" fold region for lists
syn region mkdListFold
    \ start="^\z(\s*\)\*\z(\s*\)"
    \ skip="^\z1 \z2\s*[^#]"
    \ end="^\(.\)\@="
    \ fold contains=TOP

syn sync fromstart
setlocal foldmethod=syntax

Working from @Omar comment to this answer, I coded a fold method to languages which comment with //, like JS. Add following to ~/.vimrc:

autocmd FileType javascript setlocal foldmethod=expr foldcolumn=6 
autocmd FileType javascript setlocal foldexpr=JSFolds()

" Level of a folding:
    "// #: level 1
    "// ##: level 2
    "// ###: level 3
function! JSFolds()
    " Option 1: // and no space and hashes:
        "if getline(v:lnum) =~ '^//#'
    " Option 2: // and 1 space and hashes:
        "if getline(v:lnum) =~ '^//\+ #'
    " Option 3: spaces/tabs/nothing  and // and 1 space and hashes:
    if getline(v:lnum) =~ '^\s*//\+ #'
    " Option 4: anything and // and 1 space and hashes:
    " DANEGROUS! Potential conflict with code. E.g. print("// # Title");
    " if getline(v:lnum) =~ '//\+ #'

        " Number of hashs # in line that success previous condition (if)
        " determine the fold level
       let repeatHash = len(matchstr(getline(v:lnum), '#\+'))
       return ">" . repeatHash
    endif
    return "=" 
endfunction

Examples. Note on the left the fold levels ("|" and "-"):

-     // # ** Fold style recommended **
-     // #       1  easy case
|-    // ##      2  easy case
||-   // ###     3  easy case
|||   //            Comment inside level 3
|||-  // ####    4  easy case
||||  //            Comment inside level 4
|-        // ##  2  easy case (indents are OK with Option 3)
||    /####     error (JS comment needs 2 slashes)
||  
-     // # ** Fold of just 1 line **
|--   // ###    3  easy case
||-   // ###    =  same fold level as previous line, thus previous line folds just itself ?!? (not concerns this fold function) 
|||   
-     // # ** Space needed before, BUT not needed after hash/-es **
|-    // ##Fold Level   changed Because no space after hashes is OK:    '// # ' vs '// #NoSpace'. NoSpace could even be a return carriage (enter). 
||    //## Fold Level Unchanged Because no space after pair of slashes: '// #' vs '//#'
||    //     ##txt    Unchanged Because too much space after slashes
||    //     ## txt   Unchanged Because too much space after slashes
||    
-     // # ** Odds vs Even slashes **
-     /// #     1  overrides typo 3 slash instead of just 2 (/// vs //)
-     ///// #   1  overrides typo 5 slash instead of just 4 (///// vs ////). Read Recommenting Comments notes.
|-    // ## ** As long as the pattern is at least '// # ', further previous slashes are ok  **
-     // #            1  easy case
|--   // ###          3  ok (and recommended fold style)
||-   ///// ###       3  ok (recommented + typo)
||-   ////// ###      3  ok (re-recommented)
||-   /// ###         3  ok (typo)
||-   //// ###        3  ok (recommented)
||-   ///////// ###   3  ok (who cares? it works!)
|||   
-     // # ** Recommenting Comments **
-     // #      1  easy case
|     //           Comment inside level 1
-     //// #    1  recommented a comment
|     ////         Comment inside level 1
-     ///// #   1  re-re-recomment
|     /////        Comment inside level 1
|     
-     // # ** Recommenting Comments adding text **
|--   // ### //// #    3  changing fold level on purpose of a recommented a comment
|||   //               Comment inside level 3
|||   // text  // ##         2  (recommented a comment adding text)
|||   // text#text  // ##    2  right   {recommented a comment adding initial text, as long as this text has no hash just after '// ' (2*slash + space) would be ok }
-     // #text#text  // ##   2  wrongly {recommented a comment adding initial text, as long as this text has no hash just after '// ' (2*slash + space) would be ok }
-     // # changeFoldIntentionally // ##     1  clear intention to change fold level of comments
-     // #changeFoldIntentionally // ##      1  clear intention to change fold level of comments (previousi example, with space after hash would be clearer)
|--   // ### changeFoldIntentionally // ##   3  clear intention to change fold level of comments
|||   

PD: totally open to critics and improvements of the code. Actually I'm a beginner with vimscript.

Here's what I came up with as a combination of many of the other answers here. I found that most of them, including the builtin g:markdown_folding, do not properly handle code blocks that contain # characters as part of comments. I based this on matching the syntax IDs, which also handles <h1-6> tags properly.

" ~/.vim/ftplugin/markdown.vim

function MarkdownLevel(lnum)
  for synID in synstack(a:lnum, 1)
    let name = synIDattr(synID, "name")
    if     name == 'htmlH1' | return ">1"
    elseif name == 'htmlH2' | return ">2"
    elseif name == 'htmlH3' | return ">3"
    elseif name == 'htmlH4' | return ">4"
    elseif name == 'htmlH5' | return ">5"
    elseif name == 'htmlH6' | return ">6"
    endif
  endfor
  return "="
endfunction

setlocal foldexpr=MarkdownLevel(v:lnum)
setlocal foldmethod=expr
setlocal foldlevel=1
Related