blob: 582dc106680c5bcd7b82f5e6926fdb6c14d9c670 [file] [log] [blame]
Bram Moolenaara5792f52005-11-23 21:25:05 +00001" Vim completion script
Bram Moolenaard12f5c12006-01-25 22:10:52 +00002" Language: XML
Bram Moolenaara5792f52005-11-23 21:25:05 +00003" Maintainer: Mikolaj Machowski ( mikmach AT wp DOT pl )
Bram Moolenaar910f66f2006-04-05 20:41:53 +00004" Last Change: 2006 Mar 31
Bram Moolenaara5792f52005-11-23 21:25:05 +00005
6" This function will create Dictionary with users namespace strings and values
7" canonical (system) names of data files. Names should be lowercase,
8" descriptive to avoid any future conflicts. For example 'xhtml10s' should be
9" name for data of XHTML 1.0 Strict and 'xhtml10t' for XHTML 1.0 Transitional
Bram Moolenaar910f66f2006-04-05 20:41:53 +000010" User interface will be provided by XMLns command defined in ftplugin/xml.vim
Bram Moolenaara5792f52005-11-23 21:25:05 +000011" Currently supported canonicals are:
12" xhtml10s - XHTML 1.0 Strict
13" xsl - XSL
14function! xmlcomplete#CreateConnection(canonical, ...)
15
16 " When only one argument provided treat name as default namespace (without
17 " 'prefix:').
18 if exists("a:1")
19 let users = a:1
20 else
21 let users = 'DEFAULT'
22 endif
23
24 " Source data file. Due to suspected errors in autoload do it with
25 " :runtime.
26 " TODO: make it properly (using autoload, that is) later
27 exe "runtime autoload/xml/".a:canonical.".vim"
28
29 " Remove all traces of unexisting files to return [] when trying
30 " omnicomplete something
31 " TODO: give warning about non-existing canonicals - should it be?
32 if !exists("g:xmldata_".a:canonical)
33 unlet! g:xmldata_connection
34 return 0
35 endif
36
37 " We need to initialize Dictionary to add key-value pair
38 if !exists("g:xmldata_connection")
39 let g:xmldata_connection = {}
40 endif
41
42 let g:xmldata_connection[users] = a:canonical
43
44endfunction
45
46function! xmlcomplete#CreateEntConnection(...)
47 if a:0 > 0
48 let g:xmldata_entconnect = a:1
49 else
50 let g:xmldata_entconnect = 'DEFAULT'
51 endif
52endfunction
53
54function! xmlcomplete#CompleteTags(findstart, base)
55 if a:findstart
56 " locate the start of the word
Bram Moolenaard12f5c12006-01-25 22:10:52 +000057 let curline = line('.')
Bram Moolenaara5792f52005-11-23 21:25:05 +000058 let line = getline('.')
59 let start = col('.') - 1
60 let compl_begin = col('.') - 2
61
62 while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)'
63 let start -= 1
64 endwhile
65
66 if start >= 0 && line[start - 1] =~ '&'
67 let b:entitiescompl = 1
68 let b:compl_context = ''
69 return start
70 endif
71
72 let b:compl_context = getline('.')[0:(compl_begin)]
Bram Moolenaard12f5c12006-01-25 22:10:52 +000073 if b:compl_context !~ '<[^>]*$'
74 " Look like we may have broken tag. Check previous lines. Up to
75 " 10?
76 let i = 1
77 while 1
78 let context_line = getline(curline-i)
79 if context_line =~ '<[^>]*$'
80 " Yep, this is this line
81 let context_lines = getline(curline-i, curline)
82 let b:compl_context = join(context_lines, ' ')
83 break
Bram Moolenaarc15ef302006-03-19 22:11:16 +000084 elseif context_line =~ '>[^<]*$' || i == curline
Bram Moolenaard12f5c12006-01-25 22:10:52 +000085 " Normal tag line, no need for completion at all
Bram Moolenaarc15ef302006-03-19 22:11:16 +000086 " OR reached first line without tag at all
Bram Moolenaard12f5c12006-01-25 22:10:52 +000087 let b:compl_context = ''
88 break
89 endif
90 let i += 1
91 endwhile
92 " Make sure we don't have counter
93 unlet! i
94 endif
95 let b:compl_context = matchstr(b:compl_context, '.*\zs<.*')
Bram Moolenaara5792f52005-11-23 21:25:05 +000096
97 " Make sure we will have only current namespace
98 unlet! b:xml_namespace
Bram Moolenaard12f5c12006-01-25 22:10:52 +000099 let b:xml_namespace = matchstr(b:compl_context, '^<\zs\k*\ze:')
Bram Moolenaara5792f52005-11-23 21:25:05 +0000100 if b:xml_namespace == ''
101 let b:xml_namespace = 'DEFAULT'
102 endif
103
104 return start
105
106 else
107 " There is no connction of namespace and data file. Abandon action
108 if !exists("g:xmldata_connection") || g:xmldata_connection == {}
109 return []
110 endif
111 " Initialize base return lists
112 let res = []
113 let res2 = []
114 " a:base is very short - we need context
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000115 if len(b:compl_context) == 0 && !exists("b:entitiescompl")
116 return []
117 endif
118 let context = matchstr(b:compl_context, '^<\zs.*')
Bram Moolenaara5792f52005-11-23 21:25:05 +0000119 unlet! b:compl_context
120
121 " Make entities completion
122 if exists("b:entitiescompl")
123 unlet! b:entitiescompl
124
125 if !exists("g:xmldata_entconnect") || g:xmldata_entconnect == 'DEFAULT'
126 let values = g:xmldata{'_'.g:xmldata_connection['DEFAULT']}['vimxmlentities']
127 else
128 let values = g:xmldata{'_'.g:xmldata_entconnect}['vimxmlentities']
129 endif
130
131 " Get only lines with entity declarations but throw out
132 " parameter-entities - they may be completed in future
133 let entdecl = filter(getline(1, "$"), 'v:val =~ "<!ENTITY\\s\\+[^%]"')
134
135 if len(entdecl) > 0
136 let intent = map(copy(entdecl), 'matchstr(v:val, "<!ENTITY\\s\\+\\zs\\(\\k\\|[.-:]\\)\\+\\ze")')
137 let values = intent + values
138 endif
139
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000140 if len(a:base) == 1
141 for m in values
142 if m =~ '^'.a:base
143 call add(res, m.';')
144 endif
145 endfor
146 return res
147 else
148 for m in values
149 if m =~? '^'.a:base
150 call add(res, m.';')
151 elseif m =~? a:base
152 call add(res2, m.';')
153 endif
154 endfor
Bram Moolenaara5792f52005-11-23 21:25:05 +0000155
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000156 return res + res2
157 endif
Bram Moolenaara5792f52005-11-23 21:25:05 +0000158
159 endif
160 if context =~ '>'
161 " Generally if context contains > it means we are outside of tag and
162 " should abandon action
163 return []
164 endif
165
166 " find tags matching with "a:base"
167 " If a:base contains white space it is attribute.
168 " It could be also value of attribute...
169 " We have to get first word to offer
170 " proper completions
171 if context == ''
172 let tag = ''
173 else
174 let tag = split(context)[0]
175 endif
176 " Get rid of namespace
177 let tag = substitute(tag, '^'.b:xml_namespace.':', '', '')
178
179
180 " Get last word, it should be attr name
181 let attr = matchstr(context, '.*\s\zs.*')
182 " Possible situations where any prediction would be difficult:
183 " 1. Events attributes
184 if context =~ '\s'
185
186 " If attr contains =\s*[\"'] we catched value of attribute
187 if attr =~ "=\s*[\"']"
188 " Let do attribute specific completion
189 let attrname = matchstr(attr, '.*\ze\s*=')
190 let entered_value = matchstr(attr, ".*=\\s*[\"']\\zs.*")
191
192 if tag =~ '^[?!]'
193 " Return nothing if we are inside of ! or ? tag
194 return []
195 else
196 let values = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][attrname]
197 endif
198
199 if len(values) == 0
200 return []
201 endif
202
203 " We need special version of sbase
204 let attrbase = matchstr(context, ".*[\"']")
205 let attrquote = matchstr(attrbase, '.$')
206
207 for m in values
208 " This if is needed to not offer all completions as-is
209 " alphabetically but sort them. Those beginning with entered
210 " part will be as first choices
211 if m =~ '^'.entered_value
212 call add(res, m . attrquote.' ')
213 elseif m =~ entered_value
214 call add(res2, m . attrquote.' ')
215 endif
216 endfor
217
218 return res + res2
219
220 endif
221
222 if tag =~ '?xml'
223 " Two possible arguments for <?xml> plus variation
224 let attrs = ['encoding', 'version="1.0"', 'version']
225 elseif tag =~ '^!'
226 " Don't make completion at all
Bram Moolenaar910f66f2006-04-05 20:41:53 +0000227 "
Bram Moolenaara5792f52005-11-23 21:25:05 +0000228 return []
229 else
Bram Moolenaar910f66f2006-04-05 20:41:53 +0000230 if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
231 " Abandon when data file isn't complete
232 return []
233 endif
Bram Moolenaara5792f52005-11-23 21:25:05 +0000234 let attrs = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1])
235 endif
236
237 for m in sort(attrs)
238 if m =~ '^'.attr
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000239 call add(res, m)
Bram Moolenaara5792f52005-11-23 21:25:05 +0000240 elseif m =~ attr
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000241 call add(res2, m)
Bram Moolenaara5792f52005-11-23 21:25:05 +0000242 endif
243 endfor
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000244 let menu = res + res2
245 let final_menu = []
246 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmlattrinfo')
247 for i in range(len(menu))
248 let item = menu[i]
249 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'], item)
250 let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][0]
251 let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][1]
252 else
253 let m_menu = ''
254 let m_info = ''
255 endif
256 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.'\)$'
257 let item = item
258 else
259 let item .= '="'
260 endif
261 let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}]
262 endfor
263 else
264 for i in range(len(menu))
265 let item = menu[i]
266 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.'\)$'
267 let item = item
268 else
269 let item .= '="'
270 endif
271 let final_menu += [item]
272 endfor
273 endif
274 return final_menu
Bram Moolenaara5792f52005-11-23 21:25:05 +0000275
276 endif
277 " Close tag
278 let b:unaryTagsStack = "base meta link hr br param img area input col"
279 if context =~ '^\/'
280 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
281 return [opentag.">"]
282 endif
283
284 " Complete elements of XML structure
285 " TODO: #REQUIRED, #IMPLIED, #FIXED, #PCDATA - but these should be detected like
286 " entities - in first run
287 " keywords: CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS
288 " are hardly recognizable but keep it in reserve
289 " also: EMPTY ANY SYSTEM PUBLIC DATA
290 if context =~ '^!'
291 let tags = ['!ELEMENT', '!DOCTYPE', '!ATTLIST', '!ENTITY', '!NOTATION', '![CDATA[', '![INCLUDE[', '![IGNORE[']
292
293 for m in tags
294 if m =~ '^'.context
295 let m = substitute(m, '^!\[\?', '', '')
296 call add(res, m)
297 elseif m =~ context
298 let m = substitute(m, '^!\[\?', '', '')
299 call add(res2, m)
300 endif
301 endfor
302
303 return res + res2
304
305 endif
306
307 " Complete text declaration
308 let g:co = context
309 if context =~ '^?'
310 let tags = ['?xml']
311
312 for m in tags
313 if m =~ '^'.context
314 call add(res, substitute(m, '^?', '', ''))
315 elseif m =~ context
316 call add(res, substitute(m, '^?', '', ''))
317 endif
318 endfor
319
320 return res + res2
321
322 endif
323
324 " Deal with tag completion.
325 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
326 let opentag = substitute(opentag, '^\k*:', '', '')
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000327 if opentag == ''
Bram Moolenaar7e8fd632006-02-18 22:14:51 +0000328 "return []
329 let tags = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]})
330 call filter(tags, 'v:val !~ "^vimxml"')
331 else
Bram Moolenaar910f66f2006-04-05 20:41:53 +0000332 if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
333 " Abandon when data file isn't complete
334 return []
335 endif
Bram Moolenaar7e8fd632006-02-18 22:14:51 +0000336 let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000337 endif
Bram Moolenaara5792f52005-11-23 21:25:05 +0000338
Bram Moolenaara5792f52005-11-23 21:25:05 +0000339 let context = substitute(context, '^\k*:', '', '')
340
Bram Moolenaara5792f52005-11-23 21:25:05 +0000341 for m in tags
342 if m =~ '^'.context
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000343 call add(res, m)
Bram Moolenaara5792f52005-11-23 21:25:05 +0000344 elseif m =~ context
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000345 call add(res2, m)
Bram Moolenaara5792f52005-11-23 21:25:05 +0000346 endif
347 endfor
Bram Moolenaar1d2ba7f2006-02-14 22:29:30 +0000348 let menu = res + res2
349 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmltaginfo')
350 let final_menu = []
351 for i in range(len(menu))
352 let item = menu[i]
353 if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'], item)
354 let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][0]
355 let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][1]
356 else
357 let m_menu = ''
358 let m_info = ''
359 endif
360 if b:xml_namespace == 'DEFAULT'
361 let xml_namespace = ''
362 else
363 let xml_namespace = b:xml_namespace.':'
364 endif
365 let final_menu += [{'word':xml_namespace.item, 'menu':m_menu, 'info':m_info}]
366 endfor
367 else
368 let final_menu = menu
369 endif
370 return final_menu
Bram Moolenaara5792f52005-11-23 21:25:05 +0000371
372 endif
373endfunction
374
375" MM: This is greatly reduced closetag.vim used with kind permission of Steven
376" Mueller
377" Changes: strip all comments; delete error messages; add checking for
378" namespace
379" Author: Steven Mueller <diffusor@ugcs.caltech.edu>
380" Last Modified: Tue May 24 13:29:48 PDT 2005
381" Version: 0.9.1
382
383function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
384 let linenum=line('.')
385 let lineend=col('.') - 1 " start: cursor position
386 let first=1 " flag for first line searched
387 let b:TagStack='' " main stack of tags
388 let startInComment=s:InComment()
389
390 if exists("b:xml_namespace")
391 if b:xml_namespace == 'DEFAULT'
392 let tagpat='</\=\(\k\|[.-]\)\+\|/>'
393 else
394 let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
395 endif
396 else
Bram Moolenaar9372a112005-12-06 19:59:18 +0000397 let tagpat='</\=\(\k\|[.-]\)\+\|/>'
Bram Moolenaara5792f52005-11-23 21:25:05 +0000398 endif
399 while (linenum>0)
400 let line=getline(linenum)
401 if first
402 let line=strpart(line,0,lineend)
403 else
404 let lineend=strlen(line)
405 endif
406 let b:lineTagStack=''
407 let mpos=0
408 let b:TagCol=0
409 while (mpos > -1)
410 let mpos=matchend(line,tagpat)
411 if mpos > -1
412 let b:TagCol=b:TagCol+mpos
413 let tag=matchstr(line,tagpat)
414
415 if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
416 let b:TagLine=linenum
417 call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
418 endif
419 let lineend=lineend-mpos
420 let line=strpart(line,mpos,lineend)
421 endif
422 endwhile
423 while (!s:EmptystackP('b:lineTagStack'))
424 let tag=s:Pop('b:lineTagStack')
425 if match(tag, '^/') == 0 "found end tag
426 call s:Push(tag,'b:TagStack')
427 elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack) "found unclosed tag
428 return tag
429 else
430 let endtag=s:Peekstack('b:TagStack')
431 if endtag == '/'.tag || endtag == '/'
432 call s:Pop('b:TagStack') "found a open/close tag pair
433 elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
434 return ''
435 endif
436 endif
437 endwhile
438 let linenum=linenum-1 | let first=0
439 endwhile
440return ''
441endfunction
442
443function! s:InComment()
Bram Moolenaar8b6144b2006-02-08 09:20:24 +0000444 return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
Bram Moolenaara5792f52005-11-23 21:25:05 +0000445endfunction
446
447function! s:InCommentAt(line, col)
Bram Moolenaar8b6144b2006-02-08 09:20:24 +0000448 return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
Bram Moolenaara5792f52005-11-23 21:25:05 +0000449endfunction
450
451function! s:SetKeywords()
452 let g:IsKeywordBak=&iskeyword
453 let &iskeyword='33-255'
454endfunction
455
456function! s:RestoreKeywords()
457 let &iskeyword=g:IsKeywordBak
458endfunction
459
460function! s:Push(el, sname)
461 if !s:EmptystackP(a:sname)
462 exe 'let '.a:sname."=a:el.' '.".a:sname
463 else
464 exe 'let '.a:sname.'=a:el'
465 endif
466endfunction
467
468function! s:EmptystackP(sname)
469 exe 'let stack='.a:sname
470 if match(stack,'^ *$') == 0
471 return 1
472 else
473 return 0
474 endif
475endfunction
476
477function! s:Instack(el, sname)
478 exe 'let stack='.a:sname
479 call s:SetKeywords()
480 let m=match(stack, '\<'.a:el.'\>')
481 call s:RestoreKeywords()
482 if m < 0
483 return 0
484 else
485 return 1
486 endif
487endfunction
488
489function! s:Peekstack(sname)
490 call s:SetKeywords()
491 exe 'let stack='.a:sname
492 let top=matchstr(stack, '\<.\{-1,}\>')
493 call s:RestoreKeywords()
494 return top
495endfunction
496
497function! s:Pop(sname)
498 if s:EmptystackP(a:sname)
499 return ''
500 endif
501 exe 'let stack='.a:sname
502 call s:SetKeywords()
503 let loc=matchend(stack,'\<.\{-1,}\>')
504 exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
505 let top=strpart(stack, match(stack, '\<'), loc)
506 call s:RestoreKeywords()
507 return top
508endfunction
509
510function! s:Clearstack(sname)
511 exe 'let '.a:sname."=''"
512endfunction