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: