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