Friday, October 31, 2008

Vim pr0n: Statusline flags for bundy whitespace

Lately I've been working on some projects with indenting so screwed you could print the source code out and wank with it and not be able to tell the difference. It's as though the author couldn't decide whether to use tabs or spaces, so they flipped a coin every time they began a new source file, and evidently the coin landed on its fucking edge more often than not since there are a fair number of files that use both tabs and spaces.

I've also developed an extreme anti-fetish for trailing whitespace recently. I made vim highlight it, and that shit is fucking everywhere!

ZOMG

So, in a bid to eradicate both mixed indenting and trailing whitespace from the universe, I've made vim display some flags on my statusline whenever they are present, and whenever my &expandtab setting is wrong. Read this raving to find out how.

Note: this raving assumes a fair amount of knowledge about statuslines in vim. If you aren't pro at statuslines then you should read this raving first.

Just so you know what I mean...


A buffer containing mixed indenting, and trailing whitespace (click to enlarge).


A buffer with &et set wrong, and trailing whitespace (click to enlarge).


The three statusline flags of interest in the above screenshots are:

  • [mixed-indenting]: Appears when a buffer is indented with both tabs and spaces

  • [&et]: Appears when a buffer is indented consistently (i.e. using only spaces or only tabs) but the &expandtab setting for the buffer is wrong.

  • [\s]: Appears when a buffer contains trailing whitespace


The code for the [mixed-indenting] and [&et] flags


This is snipped straight outta my vimrc:
 1 "display a warning if &et is wrong, or we have mixed-indenting
 2 set statusline+=%#error#
 3 set statusline+=%{StatuslineTabWarning()}
 4 set statusline+=%*
 5
 6 "recalculate the tab warning flag when idle and after writing
 7 autocmd cursorhold,bufwritepost * unlet! b:statusline_tab_warning
 8
 9 "return '[&et]' if &et is set wrong
10 "return '[mixed-indenting]' if spaces and tabs are used to indent
11 "return an empty string if everything is fine
12 function! StatuslineTabWarning()
13     if !exists("b:statusline_tab_warning")
14         let tabs = search('^\t', 'nw') != 0
15         let spaces = search('^ ', 'nw') != 0
16
17         if tabs && spaces
18             let b:statusline_tab_warning =  '[mixed-indenting]'
19         elseif (spaces && !&et) || (tabs && &et)
20             let b:statusline_tab_warning = '[&et]'
21         else
22             let b:statusline_tab_warning = ''
23         endif
24     endif
25     return b:statusline_tab_warning
26 endfunction

There are three parts to this code. The function (lines 12–26) works out what should be displayed on the statusline and caches the result. An autocommand on line 7 clears this cache when the user is idle or saves the file. Lines 2–4 put the flag on the statusline.

Note that the caching is needed as the function is called every time the statusline is refreshed. Without caching this would be very costly since the function searches the entire buffer for a leading space and a leading tab.

The code for the [\s] flag


Also straight from my vimrc, this code functions very similarly to the previous code. Lines 8–17 calculate and cache what should be displayed on the statusline. Line 4 clears this cache when the user is idle, or saves the file. Line 1 puts the flag on the statusline.
 1 set statusline+=%{StatuslineTrailingSpaceWarning()}
 2
 3 "recalculate the trailing whitespace warning when idle, and after saving
 4 autocmd cursorhold,bufwritepost * unlet! b:statusline_trailing_space_warning
 5
 6 "return '[\s]' if trailing white space is detected
 7 "return '' otherwise
 8 function! StatuslineTrailingSpaceWarning()
 9     if !exists("b:statusline_trailing_space_warning")
10         if search('\s\+$', 'nw') != 0
11             let b:statusline_trailing_space_warning = '[\s]'
12         else
13             let b:statusline_trailing_space_warning = ''
14         endif
15     endif
16     return b:statusline_trailing_space_warning
17 endfunction

An aside: use the list and listchars settings!


I have these lines in my vimrc:

set list
set listchars=tab:▷⋅,trail:⋅,nbsp:⋅

This tells vim to display tab characters as little arrow things and trailing spaces as floating dots (as in the two screenshots at the top of this raving). Having tabs displayed is invaluable as it becomes obvious how a file is indented, and if you aren't following suit.

Join in the fight against bundy whitespace today!


I was eavesdropping on two catholic priests yesterday* when I overheard one of them talking about people who mix tabs and spaces, saying: "... I know for a fact that they will spend their eternities in hell licking flaming shit nuggets out of satans ass crack while he jacks off furiously, periodically ejaculating streams of molten lava in their faces!"

On the other hand, studies show that people who indent code responsibly are far more likely to have sex with famous actresses than those who don't.

So what are you waiting for? Take every precaution to ensure you indent correctly! And fuck trailing whitespace!


* may not be true.

Monday, October 27, 2008

Vim pr0n: conditional statusline highlighting

I have a fair amount of info on my statusline, and have often thought it would be nice if certain parts of it could be highlighted under certain conditions. For example, I have &fileencoding displayed, but I only really care about it if its not "utf-8". In that case I would like it to be highlighted using the "error" highlight group so that I'll be sure to notice it.

If you too have jacked off far into the night imagining how much fun you could have with conditionally highlighted statusline flags, then take off your pants, point the pork sword in a safe direction and read this raving immediately.

Note: this raving assumes a fair amount of knowledge about statuslines in vim. If you aren't pro at statuslines then you should read this raving first.

The desired result



In the bottom image, the statusline was set up to display &fileformat and &fileencoding using the "error" highlight if they were anything other than "unix" and "utf-8" respectively.

