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