Files
machakann 74898e6f5c Do not touch v:errmsg
Repoted at #147, #148
2022-07-23 01:02:34 +09:00

1065 lines
31 KiB
VimL

" operator object - controlling a whole operation
let s:lib = operator#sandwich#lib#import()
" variables "{{{
" null valiables
let s:TRUE = 1
let s:FALSE = 0
let s:null_coord = [0, 0]
let s:null_pos = [0, 0, 0, 0]
let s:null_2pos = {
\ 'head': copy(s:null_pos),
\ 'tail': copy(s:null_pos),
\ }
let s:null_4pos = {
\ 'head1': copy(s:null_pos),
\ 'tail1': copy(s:null_pos),
\ 'head2': copy(s:null_pos),
\ 'tail2': copy(s:null_pos),
\ }
" types
let s:type_num = type(0)
let s:type_str = type('')
let s:type_list = type([])
let s:type_fref = type(function('tr'))
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_310 = has('patch-7.4.310')
else
let s:has_patch_7_4_310 = v:version == 704 && has('patch310')
endif
" functions
let s:exists_reg_executing = exists('*reg_executing')
" features
let s:has_gui_running = has('gui_running')
let s:has_timer = has('timers')
" visualrepeat.vim (vimscript #3848) support
let s:visualrepeat_exists = -1
"}}}
function! operator#sandwich#operator#new() abort "{{{
return deepcopy(s:operator)
endfunction
"}}}
" s:operator "{{{
let s:operator = {
\ 'state' : 0,
\ 'kind' : '',
\ 'motionwise': '',
\ 'count' : 1,
\ 'n' : 0,
\ 'mode' : 'n',
\ 'view' : {},
\ 'blockwidth': 0,
\ 'extended' : 0,
\ 'at_work' : 0,
\ 'opt' : {},
\ 'basket' : [],
\ 'recipes' : {
\ 'arg' : [],
\ 'integrated': [],
\ 'dog_ear' : [],
\ 'synchro': {'on': 0, 'kind': '', 'recipe': {}},
\ },
\ 'cursor': {
\ 'head' : copy(s:null_pos),
\ 'headend' : copy(s:null_pos),
\ 'inner_head': copy(s:null_pos),
\ 'keep' : copy(s:null_pos),
\ 'inner_tail': copy(s:null_pos),
\ 'tailstart' : copy(s:null_pos),
\ 'tail' : copy(s:null_pos),
\ 'default' : copy(s:null_pos),
\ 'keepable' : 0,
\ },
\ 'modmark': copy(s:null_2pos),
\ 'highlight': {
\ 'target': sandwich#highlight#new(),
\ 'added': sandwich#highlight#new(),
\ 'stuff': sandwich#highlight#new(),
\ },
\ }
"}}}
function! s:operator.execute(motionwise) dict abort "{{{
let options = s:shift_options(self.kind, self.mode)
let messenger = sandwich#messenger#get()
try
call self.initialize(a:motionwise)
if self.state >= 0
call self[self.kind]()
endif
catch /^OperatorSandwichError:ReadOnly/
catch /^OperatorSandwichError:IncorrectBuns/
unlet! g:operator#sandwich#object
catch /^OperatorSandwichCancel/
unlet! g:operator#sandwich#object
catch /^Vim:Interrupt$/
" cancelled by <C-c>
unlet! g:operator#sandwich#object
catch
call messenger.error.set(printf('Unanticipated error. [%s] %s', v:throwpoint, v:exception))
unlet! g:operator#sandwich#object
finally
call self.finalize()
call s:restore_options(self.kind, self.mode, options)
endtry
endfunction
"}}}
function! s:operator.initialize(motionwise) dict abort "{{{
let self.at_work = 1
let self.motionwise = a:motionwise
call self.recipes.integrate(self.kind, a:motionwise, self.mode)
let region = s:get_assigned_region(self.kind, a:motionwise)
let region_list = a:motionwise ==# 'block' ? self.split(region) : [region]
if region == s:null_2pos
" deactivate
let self.state = -1
endif
let self.n = len(region_list) " Number of lines in the target region
let self.cursor.inner_head = deepcopy(region.head)
let self.cursor.inner_tail = deepcopy(region.tail)
if self.state
let self.basket = map(range(self.n), 'operator#sandwich#stuff#new()')
else
let self.view = winsaveview()
let self.modmark.head = copy(s:null_pos)
let self.modmark.tail = copy(s:null_pos)
call self.fill()
endif
for stuff in self.basket
call stuff.initialize(self.count, self.cursor, self.modmark)
endfor
" set initial values
for i in range(self.n)
let stuff = self.basket[i]
let stuff.edges = region_list[i]
endfor
endfunction
"}}}
function! s:operator.split(region) dict abort "{{{
let reg = ['"', getreg('"'), getregtype('"')]
let view = winsaveview()
let virtualedit = &virtualedit
let &virtualedit = 'all'
try
if self.blockwidth == 0
" The case for blockwise motions in operator-pending mode
execute "normal! `[\<C-v>`]"
silent noautocmd normal! ""y
let regtype = getregtype('"')
let self.blockwidth = str2nr(regtype[1:])
endif
let disp_region = map(copy(a:region), 's:get_displaycoord(v:val)')
let lnum_top = disp_region.head[0]
let lnum_bottom = disp_region.tail[0]
let col_head = disp_region.head[1]
let col_tail = col_head + self.blockwidth - 1
let region_list = []
if self.extended
for lnum in reverse(range(lnum_top, lnum_bottom))
call s:set_displaycoord([lnum, col_head])
let head = getpos('.')
let tail = [0, lnum, col([lnum, '$']) - 1, 0]
if head[3] == 0 && s:lib.is_equal_or_ahead(tail, head)
let region_list += [{'head': head, 'tail': tail}]
endif
endfor
else
for lnum in reverse(range(lnum_top, lnum_bottom))
call s:set_displaycoord([lnum, col_head])
let head = getpos('.')
call s:set_displaycoord([lnum, col_tail])
let tail = getpos('.')
let endcol = col([lnum, '$'])
if head[2] != endcol && s:lib.is_equal_or_ahead(tail, head)
if tail[2] == endcol
let tail[2] = endcol - 1
let tail[3] = 0
endif
let region_list += [{'head': head, 'tail': tail}]
endif
endfor
endif
finally
call call('setreg', reg)
call winrestview(view)
let &virtualedit = virtualedit
endtry
return region_list
endfunction
"}}}
function! s:operator.fill() dict abort "{{{
let lack = self.n - len(self.basket)
if lack > 0
let fillings = map(range(lack), 'operator#sandwich#stuff#new()')
call extend(self.basket, fillings)
endif
endfunction
"}}}
function! s:operator.add() dict abort "{{{
let opt = self.opt
let hi_group = opt.of('highlight', '') >= 2 ? 'OperatorSandwichChange' : 'OperatorSandwichBuns'
let modified = 0
for i in range(self.count)
call self.set_target()
if self.state
" query preferable buns
call self.show('stuff', hi_group)
try
let recipe = self.query()
let self.recipes.dog_ear += [recipe]
finally
call self.quench('stuff')
endtry
call opt.update('recipe_add', recipe)
if i == 0 && self.count > 1 && opt.of('query_once')
call self.recipes.fill(recipe, self.count)
let self.state = 0
endif
else
let recipe = self.recipes.dog_ear[i]
call opt.update('recipe_add', recipe)
endif
if !has_key(recipe, 'buns') || empty(recipe.buns)
break
endif
call self.skip_space(i)
let modified = self.add_once(i, recipe) || modified
call opt.clear('recipe_add')
endfor
if modified
call self.highlight_added(opt)
" visualrepeat.vim support
call s:visualrepeat_set('add', self.count)
endif
endfunction
"}}}
function! s:operator.add_once(i, recipe) dict abort "{{{
let buns = s:get_buns(a:recipe, self.opt.of('expr'), self.opt.of('listexpr'))
let undojoin = a:i == 0 || self.state == 0 ? 0 : 1
let modified = 0
if buns[0] !=# '' || buns[1] !=# '' || self.opt.of('linewise')
for j in range(self.n)
let stuff = self.basket[j]
if stuff.active
let act = stuff.acts[a:i]
let act.opt = deepcopy(self.opt)
let success = act.add_pair(buns, stuff, undojoin)
let undojoin = success ? 0 : undojoin
let modified = modified || success
endif
endfor
if modified
let self.cursor.head = copy(self.modmark.head)
let self.cursor.tail = s:lib.get_left_pos(self.modmark.tail)
endif
endif
return modified
endfunction
"}}}
function! s:operator.delete() dict abort "{{{
let hi_exited = 0
let opt_highlight = self.opt.of('highlight', '')
let hi_duration = self.opt.of('hi_duration', '')
let hi_group = opt_highlight >= 2 ? 'OperatorSandwichDelete' : 'OperatorSandwichBuns'
let modified = 0
for i in range(self.count)
if !self.match(i)
break
endif
if opt_highlight && !hi_exited && hi_duration > 0
let hi_exited = self.blink('target', hi_group, hi_duration)
endif
let modified = self.delete_once(i) || modified
endfor
if modified
" visualrepeat.vim support
call s:visualrepeat_set('delete', self.count)
endif
endfunction
"}}}
function! s:operator.delete_once(i) dict abort "{{{
let modified = 0
for j in range(self.n)
let stuff = self.basket[j]
if stuff.active
let act = stuff.acts[a:i]
let success = act.delete_pair(stuff, modified)
let modified = modified || success
endif
endfor
if modified
let self.cursor.head = copy(self.modmark.head)
let self.cursor.tail = s:lib.get_left_pos(self.modmark.tail)
endif
return modified
endfunction
"}}}
function! s:operator.replace() dict abort "{{{
let opt = self.opt
let hi_group = opt.of('highlight', '') >= 2 ? 'OperatorSandwichDelete' : 'OperatorSandwichBuns'
let self.cursor.inner_head = copy(s:null_pos)
let self.cursor.inner_tail = copy(s:null_pos)
let modified = 0
for i in range(self.count)
if !self.match(i)
break
endif
if self.state
" query preferable buns
call self.show('target', hi_group)
try
let recipe = self.query()
let self.recipes.dog_ear += [recipe]
finally
call self.quench('target')
endtry
call opt.update('recipe_add', recipe)
if i == 0 && self.count > 1 && opt.of('query_once')
call self.recipes.fill(recipe, self.count)
let self.state = 0
endif
else
let recipe = self.recipes.dog_ear[i]
call opt.update('recipe_add', recipe)
endif
if !has_key(recipe, 'buns') || empty(recipe.buns)
break
endif
let modified = self.replace_once(i, recipe) || modified
call opt.clear('recipe_add')
endfor
if modified
call self.highlight_added(opt)
" visualrepeat.vim support
call s:visualrepeat_set('replace', self.count)
endif
endfunction
"}}}
function! s:operator.replace_once(i, recipe) dict abort "{{{
let buns = s:get_buns(a:recipe, self.opt.of('expr'), self.opt.of('listexpr'))
let undojoin = a:i == 0 || self.state == 0 ? 0 : 1
let modified = 0
for j in range(self.n)
let stuff = self.basket[j]
if stuff.active
let act = stuff.acts[a:i]
call act.opt.update('recipe_add', a:recipe)
let success = act.replace_pair(buns, stuff, undojoin, modified)
let undojoin = success ? 0 : undojoin
let modified = modified || success
endif
endfor
if modified
let self.cursor.head = copy(self.modmark.head)
let self.cursor.tail = s:lib.get_left_pos(self.modmark.tail)
endif
return modified
endfunction
"}}}
function! s:operator.set_target() dict abort "{{{
for i in range(self.n)
let stuff = self.basket[i]
call stuff.set_target()
endfor
endfunction
"}}}
function! s:operator.skip_space(i) dict abort "{{{
let opt = self.opt
if a:i == 0 && opt.of('skip_space')
" skip space only in the first count.
for j in range(self.n)
let stuff = self.basket[j]
call stuff.skip_space()
call stuff.set_target()
endfor
" for cursor positions
if !opt.of('linewise')
let top_stuff = self.basket[self.n-1]
let bot_stuff = self.basket[0]
let self.cursor.inner_head = deepcopy(top_stuff.edges.head)
let self.cursor.inner_tail = deepcopy(bot_stuff.edges.tail)
endif
endif
endfunction
"}}}
function! s:operator.query() dict abort "{{{
let filter = 'has_key(v:val, "buns")
\ && (!has_key(v:val, "regex") || !v:val.regex)
\ && s:has_action(v:val, "add")'
let recipes = filter(deepcopy(self.recipes.integrated), filter)
let opt = self.opt
let clock = sandwich#clock#new()
let timeout = s:lib.get_sandwich_option('timeout', &timeout)
let timeoutlen = s:lib.get_sandwich_option('timeoutlen', &timeoutlen)
let timeoutlen = max([0, timeoutlen])
" query phase
let input = ''
let last_compl_match = ['', []]
while 1
let c = getchar(0)
if empty(c)
if clock.started && timeout && timeoutlen > 0 && clock.elapsed() > timeoutlen
let [input, recipes] = last_compl_match
break
else
sleep 20m
continue
endif
endif
let c = type(c) == s:type_num ? nr2char(c) : c
" exit loop if <Esc> is pressed
if c is# "\<Esc>"
let input = "\<Esc>"
break
endif
let input .= c
" check forward match
let n_fwd = len(filter(recipes, 's:is_input_matched(v:val, input, opt, 0)'))
" check complete match
let n_comp = len(filter(copy(recipes), 's:is_input_matched(v:val, input, opt, 1)'))
if n_comp || strchars(input) == 1
if len(recipes) == n_comp
break
else
call clock.stop()
call clock.start()
let last_compl_match = [input, copy(recipes)]
endif
else
if clock.started && !n_fwd
let [input, recipes] = last_compl_match
break
endif
endif
if recipes == [] | break | endif
endwhile
call clock.stop()
" pick up and register a recipe
if filter(recipes, 's:is_input_matched(v:val, input, opt, 1)') != []
let recipe = recipes[0]
else
if s:is_input_fallback(input)
let c = split(input, '\zs')[0]
let recipe = {'buns': [c, c], 'expr': 0}
else
let recipe = {}
endif
endif
return extend(recipe, {'evaluated': 0})
endfunction
"}}}
function! s:operator.match(i) dict abort "{{{
let opt = self.opt
let default_expr = opt.get('expr', '', 0)
let default_listexpr = opt.get('listexpr', '', 0)
let source = self.recipes.synchro.on && self.recipes.synchro.kind ==# 'query'
\ ? self.recipes.synchro.recipe
\ : self.recipes.integrated
let filter = 's:has_action(v:val, "delete")
\ && s:is_not_expr(v:val, default_expr, default_listexpr)
\ && s:is_appropriate_patterns(v:val)'
let recipes = filter(deepcopy(source), filter)
" uniq recipes
call s:uniq_recipes(recipes, opt.of('regex'), opt.get('noremap', ''))
let success = 0
let match_edges = !self.recipes.synchro.on
for j in range(self.n)
let stuff = self.basket[j]
let act = stuff.acts[a:i]
let act.opt = deepcopy(self.opt)
let success = stuff.match(recipes, act.opt, match_edges) || success
endfor
return success
endfunction
"}}}
function! s:operator.show(place, hi_group, ...) dict abort "{{{
let forcibly = get(a:000, 0, 0)
if !forcibly && (!self.opt.of('highlight') || s:reg_executing() isnot# '')
return 1
endif
let highlight = self.highlight[a:place]
call highlight.initialize()
for i in range(self.n)
let stuff = self.basket[i]
for [order, linewise] in stuff.hi_list(a:place, self.opt.of('linewise'))
call highlight.order(order, linewise)
endfor
endfor
let success = highlight.show(a:hi_group)
call winrestview(self.view)
redraw
return !success
endfunction
"}}}
function! s:operator.quench(place) dict abort "{{{
let highlight = self.highlight[a:place]
return highlight.quench()
endfunction
"}}}
function! s:operator.quench_timer(place, duration) abort "{{{
if a:duration <= 0
call self.quench(a:place)
endif
let highlight = self.highlight[a:place]
call highlight.quench_timer(a:duration)
endfunction
"}}}
function! s:operator.blink(place, hi_group, duration) dict abort "{{{
if !self.opt.of('highlight') || a:duration <= 0
return 1
endif
" highlight off: limit the number of highlighting region to one explicitly
call sandwich#highlight#cancel()
let clock = sandwich#clock#new()
let hi_exited = 0
let linewise = get(a:000, 0, 0)
call self.show(a:place, a:hi_group)
try
let c = 0
call clock.start()
while empty(c)
let c = getchar(0)
if clock.started && clock.elapsed() > a:duration
break
endif
sleep 20m
endwhile
if c != 0
let c = type(c) == s:type_num ? nr2char(c) : c
let hi_exited = 1
call feedkeys(c, 'it')
endif
finally
call self.quench(a:place)
call clock.stop()
endtry
return hi_exited
endfunction
"}}}
function! s:operator.glow(place, hi_group, duration) dict abort "{{{
if !self.opt.of('highlight') || a:duration <= 0
return
endif
" highlight off: limit the number of highlighting region to one explicitly
call sandwich#highlight#cancel()
call self.show(a:place, a:hi_group)
call self.quench_timer(a:place, a:duration)
endfunction
"}}}
function! s:operator.highlight_added(opt) dict abort "{{{
if a:opt.of('highlight', '') < 3
return
endif
let hi_duration = a:opt.of('hi_duration', '')
let hi_method = s:lib.get_operator_option('persistent_highlight', 'glow')
if hi_method ==# 'glow' && s:has_timer
call self.glow('added', 'OperatorSandwichAdd', hi_duration)
else
call self.blink('added', 'OperatorSandwichAdd', hi_duration)
endif
endfunction
"}}}
function! s:operator.finalize() dict abort "{{{
if self.state >= 0
" restore view
if self.view != {}
call winrestview(self.view)
let self.view = {}
endif
let act = self.last_succeeded()
if act != {}
" set modified marks
let modmark = self.modmark
if modmark.head != s:null_pos && modmark.tail != s:null_pos
\ && s:lib.is_equal_or_ahead(modmark.tail, modmark.head)
call setpos("'[", modmark.head)
call setpos("']", modmark.tail)
endif
" set cursor position
let cursor_opt = act.opt.of('cursor')
if self.cursor.keepable
let self.cursor.keepable = 0
else
" In the case of dot repeat, it is impossible to keep original position
" unless self.cursor.keepable == 1.
let self.cursor.keep = copy(self.cursor.default)
endif
if cursor_opt ==# 'headend'
let self.cursor.headend = s:get_headend_cursor_pos(self.cursor.head, self.cursor.inner_head)
elseif cursor_opt ==# 'tailstart'
let self.cursor.tailstart = s:get_tailstart_cursor_pos(self.cursor.tail, self.cursor.inner_tail)
endif
let self.cursor.default = s:get_default_cursor_pos(self.cursor.inner_head)
let cursor = get(self.cursor, cursor_opt, 'default')
if cursor == s:null_pos
let cursor = self.cursor.default
endif
if s:has_patch_7_4_310
" set curswant explicitly
call setpos('.', cursor + [cursor[2]])
else
call setpos('.', cursor)
endif
endif
endif
" set state
let self.state = 0
let self.at_work = 0
endfunction
"}}}
function! s:operator.last_succeeded() abort "{{{
for i in range(self.count - 1, 0, -1)
for j in range(self.n)
let stuff = self.basket[j]
let act = stuff.acts[i]
if act.success
return act
endif
endfor
endfor
return {}
endfunction
"}}}
function! s:operator.recipes.integrate(kind, motionwise, mode) dict abort "{{{
let self.integrated = []
if self.arg_given
let self.integrated += self.arg
else
let self.integrated += sandwich#get_recipes()
let self.integrated += operator#sandwich#get_recipes()
endif
if self.synchro.on
let self.integrated += self.synchro.recipe
endif
let filter = 's:has_filetype(v:val)
\ && s:has_kind(v:val, a:kind)
\ && s:has_motionwise(v:val, a:motionwise)
\ && s:has_mode(v:val, a:mode)
\ && s:expr_filter(v:val)'
call filter(self.integrated, filter)
call reverse(self.integrated)
endfunction
"}}}
function! s:operator.recipes.fill(recipe, count) dict abort "{{{
while len(self.dog_ear) < a:count
call add(self.dog_ear, a:recipe)
endwhile
endfunction
"}}}
" filters
function! s:has_filetype(candidate) abort "{{{
if !has_key(a:candidate, 'filetype')
return 1
else
let filetypes = split(&filetype, '\.')
if filetypes == []
let filter = 'v:val ==# "all" || v:val ==# ""'
else
let filter = 'v:val ==# "all" || index(filetypes, v:val) > -1'
endif
return filter(copy(a:candidate['filetype']), filter) != []
endif
endfunction
"}}}
function! s:has_kind(candidate, kind) abort "{{{
if !has_key(a:candidate, 'kind')
return 1
else
let filter = 'v:val ==# a:kind || v:val ==# "operator" || v:val ==# "all"'
return filter(copy(a:candidate['kind']), filter) != []
endif
endfunction
"}}}
function! s:has_motionwise(candidate, motionwise) abort "{{{
if !has_key(a:candidate, 'motionwise')
return 1
else
let filter = 'v:val ==# a:motionwise || v:val ==# "all"'
return filter(copy(a:candidate['motionwise']), filter) != []
endif
endfunction
"}}}
function! s:has_mode(candidate, mode) abort "{{{
if !has_key(a:candidate, 'mode')
return 1
else
return stridx(join(a:candidate['mode'], ''), a:mode) > -1
endif
endfunction
"}}}
function! s:has_action(candidate, action) abort "{{{
if !has_key(a:candidate, 'action')
return 1
endif
let filter = 'v:val ==# a:action || v:val ==# "all"'
return filter(copy(a:candidate['action']), filter) != []
endfunction
"}}}
function! s:expr_filter(candidate) abort "{{{
if !has_key(a:candidate, 'expr_filter')
return 1
else
for filter in a:candidate['expr_filter']
if !eval(filter)
return 0
endif
endfor
return 1
endif
endfunction
"}}}
" visualrepeat.vim (vimscript #3848) support
function! s:visualrepeat_set(kind, count) abort "{{{
if !exists('g:operator_sandwich_no_visualrepeat')
let key = printf("\<Plug>(operator-sandwich-%s-visualrepeat)", a:kind)
try
call visualrepeat#set(key, a:count)
catch /^Vim\%((\a\+)\)\=:E117:/
endtry
endif
endfunction
"}}}
" private functions
function! s:shift_options(kind, mode) abort "{{{
""" save options
let options = {}
let options.virtualedit = &virtualedit
let options.whichwrap = &whichwrap
let options.cpoptions = &cpoptions
let options.formatoptions = &formatoptions
""" tweak appearance
" hide_cursor
if s:has_gui_running
let options.cursor = &guicursor
set guicursor+=n-o:block-NONE
else
let options.cursor = &t_ve
set t_ve=
endif
" hide cursorline highlight if in visualmode.
" FIXME: The cursorline would be shown at the top line of assigned region
" in a moment currently. This is not good. This could be avoidable
" if the tweak was done in operator#sandwich#prerequisite().
" However, since operatorfunc is possible not to be called (when
" motion or textobj is canceled), it cannot be restored safely...
" Another way to avoid is to set lazyredraw on constantly.
if (a:kind ==# 'add' || a:kind ==# 'replace') && a:mode ==# 'x'
let options.cursorline = &l:cursorline
setlocal nocursorline
endif
""" shift options
set virtualedit=onemore
set whichwrap=h,l
set cpoptions-=l
set cpoptions-=\
setlocal formatoptions=
return options
endfunction
"}}}
function! s:restore_options(kind, mode, options) abort "{{{
if (a:kind ==# 'add' || a:kind ==# 'replace') && a:mode ==# 'x'
let &l:cursorline = a:options.cursorline
endif
if s:has_gui_running
set guicursor&
let &guicursor = a:options.cursor
else
let &t_ve = a:options.cursor
endif
let &virtualedit = a:options.virtualedit
let &whichwrap = a:options.whichwrap
let &cpoptions = a:options.cpoptions
let &l:formatoptions = a:options.formatoptions
endfunction
"}}}
function! s:get_assigned_region(kind, motionwise) abort "{{{
let region = {'head': getpos("'["), 'tail': getpos("']")}
" early-quit conditions
if !s:is_valid_region(a:kind, region, a:motionwise)
return deepcopy(s:null_2pos)
endif
if a:motionwise ==# 'line'
let region.head[2] = 1
let region.tail[2] = col([region.tail[1], '$']) - 1
else
if region.tail[2] >= col([region.tail[1], '$'])
let region.tail[2] -= 1
endif
endif
if region.tail[2] < 1
let region.tail[2] = 1
endif
" for multibyte characters
if region.tail[2] != col([region.tail[1], '$']) && region.tail[3] == 0
let cursor = getpos('.')
call setpos('.', region.tail)
let letterhead = searchpos('\zs', 'bcn', line('.'))
if letterhead[1] > region.tail[2]
" try again without 'c' flag if letterhead is behind the original
" position. It may look strange but it happens with &enc ==# 'cp932'
let letterhead = searchpos('\zs', 'bn', line('.'))
endif
let region.tail = [0] + letterhead + [0]
call setpos('.', cursor)
endif
" check validity again
if !s:is_valid_region(a:kind, region)
return deepcopy(s:null_2pos)
endif
return region
endfunction
"}}}
function! s:is_valid_region(kind, region, ...) abort "{{{
" If the third argument is given and it is 'line', ignore the geometric
" condition of head and tail.
return s:lib.is_valid_2pos(a:region)
\ && (
\ (a:kind ==# 'add' && s:lib.is_equal_or_ahead(a:region.tail, a:region.head))
\ || ((a:kind ==# 'delete' || a:kind ==# 'replace') && s:lib.is_ahead(a:region.tail, a:region.head))
\ || (a:0 > 0 && a:1 ==# 'line')
\ )
endfunction
"}}}
function! s:get_displaycoord(pos) abort "{{{
let [lnum, col, offset] = a:pos[1:3]
if [lnum, col] != s:null_coord
if col == 1
let disp_col = 1
else
let disp_col = strdisplaywidth(getline(lnum)[: col - 2]) + 1 + offset
endif
else
let disp_col = 0
endif
return [lnum, disp_col]
endfunction
"}}}
function! s:set_displaycoord(disp_coord) abort "{{{
if a:disp_coord != s:null_coord
execute 'normal! ' . a:disp_coord[0] . 'G' . a:disp_coord[1] . '|'
endif
endfunction
"}}}
function! s:uniq_recipes(recipes, opt_regex, opt_noremap) abort "{{{
let recipes = copy(a:recipes)
call filter(a:recipes, 0)
while recipes != []
let recipe = remove(recipes, 0)
call add(a:recipes, recipe)
if has_key(recipe, 'buns')
let ref_regex = get(recipe, 'regex', a:opt_regex)
call filter(recipes, '!s:is_duplicated_buns(v:val, recipe, ref_regex, a:opt_regex)')
elseif has_key(recipe, 'external')
call filter(recipes, '!s:is_duplicated_external(v:val, recipe, a:opt_noremap)')
endif
endwhile
endfunction
"}}}
function! s:is_duplicated_buns(item, ref, ref_regex, opt_regex) abort "{{{
if has_key(a:item, 'buns')
\ && a:ref['buns'][0] ==# a:item['buns'][0]
\ && a:ref['buns'][1] ==# a:item['buns'][1]
\ && get(a:item, 'regex', a:opt_regex) == a:ref_regex
return 1
endif
return 0
endfunction
"}}}
function! s:is_duplicated_external(item, ref, opt_noremap) abort "{{{
if has_key(a:item, 'external')
\ && a:ref['external'][0] ==# a:item['external'][0]
\ && a:ref['external'][1] ==# a:item['external'][1]
let noremap_r = get(a:ref, 'noremap', a:opt_noremap)
let noremap_i = get(a:item, 'noremap', a:opt_noremap)
if noremap_r == noremap_i
return 1
endif
endif
return 0
endfunction
"}}}
function! s:is_not_expr(item, default_expr, default_listexpr) abort "{{{
return get(a:item, 'expr', a:default_expr) || get(a:item, 'listexpr', a:default_listexpr) ? 0 : 1
endfunction
"}}}
function! s:is_appropriate_patterns(item) abort "{{{
if has_key(a:item, 'buns')
return type(a:item.buns) == s:type_list && len(a:item.buns) >= 2 ? 1 : 0
elseif has_key(a:item, 'external')
return type(a:item.external) == s:type_list && len(a:item.external) >= 2 ? 1 : 0
else
return 0
endif
endfunction
"}}}
function! s:is_input_matched(candidate, input, opt, flag) abort "{{{
if !has_key(a:candidate, 'buns')
return 0
elseif !a:flag && a:input ==# ''
return 1
endif
let candidate = deepcopy(a:candidate)
call a:opt.update('recipe_add', candidate)
" 'input' is necessary for 'expr' ('listexpr') buns
if (a:opt.of('expr') || a:opt.of('listexpr')) && !has_key(candidate, 'input')
return 0
endif
" If a:flag == 0, check forward match. Otherwise, check complete match.
let inputs = get(candidate, 'input', candidate['buns'])
if a:flag
return filter(inputs, 'v:val ==# a:input') != []
else
let idx = strlen(a:input) - 1
return filter(inputs, 'v:val[: idx] ==# a:input') != []
endif
endfunction
"}}}
function! s:get_buns(recipe, opt_expr, opt_listexpr) abort "{{{
if a:opt_listexpr == 2
let buns = eval(a:recipe.buns)
elseif a:opt_listexpr == 1 && !a:recipe.evaluated
let buns = eval(a:recipe.buns)
unlet a:recipe.buns
let a:recipe.buns = buns
let a:recipe.evaluated = 1
elseif a:opt_expr == 2
let buns = map(copy(a:recipe.buns), 'eval(v:val)')
elseif a:opt_expr == 1 && !a:recipe.evaluated
let buns = map(a:recipe.buns, 'eval(v:val)')
let a:recipe.evaluated = 1
else
let buns = a:recipe.buns
endif
call s:check_buns(buns)
return buns
endfunction
"}}}
function! s:check_buns(buns) abort "{{{
let messenger = sandwich#messenger#get()
let error = 'OperatorSandwichError:IncorrectBuns'
if type(a:buns) != s:type_list
call messenger.error.set('Incorrect buns. : not a list -> ' . string(a:buns))
throw error
elseif len(a:buns) < 2
call messenger.error.set('Incorrect buns. : list too short -> ' . string(a:buns))
throw error
elseif !(type(a:buns[0]) == s:type_str || type(a:buns[0]) == s:type_num)
\ || !(type(a:buns[1]) == s:type_str || type(a:buns[1]) == s:type_num)
call messenger.error.set('Incorrect buns. : not string buns -> ' . string(a:buns))
throw error
endif
endfunction
"}}}
function! s:is_input_fallback(input) abort "{{{
if a:input ==# "\<Esc>" || a:input ==# '' || a:input =~# '^[\x80]'
return s:FALSE
endif
let input_fallback = get(g:, 'sandwich#input_fallback', s:TRUE)
if !input_fallback
return s:FALSE
endif
return s:TRUE
endfunction "}}}
function! s:get_default_cursor_pos(inner_head) abort "{{{
call setpos('.', a:inner_head)
let default = searchpos('^\s*\zs\S', 'cn', a:inner_head[1])
return default == s:null_coord ? a:inner_head : s:lib.c2p(default)
endfunction
"}}}
function! s:get_headend_cursor_pos(head, inner_head) abort "{{{
let headend = s:lib.get_left_pos(a:inner_head)
if headend == s:null_pos || s:lib.is_ahead(a:head, headend)
let headend = copy(a:head)
endif
return headend
endfunction
"}}}
function! s:get_tailstart_cursor_pos(tail, inner_tail) abort "{{{
let tailstart = s:lib.get_right_pos(a:inner_tail)
if tailstart == s:null_pos || s:lib.is_ahead(tailstart, a:tail)
let tailstart = copy(a:tail)
endif
return tailstart
endfunction
"}}}
function! s:reg_executing() abort "{{{
if s:exists_reg_executing
return reg_executing()
endif
return ''
endfunction "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2: