# HG changeset patch # User Steve Losh # Date 1324089576 18000 # Node ID 121fb4ea289b813b0f6786d20bcb7bd28c49acf4 # Parent b0e538f6533dbd53efd3063a14a1b5fc00ea69a6 Jesus Christ. diff -r b0e538f6533d -r 121fb4ea289b chapters/48.markdown --- 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`. diff -r b0e538f6533d -r 121fb4ea289b chapters/49.markdown --- /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.