ZOMG how do you do it?!

Unfortunately there is no clean way to do it, but we can easily encapsulate the hacks in a couple of functions.

Here is the code that generated the statusline above:
 1 "Add the variable with the name a:varName to the statusline. Highlight it as
 2 "'error' unless its value is in a:goodValues (a comma separated string)
 3 function! AddStatuslineFlag(varName, goodValues)
 4   set statusline+=[
 5   set statusline+=%#error#
 6   exec "set statusline+=%{RenderStlFlag(".a:varName.",'".a:goodValues."',1)}"
 7   set statusline+=%*
 8   exec "set statusline+=%{RenderStlFlag(".a:varName.",'".a:goodValues."',0)}"
 9   set statusline+=]
10 endfunction
11
12 "returns a:value or ''
13 "
14 "a:goodValues is a comma separated string of values that shouldn't be
15 "highlighted with the error group
16 "
17 "a:error indicates whether the string that is returned will be highlighted as
18 "'error'
19 "
20 function! RenderStlFlag(value, goodValues, error)
21   let goodValues = split(a:goodValues, ',')
22   let good = index(goodValues, a:value) != -1
23   if (a:error && !good) || (!a:error && good)
24     return a:value
25   else
26     return ''
27   endif
28 endfunction
29
30 "statusline setup
31 set statusline=%t       "tail of the filename
32 call AddStatuslineFlag('&ff', 'unix')    "fileformat
33 call AddStatuslineFlag('&fenc', 'utf-8') "file encoding
34 set statusline+=%h      "help file flag
35 set statusline+=%m      "modified flag
36 set statusline+=%r      "read only flag
37 set statusline+=%y      "filetype
38 set statusline+=%{StatuslineCurrentHighlight()}
39 set statusline+=%=      "left/right separator
40 set statusline+=%c,     "cursor column
41 set statusline+=%l/%L   "cursor line/total lines
42 set statusline+=\ %P    "percent through file

This code does two things. First it defines two functions that implement conditionally highlighted statusline flags. Secondly, it sets up the statusline variable, making use of AddStatusLineFlag().

How does it work?

A function call like this:
call AddStatuslineFlag('&fenc', 'utf-8,latin1')

will cause the following to be added to the &statusline variable:
[%#error#%{RenderStlFlag(&fenc,'utf-8,latin1',1)}%*%{RenderStlFlag(&fenc,'utf-8,latin1',0)}]


If we break this down we have the following:

[
output a literal "[" char


%#error#
switch to the "error" highlight group


%{RenderStlFlag(&fenc,'utf-8,latin1',1)}
render &fenc only if it's not "utf-8" or "latin1".


%*
switch back to the normal statusline highlight group


%{RenderStlFlag(&fenc,'utf-8,latin1',0)}
render &fenc only if it is "utf-8" or "latin1".


]
output a literal "]" char.


The key thing is that &fenc will either be rendered inside the error highlight or (exclusive) just outside it.

Generalizing...

This exact code wont work for all cases, but we can derive some general guidelines.

If you want to highlight something (lets call it foobar) X different ways then:

  1. You must define a function to render foobar.

  2. You must call the function X times on your statusline, where each call must be highlighted one of the possible ways.

  3. The function must take an argument to specify which highlight group it is currently rendering for.

  4. At all times, all but one of the function calls must return an empty string, while the other call must return the value of foobar which will appear on the statusline.


Already solved in Vim 8

The problem of conditionally highlighting statusline flags has already been solved for Vim 8, as those of you who keep up with the various vim mailing lists will no doubt be aware of. If you havent heard, then check out this this sneak peek of the new feature.

Sunday, October 5, 2008

git add --patch gives me an erection

One of the reasons I tossed out my playboys and husslers in favour of the git man pages is git add --patch (or git add -p)

To say something like
git add -p is the fucking bomb, I wish it was a pony so I could ride it around the backyard and pet it and feed it straw all day long
would be the understatement of the new millenium.

If you use git and aren't familiar with git add -p, then take off your pants and read this raving immediately.

OMG wtf is it?!!?


git add --patch lets you cherry pick the changes you want to add to the index. This means you can make 10 changes to a file, then only add 5 of them to the next commit.

I dunno about you, but when I'm hacking the mainframe I can never seem to stick to one task. I always notice some bug, or some fucked up white space, or have to hack something else to get the current task done etc. Its nigh fucking impossible for me to keep the changes I make suitable for one atomic commit.

So I'm a bad boy. I don't care. Smack my ass and tell my parents. Just make sure you have breasts and a vagina if you do, or I'll bust your fucking chops. Hell, you can even skip the vagina if your breasts are exceptional, but don't try and tell me your working directory doesn't get tangled as fuck too.

How do you use it?


It's easy. Just go git add -p file1 file2 ... or just git add -p to work with all tracked files. This launches a program that will loop through all the changed hunks one by one asking you whether you want to stage them or not. It looks a lil sumthang like this:



For every hunk, it prints out a diff and asks you what to do with it. You can hit ? to see the choices you have:


To quit just hit ctrl-c but be aware that the hunks you added will still be in the index.

Other useful tidbits


git add -p is actually a sub routine of the git add --interactive program. Sometimes it's faster to use git add --interactive (aka git add -i) to select which files you want git add -p to work with rather than entering them in on the command line.

Remember to do a git diff --cached to before you do the final git commit to make sure you didn't add anything extra by mistake.

If you do add something by mistake then use git reset HEAD path/to/file to remove the changes from the index.



Have a nice day.