citadel

My dotfiles, scripts and nix configs
git clone git://jb55.com/citadel
Log | Files | Refs | README | LICENSE

tabular.vim (8589B)


      1 " Tabular:     Align columnar data using regex-designated column boundaries
      2 " Maintainer:  Matthew Wozniski (mjw@drexel.edu)
      3 " Date:        Thu, 11 Oct 2007 00:35:34 -0400
      4 " Version:     0.1
      5 
      6 " Stupid vimscript crap                                                   {{{1
      7 let s:savecpo = &cpo
      8 set cpo&vim
      9 
     10 " Private Functions                                                       {{{1
     11 
     12 " Return the number of bytes in a string after expanding tabs to spaces.  {{{2
     13 " This expansion is done based on the current value of 'tabstop'
     14 function! s:Strlen(string)
     15   let rv = 0
     16   let i = 0
     17 
     18   for char in split(a:string, '\zs')
     19     if char == "\t"
     20       let rv += &ts - i
     21       let i = 0
     22     else
     23       let rv += 1
     24       let i = (i + 1) % &ts
     25     endif
     26   endfor
     27 
     28   return rv
     29 endfunction
     30 
     31 " Align a string within a field                                           {{{2
     32 " These functions do not trim leading and trailing spaces.
     33 
     34 " Right align 'string' in a field of size 'fieldwidth'
     35 function! s:Right(string, fieldwidth)
     36   let spaces = a:fieldwidth - s:Strlen(a:string)
     37   return matchstr(a:string, '^\s*') . repeat(" ", spaces) . substitute(a:string, '^\s*', '', '')
     38 endfunction
     39 
     40 " Left align 'string' in a field of size 'fieldwidth'
     41 function! s:Left(string, fieldwidth)
     42   let spaces = a:fieldwidth - s:Strlen(a:string)
     43   return a:string . repeat(" ", spaces)
     44 endfunction
     45 
     46 " Center align 'string' in a field of size 'fieldwidth'
     47 function! s:Center(string, fieldwidth)
     48   let spaces = a:fieldwidth - s:Strlen(a:string)
     49   let right = spaces / 2
     50   let left = right + (right * 2 != spaces)
     51   return repeat(" ", left) . a:string . repeat(" ", right)
     52 endfunction
     53 
     54 " Remove spaces around a string                                           {{{2
     55 
     56 " Remove all trailing spaces from a string.
     57 function! s:StripTrailingSpaces(string)
     58   return matchstr(a:string, '^.\{-}\ze\s*$')
     59 endfunction
     60 
     61 " Remove all leading spaces from a string.
     62 function! s:StripLeadingSpaces(string)
     63   return matchstr(a:string, '^\s*\zs.*$')
     64 endfunction
     65 
     66 " Split a string into fields and delimiters                               {{{2
     67 " Like split(), but include the delimiters as elements
     68 " All odd numbered elements are delimiters
     69 " All even numbered elements are non-delimiters (including zero)
     70 function! s:SplitDelim(string, delim)
     71   let rv = []
     72   let beg = 0
     73   let idx = 0
     74 
     75   let len = len(a:string)
     76 
     77   while 1
     78     let mid = match(a:string, a:delim, beg, 1)
     79     if mid == -1 || mid == len
     80       break
     81     endif
     82 
     83     let matchstr = matchstr(a:string, a:delim, beg, 1)
     84     let length = strlen(matchstr)
     85 
     86     if beg < mid
     87       let rv += [ a:string[beg : mid-1] ]
     88     else
     89       let rv += [ "" ]
     90     endif
     91 
     92     let beg = mid + length
     93     let idx = beg
     94 
     95     if beg == mid
     96       " Empty match, advance "beg" by one to avoid infinite loop
     97       let rv += [ "" ]
     98       let beg += 1
     99     else " beg > mid
    100       let rv += [ a:string[mid : beg-1] ]
    101     endif
    102   endwhile
    103 
    104   let rv += [ strpart(a:string, idx) ]
    105 
    106   return rv
    107 endfunction
    108 
    109 " Replace lines from `start' to `start + len - 1' with the given strings. {{{2
    110 " If more lines are needed to show all strings, they will be added.
    111 " If there are too few strings to fill all lines, lines will be removed.
    112 function! s:SetLines(start, len, strings)
    113   if a:start > line('$') + 1 || a:start < 1
    114     throw "Invalid start line!"
    115   endif
    116 
    117   if len(a:strings) > a:len
    118     let fensave = &fen
    119     let view = winsaveview()
    120     call append(a:start + a:len - 1, repeat([''], len(a:strings) - a:len))
    121     call winrestview(view)
    122     let &fen = fensave
    123   elseif len(a:strings) < a:len
    124     let fensave = &fen
    125     let view = winsaveview()
    126     sil exe (a:start + len(a:strings)) . ',' .  (a:start + a:len - 1) . 'd_'
    127     call winrestview(view)
    128     let &fen = fensave
    129   endif
    130 
    131   call setline(a:start, a:strings)
    132 endfunction
    133 
    134 " Runs the given commandstring argument as an expression.                 {{{2
    135 " The commandstring expression is expected to reference the a:lines argument.
    136 " If the commandstring expression returns a list the items of that list will
    137 " replace the items in a:lines, otherwise the expression is assumed to have
    138 " modified a:lines itself.
    139 function! s:FilterString(lines, commandstring)
    140   exe 'let rv = ' . a:commandstring
    141 
    142   if type(rv) == type(a:lines) && rv isnot a:lines
    143     call filter(a:lines, 0)
    144     call extend(a:lines, rv)
    145   endif
    146 endfunction
    147 
    148 " Public API                                                              {{{1
    149 
    150 if !exists("g:tabular_default_format")
    151   let g:tabular_default_format = "l1"
    152 endif
    153 
    154 let s:formatelempat = '\%([lrc]\d\+\)'
    155 
    156 function! tabular#ElementFormatPattern()
    157   return s:formatelempat
    158 endfunction
    159 
    160 " Given a list of strings and a delimiter, split each string on every
    161 " occurrence of the delimiter pattern, format each element according to either
    162 " the provided format (optional) or the default format, and join them back
    163 " together with enough space padding to guarantee that the nth delimiter of
    164 " each string is aligned.
    165 function! tabular#TabularizeStrings(strings, delim, ...)
    166   if a:0 > 1
    167     echoerr "TabularizeStrings accepts only 2 or 3 arguments (got ".(a:0+2).")"
    168     return 1
    169   endif
    170 
    171   let formatstr = (a:0 ? a:1 : g:tabular_default_format)
    172 
    173   if formatstr !~? s:formatelempat . '\+'
    174     echoerr "Tabular: Invalid format \"" . formatstr . "\" specified!"
    175     return 1
    176   endif
    177 
    178   let format = split(formatstr, s:formatelempat . '\zs')
    179 
    180   let lines = map(a:strings, 's:SplitDelim(v:val, a:delim)')
    181 
    182   " Strip spaces
    183   "   - Only from non-delimiters; spaces in delimiters must have been matched
    184   "     intentionally
    185   "   - Don't strip leading spaces from the first element; we like indenting.
    186   for line in lines
    187     if line[0] !~ '^\s*$'
    188       let line[0] = s:StripTrailingSpaces(line[0])
    189     endif
    190     if len(line) >= 3
    191       for i in range(2, len(line)-1, 2)
    192         let line[i] = s:StripLeadingSpaces(s:StripTrailingSpaces(line[i]))
    193       endfor
    194     endif
    195   endfor
    196 
    197   " Find the max length of each field
    198   let maxes = []
    199   for line in lines
    200     for i in range(len(line))
    201       if i == len(maxes)
    202         let maxes += [ s:Strlen(line[i]) ]
    203       else
    204         let maxes[i] = max( [ maxes[i], s:Strlen(line[i]) ] )
    205       endif
    206     endfor
    207   endfor
    208 
    209   let lead_blank = empty(filter(copy(lines), 'v:val[0] =~ "\\S"'))
    210 
    211   " Concatenate the fields, according to the format pattern.
    212   for idx in range(len(lines))
    213     let line = lines[idx]
    214     for i in range(len(line))
    215       let how = format[i % len(format)][0]
    216       let pad = format[i % len(format)][1:-1]
    217 
    218       if how =~? 'l'
    219         let field = s:Left(line[i], maxes[i])
    220       elseif how =~? 'r'
    221         let field = s:Right(line[i], maxes[i])
    222       elseif how =~? 'c'
    223         let field = s:Center(line[i], maxes[i])
    224       endif
    225 
    226       let line[i] = field . (lead_blank && i == 0 ? '' : repeat(" ", pad))
    227     endfor
    228 
    229     let lines[idx] = s:StripTrailingSpaces(join(line, ''))
    230   endfor
    231 endfunction
    232 
    233 " Apply 0 or more filters, in sequence, to selected text in the buffer    {{{2
    234 " The lines to be filtered are determined as follows:
    235 "   If the function is called with a range containing multiple lines, then
    236 "     those lines will be used as the range.
    237 "   If the function is called with no range or with a range of 1 line, then
    238 "     if "includepat" is not specified,
    239 "       that 1 line will be filtered,
    240 "     if "includepat" is specified and that line does not match it,
    241 "       no lines will be filtered
    242 "     if "includepat" is specified and that line does match it,
    243 "       all contiguous lines above and below the specified line matching the
    244 "       pattern will be filtered.
    245 "
    246 " The remaining arguments must each be a filter to apply to the text.
    247 " Each filter must either be a String evaluating to a function to be called.
    248 function! tabular#PipeRange(includepat, ...) range
    249   let top = a:firstline
    250   let bot = a:lastline
    251 
    252   if a:includepat != '' && top == bot
    253     if top < 0 || top > line('$') || getline(top) !~ a:includepat
    254       return
    255     endif
    256     while top > 1 && getline(top-1) =~ a:includepat
    257       let top -= 1
    258     endwhile
    259     while bot < line('$') && getline(bot+1) =~ a:includepat
    260       let bot += 1
    261     endwhile
    262   endif
    263 
    264   let lines = map(range(top, bot), 'getline(v:val)')
    265 
    266   for filter in a:000
    267     if type(filter) != type("")
    268       echoerr "PipeRange: Bad filter: " . string(filter)
    269     endif
    270 
    271     call s:FilterString(lines, filter)
    272 
    273     unlet filter
    274   endfor
    275 
    276   call s:SetLines(top, bot - top + 1, lines)
    277 endfunction
    278 
    279 " Stupid vimscript crap, part 2                                           {{{1
    280 let &cpo = s:savecpo
    281 unlet s:savecpo
    282 
    283 " vim:set sw=2 sts=2 fdm=marker: