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.

2 comments:

  1. my .vimrc - the 1st part ONLY displays when errors are encountered in RED (User? colors must be defined)
    ---------------------------------------------------

    "at first only return something visual if an error found
    set statusline=
    set statusline+=\ %1*%r "set color + RO flag
    set statusline+=%{&ff!='unix'?'['.&ff.']':''} "display ONLY if &ff != unix
    set statusline+=%{(&fenc!='utf-8'&&&fenc!='')?'['.&fenc.']':''} "display ONLY if NOT utf-8
    set statusline+=%{&bomb?'[!]':''}
    set statusline+=%{StatuslineTabWarning()} "returns mixed, !et, tabs
    set statusline+=%{&paste?'[paste]':''} "paste"
    set statusline+=%{&modified?'+':''}\ "modified, constant OUTPUT starts here

    set statusline+=%*%-40t\ "reset color, filename
    set statusline+=%y
    set statusline+=%{FileSize()}

    set statusline+=[%{&hls?'h':'-'} "highlight search?
    set statusline+=%{&is?'i':'-'} "incremental search
    set statusline+=%2*%{&sta?'s':'-'} "smarttab
    set statusline+=%3*%{&ts}%*%{&sw} "tab/shiftwidth
    set statusline+=%4*%{&ai?'a':'-'}%{&si?'i':'-'}%{&cin?'c':'-'} "indent values
    set statusline+=%5*%{&wrap?'<':'>'}%6*%{&tw}%*] "wrap/textwidth
    set statusline+=%(\ %{VisualSelectionSize()}%)
    set statusline+=%=%([%l,%c%)][%L:%p%%]\ "current line

    ReplyDelete