summaryrefslogtreecommitdiff
path: root/runtime/autoload/sqlcomplete.vim
blob: 146cf2133b212b8cc5dd8fa8482032453c60b0f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
" Vim OMNI completion script for SQL
" Language:    SQL
" Maintainer:  David Fishburn <fishburn@ianywhere.com>
" Version:     4.0
" Last Change: Wed Apr 26 2006 3:00:06 PM
" Usage:       For detailed help
"              ":help sql.txt" 
"              or ":help ft-sql-omni" 
"              or read $VIMRUNTIME/doc/sql.txt

" Set completion with CTRL-X CTRL-O to autoloaded function.
" This check is in place in case this script is
" sourced directly instead of using the autoload feature. 
if exists('&omnifunc')
    " Do not set the option if already set since this
    " results in an E117 warning.
    if &omnifunc == ""
        setlocal omnifunc=sqlcomplete#Complete
    endif
endif

if exists('g:loaded_sql_completion')
    finish 
endif
let g:loaded_sql_completion = 40

" Maintains filename of dictionary
let s:sql_file_table        = ""
let s:sql_file_procedure    = ""
let s:sql_file_view         = ""

" Define various arrays to be used for caching
let s:tbl_name              = []
let s:tbl_alias             = []
let s:tbl_cols              = []
let s:syn_list              = []
let s:syn_value             = []
 
" Used in conjunction with the syntaxcomplete plugin
let s:save_inc              = ""
let s:save_exc              = ""
if exists('g:omni_syntax_group_include_sql')
    let s:save_inc = g:omni_syntax_group_include_sql
endif
if exists('g:omni_syntax_group_exclude_sql')
    let s:save_exc = g:omni_syntax_group_exclude_sql
endif
 
" Used with the column list
let s:save_prev_table       = ""

" Default the option to verify table alias
if !exists('g:omni_sql_use_tbl_alias')
    let g:omni_sql_use_tbl_alias = 'a'
endif
" Default syntax items to precache
if !exists('g:omni_sql_precache_syntax_groups')
    let g:omni_sql_precache_syntax_groups = [
                \ 'syntax',
                \ 'sqlKeyword',
                \ 'sqlFunction',
                \ 'sqlOption',
                \ 'sqlType',
                \ 'sqlStatement'
                \ ]
endif
" Set ignorecase to the ftplugin standard
if !exists('g:omni_sql_ignorecase')
    let g:omni_sql_ignorecase = &ignorecase
endif
" During table completion, should the table list also
" include the owner name
if !exists('g:omni_sql_include_owner')
    let g:omni_sql_include_owner = 0
    if exists('g:loaded_dbext')
        if g:loaded_dbext >= 300
            " New to dbext 3.00, by default the table lists include the owner
            " name of the table.  This is used when determining how much of
            " whatever has been typed should be replaced as part of the 
            " code replacement.
            let g:omni_sql_include_owner = 1
        endif
    endif
endif

" This function is used for the 'omnifunc' option.
function! sqlcomplete#Complete(findstart, base)

    " Default to table name completion
    let compl_type = 'table'
    " Allow maps to specify what type of object completion they want
    if exists('b:sql_compl_type')
        let compl_type = b:sql_compl_type
    endif

    " First pass through this function determines how much of the line should
    " be replaced by whatever is chosen from the completion list
    if a:findstart
        " Locate the start of the item, including "."
        let line     = getline('.')
        let start    = col('.') - 1
        let lastword = -1
        let begindot = 0
        " Check if the first character is a ".", for column completion
        if line[start - 1] == '.'
            let begindot = 1
        endif
        while start > 0
            if line[start - 1] =~ '\w'
                let start -= 1
            elseif line[start - 1] =~ '\.' && 
                        \ compl_type =~ 'column\|table\|view\|procedure'
                " If lastword has already been set for column completion
                " break from the loop, since we do not also want to pickup
                " a table name if it was also supplied.
                if lastword != -1 && compl_type == 'column'
                    break
                endif
                " If column completion was specified stop at the "." if
                " a . was specified, otherwise, replace all the way up
                " to the owner name (if included).
                if lastword == -1 && compl_type == 'column' && begindot == 1
                    let lastword = start
                endif
                " If omni_sql_include_owner = 0, do not include the table
                " name as part of the substitution, so break here
                if lastword == -1 && 
                            \ compl_type =~ 'table\|view\|procedure\column_csv' && 
                            \ g:omni_sql_include_owner == 0
                    let lastword = start
                    break
                endif
                let start -= 1
            else
                break
            endif
        endwhile

        " Return the column of the last word, which is going to be changed.
        " Remember the text that comes before it in s:prepended.
        if lastword == -1
            let s:prepended = ''
            return start
        endif
        let s:prepended = strpart(line, start, lastword - start)
        return lastword
    endif

    " Second pass through this function will determine what data to put inside
    " of the completion list
    " s:prepended is set by the first pass
    let base = s:prepended . a:base

    " Default the completion list to an empty list
    let compl_list = []

    " Default to table name completion
    let compl_type = 'table'
    " Allow maps to specify what type of object completion they want
    if exists('b:sql_compl_type')
        let compl_type = b:sql_compl_type
        unlet b:sql_compl_type
    endif

    if compl_type == 'tableReset'
        let compl_type = 'table'
        let base = ''
    endif

    if compl_type == 'table' ||
                \ compl_type == 'procedure' ||
                \ compl_type == 'view' 

        " This type of completion relies upon the dbext.vim plugin
        if s:SQLCCheck4dbext() == -1
            return []
        endif

        if s:sql_file_{compl_type} == ""
            let compl_type = substitute(compl_type, '\w\+', '\u&', '')
            let s:sql_file_{compl_type} = DB_getDictionaryName(compl_type)
        endif
        let s:sql_file_{compl_type} = DB_getDictionaryName(compl_type)
        if s:sql_file_{compl_type} != ""
            if filereadable(s:sql_file_{compl_type})
                let compl_list = readfile(s:sql_file_{compl_type})
                " let dic_list = readfile(s:sql_file_{compl_type})
                " if !empty(dic_list)
                "     for elem in dic_list
                "         let kind = (compl_type=='table'?'m':(compl_type=='procedure'?'f':'v'))
                "         let item = {'word':elem, 'menu':elem, 'kind':kind, 'info':compl_type}
                "         let compl_list += [item]
                "     endfor
                " endif
            endif
        endif
    elseif compl_type == 'column'

        " This type of completion relies upon the dbext.vim plugin
        if s:SQLCCheck4dbext() == -1
            return []
        endif

        if base == ""
            " The last time we displayed a column list we stored
            " the table name.  If the user selects a column list 
            " without a table name of alias present, assume they want
            " the previous column list displayed.
            let base = s:save_prev_table
        endif

        if base != ""
            let compl_list        = s:SQLCGetColumns(base, '')
            let s:save_prev_table = base
            let base              = ''
        endif
    elseif compl_type == 'column_csv'

        " This type of completion relies upon the dbext.vim plugin
        if s:SQLCCheck4dbext() == -1
            return []
        endif

        if base == ""
            " The last time we displayed a column list we stored
            " the table name.  If the user selects a column list 
            " without a table name of alias present, assume they want
            " the previous column list displayed.
            let base = s:save_prev_table
        endif

        if base != ""
            let compl_list        = s:SQLCGetColumns(base, 'csv')
            let s:save_prev_table = base
            " Join the column array into 1 single element array
            " but make the columns column separated
            let compl_list        = [join(compl_list, ', ')]
            let base              = ''
        endif
    elseif compl_type == 'resetCache'
        " Reset all cached items
        let s:tbl_name  = []
        let s:tbl_alias = []
        let s:tbl_cols  = []
        let s:syn_list  = []
        let s:syn_value = []

        let msg = "All SQL cached items have been removed."
        call s:SQLCWarningMsg(msg)
        " Leave time for the user to read the error message
        :sleep 2
    else
        let compl_list = s:SQLCGetSyntaxList(compl_type)
    endif

    if base != ''
        " Filter the list based on the first few characters the user
        " entered
        let expr = 'v:val '.(g:omni_sql_ignorecase==1?'=~?':'=~#').' "^'.base.'"'
        let compl_list = filter(deepcopy(compl_list), expr)
    endif

    if exists('b:sql_compl_savefunc') && b:sql_compl_savefunc != ""
        let &omnifunc = b:sql_compl_savefunc
    endif

    return compl_list
