1" Function to left and rigt align text.
2"
3" Written by:	Preben "Peppe" Guldberg <c928400@student.dtu.dk>
4" Created:	980806 14:13 (or around that time anyway)
5" Revised:	001103 00:36 (See "Revisions" below)
6
7
8" function Justify( [ textwidth [, maxspaces [, indent] ] ] )
9"
10" Justify()  will  left  and  right  align  a  line  by  filling  in  an
11" appropriate amount of spaces.  Extra  spaces  are  added  to  existing
12" spaces starting from the right side of the line.  As an  example,  the
13" following documentation has been justified.
14"
15" The function takes the following arguments:
16
17" textwidth argument
18" ------------------
19" If not specified, the value of the 'textwidth'  option  is  used.   If
20" 'textwidth' is zero a value of 80 is used.
21"
22" Additionally the arguments 'tw' and '' are  accepted.   The  value  of
23" 'textwidth' will be used. These are handy, if you just want to specify
24" the maxspaces argument.
25
26" maxspaces argument
27" ------------------
28" If specified, alignment will only be done, if the  longest  space  run
29" after alignment is no longer than maxspaces.
30"
31" An argument of '' is accepted, should the user  like  to  specify  all
32" arguments.
33"
34" To aid user defined commands, negative  values  are  accepted  aswell.
35" Using a negative value specifies the default behaviour: any length  of
36" space runs will be used to justify the text.
37
38" indent argument
39" ---------------
40" This argument specifies how a line should be indented. The default  is
41" to keep the current indentation.
42"
43" Negative  values:  Keep  current   amount   of   leading   whitespace.
44" Positive values: Indent all lines with leading whitespace  using  this
45" amount of whitespace.
46"
47" Note that the value 0, needs to be quoted as  a  string.   This  value
48" leads to a left flushed text.
49"
50" Additionally units of  'shiftwidth'/'sw'  and  'tabstop'/'ts'  may  be
51" added. In this case, if the value of indent is positive, the amount of
52" whitespace to be  added  will  be  multiplied  by  the  value  of  the
53" 'shiftwidth' and 'tabstop' settings.  If these  units  are  used,  the
54"  argument must  be  given  as  a  string,  eg.   Justify('','','2sw').
55"
56" If the values of 'sw' or 'tw' are negative, they  are  treated  as  if
57" they were 0, which means that the text is flushed left.  There  is  no
58" check if a negative number prefix is used to  change  the  sign  of  a
59" negative 'sw' or 'ts' value.
60"
61" As with the other arguments,  ''  may  be  used  to  get  the  default
62" behaviour.
63
64
65" Notes:
66"
67" If the line, adjusted for space runs and leading/trailing  whitespace,
68" is wider than the used textwidth, the line will be left untouched  (no
69" whitespace removed).  This should be equivalent to  the  behaviour  of
70" :left, :right and :center.
71"
72" If the resulting line is shorter than the used textwidth  it  is  left
73" untouched.
74"
75" All space runs in the line  are  truncated  before  the  alignment  is
76" carried out.
77"
78" If you have set 'noexpandtab', :retab!  is used to replace space  runs
79"  with whitespace  using  the  value  of  'tabstop'.   This  should  be
80" conformant with :left, :right and :center.
81"
82" If joinspaces is set, an extra space is added after '.', '?' and  '!'.
83" If 'cpooptions' include 'j', extra space  is  only  added  after  '.'.
84" (This may on occasion conflict with maxspaces.)
85
86
87" Related mappings:
88"
89" Mappings that will align text using the current text width,  using  at
90" most four spaces in a  space  run  and  keeping  current  indentation.
91nmap _j :%call Justify('tw',4)<CR>
92vmap _j :call Justify('tw',4)<CR>
93"
94" Mappings that will remove space runs and format lines (might be useful
95" prior to aligning the text).
96nmap ,gq :%s/\s\+/ /g<CR>gq1G
97vmap ,gq :s/\s\+/ /g<CR>gvgq
98
99
100" User defined command:
101"
102" The following is an ex command that works as a shortcut to the Justify
103" function.  Arguments to Justify() can  be  added  after  the  command.
104com! -range -nargs=* Justify <line1>,<line2>call Justify(<f-args>)
105"
106" The following commands are all equivalent:
107"
108" 1. Simplest use of Justify():
109"       :call Justify()
110"       :Justify
111"
112" 2. The _j mapping above via the ex command:
113"       :%Justify tw 4
114"
115" 3.  Justify  visualised  text  at  72nd  column  while  indenting  all
116" previously indented text two shiftwidths
117"       :'<,'>call Justify(72,'','2sw')
118"       :'<,'>Justify 72 -1 2sw
119"
120" This documentation has been justified  using  the  following  command:
121":se et|kz|1;/^" function Justify(/+,'z-g/^" /s/^" //|call Justify(70,3)|s/^/" /
122
123" Revisions:
124" 001103: If 'joinspaces' was set, calculations could be wrong.
125"	  Tabs at start of line could also lead to errors.
126"	  Use setline() instead of "exec 's/foo/bar/' - safer.
127"	  Cleaned up the code a bit.
128"
129" Todo:	  Convert maps to the new script specific form
130
131" Error function
132function! Justify_error(message)
133    echohl Error
134    echo "Justify([tw, [maxspaces [, indent]]]): " . a:message
135    echohl None
136endfunction
137
138
139" Now for the real thing
140function! Justify(...) range
141
142    if a:0 > 3
143    call Justify_error("Too many arguments (max 3)")
144    return 1
145    endif
146
147    " Set textwidth (accept 'tw' and '' as arguments)
148    if a:0 >= 1
149	if a:1 =~ '^\(tw\)\=$'
150	    let tw = &tw
151	elseif a:1 =~ '^\d\+$'
152	    let tw = a:1
153	else
154	    call Justify_error("tw must be a number (>0), '' or 'tw'")
155	    return 2
156	endif
157    else
158	let tw = &tw
159    endif
160    if tw == 0
161	let tw = 80
162    endif
163
164    " Set maximum number of spaces between WORDs
165    if a:0 >= 2
166	if a:2 == ''
167	    let maxspaces = tw
168	elseif a:2 =~ '^-\d\+$'
169	    let maxspaces = tw
170	elseif a:2 =~ '^\d\+$'
171	    let maxspaces = a:2
172	else
173	    call Justify_error("maxspaces must be a number or ''")
174	    return 3
175	endif
176    else
177	let maxspaces = tw
178    endif
179    if maxspaces <= 1
180	call Justify_error("maxspaces should be larger than 1")
181	return 4
182    endif
183
184    " Set the indentation style (accept sw and ts units)
185    let indent_fix = ''
186    if a:0 >= 3
187	if (a:3 == '') || a:3 =~ '^-[1-9]\d*\(shiftwidth\|sw\|tabstop\|ts\)\=$'
188	    let indent = -1
189	elseif a:3 =~ '^-\=0\(shiftwidth\|sw\|tabstop\|ts\)\=$'
190	    let indent = 0
191	elseif a:3 =~ '^\d\+\(shiftwidth\|sw\|tabstop\|ts\)\=$'
192	    let indent = substitute(a:3, '\D', '', 'g')
193	elseif a:3 =~ '^\(shiftwidth\|sw\|tabstop\|ts\)$'
194	    let indent = 1
195	else
196	    call Justify_error("indent: a number with 'sw'/'ts' unit")
197	    return 5
198	endif
199	if indent >= 0
200	    while indent > 0
201		let indent_fix = indent_fix . ' '
202		let indent = indent - 1
203	    endwhile
204	    let indent_sw = 0
205	    if a:3 =~ '\(shiftwidth\|sw\)'
206		let indent_sw = &sw
207	    elseif a:3 =~ '\(tabstop\|ts\)'
208		let indent_sw = &ts
209	    endif
210	    let indent_fix2 = ''
211	    while indent_sw > 0
212		let indent_fix2 = indent_fix2 . indent_fix
213		let indent_sw = indent_sw - 1
214	    endwhile
215	    let indent_fix = indent_fix2
216	endif
217    else
218	let indent = -1
219    endif
220
221    " Avoid substitution reports
222    let save_report = &report
223    set report=1000000
224
225    " Check 'joinspaces' and 'cpo'
226    if &js == 1
227	if &cpo =~ 'j'
228	    let join_str = '\(\. \)'
229	else
230	    let join_str = '\([.!?!] \)'
231	endif
232    endif
233
234    let cur = a:firstline
235    while cur <= a:lastline
236
237	let str_orig = getline(cur)
238	let save_et = &et
239	set et
240	exec cur . "retab"
241	let &et = save_et
242	let str = getline(cur)
243
244	let indent_str = indent_fix
245	let indent_n = strlen(indent_str)
246	" Shall we remember the current indentation
247	if indent < 0
248	    let indent_orig = matchstr(str_orig, '^\s*')
249	    if strlen(indent_orig) > 0
250		let indent_str = indent_orig
251		let indent_n = strlen(matchstr(str, '^\s*'))
252	    endif
253	endif
254
255	" Trim trailing, leading and running whitespace
256	let str = substitute(str, '\s\+$', '', '')
257	let str = substitute(str, '^\s\+', '', '')
258	let str = substitute(str, '\s\+', ' ', 'g')
259	" Use substitute() hack to get strlen in characters instead of bytes
260	let str_n = strlen(substitute(str, '.', 'x', 'g'))
261
262	" Possible addition of space after punctuation
263	if exists("join_str")
264	    let str = substitute(str, join_str, '\1 ', 'g')
265	endif
266	let join_n = strlen(substitute(str, '.', 'x', 'g')) - str_n
267
268	" Can extraspaces be added?
269	" Note that str_n may be less than strlen(str) [joinspaces above]
270	if strlen(substitute(str, '.', 'x', 'g')) < tw - indent_n && str_n > 0
271	    " How many spaces should be added
272	    let s_add = tw - str_n - indent_n - join_n
273	    let s_nr  = strlen(substitute(str, '\S', '', 'g') ) - join_n
274	    let s_dup = s_add / s_nr
275	    let s_mod = s_add % s_nr
276
277	    " Test if the changed line fits with tw
278	    if 0 <= (str_n + (maxspaces - 1)*s_nr + indent_n) - tw
279
280		" Duplicate spaces
281		while s_dup > 0
282		    let str = substitute(str, '\( \+\)', ' \1', 'g')
283		    let s_dup = s_dup - 1
284		endwhile
285
286		" Add extra spaces from the end
287		while s_mod > 0
288		    let str = substitute(str, '\(\(\s\+\S\+\)\{' . s_mod .  '}\)$', ' \1', '')
289		    let s_mod = s_mod - 1
290		endwhile
291
292		" Indent the line
293		if indent_n > 0
294		    let str = substitute(str, '^', indent_str, '' )
295		endif
296
297		" Replace the line
298		call setline(cur, str)
299
300		" Convert to whitespace
301		if &et == 0
302		    exec cur . 'retab!'
303		endif
304
305	    endif   " Change of line
306	endif	" Possible change
307
308	let cur = cur + 1
309    endwhile
310
311    norm ^
312
313    let &report = save_report
314
315endfunction
316
317" EOF	vim: tw=78 ts=8 sw=4 sts=4 noet ai
318