--- a/chapters/48.markdown Thu Dec 15 18:31:41 2011 -0500
+++ b/chapters/48.markdown Fri Dec 16 21:39:36 2011 -0500
@@ -74,30 +74,60 @@
The bodies of the function and loop are both indented. This means we can get
some basic folding with very little effort by using indent folding.
+Before we start, go ahead and add a comment above the `total *= i.` line so we
+have a nice multiple-line inner block to test with. You'll learn why we need to
+do this when you do the exercises, but for now just trust me. The file should
+now look like this:
+
+ :::text
+ 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.
+
Create an `ftplugin` folder in your Potion plugin's repository, and create
-a `potion.vim` file inside of it.
+a `potion` folder inside that. Finally, create a `folding.vim` file inside of
+*that*.
Remember that Vim will run the code in this file whenever it sets a buffer's
-`filetype` to `potion` (because it's named `potion.vim`).
+`filetype` to `potion` (because it's in a folder named `potion`).
+
+Putting all folding-related code into its own file is generally a good idea and
+will help us keep the various functionality of our plugin organized.
Add the following line to this file:
:::vim
setlocal foldmethod=indent
-Close Vim and open the `factorial.pn` file again. It may or may not be folded
-immediately, but go ahead and try folding and unfolding the indented blocks.
+Close Vim and open the `factorial.pn` file again. Play around with the new
+folding with `zR`, `zM`, and `za`.
One line of Vimscript gave us some useful folding! That's pretty cool!
-Let's add one more line to the `ftplugin/potion.vim` file to make Vim
-automatically fold everything whenever we open a Potion file:
+You might notice that the lines inside the inner loop of the `factorial`
+function aren't folded even though they're indented. What's going on?
+
+It turns out that by default Vim will ignore lines beginning with a `#`
+character when using `indent` folding. This works great when editing C files
+(where `#` signals a preprocessor directive) but isn't very helpful when you're
+editing other types of files.
+
+Let's add one more line to the `ftplugin/potion/folding.vim` file to fix this:
:::vim
- setlocal foldlevel=0
+ setlocal foldmethod=indent
+ setlocal foldignore=
-Close and reopen your example file and now the folds will all be closed (if they
-weren't already).
+Close and reopen `factorial.pn` and now the inner block will be folded properly.
Exercises
---------
@@ -114,13 +144,4 @@
Read `:help foldminlines`.
-Add a line to `ftplugin/potion.vim` that uses `foldminlines` to allow
-single-line blocks to be "folded", for consistency. Make sure it works on the
-`total *= i.` line of the example file.
-
-Some people might not want to automatically fold everything when they open
-a Potion file. We want our plugin to be useful to everyone, so remove the
-`foldlevel` line from `ftplugin/potion.vim`.
-
-Add a `Filetype` autocommand to your `~/.vimrc` file that sets `foldlevel` to
-0 to replace the line we removed from the plugin.
+Read `:help foldignore`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chapters/49.markdown Fri Dec 16 21:39:36 2011 -0500
@@ -0,0 +1,758 @@
+Advanced Folding
+================
+
+In the last chapter we used Vim's `indent` folding to add some quick and dirty
+folding to Potion files.
+
+Open `factorial.pn` and make sure all the folds are closed with `zM`. The file
+should now look something like this:
+
+ :::text
+ factorial = (n):
+ +-- 5 lines: total = 1
+
+ 10 times (i):
+ +-- 4 lines: i string print
+
+Toggle the first fold and it will look like this:
+
+ :::text
+ factorial = (n):
+ total = 1
+ n to 1 (i):
+ +--- 2 lines: # Multiply the running total.
+ total.
+
+ 10 times (i):
+ +-- 4 lines: i string print
+
+This is pretty nice, but I personally prefer to fold the first line of a block
+with its contents. In this chapter we'll write some custom folding code, and
+when we're done our folds will look like this:
+
+ :::text
+ factorial = (n):
+ total = 1
+ +--- 3 lines: n to 1 (i):
+ total.
+
+ +-- 5 lines: 10 times (i):
+
+This is more compact and (to me) easier to read. If you prefer the `indent`
+method that's okay, but do this chapter anyway just to get some practice writing
+Vim folding expressions.
+
+Folding Theory
+--------------
+
+When writing custom folding code it helps to have an idea of how Vim "thinks" of
+folding. Here are the rules in a nutshell:
+
+* Each line of code in a file has a "foldlevel". This is always either zero or
+ a positive integer.
+* Lines with a foldlevel of zero are *never* included in any fold.
+* Adjacent lines with the same foldlevel are folded together.
+* If a fold of level X is closed, any subsequent lines with a foldlevel greater
+ than or equal to X are folded along with it until you reach a line with
+ a level less than X.
+
+It's easiest to get a feel for this with an example. Open a Vim window and
+paste the following text into it.
+
+ :::text
+ a
+ b
+ c
+ d
+ e
+ f
+ g
+
+Turn on `indent` folding by running the following command:
+
+ :::vim
+ :setlocal foldmethod=indent
+
+Play around with the folds for a minute to see how they behave.
+
+Now run the following command to view the foldlevel of line 1:
+
+ :::vim
+ :echom foldmethod(1)
+
+Vim will display `0`. Now let's find the foldlevel of line 2:
+
+ :::vim
+ :echom foldmethod(2)
+
+Vim will display `1`. Let's try line 3:
+
+ :::vim
+ :echom foldmethod(3)
+
+Once again Vim displays `1`. This means that lines 2 and 3 are part of a level
+1 fold.
+
+Here are the foldlevels for each line:
+
+ :::text
+ a 0
+ b 1
+ c 1
+ d 2
+ e 2
+ f 1
+ g 0
+
+Reread the rules at the beginning of this section. Open and close each fold in
+this file, look at the foldlevels, and make sure you understand why the folds
+behave as they do.
+
+Once you're confident that you understand how every line's foldlevel works to
+create the folding structure, move on to the next section.
+
+
+First: Make a Plan
+------------------
+
+Before we dive into writing code, let's try to sketch out some rough "rules" for
+our folding.
+
+First, lines that are indented should be folded together. We also want the
+*previous* line folded with them, so that something like this:
+
+ :::text
+ hello = (name):
+ 'Hello, ' print
+ name print.
+
+Will fold like this:
+
+ :::text
+ +-- 3 lines: hello = (name):
+
+Blank lines should be at the same level as *later* lines, so blank lines at the
+end of a fold won't be included in it. This means that this:
+
+ :::text
+ hello = (name):
+ 'Hello, ' print
+ name print.
+
+ hello('Steve')
+
+Will fold like this:
+
+ :::text
+ +-- 3 lines: hello = ():
+
+ hello('Steve')
+
+And *not* like this:
+
+ :::text
+ +-- 4 lines: hello = ():
+ hello('Steve')
+
+These rules are a matter of personal preference, but for now this is the way
+we're going to implement folding.
+
+Getting Started
+---------------
+
+Let's get started on our custom folding code by opening Vim with two splits.
+One should contain our `ftplugin/potion/folding.vim` file, and the other should
+contain our sample `factorial.pn`.
+
+In the previous chapter we closed and reopened Vim to make our changes to
+`folding.vim` take effect, but it turns out there's an easier way to do that.
+
+Remember that any files inside `ftplugin/potion/` will be run whenever the
+`filetype` of a buffer is set to `potion`. This means you can simply run `:set
+ft=potion` in the split containing `factorial.pn` and Vim will reload the
+folding code!
+
+This is much faster than closing and reopening the file every time. The only
+thing you need to remember is that you have to *save* `folding.vim` to disk,
+otherwise your unsaved changes won't be taken into account.
+
+Expr Folding
+------------
+
+We're going to use Vim's `expr` folding to give us unlimited flexibility in how
+our code is folded.
+
+We can go ahead and remove the `foldignore` from `folding.vim` because it's only
+relevant when using `indent` folding. We also want to tell Vim to use `expr`
+folding, so change the contents of `folding.vim` to look like this:
+
+ :::vim
+ setlocal foldmethod=expr
+ setlocal foldexpr=GetPotionFold(v:lnum)
+
+ function! GetPotionFold(lnum)
+ return '0'
+ endfunction
+
+The first line simply tells Vim to use `expr` folding.
+
+The second line defines the expression Vim should use to get the foldlevel of
+a line. When Vim runs the expression it will set `v:lnum` to the line number of
+the line it wants to know about. Our expression will call a custom function
+with this number as an argument.
+
+Finally we define a dummy function that simply returns `'0'` for every line.
+Note that it's returning a String and not an Integer. We'll see why shortly.
+
+Go ahead and reload the folding code by saving `folding.vim` and running `:set
+ft=potion` in `factorial.pn`. Our function returns `'0'` for every line, so
+Vim won't fold anything at all.
+
+Blank Lines
+-----------
+
+Let's take care of the special case of blank lines first. Modify the
+`GetPotionFold` function to look like this:
+
+ :::vim
+ function! GetPotionFold(lnum)
+ if getline(a:lnum) =~? '\v^\s*$'
+ return '-1'
+ endif
+
+ return '0'
+ endfunction
+
+We've added an `if` statement to take care of the blank lines. How does it
+work?
+
+First we use `getline(a:lnum)` to get the content of the current line as
+a String.
+
+We compare this to the regex `\v^\s*$`. Remember that `\v` turns on "very
+magic" ("sane") mode. This regex will match "beginning of line, any number
+of whitespace characters, end of line".
+
+The comparison is using the case-insensitive match operator `=~?`. Technically
+we don't have to be worried about case since we're only matching whitespace, but
+I prefer to be more explicit when using comparison operators on Strings. You
+can use `=~` instead if you prefer.
+
+If you need a refresher on using regular expressions in Vim you should go back
+and reread the "Basic Regular Expressions" chapter and the chapters on the "Grep
+Operator".
+
+If the current line has some non-whitespace characters it won't match and we'll
+just return `'0'` as before.
+
+If the current line *does* match the regex (i.e. is it's empty or just
+whitespace) we return the string `'-1'`.
+
+Earlier I said that a line's foldlevel can be zero or a positive integer, so
+what's happening here?
+
+Special Foldlevels
+------------------
+
+Your custom folding expression can return a foldlevel directly, or return one of
+a few "special" strings that tell Vim how to fold the line without directly
+specifying its level.
+
+`'-1'` is one of these special strings. It tells Vim that the level of this
+line is "undefined". Vim will interpret this as "the foldlevel of this line is
+equal to the foldlevel of the line above or below it, whichever is smaller".
+
+This isn't *exactly* what our plan called for, but we'll see that it's close
+enough and will do what we want.
+
+Vim can "chain" these undefined lines together, so if you have two in a row
+followed by a line at level 1, it will set the last undefined line to 1, then
+the next to last to 1, then the first to 1.
+
+When writing custom folding code you'll often find a few types of line that you
+can easily set a specific level for. Then you'll use `'-1'` (and some other
+special foldlevels we'll see soon) to "cascade" the proper folding levels to the
+rest of the file.
+
+If you reload the folding code for `factorial.pn` Vim *still* won't fold any
+lines together. This is because all the lines have a foldlevel of either zero
+or "undefined". The level `0` will "cascade" through the undefined lines and
+eventually all the lines will have their foldlevel set to `0`.
+
+An Indentation Level Helper
+---------------------------
+
+To tackle non-blank lines we'll need to know their indentation level, so let's
+create a small helper function to calculate it for us. Add the following
+function above `GetPotionFold`:
+
+ :::vim
+ function! IndentLevel(lnum)
+ return indent(a:lnum) / &shiftwidth
+ endfunction
+
+Reload the folding code. Test out your function by running the following
+command in the `factorial.pn` buffer:
+
+ :::vim
+ :echom IndentLevel(1)
+
+Vim displays `0` because line 1 is not indented. Now try it on line 2:
+
+ :::vim
+ :echom IndentLevel(2)
+
+This time Vim displays `1`. Line two has 4 spaces at the beginning, and
+`shiftwidth` is set to 4, so 4 divided by 4 is 1.
+
+`IndentLevel` is fairly straightforward. The `indent(a:lnum)` returns the
+number of spaces at the beginning of the given line number. We divide that by
+the `shiftwidth` of the buffer to get the indentation level.
+
+Why did we use `&shiftwidth` instead of just dividing by 4? If someone prefers
+two-space indentation in their Potion files, dividing by 4 would produce an
+incorrect result. We use the `shiftwidth` setting to allow for any number of
+spaces per level.
+
+One More Helper
+---------------
+
+It might not be obvious where to go from here. Let's stop and think about what
+type of information we need to have to figure out how to fold a non-blank line.
+
+We need to know the indentation level of the line itself. We've got that
+covered with the `IndentLevel` function, so we're all set there.
+
+We'll also need to know the indentation level of the *next non-blank line*,
+because we want to fold the "header" lines with their indented bodies.
+
+Let's write a helper function to get the number of the next non-blank line after
+a given line. Add the following function above `IndentLevel`:
+
+ :::vim
+ function! s: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 longer, but is pretty simple. Let's take it
+piece-by-piece.
+
+First we store the total number of lines in the file with `line('$')`. Check
+out the documentation for `line()` to see how this works.
+
+Next we set the variable `current` to the number of the next line.
+
+We then start a loop that will walk through each line in the file.
+
+If the line matches the regex `\v\S`, which means "match a character that's
+*not* a whitespace character", then it must be non-blank, so we should return
+its line number.
+
+If the line doesn't match, we loop around to the next one.
+
+If the loop gets all the way to the end of the file without ever returning, then
+there are *no* non-blank lines after the current line! We return `-2` if that
+happens to indicate this. `-2` isn't a valid line number, so it's an easy way
+to say "sorry, there's no valid result".
+
+We could have returned `-1`, because that's not a valid line number either.
+I could have even picked `0`, since line numbers in Vim start at `1`! So why
+did I pick `-2`, which seems like a strange choice?
+
+I chose `-2` because we're working with folding code, and `'-1'` (and `'0'`) is
+a special Vim foldlevel string.
+
+When my eyes are reading over this file and I see a `-1` my brain immediately
+thinks "undefined foldlevel". The same is true with `0`. I picked `-2` here
+simply to make it obvious that it's *not* a foldlevel, but is instead an
+"error".
+
+If this feels weird to you, you can safely change the `-2` to a `-1` or a `0`.
+It's just a coding style preference.
+
+Finishing the Fold Function
+---------------------------
+
+This is turning out to be quite a long chapter, so let's wrap up the folding
+function. Change `GetPotionFold` to look like this:
+
+ :::vim
+ 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
+
+That's a lot of new code! Let's step through it to see how it all works.
+
+### Blanks
+
+First we have our check for blank lines. Nothing's changed there.
+
+If we get past that check we know we're looking at a non-blank line.
+
+### Finding Indentation Levels
+
+Next we use our two helper functions to get the indent level of the current
+line, and the indent level of the next non-blank line.
+
+You might wonder what happens if `NextNonBlankLine` returns our error condition
+of `-2`. If that happens, `indent(-2)` will be run. Running `indent()` on
+a nonexistent line number will just return `-1`. Go ahead and try it yourself
+with `:echom indent(-2)`.
+
+`-1` divided by any `shiftwidth` larger than `1` will return `0`. This may seem
+like a problem, but it turns out that it won't be. For now, don't worry about
+it.
+
+### Equal Indents
+
+Now that we have the indentation levels of the current line and the next
+non-blank line, we can compare them and decide how to fold the current line.
+
+Here's the `if` statement again:
+
+ :::vim
+ 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 we check if the two lines have the same indentation level. If they do, we
+simply return that indentation level as the foldlevel!
+
+An example of this would be:
+
+ :::text
+ a
+ b
+ c
+ d
+ e
+
+If we're looking at the line containing "c", it has an indentation level of 1.
+This is the same as the level of the next non-blank line ("d"), so we return `1`
+as the foldlevel.
+
+If we're looking at "a", it has an indentation level of 0. This is the same as
+the level of the next non-blank line ("b"), so we return `0` as the foldlevel.
+
+This case fills in two foldlevels in this simple example:
+
+ :::text
+ a 0
+ b ?
+ c 1
+ d ?
+ e ?
+
+By pure luck this also handles the special "error" case of the last line as
+well! Remember we said that `next_indent` will be `0` if our helper function
+returns `-2`.
+
+In this example the line "e" has an indent level of `0`, and `next_indent` will
+also be set to `0`, so this case matches and returns `0`. The foldlevels now
+look like this:
+
+ :::text
+ a 0
+ b ?
+ c 1
+ d ?
+ e 0
+
+### Lesser Indent Levels
+
+Once again, here's the `if` statement:
+
+ :::vim
+ 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
+
+The second part of the `if` checks if the indentation level of the next line is
+*smaller* than the current line. This would be like line "d" in our example.
+
+If that's the case, we once again return the indentation level of the current
+line.
+
+Now our example looks like this:
+
+ :::text
+ a 0
+ b ?
+ c 1
+ d 1
+ e 0
+
+You could, of course, combine these two cases with `&&`, but I prefer to keep
+them separate to make it more explicit. You might feel differently. It's
+a style issue.
+
+Again, purely by luck, this case handles the other possible "error" case of our
+helper function. Imagine that we have a file like this:
+
+ :::text
+ a
+ b
+ c
+
+The first case takes care of line "b":
+
+ :::text
+ a ?
+ b 1
+ c ?
+
+Line "c" is the last line, and it has an indentation level of 1. The
+`next_indent` will be set to `0` thanks to our helper functions. The second
+part of the `if` matches and sets the foldlevel to the current indentation
+level, or `1`:
+
+ :::text
+ a ?
+ b 1
+ c 1
+
+This works out great, because "b" and "c" will be folded together.
+
+### Greater Indentation Levels
+
+Here's that tricky `if` statement for the last time:
+
+ :::vim
+ 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 file:
+
+ :::text
+ a 0
+ b ?
+ c 1
+ d 1
+ e 0
+
+The only line we haven't figured out is "b", because:
+
+* "b" has an indent level of `0`.
+* "c" has an indent level of `1`.
+* 1 is not equal to 0, nor is 1 less than 0.
+
+The last case checks if the next line has a *larger* indentation level than the
+current one.
+
+This is the case that Vim's `indent` folding gets wrong, and it's the entire
+reason we're writing this custom folding in the first place!
+
+The final case says that when the next line is indented more than the current
+one, it should return a string of a `>` character and the indentation level of
+the *next* line. What the heck is *that*?
+
+Returning a string like `>1` from the fold expression is another one of Vim's
+"special" foldlevels. It tells Vim that the current line should *open* a fold
+of the given level.
+
+In this simple example we could have simply returned the number, but we'll see
+why this is important shortly.
+
+In this case line "b" will open a fold at level 1, which makes our example look
+like this:
+
+ :::text
+ a 0
+ b >1
+ c 1
+ d 1
+ e 0
+
+That's exactly what we want! Hooray!
+
+Review
+------
+
+If you've made it this far you should feel proud of yourself. Even simple
+folding code like this can be tricky and mind bending.
+
+Before we end, let's go through our original `factorial.pn` code and see how our
+folding expression fills in the foldlevels of its lines.
+
+Here's `factorial.pn` for reference:
+
+ :::text
+ 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, set any blank lines' foldlevels will be set to undefined:
+
+ :::text
+ 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.
+
+Any lines where the next line's indentation is *equal* to its own are set to its
+own level:
+
+ :::text
+ 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. 1
+
+The same thing happens when the next line's indentation is *less* than the
+current line's:
+
+ :::text
+ 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 last case is when the next line's indentation is *greater* than the current
+line's. When that happens the line's foldlevel is set to *open* a fold of the
+*next* line's foldlevel:
+
+ :::text
+ 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 a foldlevel for every line in the file. All that's left is for
+Vim to resolve any undefined lines.
+
+Earlier I said that undefined lines will take on the smallest foldlevel of
+either of their neighbors.
+
+That's how Vim's manual describes it, but it's not entirely accurate. If that
+were the case, the blank line in our file would take foldlevel 1, because both
+of its neighbors have a foldlevel of 1.
+
+In reality, the blank line will be given a foldlevel of 0!
+
+The reason for this is that we didn't just set the `10 times (i):` line to
+foldlevel `1` directly. We told Vim that the line *opens* a fold of level `1`.
+Vim is smart enough to know that this means the undefined line should be set to
+`0` instead of `1`.
+
+The exact logic of this is probably buried deep within Vim's source code. In
+general Vim behaves pretty intelligently when resolving undefined lines against
+"special" foldlevels, so it will usually do what you want.
+
+Once Vim's resolved the undefined line it has a complete description of how to
+fold each line in the file, which looks like this:
+
+ :::text
+ 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 code and play around with the fancy
+new folding in `factorial.pn`.
+
+Exercises
+---------
+
+Read `:help foldexpr`.
+
+Read `:help fold-expr`. Pay particular attention to all the "special" strings
+your expression can return.
+
+Read `:help getline`.
+
+Read `:help indent()`.
+
+Read `:help line()`.
+
+Figure out why it's important that we use `.` to combine the `>` character with
+the number in our folding function. What would happen if you used `+` instead?
+Why?
+
+Put this book down and go outside for a while to let your brain recover from
+this chapter.