Better Folding in Neovim

Folding is a nice mechanism to clean up what you see on your screen while you are working on some code. Neovim, as always, offers tons of options for configuration. Here, I want to go through two configurations. At the end of the post you can find some additional info about folding in Vim.

General Setup

The easiest way to Neovim’s folding feature with code and other text documents is to use its foldmethod=indent in your init.vim.

If you want to use folds regularly, but don’t want folds to be applied when you open a file you should set nofoldenable. Actually, there seems to be a bug (or wanted behaviour I don’t understand) that applies folds then afterwards kinda everywhere. To avoid this behaviour add set foldlevel=99 (foldlevel can alternatively be set to any other high number).

Our init.vim until now:

setlocal foldmethod=indent
set nofoldenable
set foldlevel=99

Simplify Indentation Fold Text

A lot of people like how Neovim displays folds but it drives me personally completely crazy. Instead of displaying the first element of the last indentation level, Neovim’s indent mechanism folds only all elements of a certain indentation level and displays the first of those. This way you get your fold text to display some unnecessary information.


int main(int argc, char *argv[])
{
+-- 31 lines: if (argc != 2) {·······················
}

As you can see in the example above the main function is 31 lines long, but by default Neovim displays the content of the first line of that indentation level, in this case if (argc != 2) { which contains no helpful information for us when we want to fold main’s content.

An easy and effective way to improve this is by changing the foldtext. Thankfully, Vim has been around for such a long time that most of the problems one faces while customizing their dotfiles have already been solved tons of times. In this case Greg Sexton has written a lovely small function we can use in his post “Improving the text displayed in a Vim fold”. One of the benefits of Greg’s function is that it can afterwards be easily adjusted without having any knowledge of Vimscript.

We additionally need to set fillchars to a space to avoid having Neovim fill the rest of the line with dots.

We update our init.vim with a modified version of Greg’s function:

setlocal foldmethod=indent
set nofoldenable
set foldlevel=99
set fillchars=fold:\ "The backslash escapes a space
set foldtext=CustomFoldText()

function! CustomFoldText()
  let indentation = indent(v:foldstart - 1)
  let foldSize = 1 + v:foldend - v:foldstart
  let foldSizeStr = " " . foldSize . " lines "
  let foldLevelStr = repeat("+--", v:foldlevel)
  let expansionString = repeat(" ", indentation)

  return expansionString . foldLevelStr . foldSizeStr
endfunction

Which makes our fold look like the following now:


int main(int argc, char *argv[])
{
+-- 31 lines
}

Customize Fold Behaviour

This way the fold already feels way nicer. But for me it is counter intuitive that a doesn’t collapse completely but leaves one occupied with displaying the fold’s information. Again, other people thought the same. Steve Losh, who wrote the excellent book “Learn Vimscript the Hard Way”, has a chapter in it called “Advanced Folding” in which he builds step by step the wanted functionality. I recommend that you go and check it out, as we won’t discuss it here.

We add Steve’s function to our init.vim and change Greg’s function to display the wanted information:

set nofoldenable
set foldlevel=99
set fillchars=fold:\
set foldtext=CustomFoldText()
setlocal foldmethod=expr
setlocal foldexpr=GetPotionFold(v:lnum)

function! GetPotionFold(lnum)
  if getline(a:lnum) =~? '\v^\s*$'
    return '-1'
  endif

  let this_indent = IndentLevel(a:lnum)
  let next_indent = IndentLevel(NextNonBlankLine(a:lnum))

  if next_indent == this_indent
    return this_indent
  elseif next_indent < this_indent
    return this_indent
  elseif next_indent > this_indent
    return '>' . next_indent
  endif
endfunction

function! IndentLevel(lnum)
    return indent(a:lnum) / &shiftwidth
endfunction

function! NextNonBlankLine(lnum)
  let numlines = line('$')
  let current = a:lnum + 1

  while current <= numlines
      if getline(current) =~? '\v\S'
          return current
      endif

      let current += 1
  endwhile

  return -2
endfunction

function! CustomFoldText()
  " get first non-blank line
  let fs = v:foldstart

  while getline(fs) =~ '^\s*$' | let fs = nextnonblank(fs + 1)
  endwhile

  if fs > v:foldend
      let line = getline(v:foldstart)
  else
      let line = substitute(getline(fs), '\t', repeat(' ', &tabstop), 'g')
  endif

  let w = winwidth(0) - &foldcolumn - (&number ? 8 : 0)
  let foldSize = 1 + v:foldend - v:foldstart
  let foldSizeStr = " " . foldSize . " lines "
  let foldLevelStr = repeat("+--", v:foldlevel)
  let expansionString = repeat(" ", w - strwidth(foldSizeStr.line.foldLevelStr))
  return line . expansionString . foldSizeStr . foldLevelStr
endfunction

With this folding mechanism we can now already fold the indented lines when our cursor is on the 3. line. Visually, our set up leads to the following lovely result:

int main(int argc, char *argv[])
{                                                            32 lines +--
}

Now the relevant information for the information inside the fold is dispalyed in the fold header as you can see in this example.

int main(int argc, char *argv[])
{
  if (argc != 6) {                                         3 lines +--+--
  }
  //...
}

Conclusion

Whether you choose a customised folding mechanism or not doesn’t matter. It is worth customising Neovim’s default folding for a better developer experience. Personally, I might even prefer the first solution because it fights Vim’s behaviour less. We are so used to folding mechanisms from IDEs that we struggle with alternative ways of displaying information. Maybe the best route to choose is to customize Neovim a bit and accept some of its design decisions.

Folding in Vim

Here some additional informations that you are probably searching for while you learn about folding in vim:

  • You can color folds with highlight Folded and then the colors your fold should have.
  • You open folds by pressing zo and close them by pressing zc. You can toggle between the two by using za.
  • Special trick: you can open all folds with zR and close all with zM.

Thanks

Thanks to Greg Sexton and Steve Losh without whom this article wouldn’t have possible. Thanks to content creators like them we can try new tools without reinventing the wheel over and over again.

Code Examples

Don’t be surprised if your results have different coloring than the examples in this post. Your Vim setup will most likely render the fold header in one color instead of including syntax highlighting. Sorry!