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