neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

xml.vim (8770B)


      1 " Language: XML
      2 " Maintainer: Christian Brabandt <cb@256bit.org>
      3 " Repository: https://github.com/chrisbra/vim-xml-ftplugin
      4 " Previous Maintainer: Johannes Zellner <johannes@zellner.org>
      5 " Last Changed: 2020 Nov 4th
      6 " Last Change:
      7 " 20200529 - Handle empty closing tags correctly
      8 " 20191202 - Handle docbk filetype
      9 " 20190726 - Correctly handle non-tagged data
     10 " 20190204 - correctly handle wrap tags
     11 "            https://github.com/chrisbra/vim-xml-ftplugin/issues/5
     12 " 20190128 - Make sure to find previous tag
     13 "            https://github.com/chrisbra/vim-xml-ftplugin/issues/4
     14 " 20181116 - Fix indentation when tags start with a colon or an underscore
     15 "            https://github.com/vim/vim/pull/926
     16 " 20181022 - Do not overwrite indentkeys setting
     17 "            https://github.com/chrisbra/vim-xml-ftplugin/issues/1
     18 " 20180724 - Correctly indent xml comments https://github.com/vim/vim/issues/3200
     19 "
     20 " Notes:
     21 "   1) does not indent pure non-xml code (e.g. embedded scripts)
     22 "       2) will be confused by unbalanced tags in comments
     23 "       or CDATA sections.
     24 "       2009-05-26 patch by Nikolai Weibull
     25 " TODO:     implement pre-like tags, see xml_indent_open / xml_indent_close
     26 
     27 " Only load this indent file when no other was loaded.
     28 if exists("b:did_indent")
     29    finish
     30 endif
     31 let b:did_indent = 1
     32 let s:keepcpo= &cpo
     33 set cpo&vim
     34 
     35 " [-- local settings (must come before aborting the script) --]
     36 " Attention: Parameter use_syntax_check is used by the docbk.vim indent script
     37 setlocal indentexpr=XmlIndentGet(v:lnum,1)
     38 setlocal indentkeys=o,O,*<Return>,<>>,<<>,/,{,},!^F
     39 " autoindent: used when the indentexpr returns -1
     40 setlocal autoindent
     41 
     42 let b:undo_indent = "setl ai< inde< indk<"
     43 
     44 if !exists('b:xml_indent_open')
     45    let b:xml_indent_open = '.\{-}<[:A-Z_a-z]'
     46    " pre tag, e.g. <address>
     47    " let b:xml_indent_open = '.\{-}<[/]\@!\(address\)\@!'
     48 endif
     49 
     50 if !exists('b:xml_indent_close')
     51    let b:xml_indent_close = '.\{-}</\|/>.\{-}'
     52    " end pre tag, e.g. </address>
     53    " let b:xml_indent_close = '.\{-}</\(address\)\@!'
     54 endif
     55 
     56 if !exists('b:xml_indent_continuation_filetype')
     57    let b:xml_indent_continuation_filetype = 'xml'
     58 endif
     59 
     60 let &cpo = s:keepcpo
     61 unlet s:keepcpo
     62 
     63 " [-- finish, if the function already exists --]
     64 if exists('*XmlIndentGet')
     65    finish
     66 endif
     67 
     68 let s:keepcpo= &cpo
     69 set cpo&vim
     70 
     71 fun! <SID>XmlIndentWithPattern(line, pat)
     72    let s = substitute('x'.a:line, a:pat, "\1", 'g')
     73    return strlen(substitute(s, "[^\1].*$", '', ''))
     74 endfun
     75 
     76 " [-- check if it's xml --]
     77 fun! <SID>XmlIndentSynCheck(lnum)
     78    if &syntax != ''
     79        let syn1 = synIDattr(synID(a:lnum, 1, 1), 'name')
     80        let syn2 = synIDattr(synID(a:lnum, strlen(getline(a:lnum)) - 1, 1), 'name')
     81        if syn1 != '' && syn1 !~ 'xml' && syn2 != '' && syn2 !~ 'xml'
     82            " don't indent pure non-xml code
     83            return 0
     84        endif
     85    endif
     86    return 1
     87 endfun
     88 
     89 " [-- return the sum of indents of a:lnum --]
     90 fun! <SID>XmlIndentSum(line, style, add)
     91    if <SID>IsXMLContinuation(a:line) &&
     92        \ a:style == 0 &&
     93        \ !<SID>IsXMLEmptyClosingTag(a:line)
     94        " no complete tag, add one additional indent level
     95        " but only for the current line
     96        return a:add + shiftwidth()
     97    elseif <SID>HasNoTagEnd(a:line)
     98        " no complete tag, return initial indent
     99        return a:add
    100    endif
    101    if a:style == match(a:line, '^\s*</')
    102        return (shiftwidth() *
    103        \  (<SID>XmlIndentWithPattern(a:line, b:xml_indent_open)
    104        \ - <SID>XmlIndentWithPattern(a:line, b:xml_indent_close)
    105        \ - <SID>XmlIndentWithPattern(a:line, '.\{-}/>'))) + a:add
    106    else
    107        return a:add
    108    endif
    109 endfun
    110 
    111 " Main indent function
    112 fun! XmlIndentGet(lnum, use_syntax_check)
    113    " Find a non-empty line above the current line.
    114    if prevnonblank(a:lnum - 1) == 0
    115        " Hit the start of the file, use zero indent.
    116        return 0
    117    endif
    118    " Find previous line with a tag (regardless whether open or closed,
    119    " but always restrict the match to a line before the current one
    120    " Note: xml declaration: <?xml version="1.0"?>
    121    "       won't be found, as it is not a legal tag name
    122    let ptag_pattern = '\%(.\{-}<[/:A-Z_a-z]\)'. '\%(\&\%<'. a:lnum .'l\)'
    123    let ptag = search(ptag_pattern, 'bnW')
    124    " no previous tag
    125    if ptag == 0
    126        return 0
    127    endif
    128 
    129    let pline = getline(ptag)
    130    let pind  = indent(ptag)
    131 
    132    let syn_name_start = '' " Syntax element at start of line (excluding whitespace)
    133    let syn_name_end = ''   " Syntax element at end of line
    134    let curline = getline(a:lnum)
    135    if a:use_syntax_check
    136        let check_lnum = <SID>XmlIndentSynCheck(ptag)
    137        let check_alnum = <SID>XmlIndentSynCheck(a:lnum)
    138        if check_lnum == 0 || check_alnum == 0
    139            return indent(a:lnum)
    140        endif
    141        let syn_name_end   = synIDattr(synID(a:lnum, strlen(curline) - 1, 1), 'name')
    142        let syn_name_start = synIDattr(synID(a:lnum, match(curline, '\S') + 1, 1), 'name')
    143        let prev_syn_name_end   = synIDattr(synID(ptag, strlen(pline) - 1, 1), 'name')
    144        " not needed (yet?)
    145        " let prev_syn_name_start = synIDattr(synID(ptag, match(pline, '\S') + 1, 1), 'name')
    146    endif
    147 
    148    if syn_name_end =~ 'Comment' && syn_name_start =~ 'Comment'
    149        return <SID>XmlIndentComment(a:lnum)
    150    elseif empty(syn_name_start) && empty(syn_name_end) && a:use_syntax_check
    151        " non-xml tag content: use indent from 'autoindent'
    152        if pline =~ b:xml_indent_close
    153            return pind
    154        elseif !empty(prev_syn_name_end)
    155            " only indent by an extra shiftwidth, if the previous line ends
    156            " with an XML like tag
    157           return pind + shiftwidth()
    158        else
    159            " no extra indent, looks like a text continuation line
    160           return pind
    161        endif
    162    elseif empty(syn_name_start) && syn_name_end =~? 'xmlTag'
    163        " Special case: such a line, shouldn't be indented, just because it
    164        " ends with a tag
    165        " 'foobar <i>inline tags</i>'
    166        if (match(curline, '<\([:a-zA-Z_]\+\)[^>]*>.*</\1>') > -1)
    167            return pind
    168        endif
    169    endif
    170 
    171    if curline =~ '^\s*</[a-zA-Z_]>'
    172        return <SID>ReturnIndentForMatchingTag(curline)
    173    endif
    174 
    175    " Get indent from previous tag line
    176    let ind = <SID>XmlIndentSum(pline, -1, pind)
    177    " Determine indent from current line
    178    let ind = <SID>XmlIndentSum(curline, 0, ind)
    179    return ind
    180 endfun
    181 
    182 func! <SID>IsXMLContinuation(line)
    183    " Checks, whether or not the line matches a start-of-tag
    184    return a:line !~ '^\s*<' && &ft =~# b:xml_indent_continuation_filetype
    185 endfunc
    186 
    187 func! <SID>HasNoTagEnd(line)
    188    " Checks whether or not the line matches '>' (so finishes a tag)
    189    return a:line !~ '>\s*$'
    190 endfunc
    191 
    192 func! <SID>IsXMLEmptyClosingTag(line)
    193    " Checks whether the line ends with an empty closing tag such as <lb/>
    194    return a:line =~? '<[^>]*/>\s*$'
    195 endfunc
    196 
    197 func! <SID>ReturnIndentForMatchingTag(line)
    198    " For a line with just a simple closing tag
    199    " get the indent from a matching opening tag
    200    if a:line =~? '^\s*</[a-z_]*>'
    201        let _c = getcursorpos()
    202        let pat = matchstr(a:line, '^\s*</\zs[a-z_]\+\ze>')
    203        " position cursor before the opening tag
    204        norm! 0
    205        " get the indent from the matching opening tag
    206        let match_line = searchpair('<' .. pat .. '>', '', '</' .. pat .. '>', 'bn')
    207        call setpos('.', _c)
    208        return indent(match_line)
    209    endif
    210 endfunc
    211 
    212 " return indent for a commented line,
    213 " the middle part might be indented one additional level
    214 func! <SID>XmlIndentComment(lnum)
    215    let ptagopen = search('.\{-}<[:A-Z_a-z]\_[^/]\{-}>.\{-}', 'bnW')
    216    let ptagclose = search(b:xml_indent_close, 'bnW')
    217    if getline(a:lnum) =~ '<!--'
    218        " if previous tag was a closing tag, do not add
    219        " one additional level of indent
    220        if ptagclose > ptagopen && a:lnum > ptagclose
    221            " If the previous tag was closed on the same line as it was
    222            " declared, we should indent with its indent level.
    223            if !<SID>IsXMLContinuation(getline(ptagclose))
    224                return indent(ptagclose)
    225            else
    226                return indent(ptagclose) - shiftwidth()
    227            endif
    228        elseif ptagclose == ptagopen
    229            return indent(ptagclose)
    230        else
    231            " start of comment, add one indentation level
    232            return indent(ptagopen) + shiftwidth()
    233        endif
    234    elseif getline(a:lnum) =~ '-->'
    235        " end of comment, same as start of comment
    236        return indent(search('<!--', 'bnW'))
    237    else
    238        " middle part of comment, add one additional level
    239        return indent(search('<!--', 'bnW')) + shiftwidth()
    240    endif
    241 endfunc
    242 
    243 let &cpo = s:keepcpo
    244 unlet s:keepcpo
    245 
    246 " vim:ts=4 et sts=-1 sw=0