1" Vim indent file
2" Language:    SQL
3" Maintainer:  David Fishburn <dfishburn dot vim at gmail dot com>
4" Last Change: 2012 Dec 05
5" Version:     3.0
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" History:
23"    3.0 (Dec 2012)
24"        Added cpo check
25"
26"    2.0
27"        Added the FOR keyword to SQLBlockStart to handle (Alec Tica):
28"            for i in 1..100 loop
29"              |<-- I expect to have indentation here
30"            end loop;
31"
32
33" Only load this indent file when no other was loaded.
34if exists("b:did_indent")
35    finish
36endif
37let b:did_indent     = 1
38let b:current_indent = "sqlanywhere"
39let s:keepcpo= &cpo
40set cpo&vim
41
42setlocal indentkeys-=0{
43setlocal indentkeys-=0}
44setlocal indentkeys-=:
45setlocal indentkeys-=0#
46setlocal indentkeys-=e
47
48" This indicates formatting should take place when one of these
49" expressions is used.  These expressions would normally be something
50" you would type at the BEGINNING of a line
51" SQL is generally case insensitive, so this files assumes that
52" These keywords are something that would trigger an indent LEFT, not
53" an indent right, since the SQLBlockStart is used for those keywords
54setlocal indentkeys+==~end,=~else,=~elseif,=~elsif,0=~when,0=)
55
56" GetSQLIndent is executed whenever one of the expressions
57" in the indentkeys is typed
58setlocal indentexpr=GetSQLIndent()
59
60" List of all the statements that start a new block.
61" These are typically words that start a line.
62" IS is excluded, since it is difficult to determine when the
63" ending block is (especially for procedures/functions).
64let s:SQLBlockStart = '^\s*\%('.
65                \ 'if\|else\|elseif\|elsif\|'.
66                \ 'while\|loop\|do\|for\|'.
67                \ 'begin\|'.
68                \ 'case\|when\|merge\|exception'.
69                \ '\)\>'
70let s:SQLBlockEnd = '^\s*\(end\)\>'
71
72" The indent level is also based on unmatched paranethesis
73" If a line has an extra "(" increase the indent
74" If a line has an extra ")" decrease the indent
75function! s:CountUnbalancedParan( line, paran_to_check )
76    let l = a:line
77    let lp = substitute(l, '[^(]', '', 'g')
78    let l = a:line
79    let rp = substitute(l, '[^)]', '', 'g')
80
81    if a:paran_to_check =~ ')'
82        " echom 'CountUnbalancedParan ) returning: ' .
83        " \ (strlen(rp) - strlen(lp))
84        return (strlen(rp) - strlen(lp))
85    elseif a:paran_to_check =~ '('
86        " echom 'CountUnbalancedParan ( returning: ' .
87        " \ (strlen(lp) - strlen(rp))
88        return (strlen(lp) - strlen(rp))
89    else
90        " echom 'CountUnbalancedParan unknown paran to check: ' .
91        " \ a:paran_to_check
92        return 0
93    endif
94endfunction
95
96" Unindent commands based on previous indent level
97function! s:CheckToIgnoreRightParan( prev_lnum, num_levels )
98    let lnum = a:prev_lnum
99    let line = getline(lnum)
100    let ends = 0
101    let num_right_paran = a:num_levels
102    let ignore_paran = 0
103    let vircol = 1
104
105    while num_right_paran > 0
106        silent! exec 'norm! '.lnum."G\<bar>".vircol."\<bar>"
107        let right_paran = search( ')', 'W' )
108        if right_paran != lnum
109            " This should not happen since there should be at least
110            " num_right_paran matches for this line
111            break
112        endif
113        let vircol      = virtcol(".")
114
115        " if getline(".") =~ '^)'
116        let matching_paran = searchpair('(', '', ')', 'bW',
117                    \ 's:IsColComment(line("."), col("."))')
118
119        if matching_paran < 1
120            " No match found
121            " echom 'CTIRP - no match found, ignoring'
122            break
123        endif
124
125        if matching_paran == lnum
126            " This was not an unmatched parantenses, start the search again
127            " again after this column
128            " echom 'CTIRP - same line match, ignoring'
129            continue
130        endif
131
132        " echom 'CTIRP - match: ' . line(".") . '  ' . getline(".")
133
134        if getline(matching_paran) =~? '\(if\|while\)\>'
135            " echom 'CTIRP - if/while ignored: ' . line(".") . '  ' . getline(".")
136            let ignore_paran = ignore_paran + 1
137        endif
138
139        " One match found, decrease and check for further matches
140        let num_right_paran = num_right_paran - 1
141
142    endwhile
143
144    " Fallback - just move back one
145    " return a:prev_indent - &sw
146    return ignore_paran
147endfunction
148
149" Based on the keyword provided, loop through previous non empty
150" non comment lines to find the statement that initated the keyword.
151" Return its indent level
152"    CASE ..
153"    WHEN ...
154" Should return indent level of CASE
155"    EXCEPTION ..
156"    WHEN ...
157"         something;
158"    WHEN ...
159" Should return indent level of exception.
160function! s:GetStmtStarterIndent( keyword, curr_lnum )
161    let lnum  = a:curr_lnum
162
163    " Default - reduce indent by 1
164    let ind = indent(a:curr_lnum) - &sw
165
166    if a:keyword =~? 'end'
167        exec 'normal! ^'
168        let stmts = '^\s*\%('.
169                    \ '\<begin\>\|' .
170                    \ '\%(\%(\<end\s\+\)\@<!\<loop\>\)\|' .
171                    \ '\%(\%(\<end\s\+\)\@<!\<case\>\)\|' .
172                    \ '\%(\%(\<end\s\+\)\@<!\<for\>\)\|' .
173                    \ '\%(\%(\<end\s\+\)\@<!\<if\>\)'.
174                    \ '\)'
175        let matching_lnum = searchpair(stmts, '', '\<end\>\zs', 'bW',
176                    \ 's:IsColComment(line("."), col(".")) == 1')
177        exec 'normal! $'
178        if matching_lnum > 0 && matching_lnum < a:curr_lnum
179            let ind = indent(matching_lnum)
180        endif
181    elseif a:keyword =~? 'when'
182        exec 'normal! ^'
183        let matching_lnum = searchpair(
184                    \ '\%(\<end\s\+\)\@<!\<case\>\|\<exception\>\|\<merge\>',
185                    \ '',
186                    \ '\%(\%(\<when\s\+others\>\)\|\%(\<end\s\+case\>\)\)',
187                    \ 'bW',
188                    \ 's:IsColComment(line("."), col(".")) == 1')
189        exec 'normal! $'
190        if matching_lnum > 0 && matching_lnum < a:curr_lnum
191            let ind = indent(matching_lnum)
192        else
193            let ind = indent(a:curr_lnum)
194        endif
195    endif
196
197    return ind
198endfunction
199
200
201" Check if the line is a comment
202function! s:IsLineComment(lnum)
203    let rc = synIDattr(
204                \ synID(a:lnum,
205                \     match(getline(a:lnum), '\S')+1, 0)
206                \ , "name")
207                \ =~? "comment"
208
209    return rc
210endfunction
211
212
213" Check if the column is a comment
214function! s:IsColComment(lnum, cnum)
215    let rc = synIDattr(synID(a:lnum, a:cnum, 0), "name")
216                \           =~? "comment"
217
218    return rc
219endfunction
220
221
222" Instead of returning a column position, return
223" an appropriate value as a factor of shiftwidth.
224function! s:ModuloIndent(ind)
225    let ind = a:ind
226
227    if ind > 0
228        let modulo = ind % &shiftwidth
229
230        if modulo > 0
231            let ind = ind - modulo
232        endif
233    endif
234
235    return ind
236endfunction
237
238
239" Find correct indent of a new line based upon the previous line
240function! GetSQLIndent()
241    let lnum = v:lnum
242    let ind = indent(lnum)
243
244    " If the current line is a comment, leave the indent as is
245    " Comment out this additional check since it affects the
246    " indenting of =, and will not reindent comments as it should
247    " if s:IsLineComment(lnum) == 1
248    "     return ind
249    " endif
250
251    " Get previous non-blank line
252    let prevlnum = prevnonblank(lnum - 1)
253    if prevlnum <= 0
254        return ind
255    endif
256
257    if s:IsLineComment(prevlnum) == 1
258        if getline(v:lnum) =~ '^\s*\*'
259            let ind = s:ModuloIndent(indent(prevlnum))
260            return ind + 1
261        endif
262        " If the previous line is a comment, then return -1
263        " to tell Vim to use the formatoptions setting to determine
264        " the indent to use
265        " But only if the next line is blank.  This would be true if
266        " the user is typing, but it would not be true if the user
267        " is reindenting the file
268        if getline(v:lnum) =~ '^\s*$'
269            return -1
270        endif
271    endif
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"  Restore:
386let &cpo= s:keepcpo
387unlet s:keepcpo
388" vim: ts=4 fdm=marker sw=4
389