├── .gitignore ├── LICENSE.md ├── README.md ├── after └── plugin │ └── TabularMaps.vim ├── autoload └── tabular.vim ├── doc └── Tabular.txt └── plugin └── Tabular.vim /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Matthew J. Wozniski 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of the contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 16 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 18 | NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 21 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 24 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tabular 2 | ============== 3 | Sometimes, it's useful to line up text. Naturally, it's nicer to have the 4 | computer do this for you, since aligning things by hand quickly becomes 5 | unpleasant. While there are other plugins for aligning text, the ones I've 6 | tried are either impossibly difficult to understand and use, or too simplistic 7 | to handle complicated tasks. This plugin aims to make the easy things easy 8 | and the hard things possible, without providing an unnecessarily obtuse 9 | interface. It's still a work in progress, and criticisms are welcome. 10 | 11 | See [Aligning Text with Tabular.vim](http://vimcasts.org/episodes/aligning-text-with-tabular-vim/) 12 | for a screencast that shows how Tabular.vim works. 13 | 14 | See [doc/Tabular.txt](http://raw.github.com/godlygeek/tabular/master/doc/Tabular.txt) 15 | for detailed documentation. 16 | 17 | Installation 18 | ============== 19 | 20 | ## Vim 8.1+ 21 | 22 | No third-party package manager is required! Clone into: 23 | 24 | `.vim/pack/plugins/start` 25 | 26 | Make sure you include `packloadall` in your `.vimrc`. 27 | 28 | ## Pathogen 29 | 30 | mkdir -p ~/.vim/bundle 31 | cd ~/.vim/bundle 32 | git clone https://github.com/godlygeek/tabular.git 33 | 34 | Once help tags have been generated (either using Pathogen's `:Helptags` 35 | command, or by pointing vim's `:helptags` command at the directory where you 36 | installed Tabular), you can view the manual with `:help tabular`. 37 | 38 | See [pathogen.vim](https://github.com/tpope/vim-pathogen) for help or for package manager installation. 39 | -------------------------------------------------------------------------------- /after/plugin/TabularMaps.vim: -------------------------------------------------------------------------------- 1 | " Copyright (c) 2016, Matthew J. Wozniski 2 | " All rights reserved. 3 | " 4 | " Redistribution and use in source and binary forms, with or without 5 | " modification, are permitted provided that the following conditions are met: 6 | " * Redistributions of source code must retain the above copyright notice, 7 | " this list of conditions and the following disclaimer. 8 | " * Redistributions in binary form must reproduce the above copyright 9 | " notice, this list of conditions and the following disclaimer in the 10 | " documentation and/or other materials provided with the distribution. 11 | " * The names of the contributors may not be used to endorse or promote 12 | " products derived from this software without specific prior written 13 | " permission. 14 | " 15 | " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 16 | " OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 18 | " NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | " INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 21 | " OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | " LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | " NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 24 | " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | if !exists(':Tabularize') || get(g:, 'no_default_tabular_maps', 0) 27 | finish " Tabular.vim wasn't loaded or the default maps are unwanted 28 | endif 29 | 30 | let s:save_cpo = &cpo 31 | set cpo&vim 32 | 33 | AddTabularPattern! assignment /[|&+*/%<>=!~-]\@!=]=\|=\~\)\@![|&+*/%<>=!~-]*=/l1r1 34 | AddTabularPattern! two_spaces / /l0 35 | 36 | AddTabularPipeline! multiple_spaces / / map(a:lines, "substitute(v:val, ' *', ' ', 'g')") | tabular#TabularizeStrings(a:lines, ' ', 'l0') 37 | AddTabularPipeline! spaces / / map(a:lines, "substitute(v:val, ' *' , ' ' , 'g')") | tabular#TabularizeStrings(a:lines, ' ' , 'l0') 38 | 39 | AddTabularPipeline! argument_list /(.*)/ map(a:lines, 'substitute(v:val, ''\s*\([(,)]\)\s*'', ''\1'', ''g'')') 40 | \ | tabular#TabularizeStrings(a:lines, '[(,)]', 'l0') 41 | \ | map(a:lines, 'substitute(v:val, ''\(\s*\),'', '',\1 '', "g")') 42 | \ | map(a:lines, 'substitute(v:val, ''\s*)'', ")", "g")') 43 | 44 | function! SplitCDeclarations(lines) 45 | let rv = [] 46 | for line in a:lines 47 | " split the line into declaractions 48 | let split = split(line, '\s*[,;]\s*') 49 | " separate the type from the first declaration 50 | let type = substitute(split[0], '\%(\%([&*]\s*\)*\)\=\k\+$', '', '') 51 | " add the ; back on every declaration 52 | call map(split, 'v:val . ";"') 53 | " add the first element to the return as-is, and remove it from the list 54 | let rv += [ remove(split, 0) ] 55 | " transform the other elements by adding the type on at the beginning 56 | call map(split, 'type . v:val') 57 | " and add them all to the return 58 | let rv += split 59 | endfor 60 | return rv 61 | endfunction 62 | 63 | AddTabularPipeline! split_declarations /,.*;/ SplitCDeclarations(a:lines) 64 | 65 | AddTabularPattern! ternary_operator /^.\{-}\zs?\|:/l1 66 | 67 | AddTabularPattern! cpp_io /<<\|>>/l1 68 | 69 | AddTabularPattern! pascal_assign /:=/l1 70 | 71 | AddTabularPattern! trailing_c_comments /\/\*\|\*\/\|\/\//l1 72 | 73 | let &cpo = s:save_cpo 74 | unlet s:save_cpo 75 | -------------------------------------------------------------------------------- /autoload/tabular.vim: -------------------------------------------------------------------------------- 1 | " Tabular: Align columnar data using regex-designated column boundaries 2 | " Maintainer: Matthew Wozniski (godlygeek@gmail.com) 3 | " Date: Thu, 03 May 2012 20:49:32 -0400 4 | " Version: 1.0 5 | " 6 | " Long Description: 7 | " Sometimes, it's useful to line up text. Naturally, it's nicer to have the 8 | " computer do this for you, since aligning things by hand quickly becomes 9 | " unpleasant. While there are other plugins for aligning text, the ones I've 10 | " tried are either impossibly difficult to understand and use, or too simplistic 11 | " to handle complicated tasks. This plugin aims to make the easy things easy 12 | " and the hard things possible, without providing an unnecessarily obtuse 13 | " interface. It's still a work in progress, and criticisms are welcome. 14 | " 15 | " License: 16 | " Copyright (c) 2012, Matthew J. Wozniski 17 | " All rights reserved. 18 | " 19 | " Redistribution and use in source and binary forms, with or without 20 | " modification, are permitted provided that the following conditions are met: 21 | " * Redistributions of source code must retain the above copyright notice, 22 | " this list of conditions and the following disclaimer. 23 | " * Redistributions in binary form must reproduce the above copyright 24 | " notice, this list of conditions and the following disclaimer in the 25 | " documentation and/or other materials provided with the distribution. 26 | " * The names of the contributors may not be used to endorse or promote 27 | " products derived from this software without specific prior written 28 | " permission. 29 | " 30 | " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 31 | " OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 32 | " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 33 | " NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, 34 | " INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35 | " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 36 | " OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 37 | " LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 38 | " NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 39 | " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | 41 | " Stupid vimscript crap {{{1 42 | let s:savecpo = &cpo 43 | set cpo&vim 44 | 45 | " Private Functions {{{1 46 | 47 | " Return the number of bytes in a string after expanding tabs to spaces. {{{2 48 | " This expansion is done based on the current value of 'tabstop' 49 | if exists('*strdisplaywidth') 50 | " Needs vim 7.3 51 | let s:Strlen = function("strdisplaywidth") 52 | else 53 | function! s:Strlen(string) 54 | " Implement the tab handling part of strdisplaywidth for vim 7.2 and 55 | " earlier - not much that can be done about handling doublewidth 56 | " characters. 57 | let rv = 0 58 | let i = 0 59 | 60 | for char in split(a:string, '\zs') 61 | if char == "\t" 62 | let rv += &ts - i 63 | let i = 0 64 | else 65 | let rv += 1 66 | let i = (i + 1) % &ts 67 | endif 68 | endfor 69 | 70 | return rv 71 | endfunction 72 | endif 73 | 74 | " Align a string within a field {{{2 75 | " These functions do not trim leading and trailing spaces. 76 | 77 | " Right align 'string' in a field of size 'fieldwidth' 78 | function! s:Right(string, fieldwidth) 79 | let spaces = a:fieldwidth - s:Strlen(a:string) 80 | return matchstr(a:string, '^\s*') . repeat(" ", spaces) . substitute(a:string, '^\s*', '', '') 81 | endfunction 82 | 83 | " Left align 'string' in a field of size 'fieldwidth' 84 | function! s:Left(string, fieldwidth) 85 | let spaces = a:fieldwidth - s:Strlen(a:string) 86 | return a:string . repeat(" ", spaces) 87 | endfunction 88 | 89 | " Center align 'string' in a field of size 'fieldwidth' 90 | function! s:Center(string, fieldwidth) 91 | let spaces = a:fieldwidth - s:Strlen(a:string) 92 | let right = spaces / 2 93 | let left = right + (right * 2 != spaces) 94 | return repeat(" ", left) . a:string . repeat(" ", right) 95 | endfunction 96 | 97 | " Remove spaces around a string {{{2 98 | 99 | " Remove all trailing spaces from a string. 100 | function! s:StripTrailingSpaces(string) 101 | return matchstr(a:string, '^.\{-}\ze\s*$') 102 | endfunction 103 | 104 | " Remove all leading spaces from a string. 105 | function! s:StripLeadingSpaces(string) 106 | return matchstr(a:string, '^\s*\zs.*$') 107 | endfunction 108 | 109 | " Find the longest common indent for a list of strings {{{2 110 | " If a string is shorter than the others but contains no non-whitespace 111 | " characters, it does not end the match. This provides consistency with 112 | " vim's behavior that blank lines don't have trailing spaces. 113 | function! s:LongestCommonIndent(strings) 114 | if empty(a:strings) 115 | return '' 116 | endif 117 | 118 | let n = 0 119 | while 1 120 | let ns = join(map(copy(a:strings), 'v:val[n]'), '') 121 | if ns !~ '^ \+$\|^\t\+$' 122 | break 123 | endif 124 | let n += 1 125 | endwhile 126 | 127 | return strpart(a:strings[0], 0, n) 128 | endfunction 129 | 130 | " Split a string into fields and delimiters {{{2 131 | " Like split(), but include the delimiters as elements 132 | " All odd numbered elements are delimiters 133 | " All even numbered elements are non-delimiters (including zero) 134 | function! s:SplitDelim(string, delim) 135 | let rv = [] 136 | let beg = 0 137 | 138 | let len = len(a:string) 139 | let searchoff = 0 140 | 141 | while 1 142 | let mid = match(a:string, a:delim, beg + searchoff, 1) 143 | if mid == -1 || mid == len 144 | break 145 | endif 146 | 147 | let matchstr = matchstr(a:string, a:delim, beg + searchoff, 1) 148 | let length = strlen(matchstr) 149 | 150 | if length == 0 && beg == mid 151 | " Zero-length match for a zero-length delimiter - advance past it 152 | let searchoff += 1 153 | continue 154 | endif 155 | 156 | if beg == mid 157 | let rv += [ "" ] 158 | else 159 | let rv += [ a:string[beg : mid-1] ] 160 | endif 161 | 162 | let rv += [ matchstr ] 163 | 164 | let beg = mid + length 165 | let searchoff = 0 166 | endwhile 167 | 168 | let rv += [ strpart(a:string, beg) ] 169 | 170 | return rv 171 | endfunction 172 | 173 | " Replace lines from `start' to `start + len - 1' with the given strings. {{{2 174 | " If more lines are needed to show all strings, they will be added. 175 | " If there are too few strings to fill all lines, lines will be removed. 176 | function! s:SetLines(start, len, strings) 177 | if a:start > line('$') + 1 || a:start < 1 178 | throw "Invalid start line!" 179 | endif 180 | 181 | if len(a:strings) > a:len 182 | let fensave = &fen 183 | let view = winsaveview() 184 | call append(a:start + a:len - 1, repeat([''], len(a:strings) - a:len)) 185 | call winrestview(view) 186 | let &fen = fensave 187 | elseif len(a:strings) < a:len 188 | let fensave = &fen 189 | let view = winsaveview() 190 | sil exe (a:start + len(a:strings)) . ',' . (a:start + a:len - 1) . 'd_' 191 | call winrestview(view) 192 | let &fen = fensave 193 | endif 194 | 195 | call setline(a:start, a:strings) 196 | endfunction 197 | 198 | " Runs the given commandstring argument as an expression. {{{2 199 | " The commandstring expression is expected to reference the a:lines argument. 200 | " If the commandstring expression returns a list the items of that list will 201 | " replace the items in a:lines, otherwise the expression is assumed to have 202 | " modified a:lines itself. 203 | function! s:FilterString(lines, commandstring) 204 | exe 'let rv = ' . a:commandstring 205 | 206 | if type(rv) == type(a:lines) && rv isnot a:lines 207 | call filter(a:lines, 0) 208 | call extend(a:lines, rv) 209 | endif 210 | endfunction 211 | 212 | " Public API {{{1 213 | 214 | if !exists("g:tabular_default_format") 215 | let g:tabular_default_format = "l1" 216 | endif 217 | 218 | let s:formatelempat = '\%([lrc]\d\+\)' 219 | 220 | function! tabular#ElementFormatPattern() 221 | return s:formatelempat 222 | endfunction 223 | 224 | " Given a list of strings and a delimiter, split each string on every 225 | " occurrence of the delimiter pattern, format each element according to either 226 | " the provided format (optional) or the default format, and join them back 227 | " together with enough space padding to guarantee that the nth delimiter of 228 | " each string is aligned. 229 | function! tabular#TabularizeStrings(strings, delim, ...) 230 | if a:0 > 1 231 | echoerr "TabularizeStrings accepts only 2 or 3 arguments (got ".(a:0+2).")" 232 | return 1 233 | endif 234 | 235 | let formatstr = (a:0 ? a:1 : g:tabular_default_format) 236 | 237 | if formatstr !~? s:formatelempat . '\+' 238 | echoerr "Tabular: Invalid format \"" . formatstr . "\" specified!" 239 | return 1 240 | endif 241 | 242 | let format = split(formatstr, s:formatelempat . '\zs') 243 | 244 | let lines = a:strings 245 | 246 | call map(lines, 's:SplitDelim(v:val, a:delim)') 247 | 248 | let first_fields = [] 249 | 250 | " Strip spaces from non-delimiters; spaces in delimiters must have been 251 | " matched intentionally 252 | for line in lines 253 | if len(line) == 1 && s:do_gtabularize 254 | continue " Leave non-matching lines unchanged for GTabularize 255 | endif 256 | 257 | call add(first_fields, line[0]) 258 | 259 | if len(line) >= 1 260 | for i in range(0, len(line)-1, 2) 261 | let line[i] = s:StripLeadingSpaces(s:StripTrailingSpaces(line[i])) 262 | endfor 263 | endif 264 | endfor 265 | 266 | let common_indent = s:LongestCommonIndent(first_fields) 267 | 268 | " Find the max length of each field 269 | let maxes = [] 270 | for line in lines 271 | if len(line) == 1 && s:do_gtabularize 272 | continue " non-matching lines don't affect field widths for GTabularize 273 | endif 274 | 275 | for i in range(len(line)) 276 | if i == len(maxes) 277 | let maxes += [ s:Strlen(line[i]) ] 278 | else 279 | let maxes[i] = max( [ maxes[i], s:Strlen(line[i]) ] ) 280 | endif 281 | endfor 282 | endfor 283 | 284 | " Concatenate the fields, according to the format pattern. 285 | for idx in range(len(lines)) 286 | let line = lines[idx] 287 | 288 | if len(line) == 1 && s:do_gtabularize 289 | let lines[idx] = line[0] " GTabularize doesn't change non-matching lines 290 | continue 291 | endif 292 | 293 | for i in range(len(line)) 294 | let how = format[i % len(format)][0] 295 | let pad = format[i % len(format)][1:-1] 296 | 297 | if how =~? 'l' 298 | let field = s:Left(line[i], maxes[i]) 299 | elseif how =~? 'r' 300 | let field = s:Right(line[i], maxes[i]) 301 | elseif how =~? 'c' 302 | let field = s:Center(line[i], maxes[i]) 303 | endif 304 | 305 | let line[i] = field . repeat(" ", pad) 306 | endfor 307 | 308 | let prefix = common_indent 309 | if len(line) == 1 && s:do_gtabularize 310 | " We didn't strip the indent in this case; nothing to put back. 311 | let prefix = '' 312 | endif 313 | 314 | let lines[idx] = s:StripTrailingSpaces(prefix . join(line, '')) 315 | endfor 316 | endfunction 317 | 318 | " Apply 0 or more filters, in sequence, to selected text in the buffer {{{2 319 | " The lines to be filtered are determined as follows: 320 | " If the function is called with a range containing multiple lines, then 321 | " those lines will be used as the range. 322 | " If the function is called with no range or with a range of 1 line, then 323 | " if GTabularize mode is being used, 324 | " the range will not be adjusted 325 | " if "includepat" is not specified, 326 | " that 1 line will be filtered, 327 | " if "includepat" is specified and that line does not match it, 328 | " no lines will be filtered 329 | " if "includepat" is specified and that line does match it, 330 | " all contiguous lines above and below the specified line matching the 331 | " pattern will be filtered. 332 | " 333 | " The remaining arguments must each be a filter to apply to the text. 334 | " Each filter must either be a String evaluating to a function to be called. 335 | function! tabular#PipeRange(includepat, ...) range 336 | exe a:firstline . ',' . a:lastline 337 | \ . 'call tabular#PipeRangeWithOptions(a:includepat, a:000, {})' 338 | endfunction 339 | 340 | " Extended version of tabular#PipeRange, which 341 | " 1) Takes the list of filters as an explicit list rather than as varargs 342 | " 2) Supports passing a dictionary of options to control the routine. 343 | " Currently, the only supported option is 'mode', which determines whether 344 | " to behave as :Tabularize or as :GTabularize 345 | " This allows me to add new features here without breaking API compatibility 346 | " in the future. 347 | function! tabular#PipeRangeWithOptions(includepat, filterlist, options) range 348 | let top = a:firstline 349 | let bot = a:lastline 350 | 351 | let s:do_gtabularize = (get(a:options, 'mode', '') ==# 'GTabularize') 352 | 353 | if !s:do_gtabularize 354 | " In the default mode, apply range extension logic 355 | if a:includepat != '' && top == bot 356 | if top < 0 || top > line('$') || getline(top) !~ a:includepat 357 | return 358 | endif 359 | while top > 1 && getline(top-1) =~ a:includepat 360 | let top -= 1 361 | endwhile 362 | while bot < line('$') && getline(bot+1) =~ a:includepat 363 | let bot += 1 364 | endwhile 365 | endif 366 | endif 367 | 368 | let lines = map(range(top, bot), 'getline(v:val)') 369 | 370 | for filter in a:filterlist 371 | if type(filter) != type("") 372 | echoerr "PipeRange: Bad filter: " . string(filter) 373 | endif 374 | 375 | call s:FilterString(lines, filter) 376 | 377 | unlet filter 378 | endfor 379 | 380 | call s:SetLines(top, bot - top + 1, lines) 381 | endfunction 382 | 383 | " Part of the public interface so interested pipelines can query this and 384 | " adjust their behavior appropriately. 385 | function! tabular#DoGTabularize() 386 | return s:do_gtabularize 387 | endfunction 388 | 389 | function! s:SplitDelimTest(string, delim, expected) 390 | let result = s:SplitDelim(a:string, a:delim) 391 | 392 | if result !=# a:expected 393 | echomsg 'Test failed!' 394 | echomsg ' string=' . string(a:string) . ' delim=' . string(a:delim) 395 | echomsg ' Returned=' . string(result) 396 | echomsg ' Expected=' . string(a:expected) 397 | endif 398 | endfunction 399 | 400 | function! tabular#SplitDelimUnitTest() 401 | let assignment = '[|&+*/%<>=!~-]\@!=]=\|=\~\)\@![|&+*/%<>=!~-]*=' 402 | let two_spaces = ' ' 403 | let ternary_operator = '^.\{-}\zs?\|:' 404 | let cpp_io = '<<\|>>' 405 | let pascal_assign = ':=' 406 | let trailing_c_comments = '\/\*\|\*\/\|\/\/' 407 | 408 | call s:SplitDelimTest('a+=b', assignment, ['a', '+=', 'b']) 409 | call s:SplitDelimTest('a-=b', assignment, ['a', '-=', 'b']) 410 | call s:SplitDelimTest('a!=b', assignment, ['a!=b']) 411 | call s:SplitDelimTest('a==b', assignment, ['a==b']) 412 | call s:SplitDelimTest('a&=b', assignment, ['a', '&=', 'b']) 413 | call s:SplitDelimTest('a|=b', assignment, ['a', '|=', 'b']) 414 | call s:SplitDelimTest('a=b=c', assignment, ['a', '=', 'b', '=', 'c']) 415 | 416 | call s:SplitDelimTest('a b c', two_spaces, ['a', ' ', 'b', ' ', 'c']) 417 | call s:SplitDelimTest('a b c', two_spaces, ['a b', ' ', ' c']) 418 | call s:SplitDelimTest('ab c', two_spaces, ['ab', ' ', '', ' ', 'c']) 419 | 420 | call s:SplitDelimTest('a?b:c', ternary_operator, ['a', '?', 'b', ':', 'c']) 421 | 422 | call s:SplitDelimTest('a< 27 | :let g:tabular_loaded = 1 28 | 29 | ============================================================================== 30 | 1. Description *tabular-intro* 31 | 32 | Sometimes, it's useful to line up text. Naturally, it's nicer to have the 33 | computer do this for you, since aligning things by hand quickly becomes 34 | unpleasant. While there are other plugins for aligning text, the ones I've 35 | tried are either impossibly difficult to understand and use, or too simplistic 36 | to handle complicated tasks. This plugin aims to make the easy things easy 37 | and the hard things possible, without providing an unnecessarily obtuse 38 | interface. It's still a work in progress, and criticisms are welcome. 39 | 40 | ============================================================================== 41 | 2. Walkthrough *tabular-walkthrough* *:Tabularize* 42 | 43 | Tabular's commands are based largely on regular expressions. The basic 44 | technique used by Tabular is taking some regex to match field delimiters, 45 | splitting the input lines at those delimiters, trimming unnecessary spaces 46 | from the non-delimiter parts, padding the non-delimiter parts of the lines 47 | with spaces to make them the same length, and joining things back together 48 | again. 49 | 50 | For instance, consider starting with the following lines: 51 | > 52 | Some short phrase,some other phrase 53 | A much longer phrase here,and another long phrase 54 | < 55 | Let's say we want to line these lines up at the commas. We can tell 56 | Tabularize to do this by passing a pattern matching , to the Tabularize 57 | command: 58 | > 59 | :Tabularize /, 60 | 61 | Some short phrase , some other phrase 62 | A much longer phrase here , and another long phrase 63 | < 64 | I encourage you to try copying those lines to another buffer and trying to 65 | call :Tabularize. You'll want to take notice of two things quickly: First, 66 | instead of requiring a range, Tabularize tries to figure out what you want to 67 | happen. Since it knows that you want to act on lines matching a comma, it 68 | will look upwards and downwards for lines around the current line that match a 69 | comma, and consider all contiguous lines matching the pattern to be the range 70 | to be acted upon. You can always override this by specifying a range, though. 71 | 72 | The second thing you should notice is that you'll almost certainly be able to 73 | abbreviate :Tabularize to :Tab - using this form in mappings and scripts is 74 | discouraged as it will make conflicts with other scripts more likely, but for 75 | interactive use it's a nice timesaver. Another convenience feature is that 76 | running :Tabularize without providing a new pattern will cause it to reuse the 77 | last pattern it was called with. 78 | 79 | So, anyway, now the commas line up. Splitting the lines on commas, Tabular 80 | realized that 'Some short phrase' would need to be padded with spaces to match 81 | the length of 'A much longer phrase here', and it did that before joining the 82 | lines back together. You'll also notice that, in addition to the spaces 83 | inserting for padding, extra spaces were inserted between fields. That's 84 | because by default, Tabular prints things left-aligned with one space between 85 | fields. If you wanted to print things right-aligned with no spaces between 86 | fields, you would provide a different format to the Tabularize command: 87 | > 88 | :Tabularize /,/r0 89 | 90 | Some short phrase, some other phrase 91 | A much longer phrase here,and another long phrase 92 | < 93 | A format specifier is either l, r, or c, followed by one or more digits. If 94 | the letter is l, the field will be left aligned, similarly for r and right 95 | aligning and c and center aligning. The number following the letter is the 96 | number of spaces padding to insert before the start of the next field. 97 | Multiple format specifiers can be added to the same command - each field will 98 | be printed with the next format specifier in the list; when they all have been 99 | used the first will be used again, and so on. So, the last command right 100 | aligned every field, then inserted 0 spaces of padding before the next field. 101 | What if we wanted to right align the text before the comma, and left align the 102 | text after the comma? The command would look like this: 103 | > 104 | :Tabularize /,/r1c1l0 105 | 106 | Some short phrase , some other phrase 107 | A much longer phrase here , and another long phrase 108 | < 109 | That command would be read as "Align the matching text, splitting fields on 110 | commas. Print everything before the first comma right aligned, then 1 space, 111 | then the comma center aligned, then 1 space, then everything after the comma 112 | left aligned." Notice that the alignment of the field the comma is in is 113 | irrelevant - since it's only 1 cell wide, it looks the same whether it's right, 114 | left, or center aligned. Also notice that the 0 padding spaces specified for 115 | the 3rd field are unused - but they would be used if there were enough fields 116 | to require looping through the fields again. For instance: 117 | > 118 | abc,def,ghi 119 | a,b 120 | a,b,c 121 | 122 | :Tabularize /,/r1c1l0 123 | 124 | abc , def, ghi 125 | a , b 126 | a , b , c 127 | < 128 | Notice that now, the format pattern has been reused; field 4 (the second comma) 129 | is right aligned, field 5 is center aligned. No spaces were inserted between 130 | the 3rd field (containing "def") and the 4th field (the second comma) because 131 | the format specified 'l0'. 132 | 133 | But, what if you only wanted to act on the first comma on the line, rather than 134 | all of the commas on the line? Let's say we want everything before the first 135 | comma right aligned, then the comma, then everything after the comma left 136 | aligned: 137 | > 138 | abc,def,ghi 139 | a,b 140 | a,b,c 141 | 142 | :Tabularize /^[^,]*\zs,/r0c0l0 143 | 144 | abc,def,ghi 145 | a,b 146 | a,b,c 147 | < 148 | Here, we used a Vim regex that would only match the first comma on the line. 149 | It matches the beginning of the line, followed by all the non-comma characters 150 | up to the first comma, and then forgets about what it matched so far and 151 | pretends that the match starts exactly at the comma. 152 | 153 | But, now that this command does exactly what we want it to, it's become pretty 154 | unwieldy. It would be unpleasant to need to type that more than once or 155 | twice. The solution is to assign a name to it. 156 | > 157 | :AddTabularPattern first_comma /^[^,]*\zs,/r0c0l0 158 | < 159 | Now, typing ":Tabularize first_comma" will do the same thing as typing the 160 | whole pattern out each time. Of course this is more useful if you store the 161 | name in a file to be used later. 162 | 163 | NOTE: In order to make these new commands available every time vim starts, 164 | you'll need to put those new commands into a .vim file in a plugin directory 165 | somewhere in your 'runtimepath'. In order to make sure that Tabular.vim has 166 | already been loaded before your file tries to use :AddTabularPattern or 167 | :AddTabularPipeline, the new file should be installed in an after/plugin 168 | directory in 'runtimepath'. In general, it will be safe to find out where the 169 | TabularMaps.vim plugin was installed, and place other files extending 170 | Tabular.vim in the same directory as TabularMaps.vim. For more information, 171 | and some suggested best practices, check out the |tabular-scripting| section. 172 | 173 | Lastly, we'll approach the case where tabular cannot achieve your desired goal 174 | just by splitting lines apart, trimming whitespace, padding with whitespace, 175 | and rejoining the lines. As an example, consider the multiple_spaces command 176 | from TabularMaps.vim. The goal is to split using two or more spaces as a 177 | field delimiter, and join fields back together, properly lined up, with only 178 | two spaces between the end of each field and the beginning of the next. 179 | Unfortunately, Tabular can't do this with only the commands we know so far: 180 | > 181 | :Tabularize / / 182 | < 183 | The above function won't work, because it will consider "a b" as 5 fields 184 | delimited by two pairs of 2 spaces ( 'a', ' ', '', ' ', 'b' ) instead of as 185 | 3 fields delimited by one set of 2 or more spaces ( 'a', ' ', 'b' ). 186 | > 187 | :Tabularize / \+/ 188 | < 189 | The above function won't work either, because it will leave the delimiter as 4 190 | spaces when used against "a b", meaning that we would fail at our goal of 191 | collapsing everything down to two spaces between fields. So, we need a new 192 | command to get around this: 193 | > 194 | :AddTabularPipeline multiple_spaces / \{2,}/ 195 | \ map(a:lines, "substitute(v:val, ' \{2,}', ' ', 'g')") 196 | \ | tabular#TabularizeStrings(a:lines, ' ', 'l0') 197 | < 198 | Yeah. I know it looks complicated. Bear with me. I probably will try to add 199 | in some shortcuts for this syntax, but this verbose will be guaranteed to 200 | always work. 201 | 202 | You should already recognize the name being assigned. The next thing to 203 | happen is / \{2,}/ which is a pattern specifying which lines should 204 | automatically be included in the range when no range is given. Without this, 205 | there would be no pattern to use for extending the range. Everything after 206 | that is a | separated list of expressions to be evaluated. In the context in 207 | which they will be evaluated, a:lines will be set to a List of Strings 208 | containing the text of the lines being filtered as they procede through the 209 | pipeline you've set up. The \ at the start of the lines are just vim's line 210 | continuation marker; you needn't worry much about them. So, the first 211 | expression in the pipeline transforms each line by replacing every instance of 212 | 2 or more spaces with exactly two spaces. The second command in the pipeline 213 | performs the equivalent of ":Tabularize / /l0"; the only difference is that 214 | it is operating on a List of Strings rather than text in the buffer. At the 215 | end of the pipeline, the Strings in the modified a:lines (or the return value 216 | of the last expression in the pipeline, if it returns a List) will replace the 217 | chosen range. 218 | 219 | ============================================================================== 220 | 3. Extending *tabular-scripting* 221 | 222 | As mentioned above, the most important consideration when extending Tabular 223 | with new maps or commands is that your plugin must be loaded after Tabular.vim 224 | has finished loading, and only if Tabular.vim has loaded successfully. The 225 | easiest approach to making sure it loads after Tabular.vim is simply putting 226 | the new file (we'll call it "tabular_extra.vim" as an example) into an 227 | "after/plugin/" directory in 'runtimepath', for instance: 228 | > 229 | ~/.vim/after/plugin/tabular_extra.vim 230 | < 231 | The default set of mappings, found in "TabularMaps.vim", is installed in 232 | the after/plugin/ subdirectory of whatever directory Tabular was installed to. 233 | 234 | The other important consideration is making sure that your commands are only 235 | called if Tabular.vim was actually loaded. The easiest way to do this is by 236 | checking for the existence of the :Tabularize command at the start of your 237 | plugin. A short example plugin would look like this: 238 | > 239 | " after/plugin/my_tabular_commands.vim 240 | " Provides extra :Tabularize commands 241 | 242 | if !exists(':Tabularize') 243 | finish " Give up here; the Tabular plugin musn't have been loaded 244 | endif 245 | 246 | " Make line wrapping possible by resetting the 'cpo' option, first saving it 247 | let s:save_cpo = &cpo 248 | set cpo&vim 249 | 250 | AddTabularPattern! asterisk /*/l1 251 | 252 | AddTabularPipeline! remove_leading_spaces /^ / 253 | \ map(a:lines, "substitute(v:val, '^ *', '', '')") 254 | 255 | " Restore the saved value of 'cpo' 256 | let &cpo = s:save_cpo 257 | unlet s:save_cpo 258 | < 259 | ============================================================================== 260 | vim:tw=78:fo=tcq2:isk=!-~,^*,^\|,^\":ts=8:ft=help:norl: 261 | -------------------------------------------------------------------------------- /plugin/Tabular.vim: -------------------------------------------------------------------------------- 1 | " Tabular: Align columnar data using regex-designated column boundaries 2 | " Maintainer: Matthew Wozniski (godlygeek@gmail.com) 3 | " Date: Thu, 03 May 2012 20:49:32 -0400 4 | " Version: 1.0 5 | " 6 | " Long Description: 7 | " Sometimes, it's useful to line up text. Naturally, it's nicer to have the 8 | " computer do this for you, since aligning things by hand quickly becomes 9 | " unpleasant. While there are other plugins for aligning text, the ones I've 10 | " tried are either impossibly difficult to understand and use, or too simplistic 11 | " to handle complicated tasks. This plugin aims to make the easy things easy 12 | " and the hard things possible, without providing an unnecessarily obtuse 13 | " interface. It's still a work in progress, and criticisms are welcome. 14 | " 15 | " License: 16 | " Copyright (c) 2012, Matthew J. Wozniski 17 | " All rights reserved. 18 | " 19 | " Redistribution and use in source and binary forms, with or without 20 | " modification, are permitted provided that the following conditions are met: 21 | " * Redistributions of source code must retain the above copyright notice, 22 | " this list of conditions and the following disclaimer. 23 | " * Redistributions in binary form must reproduce the above copyright 24 | " notice, this list of conditions and the following disclaimer in the 25 | " documentation and/or other materials provided with the distribution. 26 | " * The names of the contributors may not be used to endorse or promote 27 | " products derived from this software without specific prior written 28 | " permission. 29 | " 30 | " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS 31 | " OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 32 | " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 33 | " NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, 34 | " INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35 | " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 36 | " OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 37 | " LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 38 | " NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 39 | " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | 41 | " Abort if running in vi-compatible mode or the user doesn't want us. 42 | if &cp || exists('g:tabular_loaded') 43 | if &cp && &verbose 44 | echo "Not loading Tabular in compatible mode." 45 | endif 46 | finish 47 | endif 48 | 49 | let g:tabular_loaded = 1 50 | 51 | " Stupid vimscript crap {{{1 52 | let s:savecpo = &cpo 53 | set cpo&vim 54 | 55 | " Private Things {{{1 56 | 57 | " Dictionary of command name to command 58 | let s:TabularCommands = {} 59 | 60 | " Generate tab completion list for :Tabularize {{{2 61 | " Return a list of commands that match the command line typed so far. 62 | " NOTE: Tries to handle commands with spaces in the name, but Vim doesn't seem 63 | " to handle that terribly well... maybe I should give up on that. 64 | function! s:CompleteTabularizeCommand(argstart, cmdline, cursorpos) 65 | let names = keys(s:TabularCommands) 66 | if exists("b:TabularCommands") 67 | let names += keys(b:TabularCommands) 68 | endif 69 | 70 | let cmdstart = substitute(a:cmdline, '^\s*\S\+\s*', '', '') 71 | 72 | return filter(names, 'v:val =~# ''^\V'' . escape(cmdstart, ''\'')') 73 | endfunction 74 | 75 | " Choose the proper command map from the given command line {{{2 76 | " Returns [ command map, command line with leading removed ] 77 | function! s:ChooseCommandMap(commandline) 78 | let map = s:TabularCommands 79 | let cmd = a:commandline 80 | 81 | if cmd =~# '^\s\+' 82 | if !exists('b:TabularCommands') 83 | let b:TabularCommands = {} 84 | endif 85 | let map = b:TabularCommands 86 | let cmd = substitute(cmd, '^\s\+', '', '') 87 | endif 88 | 89 | return [ map, cmd ] 90 | endfunction 91 | 92 | " Parse '/pattern/format' into separate pattern and format parts. {{{2 93 | " If parsing fails, return [ '', '' ] 94 | function! s:ParsePattern(string) 95 | if a:string[0] != '/' 96 | return ['',''] 97 | endif 98 | 99 | let pat = '\\\@ 0)] =~ '^\s*$' 141 | throw "Empty element" 142 | endif 143 | 144 | if end == -1 145 | let rv = [ a:string ] 146 | else 147 | let rv = [ a:string[0 : end-1] ] + s:SplitCommands(a:string[end+1 : -1]) 148 | endif 149 | 150 | return rv 151 | endfunction 152 | 153 | " Public Things {{{1 154 | 155 | " Command associating a command name with a simple pattern command {{{2 156 | " AddTabularPattern[!] [] name /pattern[/format] 157 | " 158 | " If is provided, the command will only be available in the current 159 | " buffer, and will be used instead of any global command with the same name. 160 | " 161 | " If a command with the same name and scope already exists, it is an error, 162 | " unless the ! is provided, in which case the existing command will be 163 | " replaced. 164 | " 165 | " pattern is a regex describing the delimiter to be used. 166 | " 167 | " format describes the format pattern to be used. The default will be used if 168 | " none is provided. 169 | com! -nargs=+ -bang AddTabularPattern 170 | \ call AddTabularPattern(, 0) 171 | 172 | function! AddTabularPattern(command, force) 173 | try 174 | let [ commandmap, rest ] = s:ChooseCommandMap(a:command) 175 | 176 | let name = matchstr(rest, '.\{-}\ze\s*/') 177 | let pattern = substitute(rest, '.\{-}\s*\ze/', '', '') 178 | 179 | let [ pattern, format ] = s:ParsePattern(pattern) 180 | 181 | if empty(name) || empty(pattern) 182 | throw "Invalid arguments!" 183 | endif 184 | 185 | if !a:force && has_key(commandmap, name) 186 | throw string(name) . " is already defined, use ! to overwrite." 187 | endif 188 | 189 | let command = "tabular#TabularizeStrings(a:lines, " . string(pattern) 190 | 191 | if !empty(format) 192 | let command .= ", " . string(format) 193 | endif 194 | 195 | let command .= ")" 196 | 197 | let commandmap[name] = { 'pattern' : pattern, 'commands' : [ command ] } 198 | catch 199 | echohl ErrorMsg 200 | echomsg "AddTabularPattern: " . v:exception 201 | echohl None 202 | endtry 203 | endfunction 204 | 205 | " Command associating a command name with a pipeline of functions {{{2 206 | " AddTabularPipeline[!] [] name /pattern/ func [ | func2 [ | func3 ] ] 207 | " 208 | " If is provided, the command will only be available in the current 209 | " buffer, and will be used instead of any global command with the same name. 210 | " 211 | " If a command with the same name and scope already exists, it is an error, 212 | " unless the ! is provided, in which case the existing command will be 213 | " replaced. 214 | " 215 | " pattern is a regex that will be used to determine which lines will be 216 | " filtered. If the cursor line doesn't match the pattern, using the command 217 | " will be a no-op, otherwise the cursor and all contiguous lines matching the 218 | " pattern will be filtered. 219 | " 220 | " Each 'func' argument represents a function to be called. This function 221 | " will have access to a:lines, a List containing one String per line being 222 | " filtered. 223 | com! -nargs=+ -bang AddTabularPipeline 224 | \ call AddTabularPipeline(, 0) 225 | 226 | function! AddTabularPipeline(command, force) 227 | try 228 | let [ commandmap, rest ] = s:ChooseCommandMap(a:command) 229 | 230 | let name = matchstr(rest, '.\{-}\ze\s*/') 231 | let pattern = substitute(rest, '.\{-}\s*\ze/', '', '') 232 | 233 | let commands = matchstr(pattern, '^/.\{-}\\\@CompleteTabularizeCommand 264 | \ Tabularize ,call Tabularize() 265 | 266 | function! Tabularize(command, ...) range 267 | let piperange_opt = {} 268 | if a:0 269 | let piperange_opt = a:1 270 | endif 271 | 272 | if empty(a:command) 273 | if !exists("s:last_tabularize_command") 274 | echohl ErrorMsg 275 | echomsg "Tabularize hasn't been called yet; no pattern/command to reuse!" 276 | echohl None 277 | return 278 | endif 279 | else 280 | let s:last_tabularize_command = a:command 281 | endif 282 | 283 | let command = s:last_tabularize_command 284 | 285 | let range = a:firstline . ',' . a:lastline 286 | 287 | try 288 | let [ pattern, format ] = s:ParsePattern(command) 289 | 290 | if !empty(pattern) 291 | let cmd = "tabular#TabularizeStrings(a:lines, " . string(pattern) 292 | 293 | if !empty(format) 294 | let cmd .= "," . string(format) 295 | endif 296 | 297 | let cmd .= ")" 298 | 299 | exe range . 'call tabular#PipeRangeWithOptions(pattern, [ cmd ], ' 300 | \ . 'piperange_opt)' 301 | else 302 | if exists('b:TabularCommands') && has_key(b:TabularCommands, command) 303 | let usercmd = b:TabularCommands[command] 304 | elseif has_key(s:TabularCommands, command) 305 | let usercmd = s:TabularCommands[command] 306 | else 307 | throw "Unrecognized command " . string(command) 308 | endif 309 | 310 | exe range . 'call tabular#PipeRangeWithOptions(usercmd["pattern"], ' 311 | \ . 'usercmd["commands"], piperange_opt)' 312 | endif 313 | catch 314 | echohl ErrorMsg 315 | echomsg "Tabularize: " . v:exception 316 | echohl None 317 | return 318 | endtry 319 | endfunction 320 | 321 | function! TabularizeHasPattern() 322 | return exists("s:last_tabularize_command") 323 | endfunction 324 | 325 | " GTabularize /pattern[/format] {{{2 326 | " GTabularize name 327 | " 328 | " Align text on only matching lines, either using the given pattern, or the 329 | " command associated with the given name. Mnemonically, this is similar to 330 | " the :global command, which takes some action on all rows matching a pattern 331 | " in a range. This command is different from normal :Tabularize in 3 ways: 332 | " 1) If a line in the range does not match the pattern, it will be left 333 | " unchanged, and not in any way affect the outcome of other lines in the 334 | " range (at least, normally - but Pipelines can and will still look at 335 | " non-matching rows unless they are specifically written to be aware of 336 | " tabular#DoGTabularize() and handle it appropriately). 337 | " 2) No automatic range determination - :Tabularize automatically expands 338 | " a single-line range (or a call with no range) to include all adjacent 339 | " matching lines. That behavior does not make sense for this command. 340 | " 3) If called without a range, it will act on all lines in the buffer (like 341 | " :global) rather than only a single line 342 | com! -nargs=* -range=% -complete=customlist,CompleteTabularizeCommand 343 | \ GTabularize , 344 | \ call Tabularize(, { 'mode': 'GTabularize' } ) 345 | 346 | " Stupid vimscript crap, part 2 {{{1 347 | let &cpo = s:savecpo 348 | unlet s:savecpo 349 | 350 | " vim:set sw=2 sts=2 fdm=marker: 351 | --------------------------------------------------------------------------------