blob: fc4ad7844e8cd0a829d8e507224269bd4c04dee2 [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 Moolenaard12f5c12006-01-25 22:10:52 +00004" Last Change: 2006 Jan 24
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
10" User interface will be provided by XMLns command defined ...
11" 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
84 elseif context_line =~ '>[^<]*$'
85 " Normal tag line, no need for completion at all
86 let b:compl_context = ''
87 break
88 endif
89 let i += 1
90 endwhile
91 " Make sure we don't have counter
92 unlet! i
93 endif
94 let b:compl_context = matchstr(b:compl_context, '.*\zs<.*')
Bram Moolenaara5792f52005-11-23 21:25:05 +000095
96 " Make sure we will have only current namespace
97 unlet! b:xml_namespace
Bram Moolenaard12f5c12006-01-25 22:10:52 +000098 let b:xml_namespace = matchstr(b:compl_context, '^<\zs\k*\ze:')
Bram Moolenaara5792f52005-11-23 21:25:05 +000099 if b:xml_namespace == ''
100 let b:xml_namespace = 'DEFAULT'
101 endif
102
103 return start
104
105 else
106 " There is no connction of namespace and data file. Abandon action
107 if !exists("g:xmldata_connection") || g:xmldata_connection == {}
108 return []
109 endif
110 " Initialize base return lists
111 let res = []
112 let res2 = []
113 " a:base is very short - we need context
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000114 if len(b:compl_context) == 0 && !exists("b:entitiescompl")
115 return []
116 endif
117 let context = matchstr(b:compl_context, '^<\zs.*')
Bram Moolenaara5792f52005-11-23 21:25:05 +0000118 unlet! b:compl_context
119
120 " Make entities completion
121 if exists("b:entitiescompl")
122 unlet! b:entitiescompl
123
124 if !exists("g:xmldata_entconnect") || g:xmldata_entconnect == 'DEFAULT'
125 let values = g:xmldata{'_'.g:xmldata_connection['DEFAULT']}['vimxmlentities']
126 else
127 let values = g:xmldata{'_'.g:xmldata_entconnect}['vimxmlentities']
128 endif
129
130 " Get only lines with entity declarations but throw out
131 " parameter-entities - they may be completed in future
132 let entdecl = filter(getline(1, "$"), 'v:val =~ "<!ENTITY\\s\\+[^%]"')
133
134 if len(entdecl) > 0
135 let intent = map(copy(entdecl), 'matchstr(v:val, "<!ENTITY\\s\\+\\zs\\(\\k\\|[.-:]\\)\\+\\ze")')
136 let values = intent + values
137 endif
138
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000139 if len(a:base) == 1
140 for m in values
141 if m =~ '^'.a:base
142 call add(res, m.';')
143 endif
144 endfor
145 return res
146 else
147 for m in values
148 if m =~? '^'.a:base
149 call add(res, m.';')
150 elseif m =~? a:base
151 call add(res2, m.';')
152 endif
153 endfor
Bram Moolenaara5792f52005-11-23 21:25:05 +0000154
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000155 return res + res2
156 endif
Bram Moolenaara5792f52005-11-23 21:25:05 +0000157
158 endif
159 if context =~ '>'
160 " Generally if context contains > it means we are outside of tag and
161 " should abandon action
162 return []
163 endif
164
165 " find tags matching with "a:base"
166 " If a:base contains white space it is attribute.
167 " It could be also value of attribute...
168 " We have to get first word to offer
169 " proper completions
170 if context == ''
171 let tag = ''
172 else
173 let tag = split(context)[0]
174 endif
175 " Get rid of namespace
176 let tag = substitute(tag, '^'.b:xml_namespace.':', '', '')
177
178
179 " Get last word, it should be attr name
180 let attr = matchstr(context, '.*\s\zs.*')
181 " Possible situations where any prediction would be difficult:
182 " 1. Events attributes
183 if context =~ '\s'
184
185 " If attr contains =\s*[\"'] we catched value of attribute
186 if attr =~ "=\s*[\"']"
187 " Let do attribute specific completion
188 let attrname = matchstr(attr, '.*\ze\s*=')
189 let entered_value = matchstr(attr, ".*=\\s*[\"']\\zs.*")
190
191 if tag =~ '^[?!]'
192 " Return nothing if we are inside of ! or ? tag
193 return []
194 else
195 let values = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][attrname]
196 endif
197
198 if len(values) == 0
199 return []
200 endif
201
202 " We need special version of sbase
203 let attrbase = matchstr(context, ".*[\"']")
204 let attrquote = matchstr(attrbase, '.$')
205
206 for m in values
207 " This if is needed to not offer all completions as-is
208 " alphabetically but sort them. Those beginning with entered
209 " part will be as first choices
210 if m =~ '^'.entered_value
211 call add(res, m . attrquote.' ')
212 elseif m =~ entered_value
213 call add(res2, m . attrquote.' ')
214 endif
215 endfor
216
217 return res + res2
218
219 endif
220
221 if tag =~ '?xml'
222 " Two possible arguments for <?xml> plus variation
223 let attrs = ['encoding', 'version="1.0"', 'version']
224 elseif tag =~ '^!'
225 " Don't make completion at all
226 return []
227 else
228 let attrs = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1])
229 endif
230
231 for m in sort(attrs)
232 if m =~ '^'.attr
233 if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m][0] =~ '^BOOL$'
234 call add(res, m)
235 elseif m =~ '='
236 call add(res, m)
237 else
238 call add(res, m.'="')
239 endif
240 elseif m =~ attr
241 if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m][0] =~ '^BOOL$'
242 call add(res, m)
243 elseif m =~ '='
244 call add(res, m)
245 else
246 call add(res2, m.'="')
247 endif
248 endif
249 endfor
250
251 return res + res2
252
253 endif
254 " Close tag
255 let b:unaryTagsStack = "base meta link hr br param img area input col"
256 if context =~ '^\/'
257 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
258 return [opentag.">"]
259 endif
260
261 " Complete elements of XML structure
262 " TODO: #REQUIRED, #IMPLIED, #FIXED, #PCDATA - but these should be detected like
263 " entities - in first run
264 " keywords: CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS
265 " are hardly recognizable but keep it in reserve
266 " also: EMPTY ANY SYSTEM PUBLIC DATA
267 if context =~ '^!'
268 let tags = ['!ELEMENT', '!DOCTYPE', '!ATTLIST', '!ENTITY', '!NOTATION', '![CDATA[', '![INCLUDE[', '![IGNORE[']
269
270 for m in tags
271 if m =~ '^'.context
272 let m = substitute(m, '^!\[\?', '', '')
273 call add(res, m)
274 elseif m =~ context
275 let m = substitute(m, '^!\[\?', '', '')
276 call add(res2, m)
277 endif
278 endfor
279
280 return res + res2
281
282 endif
283
284 " Complete text declaration
285 let g:co = context
286 if context =~ '^?'
287 let tags = ['?xml']
288
289 for m in tags
290 if m =~ '^'.context
291 call add(res, substitute(m, '^?', '', ''))
292 elseif m =~ context
293 call add(res, substitute(m, '^?', '', ''))
294 endif
295 endfor
296
297 return res + res2
298
299 endif
300
301 " Deal with tag completion.
302 let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
303 let opentag = substitute(opentag, '^\k*:', '', '')
Bram Moolenaard12f5c12006-01-25 22:10:52 +0000304 if opentag == ''
305 return []
306 endif
Bram Moolenaara5792f52005-11-23 21:25:05 +0000307
308 let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
309 let context = substitute(context, '^\k*:', '', '')
310
311 if b:xml_namespace == 'DEFAULT'
312 let b:xml_namespace = ''
313 else
314 let b:xml_namespace .= ':'
315 endif
316
317 for m in tags
318 if m =~ '^'.context
319 call add(res, b:xml_namespace.m)
320 elseif m =~ context
321 call add(res2, b:xml_namespace.m)
322 endif
323 endfor
324
325 return res + res2
326
327 endif
328endfunction
329
330" MM: This is greatly reduced closetag.vim used with kind permission of Steven
331" Mueller
332" Changes: strip all comments; delete error messages; add checking for
333" namespace
334" Author: Steven Mueller <diffusor@ugcs.caltech.edu>
335" Last Modified: Tue May 24 13:29:48 PDT 2005
336" Version: 0.9.1
337
338function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
339 let linenum=line('.')
340 let lineend=col('.') - 1 " start: cursor position
341 let first=1 " flag for first line searched
342 let b:TagStack='' " main stack of tags
343 let startInComment=s:InComment()
344
345 if exists("b:xml_namespace")
346 if b:xml_namespace == 'DEFAULT'
347 let tagpat='</\=\(\k\|[.-]\)\+\|/>'
348 else
349 let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
350 endif
351 else
Bram Moolenaar9372a112005-12-06 19:59:18 +0000352 let tagpat='</\=\(\k\|[.-]\)\+\|/>'
Bram Moolenaara5792f52005-11-23 21:25:05 +0000353 endif
354 while (linenum>0)
355 let line=getline(linenum)
356 if first
357 let line=strpart(line,0,lineend)
358 else
359 let lineend=strlen(line)
360 endif
361 let b:lineTagStack=''
362 let mpos=0
363 let b:TagCol=0
364 while (mpos > -1)
365 let mpos=matchend(line,tagpat)
366 if mpos > -1
367 let b:TagCol=b:TagCol+mpos
368 let tag=matchstr(line,tagpat)
369
370 if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
371 let b:TagLine=linenum
372 call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
373 endif
374 let lineend=lineend-mpos
375 let line=strpart(line,mpos,lineend)
376 endif
377 endwhile
378 while (!s:EmptystackP('b:lineTagStack'))
379 let tag=s:Pop('b:lineTagStack')
380 if match(tag, '^/') == 0 "found end tag
381 call s:Push(tag,'b:TagStack')
382 elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack) "found unclosed tag
383 return tag
384 else
385 let endtag=s:Peekstack('b:TagStack')
386 if endtag == '/'.tag || endtag == '/'
387 call s:Pop('b:TagStack') "found a open/close tag pair
388 elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
389 return ''
390 endif
391 endif
392 endwhile
393 let linenum=linenum-1 | let first=0
394 endwhile
395return ''
396endfunction
397
398function! s:InComment()
399 return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment'
400endfunction
401
402function! s:InCommentAt(line, col)
403 return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment'
404endfunction
405
406function! s:SetKeywords()
407 let g:IsKeywordBak=&iskeyword
408 let &iskeyword='33-255'
409endfunction
410
411function! s:RestoreKeywords()
412 let &iskeyword=g:IsKeywordBak
413endfunction
414
415function! s:Push(el, sname)
416 if !s:EmptystackP(a:sname)
417 exe 'let '.a:sname."=a:el.' '.".a:sname
418 else
419 exe 'let '.a:sname.'=a:el'
420 endif
421endfunction
422
423function! s:EmptystackP(sname)
424 exe 'let stack='.a:sname
425 if match(stack,'^ *$') == 0
426 return 1
427 else
428 return 0
429 endif
430endfunction
431
432function! s:Instack(el, sname)
433 exe 'let stack='.a:sname
434 call s:SetKeywords()
435 let m=match(stack, '\<'.a:el.'\>')
436 call s:RestoreKeywords()
437 if m < 0
438 return 0
439 else
440 return 1
441 endif
442endfunction
443
444function! s:Peekstack(sname)
445 call s:SetKeywords()
446 exe 'let stack='.a:sname
447 let top=matchstr(stack, '\<.\{-1,}\>')
448 call s:RestoreKeywords()
449 return top
450endfunction
451
452function! s:Pop(sname)
453 if s:EmptystackP(a:sname)
454 return ''
455 endif
456 exe 'let stack='.a:sname
457 call s:SetKeywords()
458 let loc=matchend(stack,'\<.\{-1,}\>')
459 exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
460 let top=strpart(stack, match(stack, '\<'), loc)
461 call s:RestoreKeywords()
462 return top
463endfunction
464
465function! s:Clearstack(sname)
466 exe 'let '.a:sname."=''"
467endfunction