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

let s:save_cpo = &cpo
set cpo&vim

" TODO this can probably be simplified
function! matchup#motion#op(motion) abort
  call matchup#motion_force()
  let l:sid = matchup#motion_sid()
  let s:v_operator = v:operator
  execute 'normal' l:sid.'(wise)' . (v:count > 0 ? v:count : '')
        \ . l:sid.'(matchup-'.a:motion.')'
  unlet s:v_operator
endfunction

function matchup#motion#getoper()
  return get(s:, 'v_operator', '')
endfunction

function! matchup#motion#find_matching_pair(visual, down) " {{{1
  let [l:count, l:count1] = [v:count, v:count1]

  let l:is_oper = !empty(get(s:, 'v_operator', ''))

  if a:visual && !l:is_oper
    normal! gv
  endif

  if a:down && l:count > g:matchup_motion_override_Npercent
    " TODO: dv50% does not work properly
    if a:visual && l:is_oper
      normal! V
    endif
    exe 'normal!' l:count.'%'
    return
  endif

  " disable the timeout
  call matchup#perf#timeout_start(0)

  " get a delim where the cursor is
  let l:delim = matchup#delim#get_current('all', 'both_all')
  if empty(l:delim)
    " otherwise search forward
    let l:delim = matchup#delim#get_next('all', 'both_all')
    if empty(l:delim) | return | endif
  endif

  " loop count number of times
  for l:dummy in range(l:count1)
    let l:matches = matchup#delim#get_matching(l:delim, 1)
    if len(l:matches) <= (l:delim.side ==# 'mid' ? 2 : 1) | return | endif
    if !has_key(l:delim, 'links') | return | endif
    let l:delim = get(l:delim.links, a:down ? 'next' : 'prev', {})
    if empty(l:delim) | return | endif
  endfor

  if a:visual && l:is_oper
    normal! gv
  endif

  let l:exclusive = l:is_oper && (g:v_motion_force ==# 'v')
  let l:forward = ((a:down && l:delim.side !=# 'open')
        \ || l:delim.side ==# 'close')

  " go to the end of the delimiter, if necessary
  let l:column = l:delim.cnum
  if g:matchup_motion_cursor_end && !l:is_oper && l:forward
    let l:column = matchup#delim#jump_target(l:delim)
  endif

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

  normal! m`

  " column position of last character in match
  let l:eom = l:delim.cnum + matchup#delim#end_offset(l:delim)

  if l:is_oper && l:forward
    let l:column = l:exclusive ? (l:column - 1) : l:eom
  endif

  if l:is_oper && l:exclusive
        \ && matchup#pos#smaller(l:delim, l:start_pos)
    normal! o
    call matchup#pos#set_cursor(matchup#pos#prev(l:start_pos))
    normal! o
  endif

  " special handling for d%
  let [l:start_lnum, l:start_cnum] = l:start_pos[1:2]
  if get(s:, 'v_operator', '') ==# 'd' && l:start_lnum != l:delim.lnum
        \ && g:v_motion_force ==# ''
    let l:tl = [l:start_lnum, l:start_cnum]
    let [l:tl, l:br, l:swap] = l:tl[0] <= l:delim.lnum
          \ ? [l:tl, [l:delim.lnum, l:eom], 0]
          \ : [[l:delim.lnum, l:delim.cnum], l:tl, 1]

    if getline(l:tl[0]) =~# '^[ \t]*\%'.l:tl[1].'c'
          \ && getline(l:br[0]) =~# '\%'.(l:br[1]+1).'c[ \t]*$'
      if l:swap
        normal! o
        call matchup#pos#set_cursor(l:br[0], strlen(getline(l:br[0]))+1)
        normal! o
        let l:column = 1
      else
        normal! o
        call matchup#pos#set_cursor(l:tl[0], 1)
        normal! o
        let l:column = strlen(getline(l:br[0]))+1
      endif
    endif
  endif

  let l:lnum = l:delim.lnum

  " make adjustments for selection option 'exclusive
  if l:forward && a:visual && &selection ==# 'exclusive'
    let [l:lnum, l:column] = matchup#pos#next_eol(l:lnum, l:column)[1:2]
  endif
  if !l:forward && l:is_oper && &selection ==# 'exclusive'
    normal! o
    call matchup#pos#set_cursor(matchup#pos#next_eol(
          \ matchup#pos#get_cursor()))
    normal! o
  endif

  call matchup#pos#set_cursor(l:lnum, l:column)
  if stridx(&foldopen, 'percent') >= 0
    normal! zv
  endif
endfunction

" }}}1
function! matchup#motion#find_unmatched(visual, down, ...) " {{{1
  call matchup#perf#tic('motion#find_unmatched')

  let l:opts = a:0 ? a:1 : {}
  let l:count = v:count1

  let l:is_oper = !empty(get(s:, 'v_operator', ''))
  let l:exclusive = l:is_oper
        \ && g:v_motion_force !=# 'v' && g:v_motion_force !=# "\<c-v>"

  if a:visual
    normal! gv
  endif

  " set the timeout fairly high by default
  let l:timeout = get(l:opts, 'timeout', 750)
  call matchup#perf#timeout_start(l:timeout)

  for l:tries in range(3)
    let [l:open, l:close] = matchup#delim#get_surrounding('delim_all',
          \ l:tries ? l:count : 1,
          \ { 'check_skip': get(l:opts, '__where_impl__', 0) })

    if empty(l:open) || empty(l:close)
      call matchup#perf#toc('motion#find_unmatched', 'fail'.l:tries)
      return
    endif

    let l:delim = a:down ? l:close : l:open

    let l:save_pos = matchup#pos#get_cursor()
    let l:new_pos = [l:delim.lnum, l:delim.cnum]

    " this is an exclusive motion, ]%
    if l:delim.side ==# 'close'
      if l:exclusive
        let l:new_pos[1] -= 1
      else
        let l:new_pos[1] += matchup#delim#end_offset(l:delim)
      endif
    endif

    " if the cursor didn't move, increment count
    if matchup#pos#equal(l:save_pos, l:new_pos)
      let l:count += 1
    elseif l:tries
      break
    endif

    if l:count <= 1
      break
    endif
  endfor

  if a:down && !l:is_oper
    let l:new_pos[1] = matchup#delim#jump_target(l:delim)
  endif

  " this is an exclusive motion, [%
  if !a:down && l:exclusive
    normal! o
    call matchup#pos#set_cursor(matchup#pos#prev(
          \ matchup#pos#get_cursor()))
    normal! o
  endif

  " handle selection option 'exclusive' going backwards
  if !a:down && l:is_oper && &selection ==# 'exclusive'
    normal! o
    call matchup#pos#set_cursor(matchup#pos#next_eol(
          \ matchup#pos#get_cursor()))
    normal! o
  endif

  " handle selection option 'exclusive' going forwards
  if a:down && l:is_oper && &selection ==# 'exclusive'
    let l:new_pos = matchup#pos#next_eol(l:new_pos)[1:2]
  endif

  if get(l:opts, '__where_impl__', 0)
    let l:opts.delim = l:delim
  else
    normal! m`
  endif
  call matchup#pos#set_cursor(l:new_pos)

  call matchup#perf#toc('motion#find_unmatched', 'done')
endfunction

" }}}1
function! matchup#motion#jump_inside(visual) " {{{1
  let l:count = v:count1

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

  call matchup#perf#timeout_start(750)

  if a:visual
    normal! gv
  endif

  for l:counter in range(l:count)
    if l:counter
      let l:delim = matchup#delim#get_next('all', 'open')
    else
      let l:delim = matchup#delim#get_current('all', 'open')
      if empty(l:delim)
        let l:delim = matchup#delim#get_next('all', 'open')
      endif
    endif
    if empty(l:delim)
      call matchup#pos#set_cursor(l:save_pos)
      return
    endif

    let l:new_pos = [l:delim.lnum, l:delim.cnum]
    let l:new_pos[1] += matchup#delim#end_offset(l:delim)
    call matchup#pos#set_cursor(matchup#pos#next(l:new_pos))
  endfor

  call matchup#pos#set_cursor(l:save_pos)

  " convert to [~, lnum, cnum, ~] format
  let l:new_pos = matchup#pos#next(l:new_pos)

  " this is an exclusive motion except when dealing with whitespace
  let l:is_oper = !empty(get(s:, 'v_operator', ''))
  if l:is_oper
        \ && g:v_motion_force !=# 'v' && g:v_motion_force !=# "\<c-v>"
    while matchup#util#in_whitespace(l:new_pos[1], l:new_pos[2])
      let l:new_pos = matchup#pos#next(l:new_pos)
    endwhile
    let l:new_pos = matchup#pos#prev(l:new_pos)
  endif

  " jump ahead if inside indent
  if !l:is_oper && matchup#util#in_indent(l:new_pos[1], l:new_pos[2])
    let l:new_pos[2] = 1 + strlen(matchstr(
          \ getline(l:new_pos[1]), '^\s\+'))
  endif

  " handle selection option 'exclusive' (motion only goes forwards)
  if a:visual && &selection ==# 'exclusive'
    let l:new_pos = matchup#pos#next_eol(l:new_pos)
  endif

  normal! m`
  call matchup#pos#set_cursor(l:new_pos)
endfunction

" }}}1
function! matchup#motion#insert_mode() " {{{1
  call matchup#perf#timeout_start(0) " disable the timeout

  let l:delim = matchup#delim#get_current(
        \ 'all', 'both_all', {'insertmode': 1})
  if empty(l:delim) | return | endif

  let l:matches = matchup#delim#get_matching(l:delim, 1)
  if len(l:matches) <= (l:delim.side ==# 'mid' ? 2 : 1) | return | endif
  if !has_key(l:delim, 'links') | return | endif
  let l:delim = get(l:delim.links, 'next', {})
  if empty(l:delim) | return | endif

  let l:new_pos = [l:delim.lnum, l:delim.cnum]
  let l:new_pos[1] += matchup#delim#end_offset(l:delim)
  call matchup#pos#set_cursor(matchup#pos#next_eol(l:new_pos))
endfunction

" }}}1

let &cpo = s:save_cpo

" vim: fdm=marker sw=2