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:     10.0
5" Last Change: 2012 Oct 20
6" Usage:       For detailed help, ":help ft-syntax-omni"
7
8" History
9"
10" Version 10.0
11"     Cycle through all the character ranges specified in the
12"     iskeyword option and build a list of valid word separators.
13"     Prior to this change, only actual characters were used,
14"     where for example ASCII "45" == "-".  If "45" were used
15"     in iskeyword the hyphen would not be picked up.
16"     This introduces a new option, since the character ranges
17"     specified could be multibyte:
18"         let g:omni_syntax_use_single_byte = 1
19"     This by default will only allow single byte ASCII
20"     characters to be added and an additional check to ensure
21"     the charater is printable (see documentation for isprint).
22"
23" Version 9.0
24"     Add the check for cpo.
25"
26" Version 8.0
27"     Updated SyntaxCSyntaxGroupItems()
28"         - Some additional syntax items were also allowed
29"           on nextgroup= lines which were ignored by default.
30"           Now these lines are processed independently.
31"
32" Version 7.0
33"     Updated syntaxcomplete#OmniSyntaxList()
34"         - Looking up the syntax groups defined from a syntax file
35"           looked for only 1 format of {filetype}GroupName, but some
36"           syntax writers use this format as well:
37"               {b:current_syntax}GroupName
38"           OmniSyntaxList() will now check for both if the first
39"           method does not find a match.
40"
41" Version 6.0
42"     Added syntaxcomplete#OmniSyntaxList()
43"         - Allows other plugins to use this for their own
44"           purposes.
45"         - It will return a List of all syntax items for the
46"           syntax group name passed in.
47"         - XPTemplate for SQL will use this function via the
48"           sqlcomplete plugin to populate a Choose box.
49"
50" Version 5.0
51"     Updated SyntaxCSyntaxGroupItems()
52"         - When processing a list of syntax groups, the final group
53"           was missed in function SyntaxCSyntaxGroupItems.
54"
55" Set completion with CTRL-X CTRL-O to autoloaded function.
56" This check is in place in case this script is
57" sourced directly instead of using the autoload feature.
58if exists('+omnifunc')
59    " Do not set the option if already set since this
60    " results in an E117 warning.
61    if &omnifunc == ""
62        setlocal omnifunc=syntaxcomplete#Complete
63    endif
64endif
65
66if exists('g:loaded_syntax_completion')
67    finish
68endif
69let g:loaded_syntax_completion = 100
70
71" Turn on support for line continuations when creating the script
72let s:cpo_save = &cpo
73set cpo&vim
74
75" Set ignorecase to the ftplugin standard
76" This is the default setting, but if you define a buffer local
77" variable you can override this on a per filetype.
78if !exists('g:omni_syntax_ignorecase')
79    let g:omni_syntax_ignorecase = &ignorecase
80endif
81
82" Indicates whether we should use the iskeyword option to determine
83" how to split words.
84" This is the default setting, but if you define a buffer local
85" variable you can override this on a per filetype.
86if !exists('g:omni_syntax_use_iskeyword')
87    let g:omni_syntax_use_iskeyword = 1
88endif
89
90" When using iskeyword, this setting controls whether the characters
91" should be limited to single byte characters.
92if !exists('g:omni_syntax_use_single_byte')
93    let g:omni_syntax_use_single_byte = 1
94endif
95
96" When using iskeyword, this setting controls whether the characters
97" should be limited to single byte characters.
98if !exists('g:omni_syntax_use_iskeyword_numeric')
99    let g:omni_syntax_use_iskeyword_numeric = 1
100endif
101
102" Only display items in the completion window that are at least
103" this many characters in length.
104" This is the default setting, but if you define a buffer local
105" variable you can override this on a per filetype.
106if !exists('g:omni_syntax_minimum_length')
107    let g:omni_syntax_minimum_length = 0
108endif
109
110" This script will build a completion list based on the syntax
111" elements defined by the files in $VIMRUNTIME/syntax.
112let s:syn_remove_words = 'match,matchgroup=,contains,'.
113            \ 'links to,start=,end='
114            " \ 'links to,start=,end=,nextgroup='
115
116let s:cache_name = []
117let s:cache_list = []
118let s:prepended  = ''
119
120" This function is used for the 'omnifunc' option.
121function! syntaxcomplete#Complete(findstart, base)
122
123    " Only display items in the completion window that are at least
124    " this many characters in length
125    if !exists('b:omni_syntax_ignorecase')
126        if exists('g:omni_syntax_ignorecase')
127            let b:omni_syntax_ignorecase = g:omni_syntax_ignorecase
128        else
129            let b:omni_syntax_ignorecase = &ignorecase
130        endif
131    endif
132
133    if a:findstart
134        " Locate the start of the item, including "."
135        let line = getline('.')
136        let start = col('.') - 1
137        let lastword = -1
138        while start > 0
139            " if line[start - 1] =~ '\S'
140            "     let start -= 1
141            " elseif line[start - 1] =~ '\.'
142            if line[start - 1] =~ '\k'
143                let start -= 1
144                let lastword = a:findstart
145            else
146                break
147            endif
148        endwhile
149
150        " Return the column of the last word, which is going to be changed.
151        " Remember the text that comes before it in s:prepended.
152        if lastword == -1
153            let s:prepended = ''
154            return start
155        endif
156        let s:prepended = strpart(line, start, (col('.') - 1) - start)
157        return start
158    endif
159
160    " let base = s:prepended . a:base
161    let base = s:prepended
162
163    let filetype = substitute(&filetype, '\.', '_', 'g')
164    let list_idx = index(s:cache_name, filetype, 0, &ignorecase)
165    if list_idx > -1
166        let compl_list = s:cache_list[list_idx]
167    else
168        let compl_list   = OmniSyntaxList()
169        let s:cache_name = add( s:cache_name,  filetype )
170        let s:cache_list = add( s:cache_list,  compl_list )
171    endif
172
173    " Return list of matches.
174
175    if base != ''
176        " let compstr    = join(compl_list, ' ')
177        " let expr       = (b:omni_syntax_ignorecase==0?'\C':'').'\<\%('.base.'\)\@!\w\+\s*'
178        " let compstr    = substitute(compstr, expr, '', 'g')
179        " let compl_list = split(compstr, '\s\+')
180
181        " Filter the list based on the first few characters the user
182        " entered
183        let expr = 'v:val '.(g:omni_syntax_ignorecase==1?'=~?':'=~#')." '^".escape(base, '\\/.*$^~[]').".*'"
184        let compl_list = filter(deepcopy(compl_list), expr)
185    endif
186
187    return compl_list
188endfunc
189
190function! syntaxcomplete#OmniSyntaxList(...)
191    if a:0 > 0
192        let parms = []
193        if 3 == type(a:1)
194            let parms = a:1
195        elseif 1 == type(a:1)
196            let parms = split(a:1, ',')
197        endif
198        return OmniSyntaxList( parms )
199    else
200        return OmniSyntaxList()
201    endif
202endfunc
203
204function! OmniSyntaxList(...)
205    let list_parms = []
206    if a:0 > 0
207        if 3 == type(a:1)
208            let list_parms = a:1
209        elseif 1 == type(a:1)
210            let list_parms = split(a:1, ',')
211        endif
212    endif
213
214    " Default to returning a dictionary, if use_dictionary is set to 0
215    " a list will be returned.
216    " let use_dictionary = 1
217    " if a:0 > 0 && a:1 != ''
218    "     let use_dictionary = a:1
219    " endif
220
221    " Only display items in the completion window that are at least
222    " this many characters in length
223    if !exists('b:omni_syntax_use_iskeyword')
224        if exists('g:omni_syntax_use_iskeyword')
225            let b:omni_syntax_use_iskeyword = g:omni_syntax_use_iskeyword
226        else
227            let b:omni_syntax_use_iskeyword = 1
228        endif
229    endif
230
231    " Only display items in the completion window that are at least
232    " this many characters in length
233    if !exists('b:omni_syntax_minimum_length')
234        if exists('g:omni_syntax_minimum_length')
235            let b:omni_syntax_minimum_length = g:omni_syntax_minimum_length
236        else
237            let b:omni_syntax_minimum_length = 0
238        endif
239    endif
240
241    let saveL = @l
242    let filetype = substitute(&filetype, '\.', '_', 'g')
243
244    if empty(list_parms)
245        " Default the include group to include the requested syntax group
246        let syntax_group_include_{filetype} = ''
247        " Check if there are any overrides specified for this filetype
248        if exists('g:omni_syntax_group_include_'.filetype)
249            let syntax_group_include_{filetype} =
250                        \ substitute( g:omni_syntax_group_include_{filetype},'\s\+','','g')
251            let list_parms = split(g:omni_syntax_group_include_{filetype}, ',')
252            if syntax_group_include_{filetype} =~ '\w'
253                let syntax_group_include_{filetype} =
254                            \ substitute( syntax_group_include_{filetype},
255                            \ '\s*,\s*', '\\|', 'g'
256                            \ )
257            endif
258        endif
259    else
260        " A specific list was provided, use it
261    endif
262
263    " Loop through all the syntax groupnames, and build a
264    " syntax file which contains these names.  This can
265    " work generically for any filetype that does not already
266    " have a plugin defined.
267    " This ASSUMES the syntax groupname BEGINS with the name
268    " of the filetype.  From my casual viewing of the vim7\syntax
269    " directory this is true for almost all syntax definitions.
270    " As an example, the SQL syntax groups have this pattern:
271    "     sqlType
272    "     sqlOperators
273    "     sqlKeyword ...
274    redir @l
275    silent! exec 'syntax list '.join(list_parms)
276    redir END
277
278    let syntax_full = "\n".@l
279    let @l = saveL
280
281    if syntax_full =~ 'E28'
282                \ || syntax_full =~ 'E411'
283                \ || syntax_full =~ 'E415'
284                \ || syntax_full =~ 'No Syntax items'
285        return []
286    endif
287
288    let filetype = substitute(&filetype, '\.', '_', 'g')
289
290    let list_exclude_groups = []
291    if a:0 > 0
292        " Do nothing since we have specific a specific list of groups
293    else
294        " Default the exclude group to nothing
295        let syntax_group_exclude_{filetype} = ''
296        " Check if there are any overrides specified for this filetype
297        if exists('g:omni_syntax_group_exclude_'.filetype)
298            let syntax_group_exclude_{filetype} =
299                        \ substitute( g:omni_syntax_group_exclude_{filetype},'\s\+','','g')
300            let list_exclude_groups = split(g:omni_syntax_group_exclude_{filetype}, ',')
301            if syntax_group_exclude_{filetype} =~ '\w'
302                let syntax_group_exclude_{filetype} =
303                            \ substitute( syntax_group_exclude_{filetype},
304                            \ '\s*,\s*', '\\|', 'g'
305                            \ )
306            endif
307        endif
308    endif
309
310    " Sometimes filetypes can be composite names, like c.doxygen
311    " Loop through each individual part looking for the syntax
312    " items specific to each individual filetype.
313    let syn_list = ''
314    let ftindex  = 0
315    let ftindex  = match(&filetype, '\w\+', ftindex)
316
317    while ftindex > -1
318        let ft_part_name = matchstr( &filetype, '\w\+', ftindex )
319
320        " Syntax rules can contain items for more than just the current
321        " filetype.  They can contain additional items added by the user
322        " via autocmds or their vimrc.
323        " Some syntax files can be combined (html, php, jsp).
324        " We want only items that begin with the filetype we are interested in.
325        let next_group_regex = '\n' .
326                    \ '\zs'.ft_part_name.'\w\+\ze'.
327                    \ '\s\+xxx\s\+'
328        let index    = 0
329        let index    = match(syntax_full, next_group_regex, index)
330
331        if index == -1 && exists('b:current_syntax') && ft_part_name != b:current_syntax
332            " There appears to be two standards when writing syntax files.
333            " Either items begin as:
334            "     syn keyword {filetype}Keyword         values ...
335            "     let b:current_syntax = "sql"
336            "     let b:current_syntax = "sqlanywhere"
337            " Or
338            "     syn keyword {syntax_filename}Keyword  values ...
339            "     let b:current_syntax = "mysql"
340            " So, we will make the format of finding the syntax group names
341            " a bit more flexible and look for both if the first fails to
342            " find a match.
343            let next_group_regex = '\n' .
344                        \ '\zs'.b:current_syntax.'\w\+\ze'.
345                        \ '\s\+xxx\s\+'
346            let index    = 0
347            let index    = match(syntax_full, next_group_regex, index)
348        endif
349
350        while index > -1
351            let group_name = matchstr( syntax_full, '\w\+', index )
352
353            let get_syn_list = 1
354            for exclude_group_name in list_exclude_groups
355                if '\<'.exclude_group_name.'\>' =~ '\<'.group_name.'\>'
356                    let get_syn_list = 0
357                endif
358            endfor
359
360            " This code is no longer needed in version 6.0 since we have
361            " augmented the syntax list command to only retrieve the syntax
362            " groups we are interested in.
363            "
364            " if get_syn_list == 1
365            "     if syntax_group_include_{filetype} != ''
366            "         if '\<'.syntax_group_include_{filetype}.'\>' !~ '\<'.group_name.'\>'
367            "             let get_syn_list = 0
368            "         endif
369            "     endif
370            " endif
371
372            if get_syn_list == 1
373                " Pass in the full syntax listing, plus the group name we
374                " are interested in.
375                let extra_syn_list = s:SyntaxCSyntaxGroupItems(group_name, syntax_full)
376                let syn_list = syn_list . extra_syn_list . "\n"
377            endif
378
379            let index = index + strlen(group_name)
380            let index = match(syntax_full, next_group_regex, index)
381        endwhile
382
383        let ftindex  = ftindex + len(ft_part_name)
384        let ftindex  = match( &filetype, '\w\+', ftindex )
385    endwhile
386
387    " Convert the string to a List and sort it.
388    let compl_list = sort(split(syn_list))
389
390    if &filetype == 'vim'
391        let short_compl_list = []
392        for i in range(len(compl_list))
393            if i == len(compl_list)-1
394                let next = i
395            else
396                let next = i + 1
397            endif
398            if  compl_list[next] !~ '^'.compl_list[i].'.$'
399                let short_compl_list += [compl_list[i]]
400            endif
401        endfor
402
403        return short_compl_list
404    else
405        return compl_list
406    endif
407endfunction
408
409function! s:SyntaxCSyntaxGroupItems( group_name, syntax_full )
410
411    let syn_list = ""
412
413    " From the full syntax listing, strip out the portion for the
414    " request group.
415    " Query:
416    "     \n           - must begin with a newline
417    "     a:group_name - the group name we are interested in
418    "     \s\+xxx\s\+  - group names are always followed by xxx
419    "     \zs          - start the match
420    "     .\{-}        - everything ...
421    "     \ze          - end the match
422    "     \(           - start a group or 2 potential matches
423    "     \n\w         - at the first newline starting with a character
424    "     \|           - 2nd potential match
425    "     \%$          - matches end of the file or string
426    "     \)           - end a group
427    let syntax_group = matchstr(a:syntax_full,
428                \ "\n".a:group_name.'\s\+xxx\s\+\zs.\{-}\ze\(\n\w\|\%$\)'
429                \ )
430
431    if syntax_group != ""
432        " let syn_list = substitute( @l, '^.*xxx\s*\%(contained\s*\)\?', "", '' )
433        " let syn_list = substitute( @l, '^.*xxx\s*', "", '' )
434
435        " We only want the words for the lines begining with
436        " containedin, but there could be other items.
437
438        " Tried to remove all lines that do not begin with contained
439        " but this does not work in all cases since you can have
440        "    contained nextgroup=...
441        " So this will strip off the ending of lines with known
442        " keywords.
443        let syn_list = substitute(
444                    \    syntax_group, '\<\('.
445                    \    substitute(
446                    \      escape(s:syn_remove_words, '\\/.*$^~[]')
447                    \      , ',', '\\|', 'g'
448                    \    ).
449                    \    '\).\{-}\%($\|'."\n".'\)'
450                    \    , "\n", 'g'
451                    \  )
452
453        " Now strip off the newline + blank space + contained.
454        " Also include lines with nextgroup=@someName skip_key_words syntax_element
455        let syn_list = substitute(
456                    \    syn_list, '\%(^\|\n\)\@<=\s*\<\(contained\|nextgroup=\)'
457                    \    , "", 'g'
458                    \ )
459
460        " This can leave lines like this
461        "     =@vimMenuList  skipwhite onoremenu
462        " Strip the special option keywords first
463        "     :h :syn-skipwhite*
464        let syn_list = substitute(
465                    \    syn_list, '\<\(skipwhite\|skipnl\|skipempty\)\>'
466                    \    , "", 'g'
467                    \ )
468
469        " Now remove the remainder of the nextgroup=@someName lines
470        let syn_list = substitute(
471                    \    syn_list, '\%(^\|\n\)\@<=\s*\(@\w\+\)'
472                    \    , "", 'g'
473                    \ )
474
475        if b:omni_syntax_use_iskeyword == 0
476            " There are a number of items which have non-word characters in
477            " them, *'T_F1'*.  vim.vim is one such file.
478            " This will replace non-word characters with spaces.
479            let syn_list = substitute( syn_list, '[^0-9A-Za-z_ ]', ' ', 'g' )
480        else
481            if g:omni_syntax_use_iskeyword_numeric == 1
482                " iskeyword can contain value like this
483                " 38,42,43,45,47-58,60-62,64-90,97-122,_,+,-,*,/,%,<,=,>,:,$,?,!,@-@,94
484                " Numeric values convert to their ASCII equivalent using the
485                " nr2char() function.
486                "     &       38
487                "     *       42
488                "     +       43
489                "     -       45
490                "     ^       94
491                " Iterate through all numeric specifications and convert those
492                " to their ascii equivalent ensuring the character is printable.
493                " If so, add it to the list.
494                let accepted_chars = ''
495                for item in split(&iskeyword, ',')
496                    if item =~ '-'
497                        " This is a character range (ie 47-58),
498                        " cycle through each character within the range
499                        let [b:start, b:end] = split(item, '-')
500                        for range_item in range( b:start, b:end )
501                            if range_item <= 127 || g:omni_syntax_use_single_byte == 0
502                                if nr2char(range_item) =~ '\p'
503                                    let accepted_chars = accepted_chars . nr2char(range_item)
504                                endif
505                            endif
506                        endfor
507                    elseif item =~ '^\d\+$'
508                        " Only numeric, translate to a character
509                        if item < 127 || g:omni_syntax_use_single_byte == 0
510                            if nr2char(item) =~ '\p'
511                                let accepted_chars = accepted_chars . nr2char(item)
512                            endif
513                        endif
514                    else
515                        if char2nr(item) < 127 || g:omni_syntax_use_single_byte == 0
516                            if item =~ '\p'
517                                let accepted_chars = accepted_chars . item
518                            endif
519                        endif
520                    endif
521                endfor
522                " Escape special regex characters
523                let accepted_chars = escape(accepted_chars, '\\/.*$^~[]' )
524                " Remove all characters that are not acceptable
525                let syn_list = substitute( syn_list, '[^A-Za-z'.accepted_chars.']', ' ', 'g' )
526            else
527                let accept_chars = ','.&iskeyword.','
528                " Remove all character ranges
529                " let accept_chars = substitute(accept_chars, ',[^,]\+-[^,]\+,', ',', 'g')
530                let accept_chars = substitute(accept_chars, ',\@<=[^,]\+-[^,]\+,', '', 'g')
531                " Remove all numeric specifications
532                " let accept_chars = substitute(accept_chars, ',\d\{-},', ',', 'g')
533                let accept_chars = substitute(accept_chars, ',\@<=\d\{-},', '', 'g')
534                " Remove all commas
535                let accept_chars = substitute(accept_chars, ',', '', 'g')
536                " Escape special regex characters
537                let accept_chars = escape(accept_chars, '\\/.*$^~[]' )
538                " Remove all characters that are not acceptable
539                let syn_list = substitute( syn_list, '[^0-9A-Za-z_'.accept_chars.']', ' ', 'g' )
540            endif
541        endif
542
543        if b:omni_syntax_minimum_length > 0
544            " If the user specified a minimum length, enforce it
545            let syn_list = substitute(' '.syn_list.' ', ' \S\{,'.b:omni_syntax_minimum_length.'}\ze ', ' ', 'g')
546        endif
547    else
548        let syn_list = ''
549    endif
550
551    return syn_list
552endfunction
553
554function! OmniSyntaxShowChars(spec)
555  let result = []
556  for item in split(a:spec, ',')
557    if len(item) > 1
558      if item == '@-@'
559        call add(result, char2nr(item))
560      else
561        call extend(result, call('range', split(item, '-')))
562      endif
563    else
564      if item == '@'  " assume this is [A-Za-z]
565        for [c1, c2] in [['A', 'Z'], ['a', 'z']]
566          call extend(result, range(char2nr(c1), char2nr(c2)))
567        endfor
568      else
569        call add(result, char2nr(item))
570      endif
571    endif
572  endfor
573  return join(map(result, 'nr2char(v:val)'), ', ')
574endfunction
575let &cpo = s:cpo_save
576unlet s:cpo_save
577