" vim match-up - even better matching
"
" Maintainer: Andy Massimino
" Email:      a@normed.space
"

let s:save_cpo = &cpo
set cpo&vim

function! matchup#delim#get_next(type, side, ...) " {{{1
  return s:get_delim(extend({
        \ 'direction' : 'next',
        \ 'type' : a:type,
        \ 'side' : a:side,
        \}, get(a:, '1', {})))
endfunction

" }}}1
function! matchup#delim#get_prev(type, side, ...) " {{{1
  return s:get_delim(extend({
        \ 'direction' : 'prev',
        \ 'type' : a:type,
        \ 'side' : a:side,
        \}, get(a:, '1', {})))
endfunction

" }}}1
function! matchup#delim#get_current(type, side, ...) " {{{1
  return s:get_delim(extend({
        \ 'direction' : 'current',
        \ 'type' : a:type,
        \ 'side' : a:side,
        \}, get(a:, '1', {})))
endfunction

" }}}1

function! matchup#delim#get_matching(delim, ...) " {{{1
  if empty(a:delim) || !has_key(a:delim, 'lnum') | return {} | endif

  let l:opts = a:0 && type(a:1) == type({}) ? a:1 : {}
  let l:stopline = get(l:opts, 'stopline', s:stopline)

  " get all the matching position(s)
  " *important*: in the case of mid, we search up before searching down
  " this gives us a context object which we use for the other side
  " TODO: what if no open is found here?
  let l:matches = []
  let l:save_pos = matchup#pos#get_cursor()
  for l:down in {'open': [1], 'close': [0], 'mid': [0,1]}[a:delim.side]
    call matchup#pos#set_cursor(a:delim)

    " second iteration: [] refers to the current match
    if !empty(l:matches)
      call add(l:matches, [])
    endif

    let l:res = a:delim.get_matching(l:down, l:stopline)
    if l:res[0][1] > 0
      call extend(l:matches, l:res)
    elseif l:down
      let l:matches = []
    endif
  endfor
  call matchup#pos#set_cursor(l:save_pos)

  if a:delim.side ==# 'open'
    call insert(l:matches, [])
  endif
  if a:delim.side ==# 'close'
    call add(l:matches, [])
  endif

  " create the match result(s)
  let l:matching_list = []
  for l:i in range(len(l:matches))
    if empty(l:matches[l:i])
      let a:delim.match_index = l:i
      call add(l:matching_list, a:delim)
      continue
    end

    let [l:match, l:lnum, l:cnum] = l:matches[l:i]

    let l:matching = copy(a:delim)
    let l:matching.class = copy(a:delim.class)

    let l:matching.lnum = l:lnum
    let l:matching.cnum = l:cnum
    let l:matching.match = l:match
    let l:matching.side = l:i == 0 ? 'open'
          \ : l:i == len(l:matches)-1 ? 'close' : 'mid'
    let l:matching.class[1] = 'FIXME'
    let l:matching.match_index = l:i

    call add(l:matching_list, l:matching)
  endfor

  " set up links between matches
  for l:i in range(len(l:matching_list))
    let l:c = l:matching_list[l:i]
    let l:c.links = {}
    let l:c.links.next = l:matching_list[(l:i+1) % len(l:matching_list)]
    let l:c.links.prev = l:matching_list[l:i-1]
    let l:c.links.open = l:matching_list[0]
    let l:c.links.close = l:matching_list[-1]
  endfor

  return l:matching_list
endfunction

" }}}1

function! matchup#delim#get_surrounding(type, ...) " {{{1
  call matchup#perf#tic('delim#get_surrounding')

  let l:save_pos = matchup#pos#get_cursor()
  let l:pos_val_cursor = matchup#pos#val(l:save_pos)
  let l:pos_val_last = l:pos_val_cursor
  let l:pos_val_open = l:pos_val_cursor - 1

  let l:count = a:0 >= 1 ? a:1 : 1
  let l:counter = l:count

  " third argument specifies local any block, otherwise,
  " provided count == 0 refers to local any block
  let l:opts = a:0 >= 2 ? a:2 : {}
  let l:local = get(l:opts, 'local', l:count == 0 ? 1 : 0)

  let l:delimopts = {}
  let s:invert_skip = 0   " TODO: this logic is still bad
  if matchup#delim#skip() " TODO: check for insert mode (?)
    let l:delimopts.check_skip = 0
  endif
  " TODO: pin skip
  if get(l:opts, 'check_skip', 0)
    let l:delimopts.check_skip = 1
  endif

  " keep track of the outermost pair found so far
  " returned when g:matchup_delim_count_fail = 1
  let l:best = []

  while l:pos_val_open < l:pos_val_last
    let l:open = matchup#delim#get_prev(a:type,
          \ l:local ? 'open_mid' : 'open', l:delimopts)
    if empty(l:open) | break | endif

    " if configured, we may still accept this match
    if matchup#perf#timeout_check() && !g:matchup_delim_count_fail
      break
    endif

    let l:matches = matchup#delim#get_matching(l:open, 1)

    " TODO: getting one match result here is surely wrong
    if len(l:matches) == 1
      let l:matches = []
    endif

    if has_key(l:opts, 'matches')
      let l:opts.matches = l:matches
    endif

    if len(l:matches)
      let l:close = l:local ? l:open.links.next : l:open.links.close
      let l:pos_val_try = matchup#pos#val(l:close)
            \ + matchup#delim#end_offset(l:close)
    endif

    if len(l:matches) && l:pos_val_try >= l:pos_val_cursor
      if l:counter <= 1
        " restore cursor and accept
        call matchup#pos#set_cursor(l:save_pos)
        call matchup#perf#toc('delim#get_surrounding', 'accept')
        return [l:open, l:close]
      endif
      let l:counter -= 1
      let l:best = [l:open, l:close]
    else
      let l:pos_val_last = l:pos_val_open
      let l:pos_val_open = matchup#pos#val(l:open)
    endif

    if l:open.lnum == 1 && l:open.cnum == 1
      break
    endif
    call matchup#pos#set_cursor(matchup#pos#prev(l:open))
  endwhile

  if !empty(l:best) && g:matchup_delim_count_fail
    call matchup#pos#set_cursor(l:save_pos)
    call matchup#perf#toc('delim#get_surrounding', 'bad_count')
    return l:best
  endif

  " restore cursor and return failure
  call matchup#pos#set_cursor(l:save_pos)
  call matchup#perf#toc('delim#get_surrounding', 'fail')
  return [{}, {}]
endfunction

" }}}1
function! matchup#delim#get_surround_nearest(open, ...) " {{{1
  " finds the first consecutive pair whose start
  " positions surround pos (default to the cursor)
  let l:cur_pos = a:0 ? a:1 : matchup#pos#get_cursor()
  let l:pos_val_cursor = matchup#pos#val(l:cur_pos)
  let l:pos_val_open = matchup#pos#val(a:open)

  let l:pos_val_prev = l:pos_val_open
  let l:delim = a:open.links.next
  let l:pos_val_next = matchup#pos#val(l:delim)
  while l:pos_val_next > l:pos_val_open
    let l:end_offset = matchup#delim#end_offset(l:delim)
    if l:pos_val_prev <= l:pos_val_cursor
          \ && l:pos_val_next + l:end_offset >= l:pos_val_cursor
      return [l:delim.links.prev, l:delim]
    endif
    let l:pos_val_prev = l:pos_val_next
    let l:delim = l:delim.links.next
    let l:pos_val_next = matchup#pos#val(l:delim)
  endwhile

  return [{}, {}]
endfunction

" }}}1

