" targets.vim Provides additional text objects
" Author: Christian Wellenbrock <christian.wellenbrock@gmail.com>
" License: MIT license
" save cpoptions
let s:save_cpoptions = &cpoptions
set cpo&vim
" called once when loaded
function! s:setup()
let s:argOpeningS = g:targets_argOpening . '\|' . g:targets_argSeparator
let s:argClosingS = g:targets_argClosing . '\|' . g:targets_argSeparator
let s:argOuter = g:targets_argOpening . '\|' . g:targets_argClosing
let s:argAll = s:argOpeningS . '\|' . g:targets_argClosing
let s:none = 'a^' " matches nothing
let s:rangeScores = {}
let ranges = split(g:targets_seekRanges)
let rangesN = len(ranges)
let i = 0
while i < rangesN
let s:rangeScores[ranges[i]] = rangesN - i
let i = i + 1
endwhile
let s:rangeJumps = {}
let ranges = split(g:targets_jumpRanges)
let rangesN = len(ranges)
let i = 0
while i < rangesN
let s:rangeJumps[ranges[i]] = 1
let i = i + 1
endwhile
endfunction
call s:setup()
" a:count is unused here, but added for consistency with targets#x
function! targets#o(trigger, count)
call s:init('o')
let [delimiter, which, modifier] = split(a:trigger, '\zs')
let [target, rawTarget] = s:findTarget(delimiter, which, modifier, v:count1)
if target.state().isInvalid()
return s:cleanUp()
endif
call s:handleTarget(target, rawTarget)
call s:clearCommandLine()
call s:prepareRepeat(delimiter, which, modifier)
call s:cleanUp()
endfunction
function! targets#e(modifier)
if mode() !=? 'v'
return a:modifier
endif
let char1 = nr2char(getchar())
let [delimiter, which, chars] = [char1, 'c', char1]
let i = 0
while i < 4
if g:targets_nlNL[i] ==# delimiter
" delimiter was which, get another char for delimiter
let char2 = nr2char(getchar())
let [delimiter, which, chars] = [char2, 'nlNL'[i], chars . char2]
break
endif
let i = i + 1
endwhile
let [_, _, _, err] = s:getDelimiters(delimiter)
if err
return a:modifier . chars
endif
if delimiter ==# "'"
let delimiter = "''"
endif
return "\<Esc>:\<C-U>call targets#x('" . delimiter . which . a:modifier . "', " . v:count1 . ")\<CR>"
endfunction
function! targets#x(trigger, count)
call s:initX(a:trigger)
let [delimiter, which, modifier] = split(a:trigger, '\zs')
let [target, rawTarget] = s:findTarget(delimiter, which, modifier, a:count)
if target.state().isInvalid()
call s:abortMatch('#x: ' . target.error)
return s:cleanUp()
endif
if s:handleTarget(target, rawTarget) == 0
let s:lastTrigger = a:trigger
let s:lastTarget = target
endif
call s:cleanUp()
endfunction
" initialize script local variables for the current matching
function! s:init(mapmode)
let s:mapmode = a:mapmode
let s:oldpos = getpos('.')
let s:newSelection = 1
let s:shouldGrow = 1
let s:selection = &selection " remember 'selection' setting
let &selection = 'inclusive' " and set it to inclusive
let s:virtualedit = &virtualedit " remember 'virtualedit' setting
let &virtualedit = '' " and set it to default
let s:whichwrap = &whichwrap " remember 'whichwrap' setting
let &whichwrap = 'b,s' " and set it to default
endfunction
" save old visual selection to detect new selections and reselect on fail
function! s:initX(trigger)
call s:init('x')
let s:visualTarget = targets#target#fromVisualSelection()
" reselect, save mode and go back to normal mode
normal! gv
if mode() ==# 'V'
let s:visualTarget.linewise = 1
normal! V
else
normal! v
endif
let s:newSelection = s:isNewSelection()
let s:shouldGrow = s:shouldGrow(a:trigger)
endfunction
" clean up script variables after match
function! s:cleanUp()
" reset remembered settings
let &selection = s:selection
let &virtualedit = s:virtualedit
let &whichwrap = s:whichwrap
endfunction
function! s:findTarget(delimiter, which, modifier, count)
let [kind, s:opening, s:closing, err] = s:getDelimiters(a:delimiter)
if err
let errorTarget = targets#target#withError("failed to find delimiter")
return [errorTarget, errorTarget]
endif
let view = winsaveview()
let rawTarget = s:findRawTarget(kind, a:which, a:count)
let target = s:modifyTarget(rawTarget, kind, a:modifier)
call winrestview(view)
return [target, rawTarget]
endfunction
function! s:findRawTarget(kind, which, count)
if a:kind ==# 'p'
if a:which ==# 'c'
return s:seekselectp(a:count + s:grow())
elseif a:which ==# 'n'
call s:search(a:count, s:opening, 'W')
return s:selectp()
elseif a:which ==# 'l'
call s:search(a:count, s:closing, 'bW')
return s:selectp()
else
return targets#target#withError('findRawTarget p')
endif
elseif a:kind ==# 'q'
let [dir, rateL, skipL, rateR, skipR, error] = s:quoteDir()
if error !=# ''
return targets#target#withError('findRawTarget quoteDir')
endif
if a:which ==# 'c'
return s:seekselect(dir, rateL - skipL, rateR - skipR)
elseif a:which ==# 'n'
return s:nextselect(a:count * rateR - skipR)
elseif a:which ==# 'l'
return s:lastselect(a:count * rateL - skipL)
else
return targets#target#withError('findRawTarget q: ' . a:which)
endif
elseif a:kind ==# 's'
if a:which ==# 'c'
return s:seekselect('>', 1, 1)
elseif a:which ==# 'n'
return s:nextselect(a:count)
elseif a:which ==# 'l'
return s:lastselect(a:count)
elseif a:which ==# 'N'
return s:nextselect(a:count * 2)
elseif a:which ==# 'L'
return s:lastselect(a:count * 2)
else
return targets#target#withError('findRawTarget s')
endif
elseif a:kind ==# 't'
if a:which ==# 'c'
return s:seekselectp(a:count + s:grow(), '<\a', '</\a', 't')
elseif a:which ==# 'n'
call s:search(a:count, '<\a', 'W')
return s:selectp()
elseif a:which ==# 'l'
call s:search(a:count, '</\a\zs', 'bW')
return s:selectp()
else
return targets#target#withError('findRawTarget t')
endif
elseif a:kind ==# 'a'
if a:which ==# 'c'
return s:seekselecta(a:count + s:grow())
elseif a:which ==# 'n'
return s:nextselecta(a:count)
elseif a:which ==# 'l'
return s:lastselecta(a:count)
else
return targets#target#withError('findRawTarget a')
endif
endif
return targets#target#withError('findRawTarget kind')
endfunction
function! s:modifyTarget(target, kind, modifier)
if a:target.state().isInvalid()
return targets#target#withError('modifyTarget invalid: ' . a:target.error)
endif
let target = a:target.copy()
if a:kind ==# 'p'
if a:modifier ==# 'i'
return s:drop(target)
elseif a:modifier ==# 'a'
return target
elseif a:modifier ==# 'I'
return s:shrink(target)
elseif a:modifier ==# 'A'
return s:expand(target)
else
return targets#target#withError('modifyTarget p')
endif
elseif a:kind ==# 'q'
if a:modifier ==# 'i'
return s:drop(target)
elseif a:modifier ==# 'a'
return target
elseif a:modifier ==# 'I'
return s:shrink(target)
elseif a:modifier ==# 'A'
return s:expand(target)
else
return targets#target#withError('modifyTarget q')
endif
elseif a:kind ==# 's'
if a:modifier ==# 'i'
return s:drop(target)
elseif a:modifier ==# 'a'
return s:dropr(target)
elseif a:modifier ==# 'I'
return s:shrink(target)
elseif a:modifier ==# 'A'
return s:expand(target)
else
return targets#target#withError('modifyTarget s')
endif
elseif a:kind ==# 't'
if a:modifier ==# 'i'
let target = s:innert(target)
return s:drop(target)
elseif a:modifier ==# 'a'
return target
elseif a:modifier ==# 'I'
let target = s:innert(target)
return s:shrink(target)
elseif a:modifier ==# 'A'
return s:expand(target)
else
return targets#target#withError('modifyTarget t')
endif
elseif a:kind ==# 'a'
if a:modifier ==# 'i'
return s:drop(target)
elseif a:modifier ==# 'a'
return s:dropa(target)
elseif a:modifier ==# 'I'
return s:shrink(target)
elseif a:modifier ==# 'A'
return s:expand(target)
else
return targets#target#withError('modifyTarget a')
endif
endif
return targets#target#withError('modifyTarget kind')
endfunction
function! s:getDelimiters(trigger)
" create cache
if !exists('s:delimiterCache')
let s:delimiterCache = {}
endif
" check cache
if has_key(s:delimiterCache, a:trigger)
let [kind, opening, closing] = s:delimiterCache[a:trigger]
return [kind, opening, closing, 0]
endif
let [kind, rawOpening, rawClosing, err] = s:getRawDelimiters(a:trigger)
if err > 0
return [0, 0, 0, err]
endif
let opening = s:modifyDelimiter(kind, rawOpening)
let closing = s:modifyDelimiter(kind, rawClosing)
" write to cache
let s:delimiterCache[a:trigger] = [kind, opening, closing]
return [kind, opening, closing, 0]
endfunction
function! s:getRawDelimiters(trigger)
" check more specific ones first for #145
if a:trigger ==# g:targets_tagTrigger
return ['t', 't', 0, 0]
elseif a:trigger ==# g:targets_argTrigger
return ['a', 0, 0, 0]
endif
for pair in split(g:targets_pairs)
for trigger in split(pair, '\zs')
if trigger ==# a:trigger
return ['p', pair[0], pair[1], 0]
endif
endfor
endfor
for quote in split(g:targets_quotes)
for trigger in split(quote, '\zs')
if trigger ==# a:trigger
return ['q', quote[0], quote[0], 0]
endif
endfor
endfor
for separator in split(g:targets_separators)
for trigger in split(separator, '\zs')
if trigger ==# a:trigger
return ['s', separator[0], separator[0], 0]
endif
endfor
endfor
return [0, 0, 0, 1]
endfunction
function! s:modifyDelimiter(kind, delimiter)
let delimiter = escape(a:delimiter, '.~\$')
if a:kind !=# 'q' || "eescape ==# ''
return delimiter
endif
let escapedqe = escape("eescape, ']^-\')
let lookbehind = '[' . escapedqe . ']'
if v:version >= 704
return lookbehind . '\@1<!' . delimiter
else
return lookbehind . '\@<!' . delimiter
endif
endfunction
" return 0 if the selection changed since the last invocation. used for
" growing
function! s:isNewSelection()
" no previous invocation or target
if !exists('s:lastTarget')
return 1
endif
" selection changed
if s:lastTarget != s:visualTarget
return 1
endif
return 0
endfunction
function! s:shouldGrow(trigger)
if s:newSelection
return 0
endif
if !exists('s:lastTrigger')
return 0
endif
if s:lastTrigger != a:trigger
return 0
endif
return 1
endfunction
" clear the commandline to hide targets function calls
function! s:clearCommandLine()
echo
endfunction
" handle the match by either selecting or aborting it
function! s:handleTarget(target, rawTarget)
if a:target.state().isInvalid()
return s:abortMatch('handleTarget')
elseif a:target.state().isEmpty()
return s:handleEmptyMatch(a:target)
else
return s:selectTarget(a:target, a:rawTarget)
endif
endfunction
" select a proper match
function! s:selectTarget(target, rawTarget)
" add old position to jump list
if s:addToJumplist(a:rawTarget)
call setpos('.', s:oldpos)
normal! m'
endif
call s:selectRegion(a:target)
endfunction
function! s:addToJumplist(target)
let min = line('w0')
let max = line('w$')
let range = a:target.range(s:oldpos, min, max)
return get(s:rangeJumps, range)
endfunction
" visually select a given match. used for match or old selection
function! s:selectRegion(target)
" visually select the target
call a:target.select()
" if selection should be exclusive, expand selection
if s:selection ==# 'exclusive'
normal! l
endif
endfunction
" empty matches can't visually be selected
" most operators would like to move to the end delimiter
" for change or delete, insert temporary character that will be operated on
function! s:handleEmptyMatch(target)
if s:mapmode !=# 'o' || v:operator !~# "^[cd]$"
return s:abortMatch('handleEmptyMatch')
endif
" move cursor to delimiter after zero width match
call a:target.cursorS()
let eventignore = &eventignore " remember setting
let &eventignore = 'all' " disable auto commands
" insert single space and visually select it
silent! execute "normal! i \<Esc>v"
let &eventignore = eventignore " restore setting
endfunction
" abort when no match was found
function! s:abortMatch(message)
" get into normal mode and beep
if !exists("*getcmdwintype") || getcmdwintype() ==# ""
call feedkeys("\<C-\>\<C-N>\<Esc>", 'n')
endif
call s:prepareReselect()
call setpos('.', s:oldpos)
" undo partial command
call s:triggerUndo()
" trigger reselect if called from xmap
call s:triggerReselect()
return s:fail(a:message)
endfunction
" feed keys to call undo after aborted operation and clear the command line
function! s:triggerUndo()
if exists("*undotree")
let undoseq = undotree().seq_cur
call feedkeys(":call targets#undo(" . undoseq . ")\<CR>:echo\<CR>", 'n')
endif
endfunction
" temporarily select original selection to reselect later
function! s:prepareReselect()
if s:mapmode ==# 'x'
call s:selectRegion(s:visualTarget)
endif
endfunction
" feed keys to reselect the last visual selection if called with mapmode x
function! s:triggerReselect()
if s:mapmode ==# 'x'
call feedkeys("gv", 'n')
endif
endfunction
" set up repeat.vim for older Vim versions
function! s:prepareRepeat(delimiter, which, modifier)
if v:version >= 704 " skip recent versions
return
endif
if v:operator ==# 'y' && match(&cpoptions, 'y') ==# -1 " skip yank unless set up
return
endif
let cmd = v:operator . a:modifier
if a:which !=# 'c'
let cmd .= a:which
endif
let cmd .= a:delimiter
if v:operator ==# 'c'
let cmd .= "\<C-r>.\<ESC>"
endif
silent! call repeat#set(cmd, v:count)
endfunction
" undo last operation if it created a new undo position
function! targets#undo(lastseq)
if undotree().seq_cur > a:lastseq
silent! execute "normal! u"
endif
endfunction
" returns [direction, rateL, skipL, rateR, skipR, error]
function! s:quoteDir()
let oldpos = getpos('.')
let [direction, rateL, skipL, rateR, skipR, error, rep] = s:quoteDirInternal(oldpos[2])
" echom 'rep' rep 'rateL' rateL 'skipL' skipL 'rateR' rateR 'skipR' skipR
call setpos('.', oldpos)
return [direction, rateL, skipL, rateR, skipR, error]
endfunction
" doesn't restore old position
" cursor rep dir rates/skips description
" . ()
" xx1 > 1/0 1/0 good multiline around if final
" ( bx0 > 1/0 1/0 good multiline below single if final
" ( ox0 > 1/1 1/0 good multiline below single on if final
" ( ax0 < 1/0 1/0 good multiline above if final
" ( ) bb1 2/1 2/1 bad after last if final
" ( ) bo1 < 2/0 2/1 good end on cursor select to left
" ( ) ba1 > 2/0 2/0 good around cursor select around
" ( ) oa1 > 2/1 2/0 good start on cursor select to right
" ( ) aa1 2/1 2/1 bad before first
" ) ( bb0 > 2/0 1/0 good multiline below multi if final
" ) ( ob0 > 2/1 1/0 good multiline below multi on if final
" ) ( ab0 2/1 2/1 bad between pairs
" returns [dir, skipL, skipR, error, rep]
function! s:quoteDirInternal(oldcolumn)
let column = 0
let positions = ['x', 'x']
let index = 1 " write into opening first (will be toggled first)
silent! normal! 0
let [_, column] = searchpos(s:opening, 'c', line('.'))
while column != 0
let index = !index " 0 <-> 1
if column < a:oldcolumn
let positions[index] = 'b' " before
elseif column == a:oldcolumn
let positions[index] = 'o' " on
else
let positions[index] = 'a' " after
endif
let rep = positions[0] . positions[1] . index
if rep == 'bo1'
call s:debug('good end on cursor select to left')
return ['<', 2, 0, 2, 1, '', rep]
elseif rep == 'ba1'
call s:debug('good around cursor select around')
return ['>', 2, 0, 2, 0, '', rep]
elseif rep == 'oa1'
call s:debug('good start on cursor select to right')
return ['>', 2, 1, 2, 0, '', rep]
elseif rep == 'aa1'
call s:debug('bad before first')
return ['', 2, 1, 2, 1, '', rep]
elseif rep == 'ab0'
call s:debug('bad between pairs')
return ['', 2, 1, 2, 1, '', rep]
else
" call s:debug('not final ' . rep)
endif
let [_, column] = searchpos(s:opening, '', line('.'))
endwhile
let rep = positions[0] . positions[1] . index
if rep == 'xx1'
call s:debug('good multiline around')
return ['>', 1, 0, 1, 0, '', rep]
elseif rep == 'bx0'
call s:debug('good multiline below single')
return ['>', 1, 0, 1, 0, '', rep]
elseif rep == 'ox0'
call s:debug('good multiline below single on')
return ['>', 1, 1, 1, 0, '', rep]
elseif rep == 'ax0'
call s:debug('good multiline above')
return ['<', 1, 0, 1, 0, '', rep]
elseif rep == 'bb1'
call s:debug('bad after last')
return ['', 2, 1, 2, 1, '', rep]
elseif rep == 'bb0'
call s:debug('good multiline below multi')
return ['>', 2, 0, 1, 0, '', rep]
elseif rep == 'ob0'
call s:debug('good multiline below multi on')
return ['>', 2, 1, 1, 0, '', rep]
else
return ['', 1, 0, 1, 0, 'quoteDir not found ' . rep]
endif
endfunction
function! s:nextselect(count)
" echom 'nextselect' a:count
if s:search(a:count, s:opening, 'W') > 0
return targets#target#withError('nextselect')
endif
return s:select('>')
endfunction
function! s:lastselect(count)
" echom 'lastselect' a:count
if s:search(a:count, s:closing, 'bW') > 0
return targets#target#withError('lastselect')
endif
return s:select('<')
endfunction
" match selectors
" ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
" select pair of delimiters around cursor (multi line, no seeking)
" select to the right if cursor is on a delimiter
" cursor │ ....
" line │ ' ' b ' '
" matcher │ └───┘
function! s:select(direction)
if a:direction ==# ''
return targets#target#withError('select without direction')
elseif a:direction ==# '>'
let [sl, sc] = searchpos(s:opening, 'bcW') " search left for opening
let [el, ec] = searchpos(s:closing, 'W') " then right for closing
return targets#target#fromValues(sl, sc, el, ec)
else
let [el, ec] = searchpos(s:closing, 'cW') " search right for closing
let [sl, sc] = searchpos(s:opening, 'bW') " then left for opening
return targets#target#fromValues(sl, sc, el, ec)
endif
endfunction
" select pair of delimiters around cursor (multi line, supports seeking)
function! s:seekselect(dir, countL, countR)
" echom 'seekselect' a:dir 'countL' a:countL 'countR' a:countR
let min = line('w0')
let max = line('w$')
let oldpos = getpos('.')
let around = s:select(a:dir)
call setpos('.', oldpos)
let last = s:lastselect(a:countL)
call setpos('.', oldpos)
let next = s:nextselect(a:countR)
return s:bestSeekTarget([around, next, last], oldpos, min, max, 'seekselect')
endfunction
" select a pair around the cursor
" args (count=1, trigger=s:opening)
function! s:selectp(...)
let cnt = a:0 >= 1 ? a:1 : 1
let trigger = a:0 >= 2 ? a:2 : s:opening
" try to select pair
silent! execute 'keepjumps normal! v' . cnt . 'a' . trigger
let [el, ec] = getpos('.')[1:2]
silent! normal! o
let [sl, sc] = getpos('.')[1:2]
silent! normal! v
if sc == ec && sl == el
return targets#target#withError('selectp')
endif
return targets#target#fromValues(sl, sc, el, ec)
endfunction
" pair matcher (works across multiple lines, supports seeking)
" cursor │ .....
" line │ ( ( a ) )
" modifier │ │ └─1─┘ │
" │ └── 2 ──┘
" args (count, opening=s:opening, closing=s:closing, trigger=s:closing)
function! s:seekselectp(...)
let cnt = a:1 " required
let opening = a:0 >= 2 ? a:2 : s:opening
let closing = a:0 >= 3 ? a:3 : s:closing
let trigger = a:0 >= 4 ? a:4 : s:closing
let min = line('w0')
let max = line('w$')
let oldpos = getpos('.')
let around = s:selectp(cnt, trigger)
if cnt > 1 " don't seek with count
return around
endif
call setpos('.', oldpos)
call s:search(cnt, s:closing, 'bW')
let last = s:selectp()
call setpos('.', oldpos)
call s:search(cnt, s:opening, 'W')
let next = s:selectp()
return s:bestSeekTarget([around, next, last], oldpos, min, max, 'seekselectp')
endfunction
" select an argument around the cursor
" parameter direction decides where to select when invoked on a separator:
" '>' select to the right (default)
" '<' select to the left (used when selecting or skipping to the left)
" '^' select up (surrounding argument, used for growing)
function! s:selecta(direction)
let oldpos = getpos('.')
let [opening, closing] = [g:targets_argOpening, g:targets_argClosing]
if a:direction ==# '^'
let [sl, sc, el, ec, err] = s:findArg(a:direction, 'W', 'bcW', 'bW', opening, closing)
let message = 'selecta 1'
elseif a:direction ==# '>'
let [sl, sc, el, ec, err] = s:findArg(a:direction, 'W', 'bW', 'bW', opening, closing)
let message = 'selecta 2'
elseif a:direction ==# '<' " like '>', but backwards
let [el, ec, sl, sc, err] = s:findArg(a:direction, 'bW', 'W', 'W', closing, opening)
let message = 'selecta 3'
else
return targets#target#withError('selecta')
endif
if err > 0
call setpos('.', oldpos)
return targets#target#withError(message)
endif
return targets#target#fromValues(sl, sc, el, ec)
endfunction
" find an argument around the cursor given a direction (see s:selecta)
" uses flags1 to search for end to the right; flags1 and flags2 to search for
" start to the left
function! s:findArg(direction, flags1, flags2, flags3, opening, closing)
let oldpos = getpos('.')
let char = s:getchar()
let separator = g:targets_argSeparator
if char =~# a:closing && a:direction !=# '^' " started on closing, but not up
let [el, ec] = oldpos[1:2] " use old position as end
else " find end to the right
let [el, ec, err] = s:findArgBoundary(a:flags1, a:flags1, a:opening, a:closing)
if err > 0 " no closing found
return [0, 0, 0, 0, s:fail('findArg 1', a:)]
endif
let separator = g:targets_argSeparator
if char =~# a:opening || char =~# separator " started on opening or separator
let [sl, sc] = oldpos[1:2] " use old position as start
return [sl, sc, el, ec, 0]
endif
call setpos('.', oldpos) " return to old position
endif
" find start to the left
let [sl, sc, err] = s:findArgBoundary(a:flags2, a:flags3, a:closing, a:opening)
if err > 0 " no opening found
return [0, 0, 0, 0, s:fail('findArg 2')]
endif
return [sl, sc, el, ec, 0]
endfunction
" find arg boundary by search for `finish` or `separator` while skipping
" matching `skip`s
" example: find ',' or ')' while skipping a pair when finding '('
" args (flags1, flags2, skip, finish, all=s:argAll,
" separator=g:targets_argSeparator, cnt=2)
" return (line, column, err)
function! s:findArgBoundary(...)
let flags1 = a:1 " required
let flags2 = a:2
let skip = a:3
let finish = a:4
let all = a:0 >= 5 ? a:5 : s:argAll
let separator = a:0 >= 6 ? a:6 : g:targets_argSeparator
let cnt = a:0 >= 7 ? a:7 : 1
let tl = 0
for _ in range(cnt)
let [rl, rc] = searchpos(all, flags1)
while 1
if rl == 0
return [0, 0, s:fail('findArgBoundary 1', a:)]
endif
let char = s:getchar()
if char =~# separator
if tl == 0
let [tl, tc] = [rl, rc]
endif
elseif char =~# finish
if tl > 0
return [tl, tc, 0]
endif
break
elseif char =~# skip
silent! keepjumps normal! %
else
return [0, 0, s:fail('findArgBoundary 2')]
endif
let [rl, rc] = searchpos(all, flags2)
endwhile
endfor
return [rl, rc, 0]
endfunction
" selects and argument, supports growing and seeking
function! s:seekselecta(count)
if a:count > 1
if s:getchar() =~# g:targets_argClosing
let [cnt, message] = [a:count - 2, 'seekselecta 1']
else
let [cnt, message] = [a:count - 1, 'seekselecta 2']
endif
" find cnt closing while skipping matched openings
let [opening, closing] = [g:targets_argOpening, g:targets_argClosing]
if s:findArgBoundary('W', 'W', opening, closing, s:argOuter, s:none, cnt)[2] > 0
return targets#target#withError(message . ' count')
endif
return s:selecta('^')
endif
let min = line('w0')
let max = line('w$')
let oldpos = getpos('.')
let around = s:selecta('>')
if a:count > 1 " don't seek with count
return around
endif
call setpos('.', oldpos)
let last = s:lastselecta()
call setpos('.', oldpos)
let next = s:nextselecta()
return s:bestSeekTarget([around, next, last], oldpos, min, max, 'seekselecta')
endfunction
" try to select a next argument, supports count and optional stopline
" args (count=1, stopline=0)
function! s:nextselecta(...)
let cnt = a:0 >= 1 ? a:1 : 1
let stopline = a:0 >= 2 ? a:2 : 0
if s:search(cnt, s:argOpeningS, 'W', stopline) > 0 " no start found
return targets#target#withError('nextselecta 1')
endif
let char = s:getchar()
let target = s:selecta('>')
if target.state().isValid()
return target
endif
if char !~# g:targets_argSeparator " start wasn't on comma
return targets#target#withError('nextselecta 2')
endif
call setpos('.', s:oldpos)
let opening = g:targets_argOpening
if s:search(cnt, opening, 'W', stopline) > 0 " no start found
return targets#target#withError('nextselecta 3')
endif
let target = s:selecta('>')
if target.state().isValid()
return target
endif
return targets#target#withError('nextselecta 4')
endfunction
" try to select a last argument, supports count and optional stopline
" args (count=1, stopline=0)
function! s:lastselecta(...)
let cnt = a:0 >= 1 ? a:1 : 1
let stopline = a:0 >= 2 ? a:2 : 0
" special case to handle vala when invoked on a separator
let separator = g:targets_argSeparator
if s:getchar() =~# separator && s:newSelection
let target = s:selecta('<')
if target.state().isValid()
return target
endif
endif
if s:search(cnt, s:argClosingS, 'bW', stopline) > 0 " no start found
return targets#target#withError('lastselecta 1')
endif
let char = s:getchar()
let target = s:selecta('<')
if target.state().isValid()
return target
endif
if char !~# separator " start wasn't on separator
return targets#target#withError('lastselecta 2')
endif
call setpos('.', s:oldpos)
let closing = g:targets_argClosing
if s:search(cnt, closing, 'bW', stopline) > 0 " no start found
return targets#target#withError('lastselecta 3')
endif
let target = s:selecta('<')
if target.state().isValid()
return target
endif
return targets#target#withError('lastselecta 4')
endfunction
" select best of given targets according to s:rangeScores
" detects for each given target what range type it has, depending on the
" relative positions of the start and end of the target relative to the cursor
" position and the currently visible lines
" The possibly relative positions are:
" c - on cursor position
" l - left of cursor in current line
" r - right of cursor in current line
" a - above cursor on screen
" b - below cursor on screen
" A - above cursor off screen
" B - below cursor off screen
" All possibly ranges are listed below, denoted by two characters: one for the
" relative start and for the relative end position each of the target. For
" example, `lr` means "from left of cursor to right of cursor in cursor line".
" Next to each range type is a pictogram of an example. They are made of these
" symbols:
" . - current cursor position
" ( ) - start and end of target
" / - line break before and after cursor line
" | - screen edge between hidden and visible lines
" ranges on cursor:
" cr | / () / | starting on cursor, current line
" cb | / ( /) | starting on cursor, multiline down, on screen
" cB | / ( / |) starting on cursor, multiline down, partially off screen
" lc | / () / | ending on cursor, current line
" ac | (/ ) / | ending on cursor, multiline up, on screen
" Ac (| / ) / | ending on cursor, multiline up, partially off screen
" ranges around cursor:
" lr | / (.) / | around cursor, current line
" lb | / (. /) | around cursor, multiline down, on screen
" ar | (/ .) / | around cursor, multiline up, on screen
" ab | (/ . /) | around cursor, multiline both, on screen
" lB | / (. / |) around cursor, multiline down, partially off screen
" Ar (| / .) / | around cursor, multiline up, partially off screen
" aB | (/ . / |) around cursor, multiline both, partially off screen bottom
" Ab (| / . /) | around cursor, multiline both, partially off screen top
" AB (| / . / |) around cursor, multiline both, partially off screen both
" ranges after (right of/below) cursor
" rr | / .()/ | after cursor, current line
" rb | / .( /) | after cursor, multiline, on screen
" rB | / .( / |) after cursor, multiline, partially off screen
" bb | / . /()| after cursor below, on screen
" bB | / . /( |) after cursor below, partially off screen
" BB | / . / |() after cursor below, off screen
" ranges before (left of/above) cursor
" ll | /(). / | before cursor, current line
" al | (/ ). / | before cursor, multiline, on screen
" Al (| / ). / | before cursor, multiline, partially off screen
" aa |()/ . / | before cursor above, on screen
" Aa (| )/ . / | before cursor above, partially off screen
" AA ()| / . / | before cursor above, off screen
" A a l r b B relative positions
" └───────────┘ visible screen
" └─────┘ current line
function! s:bestSeekTarget(targets, oldpos, min, max, message)
let bestScore = 0
for target in a:targets
let range = target.range(a:oldpos, a:min, a:max)
let score = get(s:rangeScores, range)
if bestScore < score
let bestScore = score
let best = target
endif
endfor
if bestScore > 0
return best
endif
return targets#target#withError(a:message)
endfunction
" selection modifiers
" ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
" drop delimiters left and right
" remove last line of multiline selection if it consists of whitespace only
" in │ ┌─────┐
" line │ a . b . c
" out │ └───┘
function! s:drop(target)
if a:target.state().isInvalid()
return a:target
endif
let [sLinewise, eLinewise] = [0, 0]
call a:target.cursorS()
if searchpos('\S', 'nW', line('.'))[0] == 0
" if only whitespace after cursor
let sLinewise = 1
endif
silent! execute "normal! 1 "
call a:target.getposS()
call a:target.cursorE()
if a:target.sl < a:target.el && searchpos('\S', 'bnW', line('.'))[0] == 0
" if only whitespace in front of cursor
let eLinewise = 1
" move to end of line above
normal! -$
else
" one character back
silent! execute "normal! \<BS>"
endif
call a:target.getposE()
let a:target.linewise = sLinewise && eLinewise
return a:target
endfunction
" drop right delimiter
" in │ ┌─────┐
" line │ a . b c . d
" out │ └────┘
function! s:dropr(target)
call a:target.cursorE()
silent! execute "normal! \<BS>"
call a:target.getposE()
return a:target
endfunction
" drop an argument separator (like a comma), prefer the right one, fall back
" to the left (one on first argument)
" in │ ┌───┐ ┌───┐ ┌───┐ ┌───┐
" line │ ( x ) ( x , a ) (a , x , b) ( a , x )
" out │ └─┘ └──┘ └──┘ └──┘
function! s:dropa(target)
let startOpening = a:target.getcharS() !~# g:targets_argSeparator
let endOpening = a:target.getcharE() !~# g:targets_argSeparator
if startOpening
if endOpening
" ( x ) select space on both sides
return s:drop(a:target)
else
" ( x , a ) select separator and space after
call a:target.cursorS()
call a:target.searchposS('\S', '', a:target.el)
return s:expand(a:target, '>')
endif
else
if !endOpening
" (a , x , b) select leading separator, no surrounding space
return s:dropr(a:target)
else
" ( a , x ) select separator and space before
call a:target.cursorE()
call a:target.searchposE('\S', 'b', a:target.sl)
return s:expand(a:target, '<')
endif
endif
endfunction
" select inner tag delimiters
" in │ ┌──────────┐
" line │ a <b> c </b> c
" out │ └─────┘
function! s:innert(target)
call a:target.cursorS()
call a:target.searchposS('>', 'W')
call a:target.cursorE()
call a:target.searchposE('<', 'bW')
return a:target
endfunction
" drop delimiters and whitespace left and right
" fall back to drop when only whitespace is inside
" in │ ┌─────┐ │ ┌──┐
" line │ a . b c . d │ a . . d
" out │ └─┘ │ └┘
function! s:shrink(target)
if a:target.state().isInvalid()
return a:target
endif
call a:target.cursorE()
call a:target.searchposE('\S', 'b', a:target.sl)
if a:target.state().isInvalidOrEmpty()
" fall back to drop when there's only whitespace in between
return s:drop(a:target)
else
call a:target.cursorS()
call a:target.searchposS('\S', '', a:target.el)
endif
return a:target
endfunction
" expand selection by some whitespace
" in │ ┌───┐ │ ┌───┐ │ ┌───┐ │ ┌───┐
" line │ a . b . c │ a . b .c │ a. c .c │ . a .c
" out │ └────┘ │ └────┘ │ └───┘ │└────┘
" args (target, direction=<try right, then left>)
function! s:expand(...)
let target = a:1
if a:0 == 1 || a:2 ==# '>'
call target.cursorE()
let [line, column] = searchpos('\S\|$', '', line('.'))
if line > 0 && column-1 > target.ec
" non whitespace or EOL after trailing whitespace found
" not counting whitespace directly after end
call target.setE(line, column-1)
return target
endif
endif
if a:0 == 1 || a:2 ==# '<'
call target.cursorS()
let [line, column] = searchpos('\S', 'b', line('.'))
if line > 0
" non whitespace before leading whitespace found
call target.setS(line, column+1)
return target
endif
" only whitespace in front of start
" include all leading whitespace from beginning of line
let target.sc = 1
endif
return target
endfunction
" return 1 if count should be increased by one to grow selection on repeated
" invocations
function! s:grow()
if s:mapmode ==# 'o' || !s:shouldGrow
return 0
endif
return 1
endfunction
" returns the character under the cursor
function! s:getchar()
return getline('.')[col('.')-1]
endfunction
" search for pattern using flags and a count, optional stopline
" args (cnt, pattern, flags, stopline=0)
function! s:search(...)
let cnt = a:1 " required
let pattern = a:2
let flags = a:3
let stopline = a:0 >= 4 ? a:4 : 0
for _ in range(cnt)
let line = searchpos(pattern, flags, stopline)[0]
if line == 0 " not enough found
return s:fail('search')
endif
endfor
endfunction
" return 1 and send a message to s:debug
" args (message, parameters=nil)
function! s:fail(...)
let message = 'fail ' . a:1
let message .= a:0 >= 2 ? ' ' . string(a:2) : ''
call s:debug(message)
return 1
endfunction
" useful for debugging
function! s:debug(message)
" echom a:message
endfunction
" reset cpoptions
let &cpoptions = s:save_cpoptions
unlet s:save_cpoptions