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