Sunday, June 21, 2009

Vim pr0n: Syntax checking on the go

A couple of days ago I was in the office, hacking the mainframe with The Great Halorgium. We got to talking about our current vim setup when he told me that it would be awesome if the statusline displayed a warning flag for buffers with syntax errors.

By the end of the day we'd written a couple of ftplugins to do just that for ruby and eruby. Read this raving to find out how.

Note: although we have only done it for ruby and eruby (at the time of this writing), you could easily do the same for other syntaxes using the same technique, provided there is a command line syntax checker for that language.

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 end result


Here is a screenshot showing the code in action:


Notice the [syntax:4] on the statusline. After this buffer was saved, our code ran it through a syntax checker which told us there was a syntax error on line 4 ZOMG! (actually its a missing bracket on line 3).

The code


Here is the code to accomplish this for the ruby filetype. It was pulled from my ~/.vim/ftplugin/ruby_stl_syntax_warning.vim.

Update (8/july/09): I've recently refactored the living shit out of this code, which has made it much simpler, and has removed much of the duplication between the syntax checkers. See my vimfiles repo for the latest hacks. Look in my vimrc (search for "syntax") and in the syntax_checkers directory.

Update (25/july/09): OK, I went completely insane and made it into a plugin with some more features, check it out here http://github.com/scrooloose/syntastic

 1 if exists("b:did_ruby_stl_syntax_warning_ftplugin") || &filetype !~ '\<ruby\>'
 2     finish
 3 endif
 4 let b:did_ruby_stl_syntax_warning_ftplugin = 1
 5
 6 "bail if the user doesnt have ruby installed
 7 if !executable("ruby")
 8     finish
 9 endif
10
11 "inject the syntax warning into the statusline
12 let &l:statusline = substitute(&statusline, '\(%=\)',
13             \ '%#warningmsg#%{StatuslineRubySyntaxWarning()}%*\1', '')
14
15 "recalculate after saving
16 autocmd bufwritepost <buffer> unlet! b:statusline_ruby_syntax_warning
17
18 "run the buffer through ruby -c
19 "
20 "return '' if no syntax errors detected
21 "return '[syntax:xxx]' if errors are detected, where xxx is the line num of
22 "the first error
23 function! StatuslineRubySyntaxWarning()
24     if !exists("b:statusline_ruby_syntax_warning")
25         let b:statusline_ruby_syntax_warning = ''
26         if filereadable(expand("%"))
27             let output = system("ruby -c " . expand("%"))
28             if v:shell_error != 0
29                 let b:statusline_ruby_syntax_warning =
30                             \ '[syntax:'. s:ExtractErrorLine(output) . ']'
31             endif
32         endif
33     endif
34     return b:statusline_ruby_syntax_warning
35 endfunction
36
37 "extract the line num of the first syntax error for the given output
38 "from 'ruby -c'
39 function! s:ExtractErrorLine(error_msg)
40     return substitute(a:error_msg, '.\{-}:\(\d*\): syntax error,.*', '\1', '')
41 endfunction


Wow! Well I have a boner, how about you?

Lets dissect this:

Lines 1–4 contain the standard house keeping code to make sure we don't source the plugin more than once. The filetype check is needed since the eruby runtime I use sources some of the ruby runtime files, including this one.

Lines 6–9 ensure that the ruby binary is present, since we are going to use ruby -c to check the syntax.

Lines 12–13 inject the syntax warning into our statusline. We chose to put it just before the left/right alignment separator since we put buffer related info on the left and cursor related info on the right. If you don't have %= on your statusline then you'll have to put it somewhere else.

Lines 23–41 are where the party is at. The StatuslineRubySyntaxWarning() function works out what should be displayed on the statusline and caches the result in b:statusline_ruby_syntax_warning. The code shells out to ruby -c to check the syntax of the buffer, before parsing any resulting error messages and returning the statusline flag containing the line number of the first error.

Line 16 clears the cached flag every time the buffer is saved, causing the syntax to be rechecked.

The future


There are a few other filetypes that I may write syntax checkers for, like sass and haml. Php could be useful too. And bash. Fuck, a bash syntax checker could actually be really handy. Getting that horrid shit right is about as easy as jacking off while watching a bunch of kitties getting nailed to a post. Things like apache configs could be good too, instead of doing apache2 -t manually.

Ive also been thinking about using the :sign commands to flag syntax errors. This way we could put a symbol next to each line containing a syntax error.