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
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                    \ 's: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                    \ 's: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                    \ 's: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 s: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 s:IsColComment(lnum, cnum)
207    let rc = synIDattr(synID(a:lnum, a:cnum, 0), "name")
208                \           =~? "comment"
209
210    return rc
211endfunction
212
213
214" Instead of returning a column position, return
215" an appropriate value as a factor of shiftwidth.
216function s:ModuloIndent(ind)
217    let ind = a:ind
218
219    if ind > 0
220        let modulo = ind % &shiftwidth
221
222        if modulo > 0
223            let ind = ind - modulo
224        endif
225    endif
226
227    return ind
228endfunction
229
230
231" Find correct indent of a new line based upon the previous line
232function GetSQLIndent()
233    let lnum = v:lnum
234    let ind = indent(lnum)
235
236    " If the current line is a comment, leave the indent as is
237    " Comment out this additional check since it affects the
238    " indenting of =, and will not reindent comments as it should
239    " if s:IsLineComment(lnum) == 1
240    "     return ind
241    " endif
242
243    " while 1
244        " Get previous non-blank line
245        let prevlnum = prevnonblank(lnum - 1)
246        if prevlnum <= 0
247            return ind
248        endif
249
250        if s:IsLineComment(prevlnum) == 1
251            if getline(v:lnum) =~ '^\s*\*'
252                let ind = s:ModuloIndent(indent(prevlnum))
253                return ind + 1
254            endif
255            " If the previous line is a comment, then return -1
256            " to tell Vim to use the formatoptions setting to determine
257            " the indent to use
258            " But only if the next line is blank.  This would be true if
259            " the user is typing, but it would not be true if the user
260            " is reindenting the file
261            if getline(v:lnum) =~ '^\s*$'
262                return -1
263            endif
264        endif
265
266    "     let prevline = getline(prevlnum)
267    "     if prevline !~ '^\s*$'
268    "         " echom 'previous non blank - break: ' . prevline
269    "         break
270    "     endif
271    " endwhile
272
273    " echom 'PREVIOUS INDENT: ' . indent(prevlnum) . '  LINE: ' . getline(prevlnum)
274
275    " This is the line you just hit return on, it is not the current line
276    " which is new and empty
277    " Based on this line, we can determine how much to indent the new
278    " line
279
280    " Get default indent (from prev. line)
281    let ind      = indent(prevlnum)
282    let prevline = getline(prevlnum)
283
284    " Now check what's on the previous line to determine if the indent
285    " should be changed, for example IF, BEGIN, should increase the indent
286    " where END IF, END, should decrease the indent.
287    if prevline =~? s:SQLBlockStart
288        " Move indent in
289        let ind = ind + &sw
290        " echom 'prevl - SQLBlockStart - indent ' . ind . '  line: ' . prevline
291    elseif prevline =~ '[()]'
292        if prevline =~ '('
293            let num_unmatched_left = s:CountUnbalancedParan( prevline, '(' )
294        else
295            let num_unmatched_left = 0
296        endif
297        if prevline =~ ')'
298            let num_unmatched_right  = s:CountUnbalancedParan( prevline, ')' )
299        else
300            let num_unmatched_right  = 0
301            " let num_unmatched_right  = s:CountUnbalancedParan( prevline, ')' )
302        endif
303        if num_unmatched_left > 0
304            " There is a open left paranethesis
305            " increase indent
306            let ind = ind + ( &sw * num_unmatched_left )
307        elseif num_unmatched_right > 0
308            " if it is an unbalanced paranethesis only unindent if
309            " it was part of a command (ie create table(..)  )
310            " instead of part of an if (ie if (....) then) which should
311            " maintain the indent level
312            let ignore = s:CheckToIgnoreRightParan( prevlnum, num_unmatched_right )
313            " echom 'prevl - ) unbalanced - CTIRP - ignore: ' . ignore
314
315            if prevline =~ '^\s*)'
316                let ignore = ignore + 1
317                " echom 'prevl - begins ) unbalanced ignore: ' . ignore
318            endif
319
320            if (num_unmatched_right - ignore) > 0
321                let ind = ind - ( &sw * (num_unmatched_right - ignore) )
322            endif
323
324        endif
325    endif
326
327
328    " echom 'CURRENT INDENT: ' . ind . '  LINE: '  . getline(v:lnum)
329
330    " This is a new blank line since we just typed a carriage return
331    " Check current line; search for simplistic matching start-of-block
332    let line = getline(v:lnum)
333
334    if line =~? '^\s*els'
335        " Any line when you type else will automatically back up one
336        " ident level  (ie else, elseif, elsif)
337        let ind = ind - &sw
338        " echom 'curr - else - indent ' . ind
339    elseif line =~? '^\s*end\>'
340        let ind = s:GetStmtStarterIndent('end', v:lnum)
341        " General case for end
342        " let ind = ind - &sw
343        " echom 'curr - end - indent ' . ind
344    elseif line =~? '^\s*when\>'
345        let ind = s:GetStmtStarterIndent('when', v:lnum)
346        " If the WHEN clause is used with a MERGE or EXCEPTION
347        " clause, do not change the indent level, since these
348        " statements do not have a corresponding END statement.
349        " if stmt_starter =~? 'case'
350        "    let ind = ind - &sw
351        " endif
352        " elseif line =~ '^\s*)\s*;\?\s*$'
353        " elseif line =~ '^\s*)'
354    elseif line =~ '^\s*)'
355        let num_unmatched_right  = s:CountUnbalancedParan( line, ')' )
356        let ignore = s:CheckToIgnoreRightParan( v:lnum, num_unmatched_right )
357        " If the line ends in a ), then reduce the indent
358        " This catches items like:
359        " CREATE TABLE T1(
360        "    c1 int,
361        "    c2 int
362        "    );
363        " But we do not want to unindent a line like:
364        " IF ( c1 = 1
365        " AND  c2 = 3 ) THEN
366        " let num_unmatched_right  = s:CountUnbalancedParan( line, ')' )
367        " if num_unmatched_right > 0
368        " elseif strpart( line, strlen(line)-1, 1 ) =~ ')'
369        " let ind = ind - &sw
370        if line =~ '^\s*)'
371            " let ignore = ignore + 1
372            " echom 'curr - begins ) unbalanced ignore: ' . ignore
373        endif
374
375        if (num_unmatched_right - ignore) > 0
376            let ind = ind - ( &sw * (num_unmatched_right - ignore) )
377        endif
378        " endif
379    endif
380
381    " echom 'final - indent ' . ind
382    return s:ModuloIndent(ind)
383endfunction
384
385" vim:sw=4:
386