endfunc

function! sqlcomplete#PreCacheSyntax(...)
    let syn_group_arr = []
    if a:0 > 0 
        let syn_group_arr = a:1
    else
        let syn_group_arr = g:omni_sql_precache_syntax_groups
    endif
    if !empty(syn_group_arr)
        for group_name in syn_group_arr
            call s:SQLCGetSyntaxList(group_name)
        endfor
    endif
endfunction

function! sqlcomplete#Map(type)
    " Tell the SQL plugin what you want to complete
    let b:sql_compl_type=a:type
    " Record previous omnifunc, if the SQL completion
    " is being used in conjunction with other filetype
    " completion plugins
    if &omnifunc != "" && &omnifunc != 'sqlcomplete#Complete'
        " Record the previous omnifunc, the plugin
        " will automatically set this back so that it
        " does not interfere with other ftplugins settings
        let b:sql_compl_savefunc=&omnifunc
    endif
    " Set the OMNI func for the SQL completion plugin
    let &omnifunc='sqlcomplete#Complete'
endfunction

function! sqlcomplete#DrillIntoTable()
    " If the omni popup window is visible
    if pumvisible()
        call sqlcomplete#Map('column')
        " C-Y, makes the currently highlighted entry active
        " and trigger the omni popup to be redisplayed
        call feedkeys("\<C-Y>\<C-X>\<C-O>")
    else
        if has('win32')
            " If the popup is not visible, simple perform the normal
            " <C-Right> behaviour
            exec "normal! \<C-Right>"
        endif
    endif
    return ""
endfunction

function! sqlcomplete#DrillOutOfColumns()
    " If the omni popup window is visible
    if pumvisible()
        call sqlcomplete#Map('tableReset')
        " Trigger the omni popup to be redisplayed
        call feedkeys("\<C-X>\<C-O>")
    else
        if has('win32')
            " If the popup is not visible, simple perform the normal
            " <C-Left> behaviour
            exec "normal! \<C-Left>"
        endif
    endif
    return ""
endfunction

function! s:SQLCWarningMsg(msg)
    echohl WarningMsg
    echomsg a:msg 
    echohl None
endfunction
      
function! s:SQLCErrorMsg(msg)
    echohl ErrorMsg
    echomsg a:msg 
    echohl None
endfunction
      
function! s:SQLCGetSyntaxList(syn_group)
    let syn_group  = a:syn_group
    let compl_list = []

    " Check if we have already cached the syntax list
    let list_idx = index(s:syn_list, syn_group, 0, &ignorecase)
    if list_idx > -1
        " Return previously cached value
        let compl_list = s:syn_value[list_idx]
    else
        " Request the syntax list items from the 
        " syntax completion plugin
        if syn_group == 'syntax'
            " Handle this special case.  This allows the user
            " to indicate they want all the syntax items available,
            " so do not specify a specific include list.
            let g:omni_syntax_group_include_sql = ''
        else
            " The user has specified a specific syntax group
            let g:omni_syntax_group_include_sql = syn_group
        endif
        let g:omni_syntax_group_exclude_sql = ''
        let syn_value                       = OmniSyntaxList()
        let g:omni_syntax_group_include_sql = s:save_inc
        let g:omni_syntax_group_exclude_sql = s:save_exc
        " Cache these values for later use
        let s:syn_list  = add( s:syn_list,  syn_group )
        let s:syn_value = add( s:syn_value, syn_value )
        let compl_list  = syn_value
    endif

    return compl_list
endfunction

