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