May 24, 2021 Vim
In the last chapter, we added some quick and dirty folds to the Potion file using Vim's
indent
folding.
Open
factorial.pn
turn off all folding with
zM
The file now looks like this:
factorial = (n):
+-- 5 lines: total = 1
10 times (i):
+-- 4 lines: i string print
Expand the first fold, and it looks like this:
factorial = (n):
total = 1
n to 1 (i):
+--- 2 lines: # Multiply the running total.
total.
10 times (i):
+-- 4 lines: i string print
That's great, but I personally like to fold the first line of each block by content. In this chapter we'll write down some custom folding code and end up with this effect:
factorial = (n):
total = 1
+--- 3 lines: n to 1 (i):
total.
+-- 5 lines: 10 times (i):
It's going to be more compact and (for me) easier to read.
If you prefer
indent
it's not a bad idea, but it's a good idea to learn more about the code in Vim that implements folding.
In order to write a custom fold, we need to understand how Vim treats ("think") folding. Explain the rules concisely:
Through an example, we can deepen our understanding. Open a Vim window and paste the text below in.
a
b
c
d
e
f
g
Do the following command to set
indent
:setlocal foldmethod=indent
Take a minute to play folding and see how it works.
Now take a look at the first line offoldlevel by following the following command:
:echom foldlevel(1)
Vim shows
0
.
Now look at the second line:
:echom foldlevel(2)
Vim shows
1
.
Try the third line:
:echom foldlevel(3)
Vim shows
1
This means that lines 2 and 3 belong to a level1 fold.
This is thefoldlevel of each line:
a 0
b 1
c 1
d 2
e 2
f 1
g 0
Reread the rules at the beginning of this section. Turn each fold on or off, observe thefoldlevel, and make sure you understand why it folds like this.
Once you've confidently thought you understand how each row offoldlevel affects the folding structure, move on to the next section.
Before we bury our heads in the keyboard, we plan a few general rules for our folding function.
First, rows that are equally indented should be folded into pieces. We also want the top line to collapse as well to achieve this effect:
hello = (name):
'Hello, ' print
name print.
Fold it like this:
+-- 3 lines: hello = (name):
Empty lines should count down, so empty lines at the bottom of the collapse are not included. This means something like this:
hello = (name):
'Hello, ' print
name print.
hello('Steve')
Fold it like this:
+-- 3 lines: hello = ():
hello('Steve')
And that's not the case:
+-- 4 lines: hello = ():
hello('Steve')
It's a matter of personal preference, of course, but now we're determined.
Start writing our custom folding code now.
Open Vim and split two, one
ftplugin/potion/folding.vim
and the other is the sample
factorial.pn
In the last chapter we closed and reopened Vim to
folding.vim
but there's actually an easier way.
Don't forget that every time
filetype
set to
potion
all files
ftplugin/potion/
executed.
This means that only
factorial.pn
the
:set ft=potion
Vim will reload the collapsed code!
This is much faster than closing and reopening files every time.
The only thing to keep in mind is that you have
to save
folding.vim
hard drive, otherwise unseeded changes won't work.
In order to get unlimited freedom on the fold, we will use
expr
We can continue and remove the foldignore from the
folding.vim
foldignore
it only takes effect when
indent
We're also going to let
expr
folding,
folding.vim
to this:
setlocal foldmethod=expr
setlocal foldexpr=GetPotionFold(v:lnum)
function! GetPotionFold(lnum)
return '0'
endfunction
The first line simply tells Vim to
expr
The second line defines the expression that Vim is used to evaluate each row offoldlevel. W
hen Vim executes an expression, it sets
v:lnum
the line number of the corresponding line it needs.
Our expression will use this number as an argument to a custom function.
Finally, we define a dummy function that returns
0
for any row. N
ote that it returns a string instead of an integer.
We'll find out why we're doing this.
Continue and reload the collapsed code
folding.vim
factorial.pn
:
:set ft=potion
Our function returns
0
for any row, so Vim will not do any folding.
Let's first solve the special case of empty lines.
Modify
GetPotionFold
function like this:
function! GetPotionFold(lnum)
if getline(a:lnum) =~? '\v^\s*$'
return '-1'
endif
return '0'
endfunction
We added an
if
to handle empty lines.
How does it work?
First, let's
getline(a:lnum)
get the contents of the current line as a string.
Let's compare the result with
\v^\s*$
regular expression, . R
emember
\v
means "very magic" (I mean, normal) mode.
This regular expression will match "the beginning of the line, any blank character, the end of the line."
The comparison is made with a case
=~?
D
one. T
echnically we don't have to worry about case, after all, we only match white space, but I prefer to use a clearer approach when comparing strings.
If you like, you
=~
. instead.
If you need to evoke memories of regular expressions in Vim, you should go back and reread the basic regular expressions and Grep Operator.
If the current line includes some non-blank characters, it will not match and we will return
0
Returns the string
'-1'
if the current line matches the regular expression (i.e. for example, if it is empty or has only spaces).
I've said before that a row offoldlevel can be 0 or positive integers, so what happens?
Your custom expression can return either afoldlevel directly or a "special string" to tell Vim how to fold the line.
'-1'
is one of the special strings. I
t informed Vim that the line'sfoldlevel was "underfined".
Vim will understand it as "thefoldlevel of the line equals the smallerfoldlevel of its last or next line".
It's not the final result we're planning, but we can see that it's close enough to meet our goals.
Vim can string the lines of underfined together, so suppose you have three lines of underfined and the next row of level1, it will set the last behavior 1, followed by the penultimate behavior 1, and then the first behavior 1.
When you write custom folding code, you often find several lines that you can easily set up theirfoldlevel.
Then you
'-1'
(or other specialfoldlevels we'll see later) to "waterfall" the remaining rows offoldlevel.
If you
factorial.pn
of the file, Vim_ still does not collapse any lines. T
his is because all rows offoldlevel are either 0 or "undefined".
Rows with a rating of 0 will affect the rows of undefined, eventually resulting in all rows offoldlevel
0
In order to handle non-empty rows, we need to know their indentation levels, so let's create a secondary function to calculate it for us.
Add
GetPotionFold
function! IndentLevel(lnum)
return indent(a:lnum) / &shiftwidth
endfunction
Reload the collapsed code.
In
factorial.pn
executes the following command to test your function:
:echom IndentLevel(1)
Vim shows
0
the first line is not indented.
Now try it in the second line:
:echom IndentLevel(2)
This time Vim shows
1
.
There are four spaces at the beginning of the second line, and
shiftwidth
to 4, so 4 divided by 4 is 1.
Let's divide it by the
shiftwidth
get the indentation level.
Why do we
&shiftwidth
of directly divided by 4? I
f someone prefers to use 2 spaces to indent their Potion code, divided by 4 will result in incorrect results.
shiftwidth
any indented number of spaces.
The direction of the next step is not yet clear. Let's stop and think about what more information is needed to determine which non-empty lines are folded.
We need to know the indentation level for each row.
IndentLevel
function, so this condition has been met.
We also need to know the indentation level for the next non-empty line, because we want to collapse the head of the segment to the corresponding indentation segment.
Let's write an auxiliary function to get the next non-empty row of the given row.
Add
IndentLevel
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
This function is a bit long, but it's simple. Let's analyze it in parts.
First we use
line('$')
number of rows of the file.
Check the documentation for
line()
Then we set the variable
current
as the line number for the next line.
Then we start a loop that traverses each line in the file.
If a line matches the regular expression, which
\v\S
that there is a non-blank character, it is a non-empty line, so its line number is returned.
If one line doesn't match, we loop to the next.
If the loop reaches the end line of the file without any return, this means that after the current line, there is no non-empty line! L
et's
-2
to indicate this.
-2
not a valid line number, so it is used simply to say "Sorry, there are no valid results".
We can
-1
because it is also an invalid line number. I
can even
0
because the line number in Vim
1
So why did I
-2
a strange-looking option?
I chose
-2
we are dealing with folding
'-1'
'0'
is a special Vim foldlevel string.
When the eye is sweeping through the
-1
the brain will immediately appear "undefined foldlevel."
0
same for 0.
I'm
-2
to highlight that it's not _foldlevel, it's a "mistake."
If you find this unreasonable, you can safely
-2
with
-1
or
0
It's just a matter of code style.
This chapter is already a long one, so wrap up the folding function now.
Change
GetPotionFold
to this:
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
There's so much new code here! Let's look at it step by step.
First we check the empty lines. There are no changes here.
If it's not empty, we're ready to handle non-empty lines.
Next, we use two auxiliary functions to get the collapse level of the current row and the next non-empty line.
You may wonder what to
NextNonBlankLine
returns
-2
I
f this happens,
indent(-2)
will continue to work. E
xecuting
indent()
on a row number that does not exist
-1
You can
:echom indent(-2)
see.
-1
divided by any
shiftwidth
0
I
t may seem problematic, but it doesn't.
Now don't get tangled up in this for the time being.
Now that we've got the indentation levels for the current row and the next non-empty row, we can compare them and decide how to collapse the current row.
Here's another
if
statement:
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
First, let's check whether the two lines have the same indentation level. If equal, we return the indentation level asfoldlevel directly!
For example:
a
b
c
d
e
Suppose we're working on the line that contains
c
and it's indented to 1.
The indentation level for the next non-empty line ("d") is the same, so
1
asfoldlevel.
Suppose we are dealing with "a" with an indentation level of 0.
This is the same level as the next non-empty line ("b"), so
0
asfoldlevel.
In this simple example, you can separate twofoldlevels.
a 0
b ?
c 1
d ?
e ?
Purely out of luck, this situation also deals with the special "error" situation on the last line.
Remember we said that if our auxiliary function
-2
next_indent
would
0
In this example, the line "e" has an indentation
0
next_indent
also set
0
so match the situation and
0
Nowfoldlevels is like this:
a 0
b ?
c 1
d ?
e 0
Let's take a look at
if
statement:
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
if
part of if checks whether the indentation level of the next row is smaller than the
current row.
It's like the line "d" in the example.
If so, the indentation level of the current row is returned again.
Now our example looks like this:
a 0
b ?
c 1
d 1
e 0
Of course, you can
||
C
onnect the two situations, but I prefer to write separately to appear clearer. Y
our ideas may be different.
It's just a matter of style.
Again, purely out of luck, this situation deals with other "error" states from auxiliary functions. Imagine we have a file like this:
a
b
c
The first case deals with the line "b":
a ?
b 1
c ?
Line "c" is the last line with indentation level 1. B
ecause of our auxiliary
next_indent
will be set
0
This matches
if
part of the if statement, sofoldlevel is set to the current indentation level, which is
1
a ?
b 1
c 1
As we wish, "b" and "c" folded together.
There is still one last
if
statement left:
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
And our example is now:
a 0
b ?
c 1
d 1
e 0
Only the line "b" is left and we don't know itsfoldlevel yet, because:
0
1
The last case checks whether the indentation level of the next row is greater than the current row.
In this case Vim's
indent
is not ideal, which is why we planned to write custom folding code in the first place!
The final scenario indicates that when the next line is indented more than the current
>
a string that consists of an indentation level that begins with the first line and the next line.
What does that mean?
A string returned from a collapsed expression, similar to
>1
one in the specialfoldlevel of Vim.
It tells Vim that the current row needs to be expanded by a given level of folding.
In this simple example, we can simply return the numbers that represent the indentation level, but we'll soon see why we're doing this.
In this case "b" will expand the folding of level1, making our example look like this:
a 0
b >1
c 1
d 1
e 0
That's what we want! Hail!
If you do it here step by step, you should be proud of yourself. Even simple folding codes like this can be exhausting.
Before we're over, let's
factorial.pn
original code and see how our folding expressions handle each line offoldlevel.
factorial.pn
code here:
factorial = (n):
total = 1
n to 1 (i):
# Multiply the running total.
total *= i.
total.
10 times (i):
i string print
'! is: ' print
factorial (i) string print
"\n" print.
First, all empty rows offoldlevel will be set toundefined:
factorial = (n):
total = 1
n to 1 (i):
# Multiply the running total.
total *= i.
total.
undefined
10 times (i):
i string print
'! is: ' print
factorial (i) string print
"\n" print.
All folding levels are equal to the next row, and theirfoldlevel is equal to the folding level:
factorial = (n):
total = 1 1
n to 1 (i):
# Multiply the running total. 2
total *= i.
total.
undefined
10 times (i):
i string print 1
'! is: ' print 1
factorial (i) string print 1
"\n" print.
The same is true in the case of indentations on the next row that are less than the current row:
factorial = (n):
total = 1 1
n to 1 (i):
# Multiply the running total. 2
total *= i. 2
total. 1
undefined
10 times (i):
i string print 1
'! is: ' print 1
factorial (i) string print 1
"\n" print. 1
The final scenario is that the next row indents more than the current row. If so, set the folding level of the current row to expand the collapse of the next line:
factorial = (n): >1
total = 1 1
n to 1 (i): >2
# Multiply the running total. 2
total *= i. 2
total. 1
undefined
10 times (i): >1
i string print 1
'! is: ' print 1
factorial (i) string print 1
"\n" print. 1
Now we've got thefoldlevel for every line in the file. The rest is for Vim to resolve undefined rows.
Not long ago I said that the row ofundefined would choose the smallerfoldlevel in the adjacent row.
That's what the Vim manual says, but it's not certainly accurate. If so, the empty line in our file has afoldlevel of 1, because it has 1 for both rows offoldlevel adjacent to it.
In fact, the emptyfoldlevel will be set to 0!
That's why we
10 times(i):
thefoldlevel is 1. W
e tell Vim line to expand a level1 fold.
Vim realizes that this means that the row of underfined should be set
0
1
The reasons behind this may be buried deep in Vim's source code. Usually Vim is smart when dealing with underfined lines, so you can always do what you want.
Once Vim has processed the underfined row, it will get a complete description of the folding of each row, which looks like this:
factorial = (n): 1
total = 1 1
n to 1 (i): 2
# Multiply the running total. 2
total *= i. 2
total. 1
0
10 times (i): 1
i string print 1
'! is: ' print 1
factorial (i) string print 1
"\n" print. 1
That's it, we're done!
Reload the folding
factorial.pn
and play with our amazing folding features in the 2000s!
Read
:help foldexpr
.
Read
:help fold-expr
Note all the special strings that your expression can return.
Read
:help getline
Read
:help indent()
Read
:help line()
Think about why
.
use C
onnect
>
the and our folding functions.
What if
+
a plus?
We define auxiliary functions in the global space, but this is not a good practice. Change it to the local namespace of the script.
Drop the book, go out and play, and keep your brain awake from this chapter.