function! s:SQLCCheck4dbext()
    if !exists('g:loaded_dbext')
        let msg = "The dbext plugin must be loaded for dynamic SQL completion"
        call s:SQLCErrorMsg(msg)
        " Leave time for the user to read the error message
        :sleep 2
        return -1
    elseif g:loaded_dbext < 300
        let msg = "The dbext plugin must be at least version 3.00 " .
                    \ " for dynamic SQL completion"
        call s:SQLCErrorMsg(msg)
        " Leave time for the user to read the error message
        :sleep 2
        return -1
    endif
    return 1
endfunction

function! s:SQLCAddAlias(table_name, table_alias, cols)
    " Strip off the owner if included
    let table_name  = matchstr(a:table_name, '\%(.\{-}\.\)\?\zs\(.*\)' )
    let table_alias = a:table_alias
    let cols        = a:cols

    if g:omni_sql_use_tbl_alias != 'n' 
        if table_alias == ''
            if 'da' =~? g:omni_sql_use_tbl_alias
                if table_name =~ '_'
                    " Treat _ as separators since people often use these
                    " for word separators
                    let save_keyword = &iskeyword
                    setlocal iskeyword-=_

                    " Get the first letter of each word
                    " [[:alpha:]] is used instead of \w 
                    " to catch extended accented characters
                    "
                    let table_alias = substitute( 
                                \ table_name, 
                                \ '\<[[:alpha:]]\+\>_\?', 
                                \ '\=strpart(submatch(0), 0, 1)', 
                                \ 'g'
                                \ )
                    " Restore original value
                    let &iskeyword = save_keyword
                elseif table_name =~ '\u\U'
                    let table_alias = substitute(
                                \ table_name, '\(\u\)\U*', '\1', 'g')
                else
                    let table_alias = strpart(table_name, 0, 1)
                endif
            endif
        endif
        if table_alias != ''
            " Following a word character, make sure there is a . and no spaces
            let table_alias = substitute(table_alias, '\w\zs\.\?\s*$', '.', '')
            if 'a' =~? g:omni_sql_use_tbl_alias && a:table_alias == ''
                let table_alias = inputdialog("Enter table alias:", table_alias)
            endif
        endif
        if table_alias != ''
            let cols = substitute(cols, '\<\w', table_alias.'&', 'g')
        endif
    endif

    return cols
endfunction

