mirror of
https://github.com/machakann/vim-sandwich.git
synced 2026-05-28 00:26:34 +02:00
508 lines
15 KiB
VimL
508 lines
15 KiB
VimL
" stuff object - managing a line on buffer
|
|
|
|
let s:lib = operator#sandwich#lib#import()
|
|
|
|
" variables "{{{
|
|
" null valiables
|
|
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),
|
|
\ }
|
|
function! s:SID() abort
|
|
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
|
|
endfunction
|
|
let s:SNR = printf("\<SNR>%s_", s:SID())
|
|
delfunction s:SID
|
|
|
|
nnoremap <SID>(v) v
|
|
|
|
let s:KEY_v = printf('%s(v)', s:SNR)
|
|
|
|
" types
|
|
let s:type_list = type([])
|
|
|
|
" patchs
|
|
if v:version > 704 || (v:version == 704 && has('patch237'))
|
|
let s:has_patch_7_4_771 = has('patch-7.4.771')
|
|
let s:has_patch_7_4_358 = has('patch-7.4.358')
|
|
else
|
|
let s:has_patch_7_4_771 = v:version == 704 && has('patch771')
|
|
let s:has_patch_7_4_358 = v:version == 704 && has('patch358')
|
|
endif
|
|
"}}}
|
|
|
|
function! operator#sandwich#stuff#new() abort "{{{
|
|
return deepcopy(s:stuff)
|
|
endfunction
|
|
"}}}
|
|
|
|
" s:stuff "{{{
|
|
let s:stuff = {
|
|
\ 'active' : 1,
|
|
\ 'edges' : copy(s:null_2pos),
|
|
\ 'target' : copy(s:null_4pos),
|
|
\ 'acts' : [],
|
|
\ 'added' : [],
|
|
\ }
|
|
"}}}
|
|
function! s:stuff.initialize(count, cursor, modmark) dict abort "{{{
|
|
let self.active = 1
|
|
let self.acts = map(range(a:count), 'operator#sandwich#act#new()')
|
|
let self.added = []
|
|
for act in self.acts
|
|
call act.initialize(a:cursor, a:modmark, self.added)
|
|
endfor
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff.set_target() dict abort "{{{
|
|
if self.active
|
|
let head = copy(self.edges.head)
|
|
let tail = copy(self.edges.tail)
|
|
let self.target.head1 = head
|
|
let self.target.tail1 = head
|
|
let self.target.head2 = tail
|
|
let self.target.tail2 = tail
|
|
endif
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff.match(recipes, opt, ...) dict abort "{{{
|
|
if !self.active
|
|
return 0
|
|
endif
|
|
|
|
let edges = self.edges
|
|
let edges_saved = deepcopy(edges)
|
|
let match_edges = get(a:000, 0, 1)
|
|
if s:lib.is_valid_2pos(edges) && s:lib.is_ahead(edges.tail, edges.head)
|
|
let edge_chars = ['', '']
|
|
if self._match_recipes(a:recipes, a:opt) || (match_edges && self._match_edges(a:recipes, a:opt, edge_chars))
|
|
" found!
|
|
return 1
|
|
else
|
|
let [head_c, tail_c] = edge_chars
|
|
if head_c =~# '\s' || tail_c =~# '\s'
|
|
call self.skip_space()
|
|
if s:lib.is_ahead(edges.head, edges.tail)
|
|
" invalid edges after skip spaces
|
|
let self.active = 0
|
|
return 0
|
|
else
|
|
if self._match_recipes(a:recipes, a:opt, 1) || self._match_edges(a:recipes, a:opt, edge_chars)
|
|
" found
|
|
return 1
|
|
endif
|
|
endif
|
|
endif
|
|
endif
|
|
let edges = edges_saved
|
|
|
|
" skip characters
|
|
let target_list = []
|
|
for candidate in a:recipes
|
|
call a:opt.update('recipe_delete', candidate)
|
|
if a:opt.of('skip_char') && has_key(candidate, 'buns')
|
|
let head = edges.head
|
|
let tail = edges.tail
|
|
let opt_regex = a:opt.of('regex')
|
|
let patterns = s:get_patterns(candidate, opt_regex)
|
|
let target_list += [s:search_edges(head, tail, patterns)]
|
|
endif
|
|
endfor
|
|
if filter(target_list, 's:lib.is_valid_4pos(v:val)') != []
|
|
" found
|
|
let self.target = s:shortest(target_list)
|
|
return 1
|
|
endif
|
|
|
|
call a:opt.clear('recipe_delete')
|
|
endif
|
|
let self.active = 0
|
|
return 0
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff._match_recipes(recipes, opt, ...) dict abort "{{{
|
|
let is_space_skipped = get(a:000, 0, 0)
|
|
|
|
let found = 0
|
|
for candidate in a:recipes
|
|
call a:opt.update('recipe_delete', candidate)
|
|
if !is_space_skipped || a:opt.of('skip_space')
|
|
if has_key(candidate, 'buns')
|
|
" search buns
|
|
let patterns = s:get_patterns(candidate, a:opt.of('regex'))
|
|
let target = s:check_edges(self.edges.head, self.edges.tail, patterns)
|
|
elseif has_key(candidate, 'external')
|
|
" get difference of external motion/textobject
|
|
let target = s:check_textobj_diff(self.edges.head, self.edges.tail, candidate, a:opt.of('noremap', 'recipe_delete'))
|
|
else
|
|
let target = deepcopy(s:null_4pos)
|
|
endif
|
|
|
|
if s:lib.is_valid_4pos(target)
|
|
let found = 1
|
|
let self.target = target
|
|
break
|
|
endif
|
|
endif
|
|
endfor
|
|
return found
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff._match_edges(recipes, opt, edge_chars) dict abort "{{{
|
|
let head = self.edges.head
|
|
let tail = self.edges.tail
|
|
let head_c = s:get_cursorchar(head)
|
|
let tail_c = s:get_cursorchar(tail)
|
|
|
|
let found = 0
|
|
if head_c ==# tail_c
|
|
" check duplicate with recipe
|
|
for candidate in a:recipes
|
|
call a:opt.update('recipe_delete', candidate)
|
|
if has_key(candidate, 'buns') && !a:opt.of('regex')
|
|
\ && candidate.buns[0] ==# head_c
|
|
\ && candidate.buns[1] ==# tail_c
|
|
" The pair has already checked by a recipe.
|
|
return 0
|
|
endif
|
|
endfor
|
|
|
|
" both edges are matched
|
|
call a:opt.clear('recipe_delete')
|
|
if !(a:opt.of('skip_space') == 2 && head_c =~# '\s')
|
|
let found = 1
|
|
let self.target = {
|
|
\ 'head1': head, 'tail1': head,
|
|
\ 'head2': tail, 'tail2': tail,
|
|
\ }
|
|
endif
|
|
endif
|
|
|
|
let a:edge_chars[0] = head_c
|
|
let a:edge_chars[1] = tail_c
|
|
return found
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff.skip_space() dict abort "{{{
|
|
if self.active
|
|
call s:skip_space(self.edges.head, self.edges.tail)
|
|
if s:lib.is_ahead(self.edges.head, self.edges.tail)
|
|
let self.active = 0
|
|
endif
|
|
endif
|
|
endfunction
|
|
"}}}
|
|
function! s:stuff.hi_list(place, linewise) dict abort "{{{
|
|
if !self.active
|
|
return []
|
|
endif
|
|
|
|
let orderlist = []
|
|
if a:place ==# 'target'
|
|
let orderlist += [[self.target, [a:linewise, a:linewise]]]
|
|
elseif a:place ==# 'added'
|
|
for added in self.added
|
|
let orderlist += [[added, added.linewise]]
|
|
endfor
|
|
elseif a:place ==# 'stuff'
|
|
let stuff = {'head1': self.edges.head, 'tail1': self.edges.tail, 'head2': copy(s:null_pos), 'tail2': copy(s:null_pos)}
|
|
let orderlist += [[stuff, [0, 0]]]
|
|
endif
|
|
return orderlist
|
|
endfunction
|
|
"}}}
|
|
|
|
" private functions
|
|
function! s:check_edges(head, tail, patterns) abort "{{{
|
|
if a:patterns[0] ==# '' || a:patterns[1] ==# '' | return s:null_4pos | endif
|
|
|
|
call setpos('.', a:head)
|
|
let head1 = searchpos(a:patterns[0], 'c', a:tail[1])
|
|
|
|
if head1 != a:head[1:2] | return s:null_4pos | endif
|
|
|
|
call setpos('.', a:tail)
|
|
let tail2 = s:searchpos_bce(a:tail, a:patterns[1], a:head[1])
|
|
|
|
if tail2 != a:tail[1:2] | return s:null_4pos | endif
|
|
|
|
let head2 = searchpos(a:patterns[1], 'bc', a:head[1])
|
|
call setpos('.', a:head)
|
|
let tail1 = searchpos(a:patterns[0], 'ce', a:tail[1])
|
|
|
|
if head1 == s:null_coord || tail1 == s:null_coord
|
|
\ || head2 == s:null_coord || tail2 == s:null_coord
|
|
\ || s:lib.is_equal_or_ahead(s:lib.c2p(tail1), s:lib.c2p(head2))
|
|
return s:null_4pos
|
|
endif
|
|
|
|
let target = {
|
|
\ 'head1': head1, 'tail1': tail1,
|
|
\ 'head2': head2, 'tail2': tail2,
|
|
\ }
|
|
return map(target, 's:lib.c2p(v:val)')
|
|
endfunction
|
|
"}}}
|
|
function! s:search_edges(head, tail, patterns) abort "{{{
|
|
if a:patterns[0] ==# '' || a:patterns[1] ==# '' | return s:null_4pos | endif
|
|
|
|
call setpos('.', a:head)
|
|
let head1 = searchpos(a:patterns[0], 'c', a:tail[1])
|
|
|
|
call setpos('.', a:tail)
|
|
let tail2 = s:searchpos_bce(a:tail, a:patterns[1], a:head[1])
|
|
|
|
if head1 == s:null_coord || tail2 == s:null_coord
|
|
\ || s:lib.is_equal_or_ahead(s:lib.c2p(head1), s:lib.c2p(tail2))
|
|
return s:null_4pos
|
|
endif
|
|
|
|
let head2 = searchpos(a:patterns[1], 'bc', head1[0])
|
|
call setpos('.', s:lib.c2p(head1))
|
|
let tail1 = searchpos(a:patterns[0], 'ce', head2[0])
|
|
|
|
if tail1 == s:null_coord || head2 == s:null_coord
|
|
\ || s:lib.is_ahead(s:lib.c2p(head1), s:lib.c2p(tail1))
|
|
\ || s:lib.is_ahead(s:lib.c2p(head2), s:lib.c2p(tail2))
|
|
\ || s:lib.is_equal_or_ahead(s:lib.c2p(tail1), s:lib.c2p(head2))
|
|
return s:null_4pos
|
|
endif
|
|
|
|
let target = {
|
|
\ 'head1': head1, 'tail1': tail1,
|
|
\ 'head2': head2, 'tail2': tail2,
|
|
\ }
|
|
return map(target, 's:lib.c2p(v:val)')
|
|
endfunction
|
|
"}}}
|
|
function! s:check_textobj_diff(head, tail, candidate, opt_noremap) abort "{{{
|
|
let target = deepcopy(s:null_4pos)
|
|
if has_key(a:candidate, 'excursus')
|
|
let coord = a:candidate.excursus.coord
|
|
let target.head1 = s:lib.c2p(coord.head)
|
|
let target.tail1 = s:lib.get_left_pos(s:lib.c2p(coord.inner_head))
|
|
let target.head2 = s:lib.get_right_pos(s:lib.c2p(coord.inner_tail))
|
|
let target.tail2 = s:lib.c2p(coord.tail)
|
|
|
|
if target.head1 == a:head && target.tail2 == a:tail
|
|
\ && target.tail1 != s:null_pos && target.head2 != s:null_pos
|
|
\ && s:lib.is_equal_or_ahead(target.tail1, target.head1)
|
|
\ && s:lib.is_equal_or_ahead(target.tail2, target.head2)
|
|
return target
|
|
endif
|
|
endif
|
|
|
|
let [textobj_i, textobj_a] = a:candidate.external
|
|
let [visual_head, visual_tail] = [getpos("'<"), getpos("'>")]
|
|
let visualmode = visualmode()
|
|
if a:opt_noremap
|
|
let cmd = 'silent! normal!'
|
|
let v = 'v'
|
|
else
|
|
let cmd = 'silent! normal'
|
|
let v = s:KEY_v
|
|
endif
|
|
|
|
let order_list = [[1, a:head], [1, a:tail]]
|
|
if has_key(a:candidate, 'excursus')
|
|
let order_list = [[a:candidate.excursus.count, a:candidate.excursus.cursor]] + order_list
|
|
endif
|
|
|
|
let found = 0
|
|
for [l:count, cursor] in order_list
|
|
" get outer positions
|
|
let [target.head1, target.tail2, motionwise_a] = s:get_textobj_region(cursor, cmd, v, l:count, textobj_a)
|
|
|
|
" get inner positions
|
|
let [target.tail1, target.head2, motionwise_i] = s:get_textobj_region(cursor, cmd, v, l:count, textobj_i)
|
|
if motionwise_i ==# "\<C-v>"
|
|
normal! gv
|
|
if getpos('.')[1] == target.head2[1]
|
|
normal! O
|
|
let target.tail1 = getpos('.')
|
|
normal! o
|
|
let target.head2 = getpos('.')
|
|
else
|
|
normal! O
|
|
let target.head2 = getpos('.')
|
|
normal! o
|
|
let target.tail1 = getpos('.')
|
|
endif
|
|
execute "normal! \<Esc>"
|
|
endif
|
|
|
|
" check validity
|
|
if target.head1 == a:head && target.tail2 == a:tail
|
|
\ && target.tail1 != s:null_pos && target.head2 != s:null_pos
|
|
\ && s:lib.is_ahead(target.tail1, target.head1)
|
|
\ && s:lib.is_ahead(target.tail2, target.head2)
|
|
let [target.tail1, target.head2] = s:lib.get_wider_region(target.tail1, target.head2)
|
|
let found = 1
|
|
break
|
|
endif
|
|
endfor
|
|
|
|
" restore visualmode
|
|
if visualmode ==# ''
|
|
call visualmode(1)
|
|
else
|
|
execute 'normal! ' . visualmode
|
|
execute 'normal! ' . "\<Esc>"
|
|
endif
|
|
" restore marks
|
|
call setpos("'<", visual_head)
|
|
call setpos("'>", visual_tail)
|
|
|
|
if found
|
|
return target
|
|
else
|
|
return s:null_4pos
|
|
endif
|
|
endfunction
|
|
"}}}
|
|
function! s:get_textobj_region(cursor, cmd, visualmode, count, key_seq) abort "{{{
|
|
call setpos('.', a:cursor)
|
|
execute printf('%s %s%d%s', a:cmd, a:visualmode, a:count, a:key_seq)
|
|
if mode() ==? 'v' || mode() ==# "\<C-v>"
|
|
execute "normal! \<Esc>"
|
|
else
|
|
return [copy(s:null_coord), copy(s:null_coord), a:visualmode]
|
|
endif
|
|
let visualmode = visualmode()
|
|
let [head, tail] = [getpos("'<"), getpos("'>")]
|
|
" NOTE: V never comes for v. Thus if head == tail == self.cursor, then
|
|
" it is failed.
|
|
if head == a:cursor && tail == a:cursor
|
|
let [head, tail] = [copy(s:null_pos), copy(s:null_pos)]
|
|
elseif visualmode ==# 'V'
|
|
let tail[2] = col([tail[1], '$'])
|
|
endif
|
|
return [head, tail, visualmode]
|
|
endfunction
|
|
"}}}
|
|
function! s:get_patterns(candidate, opt_regex) abort "{{{
|
|
let patterns = deepcopy(a:candidate.buns)
|
|
if !a:opt_regex
|
|
let patterns = map(patterns, 's:lib.escape(v:val)')
|
|
endif
|
|
|
|
" substitute a break "\n" to a regular expression pattern '\n'
|
|
let patterns = map(patterns, 'substitute(v:val, ''\n'', ''\\n'', ''g'')')
|
|
return patterns
|
|
endfunction
|
|
"}}}
|
|
" function! s:searchpos_bce(curpos, pattern, stopline) "{{{
|
|
if s:has_patch_7_4_771
|
|
function! s:searchpos_bce(curpos, pattern, stopline) abort
|
|
return searchpos(a:pattern, 'bce', a:stopline)
|
|
endfunction
|
|
else
|
|
" workaround for unicode string (because of a bug of vim)
|
|
" If the cursor is on a unicode character(uc), searchpos(uc, 'bce', stopline) always returns [0, 0],
|
|
" though searchpos(uc, 'bce') returns a correct value.
|
|
function! s:searchpos_bce(curpos, pattern, stopline) abort
|
|
if a:curpos[1] == line('$') && a:curpos[2] == col([line('$'), '$'])
|
|
silent! normal! h
|
|
return searchpos(a:pattern, 'e', a:stopline)
|
|
else
|
|
silent! normal! l
|
|
return searchpos(a:pattern, 'be', a:stopline)
|
|
endif
|
|
endfunction
|
|
endif
|
|
"}}}
|
|
function! s:get_cursorchar(pos) abort "{{{
|
|
let reg = ['"', getreg('"'), getregtype('"')]
|
|
try
|
|
call setpos('.', a:pos)
|
|
silent noautocmd normal! yl
|
|
let c = @@
|
|
finally
|
|
call call('setreg', reg)
|
|
endtry
|
|
return c
|
|
endfunction
|
|
"}}}
|
|
" function! s:shortest(list) abort "{{{
|
|
if s:has_patch_7_4_358
|
|
function! s:shortest(list) abort
|
|
call map(a:list, '[v:val, s:get_buf_length(v:val.head1, v:val.tail2)]')
|
|
call sort(a:list, 's:compare_buf_length')
|
|
return a:list[0][0]
|
|
endfunction
|
|
|
|
function! s:compare_buf_length(i1, i2) abort
|
|
return a:i2[1] - a:i1[1]
|
|
endfunction
|
|
else
|
|
function! s:shortest(list) abort
|
|
call map(a:list, '[v:val, s:get_buf_length(v:val.head1, v:val.tail2)]')
|
|
let len = len(a:list)
|
|
let min = len - 1
|
|
if len - 2 >= 0
|
|
for i in range(len - 2, 0, -1)
|
|
if a:list[min][1] >= a:list[i][1]
|
|
let min = i
|
|
endif
|
|
endfor
|
|
endif
|
|
return a:list[min][0]
|
|
endfunction
|
|
endif
|
|
"}}}
|
|
function! s:get_buf_length(start, end) abort "{{{
|
|
if a:start[1] == a:end[1]
|
|
let len = a:end[2] - a:start[2] + 1
|
|
else
|
|
let length_list = map(getline(a:start[1], a:end[1]), 'len(v:val) + 1')
|
|
let idx = 0
|
|
let accumm_length = 0
|
|
let accumm_list = [0]
|
|
for length in length_list[1:]
|
|
let accumm_length = accumm_length + length_list[idx]
|
|
let accumm_list += [accumm_length]
|
|
let idx += 1
|
|
endfor
|
|
let len = accumm_list[a:end[1] - a:start[1]] + a:end[2] - a:start[2] + 1
|
|
endif
|
|
return len
|
|
endfunction
|
|
"}}}
|
|
function! s:skip_space(head, tail) abort "{{{
|
|
" NOTE: This function is destructive, directly update a:head and a:tail.
|
|
call setpos('.', a:head)
|
|
if a:head[2] == col([a:head[1], '$'])
|
|
" if the cursor is on a line breaking, it should not be skipped.
|
|
let head = a:head
|
|
else
|
|
let head = s:lib.c2p(searchpos('\_S', 'c', a:tail[1]))
|
|
endif
|
|
|
|
call setpos('.', a:tail)
|
|
if a:tail[2] == 1
|
|
let tail = a:tail
|
|
else
|
|
let tail = s:lib.c2p(searchpos('\_S', 'bc', a:head[1]))
|
|
endif
|
|
|
|
if head != s:null_pos && tail != s:null_pos && s:lib.is_equal_or_ahead(tail, head)
|
|
let a:head[0:3] = head[0:3]
|
|
let a:tail[0:3] = tail[0:3]
|
|
endif
|
|
endfunction
|
|
"}}}
|
|
|
|
|
|
|
|
" vim:set foldmethod=marker:
|
|
" vim:set commentstring="%s:
|
|
" vim:set ts=2 sts=2 sw=2:
|