1" Vim completion script
2" Language:	XML
3" Maintainer:	Mikolaj Machowski ( mikmach AT wp DOT pl )
4" Last Change:	2006 Mar 31
5
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 in ftplugin/xml.vim
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
57	let curline = line('.')
58    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)]
73	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 =~ '>[^<]*$' || i == curline
85				" Normal tag line, no need for completion at all
86				" OR reached first line without tag at all
87				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<.*')
96
97	" Make sure we will have only current namespace
98	unlet! b:xml_namespace
99	let b:xml_namespace = matchstr(b:compl_context, '^<\zs\k*\ze:')
100	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
115	if len(b:compl_context) == 0  && !exists("b:entitiescompl")
116		return []
117	endif
118	let context = matchstr(b:compl_context, '^<\zs.*')
119	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
140		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
155
156			return res + res2
157		endif
158
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
227			"
228			return []
229		else
230            if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
231				" Abandon when data file isn't complete
232 				return []
233 			endif
234			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
239				call add(res, m)
240			elseif m =~ attr
241				call add(res2, m)
242			endif
243		endfor
244		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
275
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*:', '', '')
327	if opentag == ''
328		"return []
329	    let tags = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]})
330		call filter(tags, 'v:val !~ "^vimxml"')
331	else
332		if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
333			" Abandon when data file isn't complete
334			return []
335		endif
336		let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
337	endif
338
339	let context = substitute(context, '^\k*:', '', '')
340
341	for m in tags
342		if m =~ '^'.context
343			call add(res, m)
344		elseif m =~ context
345			call add(res2, m)
346		endif
347	endfor
348	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
371
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 <[email protected]>
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
397		let tagpat='</\=\(\k\|[.-]\)\+\|/>'
398	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()
444	return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
445endfunction
446
447function! s:InCommentAt(line, col)
448	return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
449endfunction
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
513