function! s:SQLCGetColumns(table_name, list_type)
    let table_name   = matchstr(a:table_name, '^\w\+')
    let table_name   = matchstr(a:table_name, '^[a-zA-Z0-9_.]\+')
    let table_cols   = []
    let table_alias  = ''
    let move_to_top  = 1

    if g:loaded_dbext >= 300
        let saveSettingAlias = DB_listOption('use_tbl_alias')
        exec 'DBSetOption use_tbl_alias=n'
    endif

    " Check if we have already cached the column list for this table
    " by its name
    let list_idx = index(s:tbl_name, table_name, 0, &ignorecase)
    if list_idx > -1
        let table_cols = split(s:tbl_cols[list_idx])
    else
        " Check if we have already cached the column list for this table 
        " by its alias, assuming the table_name provided was actually
        " the alias for the table instead
        "     select *
        "       from area a
        "      where a.
        let list_idx = index(s:tbl_alias, table_name, 0, &ignorecase)
        if list_idx > -1
            let table_alias = table_name
            let table_name  = s:tbl_name[list_idx]
            let table_cols  = split(s:tbl_cols[list_idx])
        endif
    endif

    " If we have not found a cached copy of the table
    " And the table ends in a "." or we are looking for a column list
    " if list_idx == -1 && (a:table_name =~ '\.' || b:sql_compl_type =~ 'column')
    " if list_idx == -1 && (a:table_name =~ '\.' || a:list_type =~ 'csv')
    if list_idx == -1 
         let saveY      = @y
         let saveSearch = @/
         let saveWScan  = &wrapscan
         let curline    = line(".")
         let curcol     = col(".")

         " Do not let searchs wrap
         setlocal nowrapscan
         " If . was entered, look at the word just before the .
         " We are looking for something like this:
         "    select * 
         "      from customer c
         "     where c.
         " So when . is pressed, we need to find 'c'
         "

         " Search backwards to the beginning of the statement
         " and do NOT wrap
         " exec 'silent! normal! v?\<\(select\|update\|delete\|;\)\>'."\n".'"yy'
         exec 'silent! normal! ?\<\(select\|update\|delete\|;\)\>'."\n"

         " Start characterwise visual mode
         " Advance right one character
         " Search foward until one of the following:
         "     1.  Another select/update/delete statement
         "     2.  A ; at the end of a line (the delimiter)
         "     3.  The end of the file (incase no delimiter)
         " Yank the visually selected text into the "y register.
         exec 'silent! normal! vl/\(\<select\>\|\<update\>\|\<delete\>\|;\s*$\|\%$\)'."\n".'"yy'

         let query = @y
         let query = substitute(query, "\n", ' ', 'g')
         let found = 0

         " if query =~? '^\(select\|update\|delete\)'
         if query =~? '^\(select\)'
             let found = 1
             "  \(\(\<\w\+\>\)\.\)\?   - 
             " 'from.\{-}'  - Starting at the from clause
             " '\zs\(\(\<\w\+\>\)\.\)\?' - Get the owner name (optional)
             " '\<\w\+\>\ze' - Get the table name 
             " '\s\+\<'.table_name.'\>' - Followed by the alias
             " '\s*\.\@!.*'  - Cannot be followed by a .
             " '\(\<where\>\|$\)' - Must be followed by a WHERE clause
             " '.*'  - Exclude the rest of the line in the match
             let table_name_new = matchstr(@y, 
                         \ 'from.\{-}'.
                         \ '\zs\(\(\<\w\+\>\)\.\)\?'.
                         \ '\<\w\+\>\ze'.
                         \ '\s\+\%(as\s\+\)\?\<'.
                         \ matchstr(table_name, '.\{-}\ze\.\?$').
                         \ '\>'.
                         \ '\s*\.\@!.*'.
                         \ '\(\<where\>\|$\)'.
                         \ '.*'
                         \ )
             if table_name_new != ''
                 let table_alias = table_name
                 let table_name  = table_name_new

                 let list_idx = index(s:tbl_name, table_name, 0, &ignorecase)
                 if list_idx > -1
                     let table_cols  = split(s:tbl_cols[list_idx])
                     let s:tbl_name[list_idx]  = table_name
                     let s:tbl_alias[list_idx] = table_alias
                 else
                     let list_idx = index(s:tbl_alias, table_name, 0, &ignorecase)
                     if list_idx > -1
                         let table_cols = split(s:tbl_cols[list_idx])
                         let s:tbl_name[list_idx]  = table_name
                         let s:tbl_alias[list_idx] = table_alias
                     endif
                 endif

             endif
         else
             " Simply assume it is a table name provided with a . on the end
             let found = 1
         endif

         let @y        = saveY
         let @/        = saveSearch
         let &wrapscan = saveWScan

         " Return to previous location
         call cursor(curline, curcol)
         
         if found == 0
             if g:loaded_dbext > 300
                 exec 'DBSetOption use_tbl_alias='.saveSettingAlias
             endif

             " Not a SQL statement, do not display a list
             return []
         endif
    endif 

    if empty(table_cols)
        " Specify silent mode, no messages to the user (tbl, 1)
        " Specify do not comma separate (tbl, 1, 1)
        let table_cols_str = DB_getListColumn(table_name, 1, 1)

        if table_cols_str != ""
            let s:tbl_name  = add( s:tbl_name,  table_name )
            let s:tbl_alias = add( s:tbl_alias, table_alias )
            let s:tbl_cols  = add( s:tbl_cols,  table_cols_str )
            let table_cols  = split(table_cols_str)
        endif

    endif

    if g:loaded_dbext > 300
        exec 'DBSetOption use_tbl_alias='.saveSettingAlias
    endif

    " If the user has asked for a comma separate list of column
    " values, ask the user if they want to prepend each column
    " with a tablename alias.
    if a:list_type == 'csv' && !empty(table_cols)
        let cols       = join(table_cols, ', ')
        let cols       = s:SQLCAddAlias(table_name, table_alias, cols)
        let table_cols = [cols]
    endif

    return table_cols
endfunction