" textobj-user - Create your own text objects " Version: 0.7.6 " Copyright (C) 2007-2018 Kana Natsuno " License: MIT license {{{ " Permission is hereby granted, free of charge, to any person obtaining " a copy of this software and associated documentation files (the " "Software"), to deal in the Software without restriction, including " without limitation the rights to use, copy, modify, merge, publish, " distribute, sublicense, and/or sell copies of the Software, and to " permit persons to whom the Software is furnished to do so, subject to " the following conditions: " " The above copyright notice and this permission notice shall be included " in all copies or substantial portions of the Software. " " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS " OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. " IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY " CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, " TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE " SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " }}} " Interfaces "{{{1 " simple "{{{2 function! textobj#user#move(pattern, flags, previous_mode) let i = v:count1 call s:prepare_movement(a:previous_mode) while 0 < i let result = searchpos(a:pattern, a:flags.'W') let i = i - 1 endwhile return result endfunction " FIXME: growing the current selection like iw/aw, is/as, and others. " FIXME: countable. function! textobj#user#select(pattern, flags, previous_mode) let ORIG_POS = s:gpos_to_spos(getpos('.')) let pft = searchpos(a:pattern, 'ceW') let pfh = searchpos(a:pattern, 'bcW') call cursor(ORIG_POS) let pbh = searchpos(a:pattern, 'bcW') let pbt = searchpos(a:pattern, 'ceW') let pos = s:choose_better_pos(a:flags, ORIG_POS, pfh, pft, pbh, pbt) if pos isnot 0 if a:flags !~# 'N' call s:range_select(pos[0], pos[1], s:choose_wise(a:flags)) endif return pos else return s:cancel_selection(a:previous_mode, ORIG_POS) endif endfunction function! s:choose_better_pos(flags, ORIG_POS, pfh, pft, pbh, pbt) " search() family with 'c' flag may not be matched to a pattern which " matches to multiple lines. To choose appropriate range, we have to check " another range [X] whether it contains the cursor or not. let vf = s:range_validp(a:pfh, a:pft) let vb = s:range_validp(a:pbh, a:pbt) let cf = vf && s:range_containsp(a:pfh, a:pft, a:ORIG_POS) let cb = vb && s:range_containsp(a:pbh, a:pbt, a:ORIG_POS) let lf = vf && s:range_in_linep(a:pfh, a:pft, a:ORIG_POS) let lb = vb && s:range_in_linep(a:pbh, a:pbt, a:ORIG_POS) if cb " [X] return [a:pbh, a:pbt] elseif cf return [a:pfh, a:pft] elseif lf && a:flags =~# '[nl]' return [a:pfh, a:pft] elseif lb && a:flags =~# '[nl]' return [a:pbh, a:pbt] elseif vf && (a:flags =~# '[fn]' || a:flags !~# '[bcl]') return [a:pfh, a:pft] elseif vb && a:flags =~# '[bn]' return [a:pbh, a:pbt] else return 0 endif endfunction " pair "{{{2 " FIXME: NIY, but is this necessary? " function! textobj#user#move_pair(pattern1, pattern2, flags) " endfunction " BUGS: With o_CTRL-V, this may not work properly. function! textobj#user#select_pair(pattern1, pattern2, flags, previous_mode) let ORIG_POS = s:gpos_to_spos(getpos('.')) " adjust the cursor to the head of a:pattern2 if it's already in the range. let pos2c_tail = searchpos(a:pattern2, 'ceW') let pos2c_head = searchpos(a:pattern2, 'bcW') if !s:range_validp(pos2c_head, pos2c_tail) return s:cancel_selection(a:previous_mode, ORIG_POS) endif if s:range_containsp(pos2c_head, pos2c_tail, ORIG_POS) let more_flags = 'c' else let more_flags = '' call cursor(ORIG_POS) endif " get the positions of a:pattern1 and a:pattern2. let pos2p_head = searchpairpos(a:pattern1, '', a:pattern2, 'W'.more_flags) let pos2p_tail = searchpos(a:pattern2, 'ceW') if !s:range_validp(pos2p_head, pos2p_tail) return s:cancel_selection(a:previous_mode, ORIG_POS) endif call cursor(pos2p_head) let pos1p_head = searchpairpos(a:pattern1, '', a:pattern2, 'bW') let pos1p_tail = searchpos(a:pattern1, 'ceW') if !s:range_validp(pos1p_head, pos1p_tail) return s:cancel_selection(a:previous_mode, ORIG_POS) endif " select the range, then adjust if necessary. if a:flags =~# 'i' if s:range_no_text_without_edgesp(pos1p_tail, pos2p_head) return s:cancel_selection(a:previous_mode, ORIG_POS) endif call s:range_select(pos1p_tail, pos2p_head, s:choose_wise(a:flags)) " adjust the range. let whichwrap_orig = &whichwrap let &whichwrap = '<,>' execute "normal! \o\" let &whichwrap = whichwrap_orig else call s:range_select(pos1p_head, pos2p_tail, s:choose_wise(a:flags)) endif return endfunction function! textobj#user#define(pat0, pat1, pat2, guideline) "{{{2 let pat0 = s:rhs_escape(a:pat0) let pat1 = s:rhs_escape(a:pat1) let pat2 = s:rhs_escape(a:pat2) for function_name in keys(a:guideline) let _lhss = a:guideline[function_name] if type(_lhss) == type('') let lhss = [_lhss] else let lhss = _lhss endif for lhs in lhss if function_name == 'move-to-next' execute 'nnoremap' s:mapargs_single_move(lhs, pat0, '', 'n') execute 'vnoremap' s:mapargs_single_move(lhs, pat0, '', 'v') execute 'onoremap' s:mapargs_single_move(lhs, pat0, '', 'o') elseif function_name == 'move-to-next-end' execute 'nnoremap' s:mapargs_single_move(lhs, pat0, 'e', 'n') execute 'vnoremap' s:mapargs_single_move(lhs, pat0, 'e', 'v') execute 'onoremap' s:mapargs_single_move(lhs, pat0, 'e', 'o') elseif function_name == 'move-to-prev' execute 'nnoremap' s:mapargs_single_move(lhs, pat0, 'b', 'n') execute 'vnoremap' s:mapargs_single_move(lhs, pat0, 'b', 'v') execute 'onoremap' s:mapargs_single_move(lhs, pat0, 'b', 'o') elseif function_name == 'move-to-prev-end' execute 'nnoremap' s:mapargs_single_move(lhs, pat0, 'be', 'n') execute 'vnoremap' s:mapargs_single_move(lhs, pat0, 'be', 'v') execute 'onoremap' s:mapargs_single_move(lhs, pat0, 'be', 'o') elseif function_name == 'select-next' || function_name == 'select' execute 'vnoremap' s:mapargs_single_select(lhs, pat0, '', 'v') execute 'onoremap' s:mapargs_single_select(lhs, pat0, '', 'o') elseif function_name == 'select-prev' execute 'vnoremap' s:mapargs_single_select(lhs, pat0, 'b', 'v') execute 'onoremap' s:mapargs_single_select(lhs, pat0, 'b', 'o') elseif function_name == 'select-pair-all' execute 'vnoremap' s:mapargs_pair_select(lhs, pat1, pat2, 'a', 'v') execute 'onoremap' s:mapargs_pair_select(lhs, pat1, pat2, 'a', 'o') elseif function_name == 'select-pair-inner' execute 'vnoremap' s:mapargs_pair_select(lhs, pat1, pat2, 'i', 'v') execute 'onoremap' s:mapargs_pair_select(lhs, pat1, pat2, 'i', 'o') else throw 'Unknown function name: ' . string(function_name) endif endfor endfor endfunction function! textobj#user#map(plugin_name, obj_specs, ...) "{{{2 if a:0 == 0 " It seems to be directly called by user - a:obj_specs are not normalized. call s:normalize(a:obj_specs) endif let banged_p = a:0 == 0 || a:1 for [obj_name, specs] in items(a:obj_specs) for [spec_name, spec_info] in items(specs) let rhs = s:interface_mapping_name(a:plugin_name, obj_name, spec_name) if s:is_non_ui_property_name(spec_name) " ignore elseif spec_name =~# '^move-[npNP]$' for lhs in spec_info call s:map(banged_p, lhs, rhs) endfor elseif spec_name =~# '^select\(\|-[ai]\)$' for lhs in spec_info call s:objmap(banged_p, lhs, rhs) endfor else throw printf('Unknown property: %s given to %s', \ string(spec_name), \ string(obj_name)) endif unlet spec_info " to avoid E706. endfor endfor call s:define_failsafe_key_mappings(a:plugin_name, a:obj_specs) endfunction function! textobj#user#plugin(plugin_name, obj_specs) "{{{2 if a:plugin_name =~# '\L' throw '{plugin} contains non-lowercase alphabet: ' . string(a:plugin_name) endif let plugin = a:plugin_name let Plugin = substitute(a:plugin_name, '^\(\l\)', '\u\1', 0) let g:__textobj_{plugin} = s:plugin.new(a:plugin_name, a:obj_specs) execute \ 'command! -bang -bar -nargs=0 Textobj'.Plugin.'DefaultKeyMappings' \ 'call g:__textobj_'.plugin.'.define_default_key_mappings("" == "!")' call g:__textobj_{plugin}.define_interface_key_mappings() if (!has_key(a:obj_specs, '*no-default-key-mappings*')) \ && (!exists('g:textobj_'.plugin.'_no_default_key_mappings')) execute 'Textobj'.Plugin.'DefaultKeyMappings' endif return g:__textobj_{plugin} endfunction " Misc. "{{{1 " pos "{{{2 " Terms: " gpos [bufnum, lnum, col, off] - a value returned by getpos() " spos [lnum, col] - a value returned by searchpos() " pos same as spos function! s:gpos_to_spos(gpos) return a:gpos[1:2] endfunction function! s:pos_headp(pos) return a:pos[1] <= 1 endfunction function! s:pos_lastp(pos) return a:pos[1] == len(getline(a:pos[0])) endfunction function! s:pos_le(pos1, pos2) " less than or equal return ((a:pos1[0] < a:pos2[0]) \ || (a:pos1[0] == a:pos2[0] && a:pos1[1] <= a:pos2[1])) endfunction " range "{{{2 function! s:range_containsp(range_head, range_tail, target_pos) return (s:pos_le(a:range_head, a:target_pos) \ && s:pos_le(a:target_pos, a:range_tail)) endfunction function! s:range_in_linep(range_head, range_tail, target_pos) return a:range_head[0] == a:target_pos[0] \ || a:range_tail[0] == a:target_pos[0] endfunction function! s:range_no_text_without_edgesp(range_head, range_tail) let [hl, hc] = a:range_head let [tl, tc] = a:range_tail return ((hl == tl && hc - tc == -1) \ || (hl - tl == -1 \ && (s:pos_lastp(a:range_head) && s:pos_headp(a:range_tail)))) endfunction function! s:range_validp(range_head, range_tail) let NULL_POS = [0, 0] return (a:range_head != NULL_POS) && (a:range_tail != NULL_POS) endfunction function! s:range_select(range_head, range_tail, fallback_wise) execute 'normal!' s:wise(a:fallback_wise) call cursor(a:range_head) normal! o call cursor(a:range_tail) if &selection ==# 'exclusive' normal! l endif endfunction " Save the last cursort position "{{{2 noremap (save-cursor-pos) save_cursor_pos() " let s:last_cursor_gpos = getpos('.') function! s:save_cursor_pos() let s:last_cursor_gpos = getpos('.') return '' endfunction " for textobj#user#define() "{{{2 function! s:rhs_escape(pattern) let r = a:pattern let r = substitute(r, '<', '', 'g') let r = substitute(r, '|', '', 'g') return r endfunction function! s:mapargs_single_move(lhs, pattern, flags, previous_mode) return printf(' %s :call textobj#user#move(%s, %s, %s)', \ a:lhs, \ string(a:pattern), string(a:flags), string(a:previous_mode)) endfunction function! s:mapargs_single_select(lhs, pattern, flags, previous_mode) return printf(' %s :call textobj#user#select(%s, %s, %s)', \ a:lhs, \ string(a:pattern), string(a:flags), string(a:previous_mode)) endfunction function! s:mapargs_pair_select(lhs, pattern1, pattern2, flags, previous_mode) return printf( \ ' %s :call textobj#user#select_pair(%s,%s,%s,%s)', \ a:lhs, \ string(a:pattern1), string(a:pattern2), \ string(a:flags), string(a:previous_mode) \ ) endfunction " for textobj#user#plugin() "{{{2 " basics "{{{3 let s:plugin = {} function! s:plugin.new(plugin_name, obj_specs) let _ = extend({'name': a:plugin_name, 'obj_specs': a:obj_specs}, \ s:plugin, 'keep') call _.normalize() return _ endfunction function! s:plugin.normalize() call s:normalize(self.obj_specs) endfunction function! s:normalize(obj_specs) call s:normalize_property_names(a:obj_specs) call s:normalize_property_values(a:obj_specs) endfunction function! s:normalize_property_names(obj_specs) for spec in values(a:obj_specs) for old_prop_name in keys(spec) if old_prop_name =~ '^\*.*\*$' let new_prop_name = substitute(old_prop_name, '^\*\(.*\)\*$', '\1', '') let spec[new_prop_name] = spec[old_prop_name] unlet spec[old_prop_name] endif endfor endfor endfunction function! s:normalize_property_values(obj_specs) for [obj_name, specs] in items(a:obj_specs) for [spec_name, spec_info] in items(specs) if s:is_ui_property_name(spec_name) if type(spec_info) == type('') let specs[spec_name] = [spec_info] endif endif if spec_name =~# '-function$' if spec_info =~# '^s:' if has_key(specs, 'sfile') let specs[spec_name] = substitute(spec_info, \ '^s:', \ s:snr_prefix(specs['sfile']), \ '') else echoerr '"sfile" must be given to use a script-local function:' \ string(spec_name) '/' string(spec_info) endif else " Nothing to do. endif elseif spec_name ==# 'pattern' if !has_key(specs, 'region-type') let specs['region-type'] = 'v' endif if !has_key(specs, 'scan') let specs['scan'] = 'forward' endif endif unlet spec_info " to avoid E706. endfor endfor endfunction function! s:plugin.define_default_key_mappings(banged_p) "{{{3 call textobj#user#map(self.name, self.obj_specs, a:banged_p) endfunction function! s:plugin.define_interface_key_mappings() "{{{3 let RHS_FORMAT = \ '(save-cursor-pos)' \ . ':call g:__textobj_' . self.name . '.%s(' \ . '"%s",' \ . '"%s",' \ . '""' \ . ')' for [obj_name, specs] in items(self.obj_specs) for spec_name in filter(keys(specs), 's:is_ui_property_name(v:val)') " lhs let lhs = self.interface_mapping_name(obj_name, spec_name) " rhs let _ = spec_name . '-function' if has_key(specs, _) let do = 'do_by_function' elseif has_key(specs, 'pattern') let do = 'do_by_pattern' else " skip to allow to define user's own {rhs} of the interface mapping. continue endif let rhs = printf(RHS_FORMAT, do, spec_name, obj_name) " map if spec_name =~# '^move' let MapFunction = function('s:noremap') else " spec_name =~# '^select' let MapFunction = function('s:objnoremap') endif call MapFunction(1, '