Merge branch 'vim-with-runtime' into feat/quickfix-title
[vim_extended.git] / runtime / autoload / xmlcomplete.vim
blob37f9bb413e7f0e55846e5724fb86fa7e75a325c2
1 " Vim completion script
2 " Language:     XML
3 " Maintainer:   Mikolaj Machowski ( mikmach AT wp DOT pl )
4 " Last Change:  2006 Aug 15
5 " Version: 1.9
7 " Changelog:
8 " 1.9 - 2007 Aug 15
9 "               - fix closing of namespaced tags (Johannes Weiss)
10 " 1.8 - 2006 Jul 18
11 "       - allow for closing of xml tags even when data file isn't available
13 " This function will create Dictionary with users namespace strings and values
14 " canonical (system) names of data files.  Names should be lowercase,
15 " descriptive to avoid any future conflicts. For example 'xhtml10s' should be
16 " name for data of XHTML 1.0 Strict and 'xhtml10t' for XHTML 1.0 Transitional
17 " User interface will be provided by XMLns command defined in ftplugin/xml.vim
18 " Currently supported canonicals are:
19 " xhtml10s - XHTML 1.0 Strict
20 " xsl      - XSL
21 function! xmlcomplete#CreateConnection(canonical, ...) " {{{
23         " When only one argument provided treat name as default namespace (without
24         " 'prefix:').
25         if exists("a:1")
26                 let users = a:1
27         else
28                 let users = 'DEFAULT'
29         endif
31         " Source data file. Due to suspected errors in autoload do it with
32         " :runtime.
33         " TODO: make it properly (using autoload, that is) later
34         exe "runtime autoload/xml/".a:canonical.".vim"
36         " Remove all traces of unexisting files to return [] when trying
37         " omnicomplete something
38         " TODO: give warning about non-existing canonicals - should it be?
39         if !exists("g:xmldata_".a:canonical)
40                 unlet! g:xmldata_connection
41                 return 0
42         endif
44         " We need to initialize Dictionary to add key-value pair
45         if !exists("g:xmldata_connection")
46                 let g:xmldata_connection = {}
47         endif
49         let g:xmldata_connection[users] = a:canonical
51 endfunction
52 " }}}
54 function! xmlcomplete#CreateEntConnection(...) " {{{
55         if a:0 > 0
56                 let g:xmldata_entconnect = a:1
57         else
58                 let g:xmldata_entconnect = 'DEFAULT'
59         endif
60 endfunction
61 " }}}
63 function! xmlcomplete#CompleteTags(findstart, base)
64   if a:findstart
65     " locate the start of the word
66         let curline = line('.')
67     let line = getline('.')
68     let start = col('.') - 1
69         let compl_begin = col('.') - 2
71     while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)'
72                 let start -= 1
73     endwhile
75         if start >= 0 && line[start - 1] =~ '&'
76                 let b:entitiescompl = 1
77                 let b:compl_context = ''
78                 return start
79         endif
81         let b:compl_context = getline('.')[0:(compl_begin)]
82         if b:compl_context !~ '<[^>]*$'
83                 " Look like we may have broken tag. Check previous lines. Up to
84                 " 10?
85                 let i = 1
86                 while 1
87                         let context_line = getline(curline-i)
88                         if context_line =~ '<[^>]*$'
89                                 " Yep, this is this line
90                                 let context_lines = getline(curline-i, curline-1) + [b:compl_context]
91                                 let b:compl_context = join(context_lines, ' ')
92                                 break
93                         elseif context_line =~ '>[^<]*$' || i == curline
94                                 " Normal tag line, no need for completion at all
95                                 " OR reached first line without tag at all
96                                 let b:compl_context = ''
97                                 break
98                         endif
99                         let i += 1
100                 endwhile
101                 " Make sure we don't have counter
102                 unlet! i
103         endif
104         let b:compl_context = matchstr(b:compl_context, '.*\zs<.*')
106         " Make sure we will have only current namespace
107         unlet! b:xml_namespace
108         let b:xml_namespace = matchstr(b:compl_context, '^<\zs\k*\ze:')
109         if b:xml_namespace == ''
110                 let b:xml_namespace = 'DEFAULT'
111         endif
113     return start
115   else
116         " Initialize base return lists
117     let res = []
118     let res2 = []
119         " a:base is very short - we need context
120         if len(b:compl_context) == 0  && !exists("b:entitiescompl")
121                 return []
122         endif
123         let context = matchstr(b:compl_context, '^<\zs.*')
124         unlet! b:compl_context
125         " There is no connection of namespace and data file.
126         if !exists("g:xmldata_connection") || g:xmldata_connection == {}
127                 " There is still possibility we may do something - eg. close tag
128                 let b:unaryTagsStack = "base meta link hr br param img area input col"
129                 if context =~ '^\/'
130                         let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
131                         return [opentag.">"]
132                 else
133                         return []
134                 endif
135         endif
137         " Make entities completion
138         if exists("b:entitiescompl")
139                 unlet! b:entitiescompl
141                 if !exists("g:xmldata_entconnect") || g:xmldata_entconnect == 'DEFAULT'
142                         let values =  g:xmldata{'_'.g:xmldata_connection['DEFAULT']}['vimxmlentities']
143                 else
144                         let values =  g:xmldata{'_'.g:xmldata_entconnect}['vimxmlentities']
145                 endif
147                 " Get only lines with entity declarations but throw out
148                 " parameter-entities - they may be completed in future
149                 let entdecl = filter(getline(1, "$"), 'v:val =~ "<!ENTITY\\s\\+[^%]"')
151                 if len(entdecl) > 0
152                         let intent = map(copy(entdecl), 'matchstr(v:val, "<!ENTITY\\s\\+\\zs\\(\\k\\|[.-:]\\)\\+\\ze")')
153                         let values = intent + values
154                 endif
156                 if len(a:base) == 1
157                         for m in values
158                                 if m =~ '^'.a:base
159                                         call add(res, m.';')
160                                 endif
161                         endfor
162                         return res
163                 else
164                         for m in values
165                                 if m =~? '^'.a:base
166                                         call add(res, m.';')
167                                 elseif m =~? a:base
168                                         call add(res2, m.';')
169                                 endif
170                         endfor
172                         return res + res2
173                 endif
175         endif
176         if context =~ '>'
177                 " Generally if context contains > it means we are outside of tag and
178                 " should abandon action
179                 return []
180         endif
182     " find tags matching with "a:base"
183         " If a:base contains white space it is attribute.
184         " It could be also value of attribute...
185         " We have to get first word to offer
186         " proper completions
187         if context == ''
188                 let tag = ''
189         else
190                 let tag = split(context)[0]
191         endif
192         " Get rid of namespace
193         let tag = substitute(tag, '^'.b:xml_namespace.':', '', '')
196         " Get last word, it should be attr name
197         let attr = matchstr(context, '.*\s\zs.*')
198         " Possible situations where any prediction would be difficult:
199         " 1. Events attributes
200         if context =~ '\s'
202                 " If attr contains =\s*[\"'] we catched value of attribute
203                 if attr =~ "=\s*[\"']" || attr =~ "=\s*$"
204                         " Let do attribute specific completion
205                         let attrname = matchstr(attr, '.*\ze\s*=')
206                         let entered_value = matchstr(attr, ".*=\\s*[\"']\\?\\zs.*")
208                         if tag =~ '^[?!]'
209                                 " Return nothing if we are inside of ! or ? tag
210                                 return []
211                         else
212                                 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag) && has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1], attrname)
213                                         let values = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][attrname]
214                                 else
215                                         return []
216                                 endif
217                         endif
219                         if len(values) == 0
220                                 return []
221                         endif
223                         " We need special version of sbase
224                         let attrbase = matchstr(context, ".*[\"']")
225                         let attrquote = matchstr(attrbase, '.$')
226                         if attrquote !~ "['\"]"
227                                 let attrquoteopen = '"'
228                                 let attrquote = '"'
229                         else
230                                 let attrquoteopen = ''
231                         endif
233                         for m in values
234                                 " This if is needed to not offer all completions as-is
235                                 " alphabetically but sort them. Those beginning with entered
236                                 " part will be as first choices
237                                 if m =~ '^'.entered_value
238                                         call add(res, attrquoteopen . m . attrquote.' ')
239                                 elseif m =~ entered_value
240                                         call add(res2, attrquoteopen . m . attrquote.' ')
241                                 endif
242                         endfor
244                         return res + res2
246                 endif
248                 if tag =~ '?xml'
249                         " Two possible arguments for <?xml> plus variation
250                         let attrs = ['encoding', 'version="1.0"', 'version']
251                 elseif tag =~ '^!'
252                         " Don't make completion at all
253                         "
254                         return []
255                 else
256             if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
257                                 " Abandon when data file isn't complete
258                                 return []
259                         endif
260                         let attrs = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1])
261                 endif
263                 for m in sort(attrs)
264                         if m =~ '^'.attr
265                                 call add(res, m)
266                         elseif m =~ attr
267                                 call add(res2, m)
268                         endif
269                 endfor
270                 let menu = res + res2
271                 let final_menu = []
272                 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmlattrinfo')
273                         for i in range(len(menu))
274                                 let item = menu[i]
275                                 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'], item)
276                                         let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][0]
277                                         let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][1]
278                                 else
279                                         let m_menu = ''
280                                         let m_info = ''
281                                 endif
282                                 if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item][0] =~ '^\(BOOL\|'.item.'\)$'
283                                         let item = item
284                                 else
285                                         let item .= '="'
286                                 endif
287                                 let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}]
288                         endfor
289                 else
290                         for i in range(len(menu))
291                                 let item = menu[i]
292                                 if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item][0] =~ '^\(BOOL\|'.item.'\)$'
293                                         let item = item
294                                 else
295                                         let item .= '="'
296                                 endif
297                                 let final_menu += [item]
298                         endfor
299                 endif
300                 return final_menu
302         endif
303         " Close tag
304         let b:unaryTagsStack = "base meta link hr br param img area input col"
305         if context =~ '^\/'
306                 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
307                 return [opentag.">"]
308         endif
310         " Complete elements of XML structure
311         " TODO: #REQUIRED, #IMPLIED, #FIXED, #PCDATA - but these should be detected like
312         " entities - in first run
313         " keywords: CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS
314         " are hardly recognizable but keep it in reserve
315         " also: EMPTY ANY SYSTEM PUBLIC DATA
316         if context =~ '^!'
317                 let tags = ['!ELEMENT', '!DOCTYPE', '!ATTLIST', '!ENTITY', '!NOTATION', '![CDATA[', '![INCLUDE[', '![IGNORE[']
319                 for m in tags
320                         if m =~ '^'.context
321                                 let m = substitute(m, '^!\[\?', '', '')
322                                 call add(res, m)
323                         elseif m =~ context
324                                 let m = substitute(m, '^!\[\?', '', '')
325                                 call add(res2, m)
326                         endif
327                 endfor
329                 return res + res2
331         endif
333         " Complete text declaration
334         if context =~ '^?'
335                 let tags = ['?xml']
337                 for m in tags
338                         if m =~ '^'.context
339                                 call add(res, substitute(m, '^?', '', ''))
340                         elseif m =~ context
341                                 call add(res, substitute(m, '^?', '', ''))
342                         endif
343                 endfor
345                 return res + res2
347         endif
349         " Deal with tag completion.
350         let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
351         let opentag = substitute(opentag, '^\k*:', '', '')
352         if opentag == ''
353                 "return []
354             let tags = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]})
355                 call filter(tags, 'v:val !~ "^vimxml"')
356         else
357                 if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, opentag)
358                         " Abandon when data file isn't complete
359                         return []
360                 endif
361                 let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
362         endif
364         let context = substitute(context, '^\k*:', '', '')
366         for m in tags
367                 if m =~ '^'.context
368                         call add(res, m)
369                 elseif m =~ context
370                         call add(res2, m)
371                 endif
372         endfor
373         let menu = res + res2
374         if b:xml_namespace == 'DEFAULT'
375                 let xml_namespace = ''
376         else
377                 let xml_namespace = b:xml_namespace.':'
378         endif
379         if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmltaginfo')
380                 let final_menu = []
381                 for i in range(len(menu))
382                         let item = menu[i]
383                         if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'], item)
384                                 let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][0]
385                                 let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][1]
386                         else
387                                 let m_menu = ''
388                                 let m_info = ''
389                         endif
390                         let final_menu += [{'word':xml_namespace.item, 'menu':m_menu, 'info':m_info}]
391                 endfor
392         else
393                 let final_menu = map(menu, 'xml_namespace.v:val')
394         endif
396         return final_menu
398   endif
399 endfunction
401 " MM: This is severely reduced closetag.vim used with kind permission of Steven
402 "     Mueller
403 "     Changes: strip all comments; delete error messages; add checking for
404 "     namespace
405 " Author: Steven Mueller <diffusor@ugcs.caltech.edu>
406 " Last Modified: Tue May 24 13:29:48 PDT 2005 
407 " Version: 0.9.1
409 function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
410         let linenum=line('.')
411         let lineend=col('.') - 1 " start: cursor position
412         let first=1              " flag for first line searched
413         let b:TagStack=''        " main stack of tags
414         let startInComment=s:InComment()
416         if exists("b:xml_namespace")
417                 if b:xml_namespace == 'DEFAULT'
418                         let tagpat='</\=\(\k\|[.:-]\)\+\|/>'
419                 else
420                         let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
421                 endif
422         else
423                 let tagpat='</\=\(\k\|[.:-]\)\+\|/>'
424         endif
425         while (linenum>0)
426                 let line=getline(linenum)
427                 if first
428                         let line=strpart(line,0,lineend)
429                 else
430                         let lineend=strlen(line)
431                 endif
432                 let b:lineTagStack=''
433                 let mpos=0
434                 let b:TagCol=0
435                 while (mpos > -1)
436                         let mpos=matchend(line,tagpat)
437                         if mpos > -1
438                                 let b:TagCol=b:TagCol+mpos
439                                 let tag=matchstr(line,tagpat)
441                                 if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
442                                         let b:TagLine=linenum
443                                         call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
444                                 endif
445                                 let lineend=lineend-mpos
446                                 let line=strpart(line,mpos,lineend)
447                         endif
448                 endwhile
449                 while (!s:EmptystackP('b:lineTagStack'))
450                         let tag=s:Pop('b:lineTagStack')
451                         if match(tag, '^/') == 0                "found end tag
452                                 call s:Push(tag,'b:TagStack')
453                         elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack) "found unclosed tag
454                                 return tag
455                         else
456                                 let endtag=s:Peekstack('b:TagStack')
457                                 if endtag == '/'.tag || endtag == '/'
458                                         call s:Pop('b:TagStack')        "found a open/close tag pair
459                                 elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
460                                         return ''
461                                 endif
462                         endif
463                 endwhile
464                 let linenum=linenum-1 | let first=0
465         endwhile
466 return ''
467 endfunction
469 function! s:InComment()
470         return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
471 endfunction
473 function! s:InCommentAt(line, col)
474         return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
475 endfunction
477 function! s:SetKeywords()
478         let g:IsKeywordBak=&iskeyword
479         let &iskeyword='33-255'
480 endfunction
482 function! s:RestoreKeywords()
483         let &iskeyword=g:IsKeywordBak
484 endfunction
486 function! s:Push(el, sname)
487         if !s:EmptystackP(a:sname)
488                 exe 'let '.a:sname."=a:el.' '.".a:sname
489         else
490                 exe 'let '.a:sname.'=a:el'
491         endif
492 endfunction
494 function! s:EmptystackP(sname)
495         exe 'let stack='.a:sname
496         if match(stack,'^ *$') == 0
497                 return 1
498         else
499                 return 0
500         endif
501 endfunction
503 function! s:Instack(el, sname)
504         exe 'let stack='.a:sname
505         call s:SetKeywords()
506         let m=match(stack, '\<'.a:el.'\>')
507         call s:RestoreKeywords()
508         if m < 0
509                 return 0
510         else
511                 return 1
512         endif
513 endfunction
515 function! s:Peekstack(sname)
516         call s:SetKeywords()
517         exe 'let stack='.a:sname
518         let top=matchstr(stack, '\<.\{-1,}\>')
519         call s:RestoreKeywords()
520         return top
521 endfunction
523 function! s:Pop(sname)
524         if s:EmptystackP(a:sname)
525                 return ''
526         endif
527         exe 'let stack='.a:sname
528         call s:SetKeywords()
529         let loc=matchend(stack,'\<.\{-1,}\>')
530         exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
531         let top=strpart(stack, match(stack, '\<'), loc)
532         call s:RestoreKeywords()
533         return top
534 endfunction
536 function! s:Clearstack(sname)
537         exe 'let '.a:sname."=''"
538 endfunction
539 " vim:set foldmethod=marker: