" targets.vim Provides additional text objects " Author: Christian Wellenbrock " 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 "\:\call targets#x('" . delimiter . which . a:modifier . "', " . v:count1 . ")\" 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', ' 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 . '\@1v" 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("\\\", '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 . ")\:echo\", '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 .= "\.\" 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! \" 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! \" 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 c 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=) 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