1" Vim indent file
2" Language:    SQL
3" Maintainer:  David Fishburn <fishburn at ianywhere dot com>
4" Last Change: Mon Apr 02 2007 9:13:47 AM
5" Version:     1.5
6" Download:    http://vim.sourceforge.net/script.php?script_id=495
7
8" Notes:
9"    Indenting keywords are based on Oracle and Sybase Adaptive Server
10"    Anywhere (ASA).  Test indenting was done with ASA stored procedures and
11"    fuctions and Oracle packages which contain stored procedures and
12"    functions.
13"    This has not been tested against Microsoft SQL Server or
14"    Sybase Adaptive Server Enterprise (ASE) which use the Transact-SQL
15"    syntax.  That syntax does not have end tags for IF's, which makes
16"    indenting more difficult.
17"
18" Known Issues:
19"    The Oracle MERGE statement does not have an end tag associated with
20"    it, this can leave the indent hanging to the right one too many.
21
22" Only load this indent file when no other was loaded.
23if exists("b:did_indent")
24    finish
25endif
26let b:did_indent     = 1
27let b:current_indent = "sqlanywhere"
28
29setlocal indentkeys-=0{
30setlocal indentkeys-=0}
31setlocal indentkeys-=:
32setlocal indentkeys-=0#
33setlocal indentkeys-=e
34
35" This indicates formatting should take place when one of these
36" expressions is used.  These expressions would normally be something
37" you would type at the BEGINNING of a line
38" SQL is generally case insensitive, so this files assumes that
39" These keywords are something that would trigger an indent LEFT, not
40" an indent right, since the SQLBlockStart is used for those keywords
41setlocal indentkeys+==~end,=~else,=~elseif,=~elsif,0=~when,0=)
42
43" GetSQLIndent is executed whenever one of the expressions
44" in the indentkeys is typed
45setlocal indentexpr=GetSQLIndent()
46
47" Only define the functions once.
48if exists("*GetSQLIndent")
49    finish
50endif
51let s:keepcpo= &cpo
52set cpo&vim
53
54" List of all the statements that start a new block.
55" These are typically words that start a line.
56" IS is excluded, since it is difficult to determine when the
57" ending block is (especially for procedures/functions).
58let s:SQLBlockStart = '^\s*\%('.
59            \ 'if\|else\|elseif\|elsif\|'.
60                \ 'while\|loop\|do\|'.
61                \ 'begin\|'.
62                \ 'case\|when\|merge\|exception'.
63                \ '\)\>'
64let s:SQLBlockEnd = '^\s*\(end\)\>'
65
66" The indent level is also based on unmatched paranethesis
67" If a line has an extra "(" increase the indent
68" If a line has an extra ")" decrease the indent
69function s:CountUnbalancedParan( line, paran_to_check )
70    let l = a:line
71    let lp = substitute(l, '[^(]', '', 'g')
72    let l = a:line
73    let rp = substitute(l, '[^)]', '', 'g')
74
75    if a:paran_to_check =~ ')'
76        " echom 'CountUnbalancedParan ) returning: ' .
77        " \ (strlen(rp) - strlen(lp))
78        return (strlen(rp) - strlen(lp))
79    elseif a:paran_to_check =~ '('
80        " echom 'CountUnbalancedParan ( returning: ' .
81        " \ (strlen(lp) - strlen(rp))
82        return (strlen(lp) - strlen(rp))
83    else
84        " echom 'CountUnbalancedParan unknown paran to check: ' .
85        " \ a:paran_to_check
86        return 0
87    endif
88endfunction
89
90" Unindent commands based on previous indent level
91function s:CheckToIgnoreRightParan( prev_lnum, num_levels )
92    let lnum = a:prev_lnum
93    let line = getline(lnum)
94    let ends = 0
95    let num_right_paran = a:num_levels
96    let ignore_paran = 0
97    let vircol = 1
98
99    while num_right_paran > 0
100        silent! exec 'norm! '.lnum."G\<bar>".vircol."\<bar>"
101        let right_paran = search( ')', 'W' )
102        if right_paran != lnum
103            " This should not happen since there should be at least
104            " num_right_paran matches for this line
105            break
106        endif
107        let vircol      = virtcol(".")
108
109        " if getline(".") =~ '^)'
110        let matching_paran = searchpair('(', '', ')', 'bW',
111                    \ 's:IsColComment(line("."), col("."))')
112
113        if matching_paran < 1
114            " No match found
115            " echom 'CTIRP - no match found, ignoring'
116            break
117        endif
118
119        if matching_paran == lnum
120            " This was not an unmatched parantenses, start the search again
121            " again after this column
122            " echom 'CTIRP - same line match, ignoring'
123            continue
124        endif
125
126        " echom 'CTIRP - match: ' . line(".") . '  ' . getline(".")
127
128        if getline(matching_paran) =~? '\(if\|while\)\>'
129            " echom 'CTIRP - if/while ignored: ' . line(".") . '  ' . getline(".")
130            let ignore_paran = ignore_paran + 1
131        endif
132
133        " One match found, decrease and check for further matches
134        let num_right_paran = num_right_paran - 1
135
136    endwhile
137
138    " Fallback - just move back one
139    " return a:prev_indent - &sw
140    return ignore_paran
141endfunction
142
143" Based on the keyword provided, loop through previous non empty
144" non comment lines to find the statement that initated the keyword.
145" Return its indent level
146"    CASE ..
147"    WHEN ...
148" Should return indent level of CASE
149"    EXCEPTION ..
150"    WHEN ...
151"         something;
152"    WHEN ...
153" Should return indent level of exception.
154function s:GetStmtStarterIndent( keyword, curr_lnum )
155    let lnum  = a:curr_lnum
156
157    " Default - reduce indent by 1
158    let ind = indent(a:curr_lnum) - &sw
159
160    if a:keyword =~? 'end'
161        exec 'normal! ^'
162        let stmts = '^\s*\%('.
163                    \ '\<begin\>\|' .
164                    \ '\%(\%(\<end\s\+\)\@<!\<loop\>\)\|' .
165                    \ '\%(\%(\<end\s\+\)\@<!\<case\>\)\|' .
166                    \ '\%(\%(\<end\s\+\)\@<!\<for\>\)\|' .
167                    \ '\%(\%(\<end\s\+\)\@<!\<if\>\)'.
168                    \ '\)'
169        let matching_lnum = searchpair(stmts, '', '\<end\>\zs', 'bW',
170                    \ 's:IsColComment(line("."), col(".")) == 1')
171        exec 'normal! $'
172        if matching_lnum > 0 && matching_lnum < a:curr_lnum
173            let ind = indent(matching_lnum)
174        endif
175    elseif a:keyword =~? 'when'
176        exec 'normal! ^'
177        let matching_lnum = searchpair(
178                    \ '\%(\<end\s\+\)\@<!\<case\>\|\<exception\>\|\<merge\>',
179                    \ '',
180                    \ '\%(\%(\<when\s\+others\>\)\|\%(\<end\s\+case\>\)\)',
181                    \ 'bW',
182                    \ 's:IsColComment(line("."), col(".")) == 1')
183        exec 'normal! $'
184        if matching_lnum > 0 && matching_lnum < a:curr_lnum
185            let ind = indent(matching_lnum)
186        else
187            let ind = indent(a:curr_lnum)
188        endif
189    endif
190
191    return ind
192endfunction
193
194
195" Check if the line is a comment
196function s:IsLineComment(lnum)
197    let rc = synIDattr(
198                \ synID(a:lnum,
199                \     match(getline(a:lnum), '\S')+1, 0)
200                \ , "name")
201                \ =~? "comment"
202
203    return rc
204endfunction
205
206
207" Check if the column is a comment
208function s:IsColComment(lnum, cnum)
209    let rc = synIDattr(synID(a:lnum, a:cnum, 0), "name")
210                \           =~? "comment"
211
212    return rc
213endfunction
214
215
216" Instead of returning a column position, return
217" an appropriate value as a factor of shiftwidth.
218function s:ModuloIndent(ind)
219    let ind = a:ind
220
221    if ind > 0
222        let modulo = ind % &shiftwidth
223
224        if modulo > 0
225            let ind = ind - modulo
226        endif
227    endif
228
229    return ind
230endfunction
231
232
233" Find correct indent of a new line based upon the previous line
234function GetSQLIndent()
235    let lnum = v:lnum
236    let ind = indent(lnum)
237
238    " If the current line is a comment, leave the indent as is
239    " Comment out this additional check since it affects the
240    " indenting of =, and will not reindent comments as it should
241    " if s:IsLineComment(lnum) == 1
242    "     return ind
243    " endif
244
245    " while 1
246        " Get previous non-blank line
247        let prevlnum = prevnonblank(lnum - 1)
248        if prevlnum <= 0
249            return ind
250        endif
251
252        if s:IsLineComment(prevlnum) == 1
253            if getline(v:lnum) =~ '^\s*\*'
254                let ind = s:ModuloIndent(indent(prevlnum))
255                return ind + 1
256            endif
257            " If the previous line is a comment, then return -1
258            " to tell Vim to use the formatoptions setting to determine
259            " the indent to use
260            " But only if the next line is blank.  This would be true if
261            " the user is typing, but it would not be true if the user
262            " is reindenting the file
263            if getline(v:lnum) =~ '^\s*$'
264                return -1
265            endif
266        endif
267
268    "     let prevline = getline(prevlnum)
269    "     if prevline !~ '^\s*$'
270    "         " echom 'previous non blank - break: ' . prevline
271    "         break
272    "     endif
273    " endwhile
274
275    " echom 'PREVIOUS INDENT: ' . indent(prevlnum) . '  LINE: ' . getline(prevlnum)
276
277    " This is the line you just hit return on, it is not the current line
278    " which is new and empty
279    " Based on this line, we can determine how much to indent the new
280    " line
281
282    " Get default indent (from prev. line)
283    let ind      = indent(prevlnum)
284    let prevline = getline(prevlnum)
285
286    " Now check what's on the previous line to determine if the indent
287    " should be changed, for example IF, BEGIN, should increase the indent
288    " where END IF, END, should decrease the indent.
289    if prevline =~? s:SQLBlockStart
290        " Move indent in
291        let ind = ind + &sw
292        " echom 'prevl - SQLBlockStart - indent ' . ind . '  line: ' . prevline
293    elseif prevline =~ '[()]'
294        if prevline =~ '('
295            let num_unmatched_left = s:CountUnbalancedParan( prevline, '(' )
296        else
297            let num_unmatched_left = 0
298        endif
299        if prevline =~ ')'
300            let num_unmatched_right  = s:CountUnbalancedParan( prevline, ')' )
301        else
302            let num_unmatched_right  = 0
303            " let num_unmatched_right  = s:CountUnbalancedParan( prevline, ')' )
304        endif
305        if num_unmatched_left > 0
306            " There is a open left paranethesis
307            " increase indent
308            let ind = ind + ( &sw * num_unmatched_left )
309        elseif num_unmatched_right > 0
310            " if it is an unbalanced paranethesis only unindent if
311            " it was part of a command (ie create table(..)  )
312            " instead of part of an if (ie if (....) then) which should
313            " maintain the indent level
314            let ignore = s:CheckToIgnoreRightParan( prevlnum, num_unmatched_right )
315            " echom 'prevl - ) unbalanced - CTIRP - ignore: ' . ignore
316
317            if prevline =~ '^\s*)'
318                let ignore = ignore + 1
319                " echom 'prevl - begins ) unbalanced ignore: ' . ignore
320            endif
321
322            if (num_unmatched_right - ignore) > 0
323                let ind = ind - ( &sw * (num_unmatched_right - ignore) )
324            endif
325
326        endif
327    endif
328
329
330    " echom 'CURRENT INDENT: ' . ind . '  LINE: '  . getline(v:lnum)
331
332    " This is a new blank line since we just typed a carriage return
333    " Check current line; search for simplistic matching start-of-block
334    let line = getline(v:lnum)
335
336    if line =~? '^\s*els'
337        " Any line when you type else will automatically back up one
338        " ident level  (ie else, elseif, elsif)
339        let ind = ind - &sw
340        " echom 'curr - else - indent ' . ind
341    elseif line =~? '^\s*end\>'
342        let ind = s:GetStmtStarterIndent('end', v:lnum)
343        " General case for end
344        " let ind = ind - &sw
345        " echom 'curr - end - indent ' . ind
346    elseif line =~? '^\s*when\>'
347        let ind = s:GetStmtStarterIndent('when', v:lnum)
348        " If the WHEN clause is used with a MERGE or EXCEPTION
349        " clause, do not change the indent level, since these
350        " statements do not have a corresponding END statement.
351        " if stmt_starter =~? 'case'
352        "    let ind = ind - &sw
353        " endif
354        " elseif line =~ '^\s*)\s*;\?\s*$'
355        " elseif line =~ '^\s*)'
356    elseif line =~ '^\s*)'
357        let num_unmatched_right  = s:CountUnbalancedParan( line, ')' )
358        let ignore = s:CheckToIgnoreRightParan( v:lnum, num_unmatched_right )
359        " If the line ends in a ), then reduce the indent
360        " This catches items like:
361        " CREATE TABLE T1(
362        "    c1 int,
363        "    c2 int
364        "    );
365        " But we do not want to unindent a line like:
366        " IF ( c1 = 1
367        " AND  c2 = 3 ) THEN
368        " let num_unmatched_right  = s:CountUnbalancedParan( line, ')' )
369        " if num_unmatched_right > 0
370        " elseif strpart( line, strlen(line)-1, 1 ) =~ ')'
371        " let ind = ind - &sw
372        if line =~ '^\s*)'
373            " let ignore = ignore + 1
374            " echom 'curr - begins ) unbalanced ignore: ' . ignore
375        endif
376
377        if (num_unmatched_right - ignore) > 0
378            let ind = ind - ( &sw * (num_unmatched_right - ignore) )
379        endif
380        " endif
381    endif
382
383    " echom 'final - indent ' . ind
384    return s:ModuloIndent(ind)
385endfunction
386
387let &cpo = s:keepcpo
388unlet s:keepcpo
389
390" vim:sw=4:
391