function! matchup#delim#jump_target(delim) " {{{1
  let l:save_pos = matchup#pos#get_cursor()

  " unicode note: technically wrong, but works in practice
  " since the cursor snaps back to start of multi-byte chars
  let l:column = a:delim.cnum
  let l:column += strlen(a:delim.match) - 1

  if strlen(a:delim.match) < 2
    return l:column
  endif

  for l:tries in range(strlen(a:delim.match)-1)
    call matchup#pos#set_cursor(a:delim.lnum, l:column)

    let l:delim_test = matchup#delim#get_current('all', 'both_all')
    if l:delim_test.class[0] ==# a:delim.class[0]
      break
    endif

    let l:column -= 1
  endfor

  call matchup#pos#set_cursor(l:save_pos)
  return l:column
endfunction

" }}}1
function! matchup#delim#end_offset(delim) " {{{1
  return max([0, match(a:delim.match, '.$')])
endfunction

" }}}1
function! matchup#delim#end_pos(delim) abort " {{{1
  return [a:delim.lnum, a:delim.cnum + matchup#delim#end_offset(a:delim)]
endfunction

" }}}1

function! s:get_delim(opts) " {{{1
  " arguments: {{{2
  "   opts = {
  "     'direction'   : 'next' | 'prev' | 'current'
  "     'type'        : 'delim_tex'
  "                   | 'delim_all'
  "                   | 'all'
  "     'side'        : 'open'     | 'close'
  "                   | 'both'     | 'mid'
  "                   | 'both_all' | 'open_mid'
  "  }
  "
  "  }}}2
  " returns: {{{2
  "   delim = {
  "     type     : 'delim'
  "     lnum     : line number
  "     cnum     : column number
  "     match    : the actual text match
  "     augment  : how to match a corresponding open
  "     groups   : dict of captured groups
  "     side     : 'open' | 'close' | 'mid'
  "     class    : [ c1, c2 ] identifies the kind of match_words
  "     regexone : the regex item, like \1foo
  "     regextwo : the regex_capture item, like \(group\)foo
  "   }
  "
  " }}}2

  if !get(b:, 'matchup_delim_enabled', 0)
    return {}
  endif

  call matchup#perf#tic('s:get_delim')

  let l:save_pos = matchup#pos#get_cursor()

  call matchup#loader#refresh_match_words()

  " this contains all the patterns for the specified type and side
  let l:re = b:matchup_delim_re[a:opts.type][a:opts.side]

  let l:cursorpos = col('.')

  let l:insertmode = get(a:opts, 'insertmode', 0)
  if l:cursorpos > 1 && l:insertmode
    let l:cursorpos -= 1
  endif
  if l:cursorpos > strlen(getline('.'))
        \ && stridx("vV\<c-v>", mode()) > -1
    let l:cursorpos -= 1
  endif

  let s:invert_skip = 0

  if a:opts.direction ==# 'current'
    let l:check_skip = get(a:opts, 'check_skip',
          \ g:matchup_delim_noskips >= 2
          \ || g:matchup_delim_noskips >= 1
          \     && getline(line('.'))[l:cursorpos-1] =~ '[^[:punct:]]')
    if l:check_skip && matchup#delim#skip(line('.'), l:cursorpos)
      return {}
    endif
  else
    " check skip if cursor is not currently in skip
    let l:check_skip = get(a:opts, 'check_skip',
          \ !matchup#delim#skip(line('.'), l:cursorpos)
          \ || g:matchup_delim_noskips >= 2)
  endif

  let a:opts.cursorpos = l:cursorpos

  " for current, we want to find matches that end after the cursor
  " note: we expect this to give false-positives with \ze
  if a:opts.direction ==# 'current'
    let l:re .= '\%>'.(l:cursorpos).'c'
  "  let l:re = '\%<'.(l:cursorpos+1).'c' . l:re
  endif

  " allow overlapping delimiters
  " without this, the > in <tag> would not be found
  if b:matchup_delim_re[a:opts.type]._engine_info.has_zs[a:opts.side]
    let l:save_cpo = &cpo
    noautocmd set cpo-=c
  else
    " faster than changing cpo but doesn't work right with \zs
    let l:re .= '\&'
  endif

  " move cursor one left for searchpos if necessary
  let l:need_restore_cursor = 0
  if l:insertmode
    call matchup#pos#set_cursor(line('.'), col('.')-1)
    let l:need_restore_cursor = 1
  endif

  " stopline may depend on the current action
  let l:stopline = get(a:opts, 'stopline', s:stopline)

  " in the first pass, we get matching line and column numbers
  " this is intended to be as fast as possible, with no capture groups
  " we look for a match on this line (if direction == current)
  " or forwards or backwards (if direction == next or prev)
  " for current, we actually search leftwards from the cursor
  while 1
    let l:to = matchup#perf#timeout()
    let [l:lnum, l:cnum] = a:opts.direction ==# 'next'
          \ ? searchpos(l:re, 'cnW', line('.') + l:stopline, l:to)
          \ : a:opts.direction ==# 'prev'
          \   ? searchpos(l:re, 'bcnW',
          \               max([line('.') - l:stopline, 1]), l:to)
          \   : searchpos(l:re, 'bcnW', line('.'), l:to)
    if l:lnum == 0 | break | endif

    " note: the skip here should not be needed
    " in 'current' mode, but be explicit
    if a:opts.direction !=# 'current'
          \ && (l:check_skip || g:matchup_delim_noskips == 1
          \     && getline(l:lnum)[l:cnum-1] =~ '[^[:punct:]]')
          \ && matchup#delim#skip(l:lnum, l:cnum)
          \ && (a:opts.direction ==# 'prev' ? (l:lnum > 1 || l:cnum > 1)
          \     : (l:lnum < line('$') || l:cnum < len(getline('$'))))

      " invalid match, move cursor and keep looking
      call matchup#pos#set_cursor(a:opts.direction ==# 'next'
            \ ? matchup#pos#next(l:lnum, l:cnum)
            \ : matchup#pos#prev(l:lnum, l:cnum))
      let l:need_restore_cursor = 1
      continue
    endif

    break
  endwhile

  " restore cpo if necessary
  " note: this messes with cursor position
  if exists('l:save_cpo')
    noautocmd let &cpo = l:save_cpo
    let l:need_restore_cursor = 1
  endif

  " restore cursor
  if l:need_restore_cursor
    call matchup#pos#set_cursor(l:save_pos)
  endif

  call matchup#perf#toc('s:get_delim', 'first_pass')

  " nothing found, leave now
  if l:lnum == 0
    call matchup#perf#toc('s:get_delim', 'nothing_found')
    return {}
  endif

  if matchup#perf#timeout_check()
    return {}
  endif

  let l:skip_state = 0
  if !l:check_skip && (!&synmaxcol || l:cnum <= &synmaxcol)
    " XXX: workaround an apparent obscure vim bug where the
    " reported syntax id is incorrect on the first synID() call
    call matchup#delim#skip(l:lnum, l:cnum)
    if matchup#perf#timeout_check()
      return {}
    endif

    let l:skip_state = matchup#delim#skip(l:lnum, l:cnum)
  endif

  " now we get more data about the match in this position
  " there may be capture groups which need to be stored

  " result stub, to be filled by the parser when there is a match
  let l:result = {
        \ 'lnum'     : l:lnum,
        \ 'cnum'     : l:cnum,
        \ 'type'     : '',
        \ 'match'    : '',
        \ 'augment'  : '',
        \ 'groups'   : '',
        \ 'side'     : '',
        \ 'class'    : [],
        \ 'regexone' : '',
        \ 'regextwo' : '',
        \ 'skip'     : l:skip_state,
        \}

  for l:type in s:types[a:opts.type]
    let l:parser_result = l:type.parser(l:lnum, l:cnum, a:opts)
    if !empty(l:parser_result)
      let l:result = extend(l:parser_result, l:result, 'keep')
      break
    endif
  endfor

  call matchup#perf#toc('s:get_delim', 'got_results')

  return empty(l:result.type) ? {} : l:result
endfunction

" }}}1

function! s:parser_delim_new(lnum, cnum, opts) " {{{1
  let l:cursorpos = a:opts.cursorpos
  let l:found = 0

  let l:sides = matchup#loader#sidedict()[a:opts.side]
  let l:rebrs = b:matchup_delim_lists[a:opts.type].regex_capture

  " use b:match_ignorecase
  let l:ic = get(b:, 'match_ignorecase', 0) ? '\c' : '\C'

  " loop through all (index, side) pairs,
  let l:ns = len(l:sides)
  let l:found = 0
  for l:i in range(len(l:rebrs)*l:ns)
    let l:side = l:sides[ l:i % l:ns ]

    if l:side ==# 'mid'
      let l:res = l:rebrs[l:i / l:ns].mid_list
      if empty(l:res) | continue | end
    else
      let l:res = [ l:rebrs[l:i / l:ns][l:side] ]
      if empty(l:res[0]) | continue | end
    endif

    " if pattern may contain \zs, extra processing is required
    let l:extra_info = l:rebrs[l:i / l:ns].extra_info
    let l:has_zs = get(l:extra_info, 'has_zs', 0)

    let l:mid_id = 0
    for l:re in l:res
      let l:mid_id += 1

      " check whether hlend needs to be handled
      let l:id = l:side ==# 'mid' ? l:mid_id : l:side ==# 'open' ? 0 : -1
      let l:extra_entry = l:rebrs[l:i / l:ns].extra_list[l:id]
      let l:has_hlend = has_key(l:extra_entry, 'hlend')

      if l:has_hlend && get(a:opts, 'highlighting', 0)
        let l:re = s:process_hlend(l:re, l:cursorpos)
      endif

      " prepend the column number and append the cursor column
      " to anchor the match; we don't use {start} for matchlist
      " because there may be zero-width look behinds
      let l:re_anchored = l:ic . s:anchor_regex(l:re, a:cnum, l:has_zs)

      " for current we want the first match which the cursor is inside
      if a:opts.direction ==# 'current'
        let l:re_anchored .= '\%>'.(l:cursorpos).'c'
      endif

      let l:matches = matchlist(getline(a:lnum), l:re_anchored)
      if empty(l:matches) | continue | endif

      " reject matches which the cursor is outside of
      " this matters only for \ze
      if !l:has_hlend && a:opts.direction ==# 'current'
          \ && a:cnum + strlen(l:matches[0]) <= l:cursorpos
        continue
      endif

      " if pattern contains \zs we need to re-check the starting column
      if l:has_zs && match(getline(a:lnum), l:re_anchored) != a:cnum-1
        continue
      endif

      let l:found = 1
      break
    endfor

    if !l:found | continue | endif

    break
  endfor

  " reset ignorecase (defunct)

  if !l:found
    return {}
  endif

  let l:match = l:matches[0]

  let l:list = b:matchup_delim_lists[a:opts.type]
  let l:thisre   = l:list.regex[l:i / l:ns]
  let l:thisrebr = l:list.regex_capture[l:i / l:ns]

  let l:augment = {}

  " these are the capture groups indexed by their 'open' id
  let l:groups = {}
  let l:id = 0

  if l:side ==# 'open'
    " XXX we might as well store all the groups...
    "for l:br in keys(l:thisrebr.need_grp)
    for l:br in range(1,9)
      if empty(l:matches[l:br]) | continue | endif
      let l:groups[l:br] = l:matches[l:br]
    endfor
  else
    let l:id = (l:side ==# 'close')
          \ ? len(l:thisrebr.mid_list)+1
          \ : l:mid_id

    if has_key(l:thisrebr.grp_renu, l:id)
      for [l:br, l:to] in items(l:thisrebr.grp_renu[l:id])
        let l:groups[l:to] = l:matches[l:br]
      endfor
    endif

    " fill in augment pattern
    " TODO all the augment patterns should match,
    " but checking might be too slow
    if has_key(l:thisrebr.aug_comp, l:id)
      let l:aug = l:thisrebr.aug_comp[l:id][0]
      let l:augment.str = matchup#delim#fill_backrefs(
            \ l:aug.str, l:groups, 0)
      let l:augment.unresolved = deepcopy(l:aug.outputmap)
    endif
  endif

  let l:result = {
        \ 'type'         : 'delim_tex',
        \ 'match'        : l:match,
        \ 'augment'      : l:augment,
        \ 'groups'       : l:groups,
        \ 'side'         : l:side,
        \ 'class'        : [(l:i / l:ns), l:id],
        \ 'get_matching' : s:basetypes['delim_tex'].get_matching,
        \ 'regexone'     : l:thisre,
        \ 'regextwo'     : l:thisrebr,
        \ 'midmap'       : get(l:list, 'midmap', {}),
        \ 'highlighting' : get(a:opts, 'highlighting', 0),
        \}

  return l:result
endfunction
" }}}1

function! s:get_matching_delims(down, stopline) dict " {{{1
  " called as:   a:delim.get_matching(...)
  " called from: matchup#delim#get_matching <- matchparen, motion
  "   from: matchup#delim#get_surrounding <- matchparen, motion, text_obj

  call matchup#perf#tic('get_matching_delims')

  " first, we figure out what the furthest match is, which will be
  " either the open or close depending on the direction
  let [l:re, l:flags, l:stopline] = a:down
      \ ? [self.regextwo.close, 'W', line('.') + a:stopline]
      \ : [self.regextwo.open, 'bW', max([line('.') - a:stopline, 1])]

  " these are the anchors for searchpairpos
  let l:open = self.regexone.open     " TODO is this right? BADLOGIC
  let l:close = self.regexone.close

  " if we're searching up, we anchor by the augment, if it exists
  if !a:down && !empty(self.augment)
    let l:open = self.augment.str
  endif

  " TODO temporary workaround for BADLOGIC
  if a:down && self.side ==# 'mid'
    let l:open = self.regextwo.open
  endif

  " turn \(\) into \%(\) for searchpairpos
  let l:open  = matchup#loader#remove_capture_groups(l:open)
  let l:close = matchup#loader#remove_capture_groups(l:close)

  " fill in back-references
  " TODO: BADLOGIC2: when going up we don't have these groups yet..
  " the second anchor needs to be mid/self for mid self
  let l:open = matchup#delim#fill_backrefs(l:open, self.groups, 0)
  let l:close = matchup#delim#fill_backrefs(l:close, self.groups, 0)

  let s:invert_skip = self.skip
  if empty(b:matchup_delim_skip)
    let l:skip = 'matchup#delim#skip_default()'
  else
    let l:skip = 'matchup#delim#skip0()'
  endif

  " disambiguate matches for languages like julia, matlab, ruby, etc
  if !empty(self.midmap)
    let l:midmap = self.midmap.elements
    if self.side ==# 'mid'
      let l:idx = filter(range(len(l:midmap)),
            \ 'self.match =~# l:midmap[v:val][1]')
    else
      let l:syn = synIDattr(synID(self.lnum, self.cnum, 0), 'name')
      let l:idx = filter(range(len(l:midmap)),
            \ 'l:syn =~# l:midmap[v:val][0]')
    endif
    if len(l:idx)
      let l:valid = l:midmap[l:idx[0]]
      let l:skip = printf('matchup#delim#skip1(%s, %s)',
            \ string(l:midmap[l:idx[0]]), string(l:skip))
    else
      let l:skip = printf('matchup#delim#skip2(%s, %s)',
            \ string(self.midmap.strike), string(l:skip))
    endif
  endif

  if matchup#perf#timeout_check() | return [['', 0, 0]] | endif

  " improves perceptual performance in insert mode
  if mode() ==# 'i' || mode() ==# 'R'
    if !g:matchup_matchparen_deferred
      sleep 1m
    endif
  endif

  " use b:match_ignorecase
  let l:ic = get(b:, 'match_ignorecase', 0) ? '\c' : '\C'
  let l:open = l:ic . l:open
  let l:close = l:ic . l:close

  let [l:lnum_corr, l:cnum_corr] = searchpairpos(l:open, '', l:close,
        \ 'n'.l:flags, l:skip, l:stopline, matchup#perf#timeout())

  call matchup#perf#toc('get_matching_delims', 'initial_pair')

  " if nothing found, bail immediately
  if l:lnum_corr == 0
    " reset ignorecase (defunct)

    return [['', 0, 0]]
  endif

  " when highlighting, respect hlend
  let l:extra_entry = self.regextwo.extra_list[a:down ? -1 : 0]
  if self.highlighting && has_key(l:extra_entry, 'hlend')
    let l:re = s:process_hlend(l:re, -1)
  endif

  " get the match and groups
  let l:has_zs = self.regextwo.extra_info.has_zs
  let l:re_anchored = l:ic . s:anchor_regex(l:re, l:cnum_corr, l:has_zs)
  let l:matches = matchlist(getline(l:lnum_corr), l:re_anchored)
  let l:match_corr = l:matches[0]

  " reset ignorecase (defunct)

  " store these in these groups
  if a:down
    " let l:id = len(self.regextwo.mid_list)+1
    " for [l:from, l:to] in items(self.regextwo.grp_renu[l:id])
    "   let self.groups[l:to] = l:matches[l:from]
    " endfor
  else
    for l:to in range(1,9)
      if !has_key(self.groups, l:to) && !empty(l:matches[l:to])
        let self.groups[l:to] = l:matches[l:to]
      endif
    endfor
  endif

  call matchup#perf#toc('get_matching_delims', 'get_matches')

  " fill in additional groups
  let l:mids = matchup#loader#remove_capture_groups(self.regexone.mid)
  let l:mids = matchup#delim#fill_backrefs(l:mids, self.groups, 1)

  " if there are no mids, we're done
  if empty(l:mids)
    return [[l:match_corr, l:lnum_corr, l:cnum_corr]]
  endif

  let l:re = l:mids

  " when highlighting, respect hlend
  if get(self.regextwo.extra_info, 'mid_hlend') && self.highlighting
    let l:re = s:process_hlend(l:re, -1)
  endif

  " use b:match_ignorecase
  let l:mid = l:ic . l:mids
  let l:re = l:ic . l:re

  let l:list = []
  while 1
    if matchup#perf#timeout_check() | break | endif

    let [l:lnum, l:cnum] = searchpairpos(l:open, l:mids, l:close,
          \ l:flags, l:skip, l:lnum_corr, matchup#perf#timeout())
    if l:lnum <= 0 | break | endif

    if a:down
      if l:lnum > l:lnum_corr || l:lnum == l:lnum_corr
          \ && l:cnum >= l:cnum_corr | break | endif
    else
      if l:lnum < l:lnum_corr || l:lnum == l:lnum_corr
          \ && l:cnum <= l:cnum_corr | break | endif
    endif

    let l:re_anchored = s:anchor_regex(l:re, l:cnum, l:has_zs)
    let l:matches = matchlist(getline(l:lnum), l:re_anchored)
    if empty(l:matches)
      " this should never happen
      continue
    endif
    let l:match = l:matches[0]

    call add(l:list, [l:match, l:lnum, l:cnum])
  endwhile

  " reset ignorecase (defunct)

  call add(l:list, [l:match_corr, l:lnum_corr, l:cnum_corr])

  if !a:down
    call reverse(l:list)
  endif

  return l:list
endfunction
" }}}1

function! matchup#delim#skip(...) " {{{1
  if a:0 >= 2
    let [l:lnum, l:cnum] = [a:1, a:2]
  else
    let [l:lnum, l:cnum] = matchup#pos#get_cursor()[1:2]
  endif

  if empty(get(b:, 'matchup_delim_skip', ''))
    return matchup#util#in_comment_or_string(l:lnum, l:cnum)
          \ ? !s:invert_skip : s:invert_skip
  endif

  let s:eff_curpos = [l:lnum, l:cnum]
  execute 'return' (s:invert_skip ? '!(' : '(') b:matchup_delim_skip ')'
endfunction

function! matchup#delim#skip_default()
  return matchup#util#in_comment_or_string(line('.'), col('.'))
        \ ? !s:invert_skip : s:invert_skip
endfunction

function! matchup#delim#skip0()
  let s:eff_curpos = [line('.'), col('.')]
  execute 'return' (s:invert_skip ? '!(' : '(') b:matchup_delim_skip ')'
endfunction

""
" advanced mid/syntax skip when found in midmap
" {val} is a 2 element array of allowed [syntax, words]
" {def} is the fallback skip expression
function! matchup#delim#skip1(val, def)
  if getline('.')[col('.')-1:] =~# '^'.a:val[1]
    return eval(a:def)
  endif
  let l:s = synIDattr(synID(line('.'),col('.'), 0), 'name')
  return l:s !~# a:val[0] || eval(a:def)
endfunction

""
" advanced mid/syntax skip when word is not in midmap
" {strike} pattern of disallowed mid words
" {def} is the fallback skip expression
function! matchup#delim#skip2(strike, def)
  return getline('.')[col('.')-1:] =~# '^' . a:strike || eval(a:def)
endfunction

let s:invert_skip = 0
let s:eff_curpos = [1, 1]

" effective column/line
function! s:effline(expr)
  return a:expr ==# '.' ? s:eff_curpos[0] : line(a:expr)
endfunction

function! s:effcol(expr)
  return a:expr ==# '.' ? s:eff_curpos[1] : col(a:expr)
endfunction

function! s:geteffline(expr)
  return a:expr ==# '.' ? getline(s:effline(a:expr)) : getline(a:expr)
endfunction

" }}}1

function! matchup#delim#fill_backrefs(re, groups, warn) " {{{1
  return substitute(a:re, g:matchup#re#backref,
        \ '\=s:get_backref(a:groups, submatch(1), a:warn)', 'g')
        " \ '\=get(a:groups, submatch(1), "")', 'g')
endfunction

function! s:get_backref(groups, bref, warn)
  if !has_key(a:groups, a:bref)
    if a:warn
      echohl WarningMsg
      echo 'match-up: requested invalid backreference \'.a:bref
      echohl None
    endif
    return ''
  endif
  return '\V'.escape(get(a:groups, a:bref), '\').'\m'
endfunction

"}}}1

function! s:anchor_regex(re, cnum, method) " {{{1
  if a:method
    " trick to re-match at a particular column
    " handles the case where pattern contains \ze, \zs, and assertions
    " but doesn't work with overlapping matches and is possibly slower
    return '\%<'.(a:cnum+1).'c\%('.a:re.'\)\%>'.(a:cnum).'c'
  else
    " fails to match with \zs
    return '\%'.(a:cnum).'c\%('.a:re.'\)'
  endif
endfunction

" }}}1
function! s:process_hlend(re, cursorpos) " {{{1
  " first replace all \ze with \%>{cursorpos}c
  let l:re = substitute(a:re, g:matchup#re#ze,
        \ a:cursorpos < 0 ? '' : '\\%>'.a:cursorpos.'c', 'g')
  " next convert hlend mark to \ze
  return substitute(l:re, '\V\\%(hlend\\)\\{0}', '\\ze', 'g')
endfunction

" }}}1

" initialize script variables
let s:stopline = get(g:, 'matchup_delim_stopline', 1500)

let s:basetypes = {
      \ 'delim_tex': {
      \   'parser'       : function('s:parser_delim_new'),
      \   'get_matching' : function('s:get_matching_delims'),
      \ },
      \}

let s:types = {
      \ 'all'       : [ s:basetypes.delim_tex ],
      \ 'delim_all' : [ s:basetypes.delim_tex ],
      \ 'delim_tex' : [ s:basetypes.delim_tex ],
      \}

let &cpo = s:save_cpo

" vim: fdm=marker sw=2