1" Vim completion script
2" Language:    All languages, uses existing syntax highlighting rules
3" Maintainer:  David Fishburn <dfishburn dot vim at gmail dot com>
4" Version:     7.0
5" Last Change: 2010 Jul 29
6" Usage:       For detailed help, ":help ft-syntax-omni" 
7
8" History
9"
10" Version 7.0
11"     Updated syntaxcomplete#OmniSyntaxList()
12"         - Looking up the syntax groups defined from a syntax file
13"           looked for only 1 format of {filetype}GroupName, but some 
14"           syntax writers use this format as well:
15"               {b:current_syntax}GroupName
16"           OmniSyntaxList() will now check for both if the first
17"           method does not find a match.
18"
19" Version 6.0
20"     Added syntaxcomplete#OmniSyntaxList()
21"         - Allows other plugins to use this for their own 
22"           purposes.
23"         - It will return a List of all syntax items for the
24"           syntax group name passed in.  
25"         - XPTemplate for SQL will use this function via the 
26"           sqlcomplete plugin to populate a Choose box.
27"
28" Version 5.0
29"     Updated SyntaxCSyntaxGroupItems()
30"         - When processing a list of syntax groups, the final group
31"           was missed in function SyntaxCSyntaxGroupItems.
32"
33" Set completion with CTRL-X CTRL-O to autoloaded function.
34" This check is in place in case this script is
35" sourced directly instead of using the autoload feature. 
36if exists('+omnifunc')
37    " Do not set the option if already set since this
38    " results in an E117 warning.
39    if &omnifunc == ""
40        setlocal omnifunc=syntaxcomplete#Complete
41    endif
42endif
43
44if exists('g:loaded_syntax_completion')
45    finish 
46endif
47let g:loaded_syntax_completion = 70
48
49" Set ignorecase to the ftplugin standard
50" This is the default setting, but if you define a buffer local
51" variable you can override this on a per filetype.
52if !exists('g:omni_syntax_ignorecase')
53    let g:omni_syntax_ignorecase = &ignorecase
54endif
55
56" Indicates whether we should use the iskeyword option to determine
57" how to split words.
58" This is the default setting, but if you define a buffer local
59" variable you can override this on a per filetype.
60if !exists('g:omni_syntax_use_iskeyword')
61    let g:omni_syntax_use_iskeyword = 1
62endif
63
64" Only display items in the completion window that are at least
65" this many characters in length.
66" This is the default setting, but if you define a buffer local
67" variable you can override this on a per filetype.
68if !exists('g:omni_syntax_minimum_length')
69    let g:omni_syntax_minimum_length = 0
70endif
71
72" This script will build a completion list based on the syntax
73" elements defined by the files in $VIMRUNTIME/syntax.
74let s:syn_remove_words = 'match,matchgroup=,contains,'.
75            \ 'links to,start=,end=,nextgroup='
76
77let s:cache_name = []
78let s:cache_list = []
79let s:prepended  = ''
80
81" This function is used for the 'omnifunc' option.
82function! syntaxcomplete#Complete(findstart, base)
83
84    " Only display items in the completion window that are at least
85    " this many characters in length
86    if !exists('b:omni_syntax_ignorecase')
87        if exists('g:omni_syntax_ignorecase')
88            let b:omni_syntax_ignorecase = g:omni_syntax_ignorecase
89        else
90            let b:omni_syntax_ignorecase = &ignorecase
91        endif
92    endif
93
94    if a:findstart
95        " Locate the start of the item, including "."
96        let line = getline('.')
97        let start = col('.') - 1
98        let lastword = -1
99        while start > 0
100            " if line[start - 1] =~ '\S'
101            "     let start -= 1
102            " elseif line[start - 1] =~ '\.'
103            if line[start - 1] =~ '\k'
104                let start -= 1
105                let lastword = a:findstart
106            else
107                break
108            endif
109        endwhile
110
111        " Return the column of the last word, which is going to be changed.
112        " Remember the text that comes before it in s:prepended.
113        if lastword == -1
114            let s:prepended = ''
115            return start
116        endif
117        let s:prepended = strpart(line, start, (col('.') - 1) - start)
118        return start
119    endif
120
121    " let base = s:prepended . a:base
122    let base = s:prepended
123
124    let filetype = substitute(&filetype, '\.', '_', 'g')
125    let list_idx = index(s:cache_name, filetype, 0, &ignorecase)
126    if list_idx > -1
127        let compl_list = s:cache_list[list_idx]
128    else
129        let compl_list   = OmniSyntaxList()
130        let s:cache_name = add( s:cache_name,  filetype )
131        let s:cache_list = add( s:cache_list,  compl_list )
132    endif
133
134    " Return list of matches.
135
136    if base != ''
137        " let compstr    = join(compl_list, ' ')
138        " let expr       = (b:omni_syntax_ignorecase==0?'\C':'').'\<\%('.base.'\)\@!\w\+\s*'
139        " let compstr    = substitute(compstr, expr, '', 'g')
140        " let compl_list = split(compstr, '\s\+')
141
142        " Filter the list based on the first few characters the user
143        " entered
144        let expr = 'v:val '.(g:omni_syntax_ignorecase==1?'=~?':'=~#')." '^".escape(base, '\\/.*$^~[]').".*'"
145        let compl_list = filter(deepcopy(compl_list), expr)
146    endif
147
148    return compl_list
149endfunc
150
151function! syntaxcomplete#OmniSyntaxList(...)
152    if a:0 > 0
153        let parms = []
154        if 3 == type(a:1) 
155            let parms = a:1
156        elseif 1 == type(a:1)
157            let parms = split(a:1, ',')
158        endif
159        return OmniSyntaxList( parms )
160    else
161        return OmniSyntaxList()
162    endif
163endfunc
164
165function! OmniSyntaxList(...)
166    let list_parms = []
167    if a:0 > 0
168        if 3 == type(a:1) 
169            let list_parms = a:1
170        elseif 1 == type(a:1)
171            let list_parms = split(a:1, ',')
172        endif
173    endif
174
175    " Default to returning a dictionary, if use_dictionary is set to 0
176    " a list will be returned.
177    " let use_dictionary = 1
178    " if a:0 > 0 && a:1 != ''
179    "     let use_dictionary = a:1
180    " endif
181
182    " Only display items in the completion window that are at least
183    " this many characters in length
184    if !exists('b:omni_syntax_use_iskeyword')
185        if exists('g:omni_syntax_use_iskeyword')
186            let b:omni_syntax_use_iskeyword = g:omni_syntax_use_iskeyword
187        else
188            let b:omni_syntax_use_iskeyword = 1
189        endif
190    endif
191
192    " Only display items in the completion window that are at least
193    " this many characters in length
194    if !exists('b:omni_syntax_minimum_length')
195        if exists('g:omni_syntax_minimum_length')
196            let b:omni_syntax_minimum_length = g:omni_syntax_minimum_length
197        else
198            let b:omni_syntax_minimum_length = 0
199        endif
200    endif
201
202    let saveL = @l
203    let filetype = substitute(&filetype, '\.', '_', 'g')
204    
205    if empty(list_parms)
206        " Default the include group to include the requested syntax group
207        let syntax_group_include_{filetype} = ''
208        " Check if there are any overrides specified for this filetype
209        if exists('g:omni_syntax_group_include_'.filetype)
210            let syntax_group_include_{filetype} =
211                        \ substitute( g:omni_syntax_group_include_{filetype},'\s\+','','g') 
212            let list_parms = split(g:omni_syntax_group_include_{filetype}, ',')
213            if syntax_group_include_{filetype} =~ '\w'
214                let syntax_group_include_{filetype} = 
215                            \ substitute( syntax_group_include_{filetype}, 
216                            \ '\s*,\s*', '\\|', 'g'
217                            \ )
218            endif
219        endif
220    else
221        " A specific list was provided, use it
222    endif
223
224    " Loop through all the syntax groupnames, and build a
225    " syntax file which contains these names.  This can 
226    " work generically for any filetype that does not already
227    " have a plugin defined.
228    " This ASSUMES the syntax groupname BEGINS with the name
229    " of the filetype.  From my casual viewing of the vim7\syntax 
230    " directory this is true for almost all syntax definitions.
231    " As an example, the SQL syntax groups have this pattern:
232    "     sqlType
233    "     sqlOperators
234    "     sqlKeyword ...
235    redir @l
236    silent! exec 'syntax list '.join(list_parms)
237    redir END
238
239    let syntax_full = "\n".@l
240    let @l = saveL
241
242    if syntax_full =~ 'E28' 
243                \ || syntax_full =~ 'E411'
244                \ || syntax_full =~ 'E415'
245                \ || syntax_full =~ 'No Syntax items'
246        return []
247    endif
248
249    let filetype = substitute(&filetype, '\.', '_', 'g')
250
251    let list_exclude_groups = []
252    if a:0 > 0 
253        " Do nothing since we have specific a specific list of groups
254    else
255        " Default the exclude group to nothing
256        let syntax_group_exclude_{filetype} = ''
257        " Check if there are any overrides specified for this filetype
258        if exists('g:omni_syntax_group_exclude_'.filetype)
259            let syntax_group_exclude_{filetype} =
260                        \ substitute( g:omni_syntax_group_exclude_{filetype},'\s\+','','g') 
261            let list_exclude_groups = split(g:omni_syntax_group_exclude_{filetype}, ',')
262            if syntax_group_exclude_{filetype} =~ '\w' 
263                let syntax_group_exclude_{filetype} = 
264                            \ substitute( syntax_group_exclude_{filetype}, 
265                            \ '\s*,\s*', '\\|', 'g'
266                            \ )
267            endif
268        endif
269    endif
270
271    " Sometimes filetypes can be composite names, like c.doxygen
272    " Loop through each individual part looking for the syntax
273    " items specific to each individual filetype.
274    let syn_list = ''
275    let ftindex  = 0
276    let ftindex  = match(&filetype, '\w\+', ftindex)
277
278    while ftindex > -1
279        let ft_part_name = matchstr( &filetype, '\w\+', ftindex )
280
281        " Syntax rules can contain items for more than just the current 
282        " filetype.  They can contain additional items added by the user
283        " via autocmds or their vimrc.
284        " Some syntax files can be combined (html, php, jsp).
285        " We want only items that begin with the filetype we are interested in.
286        let next_group_regex = '\n' .
287                    \ '\zs'.ft_part_name.'\w\+\ze'.
288                    \ '\s\+xxx\s\+' 
289        let index    = 0
290        let index    = match(syntax_full, next_group_regex, index)
291
292        if index == -1 && exists('b:current_syntax') && ft_part_name != b:current_syntax
293            " There appears to be two standards when writing syntax files.
294            " Either items begin as:
295            "     syn keyword {filetype}Keyword         values ...
296            "     let b:current_syntax = "sql"
297            "     let b:current_syntax = "sqlanywhere"
298            " Or
299            "     syn keyword {syntax_filename}Keyword  values ...
300            "     let b:current_syntax = "mysql"
301            " So, we will make the format of finding the syntax group names
302            " a bit more flexible and look for both if the first fails to 
303            " find a match.
304            let next_group_regex = '\n' .
305                        \ '\zs'.b:current_syntax.'\w\+\ze'.
306                        \ '\s\+xxx\s\+' 
307            let index    = 0
308            let index    = match(syntax_full, next_group_regex, index)
309        endif
310
311        while index > -1
312            let group_name = matchstr( syntax_full, '\w\+', index )
313
314            let get_syn_list = 1
315            for exclude_group_name in list_exclude_groups
316                if '\<'.exclude_group_name.'\>' =~ '\<'.group_name.'\>'
317                    let get_syn_list = 0
318                endif
319            endfor
320        
321            " This code is no longer needed in version 6.0 since we have
322            " augmented the syntax list command to only retrieve the syntax 
323            " groups we are interested in.
324            "
325            " if get_syn_list == 1
326            "     if syntax_group_include_{filetype} != ''
327            "         if '\<'.syntax_group_include_{filetype}.'\>' !~ '\<'.group_name.'\>'
328            "             let get_syn_list = 0
329            "         endif
330            "     endif
331            " endif
332
333            if get_syn_list == 1
334                " Pass in the full syntax listing, plus the group name we 
335                " are interested in.
336                let extra_syn_list = s:SyntaxCSyntaxGroupItems(group_name, syntax_full)
337                let syn_list = syn_list . extra_syn_list . "\n"
338            endif
339
340            let index = index + strlen(group_name)
341            let index = match(syntax_full, next_group_regex, index)
342        endwhile
343
344        let ftindex  = ftindex + len(ft_part_name)
345        let ftindex  = match( &filetype, '\w\+', ftindex )
346    endwhile
347
348    " Convert the string to a List and sort it.
349    let compl_list = sort(split(syn_list))
350
351    if &filetype == 'vim'
352        let short_compl_list = []
353        for i in range(len(compl_list))
354            if i == len(compl_list)-1
355                let next = i
356            else
357                let next = i + 1
358            endif
359            if  compl_list[next] !~ '^'.compl_list[i].'.$'
360                let short_compl_list += [compl_list[i]]
361            endif
362        endfor
363
364        return short_compl_list
365    else
366        return compl_list
367    endif
368endfunction
369
370function! s:SyntaxCSyntaxGroupItems( group_name, syntax_full )
371
372    let syn_list = ""
373
374    " From the full syntax listing, strip out the portion for the
375    " request group.
376    " Query:
377    "     \n           - must begin with a newline
378    "     a:group_name - the group name we are interested in
379    "     \s\+xxx\s\+  - group names are always followed by xxx
380    "     \zs          - start the match
381    "     .\{-}        - everything ...
382    "     \ze          - end the match
383    "     \(           - start a group or 2 potential matches
384    "     \n\w         - at the first newline starting with a character
385    "     \|           - 2nd potential match
386    "     \%$          - matches end of the file or string
387    "     \)           - end a group
388    let syntax_group = matchstr(a:syntax_full, 
389                \ "\n".a:group_name.'\s\+xxx\s\+\zs.\{-}\ze\(\n\w\|\%$\)'
390                \ )
391
392    if syntax_group != ""
393        " let syn_list = substitute( @l, '^.*xxx\s*\%(contained\s*\)\?', "", '' )
394        " let syn_list = substitute( @l, '^.*xxx\s*', "", '' )
395
396        " We only want the words for the lines begining with
397        " containedin, but there could be other items.
398        
399        " Tried to remove all lines that do not begin with contained
400        " but this does not work in all cases since you can have
401        "    contained nextgroup=...
402        " So this will strip off the ending of lines with known
403        " keywords.
404        let syn_list = substitute( 
405                    \    syntax_group, '\<\('.
406                    \    substitute(
407                    \      escape(s:syn_remove_words, '\\/.*$^~[]')
408                    \      , ',', '\\|', 'g'
409                    \    ).
410                    \    '\).\{-}\%($\|'."\n".'\)'
411                    \    , "\n", 'g' 
412                    \  )
413
414        " Now strip off the newline + blank space + contained
415        let syn_list = substitute( 
416                    \    syn_list, '\%(^\|\n\)\@<=\s*\<\(contained\)'
417                    \    , "", 'g' 
418                    \ )
419
420        if b:omni_syntax_use_iskeyword == 0
421            " There are a number of items which have non-word characters in
422            " them, *'T_F1'*.  vim.vim is one such file.
423            " This will replace non-word characters with spaces.
424            let syn_list = substitute( syn_list, '[^0-9A-Za-z_ ]', ' ', 'g' )
425        else
426            let accept_chars = ','.&iskeyword.','
427            " Remove all character ranges
428            " let accept_chars = substitute(accept_chars, ',[^,]\+-[^,]\+,', ',', 'g')
429            let accept_chars = substitute(accept_chars, ',\@<=[^,]\+-[^,]\+,', '', 'g')
430            " Remove all numeric specifications
431            " let accept_chars = substitute(accept_chars, ',\d\{-},', ',', 'g')
432            let accept_chars = substitute(accept_chars, ',\@<=\d\{-},', '', 'g')
433            " Remove all commas
434            let accept_chars = substitute(accept_chars, ',', '', 'g')
435            " Escape special regex characters
436            let accept_chars = escape(accept_chars, '\\/.*$^~[]' )
437            " Remove all characters that are not acceptable
438            let syn_list = substitute( syn_list, '[^0-9A-Za-z_ '.accept_chars.']', ' ', 'g' )
439        endif
440
441        if b:omni_syntax_minimum_length > 0
442            " If the user specified a minimum length, enforce it
443            let syn_list = substitute(' '.syn_list.' ', ' \S\{,'.b:omni_syntax_minimum_length.'}\ze ', ' ', 'g')
444        endif
445    else
446        let syn_list = ''
447    endif
448
449    return syn_list
450endfunction
451