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