1" Vim completion script 2" Language: XML 3" Maintainer: Mikolaj Machowski ( mikmach AT wp DOT pl ) 4" Last Change: 2013 Jun 29 5" Version: 1.9 6" 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 12 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 21function! xmlcomplete#CreateConnection(canonical, ...) " {{{ 22 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 30 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" 35 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 43 44 " We need to initialize Dictionary to add key-value pair 45 if !exists("g:xmldata_connection") 46 let g:xmldata_connection = {} 47 endif 48 49 let g:xmldata_connection[users] = a:canonical 50 51endfunction 52" }}} 53 54function! xmlcomplete#CreateEntConnection(...) " {{{ 55 if a:0 > 0 56 let g:xmldata_entconnect = a:1 57 else 58 let g:xmldata_entconnect = 'DEFAULT' 59 endif 60endfunction 61" }}} 62 63function! 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 70 71 while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)' 72 let start -= 1 73 endwhile 74 75 if start >= 0 && line[start - 1] =~ '&' 76 let b:entitiescompl = 1 77 let b:compl_context = '' 78 return start 79 endif 80 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<.*') 105 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 112 113 return start 114 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 136 137 " Make entities completion 138 if exists("b:entitiescompl") 139 unlet! b:entitiescompl 140 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 146 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\\+[^%]"') 150 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 155 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 171 172 return res + res2 173 endif 174 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 181 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.':', '', '') 194 195 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' 201 202 " If attr contains =\s*[\"'] we catch 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.*") 207 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 218 219 if len(values) == 0 220 return [] 221 endif 222 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 232 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 243 244 return res + res2 245 246 endif 247 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 262 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 301 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 309 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['] 318 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 328 329 return res + res2 330 331 endif 332 333 " Complete text declaration 334 if context =~ '^?' 335 let tags = ['?xml'] 336 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 344 345 return res + res2 346 347 endif 348 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 363 364 let context = substitute(context, '^\k*:', '', '') 365 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 395 396 return final_menu 397 398 endif 399endfunction 400 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 408 409function! 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() 415 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) 440 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 466return '' 467endfunction 468 469function! s:InComment() 470 return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String' 471endfunction 472 473function! s:InCommentAt(line, col) 474 return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String' 475endfunction 476 477function! s:SetKeywords() 478 let s:IsKeywordBak=&l:iskeyword 479 let &l:iskeyword='33-255' 480endfunction 481 482function! s:RestoreKeywords() 483 let &l:iskeyword=s:IsKeywordBak 484endfunction 485 486function! 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 492endfunction 493 494function! s:EmptystackP(sname) 495 exe 'let stack='.a:sname 496 if match(stack,'^ *$') == 0 497 return 1 498 else 499 return 0 500 endif 501endfunction 502 503function! 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 513endfunction 514 515function! 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 521endfunction 522 523function! 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 534endfunction 535 536function! s:Clearstack(sname) 537 exe 'let '.a:sname."=''" 538endfunction 539" vim:set foldmethod=marker: 540