Monday, July 6, 2009

Vim pr0n: Combating long lines

The other day my boss found me locked in the server room, naked and bathing myself with dish washing liquid and toilet water. When he asked what in the flying fuck I was playing at, I told him my soul was filthy and corrupt from all the long lines I'd been committing. In reply he told me I'd better sack up and write some vimscript hacks to keep my long lines under control. So that's what I did. Later that afternoon I was so happy I proposed to my boss and we lived happily ever after.

Read this raving if long lines bug you like a colony of fire ants assaulting your prostate.

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.

Reporting long lines on the statusline

I've found it quite useful to have flags on my statusline that appear when a buffer is in an undesirable state; e.g. if the file format or encoding is wrong, or there are syntax errors, or mixed indenting etc. Ive recently coded a warning for long lines into my statusline. Check out this screenshot:



Notice the [#141,m82,$162] on the statusline. This is telling us that the buffer has 141 long lines, where the median length of these lines is 82 chars and the longest is 162 chars. This notice only appears when there is at least one line that is longer than &textwidth (typically 80).

That may seem like an excessive amount of information, but when I first coded a statusline warning, I only included the length of the longest line. This turned out to be insufficient since its common for a file to have one or two crazy long lines that are acceptable or troublesome to avoid. I could have only shown the number of long lines, but then I'd be left wondering how long they were; hence the median and longest line stats. Besides, more stats means more code behind my statusline, which makes it more likely the mother fucker will become sentient and hunt down Jamis Buck.

Here's the code from my vimrc:

 1 "....
 2 set statusline+=%{StatuslineLongLineWarning()}
 3 "....
 4
 5 "recalculate the long line warning when idle and after saving
 6 autocmd cursorhold,bufwritepost * unlet! b:statusline_long_line_warning
 7
 8 "return a warning for "long lines" where "long" is either &textwidth or 80 (if
 9 "no &textwidth is set)
10 "
11 "return '' if no long lines
12 "return '[#x,my,$z] if long lines are found, were x is the number of long
13 "lines, y is the median length of the long lines and z is the length of the
14 "longest line
15 function! StatuslineLongLineWarning()
16     if !exists("b:statusline_long_line_warning")
17         let long_line_lens = s:LongLines()
18
19         if len(long_line_lens) > 0
20             let b:statusline_long_line_warning = "[" .
21                         \ '#' . len(long_line_lens) . "," .
22                         \ 'm' . s:Median(long_line_lens) . "," .
23                         \ '$' . max(long_line_lens) . "]"
24         else
25             let b:statusline_long_line_warning = ""
26         endif
27     endif
28     return b:statusline_long_line_warning
29 endfunction
30
31 "return a list containing the lengths of the long lines in this buffer
32 function! s:LongLines()
33     let threshold = (&tw ? &tw : 80)
34     let spaces = repeat(" ", &ts)
35
36     let long_line_lens = []
37
38     let i = 1
39     while i <= line("$")
40         let len = strlen(substitute(getline(i), '\t', spaces, 'g'))
41         if len > threshold
42             call add(long_line_lens, len)
43         endif
44         let i += 1
45     endwhile
46
47     return long_line_lens
48 endfunction
49
50 "find the median of the given array of numbers
51 function! s:Median(nums)
52     let nums = sort(a:nums)
53     let l = len(nums)
54
55     if l % 2 == 1
56         let i = (l-1) / 2
57         return nums[i]
58     else
59         return (nums[l/2] + nums[(l/2)-1]) / 2
60     endif
61 endfunction


Crikey dick that's a lot of code! Lets run through it.

The StatuslineLongLineWarning() function constructs and caches the flag that will appear on the statusline. The flag will be an empty string if there aren't any long lines.

The hard work is actually delegated off to the LongLines() and Median() functions. LongLines() examines every line in the buffer and returns an array containing the lengths of the long lines, where "long" is determined by the users &textwidth setting (defaulting to 80 if &textwidth is set to 0). Note that LongLines() converts tabs into spaces according to the users &tabstop setting. Median() calculates the median of a given array of numbers.

Line 2 puts the flag onto the statusline and should appear with the rest of your statusline setup code.

Line 6 clears the cached flag every time the user is idle or saves the file, thus causing the flag to be recalculated.

Highlighting the offending parts of long lines

 1 "define :HighlightLongLines command to highlight the offending parts of
 2 "lines that are longer than the specified length (defaulting to 80)
 3 command! -nargs=? HighlightLongLines call s:HighlightLongLines('<args>')
 4 function! s:HighlightLongLines(width)
 5     let targetWidth = a:width != '' ? a:width : 79
 6     if targetWidth > 0
 7         exec 'match Todo /\%>' . (targetWidth) . 'v/'
 8     else
 9         echomsg "Usage: HighlightLongLines [natural number]"
10     endif
11 endfunction


I stole line 7 above from this vim tip and wrapped it up in a the HighlightLongLines command above. When invoked, the command highlights the "long" parts of long lines. Ive found this command pretty handy when embarking on anti long line crusades.

I'd highly recommend reading that vim tip for more code to highlight long lines. Theres some pretty dynamite stuff there.

Conclusion

Long lines suck, here's what Abe had to say about the matter:

Peppering your code with long lines is highly recommended; right up there with "strolling into church with an entourage of scantly clad, chronically obese prostitutes" and "licking the testicles of senile old men who are not fully in control of their bladders".

Abraham Lincoln, 1862


Inspirational.

Use the relatively passive vim script hacks above to make vim bitch at you and steer you away from using long lines. If you want a more aggressive solution, then check out the aforementioned vim tip.