neovim

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

matchit.vim (31253B)


      1 "  matchit.vim: (global plugin) Extended "%" matching
      2 "  autload script of matchit plugin, see ../plugin/matchit.vim
      3 "  Last Change: Jan 09, 2026
      4 
      5 " Neovim does not support scriptversion
      6 if has("vimscript-4")
      7  scriptversion 4
      8 endif
      9 
     10 let s:last_mps = ""
     11 let s:last_words = ":"
     12 let s:patBR = ""
     13 
     14 let s:save_cpo = &cpo
     15 set cpo&vim
     16 
     17 " Auto-complete mappings:  (not yet "ready for prime time")
     18 " TODO Read :help write-plugin for the "right" way to let the user
     19 " specify a key binding.
     20 "   let g:match_auto = '<C-]>'
     21 "   let g:match_autoCR = '<C-CR>'
     22 " if exists("g:match_auto")
     23 "   execute "inoremap " . g:match_auto . ' x<Esc>"=<SID>Autocomplete()<CR>Pls'
     24 " endif
     25 " if exists("g:match_autoCR")
     26 "   execute "inoremap " . g:match_autoCR . ' <CR><C-R>=<SID>Autocomplete()<CR>'
     27 " endif
     28 " if exists("g:match_gthhoh")
     29 "   execute "inoremap " . g:match_gthhoh . ' <C-O>:call <SID>Gthhoh()<CR>'
     30 " endif " gthhoh = "Get the heck out of here!"
     31 
     32 let s:notslash = '\\\@1<!\%(\\\\\)*'
     33 
     34 function s:RestoreOptions()
     35  " In s:CleanUp(), :execute "set" restore_options .
     36  let restore_options = ""
     37  if get(b:, 'match_ignorecase', &ic) != &ic
     38    let restore_options ..= (&ic ? " " : " no") .. "ignorecase"
     39    let &ignorecase = b:match_ignorecase
     40  endif
     41  if &ve != ''
     42    let restore_options = " ve=" .. &ve .. restore_options
     43    set ve=
     44  endif
     45  if &smartcase
     46    let restore_options = " smartcase " .. restore_options
     47    set nosmartcase
     48  endif
     49  return restore_options
     50 endfunction
     51 
     52 function matchit#Match_wrapper(word, forward, mode) range
     53  let restore_options = s:RestoreOptions()
     54  " In s:CleanUp(), we may need to check whether the cursor moved forward.
     55  let startpos = [line("."), col(".")]
     56  " if a count has been applied, use the default [count]% mode (see :h N%)
     57  if v:count
     58    exe "normal! " .. v:count .. "%"
     59    return s:CleanUp(restore_options, a:mode, startpos)
     60  end
     61  if a:mode =~# "v" && mode(1) =~# 'ni'
     62    exe "norm! gv"
     63  elseif a:mode == "o" && mode(1) !~# '[vV]'
     64    exe "norm! v"
     65  " If this function was called from Visual mode, make sure that the cursor
     66  " is at the correct end of the Visual range:
     67  elseif a:mode == "v"
     68    execute "normal! gv\<Esc>"
     69    let startpos = [line("."), col(".")]
     70  endif
     71 
     72  " Check for custom match function hook
     73  if exists("b:match_function")
     74    try
     75      let result = call(b:match_function, [a:forward])
     76      if !empty(result)
     77        call cursor(result)
     78        return s:CleanUp(restore_options, a:mode, startpos)
     79      endif
     80    catch /.*/
     81      if exists("b:match_debug")
     82        echohl WarningMsg
     83        echom 'matchit: b:match_function error: ' .. v:exception
     84        echohl NONE
     85      endif
     86      return s:CleanUp(restore_options, a:mode, startpos)
     87    endtry
     88    " Empty result: fall through to regular matching
     89  endif
     90 
     91  " First step:  if not already done, set the script variables
     92  "   s:do_BR   flag for whether there are backrefs
     93  "   s:pat     parsed version of b:match_words
     94  "   s:all     regexp based on s:pat and the default groups
     95  if !exists("b:match_words") || b:match_words == ""
     96    let match_words = ""
     97  elseif b:match_words =~ ":"
     98    let match_words = b:match_words
     99  else
    100    " Allow b:match_words = "GetVimMatchWords()" .
    101    execute "let match_words =" b:match_words
    102  endif
    103 " Thanks to Preben "Peppe" Guldberg and Bram Moolenaar for this suggestion!
    104  if (match_words != s:last_words) || (&mps != s:last_mps)
    105      \ || exists("b:match_debug")
    106    let s:last_mps = &mps
    107    " quote the special chars in 'matchpairs', replace [,:] with \| and then
    108    " append the builtin pairs (/*, */, #if, #ifdef, #ifndef, #else, #elif,
    109    " #elifdef, #elifndef, #endif)
    110    let default = escape(&mps, '[$^.*~\\/?]') .. (strlen(&mps) ? "," : "") ..
    111      \ '\/\*:\*\/,#\s*if\%(n\=def\)\=:#\s*else\>:#\s*elif\%(n\=def\)\=\>:#\s*endif\>'
    112    " s:all = pattern with all the keywords
    113    let match_words = s:Append(match_words, default)
    114    let s:last_words = match_words
    115    if match_words !~ s:notslash .. '\\\d'
    116      let s:do_BR = 0
    117      let s:pat = match_words
    118    else
    119      let s:do_BR = 1
    120      let s:pat = s:ParseWords(match_words)
    121    endif
    122    let s:all = substitute(s:pat, s:notslash .. '\zs[,:]\+', '\\|', 'g')
    123    " un-escape \, and \: to , and :
    124    let s:all = substitute(s:all, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    125    " Just in case there are too many '\(...)' groups inside the pattern, make
    126    " sure to use \%(...) groups, so that error E872 can be avoided
    127    let s:all = substitute(s:all, '\\(', '\\%(', 'g')
    128    let s:all = '\%(' .. s:all .. '\)'
    129    if exists("b:match_debug")
    130      let b:match_pat = s:pat
    131    endif
    132    " Reconstruct the version with unresolved backrefs.
    133    let s:patBR = substitute(match_words .. ',',
    134      \ s:notslash .. '\zs[,:]*,[,:]*', ',', 'g')
    135    let s:patBR = substitute(s:patBR, s:notslash .. '\zs:\{2,}', ':', 'g')
    136    " un-escape \, to ,
    137    let s:patBR = substitute(s:patBR, '\\,', ',', 'g')
    138  endif
    139 
    140  " Second step:  set the following local variables:
    141  "     matchline = line on which the cursor started
    142  "     curcol    = number of characters before match
    143  "     prefix    = regexp for start of line to start of match
    144  "     suffix    = regexp for end of match to end of line
    145  " Require match to end on or after the cursor and prefer it to
    146  " start on or before the cursor.
    147  let matchline = getline(startpos[0])
    148  if a:word != ''
    149    " word given
    150    if a:word !~ s:all
    151      echohl WarningMsg|echo 'Missing rule for word:"'.a:word.'"'|echohl NONE
    152      return s:CleanUp(restore_options, a:mode, startpos)
    153    endif
    154    let matchline = a:word
    155    let curcol = 0
    156    let prefix = '^\%('
    157    let suffix = '\)$'
    158  " Now the case when "word" is not given
    159  else  " Find the match that ends on or after the cursor and set curcol.
    160    let regexp = s:Wholematch(matchline, s:all, startpos[1]-1)
    161    let curcol = match(matchline, regexp)
    162    " If there is no match, give up.
    163    if curcol == -1
    164      return s:CleanUp(restore_options, a:mode, startpos)
    165    endif
    166    let endcol = matchend(matchline, regexp)
    167    let suf = strlen(matchline) - endcol
    168    let prefix = (curcol ? '^.*\%' .. (curcol + 1) .. 'c\%(' : '^\%(')
    169    let suffix = (suf ? '\)\%' .. (endcol + 1) .. 'c.*$'  : '\)$')
    170  endif
    171  if exists("b:match_debug")
    172    let b:match_match = matchstr(matchline, regexp)
    173    let b:match_col = curcol+1
    174  endif
    175 
    176  " Third step:  Find the group and single word that match, and the original
    177  " (backref) versions of these.  Then, resolve the backrefs.
    178  " Set the following local variable:
    179  " group = colon-separated list of patterns, one of which matches
    180  "       = ini:mid:fin or ini:fin
    181  "
    182  " Now, set group and groupBR to the matching group: 'if:endif' or
    183  " 'while:endwhile' or whatever.  A bit of a kluge:  s:Choose() returns
    184  " group . "," . groupBR, and we pick it apart.
    185  let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, s:patBR)
    186  let i = matchend(group, s:notslash .. ",")
    187  let groupBR = strpart(group, i)
    188  let group = strpart(group, 0, i-1)
    189  " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix
    190  if s:do_BR " Do the hard part:  resolve those backrefs!
    191    let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline)
    192  endif
    193  if exists("b:match_debug")
    194    let b:match_wholeBR = groupBR
    195    let i = matchend(groupBR, s:notslash .. ":")
    196    let b:match_iniBR = strpart(groupBR, 0, i-1)
    197  endif
    198 
    199  " Fourth step:  Set the arguments for searchpair().
    200  let i = matchend(group, s:notslash .. ":")
    201  let j = matchend(group, '.*' .. s:notslash .. ":")
    202  let ini = strpart(group, 0, i-1)
    203  let mid = substitute(strpart(group, i,j-i-1), s:notslash .. '\zs:', '\\|', 'g')
    204  let fin = strpart(group, j)
    205  "Un-escape the remaining , and : characters.
    206  let ini = substitute(ini, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    207  let mid = substitute(mid, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    208  let fin = substitute(fin, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    209  " searchpair() requires that these patterns avoid \(\) groups.
    210  let ini = substitute(ini, s:notslash .. '\zs\\(', '\\%(', 'g')
    211  let mid = substitute(mid, s:notslash .. '\zs\\(', '\\%(', 'g')
    212  let fin = substitute(fin, s:notslash .. '\zs\\(', '\\%(', 'g')
    213  " Set mid.  This is optimized for readability, not micro-efficiency!
    214  if a:forward && matchline =~ prefix .. fin .. suffix
    215    \ || !a:forward && matchline =~ prefix .. ini .. suffix
    216    let mid = ""
    217  endif
    218  " Set flag.  This is optimized for readability, not micro-efficiency!
    219  if a:forward && matchline =~ prefix .. fin .. suffix
    220    \ || !a:forward && matchline !~ prefix .. ini .. suffix
    221    let flag = "bW"
    222  else
    223    let flag = "W"
    224  endif
    225  " Set skip.
    226  if exists("b:match_skip")
    227    let skip = b:match_skip
    228  elseif exists("b:match_comment") " backwards compatibility and testing!
    229    let skip = "r:" .. b:match_comment
    230  else
    231    let skip = 's:comment\|string'
    232  endif
    233  let skip = s:ParseSkip(skip)
    234  if exists("b:match_debug")
    235    let b:match_ini = ini
    236    let b:match_tail = (strlen(mid) ? mid .. '\|' : '') .. fin
    237  endif
    238 
    239  " Fifth step:  actually start moving the cursor and call searchpair().
    240  " Later, :execute restore_cursor to get to the original screen.
    241  let view = winsaveview()
    242  call cursor(0, curcol + 1)
    243  if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on"))
    244        \ || skip =~ 'v:lua.vim.treesitter' && !exists('b:ts_highlight')
    245    let skip = "0"
    246  else
    247    execute "if " .. skip .. "| let skip = '0' | endif"
    248  endif
    249  let sp_return = searchpair(ini, mid, fin, flag, skip)
    250  if &selection isnot# 'inclusive' && a:mode == 'v'
    251    " move cursor one pos to the right, because selection is not inclusive
    252    " add virtualedit=onemore, to make it work even when the match ends the
    253    " line
    254    if !(col('.') < col('$')-1)
    255      let eolmark=1 " flag to set a mark on eol (since we cannot move there)
    256    endif
    257    norm! l
    258  endif
    259  let final_position = "call cursor(" .. line(".") .. "," .. col(".") .. ")"
    260  " Restore cursor position and original screen.
    261  call winrestview(view)
    262  normal! m'
    263  if sp_return > 0
    264    execute final_position
    265  endif
    266  if exists('eolmark') && eolmark
    267    call setpos("''", [0, line('.'), col('$'), 0]) " set mark on the eol
    268  endif
    269  return s:CleanUp(restore_options, a:mode, startpos, mid .. '\|' .. fin)
    270 endfun
    271 
    272 " Restore options and do some special handling for Operator-pending mode.
    273 " The optional argument is the tail of the matching group.
    274 fun! s:CleanUp(options, mode, startpos, ...)
    275  if strlen(a:options)
    276    execute "set" a:options
    277  endif
    278  " Open folds, if appropriate.
    279  if a:mode != "o"
    280    if &foldopen =~ "percent"
    281      normal! zv
    282    endif
    283    " In Operator-pending mode, we want to include the whole match
    284    " (for example, d%).
    285    " This is only a problem if we end up moving in the forward direction.
    286  elseif (a:startpos[0] < line(".")) ||
    287        \ (a:startpos[0] == line(".") && a:startpos[1] < col("."))
    288    if a:0
    289      " Check whether the match is a single character.  If not, move to the
    290      " end of the match.
    291      let matchline = getline(".")
    292      let currcol = col(".")
    293      let regexp = s:Wholematch(matchline, a:1, currcol-1)
    294      let endcol = matchend(matchline, regexp)
    295      if endcol > currcol  " This is NOT off by one!
    296        call cursor(0, endcol)
    297      endif
    298    endif " a:0
    299  endif " a:mode != "o" && etc.
    300  return 0
    301 endfun
    302 
    303 " Example (simplified HTML patterns):  if
    304 "   a:groupBR   = '<\(\k\+\)>:</\1>'
    305 "   a:prefix    = '^.\{3}\('
    306 "   a:group     = '<\(\k\+\)>:</\(\k\+\)>'
    307 "   a:suffix    = '\).\{2}$'
    308 "   a:matchline =  "123<tag>12" or "123</tag>12"
    309 " then extract "tag" from a:matchline and return "<tag>:</tag>" .
    310 fun! s:InsertRefs(groupBR, prefix, group, suffix, matchline)
    311  if a:matchline !~ a:prefix ..
    312    \ substitute(a:group, s:notslash .. '\zs:', '\\|', 'g') .. a:suffix
    313    return a:group
    314  endif
    315  let i = matchend(a:groupBR, s:notslash .. ':')
    316  let ini = strpart(a:groupBR, 0, i-1)
    317  let tailBR = strpart(a:groupBR, i)
    318  let word = s:Choose(a:group, a:matchline, ":", "", a:prefix, a:suffix,
    319    \ a:groupBR)
    320  let i = matchend(word, s:notslash .. ":")
    321  let wordBR = strpart(word, i)
    322  let word = strpart(word, 0, i-1)
    323  " Now, a:matchline =~ a:prefix . word . a:suffix
    324  if wordBR != ini
    325    let table = s:Resolve(ini, wordBR, "table")
    326  else
    327    let table = ""
    328    let d = 0
    329    while d < 10
    330      if tailBR =~ s:notslash .. '\\' .. d
    331        let table = table .. d
    332      else
    333        let table = table .. "-"
    334      endif
    335      let d = d + 1
    336    endwhile
    337  endif
    338  let d = 9
    339  while d
    340    if table[d] != "-"
    341      let backref = substitute(a:matchline, a:prefix .. word .. a:suffix,
    342        \ '\' .. table[d], "")
    343        " Are there any other characters that should be escaped?
    344      let backref = escape(backref, '*,:')
    345      execute s:Ref(ini, d, "start", "len")
    346      let ini = strpart(ini, 0, start) .. backref .. strpart(ini, start+len)
    347      let tailBR = substitute(tailBR, s:notslash .. '\zs\\' .. d,
    348        \ escape(backref, '\\&'), 'g')
    349    endif
    350    let d = d-1
    351  endwhile
    352  if exists("b:match_debug")
    353    if s:do_BR
    354      let b:match_table = table
    355      let b:match_word = word
    356    else
    357      let b:match_table = ""
    358      let b:match_word = ""
    359    endif
    360  endif
    361  return ini .. ":" .. tailBR
    362 endfun
    363 
    364 " String append item2 to item and add ',' in between items
    365 fun! s:Append(item, item2)
    366  if a:item == ''
    367    return a:item2
    368  endif
    369  " there is already a trailing comma, don't add another one
    370  if a:item[-1:] == ','
    371    return a:item .. a:item2
    372  endif
    373  return a:item .. ',' .. a:item2
    374 endfun
    375 
    376 " Input a comma-separated list of groups with backrefs, such as
    377 "   a:groups = '\(foo\):end\1,\(bar\):end\1'
    378 " and return a comma-separated list of groups with backrefs replaced:
    379 "   return '\(foo\):end\(foo\),\(bar\):end\(bar\)'
    380 fun! s:ParseWords(groups)
    381  let groups = substitute(a:groups .. ",", s:notslash .. '\zs[,:]*,[,:]*', ',', 'g')
    382  let groups = substitute(groups, s:notslash .. '\zs:\{2,}', ':', 'g')
    383  let parsed = ""
    384  while groups =~ '[^,:]'
    385    let i = matchend(groups, s:notslash .. ':')
    386    let j = matchend(groups, s:notslash .. ',')
    387    let ini = strpart(groups, 0, i-1)
    388    let tail = strpart(groups, i, j-i-1) .. ":"
    389    let groups = strpart(groups, j)
    390    let parsed = parsed .. ini
    391    let i = matchend(tail, s:notslash .. ':')
    392    while i != -1
    393      " In 'if:else:endif', ini='if' and word='else' and then word='endif'.
    394      let word = strpart(tail, 0, i-1)
    395      let tail = strpart(tail, i)
    396      let i = matchend(tail, s:notslash .. ':')
    397      let parsed = parsed .. ":" .. s:Resolve(ini, word, "word")
    398    endwhile " Now, tail has been used up.
    399    let parsed = parsed .. ","
    400  endwhile " groups =~ '[^,:]'
    401  let parsed = substitute(parsed, ',$', '', '')
    402  return parsed
    403 endfun
    404 
    405 " TODO I think this can be simplified and/or made more efficient.
    406 " TODO What should I do if a:start is out of range?
    407 " Return a regexp that matches all of a:string, such that
    408 " matchstr(a:string, regexp) represents the match for a:pat that starts
    409 " as close to a:start as possible, before being preferred to after, and
    410 " ends after a:start .
    411 " Usage:
    412 " let regexp = s:Wholematch(getline("."), 'foo\|bar', col(".")-1)
    413 " let i      = match(getline("."), regexp)
    414 " let j      = matchend(getline("."), regexp)
    415 " let match  = matchstr(getline("."), regexp)
    416 fun! s:Wholematch(string, pat, start)
    417  let group = '\%(' .. a:pat .. '\)'
    418  let prefix = (a:start ? '\(^.*\%<' .. (a:start + 2) .. 'c\)\zs' : '^')
    419  let len = strlen(a:string)
    420  let suffix = (a:start+1 < len ? '\(\%>' .. (a:start+1) .. 'c.*$\)\@=' : '$')
    421  if a:string !~ prefix .. group .. suffix
    422    let prefix = ''
    423  endif
    424  return prefix .. group .. suffix
    425 endfun
    426 
    427 " No extra arguments:  s:Ref(string, d) will
    428 " find the d'th occurrence of '\(' and return it, along with everything up
    429 " to and including the matching '\)'.
    430 " One argument:  s:Ref(string, d, "start") returns the index of the start
    431 " of the d'th '\(' and any other argument returns the length of the group.
    432 " Two arguments:  s:Ref(string, d, "foo", "bar") returns a string to be
    433 " executed, having the effect of
    434 "   :let foo = s:Ref(string, d, "start")
    435 "   :let bar = s:Ref(string, d, "len")
    436 fun! s:Ref(string, d, ...)
    437  let len = strlen(a:string)
    438  if a:d == 0
    439    let start = 0
    440  else
    441    let cnt = a:d
    442    let match = a:string
    443    while cnt
    444      let cnt = cnt - 1
    445      let index = matchend(match, s:notslash .. '\\(')
    446      if index == -1
    447        return ""
    448      endif
    449      let match = strpart(match, index)
    450    endwhile
    451    let start = len - strlen(match)
    452    if a:0 == 1 && a:1 == "start"
    453      return start - 2
    454    endif
    455    let cnt = 1
    456    while cnt
    457      let index = matchend(match, s:notslash .. '\\(\|\\)') - 1
    458      if index == -2
    459        return ""
    460      endif
    461      " Increment if an open, decrement if a ')':
    462      let cnt = cnt + (match[index]=="(" ? 1 : -1)  " ')'
    463      let match = strpart(match, index+1)
    464    endwhile
    465    let start = start - 2
    466    let len = len - start - strlen(match)
    467  endif
    468  if a:0 == 1
    469    return len
    470  elseif a:0 == 2
    471    return "let " .. a:1 .. "=" .. start .. "| let " .. a:2 .. "=" .. len
    472  else
    473    return strpart(a:string, start, len)
    474  endif
    475 endfun
    476 
    477 " Count the number of disjoint copies of pattern in string.
    478 " If the pattern is a literal string and contains no '0' or '1' characters
    479 " then s:Count(string, pattern, '0', '1') should be faster than
    480 " s:Count(string, pattern).
    481 fun! s:Count(string, pattern, ...)
    482  let pat = escape(a:pattern, '\\')
    483  if a:0 > 1
    484    let foo = substitute(a:string, '[^' .. a:pattern .. ']', "a:1", "g")
    485    let foo = substitute(a:string, pat, a:2, "g")
    486    let foo = substitute(foo, '[^' .. a:2 .. ']', "", "g")
    487    return strlen(foo)
    488  endif
    489  let result = 0
    490  let foo = a:string
    491  let index = matchend(foo, pat)
    492  while index != -1
    493    let result = result + 1
    494    let foo = strpart(foo, index)
    495    let index = matchend(foo, pat)
    496  endwhile
    497  return result
    498 endfun
    499 
    500 " s:Resolve('\(a\)\(b\)', '\(c\)\2\1\1\2') should return table.word, where
    501 " word = '\(c\)\(b\)\(a\)\3\2' and table = '-32-------'.  That is, the first
    502 " '\1' in target is replaced by '\(a\)' in word, table[1] = 3, and this
    503 " indicates that all other instances of '\1' in target are to be replaced
    504 " by '\3'.  The hard part is dealing with nesting...
    505 " Note that ":" is an illegal character for source and target,
    506 " unless it is preceded by "\".
    507 fun! s:Resolve(source, target, output)
    508  let word = a:target
    509  let i = matchend(word, s:notslash .. '\\\d') - 1
    510  let table = "----------"
    511  while i != -2 " There are back references to be replaced.
    512    let d = word[i]
    513    let backref = s:Ref(a:source, d)
    514    " The idea is to replace '\d' with backref.  Before we do this,
    515    " replace any \(\) groups in backref with :1, :2, ... if they
    516    " correspond to the first, second, ... group already inserted
    517    " into backref.  Later, replace :1 with \1 and so on.  The group
    518    " number w+b within backref corresponds to the group number
    519    " s within a:source.
    520    " w = number of '\(' in word before the current one
    521    let w = s:Count(
    522    \ substitute(strpart(word, 0, i-1), '\\\\', '', 'g'), '\(', '1')
    523    let b = 1 " number of the current '\(' in backref
    524    let s = d " number of the current '\(' in a:source
    525    while b <= s:Count(substitute(backref, '\\\\', '', 'g'), '\(', '1')
    526    \ && s < 10
    527      if table[s] == "-"
    528        if w + b < 10
    529          " let table[s] = w + b
    530          let table = strpart(table, 0, s) .. (w+b) .. strpart(table, s+1)
    531        endif
    532        let b = b + 1
    533        let s = s + 1
    534      else
    535        execute s:Ref(backref, b, "start", "len")
    536        let ref = strpart(backref, start, len)
    537        let backref = strpart(backref, 0, start) .. ":" .. table[s]
    538        \ .. strpart(backref, start+len)
    539        let s = s + s:Count(substitute(ref, '\\\\', '', 'g'), '\(', '1')
    540      endif
    541    endwhile
    542    let word = strpart(word, 0, i-1) .. backref .. strpart(word, i+1)
    543    let i = matchend(word, s:notslash .. '\\\d') - 1
    544  endwhile
    545  let word = substitute(word, s:notslash .. '\zs:', '\\', 'g')
    546  if a:output == "table"
    547    return table
    548  elseif a:output == "word"
    549    return word
    550  else
    551    return table .. word
    552  endif
    553 endfun
    554 
    555 " Assume a:comma = ",".  Then the format for a:patterns and a:1 is
    556 "   a:patterns = "<pat1>,<pat2>,..."
    557 "   a:1 = "<alt1>,<alt2>,..."
    558 " If <patn> is the first pattern that matches a:string then return <patn>
    559 " if no optional arguments are given; return <patn>,<altn> if a:1 is given.
    560 fun! s:Choose(patterns, string, comma, branch, prefix, suffix, ...)
    561  let tail = (a:patterns =~ a:comma .. "$" ? a:patterns : a:patterns .. a:comma)
    562  let i = matchend(tail, s:notslash .. a:comma)
    563  if a:0
    564    let alttail = (a:1 =~ a:comma .. "$" ? a:1 : a:1 .. a:comma)
    565    let j = matchend(alttail, s:notslash .. a:comma)
    566  endif
    567  let current = strpart(tail, 0, i-1)
    568  if a:branch == ""
    569    let currpat = current
    570  else
    571    let currpat = substitute(current, s:notslash .. a:branch, '\\|', 'g')
    572  endif
    573  " un-escape \, and \: to , and :
    574  let currpat = substitute(currpat, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    575  while a:string !~ a:prefix .. currpat .. a:suffix
    576    let tail = strpart(tail, i)
    577    let i = matchend(tail, s:notslash .. a:comma)
    578    if i == -1
    579      return -1
    580    endif
    581    let current = strpart(tail, 0, i-1)
    582    if a:branch == ""
    583      let currpat = current
    584    else
    585      let currpat = substitute(current, s:notslash .. a:branch, '\\|', 'g')
    586    endif
    587    " un-escape \, and \: to , and :
    588    let currpat = substitute(currpat, s:notslash .. '\zs\\\(:\|,\)', '\1', 'g')
    589    if a:0
    590      let alttail = strpart(alttail, j)
    591      let j = matchend(alttail, s:notslash .. a:comma)
    592    endif
    593  endwhile
    594  if a:0
    595    let current = current .. a:comma .. strpart(alttail, 0, j-1)
    596  endif
    597  return current
    598 endfun
    599 
    600 fun! matchit#Match_debug()
    601  let b:match_debug = 1 " Save debugging information.
    602  " pat = all of b:match_words with backrefs parsed
    603  amenu &Matchit.&pat   :echo b:match_pat<CR>
    604  " match = bit of text that is recognized as a match
    605  amenu &Matchit.&match :echo b:match_match<CR>
    606  " curcol = cursor column of the start of the matching text
    607  amenu &Matchit.&curcol        :echo b:match_col<CR>
    608  " wholeBR = matching group, original version
    609  amenu &Matchit.wh&oleBR       :echo b:match_wholeBR<CR>
    610  " iniBR = 'if' piece, original version
    611  amenu &Matchit.ini&BR :echo b:match_iniBR<CR>
    612  " ini = 'if' piece, with all backrefs resolved from match
    613  amenu &Matchit.&ini   :echo b:match_ini<CR>
    614  " tail = 'else\|endif' piece, with all backrefs resolved from match
    615  amenu &Matchit.&tail  :echo b:match_tail<CR>
    616  " fin = 'endif' piece, with all backrefs resolved from match
    617  amenu &Matchit.&word  :echo b:match_word<CR>
    618  " '\'.d in ini refers to the same thing as '\'.table[d] in word.
    619  amenu &Matchit.t&able :echo '0:' .. b:match_table .. ':9'<CR>
    620 endfun
    621 
    622 " Jump to the nearest unmatched "(" or "if" or "<tag>" if a:spflag == "bW"
    623 " or the nearest unmatched "</tag>" or "endif" or ")" if a:spflag == "W".
    624 " Return a "mark" for the original position, so that
    625 "   let m = MultiMatch("bW", "n") ... call winrestview(m)
    626 " will return to the original position.  If there is a problem, do not
    627 " move the cursor and return {}, unless a count is given, in which case
    628 " go up or down as many levels as possible and again return {}.
    629 " TODO This relies on the same patterns as % matching.  It might be a good
    630 " idea to give it its own matching patterns.
    631 fun! matchit#MultiMatch(spflag, mode)
    632  let restore_options = s:RestoreOptions()
    633  let startpos = [line("."), col(".")]
    634  " save v:count1 variable, might be reset from the restore_cursor command
    635  let level = v:count1
    636  if a:mode == "o" && mode(1) !~# '[vV]'
    637    exe "norm! v"
    638  endif
    639 
    640  " First step:  if not already done, set the script variables
    641  "   s:do_BR   flag for whether there are backrefs
    642  "   s:pat     parsed version of b:match_words
    643  "   s:all     regexp based on s:pat and the default groups
    644  " This part is copied and slightly modified from matchit#Match_wrapper().
    645  if !exists("b:match_words") || b:match_words == ""
    646    let match_words = ""
    647    " Allow b:match_words = "GetVimMatchWords()" .
    648  elseif b:match_words =~ ":"
    649    let match_words = b:match_words
    650  else
    651    execute "let match_words =" b:match_words
    652  endif
    653  if (match_words != s:last_words) || (&mps != s:last_mps) ||
    654    \ exists("b:match_debug")
    655    let default = escape(&mps, '[$^.*~\\/?]') .. (strlen(&mps) ? "," : "") ..
    656      \ '\/\*:\*\/,#\s*if\%(n\=def\)\=:#\s*else\>:#\s*elif\>:#\s*endif\>'
    657    let s:last_mps = &mps
    658    let match_words = s:Append(match_words, default)
    659    let s:last_words = match_words
    660    if match_words !~ s:notslash .. '\\\d'
    661      let s:do_BR = 0
    662      let s:pat = match_words
    663    else
    664      let s:do_BR = 1
    665      let s:pat = s:ParseWords(match_words)
    666    endif
    667    let s:all = '\%(' .. substitute(s:pat, '[,:]\+', '\\|', 'g') .. '\)'
    668    if exists("b:match_debug")
    669      let b:match_pat = s:pat
    670    endif
    671    " Reconstruct the version with unresolved backrefs.
    672    let s:patBR = substitute(match_words .. ',',
    673      \ s:notslash .. '\zs[,:]*,[,:]*', ',', 'g')
    674    let s:patBR = substitute(s:patBR, s:notslash .. '\zs:\{2,}', ':', 'g')
    675  endif
    676 
    677  " Second step:  figure out the patterns for searchpair()
    678  " and save the screen, cursor position, and 'ignorecase'.
    679  " - TODO:  A lot of this is copied from matchit#Match_wrapper().
    680  " - maybe even more functionality should be split off
    681  " - into separate functions!
    682  let openlist = split(s:pat .. ',', s:notslash .. '\zs:.\{-}' .. s:notslash .. ',')
    683  let midclolist = split(',' .. s:pat, s:notslash .. '\zs,.\{-}' .. s:notslash .. ':')
    684  call map(midclolist, {-> split(v:val, s:notslash .. ':')})
    685  let closelist = []
    686  let middlelist = []
    687  call map(midclolist, {i,v -> [extend(closelist, v[-1 : -1]),
    688        \ extend(middlelist, v[0 : -2])]})
    689  call map(openlist,   {i,v -> v =~# s:notslash .. '\\|' ? '\%(' .. v .. '\)' : v})
    690  call map(middlelist, {i,v -> v =~# s:notslash .. '\\|' ? '\%(' .. v .. '\)' : v})
    691  call map(closelist,  {i,v -> v =~# s:notslash .. '\\|' ? '\%(' .. v .. '\)' : v})
    692  let open   = join(openlist, ',')
    693  let middle = join(middlelist, ',')
    694  let close  = join(closelist, ',')
    695  if exists("b:match_skip")
    696    let skip = b:match_skip
    697  elseif exists("b:match_comment") " backwards compatibility and testing!
    698    let skip = "r:" .. b:match_comment
    699  else
    700    let skip = 's:comment\|string'
    701  endif
    702  let skip = s:ParseSkip(skip)
    703  let view = winsaveview()
    704 
    705  " Third step: call searchpair().
    706  " Replace '\('--but not '\\('--with '\%(' and ',' with '\|'.
    707  let openpat = substitute(open, '\%(' .. s:notslash .. '\)\@<=\\(', '\\%(', 'g')
    708  let openpat = substitute(openpat, ',', '\\|', 'g')
    709  let closepat = substitute(close, '\%(' .. s:notslash .. '\)\@<=\\(', '\\%(', 'g')
    710  let closepat = substitute(closepat, ',', '\\|', 'g')
    711  let middlepat = substitute(middle, '\%(' .. s:notslash .. '\)\@<=\\(', '\\%(', 'g')
    712  let middlepat = substitute(middlepat, ',', '\\|', 'g')
    713 
    714  if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on"))
    715        \ || skip =~ 'v:lua.vim.treesitter' && !exists('b:ts_highlight')
    716    let skip = '0'
    717  else
    718    try
    719      execute "if " .. skip .. "| let skip = '0' | endif"
    720    catch /^Vim\%((\a\+)\)\=:E363/
    721      " We won't find anything, so skip searching, should keep Vim responsive.
    722      return {}
    723    endtry
    724  endif
    725  mark '
    726  while level
    727    if searchpair(openpat, middlepat, closepat, a:spflag, skip) < 1
    728      call s:CleanUp(restore_options, a:mode, startpos)
    729      return {}
    730    endif
    731    let level = level - 1
    732  endwhile
    733 
    734  " Restore options and return a string to restore the original position.
    735  call s:CleanUp(restore_options, a:mode, startpos)
    736  return view
    737 endfun
    738 
    739 " Search backwards for "if" or "while" or "<tag>" or ...
    740 " and return "endif" or "endwhile" or "</tag>" or ... .
    741 " For now, this uses b:match_words and the same script variables
    742 " as matchit#Match_wrapper() .  Later, it may get its own patterns,
    743 " either from a buffer variable or passed as arguments.
    744 " fun! s:Autocomplete()
    745 "   echo "autocomplete not yet implemented :-("
    746 "   if !exists("b:match_words") || b:match_words == ""
    747 "     return ""
    748 "   end
    749 "   let startpos = matchit#MultiMatch("bW")
    750 "
    751 "   if startpos == ""
    752 "     return ""
    753 "   endif
    754 "   " - TODO:  figure out whether 'if' or '<tag>' matched, and construct
    755 "   " - the appropriate closing.
    756 "   let matchline = getline(".")
    757 "   let curcol = col(".") - 1
    758 "   " - TODO:  Change the s:all argument if there is a new set of match pats.
    759 "   let regexp = s:Wholematch(matchline, s:all, curcol)
    760 "   let suf = strlen(matchline) - matchend(matchline, regexp)
    761 "   let prefix = (curcol ? '^.\{'  . curcol . '}\%(' : '^\%(')
    762 "   let suffix = (suf ? '\).\{' . suf . '}$'  : '\)$')
    763 "   " Reconstruct the version with unresolved backrefs.
    764 "   let patBR = substitute(b:match_words.',', '[,:]*,[,:]*', ',', 'g')
    765 "   let patBR = substitute(patBR, ':\{2,}', ':', "g")
    766 "   " Now, set group and groupBR to the matching group: 'if:endif' or
    767 "   " 'while:endwhile' or whatever.
    768 "   let group = s:Choose(s:pat, matchline, ",", ":", prefix, suffix, patBR)
    769 "   let i = matchend(group, s:notslash . ",")
    770 "   let groupBR = strpart(group, i)
    771 "   let group = strpart(group, 0, i-1)
    772 "   " Now, matchline =~ prefix . substitute(group,':','\|','g') . suffix
    773 "   if s:do_BR
    774 "     let group = s:InsertRefs(groupBR, prefix, group, suffix, matchline)
    775 "   endif
    776 " " let g:group = group
    777 "
    778 "   " - TODO:  Construct the closing from group.
    779 "   let fake = "end" . expand("<cword>")
    780 "   execute startpos
    781 "   return fake
    782 " endfun
    783 
    784 " Close all open structures.  "Get the heck out of here!"
    785 " fun! s:Gthhoh()
    786 "   let close = s:Autocomplete()
    787 "   while strlen(close)
    788 "     put=close
    789 "     let close = s:Autocomplete()
    790 "   endwhile
    791 " endfun
    792 
    793 " Parse special strings as typical skip arguments for searchpair():
    794 "   s:foo becomes (current syntax item) =~ foo
    795 "   S:foo becomes (current syntax item) !~ foo
    796 "   r:foo becomes (line before cursor) =~ foo
    797 "   R:foo becomes (line before cursor) !~ foo
    798 "   t:foo becomes (current treesitter captures) =~ foo
    799 "   T:foo becomes (current treesitter captures) !~ foo
    800 fun! s:ParseSkip(str)
    801  let skip = a:str
    802  if skip[1] == ":"
    803    if skip[0] ==# "t" || skip[0] ==# "s" && &syntax != 'on' && exists("b:ts_highlight")
    804      let skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" .. strpart(skip,2) .. "') != -1"
    805    elseif skip[0] ==# "T" || skip[0] ==# "S" && &syntax != 'on' && exists("b:ts_highlight")
    806      let skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" .. strpart(skip,2) .. "') == -1"
    807    elseif skip[0] ==# "s"
    808      let skip = "synIDattr(synID(line('.'),col('.'),1),'name') =~? '" ..
    809        \ strpart(skip,2) .. "'"
    810    elseif skip[0] ==# "S"
    811      let skip = "synIDattr(synID(line('.'),col('.'),1),'name') !~? '" ..
    812        \ strpart(skip,2) .. "'"
    813    elseif skip[0] ==# "r"
    814      let skip = "strpart(getline('.'),0,col('.'))=~'" .. strpart(skip,2) .. "'"
    815    elseif skip[0] ==# "R"
    816      let skip = "strpart(getline('.'),0,col('.'))!~'" .. strpart(skip,2) .. "'"
    817    endif
    818  endif
    819  return skip
    820 endfun
    821 
    822 let &cpo = s:save_cpo
    823 unlet s:save_cpo
    824 
    825 " vim:sts=2:sw=2:et: