1. Introduction

Code is often the middle ground between pure machine and human language. As such, humans usually try to make it as convenient as possible to read, write, understand, and process. Thus, code formatting is critical when it comes to development of any kind. In particular, isolating some parts of code while working on others within the same file can drastically increase productivity.

In this tutorial, we discuss code folding and how the feature works in the Vi editor. First, we talk about code and text folding in general. After that, we check how this is generally handled in the context of Vi. Next, we go over some basic folding methods within the editor. Finally, we explore some advanced ways to produce the desired folding in Vi.

For brevity, we use vi (Vi) when referencing both the Vi and Vim editors. Where they differ, the reader is free to add and remove m (M) if necessary.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.

2. Code and Text Folding

Code or text folding means hiding currently irrelevant parts of text when reading or writing.

This can be invaluable for improving a number of characteristics and activities:

  • readability
  • clarity
  • reviewing
  • troubleshooting

Let’s see some examples.

2.1. Regular Text

Of course, we can fold regular text:

Code is often the middle ground between pure machine and human language. As such, humans usually try to make it as
convenient as possible to read, write, understand, and process. Thus, code formatting is critical when it comes to
development of any kind. In particular, isolating some parts of code while working on others within the same file
can drastically increase productivity.

In this tutorial, we discuss code folding and how the feature works in the Vi editor. First, we
talk about code and text folding in general. After that, we check how this is generally handled in the context of
Vi. Next, we go over some basic folding methods within the editor. Finally, we explore some advanced ways to
produce the desired folding in Vi.

For brevity, we use vi (Vi) when referencing both
the Vi and Vim editors. Where they differ, the reader
is free to add and remove m (M) if necessary.

This is the code for the first part of this article. If we only want to work on the second paragraph, we might want to hide the rest:

[+]Code is the middle gr[...]

In this tutorial, we discuss code folding and how the feature works in the Vi editor. First, we
talk about code and text folding in general. After that, we check how this is generally handled in the context of
Vi. Next, we go over some basic folding methods within the editor. Finally, we explore some advanced ways to
produce the desired folding in Vi.

[+]For brevity, we use [...]

Basically, we collapse the first and third paragraphs to an arbitrary 20 characters, giving way to a much clearer view of the one in focus. Our actions in this imaginary editor are indicated by the common + plus sign, as seen in many interfaces with the same collapsability options.

2.2. Code

Usually, folding is even more convenient when it comes to code:

alpharaw='a,b,c,d
e,f,g
h,i,,k
l,,n,o'

readarray -t -d $'\n' alpharows < <(printf '%s' "$alpharaw")

