49f24c3dc497e285be0be4574bd19d7e45d135c0
[profile.git] / .vim / autoload / textformat.vim
1 " Text formatter plugin for Vim text editor
2 "
3 " This plugin provides commands and key mappings to quickly align and format
4 " text. Text can be aligned to either left or right margin or justified to
5 " both margins or centered. The text formatting commands in this plugin are
6 " a bit different from those integrated to Vim.
7 "
8 " Version:    0.9
9 " Maintainer: Teemu Likonen <tlikonen@iki.fi>
10 " GetLatestVimScripts: 0 0 :AutoInstall: textformat.vim
11 "
12 " {{{ Copyright and license
13 "
14 " Copyright (C) 2008 Teemu Likonen <tlikonen@iki.fi>
15 "
16 " This program is free software; you can redistribute it and/or modify
17 " it under the terms of the GNU General Public License as published by
18 " the Free Software Foundation; either version 2 of the License, or
19 " (at your option) any later version.
20 "
21 " This program is distributed in the hope that it will be useful,
22 " but WITHOUT ANY WARRANTY; without even the implied warranty of
23 " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 " GNU General Public License for more details.
25 "
26 " You should have received a copy of the GNU General Public License
27 " along with this program; if not, write to the Free Software
28 " Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
29 "
30 " The License text in full:
31 "       http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
32 "
33 " }}}
34
35 " Constant variables(s) {{{1
36 let s:default_width = 80
37
38 function! s:Align_Range_Left(...) range "{{{1
39         if a:0 > 0 && a:1 >= 0
40                 " If [indent] is given align the first line accordingly
41                 let l:start_ws = repeat(' ',a:1)
42                 let l:line_replace = s:Align_String_Left(getline(a:firstline))
43                 call setline(a:firstline,l:start_ws.l:line_replace)
44         else
45                 " If [indent] is not given get the indent of the first line
46                 " (and possibly the second too in case fo+=2).
47                 let l:start_ws = substitute(getline(a:firstline),'\m\S.*','','')
48                 let l:line_replace = s:Align_String_Left(getline(a:firstline))
49                 call setline(a:firstline,l:start_ws.l:line_replace)
50                 if match(&formatoptions,'2') >= 0 && a:lastline > a:firstline
51                         let l:start_ws = substitute(getline(a:firstline+1),'\m\S.*','','')
52                 endif
53         endif
54         " Align the rest of the lines
55         for l:i in range(a:lastline-a:firstline)
56                 let l:line = a:firstline + 1 + l:i
57                 let l:line_replace = s:Align_String_Left(getline(l:line))
58                 call setline(l:line,l:start_ws.l:line_replace)
59         endfor
60 endfunction
61
62 function! s:Align_Range_Right(width) "{{{1
63         let l:line_replace = s:Align_String_Right(getline('.'),a:width)
64         if l:line_replace =~ '\v^ *$'
65                 " If line would be full of spaces just print empty line.
66                 call setline(line('.'),'')
67         else
68                 call setline(line('.'),l:line_replace)
69         endif
70 endfunction
71
72 function! s:Align_Range_Justify(width, ...) range "{{{1
73         " Get the indent of the first line.
74         let l:start_ws = substitute(getline(a:firstline),'\m\S.*','','')
75         " 'textwidth' minus indent to get the actual text area width
76         normal! ^
77         let l:width = a:width-virtcol('.')+1
78         let l:line_replace = substitute(l:start_ws.s:Align_String_Justify(getline(a:firstline),l:width),'\m\s*$','','')
79         call setline(a:firstline,l:line_replace)
80         " If fo+=2 and range is more than one line get the indent of the
81         " second line.
82         if match(&formatoptions,'2') >= 0 && a:lastline > a:firstline
83                 let l:start_ws = substitute(getline(a:firstline+1),'\m\S.*','','')
84                 execute a:firstline+1
85                 normal! ^
86                 let l:width = a:width-virtcol('.')+1
87         endif
88         " Justify all the lines in range
89         for l:i in range(a:lastline-a:firstline)
90                 let l:line = a:firstline + 1 + l:i
91                 if l:line == a:lastline && a:0
92                         " Align the last line to left
93                         call setline(l:line,l:start_ws.s:Truncate_Spaces(getline(l:line)))
94                 else
95                         " Other lines left-right justified
96                         let l:line_replace = substitute(l:start_ws.s:Align_String_Justify(getline(l:line),l:width),'\m\s*$','','')
97                         call setline(l:line,l:line_replace)
98                 endif
99         endfor
100 endfunction
101
102 function! s:Align_Range_Center(width) "{{{1
103         let l:line_replace = s:Truncate_Spaces(getline('.'))
104         let l:line_replace = s:Add_Double_Spacing(l:line_replace)
105         call setline(line('.'),l:line_replace)
106         execute 'center '.a:width
107 endfunction
108
109 function! s:Align_String_Left(string, ...) "{{{1
110         let l:string_replace = s:Truncate_Spaces(a:string)
111         let l:string_replace = s:Add_Double_Spacing(l:string_replace)
112         if a:0 && a:1
113                 " If optional width argument is given (and is non-zero) we pad
114                 " the rest of string with spaces. Currently this code path is
115                 " never needed.
116                 let l:string_width = s:String_Width(l:string_replace)
117                 let l:more_spaces = a:1-l:string_width
118                 return l:string_replace.repeat(' ',l:more_spaces)
119         else
120                 return l:string_replace
121         endif
122 endfunction
123
124 function! s:Align_String_Right(string, width) "{{{1
125         let l:string_replace = s:Truncate_Spaces(a:string)
126         let l:string_replace = s:Add_Double_Spacing(l:string_replace)
127         let l:string_width = s:String_Width(l:string_replace)
128         let l:more_spaces = a:width-l:string_width
129         return repeat(' ',l:more_spaces).l:string_replace
130 endfunction
131
132 function! s:Align_String_Justify(string, width) "{{{1
133         let l:string = s:Truncate_Spaces(a:string)
134         " If the parameter string is empty we can just return a line full of
135         " spaces. No need to go further.
136         if l:string =~ '\v^ *$'
137                 return repeat(' ',a:width)
138         endif
139         let l:string_width = s:String_Width(l:string)
140         if l:string_width >= a:width
141                 " The original string is longer than width so we can just
142                 " return the string. No need to go further.
143                 return l:string
144         endif
145
146         " This many extra spaces we need.
147         let l:more_spaces = a:width-l:string_width
148         " Convert the string to a list of words.
149         let l:word_list = split(l:string)
150         " This is the amount of spaces available in the original string (word
151         " count minus one).
152         let l:string_spaces = len(l:word_list)-1
153         " If there are no spaces there is only one word. We can just return
154         " the string with padded spaces. No need to go further.
155         if l:string_spaces == 0
156                 return l:string.repeat(' ',l:more_spaces)
157         endif
158         " Ok, there are more than one word in the string so we get to do some
159         " real work...
160
161         " Make a list which each item represent a space available in the
162         " string. The value means how many spaces there are. At the moment set
163         " every list item to one: [1, 1, 1, 1, ...]
164         let l:space_list = []
165         for l:item in range(l:string_spaces)
166                 let l:space_list += [1]
167         endfor
168
169         " Repeat while there are no more need to add any spaces.
170         while l:more_spaces > 0
171                 if l:more_spaces >= l:string_spaces
172                         " More extra spaces are needed than there are spaces
173                         " available in the string so we add one more space to
174                         " after every word (add 1 to items of space list).
175                         for l:i in range(l:string_spaces)
176                                 let l:space_list[l:i] += 1
177                         endfor
178                         let l:more_spaces -= l:string_spaces
179                         " And then another round... and a check if more spaces
180                         " are needed.
181                 else " l:more_spaces < l:string_spaces
182                         " This list tells where .?! characters are.
183                         let l:space_sentence_full = []
184                         " This list tells where ,:; characters are.
185                         let l:space_sentence_semi = []
186                         " And this is for the rest of spaces.
187                         let l:space_other = []
188                         " Now, find those things:
189                         for l:i in range(l:string_spaces)
190                                 if match(l:word_list[l:i],'\m\S[.?!]$') >= 0
191                                         let l:space_sentence_full += [l:i]
192                                 elseif match(l:word_list[l:i],'\m\S[,:;]$') >= 0
193                                         let l:space_sentence_semi += [l:i]
194                                 else
195                                         let l:space_other += [l:i]
196                                 endif
197                         endfor
198
199                         " First distribute spaces after .?!
200                         if l:more_spaces >= len(l:space_sentence_full)
201                                 " If we need more extra spaces than there are
202                                 " .?! spaces, just add one after every item.
203                                 for l:i in l:space_sentence_full
204                                         let l:space_list[l:i] += 1
205                                 endfor
206                                 let l:more_spaces -= len(l:space_sentence_full)
207                                 if l:more_spaces == 0 | break | endif
208                         else
209                                 " Distribute the rest of spaces evenly and
210                                 " break the loop. All the spaces are added.
211                                 for l:i in s:Distributed_Selection(l:space_sentence_full,l:more_spaces)
212                                         let l:space_list[l:i] +=1
213                                 endfor
214                                 break
215                         endif
216
217                         " Then distribute spaces after ,:;
218                         if l:more_spaces >= len(l:space_sentence_semi)
219                                 " If we need more extra spaces than there are
220                                 " ,:; spaces available, just add one after
221                                 " every item.
222                                 for l:i in l:space_sentence_semi
223                                         let l:space_list[l:i] += 1
224                                 endfor
225                                 let l:more_spaces -= len(l:space_sentence_semi)
226                                 if l:more_spaces == 0 | break | endif
227                         else
228                                 " Distribute the rest of spaces evenly and
229                                 " break the loop. All the spaces are added.
230                                 for l:i in s:Distributed_Selection(l:space_sentence_semi,l:more_spaces)
231                                         let l:space_list[l:i] +=1
232                                 endfor
233                                 break
234                         endif
235
236                         " Finally distribute spaces to other available
237                         " positions and exit the loop.
238                         for l:i in s:Distributed_Selection(l:space_other,l:more_spaces)
239                                 let l:space_list[l:i] +=1
240                         endfor
241                         break
242                 endif
243         endwhile
244
245         " Now we now where all the extra spaces will go. We have to construct
246         " the string again.
247         let l:string = ''
248         for l:item in range(l:string_spaces)
249                 let l:string .= l:word_list[l:item].repeat(' ',l:space_list[l:item])
250         endfor
251         " Add the last word to the and and return the string.
252         return l:string.l:word_list[-1]
253 endfunction
254
255 function! s:Truncate_Spaces(string) "{{{1
256         let l:string = substitute(a:string,'\v\s+',' ','g')
257         let l:string = substitute(l:string,'\m^\s*','','')
258         let l:string = substitute(l:string,'\m\s*$','','')
259         return l:string
260 endfunction
261
262 function! s:String_Width(string) "{{{1
263         " This counts the string width in characters. Combining diacritical
264         " marks do not count so the base character with all the combined
265         " diacritics is just one character (which is good for our purposes).
266         " Double-wide characters will not get double width so unfortunately
267         " they don't work in our algorithm.
268         return strlen(substitute(a:string,'\m.','x','g'))
269 endfunction
270
271 function! s:Add_Double_Spacing(string) "{{{1
272         if &joinspaces
273                 return substitute(a:string,'\m\S[.?!] ','& ','g')
274         else
275                 return a:string
276         endif
277 endfunction
278
279 function! s:Distributed_Selection(list, pick) "{{{1
280         " 'list' is a list-type variable [ item1, item2, ... ]
281         " 'pick' is a number how many of the list's items we want to choose
282         "
283         " This function returns a list which has 'pick' number of items from
284         " the original list. Items are choosed in distributed manner. For
285         " example, if 'pick' is 1 then the algorithm chooses an item near the
286         " center of the 'list'. If 'pick' is 2 then the first one is about 1/3
287         " from the begining and the another one about 2/3 from the begining.
288
289         " l:pick_list is a list of 0's and 1's and its length will be the
290         " same as original list's. Number 1 means that this list item will be
291         " picked and 0 means that the item will be dropped. Finally
292         " l:pick_list could look like this: [0, 1, 0, 1, 0]
293         " (i.e., two items evenly picked from a list of five items)
294         let l:pick_list = []
295
296         " First pick items evenly from the begining of the list. This also
297         " actually constructs the list.
298         let l:div1 = len(a:list) / a:pick
299         let l:mod1 = len(a:list) % a:pick
300         for l:i in range(len(a:list)-l:mod1)
301                 if !eval(l:i%l:div1)
302                         let l:pick_list += [1]
303                 else
304                         let l:pick_list += [0]
305                 endif
306         endfor
307
308         if l:mod1 > 0
309                 " The division wasn't even so we get the remaining items and
310                 " distribute them evenly again to the list.
311                 let l:div2 = len(l:pick_list) / l:mod1
312                 let l:mod2 = len(l:pick_list) % l:mod1
313                 for l:i in range(len(l:pick_list)-l:mod2)
314                         if !eval(l:i%l:div2)
315                                 call insert(l:pick_list,0,l:i)
316                         endif
317                 endfor
318         endif
319
320         " There may be very different number of zeros in the begining and end
321         " of the list. We count them.
322         let l:zeros_begin = 0
323         for l:i in l:pick_list
324                 if l:i == 0
325                         let l:zeros_begin += 1
326                 else
327                         break
328                 endif
329         endfor
330         let l:zeros_end = 0
331         for l:i in reverse(copy(l:pick_list))
332                 if l:i == 0
333                         let l:zeros_end += 1
334                 else
335                         break
336                 endif
337         endfor
338
339         " Then we remove them.
340         if l:zeros_end
341                 " Remove 0 items from the end. We need to remove them first
342                 " from the end because list items' index number will change
343                 " when items are removed from the begining. Then it would make
344                 " a bit more difficult to remove ending spaces.
345                 call remove(l:pick_list,len(l:pick_list)-l:zeros_end,-1)
346         endif
347         if l:zeros_begin
348                 " Remove 0 items from the begining.
349                 call remove(l:pick_list,0,l:zeros_begin-1)
350         endif
351         let l:zeros_both = l:zeros_begin + l:zeros_end
352
353         " Put even amount of zeros to begining and end
354         for l:i in range(l:zeros_both/2)
355                 call insert(l:pick_list,0,0)
356         endfor
357         for l:i in range((l:zeros_both/2)+(l:zeros_both%2))
358                 call add(l:pick_list,0)
359         endfor
360
361         " Finally construct and return a new list which has only the items we
362         " have chosen.
363         let l:new_list = []
364         for l:i in range(len(l:pick_list))
365                 if l:pick_list[l:i] == 1
366                         let l:new_list += [a:list[l:i]]
367                 endif
368         endfor
369         return l:new_list
370 endfunction
371
372 function! textformat#Quick_Align_Left() "{{{1
373         let l:pos = getpos('.')
374         let l:autoindent = &autoindent
375         let l:formatoptions = &formatoptions
376         setlocal autoindent formatoptions-=w
377         silent normal! vip:call s:Align_Range_Left()\r
378         silent normal! gwip
379         call setpos('.',l:pos)
380         let &l:formatoptions = l:formatoptions
381         let &l:autoindent = l:autoindent
382 endfunction
383
384 function! textformat#Quick_Align_Right() "{{{1
385         let l:width = &textwidth
386         if l:width == 0 | let l:width = s:default_width | endif
387         let l:pos = getpos('.')
388         silent normal! vip:call s:Align_Range_Right(l:width)\r
389         call setpos('.',l:pos)
390 endfunction
391
392 function! textformat#Quick_Align_Justify() "{{{1
393         let l:width = &textwidth
394         if l:width == 0 | let l:width = s:default_width  | endif
395         let l:pos = getpos('.')
396         let l:joinspaces = &joinspaces
397         setlocal nojoinspaces
398         call textformat#Quick_Align_Left()
399         let &l:joinspaces = l:joinspaces
400         silent normal! vip:call s:Align_Range_Justify(l:width,1)\r
401         call setpos('.',l:pos)
402 endfunction
403
404 function! textformat#Quick_Align_Center() "{{{1
405         let l:width = &textwidth
406         " It's not good idea to use tabs in text area which has very different
407         " number of spaces before the lines. Plain spaces are more reliable so
408         " we set 'expandtab' variable before the operation.
409         let l:expandtab = &expandtab
410         setlocal expandtab
411         if l:width == 0 | let l:width = s:default_width  | endif
412         let l:pos = getpos('.')
413         silent normal! vip:call s:Align_Range_Center(l:width)\r
414         call setpos('.',l:pos)
415         let &l:expandtab = l:expandtab
416 endfunction
417
418 function! textformat#Align_Command(align, ...) range "{{{1
419         " For the left align the optional parameter a:1 is [indent]. For
420         " others it's [width].
421         if a:align == 'left'
422                 if a:0 && a:1 >= 0
423                         execute a:firstline.','.a:lastline.'call s:Align_Range_Left('.a:1.')'
424                 else
425                         execute a:firstline.','.a:lastline.'call s:Align_Range_Left()'
426                 endif
427         else
428                 if a:0 && a:1 > 0
429                         let l:width = a:1
430                 elseif &textwidth
431                         let l:width = &textwidth
432                 else
433                         let l:width = s:default_width
434                 endif
435
436                 if a:align == 'right'
437                         execute a:firstline.','.a:lastline.'call s:Align_Range_Right('.l:width.')'
438                 elseif a:align == 'justify'
439                         execute a:firstline.','.a:lastline.'call s:Align_Range_Justify('.l:width.')'
440                 elseif a:align == 'center'
441                         " It's not good idea to use tabs in text area which
442                         " has very different number of spaces before the
443                         " lines. Plain spaces are more reliable so we set
444                         " 'expandtab' variable before the operation.
445                         let l:expandtab = &expandtab
446                         setlocal expandtab
447                         execute a:firstline.','.a:lastline.'call s:Align_Range_Center('.l:width.')'
448                         let &l:expandtab = l:expandtab
449                 endif
450         endif
451 endfunction
452
453 " vim600: fdm=marker