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