get_element () {
  alpharaw="$1"
  row="$2"
  col="$3"
  sep="${4:-,}"
  local alpharow alpharows
  IFS=$'\n' readarray -t alpharows < <(printf '%s' "$alpharaw")
  if [[ $row -ge ${#alpharows[@]} ]]; then
    echo "Bad row."
    return
  fi
  IFS="$sep" read -ra alpharow < <(printf '%s' "${alpharows[$row]}")
  if [[ $col -ge ${#alpharow[@]} ]]; then
    echo "Bad column."
    return
  fi
  echo "${alpharow[$col]}"
}

Here, we see a code excerpt from a custom 2D array implementation. At first glance, it may look a bit messy.

Let’s only focus on the first part by folding the get_element function:

alpharaw='a,b,c,d
e,f,g
h,i,,k
l,,n,o'

readarray -t -d $'\n' alpharows < <(printf '%s' "$alpharaw")

[+]get_element () {

Now, we can see the string declaration and array conversion, as well as the function we can perform without having to clutter our view.

For this reason, many graphical user interface (GUI), command-line interface (CLI), and terminal user interface (TUI) editors implement the feature.

3. Vi Folding

Naturally, the ubiquitous Vi editor supports both text and code folding. Importantly, to have the feature, we should have a version of Vi compiled with +folding.

Further, there are several ways that we can use the feature:

  • manual (default): no automatic folds are created
  • marker: folds are designated by [marker]s
  • indent: folds based on the level of [indent]ation
  • expr: an [expr]ession defines the level of folding
  • diff: fold non-changed text
  • syntax: folds based on the language and syntax structure

To set our method of choice, we :se[t] the foldmethod (fdm) option with the respective assignment.

Regardless of the way we create folds, there are several commands to open and close them:

+----------+-----------------------------------------------+
| Shortcut | Action                                        |
+----------+-----------------------------------------------+
| za       | [a]lternate, i.e., toggle fold at cursor      |
| zA       | [A]lternate, i.e., toggle all folds at cursor |
| zo       | [o]pen fold at cursor                         |
| zO       | [O]pen all folds at cursor                    |
| zc       | [c]lose fold at cursor                        |
| zm       | increase foldlevel [m]ore, i.e., close fold   |
| zM       | [M]ost foldlevel, i.e., close all open folds  |
| zr       | [r]educe foldlevel                            |
| zR       | [r]educe foldlevel to 0, i.e., open all folds |
| zx|zX    | force update folds                            |
+----------+-----------------------------------------------+

In all cases, a fold has a fairly unified look:

+-- 666 lines: First line of fold------

The fold starts with the + plus sign, indicating the fold. Next, the number of dashes indicates the number of whitespace characters starting the line or the level of the fold. After that, we see the actual first line of the folded block. Finally, dashes fill any remaining empty part of the line.

To ensure that a folding method doesn’t immediately fold all possible lines, we can unset the foldenable option:

:set nofoldenable

Alternatively, foldlevel limits the maximum collapsed fold level.

Lastly, we can move the cursor to the [z start or ]z end of folds, as well as to the zj next and zk previous fold.

Now, let’s explore each folding method.

4. Manual Folding

By default, there is no automatic folding when working in Vi. However, we can create some manually via zf{motion} if foldmethod=manual:

int
main(int argc, char *argv[])
{
    int ch, lFlag = 0;
    const char *p;

    if (pledge("stdio rpath", NULL) == -1)
        err(1, "pledge");

    while ((ch = getopt(argc, argv, "LP")) != -1) {█
        switch (ch) {
            case 'L':
                lFlag = 1;
                break;
            case 'P':
                lFlag = 0;
                break;
            default:
                usage();
        }
    }
    argc -= optind;
    argv += optind;

    if (argc != 0)
        usage();

    if (lFlag)
        p = getcwd_logical();
    else
        p = NULL;
    if (p == NULL)
        p = getcwd(NULL, 0);

    if (p == NULL)
        err(EXIT_FAILURE, NULL);

    puts(p);

    exit(EXIT_SUCCESS);
}

If our cursor is at █, we can fold the whole while loop construct:

zf11j

This way, we fold 11 lines j downward:

int
main(int argc, char *argv[])
{
    int ch, lFlag = 0;
    const char *p;

    if (pledge("stdio rpath", NULL) == -1)
        err(1, "pledge");

+------ 12 lines: while ((ch = getopt(argc, argv, "LP")) != -1) {------
    argc -= optind;
    argv += optind;

    if (argc != 0)
        usage();

    if (lFlag)
        p = getcwd_logical();
    else
        p = NULL;
    if (p == NULL)
        p = getcwd(NULL, 0);

    if (p == NULL)
        err(EXIT_FAILURE, NULL);

    puts(p);

    exit(EXIT_SUCCESS);
}

Of course, any motion works in this case.

To delete a fold, we go over it and use zd. Naturally, to delete all folds at the cursor, we employ zD. Furthermore, zE eliminates all folds within the current window.

5. Marker Folding

Similar in most ways to the manual setting, foldmethod=marker also enables the use of markers as fold indicators:

int
main(int argc, char *argv[])
{
    int ch, lFlag = 0;
    const char *p;

    if (pledge("stdio rpath", NULL) == -1)
        err(1, "pledge");
    {{{
    while ((ch = getopt(argc, argv, "LP")) != -1) {
        switch (ch) {
            case 'L':
                lFlag = 1;
                break;
            case 'P':
                lFlag = 0;
                break;
            default:
                usage();
        }
    }}}}
    argc -= optind;
    argv += optind;

    if (argc != 0)
        usage();

    if (lFlag)
        p = getcwd_logical();
    else
        p = NULL;
    if (p == NULL)
        p = getcwd(NULL, 0);

    if (p == NULL)
        err(EXIT_FAILURE, NULL);

    puts(p);

    exit(EXIT_SUCCESS);
}

Notably, we see the default {{{ start and }}} end markers for a fold. At this point, placing the cursor anywhere between the two and using zc would collapse the fold just like before.

In addition, we can specify fold levels for nesting:

[...]
    {{{1
    while ((ch = getopt(argc, argv, "LP")) != -1) {
    {{{2switch (ch) {
            case 'L':
                lFlag = 1;█
                break;
            case 'P':
                lFlag = 0;
                break;
            default:
                usage();
        }}}}
     }}}}
[...]

Unlike with other settings, when foldmethod=marker, all manual options work as well. However, zf would insert, and zd would delete the markers in this case.

Lastly, to change markers, we can modify the foldmarker (fmr) option:

:foldmarker=[[[[,]]]]

Now, four square brackets open and close all folds. Of course, we’re not limited to repeating symbols or characters of a given class, but some are usually better-suited for the purpose.

6. Syntax Folding

One very natural way of folding follows the language syntax of the file. To leverage that, we set foldmethod=syntax. This is used mainly for programming languages.

For example, let’s check the results with our C file from earlier:

int
main(int argc, char *argv[])
+-- 39 lines: {--------------------------------------------------------

Now, if we place our cursor on the fold line and use the arrow key or zo to expand it:

int
main(int argc, char *argv[])
{
    int ch, lFlag = 0;
    const char *p;

    if (pledge("stdio rpath", NULL) == -1)
        err(1, "pledge");

+--- 12 lines: while ((ch = getopt(argc, argv, "LP")) != -1) {---------
    argc -= optind;
    argv += optind;

    if (argc != 0)
        usage();

    if (lFlag)
        p = getcwd_logical();
    else
        p = NULL;
    if (p == NULL)
        p = getcwd(NULL, 0);

    if (p == NULL)
        err(EXIT_FAILURE, NULL);

    puts(p);

    exit(EXIT_SUCCESS);
}

Expanding the inner fold, we see that only the switch statement is left:

[...]
    while ((ch = getopt(argc, argv, "LP")) != -1) {
+---- 10 lines: switch (ch) {------------------------------------------
    }
[...]

Importantly, the case statements aren’t folded:

[...]
    while ((ch = getopt(argc, argv, "LP")) != -1) {
        switch (ch) {
            case 'L':
                lFlag = 1;
                break;
            case 'P':
                lFlag = 0;
                break;
            default:
                usage();
        }
    }
[...]

Since this can produce quite a bit of folding, using foldnestmax can limit the nesting.

Of course, Vi should have a basic understanding of the language in order to ensure the correct folds. So, regular text may not yield good results with this folding method.

7. Indentation Folds

Perhaps one of the most intuitive fold methods is based on indentation, i.e., foldmethod=indent. In other words, folds are determined by the amount of indentation.

Let’s apply this to our previous example:

int
main(int argc, char *argv[])
{
+-- 37 lines: int ch, lFlag = 0;---------------------------------------
}

Here, we see the top-level fold hides everything between the curly braces of the main() function.

Now, let’s expand the fold:

int
main(int argc, char *argv[])
{
    int ch, lFlag = 0;
    const char *p;

    if (pledge("stdio rpath", NULL) == -1)
        err(1, "pledge");

    while ((ch = getopt(argc, argv, "LP")) != -1) {
+--- 10 lines: switch (ch) {-------------------------------------------
    }
    argc -= optind;
    argv += optind;

    if (argc != 0)
        usage();

    if (lFlag)
        p = getcwd_logical();
    else
        p = NULL;
    if (p == NULL)
        p = getcwd(NULL, 0);

    if (p == NULL)
        err(EXIT_FAILURE, NULL);

    puts(p);

    exit(EXIT_SUCCESS);
}

Thus, we can see that each indentation level has its own fold with the proper nesting.

8. Folding Level From Expression

When we want to fold with a way to determine the level based on an expression, we can use foldmethod=expr and set foldexpr.

In fact, foldexpr can be a whole function, the return value of which determines the level:

+--------------+-----------------------------------------+
| Return value | Effect                                  |
+--------------+-----------------------------------------+
| 0            | not in fold                             |
| #            | fold level #                            |
| -1           | undefined, use nearest lowest level     |
| "="          | fold level of last line                 |
| "a#"         | add # to fold level of last line        |
| "s#"         | subtract # from fold level of last line |
| ">#"         | fold with level # starts                |
| "<#"         | fold wth level # ends                   |
+--------------+-----------------------------------------+

Let’s see a basic example:

:set foldexpr=getline(v:lnum)[0]==\"\\t\"

Here, we use getline() with the v:lnum current line number and check whether its [0] first character is a \t Tab. Notably, we escape all set-special characters like quotes, backslashes, and spaces.

Let’s see the result for our example from earlier:

int
main(int argc, char *argv[])
{
+--  2 lines: int ch, lFlag = 0;---------------------------------------

+--  2 lines: if (pledge("stdio rpath", NULL) == -1)-------------------

+-- 14 lines: while ((ch = getopt(argc, argv, "LP")) != -1) {----------

+--  2 lines: if (argc != 0)-------------------------------------------

+--  6 lines: if (lFlag)-----------------------------------------------

+--  2 lines: if (p == NULL)-------------------------------------------

    puts(p);

    exit(EXIT_SUCCESS);
}

Due to the computation of expressions for every line, this folding method can become extremely slow.

9. diff Folding

When using the Vi diff option for file comparisons, foldmethod=diff ensures folds exist for all text not part of or close to changes.

In fact, this is the default folding method when using vimdiff:

$ vimdiff file-old.c file-new.c
[...]
    1 int                                               |    1 void                                               
    2 main(int argc, char *argv[])                      |    2 main(int argc, char *argv[])
    3 {                                                 |    3 {
    4     int ch, lFlag = 0;                            |    4     int ch, lFlag = 0;
    5     const char *p;                                |    5     const char *p;
    6                                                   |    6
    7     if (pledge("stdio rpath", NULL) == -1)        |      -------------------------------------------------
    8         err(1, "pledge");                         |      -------------------------------------------------
    9                                                   |      -------------------------------------------------
   10     while ((ch = getopt(argc, argv, "LP")) != -1) |    7     while ((ch = getopt(argc, argv, "LP")) != -1)
   11         switch (ch) {                             |    8         switch (ch) {
   12             case 'L':                             |    9             case 'L':
   13                 lFlag = 1;                        |   10                 lFlag = 1;
   14                 break;                            |   11                 break;
   15             case 'P':                             |   12             case 'P':
+  16 +-- 26 lines: lFlag = 0;--------------------------|+  13 +-- 26 lines: lFlag = 0;-------------------------

We can see the fold at the bottom, which is exactly 8 lines away from the nearest change.

By default, the close context is defined in lines by setting the context argument to the diffopt option:

:set diffopt=filler,context:1

The default value is 8, but here, we set it to 1. In addition, filler ensures we see the dashed lines where we have removed text.

At this point, we should see the view update:

$ vimdiff file-old.c file-new.c
[...]
    1 int                                               |    1 void                                               
    2 main(int argc, char *argv[])                      |    2 main(int argc, char *argv[])
    3 {                                                 |    3 {
    4     int ch, lFlag = 0;                            |    4     int ch, lFlag = 0;
    5     const char *p;                                |    5     const char *p;
    6                                                   |    6
    7     if (pledge("stdio rpath", NULL) == -1)        |      -------------------------------------------------
    8         err(1, "pledge");                         |      -------------------------------------------------
    9                                                   |      -------------------------------------------------
+  10 +-- 32 lines: while ((ch = getopt(argc, argv, "LP"|+   7 +-- 32 lines: lFlag = 0;while ((ch = getopt(argc, argv, "LP"

As expected, we only see one line around the nearest change.

10. Summary

In this article, we went through text and code folding and the ways it works within the Vi editor.

In conclusion, choosing the folding method that best applies to a given file or content is very important for leveraging the feature’s convenience.