1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,2 @@
doc/tags
doc/tags-ja

View File

@ -0,0 +1,17 @@
language: generic
env:
- PPA=yes
install:
- if [ x"$PPA" == "xyes" ] ; then sudo add-apt-repository ppa:pi-rho/dev -y; fi
- sudo apt-get update -q
- sudo apt-get install vim-nox
- git clone https://github.com/thinca/vim-themis
before_script:
- vim --version
- chmod +x ./test/dot-repeat/test_dot.sh
script:
- ./vim-themis/bin/themis && ./vim-themis/bin/themis test/surround/test-surround.vim && ./test/dot-repeat/test_dot.sh

View File

@ -0,0 +1,242 @@
vim-sandwich
============
[![Build Status](https://travis-ci.org/machakann/vim-sandwich.svg)](https://travis-ci.org/machakann/vim-sandwich)
[![Build status](https://ci.appveyor.com/api/projects/status/8hgvi5410lceq53x/branch/master?svg=true)](https://ci.appveyor.com/project/machakann/vim-sandwich/branch/master)
`sandwich.vim` is a plugin that makes it super easy to work with stuff that comes in pairs, like brackets, quotes, and even HTML or XML tags. You can quickly get rid of them, swap them out, or slap new ones around your text.
# Examples
Let's dive into some quick examples. If you're inside a string with double quotes and you hit `sr"'`, you'll swap those double quotes for single quotes.
"Hello world!" -> 'Hello world!'
Want to turn that into an HTML tag? Easy, just type `sr'<q>` and watch it transform.
'Hello world!' -> <q>Hello world!</q>
To switch it back to double quotes, you'd do `srt"`.
<q>Hello world!</q> -> "Hello world!"
To strip away those quotes, just press `sd"`.
"Hello world!" -> Hello world!
Say you want to bracket the word "Hello", move your cursor there and press `saiw]`.
Hello world! -> [Hello] world!
Fancy braces with some breathing room? Type `sr]{`.
[Hello] world! -> { Hello } world!
Wrap the whole line in parentheses with `sasb` or `sas)`.
{ Hello } world! -> ({ Hello } world!)
To get back to where you started, just do `sd{sd)`.
({ Hello } world!) -> Hello world!
Highlight "Hello" with an HTML emphasis tag by typing `saiw<em>`.
Hello world! -> <em>Hello</em> world!
For a bigger change, like wrapping the whole line in a paragraph tag with a class, first select the line with `V` and then apply `S<p class="important">`.
<em>Hello</em> world! -> <p class="important"><em>Hello</em> world!</p>
This tool is a game-changer for editing HTML and XML in Vim, which is an area that doesn't have a ton of great tools right now. With vim-sandwich, adding, changing, or removing tag pairs is super simple.
If you are using [vim-surround](https://github.com/tpope/vim-surround), you can use a preset keymappings similar as it. See [here](https://github.com/machakann/vim-sandwich/wiki/Introduce-vim-surround-keymappings)
# Design
This plugin provides functions to add/delete/replace surroundings of a sandwiched text. These functions are implemented genuinely by utilizing operator/textobject framework. Thus their action can be repeated by `.` command without any dependency. It consists of two parts, **operator-sandwich** and **textobj-sandwich**.
### operator-sandwich
A sandwiched text could be resolved into two parts, {surrounding} and {surrounded text}.
* Add surroundings: mapped to the key sequence `sa`
* {surrounded text} ---> {surrounding}{surrounded text}{surrounding}
* Delete surroundings: mapped to the key sequence `sd`
* {surrounding}{surrounded text}{surrounding} ---> {surrounded text}
* Replace surroundings: mapped to the key sequence `sr`
* {surrounding}{surrounded text}{surrounding} ---> {new surrounding}{surrounded text}{new surrounding}
### textobj-sandwich
* Search and select a sandwiched text automatically: mapped to the key sequence `ib` and `ab`
* Search and select a sandwiched text with query: mapped to the key sequence `is` and `as`
`ib` and `is` selects {surrounded text}. `ab` and `as` selects {surrounded text} including {surrounding}s.
```
|<----ib,is---->|
{surrounding}{surrounded text}{surrounding}
|<-----------------ab,as----------------->|
```
### Configuration
The point is that it would be nice to be shared the definitions of {surrounding}s pairs in all kinds of operations. User can freely add new settings to extend the functionality. If `g:sandwich#recipes` was defined, this plugin works with the settings inside. As a first step, it would be better to copy the default settings in `g:sandwich#default_recipes`.
```vim
let g:sandwich#recipes = deepcopy(g:sandwich#default_recipes)
```
Each setting, it is called `recipe`, is a set of a definition of {surrounding}s pair and options. The key named `buns` is used for the definition of {surrounding}.
```
let g:sandwich#recipes += [{'buns': [{surrounding}, {surrounding}], 'option-name1': {value1}, 'option-name2': {value2} ...}]
For example: {'buns': ['(', ')']}
foo ---> (foo)
```
Or there is a different way, use external textobjects to define {surrounding}s from the difference of two textobjects.
```
let g:sandwich#recipes += [{'external': [{textobj-i}, {textobj-a}], 'option-name1': {value1}, 'option-name2': {value} ...}]
For example: {'external': ['it', 'at']}
<title>foo</title> ---> foo
```
# Features
### Unique count handling
As for the default operators, the possible key input in normal mode is like this.
```
[count1]{operator}[count2]{textobject}
```
Default operators do not distinguish `[count1]` and `[count2]` but **operator-sandwich** does. `[count1]` is given for `{operators}` and `[count2]` is given for `{textobject}`.
### Linewise and blockwise operations
Operator-sandwich works linewise with the linewise-visual selection and linewise motions.
```vim
" press Vsa(
foo ---> (
foo
)
```
Using `command` option, user can execute vim Ex-commands after an action. For example it can be used to adjust indent automatically.
```vim
let g:sandwich#recipes += [
\ {
\ 'buns' : ['{', '}'],
\ 'motionwise' : ['line'],
\ 'kind' : ['add'],
\ 'linewise' : 1,
\ 'command' : ["'[+1,']-1normal! >>"],
\ },
\ {
\ 'buns' : ['{', '}'],
\ 'motionwise' : ['line'],
\ 'kind' : ['delete'],
\ 'linewise' : 1,
\ 'command' : ["'[,']normal! <<"],
\ }
\ ]
" press Vsa{
foo ---> {
foo
}
" press V2jsd
{ ---> foo
foo
}
```
Operator-sandwich also can work blockwise with the blockwise-visual selection and blockwise motions.
```vim
" press <C-v>2j2lsa(
foo (foo)
bar ---> (bar)
baz (baz)
```
There is an option to skip white space `skip_space`, it is valid in default. Empty line is ignored.
```vim
" press <C-v>3j$sa(
fooooooo (fooooooo)
baaaar ---> (baaaar)
baaaz (baaaz)
```
### Expression surroundings and regular expression matching
The option `expr` enables to evaluate surroundings (`buns`) before adding/deleting/replacing surroundings. The following recipe is an simple example to wrap texts by html style tags.
```vim
let g:sandwich#recipes += [
\ {
\ 'buns' : ['TagInput(1)', 'TagInput(0)'],
\ 'expr' : 1,
\ 'filetype': ['html'],
\ 'kind' : ['add', 'replace'],
\ 'action' : ['add'],
\ 'input' : ['t'],
\ },
\ ]
function! TagInput(is_head) abort
if a:is_head
let s:TagLast = input('Tag: ')
if s:TagLast !=# ''
let tag = printf('<%s>', s:TagLast)
else
throw 'OperatorSandwichCancel'
endif
else
let tag = printf('</%s>', matchstr(s:TagLast, '^\a[^[:blank:]>/]*'))
endif
return tag
endfunction
```
The option `regex` is to regard surroundings (`buns`) as regular expressions to match and delete/replace. The following recipe is an simple example to delete both ends of html tag.
```vim
let g:sandwich#recipes += [
\ {
\ 'buns' : ['<\a[^[:blank:]>/]*.\{-}>',
\ '</\a[^[:blank:]>/]*>'],
\ 'regex' : 1,
\ 'filetype': ['html'],
\ 'nesting' : 1,
\ 'input' : ['t'],
\ },
\ ]
```
However the above example is not so accurate. Instead of the example, there are excellent built-in textobjects `it` and `at`, these external textobjects also can be utilized through `external`.
```vim
let g:sandwich#recipes += [
\ {
\ 'external': ['it', 'at'],
\ 'noremap' : 1,
\ 'filetype': ['html'],
\ 'input' : ['t'],
\ },
\ ]
```
### Demo
![sandwich.vim](http://art61.photozou.jp/pub/986/3080986/photo/225500462_org.v1437577755.gif)
# Pioneers
* [vim-surround](https://github.com/tpope/vim-surround)
* [vim-operator-surround](https://github.com/rhysd/vim-operator-surround)
* [vim-textobj-multiblock](https://github.com/osyo-manga/vim-textobj-multiblock)
* [vim-textobj-anyblock](https://github.com/rhysd/vim-textobj-anyblock)
* [vim-textobj-between](https://github.com/thinca/vim-textobj-between)

View File

@ -0,0 +1,18 @@
if &compatible || exists('b:did_sandwich_initex_ftplugin') || get(g:, 'sandwich_no_initex_ftplugin', 0)
finish
endif
runtime macros/sandwich/ftplugin/tex.vim
augroup sandwich-event-FileType
autocmd!
execute 'autocmd FileType initex source ' . fnameescape(expand('<sfile>'))
augroup END
let b:did_sandwich_initex_ftplugin = 1
if !exists('b:undo_ftplugin')
let b:undo_ftplugin = ''
else
let b:undo_ftplugin .= ' | '
endif
let b:undo_ftplugin .= 'unlet b:did_sandwich_initex_ftplugin | call sandwich#util#ftrevert("tex")'

View File

@ -0,0 +1,21 @@
if &compatible || exists('b:did_sandwich_julia_ftplugin') || get(g:, 'sandwich_no_julia_ftplugin', 0)
finish
endif
" To include multibyte characters
let b:sandwich_magicchar_f_patterns = [
\ {
\ 'header' : '\%#=2\<[[:upper:][:lower:]_]\k*!\?',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
let b:did_sandwich_julia_ftplugin = 1
if !exists('b:undo_ftplugin')
let b:undo_ftplugin = ''
else
let b:undo_ftplugin .= ' | '
endif
let b:undo_ftplugin .= 'unlet! b:did_sandwich_julia_ftplugin b:sandwich_magicchar_f_patterns'

View File

@ -0,0 +1,18 @@
if &compatible || exists('b:did_sandwich_plaintex_ftplugin') || get(g:, 'sandwich_no_plaintex_ftplugin', 0)
finish
endif
runtime macros/sandwich/ftplugin/tex.vim
augroup sandwich-event-FileType
autocmd!
execute 'autocmd FileType plaintex source ' . fnameescape(expand('<sfile>'))
augroup END
let b:did_sandwich_plaintex_ftplugin = 1
if !exists('b:undo_ftplugin')
let b:undo_ftplugin = ''
else
let b:undo_ftplugin .= ' | '
endif
let b:undo_ftplugin .= 'unlet b:did_sandwich_plaintex_ftplugin | call sandwich#util#ftrevert("tex")'

View File

@ -0,0 +1,18 @@
if &compatible || exists('b:did_sandwich_tex_ftplugin') || get(g:, 'sandwich_no_tex_ftplugin', 0)
finish
endif
runtime macros/sandwich/ftplugin/tex.vim
augroup sandwich-event-FileType
autocmd!
execute 'autocmd FileType tex source ' . fnameescape(expand('<sfile>'))
augroup END
let b:did_sandwich_tex_ftplugin = 1
if !exists('b:undo_ftplugin')
let b:undo_ftplugin = ''
else
let b:undo_ftplugin .= ' | '
endif
let b:undo_ftplugin .= 'unlet b:did_sandwich_tex_ftplugin | call sandwich#util#ftrevert("tex")'

View File

@ -0,0 +1,20 @@
if &compatible || exists('b:did_sandwich_vim_ftplugin') || get(g:, 'sandwich_no_vim_ftplugin', 0)
finish
endif
let b:sandwich_magicchar_f_patterns = [
\ {
\ 'header' : '\C\<\%(\h\|[sa]:\h\|g:[A-Z]\)\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
let b:did_sandwich_vim_ftplugin = 1
if !exists('b:undo_ftplugin')
let b:undo_ftplugin = ''
else
let b:undo_ftplugin .= ' | '
endif
let b:undo_ftplugin .= 'unlet! b:did_sandwich_vim_ftplugin b:sandwich_magicchar_f_patterns'

View File

@ -0,0 +1,607 @@
" operator-sandwich: wrap by buns!
" TODO Give API to get information from operator object.
" It would be helpful for users in use of 'expr_filter' and 'command' option.
" TODO Add 'at' option
" variables "{{{
let s:constants = function('sandwich#constants#get')
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_392 = has('patch-7.4.392')
else
let s:has_patch_7_4_392 = v:version == 704 && has('patch392')
endif
" Others
" NOTE: This would be updated in each operator functions (operator#sandwich#{add/delete/replce})
let s:is_in_cmdline_window = 0
" Current set operator
let s:operator = ''
"}}}
" highlight {{{
function! s:default_highlight() abort
highlight default link OperatorSandwichBuns IncSearch
highlight default link OperatorSandwichAdd DiffAdd
highlight default link OperatorSandwichDelete DiffDelete
if hlexists('OperatorSandwichStuff')
highlight default link OperatorSandwichChange OperatorSandwichStuff
else
" obsolete
highlight default link OperatorSandwichChange DiffChange
endif
endfunction
call s:default_highlight()
augroup sandwich-event-ColorScheme
autocmd!
autocmd ColorScheme * call s:default_highlight()
augroup END
"}}}
""" Public functions
" Prerequisite
function! operator#sandwich#prerequisite(kind, mode, ...) abort "{{{
" make new operator object
let g:operator#sandwich#object = operator#sandwich#operator#new()
" prerequisite
let operator = g:operator#sandwich#object
let operator.state = 1
let operator.kind = a:kind
let operator.count = a:mode ==# 'x' ? max([1, v:prevcount]) : v:count1
let operator.mode = a:mode
let operator.view = winsaveview()
let operator.cursor.keepable = 1
let operator.cursor.keep[0:3] = getpos('.')[0:3]
let operator.opt = sandwich#opt#new(a:kind, {}, get(a:000, 0, {}))
let operator.recipes.arg = get(a:000, 1, [])
let operator.recipes.arg_given = a:0 > 1
let [operator.extended, operator.blockwidth] = s:blockwisevisual_info(a:mode)
let &l:operatorfunc = 'operator#sandwich#' . a:kind
let s:operator = a:kind
return
endfunction
"}}}
function! operator#sandwich#keymap(kind, mode, ...) abort "{{{
if a:0 == 0
call operator#sandwich#prerequisite(a:kind, a:mode)
elseif a:0 == 1
call operator#sandwich#prerequisite(a:kind, a:mode, a:1)
else
call operator#sandwich#prerequisite(a:kind, a:mode, a:1, a:2)
endif
let cmd = a:mode ==# 'x' ? 'gvg@' : 'g@'
call feedkeys(cmd, 'inx')
return
endfunction
"}}}
function! s:blockwisevisual_info(mode) abort "{{{
if a:mode ==# 'x' && visualmode() ==# "\<C-v>"
" The case for blockwise selections in visual mode
" NOTE: 'extended' could be recorded safely only at here. Do not move.
let registers = s:saveregisters()
let lazyredraw = &lazyredraw
set lazyredraw
let view = winsaveview()
try
normal! gv
let extended = winsaveview().curswant == s:constants('colmax')
silent noautocmd normal! ""y
let regtype = getregtype('"')
finally
call winrestview(view)
let &lazyredraw = lazyredraw
call s:restoreregisters(registers)
endtry
let blockwidth = str2nr(regtype[1:])
else
let extended = 0
let blockwidth = 0
endif
return [extended, blockwidth]
endfunction
"}}}
function! s:saveregisters() abort "{{{
let registers = {}
let registers['0'] = s:getregister('0')
let registers['"'] = s:getregister('"')
return registers
endfunction
"}}}
function! s:restoreregisters(registers) abort "{{{
for [register, contains] in items(a:registers)
call s:setregister(register, contains)
endfor
endfunction
"}}}
function! s:getregister(register) abort "{{{
return [getreg(a:register), getregtype(a:register)]
endfunction
"}}}
function! s:setregister(register, contains) abort "{{{
let [value, options] = a:contains
return setreg(a:register, value, options)
endfunction
"}}}
" Operator functions
function! operator#sandwich#add(motionwise, ...) abort "{{{
call s:do('add', a:motionwise, 'OperatorSandwichAddPre', 'OperatorSandwichAddPost')
endfunction
"}}}
function! operator#sandwich#delete(motionwise, ...) abort "{{{
call s:do('delete', a:motionwise, 'OperatorSandwichDeletePre', 'OperatorSandwichDeletePost')
endfunction
"}}}
function! operator#sandwich#replace(motionwise, ...) abort "{{{
call s:do('replace', a:motionwise, 'OperatorSandwichReplacePre', 'OperatorSandwichReplacePost')
endfunction
"}}}
function! s:do(kind, motionwise, AutocmdPre, AutocmdPost) abort "{{{
let s:operator = ''
if exists('g:operator#sandwich#object')
let operator = g:operator#sandwich#object
let messenger = sandwich#messenger#new()
let defaultopt = s:default_options(a:kind, a:motionwise)
call operator.opt.update('default', defaultopt)
call s:update_is_in_cmdline_window()
call s:doautocmd(a:AutocmdPre)
call operator.execute(a:motionwise)
call s:doautocmd(a:AutocmdPost)
call messenger.notify('operator-sandwich: ')
endif
endfunction
"}}}
" function! s:update_is_in_cmdline_window() abort "{{{
if s:has_patch_7_4_392
function! s:update_is_in_cmdline_window() abort
let s:is_in_cmdline_window = getcmdwintype() !=# ''
endfunction
else
function! s:update_is_in_cmdline_window() abort
let s:is_in_cmdline_window = 0
try
execute 'tabnext ' . tabpagenr()
catch /^Vim\%((\a\+)\)\=:E11/
let s:is_in_cmdline_window = 1
catch
endtry
endfunction
endif
"}}}
function! s:doautocmd(name) abort "{{{
let view = s:saveview()
try
if exists('#User#' . a:name)
execute 'doautocmd <nomodeline> User ' . a:name
endif
catch
let errormsg = printf('operator-sandwich: An error occurred in autocmd %s. [%s]', a:name, v:exception)
echoerr errormsg
finally
call s:restview(view, a:name)
endtry
endfunction
"}}}
function! s:saveview() abort "{{{
return [tabpagenr(), winnr(), winsaveview(), getpos("'["), getpos("']")]
endfunction
"}}}
function! s:restview(view, name) abort "{{{
let [tabpagenr, winnr, view, modhead, modtail] = a:view
if s:is_in_cmdline_window
" in cmdline-window
return
endif
" tabpage
try
execute 'tabnext ' . tabpagenr
if tabpagenr() != tabpagenr
throw 'OperatorSandwichError:CouldNotRestoreTabpage'
endif
catch /^OperatorSandwichError:CouldNotRestoreTabpage/
let errormsg = printf('operator-sandwich: Could not have restored tabpage after autocmd %s.', a:name)
echoerr errormsg
endtry
" window
try
execute winnr . 'wincmd w'
catch /^Vim\%((\a\+)\)\=:E16/
let errormsg = printf('operator-sandwich: Could not have restored window after autocmd %s.', a:name)
echoerr errormsg
endtry
" view
call winrestview(view)
" marks
call setpos("'[", modhead)
call setpos("']", modtail)
endfunction
"}}}
" For the query1st series mappings
function! operator#sandwich#query1st(kind, mode, ...) abort "{{{
if a:kind !=# 'add' && a:kind !=# 'replace'
return
endif
" prerequisite
let arg_opt = get(a:000, 0, {})
let arg_recipes = get(a:000, 1, [])
call operator#sandwich#prerequisite(a:kind, a:mode, arg_opt, arg_recipes)
let operator = g:operator#sandwich#object
" NOTE: force to set highlight=0 and query_once=1
call operator.opt.update('default', {'highlight': 0, 'query_once': 1, 'expr': 0, 'listexpr': 0})
let operator.recipes.arg_given = a:0 > 1
let stuff = operator#sandwich#stuff#new()
call stuff.initialize(operator.count, operator.cursor, operator.modmark)
let operator.basket = [stuff]
" pick 'recipe' up and query prefered buns
call operator.recipes.integrate(a:kind, 'all', a:mode)
for i in range(operator.count)
let opt = operator.opt
let recipe = operator.query()
let operator.recipes.dog_ear += [recipe]
if !has_key(recipe, 'buns') || recipe.buns == []
break
endif
call opt.update('recipe_add', recipe)
if i == 0 && operator.count > 1 && opt.of('query_once')
call operator.recipes.fill(recipe, operator.count)
break
endif
endfor
if filter(copy(operator.recipes.dog_ear), 'has_key(v:val, "buns")') != []
let operator.state = 0
let cmd = a:mode ==# 'x'
\ ? "\<Plug>(operator-sandwich-gv)\<Plug>(operator-sandwich-g@)"
\ : "\<Plug>(operator-sandwich-g@)"
call feedkeys(cmd, 'im')
else
unlet g:operator#sandwich#object
endif
return
endfunction
"}}}
" Supplementary keymappings
function! operator#sandwich#synchro_count() abort "{{{
if exists('g:operator#sandwich#object')
return g:operator#sandwich#object.count
else
return ''
endif
endfunction
"}}}
function! operator#sandwich#release_count() abort "{{{
if exists('g:operator#sandwich#object')
let l:count = g:operator#sandwich#object.count
let g:operator#sandwich#object.count = 1
return l:count
else
return ''
endif
endfunction
"}}}
function! operator#sandwich#squash_count() abort "{{{
if exists('g:operator#sandwich#object')
let g:operator#sandwich#object.count = 1
endif
return ''
endfunction
"}}}
function! operator#sandwich#predot() abort "{{{
if exists('g:operator#sandwich#object')
let operator = g:operator#sandwich#object
let operator.cursor.keepable = 1
let operator.cursor.keep[0:3] = getpos('.')[0:3]
endif
return ''
endfunction
"}}}
function! operator#sandwich#dot() abort "{{{
call operator#sandwich#predot()
return '.'
endfunction
"}}}
" visualrepeat.vim (vimscript #3848) support
function! operator#sandwich#visualrepeat(kind) abort "{{{
let operator = g:operator#sandwich#object
let original_mode = operator.mode
let original_extended = operator.extended
let original_blockwidth = operator.blockwidth
let operator.mode = 'x'
let [operator.extended, operator.blockwidth] = s:blockwisevisual_info('x')
try
normal! gv
let operator.cursor.keepable = 1
let operator.cursor.keep[0:3] = getpos('.')[0:3]
let l:count = v:count ? v:count : ''
let &l:operatorfunc = 'operator#sandwich#' . a:kind
let cmd = printf("normal! %sg@", l:count)
execute cmd
finally
let operator.mode = original_mode
let operator.extended = original_extended
let operator.blockwidth = original_blockwidth
endtry
endfunction
"}}}
" API
function! operator#sandwich#show(...) abort "{{{
if !exists('g:operator#sandwich#object') || !g:operator#sandwich#object.at_work
echoerr 'operator-sandwich: Not in an operator-sandwich operation!'
return 1
endif
let operator = g:operator#sandwich#object
let kind = operator.kind
let opt = operator.opt
let place = get(a:000, 0, '')
if kind ==# 'add'
if place ==# ''
let place = 'stuff'
endif
if place ==# 'added'
let hi_group = s:get_ifnotempty(a:000, 1, 'OperatorSandwichAdd')
else
let hi_group = opt.of('highlight') >= 2
\ ? s:get_ifnotempty(a:000, 1, 'OperatorSandwichChange')
\ : s:get_ifnotempty(a:000, 1, 'OperatorSandwichBuns')
endif
elseif kind ==# 'delete'
if place ==# ''
let place = 'target'
endif
let hi_group = opt.of('highlight') >= 2
\ ? s:get_ifnotempty(a:000, 1, 'OperatorSandwichDelete')
\ : s:get_ifnotempty(a:000, 1, 'OperatorSandwichBuns')
elseif kind ==# 'replace'
if place ==# ''
let place = 'target'
endif
if place ==# 'added'
let hi_group = s:get_ifnotempty(a:000, 1, 'OperatorSandwichAdd')
elseif place ==# 'target'
let hi_group = opt.of('highlight') >= 2
\ ? s:get_ifnotempty(a:000, 1, 'OperatorSandwichDelete')
\ : s:get_ifnotempty(a:000, 1, 'OperatorSandwichBuns')
else
let hi_group = opt.of('highlight') >= 2
\ ? s:get_ifnotempty(a:000, 1, 'OperatorSandwichChange')
\ : s:get_ifnotempty(a:000, 1, 'OperatorSandwichBuns')
endif
else
return 1
endif
let forcibly = get(a:000, 2, 0)
return operator.show(place, hi_group, forcibly)
endfunction
"}}}
function! operator#sandwich#quench(...) abort "{{{
if exists('g:operator#sandwich#object')
let operator = g:operator#sandwich#object
let kind = operator.kind
let place = get(a:000, 0, '')
if place ==# ''
if kind ==# 'add'
let place = 'stuff'
elseif kind ==# 'delete'
let place = 'target'
elseif kind ==# 'replace'
let place = 'target'
else
return 1
endif
endif
return operator.quench(place)
endif
endfunction
"}}}
function! operator#sandwich#get_info(...) abort "{{{
if !exists('g:operator#sandwich#object') || !g:operator#sandwich#object.at_work
echoerr 'operator-sandwich: Not in an operator-sandwich operation!'
return 1
endif
let info = get(a:000, 0, '')
if a:0 == 0 || info ==# ''
return g:operator#sandwich#object
elseif info ==# 'state' || info ==# 'kind' || info ==# 'count' || info ==# 'mode' || info ==# 'motionwise'
return g:operator#sandwich#object[info]
else
echoerr printf('operator-sandwich: Identifier "%s" is not supported.', string(info))
return 1
endif
endfunction
"}}}
function! operator#sandwich#kind() abort "{{{
return exists('g:operator#sandwich#object') && g:operator#sandwich#object.at_work
\ ? g:operator#sandwich#object.kind
\ : s:operator
endfunction
"}}}
function! s:get_ifnotempty(list, idx, default) abort "{{{
let item = get(a:list, a:idx, '')
if item ==# ''
let item = a:default
endif
return item
endfunction
"}}}
" For internal communication
function! operator#sandwich#is_in_cmd_window() abort "{{{
return s:is_in_cmdline_window
endfunction
"}}}
function! operator#sandwich#synchronize(kind, recipe) abort "{{{
if exists('g:operator#sandwich#object') && !empty(a:recipe)
let g:operator#sandwich#object.recipes.synchro.on = 1
let g:operator#sandwich#object.recipes.synchro.kind = a:kind
let g:operator#sandwich#object.recipes.synchro.recipe = [a:recipe]
endif
endfunction
"}}}
" recipes "{{{
function! operator#sandwich#get_recipes() abort "{{{
if exists('b:operator_sandwich_recipes')
let recipes = b:operator_sandwich_recipes
elseif exists('g:operator#sandwich#recipes')
let recipes = g:operator#sandwich#recipes
else
let recipes = g:operator#sandwich#default_recipes
endif
return deepcopy(recipes)
endfunction
"}}}
if exists('g:operator#sandwich#default_recipes')
unlockvar! g:operator#sandwich#default_recipes
endif
let g:operator#sandwich#default_recipes = []
lockvar! g:operator#sandwich#default_recipes
"}}}
" options "{{{
function! s:default_options(kind, motionwise) abort "{{{
return get(b:, 'operator_sandwich_options', g:operator#sandwich#options)[a:kind][a:motionwise]
endfunction
"}}}
function! s:initialize_options(...) abort "{{{
let manner = a:0 ? a:1 : 'keep'
let g:operator#sandwich#options = get(g:, 'operator#sandwich#options', {})
for kind in ['add', 'delete', 'replace']
if !has_key(g:operator#sandwich#options, kind)
let g:operator#sandwich#options[kind] = {}
endif
for motionwise in ['char', 'line', 'block']
if !has_key(g:operator#sandwich#options[kind], motionwise)
let g:operator#sandwich#options[kind][motionwise] = {}
endif
call extend(g:operator#sandwich#options[kind][motionwise],
\ sandwich#opt#defaults(kind, motionwise), manner)
endfor
endfor
endfunction
call s:initialize_options()
"}}}
function! operator#sandwich#set_default() abort "{{{
call s:initialize_options('force')
endfunction
"}}}
function! operator#sandwich#set(kind, motionwise, option, value) abort "{{{
if s:argument_error(a:kind, a:motionwise, a:option, a:value)
return
endif
if a:kind ==# 'all'
let kinds = ['add', 'delete', 'replace']
else
let kinds = [a:kind]
endif
if a:motionwise ==# 'all'
let motionwises = ['char', 'line', 'block']
else
let motionwises = [a:motionwise]
endif
call s:set_option_value(g:operator#sandwich#options, kinds, motionwises, a:option, a:value)
endfunction
"}}}
function! operator#sandwich#setlocal(kind, motionwise, option, value) abort "{{{
if s:argument_error(a:kind, a:motionwise, a:option, a:value)
return
endif
if !exists('b:operator_sandwich_options')
let b:operator_sandwich_options = deepcopy(g:operator#sandwich#options)
endif
if a:kind ==# 'all'
let kinds = ['add', 'delete', 'replace']
else
let kinds = [a:kind]
endif
if a:motionwise ==# 'all'
let motionwises = ['char', 'line', 'block']
else
let motionwises = [a:motionwise]
endif
call s:set_option_value(b:operator_sandwich_options, kinds, motionwises, a:option, a:value)
endfunction
"}}}
function! s:argument_error(kind, motionwise, option, value) abort "{{{
if !(a:kind ==# 'add' || a:kind ==# 'delete' || a:kind ==# 'replace' || a:kind ==# 'all')
echohl WarningMsg
echomsg 'Invalid kind "' . a:kind . '".'
echohl NONE
return 1
endif
if !(a:motionwise ==# 'char' || a:motionwise ==# 'line' || a:motionwise ==# 'block' || a:motionwise ==# 'all')
echohl WarningMsg
echomsg 'Invalid motion-wise "' . a:motionwise . '".'
echohl NONE
return 1
endif
if a:kind !=# 'all' && a:motionwise !=# 'all'
let defaults = sandwich#opt#defaults(a:kind, a:motionwise)
if filter(keys(defaults), 'v:val ==# a:option') == []
echohl WarningMsg
echomsg 'Invalid option name "' . a:option . '".'
echohl NONE
return 1
endif
if a:option !~# 'indentkeys[-+]\?' && type(a:value) != type(defaults[a:option])
echohl WarningMsg
echomsg 'Invalid type of value. ' . string(a:value)
echohl NONE
return 1
endif
endif
return 0
endfunction
"}}}
function! s:set_option_value(dest, kinds, motionwises, option, value) abort "{{{
for kind in a:kinds
for motionwise in a:motionwises
let defaults = sandwich#opt#defaults(kind, motionwise)
if filter(keys(defaults), 'v:val ==# a:option') != []
if a:option =~# 'indentkeys[-+]\?' || type(a:value) == type(defaults[a:option])
let a:dest[kind][motionwise][a:option] = a:value
endif
endif
endfor
endfor
endfunction
"}}}
"}}}
unlet! g:operator#sandwich#object
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,836 @@
" act object - editing buffer
let s:lib = operator#sandwich#lib#import()
" variables "{{{
let s:constants = function('sandwich#constants#get')
let s:TRUE = 1
let s:FALSE = 0
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>(i) i
nnoremap <SID>(o) o
nnoremap <SID>(O) O
let s:KEY_i = printf('%s(i)', s:SNR)
let s:KEY_o = printf('%s(o)', s:SNR)
let s:KEY_O = printf('%s(O)', s:SNR)
" null valiables
let s:null_pos = [0, 0, 0, 0]
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_str = type('')
" features
let s:has_gui_running = has('gui_running')
"}}}
function! operator#sandwich#act#new() abort "{{{
return deepcopy(s:act)
endfunction
"}}}
" s:act "{{{
let s:act = {
\ 'cursor' : {},
\ 'modmark': {},
\ 'opt' : {},
\ 'success': 0,
\ 'added' : [],
\ }
"}}}
function! s:act.initialize(cursor, modmark, added) dict abort "{{{
let self.cursor = a:cursor
let self.modmark = a:modmark
let self.opt = {}
let self.added = a:added
let self.success = 0
endfunction
"}}}
function! s:act.add_pair(buns, stuff, undojoin) dict abort "{{{
let target = a:stuff.target
let edges = a:stuff.edges
let modmark = self.modmark
let opt = self.opt
let indent = [0, 0]
let is_linewise = [0, 0]
if s:lib.is_valid_4pos(target) && s:lib.is_equal_or_ahead(target.head2, target.head1)
if target.head2[2] != col([target.head2[1], '$'])
let target.head2[0:3] = s:lib.get_right_pos(target.head2)
endif
let indentopt = s:set_indent(opt)
let messenger = sandwich#messenger#get()
try
let pos = target.head1
let [is_linewise[0], indent[0], head1, tail1] = s:add_former(a:buns, pos, opt, a:undojoin)
let pos = s:push1(copy(target.head2), target, a:buns, indent, is_linewise)
let [is_linewise[1], indent[1], head2, tail2] = s:add_latter(a:buns, pos, opt)
catch /^Vim\%((\a\+)\)\=:E21/
call messenger.notice.queue(['Cannot make changes to read-only buffer.', 'WarningMsg'])
throw 'OperatorSandwichError:ReadOnly'
finally
call s:restore_indent(indentopt)
endtry
let [mod_head, mod_tail] = s:execute_command(head1, tail2, opt.of('command'))
if opt.of('highlight', '') >= 3
call map(self.added, 's:shift_added("s:shift_for_add", v:val, target, a:buns, indent, is_linewise)')
call add(self.added, {
\ 'head1': head1,
\ 'tail1': s:added_tail(head1, tail1, is_linewise[0]),
\ 'head2': head2,
\ 'tail2': s:added_tail(head2, tail2, is_linewise[1]),
\ 'linewise': is_linewise,
\ })
endif
" update modmark
if modmark.head == s:null_pos || s:lib.is_ahead(modmark.head, mod_head)
let modmark.head = mod_head
endif
if modmark.tail == s:null_pos
let modmark.tail = mod_tail
else
call s:shift_for_add(modmark.tail, target, a:buns, indent, is_linewise)
if s:lib.is_ahead(mod_tail, modmark.tail)
let modmark.tail = mod_tail
endif
endif
" update cursor positions
call s:shift_for_add(self.cursor.inner_head, target, a:buns, indent, is_linewise)
call s:shift_for_add(self.cursor.keep, target, a:buns, indent, is_linewise)
call s:shift_for_add(self.cursor.inner_tail, target, a:buns, indent, is_linewise)
" update next target positions
let edges.head = copy(head1)
let edges.tail = s:lib.get_left_pos(tail2)
let self.success = 1
endif
return self.success
endfunction
"}}}
function! s:act.delete_pair(stuff, modified) dict abort "{{{
let target = a:stuff.target
let edges = a:stuff.edges
let modmark = self.modmark
let opt = self.opt
if s:lib.is_valid_4pos(target) && s:lib.is_ahead(target.head2, target.tail1)
let reg = ['"', getreg('"'), getregtype('"')]
let deletion = ['', '']
let is_linewise = [0, 0]
let messenger = sandwich#messenger#get()
try
let former_head = target.head1
let former_tail = target.tail1
let latter_head = target.head2
let [deletion[0], is_linewise[0], head] = s:delete_former(former_head, former_tail, latter_head, opt)
let latter_head = s:pull1(copy(target.head2), target, deletion, is_linewise)
let latter_tail = s:pull1(copy(target.tail2), target, deletion, is_linewise)
let [deletion[1], is_linewise[1], tail] = s:delete_latter(latter_head, latter_tail, former_head, opt)
catch /^Vim\%((\a\+)\)\=:E21/
call messenger.notice.queue(['Cannot make changes to read-only buffer.', 'WarningMsg'])
throw 'OperatorSandwichError:ReadOnly'
finally
call call('setreg', reg)
endtry
let [mod_head, mod_tail] = s:execute_command(head, tail, opt.of('command'))
" update modmark
if modmark.head == s:null_pos || s:lib.is_ahead(modmark.head, mod_head)
let modmark.head = mod_head
endif
" NOTE: Probably, there is no possibility to delete breakings along multiple acts.
if !a:modified
if modmark.tail == s:null_pos
let modmark.tail = mod_tail
else
call s:shift_for_delete(modmark.tail, target, deletion, is_linewise)
if mod_tail[1] >= modmark.tail[1]
let modmark.tail = mod_tail
endif
endif
endif
" update cursor positions
call s:shift_for_delete(self.cursor.inner_head, target, deletion, is_linewise)
call s:shift_for_delete(self.cursor.keep, target, deletion, is_linewise)
call s:shift_for_delete(self.cursor.inner_tail, target, deletion, is_linewise)
" update target positions
let edges.head = copy(head)
let edges.tail = s:lib.get_left_pos(tail)
let self.success = 1
endif
return self.success
endfunction
"}}}
function! s:act.replace_pair(buns, stuff, undojoin, modified) dict abort "{{{
let target = a:stuff.target
let edges = a:stuff.edges
let modmark = self.modmark
let opt = self.opt
if s:lib.is_valid_4pos(target) && s:lib.is_ahead(target.head2, target.tail1)
set virtualedit=
let next_head = s:lib.get_right_pos(target.tail1)
let next_tail = s:lib.get_left_pos(target.head2)
set virtualedit=onemore
let reg = ['"', getreg('"'), getregtype('"')]
let deletion = ['', '']
let indent = [0, 0]
let is_linewise = [0, 0]
let indentopt = s:set_indent(opt)
let messenger = sandwich#messenger#get()
try
let within_a_line = target.tail1[1] == target.head2[1]
let former_head = target.head1
let former_tail = target.tail1
let latter_head = copy(target.head2)
let latter_tail = copy(target.tail2)
let [deletion[0], is_linewise[0], indent[0], head1, tail1] = s:replace_former(a:buns, former_head, former_tail, within_a_line, opt, a:undojoin)
call s:pull1(latter_head, target, deletion, is_linewise)
call s:push1(latter_head, target, a:buns, indent, is_linewise)
call s:pull1(latter_tail, target, deletion, is_linewise)
call s:push1(latter_tail, target, a:buns, indent, is_linewise)
let [deletion[1], is_linewise[1], indent[1], head2, tail2] = s:replace_latter(a:buns, latter_head, latter_tail, within_a_line, opt)
catch /^Vim\%((\a\+)\)\=:E21/
call messenger.notice.queue(['Cannot make changes to read-only buffer.', 'WarningMsg'])
throw 'OperatorSandwichError:ReadOnly'
finally
call call('setreg', reg)
call s:restore_indent(indentopt)
endtry
let [mod_head, mod_tail] = s:execute_command(head1, tail2, opt.of('command'))
if opt.of('highlight', '') >= 3
call map(self.added, 's:shift_added("s:shift_for_replace", v:val, target, a:buns, deletion, indent, is_linewise)')
call add(self.added, {
\ 'head1': head1,
\ 'tail1': s:lib.get_left_pos(tail1),
\ 'head2': head2,
\ 'tail2': s:lib.get_left_pos(tail2),
\ 'linewise': is_linewise
\ })
endif
" update modmark
if modmark.head == s:null_pos || s:lib.is_ahead(modmark.head, mod_head)
let modmark.head = copy(mod_head)
endif
if !a:modified
if modmark.tail == s:null_pos
let modmark.tail = copy(mod_tail)
else
call s:shift_for_replace(modmark.tail, target, a:buns, deletion, indent, is_linewise)
if modmark.tail[1] < mod_tail[1]
let modmark.tail = copy(mod_tail)
endif
endif
endif
" update cursor positions
call s:shift_for_replace(self.cursor.keep, target, a:buns, deletion, indent, is_linewise)
call s:shift_for_replace(next_head, target, a:buns, deletion, indent, is_linewise)
call s:shift_for_replace(next_tail, target, a:buns, deletion, indent, is_linewise)
if self.cursor.inner_head == s:null_pos || target.head1[1] <= self.cursor.inner_head[1]
let self.cursor.inner_head = copy(next_head)
endif
if self.cursor.inner_tail == s:null_pos
let self.cursor.inner_tail = copy(next_tail)
else
call s:shift_for_replace(self.cursor.inner_tail, target, a:buns, deletion, indent, is_linewise)
if self.cursor.inner_tail[1] <= next_tail[1]
let self.cursor.inner_tail = copy(next_tail)
endif
endif
" update target positions
let edges.head = next_head
let edges.tail = next_tail
let self.success = 1
endif
return self.success
endfunction
"}}}
" supplemental classes
" Indent class {{{
let s:Indent = {
\ 'len': 0,
\ 'str': '',
\ 'savedstr': '',
\ 'linehead': s:FALSE
\ }
function! s:Indent(pos, opt) abort "{{{
let line = getline(a:pos[1])
let indent = deepcopy(s:Indent)
if a:pos[2] == 1
let indent.linehead = s:TRUE
return indent
endif
let indent.str = matchstr(line, '^\s*')
let indent.len = strlen(indent.str)
if a:pos[2] <= indent.len
" shorten if cursor is in indentation
let indent.str = indent.str[: a:pos[2] - 2]
let indent.len = strlen(indent.str)
let indent.linehead = s:TRUE
endif
if a:opt.of('linewise') && a:opt.of('autoindent') == 4
let indent.savedstr = indent.str
endif
return indent
endfunction "}}}
function! s:Indent.diff(addition) abort "{{{
let addition = split(a:addition, '\m\%(\n\|\r\|\r\n\)', 1)
if len(addition) == 1
let indentstr = matchstr(getline("'["), '\m^\s*')
let indentlen = strlen(indentstr)
if self.linehead
let indentlen -= strlen(matchstr(addition[0], '\m^\s*'))
endif
let diff = indentlen - self.len
else
let indentstr = matchstr(getline("']"), '\m^\s*')
let indentlen = strlen(indentstr)
let diff = indentlen - strlen(matchstr(addition[-1], '\m^\s*'))
endif
return diff
endfunction "}}}
"}}}
" private functions
function! s:set_indent(opt) abort "{{{
let indentopt = {
\ 'autoindent': {
\ 'restore': 0,
\ 'value' : [&l:autoindent, &l:smartindent, &l:cindent, &l:indentexpr],
\ },
\ 'indentkeys': {
\ 'restore': 0,
\ 'name' : '',
\ 'value' : '',
\ },
\ }
" set autoindent options
if a:opt.of('autoindent') == 0 || a:opt.of('autoindent') == 4
let [&l:autoindent, &l:smartindent, &l:cindent, &l:indentexpr] = [0, 0, 0, '']
let indentopt.autoindent.restore = 1
elseif a:opt.of('autoindent') == 1
let [&l:autoindent, &l:smartindent, &l:cindent, &l:indentexpr] = [1, 0, 0, '']
let indentopt.autoindent.restore = 1
elseif a:opt.of('autoindent') == 2
" NOTE: 'Smartindent' requires 'autoindent'. :help 'smartindent'
let [&l:autoindent, &l:smartindent, &l:cindent, &l:indentexpr] = [1, 1, 0, '']
let indentopt.autoindent.restore = 1
elseif a:opt.of('autoindent') == 3
let [&l:cindent, &l:indentexpr] = [1, '']
let indentopt.autoindent.restore = 1
endif
" set indentkeys
if &l:indentexpr !=# ''
let indentopt.indentkeys.name = 'indentkeys'
let indentopt.indentkeys.value = &l:indentkeys
else
let indentopt.indentkeys.name = 'cinkeys'
let indentopt.indentkeys.value = &l:cinkeys
endif
let val = a:opt.of('indentkeys')
if type(val) == s:type_str
execute printf('setlocal %s=%s', indentopt.indentkeys.name, val)
let indentopt.indentkeys.restore = 1
endif
let val = a:opt.of('indentkeys+')
if type(val) == s:type_str && val !=# ''
execute printf('setlocal %s+=%s', indentopt.indentkeys.name, val)
let indentopt.indentkeys.restore = 1
endif
let val = a:opt.of('indentkeys-')
if type(val) == s:type_str && val !=# ''
" It looks there is no way to add ',' itself to 'indentkeys'
for item in split(val, ',')
execute printf('setlocal %s-=%s', indentopt.indentkeys.name, item)
endfor
let indentopt.indentkeys.restore = 1
endif
return indentopt
endfunction
"}}}
function! s:restore_indent(indentopt) abort "{{{
" restore indentkeys first
if a:indentopt.indentkeys.restore
execute printf('setlocal %s=%s', a:indentopt.indentkeys.name, escape(a:indentopt.indentkeys.value, ' \'))
endif
" restore autoindent options
if a:indentopt.autoindent.restore
let [&l:autoindent, &l:smartindent, &l:cindent, &l:indentexpr] = a:indentopt.autoindent.value
endif
endfunction
"}}}
function! s:add_former(buns, pos, opt, ...) abort "{{{
let undojoin_cmd = get(a:000, 0, 0) ? 'undojoin | ' : ''
let indent = s:Indent(a:pos, a:opt)
let opt_linewise = a:opt.of('linewise')
if opt_linewise
let startinsert = a:opt.of('noremap') ? 'normal! O' : 'normal ' . s:KEY_O
let insertion = indent.savedstr . a:buns[0]
else
let startinsert = a:opt.of('noremap') ? 'normal! i' : 'normal ' . s:KEY_i
let insertion = a:buns[0]
endif
call s:add_portion(insertion, a:pos, undojoin_cmd, startinsert)
return [opt_linewise, indent.diff(a:buns[0]), getpos("'["), getpos("']")]
endfunction
"}}}
function! s:add_latter(buns, pos, opt) abort "{{{
let undojoin_cmd = ''
let indent = s:Indent(a:pos, a:opt)
let opt_linewise = a:opt.of('linewise')
if opt_linewise
let startinsert = a:opt.of('noremap') ? 'normal! o' : 'normal ' . s:KEY_o
let insertion = indent.savedstr . a:buns[1]
else
let startinsert = a:opt.of('noremap') ? 'normal! i' : 'normal ' . s:KEY_i
let insertion = a:buns[1]
endif
call s:add_portion(insertion, a:pos, undojoin_cmd, startinsert)
return [opt_linewise, indent.diff(a:buns[1]), getpos("'["), getpos("']")]
endfunction
"}}}
function! s:add_portion(bun, pos, undojoin_cmd, startinsert) abort "{{{
call setpos('.', a:pos)
if operator#sandwich#is_in_cmd_window()
" workaround for a bug in cmdline-window
call s:paste(a:bun, a:undojoin_cmd)
else
execute a:undojoin_cmd . 'silent noautocmd ' . a:startinsert . a:bun
endif
endfunction
"}}}
function! s:delete_former(head, tail, latter_head, opt, ...) abort "{{{
let is_linewise = 0
let opt_linewise = a:opt.of('linewise')
let undojoin_cmd = get(a:000, 0, 0) ? 'undojoin | ' : ''
let deletion = s:delete_portion(a:head, a:tail, undojoin_cmd)
if opt_linewise == 2 || (opt_linewise == 1 && match(getline('.'), '^\s*$') > -1)
if line('.') != a:latter_head[1]
.delete
let is_linewise = 1
endif
let head = getpos("']")
else
let head = getpos('.')
endif
return [deletion, is_linewise, head]
endfunction
"}}}
function! s:delete_latter(head, tail, former_head, opt) abort "{{{
let is_linewise = 0
let opt_linewise = a:opt.of('linewise')
let undojoin_cmd = ''
let deletion = s:delete_portion(a:head, a:tail, undojoin_cmd)
if opt_linewise == 2 || (opt_linewise == 1 && match(getline('.'), '^\s*$') > -1)
.delete
let is_linewise = 1
let tail = getpos("']")
if tail[1] != 1 && tail[1] != a:former_head[1]
let prevline = line("']") - 1
let tail = [0, prevline, col([prevline, '$']), 0]
endif
else
let tail = getpos("']")
endif
return [deletion, is_linewise, tail]
endfunction
"}}}
function! s:delete_portion(head, tail, undojoin_cmd) abort "{{{
let cmd = "%ssilent noautocmd normal! \"\"dv:call setpos('\.', %s)\<CR>"
call setpos('.', a:head)
let @@ = ''
execute printf(cmd, a:undojoin_cmd, 'a:tail')
return @@
endfunction
"}}}
function! s:replace_former(buns, head, tail, within_a_line, opt, ...) abort "{{{
let is_linewise = 0
let opt_linewise = a:opt.of('linewise')
let undojoin_cmd = get(a:000, 0, 0) ? 'undojoin | ' : ''
let deletion = s:delete_portion(a:head, a:tail, undojoin_cmd)
let indent = s:Indent(a:head, a:opt)
if operator#sandwich#is_in_cmd_window()
" workaround for a bug in cmdline-window
call s:paste(a:buns[0])
else
if opt_linewise == 1 && getline('.') =~# '^\s*$'
.delete
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! O' : 'normal ' . s:KEY_O
execute 'silent noautocmd ' . startinsert . indent.savedstr . a:buns[0]
let is_linewise = 1
elseif opt_linewise == 2
if !a:within_a_line
.delete
endif
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! O' : 'normal ' . s:KEY_O
execute 'silent noautocmd ' . startinsert . indent.savedstr . a:buns[0]
let is_linewise = 1
else
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! i' : 'normal ' . s:KEY_i
execute 'silent noautocmd ' . startinsert . a:buns[0]
endif
endif
return [deletion, is_linewise, indent.diff(a:buns[0]), getpos("'["), getpos("']")]
endfunction
"}}}
function! s:replace_latter(buns, head, tail, within_a_line, opt) abort "{{{
let is_linewise = 0
let opt_linewise = a:opt.of('linewise')
let undojoin_cmd = ''
let deletion = s:delete_portion(a:head, a:tail, undojoin_cmd)
let indent = s:Indent(a:head, a:opt)
if operator#sandwich#is_in_cmd_window()
" workaround for a bug in cmdline-window
call s:paste(a:buns[1])
let head = getpos("'[")
let tail = getpos("']")
else
if opt_linewise == 1 && getline('.') =~# '^\s*$'
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! o' : 'normal ' . s:KEY_o
let current = line('.')
let fileend = line('$')
.delete
if current != fileend
normal! k
endif
execute 'silent noautocmd ' . startinsert . indent.savedstr . a:buns[1]
let head = getpos("'[")
let tail = getpos("']")
let is_linewise = 1
elseif opt_linewise == 2
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! o' : 'normal ' . s:KEY_o
if a:within_a_line
" exceptional behavior
let lnum = line('.')
execute 'silent noautocmd ' . startinsert . indent.savedstr . a:buns[1]
let head = getpos("'[")
let tail = getpos("']")
execute lnum . 'delete'
let head = [0, head[1]-1, head[2], 0]
let tail = [0, tail[1]-1, tail[2], 0]
else
" usual way (same as opt_linewise == 1)
let current = line('.')
let fileend = line('$')
.delete
if current != fileend
normal! k
endif
execute 'silent noautocmd ' . startinsert . indent.savedstr . a:buns[1]
let head = getpos("'[")
let tail = getpos("']")
endif
let is_linewise = 1
else
let startinsert = a:opt.of('noremap', 'recipe_add') ? 'normal! i' : 'normal ' . s:KEY_i
execute 'silent noautocmd ' . startinsert . a:buns[1]
let head = getpos("'[")
let tail = getpos("']")
endif
endif
return [deletion, is_linewise, indent.diff(a:buns[1]), head, tail]
endfunction
"}}}
function! s:paste(bun, ...) abort "{{{
let undojoin_cmd = a:0 > 0 ? a:1 : ''
let reg = ['"', getreg('"'), getregtype('"')]
let @@ = a:bun
if s:has_gui_running
execute undojoin_cmd . 'normal! ""P'
else
let paste = &paste
let &paste = 1
execute undojoin_cmd . 'normal! ""P'
let &paste = paste
endif
call call('setreg', reg)
endfunction
"}}}
function! s:execute_command(head, tail, command_list) abort "{{{
let mod_head = deepcopy(a:head)
let mod_tail = deepcopy(a:tail)
if a:command_list != []
let before_mod_head = getpos("'[")
let before_mod_tail = getpos("']")
call setpos("'[", a:head)
call setpos("']", a:tail)
for command in a:command_list
execute command
endfor
let after_mod_head = getpos("'[")
let after_mod_tail = getpos("']")
if before_mod_head != after_mod_head || before_mod_tail != after_mod_tail
let mod_head = after_mod_head
let mod_tail = after_mod_tail
endif
endif
return [mod_head, mod_tail]
endfunction
"}}}
function! s:shift_for_add(shifted_pos, target, addition, indent, is_linewise) abort "{{{
call s:push2(a:shifted_pos, a:target, a:addition, a:indent, a:is_linewise)
call s:push1(a:shifted_pos, a:target, a:addition, a:indent, a:is_linewise)
return a:shifted_pos
endfunction
"}}}
function! s:shift_for_delete(shifted_pos, target, deletion, is_linewise) abort "{{{
call s:pull2(a:shifted_pos, a:target, a:deletion, a:is_linewise)
call s:pull1(a:shifted_pos, a:target, a:deletion, a:is_linewise)
return a:shifted_pos
endfunction
"}}}
function! s:shift_for_replace(shifted_pos, target, addition, deletion, indent, is_linewise) abort "{{{
if s:lib.is_in_between(a:shifted_pos, a:target.head1, a:target.tail1)
let startpos = copy(a:target.head1)
let endpos = copy(startpos)
call s:push1(endpos, a:target, a:addition, a:indent, a:is_linewise)
let endpos = s:lib.get_left_pos(endpos)
if s:lib.is_equal_or_ahead(a:shifted_pos, endpos)
let a:shifted_pos[0:3] = endpos
endif
elseif s:lib.is_in_between(a:shifted_pos, a:target.head2, a:target.tail2)
let startpos = copy(a:target.head2)
call s:pull1(startpos, a:target, a:deletion, a:is_linewise)
call s:push1(startpos, a:target, a:addition, a:indent, a:is_linewise)
let endpos = copy(startpos)
let target = copy(s:null_4pos)
let target.head2 = copy(startpos)
call s:push2(endpos, target, a:addition, a:indent, a:is_linewise)
let endpos = s:lib.get_left_pos(endpos)
call s:pull1(a:shifted_pos, a:target, a:deletion, a:is_linewise)
call s:push1(a:shifted_pos, a:target, a:addition, a:indent, a:is_linewise)
if s:lib.is_equal_or_ahead(a:shifted_pos, endpos)
let a:shifted_pos[0:3] = endpos
endif
else
call s:pull2(a:shifted_pos, a:target, a:deletion, a:is_linewise)
if a:is_linewise[1]
let a:target.head2[1] -= 1
endif
call s:push2(a:shifted_pos, a:target, a:addition, a:indent, a:is_linewise)
if a:is_linewise[1]
let a:target.head2[1] += 1
endif
call s:pull1(a:shifted_pos, a:target, a:deletion, a:is_linewise)
if a:is_linewise[0]
let a:target.head1[1] -= 1
endif
call s:push1(a:shifted_pos, a:target, a:addition, a:indent, a:is_linewise)
if a:is_linewise[0]
let a:target.head1[1] += 1
endif
endif
return a:shifted_pos
endfunction
"}}}
function! s:shift_added(func_name, added, ...) abort "{{{
for pos in ['head1', 'tail1', 'head2', 'tail2']
call call(a:func_name, [a:added[pos]] + a:000)
endfor
return a:added
endfunction
"}}}
function! s:push1(shifted_pos, target, addition, indent, is_linewise) abort "{{{
if a:shifted_pos != s:null_pos
let shift = [0, 0, 0, 0]
let head = a:target.head1
if a:is_linewise[0] && a:shifted_pos[1] >= head[1]
" lnum
let shift[1] += 1
endif
if s:lib.is_equal_or_ahead(a:shifted_pos, head) || (a:is_linewise[0] && a:shifted_pos[1] == head[1])
call s:push(shift, a:shifted_pos, head, a:addition[0], a:indent[0], a:is_linewise[0])
endif
let a:shifted_pos[1:2] += shift[1:2]
endif
return a:shifted_pos
endfunction
"}}}
function! s:push2(shifted_pos, target, addition, indent, is_linewise) abort "{{{
if a:shifted_pos != s:null_pos
let shift = [0, 0, 0, 0]
let head = a:target.head2
if a:is_linewise[1] && a:shifted_pos[1] > head[1]
" lnum
let shift[1] += 1
endif
if s:lib.is_equal_or_ahead(a:shifted_pos, head)
call s:push(shift, a:shifted_pos, head, a:addition[1], a:indent[1], a:is_linewise[1])
endif
let a:shifted_pos[1:2] += shift[1:2]
endif
return a:shifted_pos
endfunction
"}}}
function! s:push(shift, shifted_pos, head, addition, indent, is_linewise) abort "{{{
let addition = split(a:addition, '\%(\n\|\r\|\r\n\)', 1)
" lnum
let a:shift[1] += len(addition) - 1
" column
if !a:is_linewise && a:head[1] == a:shifted_pos[1]
let a:shift[2] += a:indent + strlen(addition[-1])
if len(addition) > 1
let a:shift[2] -= a:head[2] - 1
endif
endif
endfunction
"}}}
function! s:pull1(shifted_pos, target, deletion, is_linewise) abort "{{{
if a:shifted_pos != s:null_pos
let shift = [0, 0, 0, 0]
let head = a:target.head1
let tail = a:target.tail1
" lnum
if a:shifted_pos[1] > head[1]
if a:shifted_pos[1] <= tail[1]
let shift[1] -= a:shifted_pos[1] - head[1]
else
let shift[1] -= tail[1] - head[1]
endif
endif
" column
if s:lib.is_ahead(a:shifted_pos, head) && a:shifted_pos[1] <= tail[1]
if s:lib.is_ahead(a:shifted_pos, tail)
let shift[2] -= strlen(split(a:deletion[0], '\%(\n\|\r\|\r\n\)', 1)[-1])
let shift[2] += head[1] != a:shifted_pos[1] ? head[2] - 1 : 0
else
let shift[2] -= a:shifted_pos[2]
let shift[2] += head[2]
endif
endif
let a:shifted_pos[1] += shift[1]
" the case for linewise action
if a:is_linewise[0]
if a:shifted_pos[1] == head[1]
" col
let a:shifted_pos[2] = 0
endif
if a:shifted_pos[1] > head[1]
" lnum
let a:shifted_pos[1] -= 1
endif
endif
if a:shifted_pos[2] == 0
let a:shifted_pos[2] = 1
elseif a:shifted_pos[2] == s:constants('colmax')
let a:shifted_pos[2] = col([a:shifted_pos[1], '$']) - 1
let a:shifted_pos[2] += shift[2]
else
let a:shifted_pos[2] += shift[2]
endif
endif
return a:shifted_pos
endfunction
"}}}
function! s:pull2(shifted_pos, target, deletion, is_linewise) abort "{{{
if a:shifted_pos != s:null_pos
let shift = [0, 0, 0, 0]
let head = a:target.head2
let tail = a:target.tail2
" lnum
if a:shifted_pos[1] >= head[1]
if a:shifted_pos[1] < tail[1]
let shift[1] -= a:shifted_pos[1] - head[1]
else
let shift[1] -= tail[1] - head[1]
endif
endif
" column
if s:lib.is_equal_or_ahead(a:shifted_pos, head) && a:shifted_pos[1] <= tail[1]
if s:lib.is_ahead(a:shifted_pos, tail)
let shift[2] -= strlen(split(a:deletion[1], '\%(\n\|\r\|\r\n\)', 1)[-1])
let shift[2] += head[1] != a:shifted_pos[1] ? head[2] - 1 : 0
else
let shift[2] -= a:shifted_pos[2] + 1
let shift[2] += head[2]
endif
endif
let a:shifted_pos[1:2] += shift[1:2]
" the case for linewise action
if a:is_linewise[1]
if a:shifted_pos[1] == head[1]
" col
let a:shifted_pos[2] = s:constants('colmax')
endif
if a:shifted_pos[1] >= head[1]
" lnum
let a:shifted_pos[1] -= 1
endif
endif
endif
return a:shifted_pos
endfunction
"}}}
function! s:added_tail(head, tail, linewise) abort "{{{
if a:tail[1] > 1 && a:tail[2] == 1
let lnum = a:tail[1] - 1
let col = col([lnum, '$'])
let tail = [a:tail[0], lnum, col, a:tail[3]]
elseif a:linewise
let tail = a:tail
else
let tail = s:lib.get_left_pos(a:tail)
endif
return tail
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,108 @@
" common functions
" variables "{{{
" null valiables
let s:null_pos = [0, 0, 0, 0]
"}}}
let s:lib = {}
function! s:get_wider_region(head_edge, tail_edge) abort "{{{
return [s:get_left_pos(a:head_edge), s:get_right_pos(a:tail_edge)]
endfunction
"}}}
function! s:get_left_pos(pos) abort "{{{
call setpos('.', a:pos)
normal! h
return getpos('.')
endfunction
"}}}
function! s:get_right_pos(pos) abort "{{{
call setpos('.', a:pos)
normal! l
return getpos('.')
endfunction
"}}}
function! s:c2p(coord) abort "{{{
return [0] + a:coord + [0]
endfunction
"}}}
function! s:is_valid_2pos(pos) abort "{{{
" NOTE: This function do not check the geometric relationships.
" It should be checked by s:is_ahead or s:is_equal_or_ahead
" separately.
return a:pos.head != s:null_pos && a:pos.tail != s:null_pos
endfunction
"}}}
function! s:is_valid_4pos(pos) abort "{{{
" NOTE: This function do not check the geometric relationships.
" It should be checked by s:is_ahead or s:is_equal_or_ahead
" separately.
return a:pos.head1 != s:null_pos && a:pos.tail1 != s:null_pos
\ && a:pos.head2 != s:null_pos && a:pos.tail2 != s:null_pos
endfunction
"}}}
function! s:is_ahead(pos1, pos2) abort "{{{
return a:pos1[1] > a:pos2[1] || (a:pos1[1] == a:pos2[1] && a:pos1[2] > a:pos2[2])
endfunction
"}}}
function! s:is_equal_or_ahead(pos1, pos2) abort "{{{
return a:pos1[1] > a:pos2[1] || (a:pos1[1] == a:pos2[1] && a:pos1[2] >= a:pos2[2])
endfunction
"}}}
function! s:is_in_between(pos, head, tail) abort "{{{
return (a:pos != s:null_pos) && (a:head != s:null_pos) && (a:tail != s:null_pos)
\ && ((a:pos[1] > a:head[1]) || ((a:pos[1] == a:head[1]) && (a:pos[2] >= a:head[2])))
\ && ((a:pos[1] < a:tail[1]) || ((a:pos[1] == a:tail[1]) && (a:pos[2] <= a:tail[2])))
endfunction
"}}}
function! s:get_sandwich_option(name, default) abort "{{{
if exists('g:operator#sandwich#' . a:name)
return eval('g:operator#sandwich#' . a:name)
endif
if exists('g:sandwich#' . a:name)
return eval('g:sandwich#' . a:name)
endif
return a:default
endfunction
"}}}
function! s:get_operator_option(name, default) abort "{{{
return get(g:, 'operator#sandwich#' . a:name, a:default)
endfunction
"}}}
function! s:escape(string) abort "{{{
return escape(a:string, '~"\.^$[]*')
endfunction
"}}}
function! s:export(namelist) abort "{{{
let module = {}
for name in a:namelist
let module[name] = function('s:' . name)
endfor
return module
endfunction
"}}}
let s:lib = s:export([
\ 'get_wider_region',
\ 'get_left_pos',
\ 'get_right_pos',
\ 'c2p',
\ 'is_valid_2pos',
\ 'is_valid_4pos',
\ 'is_ahead',
\ 'is_equal_or_ahead',
\ 'is_in_between',
\ 'get_sandwich_option',
\ 'get_operator_option',
\ 'escape',
\ ])
lockvar! s:lib
function! operator#sandwich#lib#import() abort "{{{
return s:lib
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,507 @@
" 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:

View File

@ -0,0 +1,52 @@
" prepare recipes
function! sandwich#get_recipes() abort "{{{
if exists('b:sandwich_recipes')
let recipes = b:sandwich_recipes
elseif exists('g:sandwich#recipes')
let recipes = g:sandwich#recipes
else
let recipes = g:sandwich#default_recipes
endif
return deepcopy(recipes)
endfunction
"}}}
if exists('g:sandwich#default_recipes')
unlockvar! g:sandwich#default_recipes
endif
let g:sandwich#default_recipes = [
\ {'buns': ['\s\+', '\s\+'], 'regex': 1, 'kind': ['delete', 'replace', 'query'], 'input': [' ']},
\ {'buns': ['', ''], 'action': ['add'], 'motionwise': ['line'], 'linewise': 1, 'input': ["\<CR>"]},
\ {'buns': ['^$', '^$'], 'regex': 1, 'linewise': 1, 'input': ["\<CR>"]},
\ {'buns': ['<', '>'], 'expand_range': 0},
\ {'buns': ['"', '"'], 'quoteescape': 1, 'expand_range': 0, 'nesting': 0, 'linewise': 0},
\ {'buns': ["'", "'"], 'quoteescape': 1, 'expand_range': 0, 'nesting': 0, 'linewise': 0},
\ {'buns': ["`", "`"], 'quoteescape': 1, 'expand_range': 0, 'nesting': 0, 'linewise': 0},
\ {'buns': ['{', '}'], 'nesting': 1, 'skip_break': 1},
\ {'buns': ['[', ']'], 'nesting': 1},
\ {'buns': ['(', ')'], 'nesting': 1},
\ {'buns': 'sandwich#magicchar#t#tag()', 'listexpr': 1, 'kind': ['add'], 'action': ['add'], 'input': ['t', 'T']},
\ {'buns': 'sandwich#magicchar#t#tag()', 'listexpr': 1, 'kind': ['replace'], 'action': ['add'], 'input': ['T']},
\ {'buns': 'sandwich#magicchar#t#tagname()', 'listexpr': 1, 'kind': ['replace'], 'action': ['add'], 'input': ['t']},
\ {'external': ["\<Plug>(textobj-sandwich-tag-i)", "\<Plug>(textobj-sandwich-tag-a)"], 'noremap' : 0, 'kind' : ['delete', 'textobj'], 'expr_filter': ['operator#sandwich#kind() !=# "replace"'], 'linewise': 1, 'input': ['t', 'T']},
\ {'external': ["\<Plug>(textobj-sandwich-tag-i)", "\<Plug>(textobj-sandwich-tag-a)"], 'noremap' : 0, 'kind' : ['replace', 'query'], 'expr_filter': ['operator#sandwich#kind() ==# "replace"'], 'input': ['T']},
\ {'external': ["\<Plug>(textobj-sandwich-tagname-i)", "\<Plug>(textobj-sandwich-tagname-a)"], 'noremap' : 0, 'kind' : ['replace', 'textobj'], 'expr_filter': ['operator#sandwich#kind() ==# "replace"'], 'input': ['t']},
\ {'buns': ['sandwich#magicchar#f#fname()', '")"'], 'kind': ['add', 'replace'], 'action': ['add'], 'expr': 1, 'input': ['f']},
\ {'external': ["\<Plug>(textobj-sandwich-function-ip)", "\<Plug>(textobj-sandwich-function-i)"], 'noremap': 0, 'kind': ['delete', 'replace', 'query'], 'input': ['f']},
\ {'external': ["\<Plug>(textobj-sandwich-function-ap)", "\<Plug>(textobj-sandwich-function-a)"], 'noremap': 0, 'kind': ['delete', 'replace', 'query'], 'input': ['F']},
\ {'buns': 'sandwich#magicchar#i#input("operator")', 'kind': ['add', 'replace'], 'action': ['add'], 'listexpr': 1, 'input': ['i']},
\ {'buns': 'sandwich#magicchar#i#input("textobj", 1)', 'kind': ['delete', 'replace', 'query'], 'listexpr': 1, 'input': ['i']},
\ {'buns': 'sandwich#magicchar#i#lastinput("operator", 1)', 'kind': ['add', 'replace'], 'action': ['add'], 'listexpr': 1, 'input': ['I']},
\ {'buns': 'sandwich#magicchar#i#lastinput("textobj")', 'kind': ['delete', 'replace', 'query'], 'listexpr': 1, 'input': ['I']},
\ ]
lockvar! g:sandwich#default_recipes
let g:sandwich#jsx_filetypes = [
\ 'javascript.jsx',
\ 'typescript.tsx',
\ 'javascriptreact',
\ 'typescriptreact'
\ ]
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,60 @@
" clock object - measuring elapsed time in a operation
" variables "{{{
" features
let s:has_reltime_and_float = has('reltime') && has('float')
"}}}
function! sandwich#clock#new() abort "{{{
return deepcopy(s:clock)
endfunction
"}}}
" s:clock "{{{
let s:clock = {
\ 'started' : 0,
\ 'paused' : 0,
\ 'zerotime': reltime(),
\ 'stoptime': reltime(),
\ 'losstime': 0,
\ }
"}}}
function! s:clock.start() dict abort "{{{
if self.started
if self.paused
let self.losstime += str2float(reltimestr(reltime(self.stoptime)))
let self.paused = 0
endif
else
if s:has_reltime_and_float
let self.zerotime = reltime()
let self.started = 1
endif
endif
endfunction
"}}}
function! s:clock.pause() dict abort "{{{
let self.stoptime = reltime()
let self.paused = 1
endfunction
"}}}
function! s:clock.elapsed() dict abort "{{{
if self.started
let total = str2float(reltimestr(reltime(self.zerotime)))
return floor((total - self.losstime)*1000)
else
return 0
endif
endfunction
"}}}
function! s:clock.stop() dict abort "{{{
let self.started = 0
let self.paused = 0
let self.losstime = 0
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,42 @@
" constants - storing essential constants
" variables "{{{
" types
let s:type_list = type([])
"}}}
function! sandwich#constants#get(name) abort "{{{
return type(a:name) == s:type_list
\ ? map(copy(a:name), 's:constants[v:val]()')
\ : s:constants[a:name]()
endfunction
"}}}
" s:constants "{{{
let s:constants = {}
"}}}
" The maximum number of columns "{{{
function! s:constants.colmax() dict abort
return s:colmax_obtained ? s:colmax : s:colmax()
endfunction
let s:colmax = 2147483647 " default value in many cases
let s:colmax_obtained = 0
function! s:colmax() abort
let view = winsaveview()
try
normal! $
let colmax = winsaveview().curswant
call winrestview(view)
let s:colmax_obtained = 1
catch
let colmax = s:colmax
let s:colmax_obtained = 0
endtry
return colmax
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,34 @@
let g:sandwich#filetype#tex#environments = get(g:, 'sandwich#filetype#tex#environments', [
\ 'array', 'center', 'description', 'enumerate', 'eqnarray', 'equation',
\ 'equation*', 'figure', 'flushleft', 'flushright', 'itemize', 'list',
\ 'minipage', 'picture', 'quotation', 'quote', 'tabbing', 'table',
\ 'tabular', 'tabular*', 'thebibliography', 'theorem', 'titlepage',
\ 'verbatim', 'verse'
\ ])
function! sandwich#filetype#tex#EnvCompl(argread, cmdline, cursorpos) abort
let n = strlen(a:argread)
if exists('b:sandwich#filetype#tex#environments')
let list = copy(b:sandwich#filetype#tex#environments)
else
let list = copy(g:sandwich#filetype#tex#environments)
endif
if n > 0
let list = filter(list, 'v:val[: n-1] ==# a:argread')
endif
return list
endfunction
function! sandwich#filetype#tex#EnvInput() abort
echohl MoreMsg
let env = input('Environment-name: ', '', 'customlist,sandwich#filetype#tex#EnvCompl')
echohl NONE
return [printf('\begin{%s}', env), printf('\end{%s}', env)]
endfunction
function! sandwich#filetype#tex#CmdInput() abort
echohl MoreMsg
let cmd = input('Command: ', '')
echohl NONE
return [printf('\%s{', cmd), '}']
endfunction

View File

@ -0,0 +1,514 @@
" highlight object - managing highlight on a buffer
" variables "{{{
" null valiables
let s:null_pos = [0, 0, 0, 0]
" types
let s:type_list = type([])
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_362 = has('patch-7.4.362')
let s:has_patch_7_4_392 = has('patch-7.4.392')
else
let s:has_patch_7_4_362 = v:version == 704 && has('patch362')
let s:has_patch_7_4_392 = v:version == 704 && has('patch392')
endif
" features
let s:has_gui_running = has('gui_running')
let s:has_window_ID = exists('*win_getid')
" SID
function! s:SID() abort
return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$')
endfunction
let s:SID = printf("\<SNR>%s_", s:SID())
delfunction s:SID
"}}}
function! sandwich#highlight#new() abort "{{{
return deepcopy(s:highlight)
endfunction
"}}}
" s:highlight "{{{
let s:highlight = {
\ 'status': 0,
\ 'group' : '',
\ 'id' : [],
\ 'order_list': [],
\ 'region': {},
\ 'linewise': 0,
\ 'bufnr': 0,
\ 'winid': 0,
\ }
"}}}
function! s:highlight.initialize() dict abort "{{{
call self.quench()
let self.status = 0
let self.group = ''
let self.id = []
let self.order_list = []
let self.region = {}
let self.linewise = 0
let self.bufnr = 0
let self.winid = 0
endfunction
"}}}
function! s:highlight.order(target, linewise) dict abort "{{{
let order = []
let order_list = []
for [head, tail, linewise] in [[a:target.head1, a:target.tail1, a:linewise[0]],
\ [a:target.head2, a:target.tail2, a:linewise[1]]]
if linewise
call s:highlight_order_linewise(order_list, order, head, tail)
else
call s:highlight_order_charwise(order_list, order, head, tail)
endif
endfor
if order != []
call add(order_list, order)
endif
let self.order_list += order_list
let self.region = deepcopy(a:target)
let self.linewise = a:linewise
endfunction
"}}}
function! s:highlight.show(...) dict abort "{{{
if self.order_list == []
return 0
endif
if a:0 < 1
if self.group ==# ''
return 0
else
let hi_group = self.group
endif
else
let hi_group = a:1
endif
if self.status
if hi_group ==# self.group
return 0
else
call self.quench()
endif
endif
for order in self.order_list
let self.id += s:matchaddpos(hi_group, order)
endfor
call filter(self.id, 'v:val > 0')
let self.status = 1
let self.group = hi_group
let self.bufnr = bufnr('%')
if s:has_window_ID
let self.winid = win_getid()
endif
return 1
endfunction
"}}}
function! s:highlight.quench() dict abort "{{{
if !self.status
return 0
endif
let tabnr = tabpagenr()
let winnr = winnr()
let view = winsaveview()
if self.highlighted_window()
call s:matchdelete_all(self.id)
let self.status = 0
return 1
endif
if s:is_in_cmdline_window() || s:is_in_popup_terminal_window()
let s:paused += [self]
augroup sandwich-pause-quenching
autocmd!
autocmd WinEnter * call s:got_out_of_cmdwindow()
augroup END
return 0
endif
if self.goto_highlighted_window()
call s:matchdelete_all(self.id)
else
call filter(self.id, 0)
endif
let self.status = 0
call s:goto_window(winnr, tabnr, view)
return 1
endfunction
"}}}
function! s:highlight.quench_timer(time) dict abort "{{{
let id = timer_start(a:time, s:SID . 'quench')
let s:quench_table[string(id)] = self
" this is called when user gets control again
call timer_start(1, {-> s:set_autocmds(id)})
return id
endfunction
"}}}
" function! s:highlight.highlighted_window() dict abort "{{{
if s:has_window_ID
function! s:highlight.highlighted_window() dict abort
return self.winid == win_getid()
endfunction
else
function! s:highlight.highlighted_window() dict abort
if self.id == []
return 0
endif
let id = self.id[0]
return filter(getmatches(), 'v:val.id == id') != [] ? 1 : 0
endfunction
endif
"}}}
" function! s:highlight.goto_highlighted_window() dict abort "{{{
if s:has_window_ID
function! s:highlight.goto_highlighted_window() dict abort
noautocmd let reached = win_gotoid(self.winid)
return reached
endfunction
else
function! s:highlight.goto_highlighted_window() dict abort
return s:search_highlighted_windows(self.id, tabpagenr()) != [0, 0]
endfunction
endif
"}}}
" for delayed quenching "{{{
let s:quench_table = {}
let s:paused = []
function! s:quench(id) abort "{{{
let options = s:shift_options()
let highlight = s:get(a:id)
try
if highlight != {}
call highlight.quench()
endif
catch /^Vim\%((\a\+)\)\=:E523/
" NOTE: For "textlock"
if highlight != {}
call highlight.quench_timer(50)
endif
return 1
finally
unlet s:quench_table[a:id]
call timer_stop(a:id)
call s:clear_autocmds()
call s:restore_options(options)
endtry
endfunction
"}}}
function! s:get(id) abort "{{{
return get(s:quench_table, string(a:id), {})
endfunction
"}}}
function! sandwich#highlight#cancel(...) abort "{{{
if a:0 > 0
let id_list = type(a:1) == s:type_list ? a:1 : a:000
else
let id_list = map(keys(s:quench_table), 'str2nr(v:val)')
endif
for id in id_list
call s:quench(id)
endfor
endfunction
"}}}
function! s:quench_paused(...) abort "{{{
if s:is_in_cmdline_window() || s:is_in_popup_terminal_window()
return
endif
augroup sandwich-pause-quenching
autocmd!
augroup END
for highlight in s:paused
call highlight.quench()
endfor
let s:paused = []
endfunction
"}}}
function! s:got_out_of_cmdwindow() abort "{{{
augroup sandwich-pause-quenching
autocmd!
autocmd CursorMoved * call s:quench_paused()
augroup END
endfunction
"}}}
function! s:set_autocmds(id) abort "{{{
augroup sandwich-highlight
autocmd!
execute printf('autocmd TextChanged,InsertEnter,BufUnload <buffer> call s:cancel_highlight(%s)', a:id)
execute printf('autocmd BufEnter * call s:switch_highlight(%s)', a:id)
augroup END
endfunction
"}}}
function! s:clear_autocmds() abort "{{{
augroup sandwich-highlight
autocmd!
augroup END
endfunction
"}}}
function! s:cancel_highlight(id) abort "{{{
call s:quench(a:id)
endfunction
"}}}
function! s:switch_highlight(id) abort "{{{
let highlight = s:get(a:id)
if highlight == {} || !highlight.highlighted_window()
return
endif
if highlight.bufnr == bufnr('%')
call highlight.show()
else
call highlight.quench()
endif
endfunction
"}}}
"}}}
" private functions
function! s:highlight_order_charwise(order_list, order, head, tail) abort "{{{
let n = len(a:order)
if a:head != s:null_pos && a:tail != s:null_pos && s:is_equal_or_ahead(a:tail, a:head)
if a:head[1] == a:tail[1]
call add(a:order, a:head[1:2] + [a:tail[2] - a:head[2] + 1])
let n += 1
else
for lnum in range(a:head[1], a:tail[1])
if lnum == a:head[1]
call add(a:order, a:head[1:2] + [col([a:head[1], '$']) - a:head[2] + 1])
elseif lnum == a:tail[1]
call add(a:order, [a:tail[1], 1] + [a:tail[2]])
else
call add(a:order, [lnum])
endif
if n == 7
call add(a:order_list, deepcopy(a:order))
call filter(a:order, 0)
let n = 0
else
let n += 1
endif
endfor
endif
endif
endfunction
"}}}
function! s:highlight_order_linewise(order_list, order, head, tail) abort "{{{
let n = len(a:order)
if a:head != s:null_pos && a:tail != s:null_pos && a:head[1] <= a:tail[1]
for lnum in range(a:head[1], a:tail[1])
call add(a:order, [lnum])
if n == 7
call add(a:order_list, deepcopy(a:order))
call filter(a:order, 0)
let n = 0
else
let n += 1
endif
endfor
endif
endfunction
"}}}
" function! s:matchaddpos(group, pos) abort "{{{
if s:has_patch_7_4_362
function! s:matchaddpos(group, pos) abort
return [matchaddpos(a:group, a:pos)]
endfunction
else
function! s:matchaddpos(group, pos) abort
let id_list = []
for pos in a:pos
if len(pos) == 1
let id_list += [matchadd(a:group, printf('\%%%dl', pos[0]))]
else
let id_list += [matchadd(a:group, printf('\%%%dl\%%>%dc.*\%%<%dc', pos[0], pos[1]-1, pos[1]+pos[2]))]
endif
endfor
return id_list
endfunction
endif
"}}}
function! s:matchdelete_all(ids) abort "{{{
if empty(a:ids)
return
endif
let alive_ids = map(getmatches(), 'v:val.id')
" Return if another plugin called clearmatches() which clears *ALL*
" highlights including others set.
if empty(alive_ids)
return
endif
if !count(alive_ids, a:ids[0])
return
endif
for id in a:ids
try
call matchdelete(id)
catch
endtry
endfor
call filter(a:ids, 0)
endfunction
"}}}
function! s:is_ahead(pos1, pos2) abort "{{{
return a:pos1[1] > a:pos2[1] || (a:pos1[1] == a:pos2[1] && a:pos1[2] > a:pos2[2])
endfunction
"}}}
function! s:is_equal_or_ahead(pos1, pos2) abort "{{{
return a:pos1[1] > a:pos2[1] || (a:pos1[1] == a:pos2[1] && a:pos1[2] >= a:pos2[2])
endfunction
"}}}
" function! s:is_in_cmdline_window() abort "{{{
if s:has_patch_7_4_392
function! s:is_in_cmdline_window() abort
return getcmdwintype() !=# ''
endfunction
else
function! s:is_in_cmdline_window() abort
let is_in_cmdline_window = 0
try
execute 'tabnext ' . tabpagenr()
catch /^Vim\%((\a\+)\)\=:E11/
let is_in_cmdline_window = 1
catch
finally
return is_in_cmdline_window
endtry
endfunction
endif
"}}}
" function! s:is_in_popup_terminal_window() abort "{{{
if exists('*popup_list')
function! s:is_in_popup_terminal_window() abort
return &buftype is# 'terminal' && count(popup_list(), win_getid())
endfunction
else
function! s:is_in_popup_terminal_window() abort
return 0
endfunction
endif
" }}}
function! s:shift_options() abort "{{{
let options = {}
""" tweak appearance
" hide_cursor
if s:has_gui_running
let options.cursor = &guicursor
set guicursor+=a:block-NONE
else
let options.cursor = &t_ve
set t_ve=
endif
return options
endfunction
"}}}
function! s:restore_options(options) abort "{{{
if s:has_gui_running
set guicursor&
let &guicursor = a:options.cursor
else
let &t_ve = a:options.cursor
endif
endfunction
"}}}
function! s:search_highlighted_windows(id, ...) abort "{{{
if a:id == []
return 0
endif
let original_winnr = winnr()
let original_tabnr = tabpagenr()
let original_view = winsaveview()
let tablist = range(1, tabpagenr('$'))
if a:0 > 0
let tabnr = a:1
let [tabnr, winnr] = s:scan_windows(a:id, tabnr)
if tabnr != 0
return [tabnr, winnr]
endif
call filter(tablist, 'v:val != tabnr')
endif
for tabnr in tablist
let [tabnr, winnr] = s:scan_windows(a:id, tabnr)
if tabnr != 0
return [tabnr, winnr]
endif
endfor
call s:goto_window(original_winnr, original_tabnr, original_view)
return [0, 0]
endfunction
"}}}
function! s:scan_windows(id, tabnr) abort "{{{
if s:goto_tab(a:tabnr)
for winnr in range(1, winnr('$'))
if s:goto_window(winnr) && s:is_highlighted_window(a:id)
return [a:tabnr, winnr]
endif
endfor
endif
return [0, 0]
endfunction
"}}}
function! s:is_highlighted_window(id) abort "{{{
if a:id != []
let id = a:id[0]
if filter(getmatches(), 'v:val.id == id') != []
return 1
endif
endif
return 0
endfunction
"}}}
function! s:goto_window(winnr, ...) abort "{{{
if a:0 > 0
if !s:goto_tab(a:1)
return 0
endif
endif
try
if a:winnr != winnr()
execute printf('noautocmd %swincmd w', a:winnr)
endif
catch /^Vim\%((\a\+)\)\=:E16/
return 0
endtry
if a:0 > 1
noautocmd call winrestview(a:2)
endif
return 1
endfunction
"}}}
function! s:goto_tab(tabnr) abort "{{{
if a:tabnr != tabpagenr()
execute 'noautocmd tabnext ' . a:tabnr
endif
return tabpagenr() == a:tabnr ? 1 : 0
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,438 @@
let s:save_cpo = &cpo
set cpo&vim
" variables "{{{
let s:FALSE = 0
let s:TRUE = 1
let s:null_pos = [0, 0]
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_1685 = has('patch-7.4.1685')
let s:has_patch_7_4_2011 = has('patch-7.4.2011')
else
let s:has_patch_7_4_1685 = v:version == 704 && has('patch1685')
let s:has_patch_7_4_2011 = v:version == 704 && has('patch2011')
endif
"}}}
" default patterns
let g:sandwich#magicchar#f#default_patterns = [
\ {
\ 'header' : '\<\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
function! sandwich#magicchar#f#fname() abort "{{{
call operator#sandwich#show()
try
echohl MoreMsg
if &filetype ==# 'vim'
let funcname = input('funcname: ', '', 'custom,sandwich#magicchar#f#fnamecompl_vim')
else
let funcname = input('funcname: ', '', 'custom,sandwich#magicchar#f#fnamecompl')
endif
" flash prompt
echo ''
finally
echohl NONE
call operator#sandwich#quench()
endtry
if funcname ==# ''
throw 'OperatorSandwichCancel'
endif
return funcname . '('
endfunction
"}}}
function! sandwich#magicchar#f#fnamecompl(ArgLead, CmdLine, CursorPos) abort "{{{
return join(uniq(sort(s:buffer_completion())), "\n")
endfunction
"}}}
function! sandwich#magicchar#f#fnamecompl_vim(ArgLead, CmdLine, CursorPos) abort "{{{
if s:has_patch_7_4_2011
let getcomp = map(filter(getcompletion(a:ArgLead, 'function'), 'v:val =~# ''\C^[a-z][a-zA-Z0-9_]*($'''), 'matchstr(v:val, ''\C^[a-z][a-zA-Z0-9_]*'')')
else
let getcomp = []
endif
let buffer = s:buffer_completion()
return join(uniq(sort(getcomp + buffer)), "\n")
endfunction
"}}}
function! s:buffer_completion() abort "{{{
" NOTE: This func does neither sort nor uniq.
let list = []
let lines = getline(1, '$')
let pattern_list = s:resolve_patterns()
for func in pattern_list
let pat = printf('%s\ze%s', func.header, func.bra)
for line in lines
let list += s:extract_pattern(line, pat)
endfor
endfor
return list
endfunction
"}}}
function! s:extract_pattern(string, pat) abort "{{{
let list = []
let end = 0
while 1
let [str, start, end] = s:matchstrpos(a:string, a:pat, end)
if start < 0
break
endif
let list += [str]
endwhile
return list
endfunction
"}}}
" function! s:matchstrpos(expr, pat, ...) abort "{{{
if s:has_patch_7_4_1685
let s:matchstrpos = function('matchstrpos')
else
function! s:matchstrpos(expr, pat, ...) abort
if a:0 == 0
return [matchstr(a:expr, a:pat), match(a:expr, a:pat), matchend(a:expr, a:pat)]
elseif a:0 == 1
return [matchstr(a:expr, a:pat, a:1), match(a:expr, a:pat, a:1), matchend(a:expr, a:pat, a:1)]
else
return [matchstr(a:expr, a:pat, a:1, a:2), match(a:expr, a:pat, a:1, a:2), matchend(a:expr, a:pat, a:1, a:2)]
endif
endfunction
endif
"}}}
" textobj-functioncall (bundle version)
" NOTE: https://github.com/machakann/vim-textobj-functioncall
function! sandwich#magicchar#f#i(mode) abort "{{{
call sandwich#magicchar#f#textobj('i', a:mode)
endfunction
"}}}
function! sandwich#magicchar#f#a(mode) abort "{{{
call sandwich#magicchar#f#textobj('a', a:mode)
endfunction
"}}}
function! sandwich#magicchar#f#ip(mode) abort "{{{
call sandwich#magicchar#f#textobj('ip', a:mode)
endfunction
"}}}
function! sandwich#magicchar#f#ap(mode) abort "{{{
call sandwich#magicchar#f#textobj('ap', a:mode)
endfunction
"}}}
function! sandwich#magicchar#f#textobj(kind, mode, ...) abort "{{{
let l:count = v:count1
if a:0
let pattern_list = a:1
else
let pattern_list = s:resolve_patterns()
endif
let searchlines = s:get('textobj_sandwich_function_searchlines' , 30)
let stopline = {}
if searchlines < 0
let stopline.upper = 1
let stopline.lower = line('$')
else
let stopline.upper = max([1, line('.') - searchlines])
let stopline.lower = min([line('.') + searchlines, line('$')])
endif
let [start, end] = [s:null_pos, s:null_pos]
let view = winsaveview()
try
let candidates = s:gather_candidates(a:kind, a:mode, l:count, pattern_list, stopline)
let elected = s:elect(candidates, l:count)
if elected != {}
let [start, end] = s:to_range(a:kind, elected)
endif
finally
call winrestview(view)
endtry
call s:select(start, end)
endfunction
"}}}
function! s:Candidate(head, bra, ket, tail, pat, rank) abort
return {
\ 'head': a:head,
\ 'bra': a:bra,
\ 'ket': a:ket,
\ 'tail': a:tail,
\ 'rank': a:rank,
\ 'pattern': a:pat,
\ 'len': s:buflen(a:head, a:tail),
\ }
endfunction
function! s:gather_candidates(kind, mode, count, pattern_list, stopline) abort "{{{
let curpos = getpos('.')[1:2]
let rank = 0
let candidates = []
for pattern in a:pattern_list
let rank += 1
let candidates += s:search_pattern(pattern, a:kind, a:mode, a:count, rank, curpos, a:stopline)
call cursor(curpos)
endfor
return a:kind[0] is# 'a' && candidates == []
\ ? s:gather_candidates('i', a:mode, a:count, a:pattern_list, a:stopline)
\ : candidates
endfunction
"}}}
function! s:search_pattern(pat, kind, mode, count, rank, curpos, stopline) abort "{{{
let a:pat.head = a:pat.header . a:pat.bra
let a:pat.tail = a:pat.ket . a:pat.footer
let brapos = s:search_key_bra(a:kind, a:curpos, a:pat, a:stopline)
if brapos == s:null_pos | return [] | endif
let is_string = s:is_string_syntax(brapos)
let candidates = []
while len(candidates) < a:count
let c = s:get_candidate(a:pat, a:kind, a:mode, a:rank, brapos, is_string, a:stopline)
if c != {}
call add(candidates, c)
endif
call cursor(brapos)
" move to the next 'bra'
let brapos = searchpairpos(a:pat.bra, '', a:pat.ket, 'b', '', a:stopline.upper)
if brapos == s:null_pos | break | endif
let is_string = s:is_string_syntax(brapos)
endwhile
return candidates
endfunction
"}}}
function! s:search_key_bra(kind, curpos, pat, stopline) abort "{{{
let brapos = s:null_pos
if a:kind[0] is# 'a'
" search for the first 'bra'
if searchpos(a:pat.tail, 'cn', a:stopline.lower) == a:curpos
let brapos = searchpairpos(a:pat.bra, '', a:pat.ket, 'b', '', a:stopline.upper)
endif
let brapos = searchpairpos(a:pat.bra, '', a:pat.ket, 'b', '', a:stopline.upper)
elseif a:kind[0] is# 'i'
let head_start = searchpos(a:pat.head, 'bc', a:stopline.upper)
let head_end = searchpos(a:pat.head, 'ce', a:stopline.lower)
call cursor(a:curpos)
let tail_start = searchpos(a:pat.tail, 'bc', a:stopline.upper)
let tail_end = searchpos(a:pat.tail, 'ce', a:stopline.lower)
" check the initial position
if s:is_in_between(a:curpos, head_start, head_end)
" cursor is on a header
call cursor(head_end)
elseif s:is_in_between(a:curpos, tail_start, tail_end)
" cursor is on a footer
call cursor(tail_start)
if tail_start[1] == 1
normal! k$
else
normal! h
endif
else
" cursor is in between a bra and a ket
call cursor(a:curpos)
endif
" move to the corresponded 'bra'
let brapos = searchpairpos(a:pat.bra, '', a:pat.ket, 'bc', '', a:stopline.upper)
endif
return brapos
endfunction
"}}}
function! s:get_candidate(pat, kind, mode, rank, brapos, is_string, stopline) abort "{{{
" 'bra' should accompany with 'header'
let headstart = searchpos(a:pat.head, 'bc', a:stopline.upper)
let headend = searchpos(a:pat.head, 'ce', a:stopline.lower)
call cursor(a:brapos)
if !s:is_in_between(a:brapos, headstart, headend)
return {}
endif
let headpos = headstart
" search for the paired 'ket'
let skip = 's:is_string_syntax(getpos(".")[1:2]) != a:is_string'
let ketpos = searchpairpos(a:pat.bra, '', a:pat.ket, '', skip, a:stopline.lower)
if ketpos == s:null_pos
return {}
endif
let tailpos = searchpos(a:pat.tail, 'ce', a:stopline.lower)
if searchpos(a:pat.tail, 'bcn', a:stopline.upper) != ketpos
return {}
endif
let c = s:Candidate(headpos, a:brapos, ketpos, tailpos, a:pat, a:rank)
if !s:is_valid_candidate(c, a:kind, a:mode, a:is_string)
return {}
endif
return c
endfunction
"}}}
function! s:elect(candidates, count) abort "{{{
if a:candidates == []
return {}
endif
let filter = 'v:val.head != s:null_pos && v:val.bra != s:null_pos && v:val.ket != s:null_pos && v:val.tail != s:null_pos'
call filter(a:candidates, filter)
call sort(a:candidates, 's:compare')
if len(a:candidates) < a:count
return {}
endif
return a:candidates[a:count - 1]
endfunction
"}}}
function! s:to_range(kind, candidate) abort "{{{
if a:kind[1] is# 'p'
let [start, end] = s:parameter_region(a:candidate)
else
let start = a:candidate.head
let end = a:candidate.tail
endif
return [start, end]
endfunction
"}}}
function! s:select(start, end) abort "{{{
if a:start == s:null_pos || a:end == s:null_pos
return
endif
normal! v
call cursor(a:start)
normal! o
call cursor(a:end)
if &selection is# 'exclusive'
normal! l
endif
endfunction
"}}}
function! s:is_in_between(pos, start, end) abort "{{{
return (a:pos != s:null_pos) && (a:start != s:null_pos) && (a:end != s:null_pos)
\ && ((a:pos[0] > a:start[0]) || ((a:pos[0] == a:start[0]) && (a:pos[1] >= a:start[1])))
\ && ((a:pos[0] < a:end[0]) || ((a:pos[0] == a:end[0]) && (a:pos[1] <= a:end[1])))
endfunction
"}}}
function! s:is_string_syntax(pos) abort "{{{
return match(map(synstack(a:pos[0], a:pos[1]), 'synIDattr(synIDtrans(v:val), "name")'), 'String') > -1
endfunction
"}}}
function! s:is_continuous_syntax(brapos, ketpos) abort "{{{
let start_col = a:brapos[1]
for lnum in range(a:brapos[0], a:ketpos[0])
if lnum == a:ketpos[0]
let end_col= a:ketpos[1]
else
let end_col= col([lnum, '$'])
endif
for col in range(start_col, end_col)
if match(map(synstack(lnum, col), 'synIDattr(synIDtrans(v:val), "name")'), 'String') < 0
return 0
endif
endfor
let start_col = 1
endfor
return 1
endfunction
"}}}
function! s:is_valid_syntax(candidate, is_string) abort "{{{
return !a:is_string || s:is_continuous_syntax(a:candidate.bra, a:candidate.ket)
endfunction
"}}}
function! s:is_same_or_adjacent(p1, p2) abort "{{{
return a:p1 == a:p2 || (a:p1[0] == a:p2[0] && a:p1[1]+1 == a:p2[1])
endfunction
"}}}
function! s:is_wider(candidate, start, end) abort "{{{
return (s:is_ahead(a:start, a:candidate.head) && s:is_same_or_ahead(a:candidate.tail, a:end)) ||
\ (s:is_same_or_ahead(a:start, a:candidate.head) && s:is_ahead(a:candidate.tail, a:end))
endfunction
"}}}
function! s:is_ahead(p1, p2) abort "{{{
return (a:p1[0] > a:p2[0]) || (a:p1[0] == a:p2[0] && a:p1[1] > a:p2[1])
endfunction
"}}}
function! s:is_same_or_ahead(p1, p2) abort "{{{
return (a:p1[0] > a:p2[0]) || (a:p1[0] == a:p2[0] && a:p1[1] >= a:p2[1])
endfunction
"}}}
function! s:is_valid_candidate(c, kind, mode, is_string) abort "{{{
if a:kind[1] is# 'p' && s:is_same_or_adjacent(a:c.bra, a:c.ket)
return s:FALSE
endif
if a:mode is# 'x' && !s:is_wider(a:c, getpos("'<")[1:2], getpos("'>")[1:2])
return s:FALSE
endif
if !s:is_valid_syntax(a:c, a:is_string)
return s:FALSE
endif
return s:TRUE
endfunction
"}}}
function! s:resolve_patterns() abort "{{{
return deepcopy(get(b:, 'sandwich_magicchar_f_patterns',
\ get(g:, 'sandwich#magicchar#f#patterns',
\ g:sandwich#magicchar#f#default_patterns)))
endfunction
"}}}
function! s:parameter_region(candidate) abort "{{{
let whichwrap = &whichwrap
let &whichwrap = 'h,l'
let [visualhead, visualtail] = [getpos("'<"), getpos("'>")]
try
normal! v
call cursor(a:candidate.bra)
call search(a:candidate.pattern.bra, 'ce', a:candidate.ket[0])
normal! l
let head = getpos('.')[1:2]
normal! o
call cursor(a:candidate.ket)
normal! h
let tail = getpos('.')[1:2]
execute "normal! \<Esc>"
finally
let &whichwrap = whichwrap
call setpos("'<", visualhead)
call setpos("'>", visualtail)
endtry
return [head, tail]
endfunction
"}}}
function! s:buflen(start, end) abort "{{{
" start, end -> [lnum, col]
if a:start[0] == a:end[0]
let len = a:end[1] - a:start[1] + 1
else
let len = (line2byte(a:end[0]) + a:end[1]) - (line2byte(a:start[0]) + a:start[1]) + 1
endif
return len
endfunction
"}}}
function! s:compare(i1, i2) abort "{{{
" i1, i2 -> Candidate
if a:i1.len < a:i2.len
return -1
elseif a:i1.len > a:i2.len
return 1
else
return a:i2.rank - a:i1.rank
endif
endfunction
"}}}
function! s:get(name, default) abort "{{{
return exists('b:' . a:name) ? b:[a:name]
\ : exists('g:' . a:name) ? g:[a:name]
\ : a:default
endfunction
"}}}
let &cpo = s:save_cpo
unlet s:save_cpo
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,102 @@
let s:last_inserted = []
let s:last_searched = []
" variables "{{{
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_1685 = has('patch-7.4.1685')
else
let s:has_patch_7_4_1685 = v:version == 704 && has('patch1685')
endif
"}}}
function! sandwich#magicchar#i#input(kind, ...) abort "{{{
let storepoint = a:kind ==# 'operator' ? 'last_inserted' : 'last_searched'
let former = s:input(a:kind . '-sandwich:head: ')
let quit_if_empty = get(a:000, 0, 0)
if quit_if_empty && former ==# ''
let s:{storepoint} = []
return ['', '']
endif
let latter = s:input(a:kind . '-sandwich:tail: ')
let s:{storepoint} = [former, latter]
return copy(s:{storepoint})
endfunction
"}}}
function! sandwich#magicchar#i#lastinput(kind, ...) abort "{{{
let storepoint = a:kind ==# 'operator' ? 'last_inserted' : 'last_searched'
let cancel_if_invalid = get(a:000, 0, 0)
if cancel_if_invalid && s:{storepoint} == []
throw 'OperatorSandwichCancel'
endif
return s:{storepoint} != [] ? copy(s:{storepoint}) : ['', '']
endfunction
"}}}
function! sandwich#magicchar#i#compl(ArgLead, CmdLine, CursorPos) abort "{{{
let list = []
let lines = getline(1, '$')
if a:ArgLead ==# ''
let list += s:extract_patterns_from_lines(lines, '\<\k\{3,}\>')
else
let list += s:extract_patterns_from_lines(lines, printf('\<%s.\{-}\>', a:ArgLead))
if list == []
let tail = matchstr(a:ArgLead, '\<\k\+$')
if tail !=# '' && tail !=# a:ArgLead
let list += s:extract_patterns_from_lines(lines, tail . '.\{-}\>')
endif
endif
endif
return join(uniq(sort(list)), "\n")
endfunction
"}}}
function! s:input(mes) abort "{{{
echohl MoreMsg
try
let input = input(a:mes, '', 'custom,sandwich#magicchar#i#compl')
" flash prompt
echo ''
finally
echohl NONE
endtry
return input
endfunction
"}}}
function! s:extract_patterns_from_lines(lines, pat) abort "{{{
let list = []
for line in a:lines
let list += s:extract_pattern(line, a:pat)
endfor
return list
endfunction
"}}}
function! s:extract_pattern(string, pat) abort "{{{
let list = []
let end = 0
while 1
let [str, start, end] = s:matchstrpos(a:string, a:pat, end)
if start < 0
break
endif
let list += [str]
endwhile
return list
endfunction
"}}}
" function! s:matchstrpos(expr, pat, ...) abort "{{{
if s:has_patch_7_4_1685
let s:matchstrpos = function('matchstrpos')
else
function! s:matchstrpos(expr, pat, ...) abort
if a:0 == 0
return [matchstr(a:expr, a:pat), match(a:expr, a:pat), matchend(a:expr, a:pat)]
elseif a:0 == 1
return [matchstr(a:expr, a:pat, a:1), match(a:expr, a:pat, a:1), matchend(a:expr, a:pat, a:1)]
else
return [matchstr(a:expr, a:pat, a:1, a:2), match(a:expr, a:pat, a:1, a:2), matchend(a:expr, a:pat, a:1, a:2)]
endif
endfunction
endif
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,446 @@
" variables "{{{
let s:null_coord = [0, 0]
" types
let s:type_num = type(0)
let s:type_str = type('')
let s:type_list = type([])
"}}}
function! sandwich#magicchar#t#tag() abort "{{{
call operator#sandwich#show()
try
echohl MoreMsg
let old_imsearch = &l:imsearch
let &l:imsearch = 0
let tag = input('Input tag: ')
let &l:imsearch = old_imsearch
" flash prompt
echo ''
finally
echohl NONE
call operator#sandwich#quench()
endtry
if tag ==# ''
throw 'OperatorSandwichCancel'
endif
let [open, close] = s:expand(tag)
return [printf('<%s>', open), printf('</%s>', close)]
endfunction
"}}}
function! sandwich#magicchar#t#tagname() abort "{{{
call operator#sandwich#show()
try
echohl MoreMsg
let old_imsearch = &l:imsearch
let &l:imsearch = 0
let tagname = input('Input tag name: ')
let &l:imsearch = old_imsearch
" flash prompt
echo ''
finally
echohl NONE
call operator#sandwich#quench()
endtry
if tagname ==# ''
throw 'OperatorSandwichCancel'
endif
return s:expand(tagname)
endfunction
"}}}
function! s:tokenize(text) abort "{{{
let j = 0
let n = strlen(a:text)
let tokenlist = []
while j >= 0 && j < n
let i = j
let j = match(a:text, '\m[[[:blank:]#.]', i)
if i < j
let tokenlist += [a:text[i : j-1]]
elseif i == j
let char = a:text[i]
if char =~# '\m\s'
" space is not allowed
throw 'SandwichMagiccharTIncorrectSyntax'
elseif char ==# '['
" tokenize custom attributes
let tokenlist += [char]
let j += 1
let [j, tokenlist] += s:tokenize_custom_attributes(a:text, j)
else
let tokenlist += [char]
let j += 1
endif
endif
endwhile
let i = j >= 0 ? j : i
if i < n
let tokenlist += [a:text[i :]]
endif
return tokenlist
endfunction
"}}}
function! s:tokenize_custom_attributes(text, j) abort "{{{
let j = a:j
let n = strlen(a:text)
let closed = 0
let tokenlist = []
while j >= 0 && j < n
let i = j
let j = match(a:text, '\m[][:blank:]="'']', i)
if i < j
let string = a:text[i : j-1]
let tokenlist += [string]
elseif i == j
let char = a:text[i]
if char =~# '\m\s'
" skip space
let j = matchend(a:text, '\m\s\+', i)
if j > i
let tokenlist += [a:text[i : j-1]]
endif
elseif char =~# '\m["'']'
" skip string literal
let j = match(a:text, char, i+1)
if j > 0
let tokenlist += [a:text[i : j]]
let j += 1
else
" unclosed string literal
throw 'SandwichMagiccharTIncorrectSyntax'
endif
elseif char ==# ']'
" the end of custom attributes region
let tokenlist += [char]
let j += 1
let closed = 1
break
else
let tokenlist += [char]
let j += 1
endif
endif
endwhile
if !closed
" unclosed braket
throw 'SandwichMagiccharTIncorrectSyntax'
endif
return [j - a:j, tokenlist]
endfunction
"}}}
function! s:matches_filetype(filetype, list) abort "{{{
return index(a:list, a:filetype) != -1
endfunction
" }}}
function! s:parse(tokenlist) abort "{{{
let itemdict = deepcopy(s:itemdict)
let itemlist = map(copy(a:tokenlist), '{"is_operator": v:val =~# ''^\%([][#.=]\|\s\+\)$'' ? 1 : 0, "string": v:val}')
let i = 0
let n = len(itemlist)
let item = itemlist[i]
if item.is_operator
call itemdict.queue_element('div')
else
call itemdict.queue_element(item.string)
let i += 1
endif
while i < n
let item = itemlist[i]
if item.is_operator
if item.string ==# '#'
let i = s:handle_id(itemdict, itemlist, i)
elseif item.string ==# '.'
if s:matches_filetype(&filetype, g:sandwich#jsx_filetypes)
let i = s:handle_className(itemdict, itemlist, i)
else
let i = s:handle_class(itemdict, itemlist, i)
endif
elseif item.string ==# '['
let i = s:parse_custom_attributes(itemdict, itemlist, i)
else
" unanticipated operator (should not reach here)
throw 'SandwichMagiccharTIncorrectSyntax'
endif
else
" successive operand is not allowed here (should not reach here)
throw 'SandwichMagiccharTIncorrectSyntax'
endif
endwhile
let parsed = deepcopy(itemdict.queue)
for item in parsed
call filter(item, 'v:key =~# ''\%(name\|value\)''')
endfor
return parsed
endfunction
"}}}
function! s:parse_custom_attributes(itemdict, itemlist, i) abort "{{{
" check ket
let i_ket = s:check_ket(a:itemlist, a:i)
if i_ket < 0
" ket does not exist (should not reach here)
throw 'SandwichMagiccharTIncorrectSyntax'
endif
let i = a:i + 1
while i < i_ket
let item = a:itemlist[i]
if item.is_operator
if item.string ==# '='
let i = s:handle_equal(a:itemdict, a:itemlist, i)
else
let i += 1
endif
else
let i = s:handle_custom_attr_name(a:itemdict, a:itemlist, i)
endif
endwhile
call a:itemdict.queue_custom_attr()
return i_ket + 1
endfunction
"}}}
function! s:handle_id(itemdict, itemlist, i) abort "{{{
let i = a:i + 1
let item = get(a:itemlist, i, {'is_operator': 1})
if item.is_operator
call a:itemdict.queue_id()
else
call a:itemdict.queue_id(item.string)
let i += 1
endif
return i
endfunction
"}}}
function! s:handle_class(itemdict, itemlist, i) abort "{{{
let i = a:i + 1
let item = get(a:itemlist, i, {'is_operator': 1})
if item.is_operator
call a:itemdict.queue_class()
else
call a:itemdict.queue_class(item.string)
let i += 1
endif
return i
endfunction
"}}}
function! s:handle_className(itemdict, itemlist, i) abort "{{{
let i = a:i + 1
let item = get(a:itemlist, i, {'is_operator': 1})
if item.is_operator
call a:itemdict.queue_className()
else
call a:itemdict.queue_className(item.string)
let i += 1
endif
return i
endfunction
"}}}
function! s:handle_equal(itemdict, itemlist, i, ...) abort "{{{
let i = a:i + 1
let item = a:itemlist[i]
let name = get(a:000, 0, '')
if item.is_operator
if item.string ==# '='
call a:itemdict.list_custom_attr(name)
else
call a:itemdict.list_custom_attr(name)
let i += 1
endif
else
call a:itemdict.list_custom_attr(name, item.string)
let i += 1
endif
return i
endfunction
"}}}
function! s:handle_custom_attr_name(itemdict, itemlist, i) abort "{{{
let item = a:itemlist[a:i]
let name = item.string
let i = a:i + 1
let item = a:itemlist[i]
if item.is_operator
if item.string ==# '='
let i = s:handle_equal(a:itemdict, a:itemlist, i, name)
else
call a:itemdict.list_custom_attr(name)
let i += 1
endif
else
call a:itemdict.list_custom_attr(name)
endif
return i
endfunction
"}}}
function! s:check_ket(itemlist, i) abort "{{{
let i = a:i + 1
let n = len(a:itemlist)
let i_ket = -1
while i < n
let item = a:itemlist[i]
if item.is_operator && item.string ==# ']'
let i_ket = i
break
endif
let i += 1
endwhile
return i_ket
endfunction
"}}}
function! s:build(itemlist) abort "{{{
let fragments = [a:itemlist[0]['value']]
if len(a:itemlist) > 1
for item in a:itemlist[1:]
let name = item.name
let value = type(item.value) == s:type_list ? join(filter(item.value, 'v:val !=# ""')) : item.value
let value = value !~# '^\(["'']\).*\1$' ? printf('"%s"', value) : value
let fragments += [printf('%s=%s', name, value)]
endfor
endif
return join(fragments)
endfunction
"}}}
function! s:expand(text) abort "{{{
try
let tokenlist = s:tokenize(a:text)
let itemlist = s:parse(tokenlist)
catch /^SandwichMagiccharTIncorrectSyntax$/
return [a:text, matchstr(a:text, '^\s*\zs\a[^[:blank:]>/]*')]
endtry
let element = itemlist[0]['value']
return [s:build(itemlist), element]
endfunction
"}}}
function! sandwich#magicchar#t#i() abort "{{{
call s:prototype('i')
endfunction
"}}}
function! sandwich#magicchar#t#a() abort "{{{
call s:prototype('a')
endfunction
"}}}
function! s:prototype(kind) abort "{{{
let view = winsaveview()
let visualhead = getpos("'<")
let visualtail = getpos("'>")
execute printf('silent! normal! v%dat', v:count1)
execute "normal! \<Esc>"
if getpos("'<") != getpos("'>")
normal! gv
let pat = a:kind ==# 'i' ? '</\ze\a[^[:blank:]>/]*' : '</\a[^[:blank:]>/]*\ze\s*>'
call search(pat, 'be', line("'<"))
normal! o
let pat = a:kind ==# 'i' ? '<\a[^[:blank:]>/]*\zs\_.' : '<\zs\a[^[:blank:]>/]*'
call search(pat, '', line("'>"))
else
call winrestview(view)
call setpos("'<", visualhead)
call setpos("'>", visualtail)
endif
endfunction
"}}}
function! sandwich#magicchar#t#it() abort "{{{
call s:textobj('i')
endfunction
"}}}
function! sandwich#magicchar#t#at() abort "{{{
call s:textobj('a')
endfunction
"}}}
function! s:textobj(a_or_i) abort "{{{
let head = searchpos('<\a[^[:blank:]>/]*', 'bcn', 0, 50)
if head != s:null_coord
let tagname = matchstr(getline(head[0])[head[1]-1 :], '^<\zs\a[^[:blank:]>/]*')
if search(printf('</%s>', s:escape(tagname)), 'cn', 0, 50)
" add :silent! to suppress errorbell
if a:a_or_i ==# 'i'
silent! normal! vit
else
silent! normal! vat
endif
endif
endif
endfunction
"}}}
function! s:escape(string) abort "{{{
return escape(a:string, '~"\.^$[]*')
endfunction
"}}}
" itemdict object "{{{
let s:itemdict = {
\ 'element': {'name': 'element', 'value': ''},
\ 'id' : {'name': 'id', 'value': '', 'queued': 0},
\ 'class' : {'name': 'class', 'value': [], 'queued': 0},
\ 'className' : {'name': 'className', 'value': [], 'queued': 0},
\ 'queue' : [],
\ 'customlist': [],
\ }
function! s:itemdict.queue_element(value) dict abort "{{{
let self.element.value = a:value
call add(self.queue, self.element)
endfunction
"}}}
function! s:itemdict.queue_id(...) dict abort "{{{
let self.id.value = get(a:000, 0, '')
if !self.id.queued
call add(self.queue, self.id)
let self.id.queued = 1
endif
endfunction
"}}}
function! s:itemdict.queue_class(...) dict abort "{{{
call add(self.class.value, get(a:000, 0, ''))
if !self.class.queued
call add(self.queue, self.class)
let self.class.queued = 1
endif
endfunction
"}}}
function! s:itemdict.queue_className(...) dict abort "{{{
call add(self.className.value, get(a:000, 0, ''))
if !self.className.queued
call add(self.queue, self.className)
let self.className.queued = 1
endif
endfunction
"}}}
function! s:itemdict.queue_custom_attr() dict abort "{{{
if self.customlist != []
call extend(self.queue, remove(self.customlist, 0, -1))
endif
endfunction
"}}}
function! s:itemdict.list_custom_attr(name, ...) dict abort "{{{
let value = get(a:000, 0, '')
if a:name ==# 'id'
call self.queue_id(value)
elseif a:name ==# 'class'
call self.queue_class(value)
endif
if a:name ==# ''
let custom_attr = {'kind': 'custom', 'name': a:name, 'value': value}
call add(self.customlist, custom_attr)
else
if !has_key(self, a:name)
let self[a:name] = {'kind': 'custom', 'name': a:name, 'value': value}
call add(self.customlist, self[a:name])
else
let self[a:name]['value'] = value
endif
endif
endfunction
"}}}
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,67 @@
" messenger object - managing messages and errors.
function! sandwich#messenger#new() abort "{{{
let s:messenger = deepcopy(s:messenger_prototype)
return s:messenger
endfunction
"}}}
function! sandwich#messenger#get() abort "{{{
return s:messenger
endfunction
"}}}
" s:messenger_prototype "{{{
let s:messenger_prototype = {
\ 'error' : {'flag': 0, 'string': ''},
\ 'notice': {'flag': 0, 'list': []},
\ }
"}}}
function! s:messenger_prototype.initialize() dict abort "{{{
let self.error.flag = 0
let self.error.string = ''
let self.notice.flag = 0
let self.notice.list = []
endfunction
"}}}
function! s:messenger_prototype.notify(prefix) dict abort "{{{
if self.error.flag
call self.error.echoerr(a:prefix)
elseif self.notice.flag
call self.notice.echo(a:prefix)
endif
endfunction
"}}}
function! s:messenger_prototype.error.echoerr(prefix) dict abort "{{{
echoerr a:prefix . self.string
endfunction
"}}}
function! s:messenger_prototype.notice.echo(prefix) dict abort "{{{
let queue = []
if self.list != []
for line in self.list
let queue += [[a:prefix, 'MoreMsg']]
let queue += [line]
let queue += [["\n", 'NONE']]
endfor
call remove(queue, -1)
endif
call sandwich#util#echo(queue)
endfunction
"}}}
function! s:messenger_prototype.error.set(errmes) dict abort "{{{
let self.flag = 1
let self.string = a:errmes
endfunction
"}}}
function! s:messenger_prototype.notice.queue(line) dict abort "{{{
let self.flag = 1
call add(self.list, a:line)
endfunction
"}}}
call sandwich#messenger#new()
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,346 @@
" opt object - managing options
function! sandwich#opt#new(kind, defaultopt, argopt) abort "{{{
let opt = deepcopy(s:opt)
if a:kind ==# 'auto' || a:kind ==# 'query'
let opt.recipe = {}
let opt.of = function('s:of_for_textobj')
let opt.filter = s:default_values['textobj']['filter']
call opt.update('arg', a:argopt)
call opt.update('default', a:defaultopt)
else
let opt.recipe_add = {}
let opt.recipe_delete = {}
let opt.of = function('s:of_for_' . a:kind)
let opt.filter = s:default_values[a:kind]['filter']
call opt.update('arg', a:argopt)
endif
return opt
endfunction
"}}}
function! sandwich#opt#defaults(kind, ...) abort "{{{
if a:kind ==# 'auto' || a:kind ==# 'query'
return deepcopy(s:default_values['textobj'][a:kind])
elseif a:kind ==# 'add' || a:kind ==# 'delete' || a:kind ==# 'replace'
let motionwise = get(a:000, 0, 'char')
return deepcopy(s:default_values[a:kind][motionwise])
else
return {}
endif
endfunction
"}}}
" opt "{{{
let s:opt = {
\ 'default' : {},
\ 'arg' : {},
\ 'filter' : '',
\ }
"}}}
function! s:opt.clear(target) dict abort "{{{
call filter(self[a:target], 0)
endfunction
"}}}
function! s:opt.update(target, dict) dict abort "{{{
call self.clear(a:target)
call extend(self[a:target], filter(deepcopy(a:dict), self.filter), 'force')
endfunction
"}}}
function! s:opt.has(opt_name) dict abort "{{{
return has_key(self.default, a:opt_name)
endfunction
"}}}
function! s:opt.max(opt_name, ...) dict abort "{{{
let minimum = get(a:000, 0, 0)
let list = []
if has_key(self['recipe_delete'], a:opt_name)
let list += [self['recipe_delete'][a:opt_name]]
endif
if has_key(self['recipe_add'], a:opt_name)
let list += [self['recipe_add'][a:opt_name]]
endif
if list == []
let list += [self._of(a:opt_name)]
endif
return max([minimum] + list)
endfunction
"}}}
function! s:opt.get(opt_name, kind, ...) dict abort "{{{
return self.has(a:opt_name) ? self.of(a:opt_name, a:kind) : get(a:000, 0, 0)
endfunction
"}}}
function! s:opt._of(opt_name, ...) dict abort "{{{
let kind = get(a:000, 0, '')
if kind !=# '' && has_key(self[kind], a:opt_name)
return self[kind][a:opt_name]
elseif has_key(self['arg'], a:opt_name)
return self['arg'][a:opt_name]
else
return self['default'][a:opt_name]
endif
endfunction
"}}}
function! s:of_for_add(opt_name, ...) dict abort "{{{
return self._of(a:opt_name, 'recipe_add')
endfunction
"}}}
function! s:of_for_delete(opt_name, ...) dict abort "{{{
return self._of(a:opt_name, 'recipe_delete')
endfunction
"}}}
function! s:of_for_replace(opt_name, ...) dict abort "{{{
let kind = get(a:000, 0, '')
if kind !=# ''
return self._of(a:opt_name, kind)
else
if a:opt_name ==# 'cursor'
" recipe_add > recipe_delete > arg > default
if has_key(self.recipe_add, a:opt_name)
return self.recipe_add[a:opt_name]
else
return self._of(a:opt_name, 'recipe_delete')
endif
elseif a:opt_name ==# 'query_once'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'regex'
return self._of(a:opt_name, 'recipe_delete')
elseif a:opt_name ==# 'expr'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'listexpr'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'noremap'
return self._of(a:opt_name, '')
elseif a:opt_name ==# 'skip_space'
return self._of(a:opt_name, 'recipe_delete')
elseif a:opt_name ==# 'skip_char'
return self._of(a:opt_name, 'recipe_delete')
elseif a:opt_name ==# 'highlight'
return self._of(a:opt_name, '')
elseif a:opt_name ==# 'hi_duration'
return self._of(a:opt_name, '')
elseif a:opt_name ==# 'command'
let commands = []
let commands += get(self.recipe_delete, a:opt_name, [])
let commands += get(self.recipe_add, a:opt_name, [])
if commands == []
let commands += self._of(a:opt_name)
endif
return commands
elseif a:opt_name ==# 'linewise'
return self.max(a:opt_name)
elseif a:opt_name ==# 'autoindent'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'indentkeys'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'indentkeys+'
return self._of(a:opt_name, 'recipe_add')
elseif a:opt_name ==# 'indentkeys-'
return self._of(a:opt_name, 'recipe_add')
else
" should not reach here!
throw 'OperatorSandwichError:Replace:InvalidOption:' . a:opt_name
endif
endif
endfunction
"}}}
function! s:of_for_textobj(opt_name) dict abort "{{{
if has_key(self['recipe'], a:opt_name)
return self['recipe'][a:opt_name]
elseif has_key(self['arg'], a:opt_name)
return self['arg'][a:opt_name]
else
return self['default'][a:opt_name]
endif
endfunction
"}}}
" default values "{{{
let s:default_values = {}
let s:default_values.add = {}
let s:default_values.add.char = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.add.line = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 1,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 1,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.add.block = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 1,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.add.filter = printf('v:key =~# ''\%%(%s\)''', join(keys(s:default_values['add']['char']), '\|'))
let s:default_values.delete = {}
let s:default_values.delete.char = {
\ 'cursor' : 'default',
\ 'noremap' : 1,
\ 'regex' : 0,
\ 'skip_space' : 1,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ }
let s:default_values.delete.line = {
\ 'cursor' : 'default',
\ 'noremap' : 1,
\ 'regex' : 0,
\ 'skip_space' : 2,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 1,
\ }
let s:default_values.delete.block = {
\ 'cursor' : 'default',
\ 'noremap' : 1,
\ 'regex' : 0,
\ 'skip_space' : 1,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ }
let s:default_values.delete.filter = printf('v:key =~# ''\%%(%s\)''', join(keys(s:default_values['delete']['char']), '\|'))
let s:default_values.replace = {}
let s:default_values.replace.char = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'regex' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 1,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.replace.line = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'regex' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 2,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.replace.block = {
\ 'cursor' : 'default',
\ 'query_once' : 0,
\ 'regex' : 0,
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'noremap' : 1,
\ 'skip_space' : 1,
\ 'skip_char' : 0,
\ 'highlight' : 3,
\ 'hi_duration': 200,
\ 'command' : [],
\ 'linewise' : 0,
\ 'autoindent' : -1,
\ 'indentkeys' : 0,
\ 'indentkeys+': 0,
\ 'indentkeys-': 0,
\ }
let s:default_values.replace.filter = printf('v:key =~# ''\%%(%s\)''', join(keys(s:default_values['replace']['char']), '\|'))
let s:default_values.textobj = {}
let s:default_values.textobj.auto = {
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'regex' : 0,
\ 'skip_regex' : [],
\ 'skip_regex_head': [],
\ 'skip_regex_tail': [],
\ 'quoteescape' : 0,
\ 'expand_range' : -1,
\ 'nesting' : 0,
\ 'synchro' : 1,
\ 'noremap' : 1,
\ 'syntax' : [],
\ 'inner_syntax' : [],
\ 'match_syntax' : 0,
\ 'skip_break' : 0,
\ 'skip_expr' : [],
\ }
let s:default_values.textobj.query = {
\ 'expr' : 0,
\ 'listexpr' : 0,
\ 'regex' : 0,
\ 'skip_regex' : [],
\ 'skip_regex_head': [],
\ 'skip_regex_tail': [],
\ 'quoteescape' : 0,
\ 'expand_range' : -1,
\ 'nesting' : 0,
\ 'synchro' : 1,
\ 'noremap' : 1,
\ 'syntax' : [],
\ 'inner_syntax' : [],
\ 'match_syntax' : 0,
\ 'skip_break' : 0,
\ 'skip_expr' : [],
\ }
let s:default_values.textobj.filter = printf('v:key =~# ''\%%(%s\)''', join(keys(s:default_values.textobj.auto), '\|'))
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,41 @@
function! sandwich#util#echo(messages) abort "{{{
echo ''
for [mes, hi_group] in a:messages
execute 'echohl ' . hi_group
echon mes
echohl NONE
endfor
endfunction
"}}}
function! sandwich#util#addlocal(recipes) abort "{{{
return s:extend_local(a:recipes)
endfunction
"}}}
function! sandwich#util#insertlocal(recipes) abort "{{{
return s:extend_local(a:recipes, 0)
endfunction
"}}}
function! sandwich#util#ftrevert(filetype) abort "{{{
if exists('b:sandwich_recipes')
call filter(b:sandwich_recipes, 'get(v:val, "__filetype__", "") !=# a:filetype')
endif
endfunction
"}}}
function! s:extend_local(recipes, ...) abort "{{{
if !exists('b:sandwich_recipes')
if exists('g:sandwich#recipes')
let b:sandwich_recipes = deepcopy(g:sandwich#recipes)
else
let b:sandwich_recipes = deepcopy(g:sandwich#default_recipes)
endif
endif
let i = get(a:000, 0, len(b:sandwich_recipes))
call extend(b:sandwich_recipes, copy(a:recipes), i)
return b:sandwich_recipes
endfunction "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,204 @@
" textobj-sandwich: pick up a sandwich!
function! textobj#sandwich#auto(mode, a_or_i, ...) abort "{{{
let kind = 'auto'
let defaultopt = s:default_options(kind)
let argopt = get(a:000, 0, {})
let opt = sandwich#opt#new(kind, defaultopt, argopt)
if a:0 >= 2
let recipes = textobj#sandwich#recipes#new(kind, a:mode, a:2)
else
let recipes = textobj#sandwich#recipes#new(kind, a:mode)
endif
call recipes.uniq(opt)
if recipes.integrated != []
let g:textobj#sandwich#object = textobj#sandwich#textobj#new(
\ kind, a:a_or_i, a:mode, v:count1, recipes, opt)
let cmd = ":\<C-u>call textobj#sandwich#select()\<CR>"
else
let cmd = a:mode ==# 'o' ? "\<Esc>" : "\<Ignore>"
endif
return cmd
endfunction
"}}}
function! textobj#sandwich#query(mode, a_or_i, ...) abort "{{{
let kind = 'query'
let defaultopt = s:default_options(kind)
let argopt = get(a:000, 0, {})
let opt = sandwich#opt#new(kind, defaultopt, argopt)
if a:0 >= 2
let recipes = textobj#sandwich#recipes#new(kind, a:mode, a:2)
else
let recipes = textobj#sandwich#recipes#new(kind, a:mode)
endif
let timeout = s:get_sandwich_option('timeout', &timeout)
let timeoutlen = max([0, s:get_sandwich_option('timeoutlen', &timeoutlen)])
call recipes.query(opt, timeout, timeoutlen)
if recipes.integrated != []
let g:textobj#sandwich#object = textobj#sandwich#textobj#new(
\ kind, a:a_or_i, a:mode, v:count1, recipes, opt)
let cmd = ":\<C-u>call textobj#sandwich#select()\<CR>"
else
let cmd = a:mode ==# 'o' ? "\<Esc>" : "\<Ignore>"
endif
return cmd
endfunction
"}}}
function! textobj#sandwich#select() abort "{{{
if !exists('g:textobj#sandwich#object')
return
endif
let view = winsaveview()
let textobj = g:textobj#sandwich#object
call textobj.initialize()
let stimeoutlen = max([0, s:get_textobj_option('stimeoutlen', 500)])
let [virtualedit, whichwrap] = [&virtualedit, &whichwrap]
let [&virtualedit, &whichwrap] = ['onemore', 'h,l']
try
let candidates = textobj.list(stimeoutlen)
let elected = textobj.elect(candidates)
call winrestview(view)
call textobj.select(elected)
finally
let mark_latestjump = s:get_textobj_option('latest_jump', 1)
call textobj.finalize(mark_latestjump)
if !textobj.done
call winrestview(view)
endif
let [&virtualedit, &whichwrap] = [virtualedit, whichwrap]
endtry
let g:textobj#sandwich#object = textobj " This is required in case that textobj-sandwich call textobj-sandwich itself in its recipe.
endfunction
"}}}
function! s:get_sandwich_option(name, default) abort "{{{
if exists('g:textobj#sandwich#' . a:name)
return eval('g:textobj#sandwich#' . a:name)
endif
if exists('g:sandwich#' . a:name)
return eval('g:sandwich#' . a:name)
endif
return a:default
endfunction
"}}}
function! s:get_textobj_option(name, default) abort "{{{
return get(g:, 'textobj#sandwich#' . a:name, a:default)
endfunction
"}}}
" recipe "{{{
function! textobj#sandwich#get_recipes() abort "{{{
if exists('b:textobj_sandwich_recipes')
let recipes = b:textobj_sandwich_recipes
elseif exists('g:textobj#sandwich#recipes')
let recipes = g:textobj#sandwich#recipes
else
let recipes = g:textobj#sandwich#default_recipes
endif
return deepcopy(recipes)
endfunction
"}}}
if exists('g:textobj#sandwich#default_recipes')
unlockvar! g:textobj#sandwich#default_recipes
endif
let g:textobj#sandwich#default_recipes = []
lockvar! g:textobj#sandwich#default_recipes
"}}}
" option "{{{
function! s:default_options(kind) abort "{{{
return get(b:, 'textobj_sandwich_options', g:textobj#sandwich#options)[a:kind]
endfunction
"}}}
function! s:initialize_options(...) abort "{{{
let manner = a:0 ? a:1 : 'keep'
let g:textobj#sandwich#options = s:get_textobj_option('options', {})
for kind in ['auto', 'query']
if !has_key(g:textobj#sandwich#options, kind)
let g:textobj#sandwich#options[kind] = {}
endif
call extend(g:textobj#sandwich#options[kind],
\ sandwich#opt#defaults(kind), manner)
endfor
endfunction
call s:initialize_options()
"}}}
function! textobj#sandwich#set_default() abort "{{{
call s:initialize_options('force')
endfunction
"}}}
function! textobj#sandwich#set(kind, option, value) abort "{{{
if s:argument_error(a:kind, a:option, a:value)
return
endif
if a:kind ==# 'all'
let kinds = ['auto', 'query']
else
let kinds = [a:kind]
endif
for kind in kinds
let g:textobj#sandwich#options[kind][a:option] = a:value
endfor
endfunction
"}}}
function! textobj#sandwich#setlocal(kind, option, value) abort "{{{
if s:argument_error(a:kind, a:option, a:value)
return
endif
if !exists('b:textobj_sandwich_options')
let b:textobj_sandwich_options = deepcopy(g:textobj#sandwich#options)
endif
if a:kind ==# 'all'
let kinds = ['auto', 'query']
else
let kinds = [a:kind]
endif
for kind in kinds
let b:textobj_sandwich_options[kind][a:option] = a:value
endfor
endfunction
"}}}
function! s:argument_error(kind, option, value) abort "{{{
if !(a:kind ==# 'auto' || a:kind ==# 'query' || a:kind ==# 'all')
echohl WarningMsg
echomsg 'Invalid kind "' . a:kind . '".'
echohl NONE
return 1
endif
" NOTE: Regaardless of a:kind, keys(defaults) is fixed since
" the two textobjects have same kinds of options.
let defaults = sandwich#opt#defaults('auto')
if filter(keys(defaults), 'v:val ==# a:option') == []
echohl WarningMsg
echomsg 'Invalid option name "' . a:kind . '".'
echohl NONE
return 1
endif
if type(a:value) != type(defaults[a:option])
echohl WarningMsg
echomsg 'Invalid type of value. ' . string(a:value)
echohl NONE
return 1
endif
return 0
endfunction
"}}}
"}}}
unlet! g:textobj#sandwich#object
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,128 @@
" common functions
" variables "{{{
" null valiables
let s:null_coord = [0, 0]
" patchs
if v:version > 704 || (v:version == 704 && has('patch237'))
let s:has_patch_7_4_358 = has('patch-7.4.358')
else
let s:has_patch_7_4_358 = v:version == 704 && has('patch358')
endif
"}}}
function! s:c2p(coord) abort "{{{
return [0] + a:coord + [0]
endfunction
"}}}
function! s:escape(string) abort "{{{
return escape(a:string, '~"\.^$[]*')
endfunction
"}}}
" function! s:sort(list, func, count) abort "{{{
if s:has_patch_7_4_358
function! s:sort(list, func, count) abort
return sort(a:list, a:func)
endfunction
else
function! s:sort(list, func, count) abort
" NOTE: len(a:list) is always larger than count or same.
" FIXME: The number of item in a:list would not be large, but if there was
" any efficient argorithm, I would rewrite here.
let len = len(a:list)
for i in range(a:count)
if len - 2 >= i
let min = len - 1
for j in range(len - 2, i, -1)
if call(a:func, [a:list[min], a:list[j]]) >= 0
let min = j
endif
endfor
if min > i
call insert(a:list, remove(a:list, min), i)
endif
endif
endfor
return a:list
endfunction
endif
"}}}
function! s:get_displaycoord(coord) abort "{{{
let [lnum, col] = a:coord
if [lnum, col] != s:null_coord
let disp_col = col == 1 ? 1 : strdisplaywidth(getline(lnum)[: col - 2]) + 1
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:get_displaysyntax(coord) abort "{{{
return synIDattr(synIDtrans(synID(a:coord[0], a:coord[1], 1)), 'name')
endfunction
"}}}
function! s:is_matched_syntax(coord, syntaxID) abort "{{{
if a:coord == s:null_coord
return 0
elseif a:syntaxID == []
return 1
else
return s:get_displaysyntax(a:coord) ==? a:syntaxID[0]
endif
endfunction
"}}}
function! s:is_included_syntax(coord, syntaxID) abort "{{{
let synstack = map(synstack(a:coord[0], a:coord[1]),
\ 'synIDattr(synIDtrans(v:val), "name")')
if a:syntaxID == []
return 1
elseif synstack == []
if a:syntaxID == ['']
return 1
else
return 0
endif
else
return filter(map(copy(a:syntaxID), '''\c'' . v:val'), 'match(synstack, v:val) > -1') != []
endif
endfunction
"}}}
function! s:export(namelist) abort "{{{
let module = {}
for name in a:namelist
let module[name] = function('s:' . name)
endfor
return module
endfunction
"}}}
let s:lib = s:export([
\ 'c2p',
\ 'escape',
\ 'sort',
\ 'get_displaycoord',
\ 'set_displaycoord',
\ 'get_displaysyntax',
\ 'is_matched_syntax',
\ 'is_included_syntax',
\ ])
lockvar! s:lib
function! textobj#sandwich#lib#import() abort "{{{
return s:lib
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,278 @@
" recipes object
" variables "{{{
let s:TRUE = 1
let s:FALSE = 0
" types
let s:type_num = type(0)
let s:type_str = type('')
"}}}
function! textobj#sandwich#recipes#new(kind, mode, ...) abort "{{{
let recipes = deepcopy(s:recipes)
if a:0 > 0
let recipes.arg = a:1
let recipes.arg_given = 1
endif
call recipes._integrate(a:kind, a:mode)
return recipes
endfunction
"}}}
" s:recipes "{{{
let s:recipes = {
\ 'arg': [],
\ 'arg_given': 0,
\ 'integrated': [],
\ }
"}}}
function! s:recipes.uniq(opt) dict abort "{{{
let opt_regex = a:opt.of('regex')
let opt_expr = a:opt.of('expr')
let opt_noremap = a:opt.of('noremap')
let recipes = copy(self.integrated)
call filter(self.integrated, 0)
while recipes != []
let recipe = remove(recipes, 0)
call add(self.integrated, recipe)
if has_key(recipe, 'buns')
call filter(recipes, '!s:is_duplicated_buns(v:val, recipe, opt_regex, opt_expr)')
elseif has_key(recipe, 'external')
call filter(recipes, '!s:is_duplicated_external(v:val, recipe, opt_noremap)')
endif
endwhile
endfunction
"}}}
function! s:recipes.query(opt, timeout, timeoutlen) dict abort "{{{
let recipes = deepcopy(self.integrated)
let clock = sandwich#clock#new()
let input = ''
let last_compl_match = ['', []]
while 1
let c = getchar(0)
if empty(c)
if clock.started && a:timeout && a:timeoutlen > 0 && clock.elapsed() > a: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, a:opt, 0)'))
" check complete match
let n_comp = len(filter(copy(recipes), 's:is_input_matched(v:val, input, a: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
" FIXME: Additional keypress, 'c', is ignored now, but it should be pressed.
" The problem is feedkeys() cannot be used for here...
break
endif
endif
if recipes == [] | break | endif
endwhile
call clock.stop()
if filter(recipes, 's:is_input_matched(v:val, input, a:opt, 1)') != []
let recipe = recipes[0]
if has_key(recipe, 'buns') || has_key(recipe, 'external')
let self.integrated = [recipe]
else
let self.integrated = []
endif
else
if s:is_input_fallback(input)
let c = split(input, '\zs')[0]
let recipe = {'buns': [c, c], 'expr': 0, 'regex': 0}
let self.integrated = [recipe]
else
let self.integrated = []
endif
endif
endfunction
"}}}
function! s:recipes._integrate(kind, 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 += textobj#sandwich#get_recipes()
endif
let filter = 's:has_filetype(v:val)
\ && s:has_kind(v:val, a:kind)
\ && s:has_mode(v:val, a:mode)
\ && s:has_action(v:val)
\ && s:expr_filter(v:val)'
call filter(self.integrated, filter)
call reverse(self.integrated)
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 ==# "textobj" || v:val ==# "all"'
return filter(copy(a:candidate['kind']), 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) abort "{{{
if !has_key(a:candidate, 'action')
return 1
endif
let filter = 'v:val ==# "delete" || 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
"}}}
function! s:is_duplicated_buns(item, ref, opt_regex, opt_expr) abort "{{{
if has_key(a:item, 'buns')
\ && type(a:ref['buns'][0]) == s:type_str
\ && type(a:ref['buns'][1]) == s:type_str
\ && type(a:item['buns'][0]) == s:type_str
\ && type(a:item['buns'][1]) == s:type_str
\ && a:ref['buns'][0] ==# a:item['buns'][0]
\ && a:ref['buns'][1] ==# a:item['buns'][1]
let regex_r = get(a:ref, 'regex', a:opt_regex)
let regex_i = get(a:item, 'regex', a:opt_regex)
let expr_r = get(a:ref, 'expr', a:opt_expr)
let expr_i = get(a:item, 'expr', a:opt_expr)
let expr_r = expr_r ? 1 : 0
let expr_i = expr_i ? 1 : 0
if regex_r == regex_i && expr_r == expr_i
return 1
endif
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_input_matched(candidate, input, opt, flag) abort "{{{
let has_buns = has_key(a:candidate, 'buns')
let has_ext = has_key(a:candidate, 'external') && has_key(a:candidate, 'input')
if !(has_buns || has_ext)
return 0
elseif !a:flag && a:input ==# ''
return 1
endif
let candidate = deepcopy(a:candidate)
if has_buns
call a:opt.update('recipe', candidate)
" 'input' is necessary for 'expr' or 'regex' buns
if (a:opt.of('expr') || a:opt.of('regex')) && !has_key(candidate, 'input')
return 0
endif
let inputs = copy(get(candidate, 'input', candidate['buns']))
elseif has_ext
" 'input' is necessary for 'external' textobjects assignment
if !has_key(candidate, 'input')
return 0
endif
let inputs = copy(a:candidate['input'])
endif
" If a:flag == 0, check forward match. Otherwise, check complete match.
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: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 "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,344 @@
" sandwich object - manage recipe and positions of a sandwiched text
" variables "{{{
" null valiables
let s:null_coord = [0, 0]
let s:null_4coord = {
\ 'head': copy(s:null_coord),
\ 'tail': copy(s:null_coord),
\ 'inner_head': copy(s:null_coord),
\ 'inner_tail': copy(s:null_coord),
\ }
" types
let s:type_num = type(0)
let s:type_str = type('')
let s:type_list = type([])
let s:type_fref = type(function('tr'))
" common functions
let s:lib = textobj#sandwich#lib#import()
"}}}
function! textobj#sandwich#sandwich#new(recipe, opt) abort "{{{
let sandwich = deepcopy(s:sandwich)
let sandwich.opt = copy(a:opt)
let sandwich.opt.recipe = {}
call sandwich.opt.update('recipe', a:recipe)
if has_key(a:recipe, 'buns')
let sandwich.searchby = 'buns'
unlet sandwich.dough
let sandwich.dough = deepcopy(a:recipe.buns)
elseif has_key(a:recipe, 'external')
let sandwich.searchby = 'external'
let sandwich.external = deepcopy(a:recipe.external)
else
return {}
endif
let sandwich.recipe = a:recipe
return sandwich
endfunction
"}}}
" coord object"{{{
let s:coord = deepcopy(s:null_4coord)
function! s:coord.initialize() dict abort "{{{
let self.head = deepcopy(s:null_coord)
let self.tail = deepcopy(s:null_coord)
endfunction
"}}}
function! s:coord.get_inner(buns, skip_break) dict abort "{{{
call cursor(self.head)
call search(a:buns[0], 'ce', self.tail[0])
normal! l
if a:skip_break && col('.') == col([line('.'), '$'])
let self.inner_head = searchpos('\_S', '', self.tail[0])
else
let self.inner_head = getpos('.')[1:2]
endif
call cursor(self.tail)
call search(a:buns[1], 'bc', self.head[0])
if a:skip_break && (col('.') < 2 || getline(line('.'))[: col('.')-2] =~# '^\s*$')
let self.inner_tail = searchpos('\_S', 'be', self.head[0])
else
if getpos('.')[2] == 1
normal! hl
else
normal! h
endif
let self.inner_tail = getpos('.')[1:2]
endif
endfunction
"}}}
function! s:coord.next() dict abort "{{{
call cursor(self.head)
normal! h
let self.head = getpos('.')[1:2]
endfunction
"}}}
"}}}
" range object"{{{
let s:range = {
\ 'valid' : 0,
\ 'top' : 0,
\ 'bottom' : 0,
\ 'toplim' : 0,
\ 'botlim' : 0,
\ 'stride' : &lines,
\ 'count' : 1,
\ }
function! s:range.initialize(cursor, expand_range) dict abort "{{{
let filehead = 1
let fileend = line('$')
let self.valid = 1
let self.top = a:cursor[0]
let self.bottom = a:cursor[0]
if a:expand_range >= 0
let self.toplim = max([filehead, a:cursor[0] - a:expand_range])
let self.botlim = min([a:cursor[0] + a:expand_range, fileend])
else
let self.toplim = filehead
let self.botlim = fileend
endif
let self.count = 1
endfunction
"}}}
function! s:range.next() dict abort "{{{
if (self.top == 1/0 && self.bottom < 1)
\ || (self.top <= self.toplim && self.bottom >= self.botlim)
let self.top = 1/0
let self.bottom = 0
let self.valid = 0
else
if self.top > self.toplim
let self.top = self.top - self.stride
let self.top = self.top < self.toplim ? self.toplim : self.top
endif
if self.bottom < self.botlim
let self.bottom = self.bottom + self.stride
let self.bottom = self.bottom > self.botlim ? self.botlim : self.bottom
endif
endif
endfunction
"}}}
"}}}
" s:sandwich "{{{
let s:sandwich = {
\ 'buns' : [],
\ 'dough' : [],
\ 'external' : [],
\ 'searchby' : '',
\ 'recipe' : {},
\ 'cursor' : copy(s:null_coord),
\ 'coord' : deepcopy(s:coord),
\ 'range' : deepcopy(s:range),
\ 'visualmode': '',
\ 'syntax_on' : 1,
\ 'syntax' : [],
\ 'opt' : {},
\ 'escaped' : 0,
\ 'not_escaped': [],
\ }
"}}}
function! s:sandwich.initialize(cursor, is_syntax_on) dict abort "{{{
let self.syntax_on = a:is_syntax_on
let self.cursor = a:cursor
call self.coord.initialize()
call self.range.initialize(a:cursor, self.opt.of('expand_range'))
return self
endfunction
"}}}
function! s:sandwich.bake_buns(state, clock) dict abort "{{{
let opt_listexpr = self.opt.of('listexpr')
let opt_expr = self.opt.of('expr')
let opt_regex = self.opt.of('regex')
call a:clock.pause()
if a:state
if opt_listexpr
let self.buns = eval(self.dough)
elseif opt_expr
let self.buns = s:evalexpr(self.dough)
else
let self.buns = self.dough
endif
else
if opt_listexpr >= 2
let self.buns = eval(self.dough)
let self.escaped = 0
elseif opt_expr >= 2
let self.buns = s:evalexpr(self.dough)
let self.escaped = 0
endif
endif
call a:clock.start()
if !s:valid_buns(self.buns)
return ['', '']
endif
if !self.escaped
let self.not_escaped = copy(self.buns)
if !opt_regex
call map(self.buns, 's:lib.escape(v:val)')
let self.escaped = 1
endif
endif
return self.buns
endfunction
"}}}
function! s:sandwich.searchpair_head(stimeoutlen) dict abort "{{{
let tailpos = searchpos(self.buns[1], 'cn', self.range.bottom, a:stimeoutlen)
let flag = tailpos == getpos('.')[1:2] ? 'b' : 'bc'
let stopline = self.range.top
return searchpairpos(self.buns[0], '', self.buns[1], flag, 'self.skip(1)', stopline, a:stimeoutlen)
endfunction
"}}}
function! s:sandwich.searchpair_tail(stimeoutlen) dict abort "{{{
let stopline = self.range.bottom
return searchpairpos(self.buns[0], '', self.buns[1], '', 'self.skip(0)', stopline, a:stimeoutlen)
endfunction
"}}}
function! s:sandwich.search_head(flag, stimeoutlen) dict abort "{{{
return self._searchpos(self.buns[0], a:flag, 1, self.range.top, a:stimeoutlen)
endfunction
"}}}
function! s:sandwich.search_tail(flag, stimeoutlen) dict abort "{{{
return self._searchpos(self.buns[1], a:flag, 0, self.range.bottom, a:stimeoutlen)
endfunction
"}}}
function! s:sandwich._searchpos(pattern, flag, is_head, stopline, stimeoutlen) dict abort "{{{
let coord = searchpos(a:pattern, a:flag, a:stopline, a:stimeoutlen)
let flag = substitute(a:flag, '\m\Cc', '', 'g')
while coord != s:null_coord && self.skip(a:is_head, coord)
let coord = searchpos(a:pattern, flag, a:stopline, a:stimeoutlen)
endwhile
return coord
endfunction
"}}}
function! s:sandwich.skip(is_head, ...) dict abort "{{{
" NOTE: for sandwich.searchpairpos()/sandwich.searchpos() functions.
let opt = self.opt
let checkpos = get(a:000, 0, getpos('.')[1:2])
if checkpos == s:null_coord
return 1
endif
" quoteescape option
let skip_patterns = []
if opt.of('quoteescape') && &quoteescape !=# ''
for c in split(&quoteescape, '\zs')
let c = s:lib.escape(c)
let pattern = printf('[^%s]%s\%%(%s%s\)*\zs\%%#', c, c, c, c)
let skip_patterns += [pattern]
endfor
endif
" skip_regex option
let skip_patterns += opt.of('skip_regex')
let skip_patterns += a:is_head ? opt.of('skip_regex_head') : opt.of('skip_regex_tail')
if skip_patterns != []
call cursor(checkpos)
for pattern in skip_patterns
let skip = searchpos(pattern, 'cn', checkpos[0]) == checkpos
if skip
return 1
endif
endfor
endif
" syntax, match_syntax option
if self.syntax_on
if a:is_head || !opt.of('match_syntax')
let opt_syntax = opt.of('syntax')
if opt_syntax != [] && !s:lib.is_included_syntax(checkpos, opt_syntax)
return 1
endif
else
if !s:lib.is_matched_syntax(checkpos, self.syntax)
return 1
endif
endif
endif
" skip_expr option
for Expr in opt.of('skip_expr')
if s:eval(Expr, a:is_head, s:lib.c2p(checkpos))
return 1
endif
endfor
return 0
endfunction
"}}}
function! s:sandwich.check_syntax(coord) dict abort "{{{
if !self.syntax_on || !self.opt.of('match_syntax')
return
endif
let synitem = s:lib.get_displaysyntax(a:coord)
if synitem !=# ''
let self.syntax = [synitem]
endif
endfunction
"}}}
function! s:sandwich.export_recipe() dict abort "{{{
" For the cooperation with operator-sandwich
" NOTE: After visual selection by a user-defined textobject, v:operator is set as ':'
" NOTE: 'synchro' option is not valid for visual mode, because there is no guarantee that g:operator#sandwich#object exists.
if self.opt.of('synchro') && exists('g:operator#sandwich#object')
\ && ((self.searchby ==# 'buns' && v:operator ==# 'g@') || (self.searchby ==# 'external' && v:operator =~# '\%(:\|g@\)'))
\ && &operatorfunc =~# '^operator#sandwich#\%(delete\|replace\)'
let recipe = self.recipe
if self.searchby ==# 'buns'
call extend(recipe, {'buns': self.not_escaped}, 'force')
call extend(recipe, {'expr': 0}, 'force')
call extend(recipe, {'listexpr': 0}, 'force')
elseif self.searchby ==# 'external'
let excursus = {
\ 'count' : self.range.count,
\ 'cursor': s:lib.c2p(self.cursor),
\ 'coord' : self.coord,
\ }
call extend(recipe, {'excursus': excursus}, 'force')
endif
call extend(recipe, {'action': ['delete']}, 'force')
else
let recipe = {}
endif
return recipe
endfunction
"}}}
function! s:evalexpr(dough) abort "{{{
let buns = []
let buns += [eval(a:dough[0])]
if buns[0] !=# ''
let buns += [eval(a:dough[1])]
endif
return buns
endfunction
"}}}
function! s:valid_buns(buns) abort "{{{
return type(a:buns) == s:type_list && len(a:buns) >= 2 && s:check_a_bun(a:buns[0]) && s:check_a_bun(a:buns[1])
endfunction
"}}}
function! s:check_a_bun(bun) abort "{{{
let type_bun = type(a:bun)
return (type_bun ==# s:type_str && a:bun !=# '') || type_bun ==# s:type_num
endfunction
"}}}
function! s:eval(expr, ...) abort "{{{
return type(a:expr) == s:type_fref ? call(a:expr, a:000) : eval(a:expr)
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,592 @@
" textobj object - search & select a sandwiched text
" variables "{{{
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
nnoremap <SID>(V) V
nnoremap <SID>(CTRL-v) <C-v>
let s:KEY_v = printf('%s(v)', s:SNR)
let s:KEY_V = printf('%s(V)', s:SNR)
let s:KEY_CTRL_v = printf('%s(CTRL-v)', s:SNR)
" null valiables
let s:null_coord = [0, 0]
" common functions
let s:lib = textobj#sandwich#lib#import()
"}}}
function! textobj#sandwich#textobj#new(kind, a_or_i, mode, count, recipes, opt) abort "{{{
let textobj = deepcopy(s:textobj)
let textobj.kind = a:kind
let textobj.a_or_i = a:a_or_i
let textobj.mode = a:mode
let textobj.count = a:count
let textobj.recipes = a:recipes
let textobj.opt = a:opt
" NOTE: The cursor position should be recorded in textobj#sandwich#auto()/textobj#sandwich#query().
" It is impossible to get it in textobj#sandwich#select() if in visual mode.
let textobj.cursor = getpos('.')[1:2]
return textobj
endfunction
"}}}
" s:textobj "{{{
let s:textobj = {
\ 'state' : 1,
\ 'kind' : '',
\ 'a_or_i' : 'i',
\ 'mode' : '',
\ 'count' : 0,
\ 'cursor' : copy(s:null_coord),
\ 'view' : {},
\ 'recipes': {
\ 'arg' : [],
\ 'arg_given' : 0,
\ 'integrated': [],
\ },
\ 'visual': {
\ 'mode': '',
\ 'head': copy(s:null_coord),
\ 'tail': copy(s:null_coord),
\ },
\ 'basket': [],
\ 'opt' : {},
\ 'done' : 0,
\ 'clock' : sandwich#clock#new(),
\ }
"}}}
function! s:textobj.initialize() dict abort "{{{
let self.done = 0
let self.count = !self.state && self.count != v:count1 ? v:count1 : self.count
if self.state
let self.basket = map(copy(self.recipes.integrated), 'textobj#sandwich#sandwich#new(v:val, self.opt)')
call filter(self.basket, 'v:val != {}')
else
let self.cursor = getpos('.')[1:2]
endif
if self.mode ==# 'x'
let self.visual.mode = visualmode() ==# "\<C-v>" ? "\<C-v>" : 'v'
let self.visual.head = getpos("'<")[1:2]
let self.visual.tail = getpos("'>")[1:2]
else
let self.visual.mode = 'v'
endif
let is_syntax_on = exists('g:syntax_on') || exists('g:syntax_manual')
call map(self.basket, 'v:val.initialize(self.cursor, is_syntax_on)')
endfunction
"}}}
function! s:textobj.list(stimeoutlen) dict abort "{{{
let clock = self.clock
" gather candidates
let candidates = []
call clock.stop()
call clock.start()
while filter(copy(self.basket), 'v:val.range.valid') != []
for sandwich in self.basket
let candidates += self.search(sandwich, a:stimeoutlen)
endfor
call s:uniq_candidates(candidates, self.a_or_i)
if len(candidates) >= self.count
break
endif
" time out
if clock.started && a:stimeoutlen > 0
let elapsed = clock.elapsed()
if elapsed > a:stimeoutlen
echo 'textobj-sandwich: Timed out.'
break
endif
endif
endwhile
call clock.stop()
return candidates
endfunction
"}}}
function! s:textobj.search(sandwich, stimeoutlen) dict abort "{{{
if a:sandwich.searchby ==# 'buns'
if a:sandwich.opt.of('nesting')
let candidates = self._search_with_nest(a:sandwich, a:stimeoutlen)
else
let candidates = self._search_without_nest(a:sandwich, a:stimeoutlen)
endif
elseif a:sandwich.searchby ==# 'external'
let candidates = self._get_region(a:sandwich, a:stimeoutlen)
else
let candidates = []
endif
return candidates
endfunction
"}}}
function! s:textobj._search_with_nest(sandwich, stimeoutlen) dict abort "{{{
let buns = a:sandwich.bake_buns(self.state, self.clock)
let range = a:sandwich.range
let coord = a:sandwich.coord
let opt = a:sandwich.opt
let candidates = []
if buns[0] ==# '' || buns[1] ==# ''
let range.valid = 0
endif
if !range.valid | return candidates | endif
" check whether the cursor is on the buns[1] or not
call cursor(self.cursor)
let _head = searchpos(buns[1], 'bcW', range.top, a:stimeoutlen)
let _tail = searchpos(buns[1], 'cenW', range.bottom, a:stimeoutlen)
if _head != s:null_coord && _tail != s:null_coord && s:is_in_between(self.cursor, _head, _tail)
call cursor(_head)
else
call cursor(self.cursor)
endif
while 1
" search head
let head = a:sandwich.searchpair_head(a:stimeoutlen)
if head == s:null_coord | break | endif
let coord.head = head
call a:sandwich.check_syntax(head)
" search tail
let tail = a:sandwich.searchpair_tail(a:stimeoutlen)
if tail == s:null_coord | break | endif
let tail = searchpos(buns[1], 'ce', range.bottom, a:stimeoutlen)
if tail == s:null_coord | break | endif
let coord.tail = tail
" add to candidates
call coord.get_inner(buns, opt.of('skip_break'))
if self.is_valid_candidate(a:sandwich)
let candidate = deepcopy(a:sandwich)
let candidate.visualmode = self.visual.mode
let candidates += [candidate]
endif
if coord.head == [1, 1]
" finish!
let range.valid = 0
break
else
call coord.next()
endif
endwhile
call range.next()
return candidates
endfunction
"}}}
function! s:textobj._search_without_nest(sandwich, stimeoutlen) dict abort "{{{
let buns = a:sandwich.bake_buns(self.state, self.clock)
let range = a:sandwich.range
let coord = a:sandwich.coord
let opt = a:sandwich.opt
let candidates = []
if buns[0] ==# '' || buns[1] ==# ''
let range.valid = 0
endif
if !range.valid | return candidates | endif
" search nearest head
call cursor(self.cursor)
let head = a:sandwich.search_head('bc', a:stimeoutlen)
if head == s:null_coord
call range.next()
return candidates
endif
call a:sandwich.check_syntax(head)
let _tail = searchpos(buns[0], 'ce', range.bottom, a:stimeoutlen)
" search nearest tail
call cursor(self.cursor)
let tail = a:sandwich.search_tail('ce', a:stimeoutlen)
if tail == s:null_coord
call range.next()
return candidates
endif
" If the cursor is right on a bun
if tail == _tail
" check whether it is head or tail
let odd = 1
call cursor([range.top, 1])
let pos = searchpos(buns[0], 'c', range.top, a:stimeoutlen)
while pos != head && pos != s:null_coord
let odd = !odd
let pos = searchpos(buns[0], '', range.top, a:stimeoutlen)
endwhile
if pos == s:null_coord | return candidates | endif
if odd
" pos is head
let head = pos
call a:sandwich.check_syntax(head)
" search tail
call search(buns[0], 'ce', range.bottom, a:stimeoutlen)
let tail = a:sandwich.search_tail('e', a:stimeoutlen)
if tail == s:null_coord
call range.next()
return candidates
endif
else
" pos is tail
call cursor(pos)
let tail = a:sandwich.search_tail('ce', a:stimeoutlen)
call a:sandwich.check_syntax(tail)
" search head
call search(buns[1], 'bc', range.top, a:stimeoutlen)
let head = a:sandwich.search_head('b', a:stimeoutlen)
if head == s:null_coord
call range.next()
return candidates
endif
endif
endif
let coord.head = head
let coord.tail = tail
call coord.get_inner(buns, a:sandwich.opt.of('skip_break'))
if self.is_valid_candidate(a:sandwich)
let candidate = deepcopy(a:sandwich)
let candidate.visualmode = self.visual.mode
let candidates += [candidate]
endif
let range.valid = 0
return candidates
endfunction
"}}}
function! s:textobj._get_region(sandwich, stimeoutlen) dict abort "{{{
" NOTE: Because of the restriction of vim, if a operator to get the assigned
" region is employed for 'external' user-defined textobjects, it makes
" impossible to repeat by dot command. Thus, 'external' is checked by
" using visual selection xmap in any case.
let range = a:sandwich.range
let coord = a:sandwich.coord
let opt = a:sandwich.opt
let candidates = []
if !range.valid | return candidates | endif
if opt.of('noremap')
let cmd = 'normal!'
let v = self.visual.mode
else
let cmd = 'normal'
let v = self.visual.mode ==# 'v' ? s:KEY_v :
\ self.visual.mode ==# 'V' ? s:KEY_V :
\ s:KEY_CTRL_v
endif
if self.mode ==# 'x'
let initpos = [self.visual.head, self.visual.tail]
else
let initpos = [self.cursor, self.cursor]
endif
let selection = &selection
set selection=inclusive
try
while 1
let [prev_head, prev_tail] = [coord.head, coord.tail]
let [prev_inner_head, prev_inner_tail] = [coord.inner_head, coord.inner_tail]
" get outer positions
let [head, tail, visualmode_a] = s:get_textobj_region(initpos, cmd, v, range.count, a:sandwich.external[1])
" get inner positions
if head != s:null_coord && tail != s:null_coord
let [inner_head, inner_tail, visualmode_i] = s:get_textobj_region(initpos, cmd, v, range.count, a:sandwich.external[0])
else
let [inner_head, inner_tail, visualmode_i] = [copy(s:null_coord), copy(s:null_coord), 'v']
endif
if (self.a_or_i ==# 'i' && s:is_valid_region(inner_head, inner_tail, prev_inner_head, prev_inner_tail, range.count))
\ || (self.a_or_i ==# 'a' && s:is_valid_region(head, tail, prev_head, prev_tail, range.count))
if head[0] >= range.top && tail[0] <= range.bottom
let coord.head = head
let coord.tail = tail
let coord.inner_head = inner_head
let coord.inner_tail = inner_tail
if self.is_valid_candidate(a:sandwich)
let candidate = deepcopy(a:sandwich)
let candidate.visualmode = self.a_or_i ==# 'a' ? visualmode_a : visualmode_i
let candidates += [candidate]
endif
else
call range.next()
break
endif
else
let range.valid = 0
break
endif
let range.count += 1
endwhile
finally
" restore visualmode
execute 'normal! ' . self.visual.mode
execute "normal! \<Esc>"
call cursor(self.cursor)
" restore marks
call setpos("'<", s:lib.c2p(self.visual.head))
call setpos("'>", s:lib.c2p(self.visual.tail))
" restore options
let &selection = selection
endtry
return candidates
endfunction
"}}}
function! s:textobj.is_valid_candidate(sandwich) dict abort "{{{
let coord = a:sandwich.coord
if !s:is_in_between(self.cursor, coord.head, coord.tail)
return 0
endif
if self.a_or_i ==# 'i'
let [head, tail] = [coord.inner_head, coord.inner_tail]
else
let [head, tail] = [coord.head, coord.tail]
endif
if head == s:null_coord || tail == s:null_coord || s:is_ahead(head, tail)
return 0
endif
" specific condition in visual mode
if self.mode !=# 'x'
let visual_mode_affair = 1
else
let visual_mode_affair = s:visual_mode_affair(
\ head, tail, self.a_or_i, self.cursor, self.visual)
endif
if !visual_mode_affair
return 0
endif
" specific condition for the option 'matched_syntax' and 'inner_syntax'
let opt_syntax_affair = s:opt_syntax_affair(a:sandwich)
if !opt_syntax_affair
return 0
endif
return 1
endfunction
"}}}
function! s:textobj.elect(candidates) dict abort "{{{
let elected = {}
if len(a:candidates) >= self.count
" election
let cursor = self.cursor
let map_rule = 'extend(v:val, {"len": s:representative_length(v:val.coord, cursor)})'
call map(a:candidates, map_rule)
call s:lib.sort(a:candidates, function('s:compare_buf_length'), self.count)
let elected = a:candidates[self.count - 1]
endif
return elected
endfunction
"}}}
function! s:textobj.select(sandwich) dict abort "{{{
if a:sandwich == {}
if self.mode ==# 'x'
normal! gv
endif
let self.done = 1
return
endif
let head = self.a_or_i ==# 'i' ? a:sandwich.coord.inner_head : a:sandwich.coord.head
let tail = self.a_or_i ==# 'i' ? a:sandwich.coord.inner_tail : a:sandwich.coord.tail
if self.mode ==# 'x' && self.visual.mode ==# "\<C-v>"
" trick for the blockwise visual mode
if self.cursor[0] == self.visual.tail[0]
let disp_coord = s:lib.get_displaycoord(head)
let disp_coord[0] = self.visual.head[0]
call s:lib.set_displaycoord(disp_coord)
let head = getpos('.')[1:2]
elseif self.cursor[0] == self.visual.head[0]
let disp_coord = s:lib.get_displaycoord(tail)
let disp_coord[0] = self.visual.tail[0]
call s:lib.set_displaycoord(disp_coord)
let tail = getpos('.')[1:2]
endif
endif
execute 'normal! ' . a:sandwich.visualmode
call cursor(head)
normal! o
call cursor(tail)
" counter measure for the 'selection' option being 'exclusive'
if &selection ==# 'exclusive'
normal! l
endif
call operator#sandwich#synchronize(self.kind, a:sandwich.export_recipe())
let self.done = 1
endfunction
"}}}
function! s:textobj.finalize(mark_latestjump) dict abort "{{{
if self.done && a:mark_latestjump
call setpos("''", s:lib.c2p(self.cursor))
endif
" flash echoing
if !self.state
echo ''
endif
let self.state = 0
endfunction
"}}}
function! s:is_valid_region(head, tail, prev_head, prev_tail, count) abort "{{{
return a:head != s:null_coord && a:tail != s:null_coord && (a:count == 1 || s:is_ahead(a:prev_head, a:head) || s:is_ahead(a:tail, a:prev_tail))
endfunction
"}}}
function! s:get_textobj_region(initpos, cmd, visualmode, count, key_seq) abort "{{{
call cursor(a:initpos[0])
execute printf('silent! %s %s', a:cmd, a:visualmode)
call cursor(a:initpos[1])
execute printf('silent! %s %d%s', a:cmd, 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("'<")[1:2], getpos("'>")[1:2]]
if head == a:initpos[0] && tail == a:initpos[1]
let [head, tail] = [copy(s:null_coord), copy(s:null_coord)]
elseif visualmode ==# 'V'
let tail[2] = col([tail[1], '$'])
endif
return [head, tail, visualmode]
endfunction
"}}}
function! s:get_buf_length(start, end) abort "{{{
if a:start[0] == a:end[0]
let len = a:end[1] - a:start[1] + 1
else
let len = (line2byte(a:end[0]) + a:end[1]) - (line2byte(a:start[0]) + a:start[1]) + 1
endif
return len
endfunction
"}}}
function! s:uniq_candidates(candidates, a_or_i) abort "{{{
let i = 0
if a:a_or_i ==# 'i'
let filter = 'v:val.coord.inner_head != candidate.coord.inner_head || v:val.coord.inner_tail != candidate.coord.inner_tail'
else
let filter = 'v:val.coord.head == candidate.coord.head || v:val.coord.tail == candidate.coord.tail'
endif
while i+1 < len(a:candidates)
let candidate = a:candidates[i]
call filter(a:candidates[i+1 :], filter)
let i += 1
endwhile
return a:candidates
endfunction
"}}}
function! s:visual_mode_affair(head, tail, a_or_i, cursor, visual) abort "{{{
" a:visual.mode ==# 'V' never comes.
if a:visual.mode ==# 'v'
" character-wise
if a:a_or_i ==# 'i'
let visual_mode_affair = s:is_ahead(a:visual.head, a:head)
\ || s:is_ahead(a:tail, a:visual.tail)
else
let visual_mode_affair = (s:is_ahead(a:visual.head, a:head) && s:is_equal_or_ahead(a:tail, a:visual.tail))
\ || (s:is_equal_or_ahead(a:visual.head, a:head) && s:is_ahead(a:tail, a:visual.tail))
endif
else
" block-wise
let orig_pos = getpos('.')
let visual_head = s:lib.get_displaycoord(a:visual.head)
let visual_tail = s:lib.get_displaycoord(a:visual.tail)
call s:lib.set_displaycoord([a:cursor[0], visual_head[1]])
let thr_head = getpos('.')
call s:lib.set_displaycoord([a:cursor[0], visual_tail[1]])
let thr_tail = getpos('.')
let visual_mode_affair = s:is_ahead(thr_head, a:head)
\ || s:is_ahead(a:tail, thr_tail)
call setpos('.', orig_pos)
endif
return visual_mode_affair
endfunction
"}}}
function! s:opt_syntax_affair(sandwich) abort "{{{
if !a:sandwich.syntax_on
return 1
endif
let coord = a:sandwich.coord
let opt = a:sandwich.opt
if opt.of('match_syntax') == 2
let opt_syntax_affair = s:lib.is_included_syntax(coord.inner_head, a:sandwich.syntax)
\ && s:lib.is_included_syntax(coord.inner_tail, a:sandwich.syntax)
elseif opt.of('match_syntax') == 3
" check inner syntax independently
if opt.of('inner_syntax') == []
let syntax = [s:lib.get_displaysyntax(coord.inner_head)]
let opt_syntax_affair = s:lib.is_included_syntax(coord.inner_tail, syntax)
else
if s:lib.is_included_syntax(coord.inner_head, opt.of('inner_syntax'))
let syntax = [s:lib.get_displaysyntax(coord.inner_head)]
let opt_syntax_affair = s:lib.is_included_syntax(coord.inner_tail, syntax)
else
let opt_syntax_affair = 0
endif
endif
else
if opt.of('inner_syntax') == []
let opt_syntax_affair = 1
else
let opt_syntax_affair = s:lib.is_included_syntax(coord.inner_head, opt.of('inner_syntax'))
\ && s:lib.is_included_syntax(coord.inner_tail, opt.of('inner_syntax'))
endif
endif
return opt_syntax_affair
endfunction
"}}}
function! s:is_in_between(coord, head, tail) abort "{{{
return (a:coord != s:null_coord) && (a:head != s:null_coord) && (a:tail != s:null_coord)
\ && ((a:coord[0] > a:head[0]) || ((a:coord[0] == a:head[0]) && (a:coord[1] >= a:head[1])))
\ && ((a:coord[0] < a:tail[0]) || ((a:coord[0] == a:tail[0]) && (a:coord[1] <= a:tail[1])))
endfunction
"}}}
function! s:is_ahead(coord1, coord2) abort "{{{
return a:coord1[0] > a:coord2[0] || (a:coord1[0] == a:coord2[0] && a:coord1[1] > a:coord2[1])
endfunction
"}}}
function! s:is_equal_or_ahead(coord1, coord2) abort "{{{
return a:coord1[0] > a:coord2[0] || (a:coord1[0] == a:coord2[0] && a:coord1[1] >= a:coord2[1])
endfunction
"}}}
function! s:representative_length(coord, cursor) abort "{{{
let inner_head = a:coord.inner_head
let inner_tail = a:coord.inner_tail
if s:is_in_between(a:cursor, inner_head, inner_tail)
return s:get_buf_length(inner_head, inner_tail)
else
return s:get_buf_length(a:coord.head, a:coord.tail)
endif
endfunction "}}}
function! s:compare_buf_length(i1, i2) abort "{{{
return a:i1.len - a:i2.len
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:
" vim:set ts=2 sts=2 sw=2:

View File

@ -0,0 +1,96 @@
=============================================================================
COMPOUND RECIPES *sandwich-compound-recipes*
A compound recipe is one that contains multiple simple recipes. A simple
recipe could be a pair of parentheses `()` or braces `{}`, while a compound
recipe could be made up of all types of brackets `(),[],{}`, or both types of
quotes `'',""`.
sandwich.vim allows for the creation of compound recipes. This tutorial
demonstrates the creation of two compound recipes, brackets and quotes.
Users can adapt the code provided to customize their own compound recipes.
Compound text-objects~
By default, sandwich.vim can automatically search for a set of surroundings.
This functionality is provided by the `srb` and `sdb` |sandwich-keymappings|.
Under the hood, these mappings call |textobj#sandwich#auto()|.
This function |textobj#sandwich#auto()| also accepts an optional fourth
argument. If a list of (simple) recipes is given to the fourth argument, this
list is used instead.
We create a list of recipes for the brackets >
let g:sandwich_bracket_recipes = [
\ {'buns': ['{', '}'], 'nesting': 1, 'skip_break': 1},
\ {'buns': ['[', ']'], 'nesting': 1},
\ {'buns': ['(', ')'], 'nesting': 1},
\ ]
<
and another for the quotes >
let g:sandwich_quote_recipes = [
\ {'buns': ['"', '"'], 'quoteescape': 1, 'expand_range': 0, 'nesting': 0, 'linewise': 0},
\ {'buns': ["'", "'"], 'quoteescape': 1, 'expand_range': 0, 'nesting': 0, 'linewise': 0},
\ ]
<
Then, we pass these lists of (simple) recipes into |textobj#sandwich#auto()|
to create our text-objects. It is also convenient to access these
text-objects with key mappings. In this tutorial, we assign the mappings `ij,
aj` and `io, ao` to the brackets and quotes text-objects respectively >
onoremap <silent><expr> ij textobj#sandwich#auto('x', 'i', {}, g:sandwich_bracket_recipes)
onoremap <silent><expr> aj textobj#sandwich#auto('x', 'a', {}, g:sandwich_bracket_recipes)
xnoremap <silent><expr> ij textobj#sandwich#auto('x', 'i', {}, g:sandwich_bracket_recipes)
xnoremap <silent><expr> aj textobj#sandwich#auto('x', 'a', {}, g:sandwich_bracket_recipes)
onoremap <silent><expr> io textobj#sandwich#auto('x', 'i', {}, g:sandwich_quote_recipes)
onoremap <silent><expr> ao textobj#sandwich#auto('x', 'a', {}, g:sandwich_quote_recipes)
xnoremap <silent><expr> io textobj#sandwich#auto('x', 'i', {}, g:sandwich_quote_recipes)
xnoremap <silent><expr> ao textobj#sandwich#auto('x', 'a', {}, g:sandwich_quote_recipes)
<
With these, one can visually select the nearest pair of brackets with `vaj`.
In a similar manner, `dio` deletes the text between the two nearest quotes.
Compound recipes~
The next step is to add these text objects as compound recipes, and use them
with sandwich operators such as |<Plug>(operator-sandwich-delete)| and
|<Plug>(operator-sandwich-replace)| (default: `sd` and `sr`).
We define these compound recipes using external requisites (see
|textobj-sandwich-configuration| or |operator-sandwich-configuration|). The
text objects defined above and are passed into the `'external'` item, as seen
below >
let g:sandwich_compound_recipes = [
\ {
\ 'external': ['ij', 'aj'],
\ 'kind' : ['delete', 'replace', 'query'],
\ 'noremap' : 0,
\ 'input' : ['j'],
\ },
\ {
\ 'external': ['io', 'ao'],
\ 'kind' : ['delete', 'replace', 'query'],
\ 'noremap' : 0,
\ 'input' : ['o'],
\ },
\ ]
<
These recipes require an `'input'`, which we specify to be `'j'` and `'o'`
for brackets and quotes respectively.
Finally, we add these compound recipes to |g:sandwich#recipes|,
that is, the list of recipes used by sandwich.vim >
call extend(g:sandwich#recipes, g:sandwich_compound_recipes)
<
With these, one can delete the nearest pair of quotes simply with `sdo`.
Similarly, one can replace the nearest pair of brackets with `srj{char}`.
==============================================================================
vim:tw=78:ts=8:noet:ft=help:norl:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,687 @@
*sandwich.jax* 日本語ヘルプ Last change:14-May-2022.
“挟まれた”テキストを編集するためのオペレータとテキストオブジェクトの詰め合わせ
です。
書いた人 : machakann <mckn{at}outlook.jp>
ライセンス : NYSL license
日本語 <http://www.kmonos.net/nysl/>
English (Unofficial) <http://www.kmonos.net/nysl/index.en.html>
必須要件: Vim 7.4 かそれ以降のバージョン
|+reltime| 機能 (任意)
|+float| 機能 (任意)
==============================================================================
CONTENTS *sandwich-contents*
QUICK START |sandwich-quick-start|
INTRODUCTION |sandwich-introduction|
KEYMAPPINGS |sandwich-keymappings|
CONFIGURATION |sandwich-configuration|
MAGICCHARACTERS |sandwich-magiccharacters|
FILETYPE RECIPES |sandwich-filetype-recipes|
FUNCTIONS |sandwich-functions|
MISCELLANEOUS |sandwich-miscellaneous|
vim-surround のキ-マッピングを使う
magicchar-f の挙動を変更する
==============================================================================
QUICK START *sandwich-quick-start*
*sandwich.vim* は文字列を括弧などで囲む/囲みを外す/囲みを置き換えることを目的
としたオペレータとテキストオブジェクトのセットです。例えば、(foo) や "bar" の
ような文字列が処理の対象になります。
囲む~
sa{motion/textobject}{addition} と、キー入力します。
例えば、 foo という単語にカーソルを合わせて saiw( と入力すると (foo) となりま
す。
囲みを外す~
sdb あるいは sd{deletion} とキー入力します。
例えば、 (foo) というテキストにカーソルを合わせて sdb あるいは sd( と入力する
と foo となります。 sdb という入力は自動的に囲まれたテキストを検索します。
囲みを置き換える~
srb{addition} あるいは sr{deletion}{addition} キー入力します。
例えば、 (foo) というテキストにカーソルを合わせて srb" あるいは sr(" と入力す
ると "foo" となります。
これだけ読めばこのプラグインを使うのに十分でしょう。もし、さらに興味がわいた方
、挙動を細かく変更したい方は続く記述およびオペレータとテキストオブジェクトそれ
ぞれのヘルプ |operator-sandwich| 、 |textobj-sandwich| をご覧ください。
==============================================================================
INTRODUCTION *sandwich-introduction*
このプラグインは文字列を括弧などで囲む/囲みを外す/囲みを置き換えるための機能を
提供します。オペレータ部分 |operator-sandwich| とテキストオブジェクト部分
|textobj-sandwich| の二つの部分からなり、これらが共働することにより機能を実現
します。また、同時にそれぞれが独立したオペレータ/テキストオブジェクトであるの
で、ほかのあらゆるオペレータ/テキストオブジェクトと組み合わせることもできます
。これらの機能は純粋にオペレータ及びテキストオブジェクトの枠組みを使って実装さ
れているので、いかなるライブラリにも依存することなく |.| コマンドによって繰り
返すことができます。
|sandwich.vim| の提供するキーマッピングおよびマッピング変更のための情報につい
ては|sandwich-keymappings|を参照してください。
|sandwich.vim|を構成する部品としての純粋なオペレータについての情報は
|operator-sandwich|のヘルプを参照してください。囲みを編集する機能をカスタマイ
ズするための情報もこちらのヘルプに詳細があります。
|sandwich.vim|を構成する部品としての純粋なテキストオブジェクトについての情報は
|textobj-sandwich| のヘルプを参照してください。テキストオブジェクトの挙動をカ
スタマイズするための情報もこちらのヘルプに詳細があります。
==============================================================================
KEYMAPPINGS *sandwich-keymappings*
このプラグインは以下のキーマッピングを提供します。
機能 デフォルトキーマッピング
--------------------------------------------------------------------------
囲む sa{motion/textobject}{addition} (ノーマル、ビジュアルモード)
-> |<Plug>(sandwich-add)|
囲みを外す
sd{deletion} (ノーマルモード)
sd (ビジュアルモード)
-> |<Plug>(sandwich-delete)|
sdb (ノーマルモード)
-> |<Plug>(sandwich-delete-auto)|
囲みを置き換える
sr{deletion}{addition} (ノーマルモード)
sr{addition} (ビジュアルモード)
-> |<Plug>(sandwich-replace)|
srb{addition} (ノーマルモード)
-> |<Plug>(sandwich-replace-auto)|
テキストオブジェクト
ib (オペレータ待機、ビジュアルモード)
-> |<Plug>(textobj-sandwich-auto-i)|
ab (オペレータ待機、ビジュアルモード)
-> |<Plug>(textobj-sandwich-auto-a)|
is (オペレータ待機、ビジュアルモード)
-> |<Plug>(textobj-sandwich-query-i)|
as (オペレータ待機、ビジュアルモード)
-> |<Plug>(textobj-sandwich-query-a)|
--------------------------------------------------------------------------
NOTE: 誤操作を防ぐため以下の設定を vimrc に追加することを強く推奨します。
>
nmap s <Nop>
xmap s <Nop>
<
|s| コマンドは |c|l| コマンドによって代替できます。
デフォルトのマッピングがお気に召さなければ*g:sandwich_no_default_key_mappings*
をあなたの vimrc で定義しておいてください。
>
let g:sandwich_no_default_key_mappings = 1
<
以下のコードはキーマッピングの開始キーを s から z に変える例です。
>
let g:sandwich_no_default_key_mappings = 1
" add
nmap za <Plug>(sandwich-add)
xmap za <Plug>(sandwich-add)
omap za <Plug>(sandwich-add)
" delete
nmap zd <Plug>(sandwich-delete)
xmap zd <Plug>(sandwich-delete)
nmap zdb <Plug>(sandwich-delete-auto)
" replace
nmap zr <Plug>(sandwich-replace)
xmap zr <Plug>(sandwich-replace)
nmap zrb <Plug>(sandwich-replace-auto)
<
必要ならテキストオブジェクトもマッピングしましょう。
>
" text-objects (if you need)
omap ib <Plug>(textobj-sandwich-auto-i)
xmap ib <Plug>(textobj-sandwich-auto-i)
omap ab <Plug>(textobj-sandwich-auto-a)
xmap ab <Plug>(textobj-sandwich-auto-a)
omap is <Plug>(textobj-sandwich-query-i)
xmap is <Plug>(textobj-sandwich-query-i)
omap as <Plug>(textobj-sandwich-query-a)
xmap as <Plug>(textobj-sandwich-query-a)
<
*<Plug>(sandwich-add)*
[count1] <Plug>(sandwich-add) [count2] {motion} {addition}
指定されたテキストを囲みます。
このキーマッピングは[count]を独特なルールで扱います。
[count1] は|<Plug>(sandwich-add)|に渡され [count1] 回囲みます。他方で
[count2] は通常通り {motion} に渡されます。どちらの[count]も省略可能で
す。{addition}は囲みを指定するキーで、例えば saiw( と入力すると単語を
丸括弧()で囲みます。
{Visual} [count] <Plug>(sandwich-add) {addition}
[count]回ヴィジュアル選択した範囲を囲みます。
<Plug>(sandwich-add)はノーマルモード、ヴィジュアルモード、オペレータ待
機モードにマップ可能です。デフォルトでは sa にマップされています。
>
nmap sa <Plug>(sandwich-add)
xmap sa <Plug>(sandwich-add)
omap sa <Plug>(sandwich-add)
<
*<Plug>(sandwich-delete)*
[count] <Plug>(sandwich-delete) {deletion}
カーソルから最も近い{deletion}に指定される囲みを外します。例えば、
sd( と入力するとカーソルから最も近い対応する丸括弧()を削除します。
>
(foo) -> foo
<
[count]を指定するとカーソルから[count]番目に近い囲みを削除します。
>
(foo(bar)baz) cursor is on "bar"
-- sd( --> (foobarbaz)
-- 2sd( --> foo(bar)baz
<
{Visual} [count] <Plug>(sandwich-delete)
ヴィジュアル選択範囲の両端にある連続した囲みを[count]回外します。
<Plug>(sandwich-delete)はノーマルモード、ヴィジュアルモードにマップ可
能です。デフォルトでは sd にマップされています。
>
nmap sd <Plug>(sandwich-delete)
xmap sd <Plug>(sandwich-delete)
<
*<Plug>(sandwich-delete-auto)*
[count] <Plug>(sandwich-delete-auto)
カーソルから最も近い囲みを自動的に検索して外します。
[count]を指定するとカーソルから[count]番目に近い囲みを削除します。
>
[foo(bar)baz] cursor is on "bar"
-- sdb --> [foobarbaz]
-- 2sdb --> foo(bar)baz
<
<Plug>(sandwich-delete-auto)はノーマルモードにマップ可能です。
デフォルトでは sdb にマップされています。
>
nmap sdb <Plug>(sandwich-delete-auto)
<
*<Plug>(sandwich-replace)*
[count] <Plug>(sandwich-replace) {deletion} {addition}
カーソルから最も近い{deletion}に指定される囲みを{addition}に指定される
囲みに置換します。例えば、sr([ と入力するとカーソルから最も近い対応す
る丸括弧()を角括弧[]に置換します。
>
(foo) -> [foo]
<
[count]を指定するとカーソルから[count]番目に近い囲みを置換します。
>
(foo(bar)baz) cursor is on "bar"
-- sr([ --> (foo[bar]baz)
-- 2sr([ --> [foo(bar)baz]
<
{Visual} [count] <Plug>(sandwich-replace) {addition}
ヴィジュアル選択範囲の両端にある連続した囲みを[count]回置換します。
<Plug>(sandwich-replace)はノーマルモード、ヴィジュアルモードにマップ可
能です。デフォルトでは sr にマップされています。
>
nmap sr <Plug>(sandwich-replace)
xmap sr <Plug>(sandwich-replace)
<
*<Plug>(sandwich-replace-auto)*
[count] <Plug>(sandwich-replace-auto) {addition}
カーソルから最も近い囲みを自動的に検索して置換します。
[count]を指定するとカーソルから[count]番目に近い囲みを削除します。
>
[foo(bar)baz] cursor is on "bar"
-- srb{ --> [foo{bar}baz]
-- 2srb{ --> {foo(bar)baz}
<
<Plug>(sandwich-replace-auto)はノーマルモードにマップ可能です。
デフォルトでは srb にマップされています。
>
nmap srb <Plug>(sandwich-replace-auto)
<
==============================================================================
CONFIGURATION *sandwich-configuration*
括弧などのセットとその性質に依るオプションをまとめた情報をレシピ "recipe" と呼
びます。一つ一つのレシピは |Dictionary| で、これらを集めた |list| がオペレータ
やテキストオブジェクトの動作を決めます。 |g:sandwich#default_recipes| はその一
つで、 |operator-sandwich| と |textobj-sandwich| の両方から参照されます。多く
の場合、この情報は共有したほうが便利であるためです。 |g:sandwich#recipes| がユ
ーザーによって定義された場合こちらがかわりに参照されます。デフォルト設定の
|g:sandwich#default_recipes| は |:echo| コマンドによって確認できます。
>
:echo g:sandwich#default_recipes
<
上記に加え、 |g:operator#sandwich#recipes| と |g:textobj#sandwich#recipes| も
レシピを持つことができます。これらは |operator-sandwich| と |textobj-sandwich|
のそれぞれからしか参照されません。固有のレシピをおきたい場合に使いましょう。
レシピの細かい仕様については、オペレータ及びテキストオブジェクトのヘルプ、
|operator-sandwich-configuration| 及び |textobj-sandwich-configuration| をご覧
ください。
g:sandwich#recipes *g:sandwich#recipes*
|operator-sandwich| と |textobj-sandwich| の両方から参照されるレシピの
リストです。もし存在しなければ |g:sandwich#default_recipes| がかわりに
つかわれます。
*b:sandwich_recipes*
|b:sandwich_recipes| が存在する場合は、 |g:sandwich#recipes| のかわり
にそちらが使われます。これはバッファについてローカルな値なので、ファイ
ルタイプ固有な設定が増えすぎた時に使うと便利かもしれません。
g:sandwich#default_recipes *g:sandwich#default_recipes*
デフォルトで用意されたレシピのリストです。 |g:sandwich#recipes| が存在
すれば、そちらがかわりにつかわれます。
この変数は変更を禁止されていますが、 |g:sandwich#recipes| を宣言する際
に必要ならコピーすることができます。
>
:let g:sandwich#recipes = deepcopy(g:sandwich#default_recipes)
<
デフォルトレシピについて~
`(`, `)`
`[`, `]`
`{`, `}`
`<`, `>`
開き/閉じ括弧は同様に機能します。例えば、 `saiw(` と `saiw)` は同様の
結果を与えます。
>
foo -> (foo)
<
`sd(` と `sd)` はこの反対のことをします。
------------------------------------------------------------------------------
`'`
`"`
クオーテーションで文字列を囲みます。
>
foo -> 'foo'
<
クオーテーションによる囲みを消す場合 `quoteescape` オプションが考慮さ
れます。また、両端が同じ行の中にある必要があります。
------------------------------------------------------------------------------
`<Space>`
スペース囲みを消す場合、連続するスペースを一度に消します。
------------------------------------------------------------------------------
`t`, `T`
`f`, `F`
`i`, `I`
|sandwich-magiccharacters| をご覧ください。
------------------------------------------------------------------------------
Global options~
g:sandwich#timeout *g:sandwich#timeout*
この変数に偽値が設定されている場合は、オペレータやテキストオブジェクト
は一つのレシピを指定する完全な入力がなされるまで待ちます。例えば、下記
のレシピを用意します。
>
let g:sandwich#recipes = [
\ {'buns': ['for {', '}'], 'nesting': 1, 'input': ['bf']}
\ {'buns': ['if {', '}'], 'nesting': 1, 'input': ['bi']}
\ {'buns': ['else {', '}'], 'nesting': 1, 'input': ['be']}
\ ]
<
このオプションが真の場合は `saiwb` とタイプして少し待つと、オペレータ
はタイムアウトのため単語を `b` で囲みます。しかし、偽の場合は一つのレ
シピを指定する入力が完成するまで待ちます。この変数はオペレータとテキス
トオブジェクトの両方に効果を及ぼします。|g:operator#sandwich#timeout|
や |g:textobj#sandwich#timeout| が存在する場合はそれらが優先的に使われ
ます。この変数が定義されていなければ 'timeout' オプションが代わりに参
照されます。
関連:|g:sandwich#timeoutlen|
g:sandwich#timeoutlen *g:sandwich#timeoutlen*
入力に前方一致で重複するレシピが存在する場合に次のキーシーケンスを待つ
時間をミリ秒単位で指定します。
>
let g:sandwich#recipes = [
\ {'buns': ['(', ')']}
\ {'buns': ['((', '))']}
\ ]
<
saiw( とキー入力するとオペレータは次に ( が入力されるかこの時間だけ待
ちます。この間にもう一度 ( を押下すると '((' と '))' のレシピが使われ
るでしょう。キーの押下なしでこの時間が過ぎると '(' と ')' のレシピが使
われるでしょう。この変数はオペレータとテキストオブジェクトの両方に効果
を及ぼします。|g:operator#sandwich#timeoutlen| や
|g:textobj#sandwich#timeout| が存在する場合はそれらが優先的に使われま
す。この変数が定義されていなければ 'timeoutlen' が代わりに参照されま
す。
タイムアウト機能(|g:operator#sandwich#timeout|,
|g:textobj#sandwich#timeout|, |g:sandwich#timeout|, 'timeout')がオフの
場合はこのオプションは無視されます。
g:sandwich#input_fallback *g:sandwich#input_fallback*
このオプションは囲みを追加・削除・置換するためのユーザーの入力に合致す
るレシピが存在しない場合の挙動を制御します。このオプションが真のとき、
ユーザーの入力に合致するレシピがなければ、入力された文字自体を追加・削
除・置換の対象とします。例えば、a という入力に合致するレシピが存在しな
くても saiwa と入力すると単語を a で囲みます。
>
foo -> afooa
<
この動作が必要ない場合はこのオプションに偽値を設定します。
>
let g:sandwich#input_fallback = 0
<
==============================================================================
MAGICCHARACTERS *sandwich-magiccharacters*
sandwich.vim は {addition}/{deletion} を決めるためにユーザーに入力を促します。
これは例えば `(` が `()` のペアを、 `"` が `""` のペアを指しますが、より特殊な
用途のための入力がいくつか存在します。
f~
F~
関数で囲みます。例えば `saiwf` と入力すると `f` キーの入力の後に関数名
の入力を求められます。関数名を入力し <CR> キーを押すと、その関数名付き
の丸括弧でテキストオブジェクトを囲みます。関数名の入力中、 <Tab> キー
で現在のバッファから関数名を補完できます。
>
arg -- saiwffunc<CR> --> func(arg)
<
逆に `sdf` は関数囲みを削除します。
>
func(arg) -- sdf --> arg
<
関数がネストしている場合、 `sdf` はカーソル下の関数囲みを削除し、
`sdF` は外側の関数囲みを削除します。
>
cursor is on 'func2':
func1(func2(arg)) -- sdf --> func1(arg)
-- sdF --> func2(arg)
<
i~
I~
その時だけの (`I`nstant) 囲みを定義します。 `saiwi` はユーザーに前方囲
みと後方囲みの入力を求めます。例えば、 `saiwifoo<CR>bar<CR>` という
入力はカーソル下の単語を `foo` と `bar` で囲みます。入力中は <Tab>
キーにより現在のバッファから簡単な単語補完を使えます。反対に `sdi` は
任意の入力された囲みを削除します。つまり、 `sdifoo<CR>bar<CR>` は
`foowordbar` という文字列を `word` にします。これは |.| コマンドによっ
て繰り返すことができるので、編集対象の文字列が多く存在する場合に便利か
もしれません。
`sa{textobj}I` や `sdI` は最後の入力を再利用します。
t~
T~
`t` および `T` は HTML 様のタグの編集を支援します。 `saiwt` はユーザー
にタグの要素名の入力を促し、そのタグで囲みます。 `saiwT` も同様に機能
します。
>
word -- saiwtp<CR> --> <p>word</p>
<
`sdt` は直近のタグ囲みを削除します。 `sdT` も同様に機能します。
>
<p>word</p> -- sdt --> word
<
`t` と `T` は囲みを置換するときに別のふるまいをします。
`srtt` はタグの要素名のみを置き換え、属性等は変更しません。
`srTT` はタグ全体を置き換えます。
==============================================================================
FILETYPE RECIPES *sandwich-filetype-recipes*
Sandwich.vim にはファイルタイプに固有のレシピもあります。これらは 'filetype'
オプションが特定の値のときのみ読み込まれ、有効になります。またこれらはユーザー
設定を上書きしないので、必要なものだけ残して使うとよいでしょう。設定の本体は
ftplugin/{filetype}/sandwich.vim に記述されています。
もし、それらのファイルを読み込んでほしくない場合は
`g:sandwich_no_{filetype}_ftplugin` に真値を設定しておいてください。例えば、
tex 固有のレシピが必要ない場合、次の一行を vimrc に加えます。
>
let g:sandwich_no_tex_ftplugin = 1
<
------------------------------------------------------------------------------
plaintex~
tex~
>
囲み 入力
`{text}' u'
“{text}” u"
„{text}“ U"
ug
u,
«{text}» u<
uf
`{text}' l'
l`
``{text}'' l"
"`{text}"' L"
,,{text}`` l,
<<{text}>> l<
\{{text}\} \{
\[{text}\] \[
\left({text}\right) m(
\left[{text}\right] m[
\left|{text}\right| m|
\left\{{text}\right\} m{
\left\langle {text}\right\rangle m<
< 'm' 始まりの入力で挿入される囲みはすべて `ma` という入力で削除すること
ができます。例えば `sdma` のように使います。
>
\big({text}\big) M(
\big[{text}\big] M[
\big|{text}\big| M|
\big\{{text}\big\} M{
\big\langle {text}\big\rangle M<
\begingroup{text}\endgroup gr
\gr
\toprule{text}\bottomrule tr
br
\tr
\br
\{input}{{text}} c
< このレシピはユーザーに入力を求め、入力されたテキストで {input} を置き
換えます。
>
\begin{{input}}{text}\end{{input}} e
< このレシピはユーザーに入力を求め、入力されたテキストで {input} を置き
換えます。<Tab> キーにより {input} の入力を補完することができます。
この時、補完候補は `g:sandwich#filetype#tex#environments`
(あるいは存在すれば `b:sandwich#filetype#tex#environments`) から読み込
まれます。
==============================================================================
FUNCTIONS *sandwich-functions*
sandwich#util#addlocal({recipes}) *sandwich#util#addlocal()*
レシピのリスト {recipes} をそのバッファにのみ有効な設定として追加しま
す。この際、既存のグローバルな設定はそのまま受け継がれます。この関数は
グローバルな設定をきれいに保ったままファイルタイプに特有な設定を追加す
る、などの用途に使えます。{recipes} はレシピのリスト |List| である点は
注意してください。
>
autocmd FileType python call sandwich#util#addlocal([
\ {'buns': ['"""', '"""'], 'nesting': 0, 'input': ['3"']},
\ ])
<
==============================================================================
MISCELLANEOUS *sandwich-miscellaneous*
vim-surround のキーマッピングを使う~
vim-surround (vim script #1697) と同じキーマッピングが使いたければ次の
行を vimrc に追加してください。
>
runtime macros/sandwich/keymap/surround.vim
<
NOTE: surround.vim とは違い、 `(` と `)` の入力は同じように機能しま
す。もし、オリジナルの surround.vim のように `(` の入力により、
空白を含んだ括弧で囲みたい場合は次のように書きます。
>
runtime macros/sandwich/keymap/surround.vim
let g:sandwich#recipes += [
\ {'buns': ['{ ', ' }'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['{']},
\
\ {'buns': ['[ ', ' ]'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['[']},
\
\ {'buns': ['( ', ' )'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['(']},
\
\ {'buns': ['{\s*', '\s*}'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['{']},
\
\ {'buns': ['\[\s*', '\s*\]'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['[']},
\
\ {'buns': ['(\s*', '\s*)'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['(']},
\ ]
<
ノーマルモードにおける `ys`, `yss`, `yS`, `ds`, `cs` 及び、ビジュアル
モードにおける `S` が使用できます。また、 vim-surround にはありません
が `dss` と `css` も使用できます。これらは `ds` と `cs` に似ていますが
削除/置換対象となる括弧やクオーテーションを自動的に選択します。
さらに vim-sandwich は便利なテキストオブジェクトが独立して使用できま
す。ぜひ試してみてください。
* ユーザーの入力に従い、括弧や同じ文字に囲まれた領域を選択
|<Plug>(textobj-sandwich-query-i)|, |<Plug>(textobj-sandwich-query-a)|
>
xmap is <Plug>(textobj-sandwich-query-i)
xmap as <Plug>(textobj-sandwich-query-a)
omap is <Plug>(textobj-sandwich-query-i)
omap as <Plug>(textobj-sandwich-query-a)
<
* 最も近い括弧に囲まれた領域を選択
|<Plug>(textobj-sandwich-auto-i)|, |<Plug>(textobj-sandwich-auto-a)|
>
xmap iss <Plug>(textobj-sandwich-auto-i)
xmap ass <Plug>(textobj-sandwich-auto-a)
omap iss <Plug>(textobj-sandwich-auto-i)
omap ass <Plug>(textobj-sandwich-auto-a)
<
* ユーザーの入力に従い、同じ文字に囲まれた領域を選択
|<Plug>(textobj-sandwich-literal-query-i)|,
|<Plug>(textobj-sandwich-literal-query-a)|
>
xmap im <Plug>(textobj-sandwich-literal-query-i)
xmap am <Plug>(textobj-sandwich-literal-query-a)
omap im <Plug>(textobj-sandwich-literal-query-i)
omap am <Plug>(textobj-sandwich-literal-query-a)
<
------------------------------------------------------------------------------
magicchar-f の挙動を変更する~
`magicchar-f` は単純な関数呼び出しを削除できます。
>
func(arg) -- sdf --> arg
<
もし、もっと複雑なパターンにも対応させたい場合、例えば、
>
obj.method(arg) -- sdf --> arg
<
このような場合に対応させるには `g:sandwich#magicchar#f#patterns` か
`b:sandwich_magicchar_f_patterns` を使います。 これらは次のようなパターンのリ
ストです。
>
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
<
四つの文字列はすべて正規表現パターンで、開括弧の前の文字列、開括弧、閉括弧、
閉括弧の後に続く文字列にマッチします。すなわち、先に述べたようなパターンに対応
するには以下のような設定をします。
>
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
<
`b:sandwich_magicchar_f_patterns` はファイルタイプ毎の設定を定義するのに使えま
す。
>
augroup sandwich-ft-python
autocmd Filetype python let b:sandwich_magicchar_f_patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
augroup END
<
デフォルトの設定は `g:sandwich#magicchar#f#default_patterns` にあります。
==============================================================================
vim:tw=78:ts=8:ft=help:norl:noet:

View File

@ -0,0 +1,699 @@
*sandwich.txt* Last change:03-Jul-2022.
The set of operator and textobject plugins to edit sandwiched textobjects.
Author : machakann <mckn{at}outlook.jp>
License : NYSL license
Japanese <http://www.kmonos.net/nysl/>
English (Unofficial) <http://www.kmonos.net/nysl/index.en.html>
Requirement: Vim 7.4 or higher
|+reltime| feature (optional)
|+float| feature (optional)
==============================================================================
CONTENTS *sandwich-contents*
QUICK START |sandwich-quick-start|
INTRODUCTION |sandwich-introduction|
KEYMAPPINGS |sandwich-keymappings|
CONFIGURATION |sandwich-configuration|
MAGICCHARACTERS |sandwich-magiccharacters|
FILETYPE RECIPES |sandwich-filetype-recipes|
FUNCTIONS |sandwich-functions|
MISCELLANEOUS |sandwich-miscellaneous|
Introduce vim-surround keymappings
Customize the behavior of magicchar-f
Any way to make a recipe deletes kinds of parentheses and brackets?
==============================================================================
QUICK START *sandwich-quick-start*
*sandwich.vim* is the set of operator and textobject plugins to
add/delete/replace surroundings of a sandwiched textobject, like (foo), "bar".
add~
Press sa{motion/textobject}{addition}.
For example, saiw( makes foo to (foo).
delete~
Press sdb or sd{deletion}.
For example, sdb or sd( makes (foo) to foo.
sdb searchs a set of surrounding automatically.
replace~
Press srb{addition} or sr{deletion}{addition}.
For example, srb" or sr(" makes (foo) to "foo".
Now you already know enough about sandwich.vim. If you want more,
read this help and each help documents of operator/textobject,
|operator-sandwich| and |textobj-sandwich|.
==============================================================================
INTRODUCTION *sandwich-introduction*
This plugin provides functions to add/delete/replace surroundings of
sandwiched texts. These functions are implemented genuinely by utilizing
operator/textobject framework. Their action can be repeated by |.| command
without any dependency.
Refer to the |sandwich-keymappings| for the key mappings supplied by
|sandwich.vim|. It also explains how to change key mappings for your
preference.
Refer to the |operator-sandwich| for the details of the genuine operators.
It also explains how to customize the functions to edit surroundings.
Refer to the |textobj-sandwich| for the details of the genuine textobjects.
It also explains how to customize the behavior of textobjects.
==============================================================================
KEYMAPPINGS *sandwich-keymappings*
This plugin defines the following keymappings.
function default keymappings
--------------------------------------------------------------------------
add sa{motion/textobject}{addition} (normal and visual mode)
-> |<Plug>(sandwich-add)|
delete
sd{deletion} (normal mode)
sd (visual mode)
-> |<Plug>(sandwich-delete)|
sdb (normal mode)
-> |<Plug>(sandwich-delete-auto)|
replace
sr{deletion}{addition} (normal mode)
sr{addition} (visual mode)
-> |<Plug>(sandwich-replace)|
srb{addition} (normal mode)
-> |<Plug>(sandwich-replace-auto)|
textobjct
ib (operator-pending and visual mode)
-> |<Plug>(textobj-sandwich-auto-i)|
ab (operator-pending and visual mode)
-> |<Plug>(textobj-sandwich-auto-a)|
is (operator-pending and visual mode)
-> |<Plug>(textobj-sandwich-query-i)|
as (operator-pending and visual mode)
-> |<Plug>(textobj-sandwich-query-a)|
--------------------------------------------------------------------------
NOTE: To prevent unintended operation, the following setting is strongly
recommended to add to your vimrc.
>
nmap s <Nop>
xmap s <Nop>
<
|s| could be easily replaced by |c|l| commands.
If you don't need the default mappings, define
*g:sandwich_no_default_key_mappings* in your vimrc.
>
let g:sandwich_no_default_key_mappings = 1
<
The following code snippet shows how to change the trigger key from s to z.
>
let g:sandwich_no_default_key_mappings = 1
" add
nmap za <Plug>(sandwich-add)
xmap za <Plug>(sandwich-add)
omap za <Plug>(sandwich-add)
" delete
nmap zd <Plug>(sandwich-delete)
xmap zd <Plug>(sandwich-delete)
nmap zdb <Plug>(sandwich-delete-auto)
" replace
nmap zr <Plug>(sandwich-replace)
xmap zr <Plug>(sandwich-replace)
nmap zrb <Plug>(sandwich-replace-auto)
<
Additionally, map textobjects if you need.
>
" text-objects (if you need)
omap ib <Plug>(textobj-sandwich-auto-i)
xmap ib <Plug>(textobj-sandwich-auto-i)
omap ab <Plug>(textobj-sandwich-auto-a)
xmap ab <Plug>(textobj-sandwich-auto-a)
omap is <Plug>(textobj-sandwich-query-i)
xmap is <Plug>(textobj-sandwich-query-i)
omap as <Plug>(textobj-sandwich-query-a)
xmap as <Plug>(textobj-sandwich-query-a)
<
*<Plug>(sandwich-add)*
[count1] <Plug>(sandwich-add) [count2] {motion} {addition}
Wrap an assigned text on the buffer.
This key mapping handles [count] uniquely.
[count1] is given to |<Plug>(sandwich-add)| and thus surround the text
[count1] times. On the other hand, [count2] is passed to {motion} as
usually. Both of those [count]s are optional. The {addition} is the
key to specify the surroundings; for example, an input saiw( wraps a
word by parentheses().
{Visual} [count] <Plug>(sandwich-add) {addition}
Wrap the visual-selected text [count] times.
<Plug>(sandwich-add) is available in normal, visual, and
operator-pending mode. It is mapped at sa in default.
>
nmap sa <Plug>(sandwich-add)
xmap sa <Plug>(sandwich-add)
omap sa <Plug>(sandwich-add)
<
*<Plug>(sandwich-delete)*
[count] <Plug>(sandwich-delete) {deletion}
Delete a pair of surroundings nearest to the cursor specified by
{deletion}. For example, an input sd( deletes a pair of parentheses()
nearest to the cursor.
>
(foo) -> foo
<
Delete the [count]th closest surroundings if [count] is given.
>
(foo(bar)baz) cursor is on "bar"
-- sd( --> (foobarbaz)
-- 2sd( --> foo(bar)baz
<
{Visual} [count] <Plug>(sandwich-delete)
Delete the successive surroundings at the both ends of the visually
selected text. Delete [count] times if [count] is given.
<Plug>(sandwich-delete) is available in normal and visual mode. It is
mapped at sd in default.
>
nmap sd <Plug>(sandwich-delete)
xmap sd <Plug>(sandwich-delete)
<
*<Plug>(sandwich-delete-auto)*
[count] <Plug>(sandwich-delete-auto)
Delete the [count]th closest surroundings from the cursor.
>
[foo(bar)baz] cursor is on "bar"
-- sdb --> [foobarbaz]
-- 2sdb --> foo(bar)baz
<
<Plug>(sandwich-delete-auto) is available in normal mode. It is mapped
at sdb in default.
>
nmap sdb <Plug>(sandwich-delete-auto)
<
*<Plug>(sandwich-replace)*
[count] <Plug>(sandwich-replace) {deletion} {addition}
Replace the closest surroundings from the cursor specified by
{deletion} to another surroundings specified by {addition}. For
example, an input sr([ replaces a pair of parentheses() to a pair of
square brackets[].
>
(foo) -> [foo]
<
Replace the [count]th closest surroundings if [count] is given.
>
(foo(bar)baz) cursor is on "bar"
-- sr([ --> (foo[bar]baz)
-- 2sr([ --> [foo(bar)baz]
<
{Visual} [count] <Plug>(sandwich-replace) {addition}
Replace the successive surroundings at the both ends of the visually
selected text to another surroundings specified by {addition}.
Replace [count] times if [count] is given.
<Plug>(sandwich-replace) is available in normal and visual mode. It is
mapped at sr in default.
>
nmap sr <Plug>(sandwich-replace)
xmap sr <Plug>(sandwich-replace)
<
*<Plug>(sandwich-replace-auto)*
[count] <Plug>(sandwich-replace-auto) {addition}
Replace the [count]th closest surroundings from the cursor to another
surroundings specified by {addition}.
>
[foo(bar)baz] cursor is on "bar"
-- srb{ --> [foo{bar}baz]
-- 2srb{ --> {foo(bar)baz}
<
<Plug>(sandwich-replace-auto) is available in normal mode. It is
mapped at srb in default.
>
nmap srb <Plug>(sandwich-replace-auto)
<
==============================================================================
CONFIGURATION *sandwich-configuration*
A set of surroundings and options for it is called "recipe". Each recipe is a
dictionary and the |list|s of recipes determines the operator's behavior and
textobject's behavior. |g:sandwich#default_recipes| is one of the |list|s of
recipes. This is shared to be used by |operator-sandwich| and
|textobj-sandwich| since it is convenient in many cases. If
|g:sandwich#recipes| is defined by user, it is employed alternatively. The
default recipes |g:sandwich#default_recipes| can be checked by |:echo|
command.
>
:echo g:sandwich#default_recipes
<
Besides them, |g:operator#sandwich#recipes| and |g:textobj#sandwich#recipes|
can be used. They are used only by |operator-sandwich| and |textobj-sandwich|
respectively.
About the contents of a recipe, please see |operator-sandwich-configuration|
and |textobj-sandwich-configuration|.
g:sandwich#recipes *g:sandwich#recipes*
This is one of the lists of recipes which is referred from both
|operator-sandwich| and |textobj-sandwich|. If this list does not
exist, |g:sandwich#default_recipes| is used.
*b:sandwich_recipes*
If |b:sandwich_recipes| exists, it would be used instead of
|g:sandwich#recipes|. This is buffer local, thus it might be
convenient to manage too many filetype-specific recipes.
g:sandwich#default_recipes *g:sandwich#default_recipes*
This is a list of recipes which is prepared in default. If
|g:sandwich#recipes| exists, it will be used instead.
This variable is locked usually, but it can be copied when you declare
|g:sandwich#recipes| if you need.
>
:let g:sandwich#recipes = deepcopy(g:sandwich#default_recipes)
<
Notes on default recipes~
`(`, `)`
`[`, `]`
`{`, `}`
`<`, `>`
Both open/close braces behave as same. For example, `saiw(` and `saiw)`
result in the same text.
>
foo -> (foo)
<
`sd(` and `sd)` do the opposite.
------------------------------------------------------------------------------
`'`
`"`
Wrap a text by quotes.
>
foo -> 'foo'
<
When deleting a pair of quotes, 'quoteescape' option is considered.
The pair of quotes are searched only in a same line.
------------------------------------------------------------------------------
`<Space>`
When deleting a pair of spaces, successive spaces are deleted at a
time.
------------------------------------------------------------------------------
`t`, `T`
`f`, `F`
`i`, `I`
See |sandwich-magiccharacters|.
------------------------------------------------------------------------------
Global options~
g:sandwich#timeout *g:sandwich#timeout*
If this option is a falsy value, the operators and the
query-textobject will wait for subsequent inputs until the complete
key sequence has been received to specify a recipe. For example, with
the following recipes,
>
let g:sandwich#recipes = [
\ {'buns': ['for {', '}'], 'nesting': 1, 'input': ['bf']}
\ {'buns': ['if {', '}'], 'nesting': 1, 'input': ['bi']}
\ {'buns': ['else {', '}'], 'nesting': 1, 'input': ['be']}
\ ]
<
type `saiwb` and a while later the operator eagerly wrap a word with
`b` if this option is true. The operators wait next input until a
recipe is specified if this option is false. This option takes effect
both on the operators and the query-textobject.
|g:operator#sandwich#timeout| or |g:textobj#sandwich#timeout| takes
priority over this option if it exists. If this has not been defined,
'timeout' option is referred. See |g:sandwich#timeoutlen| also.
g:sandwich#timeoutlen *g:sandwich#timeoutlen*
The time in milli seconds that waits for a key code or mapped key
sequence to complete. If there are recipes overlapped, this option is
used. Assume that the following recipes are prepared:
>
let g:sandwich#recipes = [
\ {'buns': ['(', ')']}
\ {'buns': ['((', '))']}
\ ]
<
after pressing saiw(, the operator waits in the time. If you press one
more ( in the time, then a recipe for '((' and '))' is decided to use.
No keypress has come through the time a recipe for '(' and ')' is
settled. This option takes effect both on the operators and the
query-textobject. |g:operator#sandwich#timeoutlen| or
|g:textobj#sandwich#timeoutlen| takes priority over this option if it
exists. If this variable has not been defined, 'timeoutlen' option is
referred.
When the timeout option (|g:operator#sandwich#timeout|,
|g:textobj#sandwich#timeout|, |g:sandwich#timeout|, 'timeout') is off,
this option is ignored.
g:sandwich#input_fallback *g:sandwich#input_fallback*
This bool option controls the behavior when no recipe matches with
user input to add/delete/replace surroundings. If this option is true
and no recipe matches with user input, the user input character itself
is used as the surroundings for add/delete/replace. For example, even
if there is no recipe associated with input `a` the key sequence
`saiwa` wraps a word with `a`.
>
foo -> afooa
<
Set falthy value to this option if this behavior is not desired.
>
let g:sandwich#input_fallback = 0
<
==============================================================================
MAGICCHARACTERS *sandwich-magiccharacters*
Sandwich.vim requests user to input keys for determination of
{addtion}/{deletion}. Usually it is something like `(` for `()` pair or
`"` for `""` pair, but there is several functional inputs for cumbersome
editings. It might be helpful for your work, give it a try!
f~
F~
Press `saiwf` to surround a word by function. After inputting `f` key,
user would be requested to input function name and press <CR>, then
the target textobject would be surrounded by parenthesis with the
function name. <Tab> key completes function names from the current
buffer in input.
>
arg -- saiwffunc<CR> --> func(arg)
<
The key sequence `sdf`, conversely, deletes function surrounding.
>
func(arg) -- sdf --> arg
<
In case of nested functions, `sdf` deletes the function under the
cursor while `sdF` deletes the function surrounding.
>
cursor is on 'func2':
func1(func2(arg)) -- sdf --> func1(arg)
-- sdF --> func2(arg)
<
i~
I~
It realizes to define `I`nstant surroundings. `saiwi` ask user for
inputting former and latter surroundings. For example,
`saiwifoo<CR>bar<CR>` makes a word surrounded by `foo` and `bar`.
<Tab> key completes words from the current buffer, just simply.
On the other hand `sdi` deletes arbitrary surroundings. For example,
`sdifoo<CR>bar<CR>` makes `foowordbar` to `word`, the inputs would be
interpreted as regular expressions. This is useful when a lot of
targets are there because the action could be repeated by |.| command.
`sa{textobj}I`, `sdI` reuse the last inputs.
t~
T~
The inputs `t` and `T` support to edit HTML style tags. `saiwt` ask
user to input a name of element, then a textobject would be surrounded
by the tag. `saiwT` works as same.
>
word -- saiwtp<CR> --> <p>word</p>
<
`sdt` deletes the nearest tag surroundings. `sdT` works as same.
>
<p>word</p> -- sdt --> word
<
`t` and `T` works differently only when replacing surroundings.
`srtt` replaces only the name of element, does not touch attributes.
`srTT` replaces the whole body of tags.
==============================================================================
FILETYPE RECIPES *sandwich-filetype-recipes*
Sandwich.vim has filetype specific settings. They will be available when
'filetype' option was set to a certain value. User settings are not
overwrittern by them, use only the recipes helpful for you. These recipes are
written in ftplugin/{filetype}/sandwich.vim.
If you don't want vim to load these files, set
`g:sandwich_no_{filetype}_ftplugin` as true in advance. For example, add the
following line to your vimrc in case you don't need tex specific recipes.
>
let g:sandwich_no_tex_ftplugin = 1
<
------------------------------------------------------------------------------
plaintex~
tex~
>
Surroundings Input
“{text}” u"
„{text}“ U"
ug
u,
«{text}» u<
uf
`{text}' l'
l`
``{text}'' l"
"`{text}"' L"
,,{text}`` l,
<<{text}>> l<
\{{text}\} \{
\[{text}\] \[
\left({text}\right) m(
\left[{text}\right] m[
\left|{text}\right| m|
\left\{{text}\right\} m{
\left\langle {text}\right\rangle m<
< The surroundings its input starting from 'm' could be removed/replaced
by a input `ma`, for example press `sdma`.
>
\big({text}\big) M(
\big[{text}\big] M[
\big|{text}\big| M|
\big\{{text}\big\} M{
\big\langle {text}\big\rangle M<
\begingroup{text}\endgroup gr
\gr
\toprule{text}\bottomrule tr
br
\tr
\br
\{input}{{text}} c
< This recipe asks user to input a string and then {input} is
substituted by the input string.
>
\begin{{input}}{text}\end{{input}} e
< This recipe asks user to input a string and then {input} is
substituted by the input string.
Use <Tab> to complete {input}, the completion items are loaded from
`g:sandwich#filetype#tex#environments`
(or `b:sandwich#filetype#tex#environments` if exists).
==============================================================================
FUNCTIONS *sandwich-functions*
sandwich#util#addlocal({recipes}) *sandwich#util#addlocal()*
This function appends the list of recipes {recipes} as buffer-local
settings with inheritance of the global settings. This is useful when
one wants to add filetype specific settings with keeping global
setting |g:sandwich#recipes| clean. Note that {recipe} is a |List| of
recipes.
>
autocmd FileType python call sandwich#util#addlocal([
\ {'buns': ['"""', '"""'], 'nesting': 0, 'input': ['3"']},
\ ])
<
==============================================================================
MISCELLANEOUS *sandwich-miscellaneous*
Introduce vim-surround keymappings~
If you want to use with vim-surround (vim script #1697) keymappings,
add the following line to your vimrc.
>
runtime macros/sandwich/keymap/surround.vim
<
NOTE: Unlike surround.vim, the inputs `(` and `)` behave as same.
If you want the spaces inside braces with `(` input, add the
lines.
>
runtime macros/sandwich/keymap/surround.vim
let g:sandwich#recipes += [
\ {'buns': ['{ ', ' }'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['{']},
\
\ {'buns': ['[ ', ' ]'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['[']},
\
\ {'buns': ['( ', ' )'], 'nesting': 1, 'match_syntax': 1,
\ 'kind': ['add', 'replace'], 'action': ['add'], 'input': ['(']},
\
\ {'buns': ['{\s*', '\s*}'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['{']},
\
\ {'buns': ['\[\s*', '\s*\]'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['[']},
\
\ {'buns': ['(\s*', '\s*)'], 'nesting': 1, 'regex': 1,
\ 'match_syntax': 1, 'kind': ['delete', 'replace', 'textobj'],
\ 'action': ['delete'], 'input': ['(']},
\ ]
<
`ys`, `yss`, `yS`, `ds`, `cs` in normal mode and `S` in visual mode
are available. Not in vim-surround but `dss` and `css` are also
available, these are similar as `ds` and `cs` but determine
deleted/replaced texts automatically. See the file directly for
detail.
Additionally, vim-sandwich provides several textobjects. They would
also be helpful, give it a try!
* Textobjects to select a text surrounded by braket or same characters
user input.
|<Plug>(textobj-sandwich-query-i)|, |<Plug>(textobj-sandwich-query-a)|
>
xmap is <Plug>(textobj-sandwich-query-i)
xmap as <Plug>(textobj-sandwich-query-a)
omap is <Plug>(textobj-sandwich-query-i)
omap as <Plug>(textobj-sandwich-query-a)
<
* Textobjects to select the nearest surrounded text automatically.
|<Plug>(textobj-sandwich-auto-i)|, |<Plug>(textobj-sandwich-auto-a)|.
>
xmap iss <Plug>(textobj-sandwich-auto-i)
xmap ass <Plug>(textobj-sandwich-auto-a)
omap iss <Plug>(textobj-sandwich-auto-i)
omap ass <Plug>(textobj-sandwich-auto-a)
<
* Textobjects to select a text surrounded by same characters user
input.
|<Plug>(textobj-sandwich-literal-query-i)|,
|<Plug>(textobj-sandwich-literal-query-a)|
>
xmap im <Plug>(textobj-sandwich-literal-query-i)
xmap am <Plug>(textobj-sandwich-literal-query-a)
omap im <Plug>(textobj-sandwich-literal-query-i)
omap am <Plug>(textobj-sandwich-literal-query-a)
<
------------------------------------------------------------------------------
Customize the behavior of magicchar-f~
`magicchar-f` can delete simple function-call like syntax:
>
func(arg) -- sdf --> arg
<
If you want to delete more advanced patterns, for example:
>
obj.method(arg) -- sdf --> arg
<
You can use `g:sandwich#magicchar#f#patterns` or
`b:sandwich_magicchar_f_patterns` for the purpose. Each of those are a list of
patterns like:
>
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
<
Those four values are all regex patterns, which match with something before
open parenthesis, open & close parentheses, something after close parenthesis.
Therefore, you can delete a method with an object by the following setting.
>
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
<
`b:sandwich_magicchar_f_patterns` can be used to define filetype specific
setting.
>
augroup sandwich-ft-python
autocmd Filetype python let b:sandwich_magicchar_f_patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
augroup END
<
The default settings is in `g:sandwich#magicchar#f#default_patterns`.
------------------------------------------------------------------------------
Any way to make a recipe deletes kinds of parentheses and brackets?~
See |sandwich-compound-recipes|.
==============================================================================
vim:tw=78:ts=8:ft=help:norl:noet:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,176 @@
scriptencoding utf-8
if !exists('s:local_recipes')
let s:local_recipes = [
\ {'__filetype__': 'tex', 'buns': ['“', '”'], 'nesting': 1, 'input': [ 'u"' ]},
\ {'__filetype__': 'tex', 'buns': ['„', '“'], 'nesting': 1, 'input': [ 'U"', 'ug', 'u,' ]},
\ {'__filetype__': 'tex', 'buns': ['«', '»'], 'nesting': 1, 'input': [ 'u<', 'uf' ]},
\ {'__filetype__': 'tex', 'buns': ["`", "'"], 'nesting': 1, 'input': [ "l'", "l`" ]},
\ {'__filetype__': 'tex', 'buns': ["``", "''"], 'nesting': 1, 'input': [ 'l"' ]},
\ {'__filetype__': 'tex', 'buns': ['"`', "\"'"], 'nesting': 1, 'input': [ 'L"' ]},
\ {'__filetype__': 'tex', 'buns': [",,", "``"], 'nesting': 1, 'input': [ 'l,' ]},
\ {'__filetype__': 'tex', 'buns': ['<<', '>>'], 'nesting': 1, 'input': [ 'l<' ]},
\ {'__filetype__': 'tex', 'buns': ['$', '$'], 'nesting': 0},
\ {'__filetype__': 'tex', 'buns': ['\(', '\)'], 'nesting': 1, 'input': [ '\(' ], 'indentkeys-': '{,},0{,0}'},
\ {'__filetype__': 'tex', 'buns': ['\{', '\}'], 'nesting': 1, 'input': [ '\{' ], 'indentkeys-': '{,},0{,0}'},
\ {'__filetype__': 'tex', 'buns': ['\[', '\]'], 'nesting': 1, 'input': [ '\[' ], 'indentkeys-': '{,},0{,0}'},
\ {'__filetype__': 'tex', 'buns': ['\left(', '\right)'], 'nesting': 1, 'input': [ 'm(' ], 'action': ['add'], 'indentkeys-': '(,)'},
\ {'__filetype__': 'tex', 'buns': ['\left[', '\right]'], 'nesting': 1, 'input': [ 'm[' ], 'action': ['add'], 'indentkeys-': '[,]'},
\ {'__filetype__': 'tex', 'buns': ['\left|', '\right|'], 'nesting': 1, 'input': [ 'm|' ], 'action': ['add']},
\ {'__filetype__': 'tex', 'buns': ['\left\{', '\right\}'], 'nesting': 1, 'input': [ 'm{' ], 'action': ['add'], 'indentkeys-': '{,},0{,0}'},
\ {'__filetype__': 'tex', 'buns': ['\left\langle ', '\right\rangle '], 'nesting': 1, 'input': [ 'm<' ], 'action': ['add']},
\ {'__filetype__': 'tex', 'buns': ['\bigl(', '\bigr)'], 'nesting': 1, 'input': [ 'M(' ], 'action': ['add'], 'indentkeys-': '(,)'},
\ {'__filetype__': 'tex', 'buns': ['\bigl[', '\bigr]'], 'nesting': 1, 'input': [ 'M[' ], 'action': ['add'], 'indentkeys-': '[,]'},
\ {'__filetype__': 'tex', 'buns': ['\bigl|', '\bigr|'], 'nesting': 1, 'input': [ 'M|' ], 'action': ['add']},
\ {'__filetype__': 'tex', 'buns': ['\bigl\{', '\bigr\}'], 'nesting': 1, 'input': [ 'M{' ], 'action': ['add'], 'indentkeys-': '{,},0{,0}'},
\ {'__filetype__': 'tex', 'buns': ['\bigl\langle ', '\bigr\rangle '], 'nesting': 1, 'input': [ 'M<' ], 'action': ['add']},
\ {
\ '__filetype__': 'tex',
\ 'buns' : ['\begingroup', '\endgroup'],
\ 'nesting' : 1,
\ 'input': ['gr', '\gr'],
\ 'linewise': 1,
\ },
\ {
\ '__filetype__': 'tex',
\ 'buns' : ['\toprule', '\bottomrule'],
\ 'nesting' : 1,
\ 'input': ['tr', '\tr', 'br', '\br'],
\ 'linewise': 1,
\ },
\ {
\ '__filetype__': 'tex',
\ 'buns' : 'sandwich#filetype#tex#CmdInput()',
\ 'kind' : ['add', 'replace'],
\ 'action' : ['add'],
\ 'listexpr': 1,
\ 'nesting' : 1,
\ 'input' : ['c'],
\ 'indentkeys-' : '{,},0{,0}',
\ },
\ {
\ '__filetype__': 'tex',
\ 'buns' : 'sandwich#filetype#tex#EnvInput()',
\ 'kind' : ['add', 'replace'],
\ 'action' : ['add'],
\ 'listexpr': 1,
\ 'nesting' : 1,
\ 'linewise' : 1,
\ 'input' : ['e'],
\ 'indentkeys-' : '{,},0{,0}',
\ 'autoindent' : 0,
\ },
\ {
\ '__filetype__': 'tex',
\ 'buns' : ['\\\a\+\*\?{', '}'],
\ 'kind' : ['delete', 'replace', 'auto', 'query'],
\ 'regex' : 1,
\ 'nesting' : 1,
\ 'input' : ['c'],
\ 'indentkeys-' : '{,},0{,0}',
\ },
\ {
\ '__filetype__': 'tex',
\ 'buns' : ['\\begin{[^}]*}\%(\[.*\]\)\?', '\\end{[^}]*}'],
\ 'kind' : ['delete', 'replace', 'auto', 'query'],
\ 'regex' : 1,
\ 'nesting' : 1,
\ 'linewise' : 1,
\ 'input' : ['e'],
\ 'indentkeys-' : '{,},0{,0}',
\ 'autoindent' : 0,
\ },
\ {
\ '__filetype__': 'tex',
\ 'external': ["\<Plug>(textobj-sandwich-filetype-tex-marks-i)", "\<Plug>(textobj-sandwich-filetype-tex-marks-a)"],
\ 'kind' : ['delete', 'replace', 'auto', 'query'],
\ 'noremap' : 0,
\ 'input' : ['ma'],
\ 'indentkeys': '{,},0{,0}',
\ 'autoindent': 0,
\ },
\ ]
xnoremap <silent><expr> <Plug>(textobj-sandwich-filetype-tex-marks-i) textobj#sandwich#auto('x', 'i', {'synchro': 0}, b:sandwich_tex_marks_recipes)
xnoremap <silent><expr> <Plug>(textobj-sandwich-filetype-tex-marks-a) textobj#sandwich#auto('x', 'a', {'synchro': 0}, b:sandwich_tex_marks_recipes)
let s:marks_recipes = []
let s:marks_recipes += [
\ {
\ 'buns': ['\%([[(]\|\\{\)', '\%([])]\|\\}\)'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['|', '|'],
\ 'nesting': 0,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?l|', '\m\C\\[Bb]igg\?r|'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['\m\C\\\%(langle\|lVert\|lvert\|lceil\|lfloor\)', '\m\C\\\%(rangle\|rVert\|rvert\|rceil\|rfloor\)'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['\m\C\\left\%([[(|.]\|\\{\|\\langle\|\\lVert\|\\lvert\|\\lceil\|\\lfloor\)', '\m\C\\right\%([])|.]\|\\}\|\\rangle\|\\rVert\|\\rvert\|\\rceil\|\\rfloor\)'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ ]
" NOTE: It is not reasonable to set 'nesting' on when former and latter surrounds are same.
let s:marks_recipes += [
\ {
\ 'buns': ['\m\C\\[Bb]igg\?|', '\m\C\\[Bb]igg\?|'],
\ 'regex': 1,
\ 'nesting': 0,
\ },
\ ]
" NOTE: The existence of '\big.' makes the situation tricky.
" Try to search those two cases independently and adopt the nearest item.
" \big. foo \big)
" \big( foo \big.
" This roundabout enables the following:
" \big( foo \big. bar \big. baz \big)
" When the cursor is on;
" foo -> \big( and \big.
" bar -> nothing
" foo -> \big. and \big)
" were deleted by the input 'sdma'.
let s:marks_recipes += [
\ {
\ 'buns': ['\m\C\\[Bb]igg\?l\?\%([[(]\|\\{\|\\langle\|\\lVert\|\\lvert\|\\lceil\|\\lfloor\)', '\m\C\\[Bb]igg\?r\?\%([]).]\|\\}\|\\rangle\|\\rVert\|\\rvert\|\\rceil\|\\rfloor\)'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?l\?\%([[(.]\|\\{\|\\langle\|\\lVert\|\\lvert\|\\lceil\|\\lfloor\)', '\m\C\\[Bb]igg\?r\?\%([])]\|\\}\|\\rangle\|\\rVert\|\\rvert\|\\rceil\|\\rfloor\)'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?|', '\m\C\\[Bb]igg\?.'],
\ 'regex': 1,
\ 'nesting': 0,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?.', '\m\C\\[Bb]igg\?|'],
\ 'regex': 1,
\ 'nesting': 0,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?l|', '\m\C\\[Bb]igg\?r[|.]'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ {
\ 'buns': ['\m\C\\[Bb]igg\?l[|.]', '\m\C\\[Bb]igg\?r|'],
\ 'regex': 1,
\ 'nesting': 1,
\ },
\ ]
endif
call sandwich#util#insertlocal(s:local_recipes)
let b:sandwich_tex_marks_recipes = deepcopy(s:marks_recipes)

View File

@ -0,0 +1,200 @@
let g:sandwich_no_default_key_mappings = 1
let g:operator_sandwich_no_default_key_mappings = 1
let g:textobj_sandwich_no_default_key_mappings = 1
nmap ys <Plug>(sandwich-add)
onoremap <SID>line :normal! ^vg_<CR>
nmap <silent> yss <Plug>(sandwich-add)<SID>line
onoremap <SID>gul g_
nmap <silent> yS <Plug>(sandwich-add)<SID>gul
nmap ds <Plug>(sandwich-delete)
nmap dss <Plug>(sandwich-delete-auto)
nmap cs <Plug>(sandwich-replace)
nmap css <Plug>(sandwich-replace-auto)
xmap S <Plug>(sandwich-add)
runtime autoload/repeat.vim
if hasmapto('<Plug>(RepeatDot)')
nmap . <Plug>(operator-sandwich-predot)<Plug>(RepeatDot)
else
nmap . <Plug>(operator-sandwich-dot)
endif
" Default recipes
let g:sandwich#recipes = [
\ {
\ 'buns': ['\s\+', '\s\+'],
\ 'regex': 1,
\ 'kind': ['delete', 'replace', 'query'],
\ 'input': [' ']
\ },
\
\ {
\ 'buns': ['', ''],
\ 'action': ['add'],
\ 'motionwise': ['line'],
\ 'linewise': 1,
\ 'input': ["\<CR>"]
\ },
\
\ {
\ 'buns': ['^$', '^$'],
\ 'regex': 1,
\ 'linewise': 1,
\ 'input': ["\<CR>"]
\ },
\
\ {
\ 'buns': ['<', '>'],
\ 'expand_range': 0,
\ 'input': ['>', 'a'],
\ },
\
\ {
\ 'buns': ['`', '`'],
\ 'quoteescape': 1,
\ 'expand_range': 0,
\ 'nesting': 0,
\ 'linewise': 0,
\ },
\
\ {
\ 'buns': ['"', '"'],
\ 'quoteescape': 1,
\ 'expand_range': 0,
\ 'nesting': 0,
\ 'linewise': 0,
\ },
\
\ {
\ 'buns': ["'", "'"],
\ 'quoteescape': 1,
\ 'expand_range': 0,
\ 'nesting': 0,
\ 'linewise': 0,
\ },
\
\ {
\ 'buns': ['{', '}'],
\ 'nesting': 1,
\ 'skip_break': 1,
\ 'input': ['{', '}', 'B'],
\ },
\
\ {
\ 'buns': ['[', ']'],
\ 'nesting': 1,
\ 'input': ['[', ']', 'r'],
\ },
\
\ {
\ 'buns': ['(', ')'],
\ 'nesting': 1,
\ 'input': ['(', ')', 'b'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#t#tag()',
\ 'listexpr': 1,
\ 'kind': ['add'],
\ 'action': ['add'],
\ 'input': ['t', 'T'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#t#tag()',
\ 'listexpr': 1,
\ 'kind': ['replace'],
\ 'action': ['add'],
\ 'input': ['T', '<'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#t#tagname()',
\ 'listexpr': 1,
\ 'kind': ['replace'],
\ 'action': ['add'],
\ 'input': ['t'],
\ },
\
\ {
\ 'external': ["\<Plug>(textobj-sandwich-tag-i)", "\<Plug>(textobj-sandwich-tag-a)"],
\ 'noremap': 0,
\ 'kind': ['delete', 'textobj'],
\ 'expr_filter': ['operator#sandwich#kind() !=# "replace"'],
\ 'linewise': 1,
\ 'input': ['t', 'T', '<'],
\ },
\
\ {
\ 'external': ["\<Plug>(textobj-sandwich-tag-i)", "\<Plug>(textobj-sandwich-tag-a)"],
\ 'noremap': 0,
\ 'kind': ['replace', 'query'],
\ 'expr_filter': ['operator#sandwich#kind() ==# "replace"'],
\ 'input': ['T', '<'],
\ },
\
\ {
\ 'external': ["\<Plug>(textobj-sandwich-tagname-i)", "\<Plug>(textobj-sandwich-tagname-a)"],
\ 'noremap': 0,
\ 'kind': ['replace', 'textobj'],
\ 'expr_filter': ['operator#sandwich#kind() ==# "replace"'],
\ 'input': ['t'],
\ },
\
\ {
\ 'buns': ['sandwich#magicchar#f#fname()', '")"'],
\ 'kind': ['add', 'replace'],
\ 'action': ['add'],
\ 'expr': 1,
\ 'input': ['f']
\ },
\
\ {
\ 'external': ["\<Plug>(textobj-sandwich-function-ip)", "\<Plug>(textobj-sandwich-function-i)"],
\ 'noremap': 0,
\ 'kind': ['delete', 'replace', 'query'],
\ 'input': ['f']
\ },
\
\ {
\ 'external': ["\<Plug>(textobj-sandwich-function-ap)", "\<Plug>(textobj-sandwich-function-a)"],
\ 'noremap': 0,
\ 'kind': ['delete', 'replace', 'query'],
\ 'input': ['F']
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#i#input("operator")',
\ 'kind': ['add', 'replace'],
\ 'action': ['add'],
\ 'listexpr': 1,
\ 'input': ['i'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#i#input("textobj", 1)',
\ 'kind': ['delete', 'replace', 'query'],
\ 'listexpr': 1,
\ 'regex': 1,
\ 'input': ['i'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#i#lastinput("operator", 1)',
\ 'kind': ['add', 'replace'],
\ 'action': ['add'],
\ 'listexpr': 1,
\ 'input': ['I'],
\ },
\
\ {
\ 'buns': 'sandwich#magicchar#i#lastinput("textobj")',
\ 'kind': ['delete', 'replace', 'query'],
\ 'listexpr': 1,
\ 'regex': 1,
\ 'input': ['I'],
\ },
\ ]

View File

@ -0,0 +1,60 @@
" The vim operator plugin to do well with 'sandwich' like structure
" Last Change: 31-Oct-2021.
" Maintainer : Masaaki Nakamura <mckn@outlook.jp>
" License : NYSL
" Japanese <http://www.kmonos.net/nysl/>
" English (Unofficial) <http://www.kmonos.net/nysl/index.en.html>
if &compatible || exists("g:loaded_operator_sandwich")
finish
endif
let g:loaded_operator_sandwich = 1
" keymappings
nmap <silent> <Plug>(operator-sandwich-add) <Plug>(operator-sandwich-add-pre)<Plug>(operator-sandwich-g@)
xmap <silent> <Plug>(operator-sandwich-add) <Plug>(operator-sandwich-add-pre)<Plug>(operator-sandwich-gv)<Plug>(operator-sandwich-g@)
omap <silent> <Plug>(operator-sandwich-add) <Plug>(operator-sandwich-g@)
nmap <silent> <Plug>(operator-sandwich-delete) <Plug>(operator-sandwich-delete-pre)<Plug>(operator-sandwich-g@)
xmap <silent> <Plug>(operator-sandwich-delete) <Plug>(operator-sandwich-delete-pre)<Plug>(operator-sandwich-gv)<Plug>(operator-sandwich-g@)
omap <silent> <Plug>(operator-sandwich-delete) <Plug>(operator-sandwich-g@)
nmap <silent> <Plug>(operator-sandwich-replace) <Plug>(operator-sandwich-replace-pre)<Plug>(operator-sandwich-g@)
xmap <silent> <Plug>(operator-sandwich-replace) <Plug>(operator-sandwich-replace-pre)<Plug>(operator-sandwich-gv)<Plug>(operator-sandwich-g@)
omap <silent> <Plug>(operator-sandwich-replace) <Plug>(operator-sandwich-g@)
nnoremap <silent> <Plug>(operator-sandwich-add-pre) :<C-u>call operator#sandwich#prerequisite('add', 'n')<CR>
xnoremap <silent> <Plug>(operator-sandwich-add-pre) <Esc>:call operator#sandwich#prerequisite('add', 'x')<CR>
nnoremap <silent> <Plug>(operator-sandwich-delete-pre) :<C-u>call operator#sandwich#prerequisite('delete', 'n')<CR>
xnoremap <silent> <Plug>(operator-sandwich-delete-pre) <Esc>:call operator#sandwich#prerequisite('delete', 'x')<CR>
nnoremap <silent> <Plug>(operator-sandwich-replace-pre) :<C-u>call operator#sandwich#prerequisite('replace', 'n')<CR>
xnoremap <silent> <Plug>(operator-sandwich-replace-pre) <Esc>:call operator#sandwich#prerequisite('replace', 'x')<CR>
nnoremap <silent> <Plug>(operator-sandwich-add-query1st) :<C-u>call operator#sandwich#query1st('add', 'n')<CR>
xnoremap <silent> <Plug>(operator-sandwich-add-query1st) <Esc>:call operator#sandwich#query1st('add', 'x')<CR>
nnoremap <silent> <Plug>(operator-sandwich-replace-query1st) :<C-u>call operator#sandwich#query1st('replace', 'n')<CR>
xnoremap <silent> <Plug>(operator-sandwich-replace-query1st) <Esc>:call operator#sandwich#query1st('replace', 'x')<CR>
" supplementary keymappings
onoremap <expr><silent> <Plug>(operator-sandwich-synchro-count) operator#sandwich#synchro_count()
onoremap <expr><silent> <Plug>(operator-sandwich-release-count) operator#sandwich#release_count()
onoremap <expr><silent> <Plug>(operator-sandwich-squash-count) operator#sandwich#squash_count()
nnoremap <expr><silent> <Plug>(operator-sandwich-predot) operator#sandwich#predot()
nnoremap <expr><silent> <Plug>(operator-sandwich-dot) operator#sandwich#dot()
" visualrepeat.vim (vimscript #3848) support
noremap <silent> <Plug>(operator-sandwich-add-visualrepeat) :<C-u>call operator#sandwich#visualrepeat('add')<CR>
noremap <silent> <Plug>(operator-sandwich-delete-visualrepeat) :<C-u>call operator#sandwich#visualrepeat('delete')<CR>
noremap <silent> <Plug>(operator-sandwich-replace-visualrepeat) :<C-u>call operator#sandwich#visualrepeat('replace')<CR>
" intrinsic keymappings
noremap <Plug>(operator-sandwich-g@) g@
inoremap <Plug>(operator-sandwich-g@) <C-o>g@
nnoremap <Plug>(operator-sandwich-gv) gv
inoremap <Plug>(operator-sandwich-gv) <C-o>gv
" use of vim-event-DotCommandPre
if !hasmapto('<Plug>(operator-sandwich-predot)') && !hasmapto('<Plug>(operator-sandwich-dot)') && (hasmapto('<Plug>(event-DotCommandPre)') || hasmapto('<Plug>(event-DotCommandPre+Dot)'))
augroup sandwich-predot
autocmd User DotCommandPre call operator#sandwich#predot()
augroup END
endif

View File

@ -0,0 +1,75 @@
" The set of operator/textobj plugins to do well with 'sandwich' like structure
" Last Change: 18-Jan-2022.
" Maintainer : Masaaki Nakamura <mckn@outlook.jp>
" License : NYSL
" Japanese <http://www.kmonos.net/nysl/>
" English (Unofficial) <http://www.kmonos.net/nysl/index.en.html>
if &compatible || exists("g:loaded_sandwich")
finish
endif
let g:loaded_sandwich = 1
" intrinsic keymappings
onoremap <silent> <Plug>(textobj-sandwich-function-ip) :<C-u>call sandwich#magicchar#f#ip('o')<CR>
onoremap <silent> <Plug>(textobj-sandwich-function-i) :<C-u>call sandwich#magicchar#f#i('o')<CR>
xnoremap <silent> <Plug>(textobj-sandwich-function-ip) :<C-u>call sandwich#magicchar#f#ip('x')<CR>
xnoremap <silent> <Plug>(textobj-sandwich-function-i) :<C-u>call sandwich#magicchar#f#i('x')<CR>
onoremap <silent> <Plug>(textobj-sandwich-function-ap) :<C-u>call sandwich#magicchar#f#ap('o')<CR>
onoremap <silent> <Plug>(textobj-sandwich-function-a) :<C-u>call sandwich#magicchar#f#a('o')<CR>
xnoremap <silent> <Plug>(textobj-sandwich-function-ap) :<C-u>call sandwich#magicchar#f#ap('x')<CR>
xnoremap <silent> <Plug>(textobj-sandwich-function-a) :<C-u>call sandwich#magicchar#f#a('x')<CR>
onoremap <silent> <Plug>(textobj-sandwich-tagname-i) :<C-u>call sandwich#magicchar#t#i()<CR>
onoremap <silent> <Plug>(textobj-sandwich-tagname-a) :<C-u>call sandwich#magicchar#t#a()<CR>
xnoremap <silent> <Plug>(textobj-sandwich-tagname-i) :<C-u>call sandwich#magicchar#t#i()<CR>
xnoremap <silent> <Plug>(textobj-sandwich-tagname-a) :<C-u>call sandwich#magicchar#t#a()<CR>
onoremap <silent> <Plug>(textobj-sandwich-tag-i) :<C-u>call sandwich#magicchar#t#it()<CR>
onoremap <silent> <Plug>(textobj-sandwich-tag-a) :<C-u>call sandwich#magicchar#t#at()<CR>
xnoremap <silent> <Plug>(textobj-sandwich-tag-i) :<C-u>call sandwich#magicchar#t#it()<CR>
xnoremap <silent> <Plug>(textobj-sandwich-tag-a) :<C-u>call sandwich#magicchar#t#at()<CR>
nmap <silent> <Plug>(sandwich-add) <Plug>(operator-sandwich-add)
xmap <silent> <Plug>(sandwich-add) <Plug>(operator-sandwich-add)
omap <silent> <Plug>(sandwich-add) <Plug>(operator-sandwich-add)
nmap <silent> <Plug>(sandwich-delete) <Plug>(operator-sandwich-delete)<Plug>(operator-sandwich-release-count)<Plug>(textobj-sandwich-query-a)
xmap <silent> <Plug>(sandwich-delete) <Plug>(operator-sandwich-delete)
nmap <silent> <Plug>(sandwich-replace) <Plug>(operator-sandwich-replace)<Plug>(operator-sandwich-release-count)<Plug>(textobj-sandwich-query-a)
xmap <silent> <Plug>(sandwich-replace) <Plug>(operator-sandwich-replace)
nmap <silent> <Plug>(sandwich-delete-auto) <Plug>(operator-sandwich-delete)<Plug>(operator-sandwich-release-count)<Plug>(textobj-sandwich-auto-a)
nmap <silent> <Plug>(sandwich-replace-auto) <Plug>(operator-sandwich-replace)<Plug>(operator-sandwich-release-count)<Plug>(textobj-sandwich-auto-a)
""" default keymappings
" If g:sandwich_no_default_key_mappings has been defined, then quit immediately.
if exists('g:sandwich_no_default_key_mappings') | finish | endif
if !exists('g:operator_sandwich_no_default_key_mappings')
" add
silent! nmap <unique> sa <Plug>(sandwich-add)
silent! xmap <unique> sa <Plug>(sandwich-add)
silent! omap <unique> sa <Plug>(sandwich-add)
" delete
silent! nmap <unique> sd <Plug>(sandwich-delete)
silent! xmap <unique> sd <Plug>(sandwich-delete)
silent! nmap <unique> sdb <Plug>(sandwich-delete-auto)
" replace
silent! nmap <unique> sr <Plug>(sandwich-replace)
silent! xmap <unique> sr <Plug>(sandwich-replace)
silent! nmap <unique> srb <Plug>(sandwich-replace-auto)
endif
if !exists('g:textobj_sandwich_no_default_key_mappings')
" auto
silent! omap <unique> ib <Plug>(textobj-sandwich-auto-i)
silent! xmap <unique> ib <Plug>(textobj-sandwich-auto-i)
silent! omap <unique> ab <Plug>(textobj-sandwich-auto-a)
silent! xmap <unique> ab <Plug>(textobj-sandwich-auto-a)
" query
silent! omap <unique> is <Plug>(textobj-sandwich-query-i)
silent! xmap <unique> is <Plug>(textobj-sandwich-query-i)
silent! omap <unique> as <Plug>(textobj-sandwich-query-a)
silent! xmap <unique> as <Plug>(textobj-sandwich-query-a)
endif

View File

@ -0,0 +1,33 @@
" The vim textobject plugin to search and select 'sandwich' like structure
" Last Change: 30-Oct-2021.
" Maintainer : Masaaki Nakamura <mckn@outlook.jp>
" License : NYSL
" Japanese <http://www.kmonos.net/nysl/>
" English (Unofficial) <http://www.kmonos.net/nysl/index.en.html>
if exists("g:loaded_textobj_sandwich")
finish
endif
let g:loaded_textobj_sandwich = 1
nnoremap <silent><expr> <Plug>(textobj-sandwich-auto-i) textobj#sandwich#auto('n', 'i')
onoremap <silent><expr> <Plug>(textobj-sandwich-auto-i) textobj#sandwich#auto('o', 'i')
xnoremap <silent><expr> <Plug>(textobj-sandwich-auto-i) textobj#sandwich#auto('x', 'i')
nnoremap <silent><expr> <Plug>(textobj-sandwich-auto-a) textobj#sandwich#auto('n', 'a')
onoremap <silent><expr> <Plug>(textobj-sandwich-auto-a) textobj#sandwich#auto('o', 'a')
xnoremap <silent><expr> <Plug>(textobj-sandwich-auto-a) textobj#sandwich#auto('x', 'a')
nnoremap <silent><expr> <Plug>(textobj-sandwich-query-i) textobj#sandwich#query('n', 'i')
onoremap <silent><expr> <Plug>(textobj-sandwich-query-i) textobj#sandwich#query('o', 'i')
xnoremap <silent><expr> <Plug>(textobj-sandwich-query-i) textobj#sandwich#query('x', 'i')
nnoremap <silent><expr> <Plug>(textobj-sandwich-query-a) textobj#sandwich#query('n', 'a')
onoremap <silent><expr> <Plug>(textobj-sandwich-query-a) textobj#sandwich#query('o', 'a')
xnoremap <silent><expr> <Plug>(textobj-sandwich-query-a) textobj#sandwich#query('x', 'a')
nnoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-i) textobj#sandwich#query('n', 'i', {}, [])
onoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-i) textobj#sandwich#query('o', 'i', {}, [])
xnoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-i) textobj#sandwich#query('x', 'i', {}, [])
nnoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-a) textobj#sandwich#query('n', 'a', {}, [])
onoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-a) textobj#sandwich#query('o', 'a', {}, [])
xnoremap <silent><expr> <Plug>(textobj-sandwich-literal-query-a) textobj#sandwich#query('x', 'a', {}, [])

View File

@ -0,0 +1,65 @@
set encoding=utf-8
execute 'set runtimepath+=' . expand('<sfile>:p:h:h')
runtime! plugin/*.vim
runtime! plugin/**/*.vim
set noswapfile
let g:operator_sandwich_no_visualrepeat = 1
let g:assert = themis#helper('assert')
function! TextobjCoord(l1, c1, l2, c2) abort
normal! v
call cursor(a:l1, a:c1)
normal! o
call cursor(a:l2, a:c2)
endfunction
function! TextobjFail() abort
endfunction
function! TestIndent() abort
" always the indent of the previous nonbland line + shiftwidth()
return indent(prevnonblank(v:lnum)) + shiftwidth()
endfunction
function! SandwichExprCancel() abort
throw 'OperatorSandwichCancel'
endfunction
function! SandwichExprEmpty() abort
return ''
endfunction
function! SandwichExprBuns(is_head) abort
if a:is_head
return 'head'
else
return 'tail'
endif
endfunction
function! SandwichListexprEmpty(which) abort
if a:which ==# 'former'
return ['', 'bar']
elseif a:which ==# 'latter'
return ['foo', '']
else
return ['', '']
endif
endfunction
function! SandwichListexprBuns(cancel) abort
if a:cancel
throw 'OperatorSandwichCancel'
else
return ['foo', 'baz']
endif
endfunction
function! SandwichSkipIntermediate(is_head, pos, ...) abort
if a:is_head
return !(a:pos[2] == 1)
else
return !(a:pos[2] == col([a:pos[1], '$'])-1)
endif
endfunction

View File

@ -0,0 +1,6 @@
Use themis.vim (https://github.com/thinca/vim-themis)
1. git clone https://github.com/thinca/vim-themis.git
2. cd path/to/vim-sandwich
3. path/to/vim-themis/bin/themis (Linux)
path/to/vim-themis/bin/themis.bat (Windows)

View File

@ -0,0 +1,11 @@
@echo off
set VIM=vim
if defined THEMIS_VIM set VIM=%THEMIS_VIM%
%VIM% -u NONE -i NONE -N -n -e -s -S %~dp0\test_dot.vim
if %errorlevel% neq 0 goto ERROR
echo Succeeded.
exit /b 0
:ERROR
exit /b 1

View File

@ -0,0 +1,15 @@
#! /bin/sh
SCRIPT_HOME=$0
if [ -n "`readlink $SCRIPT_HOME`" ] ; then
SCRIPT_HOME="`readlink $SCRIPT_HOME`"
fi
SCRIPT_HOME="`dirname $SCRIPT_HOME`"
VIM=vim
if [ -n "$THEMIS_VIM" ] ; then
VIM="$THEMIS_VIM"
fi
$VIM -u NONE -i NONE -N -n -e -s -S $SCRIPT_HOME/test_dot.vim || exit 1
echo "Succeeded."

View File

@ -0,0 +1,751 @@
if has('win16') || has('win32') || has('win64') || has('win95')
set shellslash
endif
execute 'set runtimepath+=' . expand('<sfile>:p:h:h:h')
source <sfile>:p:h:h:h/plugin/operator/sandwich.vim
source <sfile>:p:h:h:h/plugin/textobj/sandwich.vim
source <sfile>:p:h:h:h/plugin/sandwich.vim
nnoremap <Plug>(test-dot) .
let g:operator_sandwich_no_visualrepeat = 1
function! s:assert(a1, a2, kind) abort
if type(a:a1) == type(a:a2) && string(a:a1) ==# string(a:a2)
return
endif
%delete
call append(0, ['Got:', string(a:a1)])
call append(0, [printf('Failured at "%s"', a:kind), '', 'Expect:', string(a:a2)])
$delete
1,$print
cquit
endfunction
function! s:quit_by_error() abort
%delete
call append(0, [printf('Catched the following error at %s.', v:throwpoint), v:exception])
$delete
1,$print
cquit
endfunction
function! Count(...) abort
let s:count += 1
return s:count
endfunction
function! ListCount(...) abort
let s:count += 1
return [s:count, s:count]
endfunction
try
""" operator-add
" normal use
call setline('.', 'foo')
normal saiw(
normal .
call s:assert(getline('.'), '((foo))', 'operator-add:normal use #1')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-add:normal use #2')
normal saiw[
normal .
call s:assert(getline('.'), '(([[foo]]))', 'operator-add:normal use #3')
call s:assert(getpos('.'), [0, 1, 5, 0], 'operator-add:normal use #4')
%delete
" blockwise-visual
call append(0, ['foo', 'bar', 'baz'])
$delete
execute "normal gg\<C-v>2j2lsa("
normal .
call s:assert(getline(1), '((foo))', 'operator-add:blockwise-visual #1')
call s:assert(getline(2), '((bar))', 'operator-add:blockwise-visual #2')
call s:assert(getline(3), '((baz))', 'operator-add:blockwise-visual #3')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-add:blockwise-visual #4')
normal j.
call s:assert(getline(1), '((foo))', 'operator-add:blockwise-visual #5')
call s:assert(getline(2), '(((bar)))', 'operator-add:blockwise-visual #6')
call s:assert(getline(3), '(((baz)))', 'operator-add:blockwise-visual #7')
call s:assert(getpos('.'), [0, 2, 4, 0], 'operator-add:blockwise-visual #8')
normal j.
call s:assert(getline(1), '((foo))', 'operator-add:blockwise-visual #9')
call s:assert(getline(2), '(((bar)))', 'operator-add:blockwise-visual #10')
call s:assert(getline(3), '((((baz))))', 'operator-add:blockwise-visual #11')
call s:assert(getpos('.'), [0, 3, 5, 0], 'operator-add:blockwise-visual #12')
%delete
" count
call setline('.', 'foo')
normal 2saiw((
normal .
call s:assert(getline('.'), '((((foo))))', 'operator-add:count #1')
call s:assert(getpos('.'), [0, 1, 5, 0], 'operator-add:count #2')
call setline('.', 'foo')
normal saiw(
call setline('.', 'foo bar')
normal 3.
call s:assert(getline('.'), '(foo bar)', 'operator-add:count #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-add:count #4')
%delete
" expr
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 1, 'input': ['c']}]
call setline('.', 'foo')
let s:count = 0
normal saiwc
normal .
call s:assert(getline('.'), '11foo22', 'operator-add:expr #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-add:expr #2')
let g:operator#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 2, 'input': ['c']}]
call setline('.', 'foo')
let s:count = 0
normal saiwc
normal .
call s:assert(getline('.'), '31foo24', 'operator-add:expr #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-add:expr #4')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" listexpr
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 1, 'input': ['c']}]
call setline('.', 'foo')
let s:count = 0
normal saiwc
normal .
call s:assert(getline('.'), '11foo11', 'operator-add:listexpr #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-add:listexpr #2')
let g:operator#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 2, 'input': ['c']}]
call setline('.', 'foo')
let s:count = 0
normal saiwc
normal .
call s:assert(getline('.'), '21foo12', 'operator-add:listexpr #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-add:listexpr #4')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" cursor option 'keep'
call operator#sandwich#set('add', 'all', 'cursor', 'keep')
nmap . <Plug>(operator-sandwich-dot)
call setline('.', 'foo')
normal 0lsaiw(
normal .
call s:assert(getline('.'), '((foo))', 'operator-add:cursor keep #1')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-add:cursor keep #2')
call setline('.', 'foo')
normal 0lsaiw(
call setline('.', 'foo bar')
normal 0l3.
call s:assert(getline('.'), '(foo bar)', 'operator-add:cursor keep #3')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-add:cursor keep #4')
nmap . <Plug>(operator-sandwich-predot)<Plug>(test-dot)
call setline('.', 'foo')
normal 0lsaiw(
normal .
call s:assert(getline('.'), '((foo))', 'operator-add:cursor keep #5')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-add:cursor keep #6')
call setline('.', 'foo')
normal 0lsaiw(
call setline('.', 'foo bar')
normal 0l3.
call s:assert(getline('.'), '(foo bar)', 'operator-add:cursor keep #7')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-add:cursor keep #8')
call operator#sandwich#set_default()
nunmap .
%delete
""" operator-delete
nmap sd <Plug>(operator-sandwich-delete)
xmap sd <Plug>(operator-sandwich-delete)
" normal use
call setline('.', '((foo))')
normal sda(
normal .
call s:assert(getline('.'), 'foo', 'operator-delete:normal use #1')
call s:assert(getpos('.'), [0, 1, 1, 0], 'operator-delete:normal use #2')
call setline('.', '[[foo]]')
normal sda[
normal .
call s:assert(getline('.'), 'foo', 'operator-delete:normal use #3')
call s:assert(getpos('.'), [0, 1, 1, 0], 'operator-delete:normal use #4')
%delete
" blockwise-visual
call append(0, ['(((((foo)))))', '(((((bar)))))', '(((((baz)))))'])
$delete
execute "normal ggffh\<C-v>2j4lsd"
normal h.
call s:assert(getline(1), '(((foo)))', 'operator-delete:blockwise-visual #1')
call s:assert(getline(2), '(((bar)))', 'operator-delete:blockwise-visual #2')
call s:assert(getline(3), '(((baz)))', 'operator-delete:blockwise-visual #3')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-delete:blockwise-visual #4')
normal jh.
call s:assert(getline(1), '(((foo)))', 'operator-delete:blockwise-visual #5')
call s:assert(getline(2), '((bar))', 'operator-delete:blockwise-visual #6')
call s:assert(getline(3), '((baz))', 'operator-delete:blockwise-visual #7')
call s:assert(getpos('.'), [0, 2, 3, 0], 'operator-delete:blockwise-visual #8')
normal jh.
call s:assert(getline(1), '(((foo)))', 'operator-delete:blockwise-visual #9')
call s:assert(getline(2), '((bar))', 'operator-delete:blockwise-visual #10')
call s:assert(getline(3), '(baz)', 'operator-delete:blockwise-visual #11')
call s:assert(getpos('.'), [0, 3, 2, 0], 'operator-delete:blockwise-visual #12')
%delete
" count
call setline('.', '((((foo))))')
normal 02sda(
normal .
call s:assert(getline('.'), 'foo', 'operator-delete:count #1')
call s:assert(getpos('.'), [0, 1, 1, 0], 'operator-delete:count #2')
call setline('.', '[([[foo]])]')
normal ffsda[
normal 2.
call s:assert(getline('.'), '([foo])', 'operator-delete:count #3')
call s:assert(getpos('.'), [0, 1, 1, 0], 'operator-delete:count #4')
%delete
" external textobjct
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'external': ['it', 'at'], 'noremap': 1}]
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
normal ggsdat
normal j.
call s:assert(getline(1), 'fooo', 'operator-delete:external textobject #1')
call s:assert(getline(2), 'bar', 'operator-delete:external textobject #2')
call s:assert(getpos('.'), [0, 2, 1, 0], 'operator-delete:external textobject #3')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" cursor option 'keep'
call operator#sandwich#set('delete', 'all', 'cursor', 'keep')
nmap . <Plug>(operator-sandwich-dot)
call setline('.', '((foo))')
normal 03lsda(
normal .
call s:assert(getline('.'), 'foo', 'operator-delete:cursor keep #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-delete:cursor keep #2')
call setline('.', '(foo)')
normal 0sda(
call setline('.', '((foo) bar)')
normal 03l2.
call s:assert(getline('.'), '(foo) bar', 'operator-delete:cursor keep #3')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-delete:cursor keep #4')
nmap . <Plug>(operator-sandwich-predot)<Plug>(test-dot)
call setline('.', '((foo))')
normal 03lsda(
normal .
call s:assert(getline('.'), 'foo', 'operator-delete:cursor keep #5')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-delete:cursor keep #6')
call setline('.', '(foo)')
normal 0sda(
call setline('.', '((foo) bar)')
normal 03l2.
call s:assert(getline('.'), '(foo) bar', 'operator-delete:cursor keep #7')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-delete:cursor keep #8')
call operator#sandwich#set_default()
nunmap .
%delete
""" operator-replace
nmap sr <Plug>(operator-sandwich-replace)
xmap sr <Plug>(operator-sandwich-replace)
" normal use
call setline('.', '((foo))')
normal 0ffsra([
normal .
call s:assert(getline('.'), '[[foo]]', 'operator-replace:normal use #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:normal use #2')
normal 0ffsra[(
normal .
call s:assert(getline('.'), '((foo))', 'operator-replace:normal use #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:normal use #4')
%delete
" blockwise-visual
call append(0, ['(foo)', '(bar)', '(baz)'])
$delete
execute "normal gg\<C-v>2j4lsr["
call setline(1, '(foo)')
call setline(2, '(bar)')
call setline(3, '(baz)')
normal h.
call s:assert(getline(1), '[foo]', 'operator-replace:blockwise-visual #1')
call s:assert(getline(2), '[bar]', 'operator-replace:blockwise-visual #2')
call s:assert(getline(3), '[baz]', 'operator-replace:blockwise-visual #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:blockwise-visual #4')
call setline(1, '(foo)')
call setline(2, '(bar)')
call setline(3, '(baz)')
normal jh.
call s:assert(getline(1), '(foo)', 'operator-replace:blockwise-visual #5')
call s:assert(getline(2), '[bar]', 'operator-replace:blockwise-visual #6')
call s:assert(getline(3), '[baz]', 'operator-replace:blockwise-visual #7')
call s:assert(getpos('.'), [0, 2, 2, 0], 'operator-replace:blockwise-visual #8')
call setline(1, '(foo)')
call setline(2, '(bar)')
call setline(3, '(baz)')
normal jh.
call s:assert(getline(1), '(foo)', 'operator-replace:blockwise-visual #9')
call s:assert(getline(2), '(bar)', 'operator-replace:blockwise-visual #10')
call s:assert(getline(3), '[baz]', 'operator-replace:blockwise-visual #11')
call s:assert(getpos('.'), [0, 3, 2, 0], 'operator-replace:blockwise-visual #12')
%delete
" count
call setline('.', '((((foo))))')
normal 0ff2sr2a([[
normal .
call s:assert(getline('.'), '[[[[foo]]]]', 'operator-replace:count #1')
call s:assert(getpos('.'), [0, 1, 3, 0], 'operator-replace:count #2')
call setline('.', '[([[foo]])]')
normal 0ffsra[(
normal 2.
call s:assert(getline('.'), '(([(foo)]))', 'operator-replace:count #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:count #2')
%delete
" expr
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 1, 'input': ['c']}, {'buns': ['(', ')']}]
call setline('.', '((foo))')
let s:count = 0
normal ffsra(c
normal .
call s:assert(getline('.'), '11foo22', 'operator-replace:expr #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:expr #2')
let g:operator#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 2, 'input': ['c']}, {'buns': ['(', ')']}]
call setline('.', '((foo))')
let s:count = 0
normal ffsra(c
normal .
call s:assert(getline('.'), '31foo24', 'operator-replace:expr #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:expr #4')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" listexpr
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 1, 'input': ['c']}, {'buns': ['(', ')']}]
call setline('.', '((foo))')
let s:count = 0
normal ffsra(c
normal .
call s:assert(getline('.'), '11foo11', 'operator-replace:listexpr #1')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:listexpr #2')
let g:operator#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 2, 'input': ['c']}, {'buns': ['(', ')']}]
call setline('.', '((foo))')
let s:count = 0
normal ffsra(c
normal .
call s:assert(getline('.'), '21foo12', 'operator-replace:listexpr #3')
call s:assert(getpos('.'), [0, 1, 2, 0], 'operator-replace:listexpr #4')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" external textobjct
let g:sandwich#recipes = []
let g:operator#sandwich#recipes = [{'external': ['it', 'at'], 'noremap': 1}, {'buns': ['(', ')']}]
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
normal ggsrat(
normal j.
call s:assert(getline(1), '(fooo)', 'operator-replace:external textobject #1')
call s:assert(getline(2), '(bar)', 'operator-replace:external textobject #2')
call s:assert(getpos('.'), [0, 2, 2, 0], 'operator-replace:external textobject #3')
unlet g:sandwich#recipes
unlet g:operator#sandwich#recipes
%delete
" cursor option 'keep'
call operator#sandwich#set('replace', 'all', 'cursor', 'keep')
nmap . <Plug>(operator-sandwich-dot)
call setline('.', '((foo))')
normal 03lsra([
normal .
call s:assert(getline('.'), '[[foo]]', 'operator-delete:cursor keep #1')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-delete:cursor keep #2')
call setline('.', '(foo)')
normal 0sra([
call setline('.', '((foo) bar)')
normal 03l2.
call s:assert(getline('.'), '[(foo) bar]', 'operator-delete:cursor keep #3')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-delete:cursor keep #4')
nmap . <Plug>(operator-sandwich-predot)<Plug>(test-dot)
call setline('.', '((foo))')
normal 03lsra([
normal .
call s:assert(getline('.'), '[[foo]]', 'operator-delete:cursor keep #5')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-delete:cursor keep #6')
call setline('.', '(foo)')
normal 0sra([
call setline('.', '((foo) bar)')
normal 03l2.
call s:assert(getline('.'), '[(foo) bar]', 'operator-delete:cursor keep #7')
call s:assert(getpos('.'), [0, 1, 4, 0], 'operator-delete:cursor keep #8')
call operator#sandwich#set_default()
nunmap .
%delete
""" textobj-query
" normal use
call setline('.', '(foo)')
normal dis(
call setline('.', '(foo)')
normal .
call s:assert(getline('.'), '()', 'textobj-query:normal use #1')
call setline('.', '(foo)')
normal das(
call setline('.', '(foo)')
normal .
call s:assert(getline('.'), '', 'textobj-query:normal use #2')
%delete
" count
call setline('.', '((foo))((bar))')
normal 0ffdis(
normal 0fb2.
call s:assert(getline('.'), '(())()', 'textobj-query:count #1')
call setline('.', '((foo))((bar))')
normal 0ffdas(
normal 0fb2.
call s:assert(getline('.'), '()', 'textobj-query:count #2')
%delete
" expr
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 1, 'input': ['c']}]
call setline('.', '1foo2')
let s:count = 0
normal disc
call setline('.', '1foo2')
normal .
call s:assert(getline('.'), '12', 'textobj-query:expr #1')
call setline('.', '1foo2')
let s:count = 0
normal dasc
call setline('.', '1foo2')
normal .
call s:assert(getline('.'), '', 'textobj-query:expr #2')
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 2, 'input': ['c']}]
call setline('.', '1foo2')
let s:count = 0
normal disc
call setline('.', '3foo4')
normal .
call s:assert(getline('.'), '34', 'textobj-query:expr #3')
call setline('.', '1foo2')
let s:count = 0
normal dasc
call setline('.', '3foo4')
normal .
call s:assert(getline('.'), '', 'textobj-query:expr #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" listexpr
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 1, 'input': ['c']}]
call setline('.', '1foo1')
let s:count = 0
normal disc
call setline('.', '1foo1')
normal .
call s:assert(getline('.'), '11', 'textobj-query:listexpr #1')
call setline('.', '1foo1')
let s:count = 0
normal dasc
call setline('.', '1foo1')
normal .
call s:assert(getline('.'), '', 'textobj-query:listexpr #2')
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 2, 'input': ['c']}]
call setline('.', '1foo1')
let s:count = 0
normal disc
call setline('.', '2foo2')
normal .
call s:assert(getline('.'), '22', 'textobj-query:listexpr #3')
call setline('.', '1foo1')
let s:count = 0
normal dasc
call setline('.', '2foo2')
normal .
call s:assert(getline('.'), '', 'textobj-query:listexpr #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" external textobjct
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'external': ['it', 'at'], 'noremap': 1, 'input': ['t']}]
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
$delete
normal ggdist
normal j.
call s:assert(getline(1), '<title></title>', 'textobj-query:external textobject #1')
call s:assert(getline(2), '<body></body>', 'textobj-query:external textobject #2')
%delete
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
$delete
normal ggdast
normal j.
call s:assert(getline(1), '', 'textobj-query:external textobject #3')
call s:assert(getline(2), '', 'textobj-query:external textobject #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" synchro option
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['foo', 'baz'], 'synchro': 1, 'input': ['f']}]
call append(0, ['foo bar baz', ' foo baaaaar baz'])
$delete
normal ggdisf
normal jl.
call s:assert(getline(1), 'foobaz', 'textobj-query: synchro option #1')
call s:assert(getline(2), ' foobaz', 'textobj-query: synchro option #2')
%delete
call append(0, ['foo bar baz', ' foo baaaaar baz'])
$delete
normal ggdasf
normal jl.
call s:assert(getline(1), '', 'textobj-query: synchro option #3')
call s:assert(getline(2), ' ', 'textobj-query: synchro option #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
""" textobj-auto
" normal use
call setline('.', '(foo)')
normal dib
call setline('.', '(foo)')
normal .
call s:assert(getline('.'), '()', 'textobj-auto:normal use #1')
call setline('.', '(foo)')
normal dab
call setline('.', '(foo)')
normal .
call s:assert(getline('.'), '', 'textobj-auto:normal use #2')
%delete
" count
call setline('.', '((foo))((bar))')
normal 0ffdib
normal 0fb2.
call s:assert(getline('.'), '(())()', 'textobj-auto:count #1')
call setline('.', '((foo))((bar))')
normal 0ffdab
normal 0fb2.
call s:assert(getline('.'), '()', 'textobj-auto:count #2')
%delete
" expr
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 1, 'input': ['c']}]
call setline('.', '1foo2')
let s:count = 0
normal dib
call setline('.', '1foo2')
normal .
call s:assert(getline('.'), '12', 'textobj-auto:expr #1')
call setline('.', '1foo2')
let s:count = 0
normal dab
call setline('.', '1foo2')
normal .
call s:assert(getline('.'), '', 'textobj-auto:expr #2')
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['Count()', 'Count()'], 'expr': 2, 'input': ['c']}]
call setline('.', '1foo2')
let s:count = 0
normal dib
call setline('.', '3foo4')
normal .
call s:assert(getline('.'), '34', 'textobj-auto:expr #3')
call setline('.', '1foo2')
let s:count = 0
normal dab
call setline('.', '3foo4')
normal .
call s:assert(getline('.'), '', 'textobj-auto:expr #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" listexpr
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 1, 'input': ['c']}]
call setline('.', '1foo1')
let s:count = 0
normal dib
call setline('.', '1foo1')
normal .
call s:assert(getline('.'), '11', 'textobj-query:listexpr #1')
call setline('.', '1foo1')
let s:count = 0
normal dab
call setline('.', '1foo1')
normal .
call s:assert(getline('.'), '', 'textobj-query:listexpr #2')
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': 'ListCount()', 'listexpr': 2, 'input': ['c']}]
call setline('.', '1foo1')
let s:count = 0
normal dib
call setline('.', '2foo2')
normal .
call s:assert(getline('.'), '22', 'textobj-query:listexpr #3')
call setline('.', '1foo1')
let s:count = 0
normal dab
call setline('.', '2foo2')
normal .
call s:assert(getline('.'), '', 'textobj-query:listexpr #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" external textobjct
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'external': ['it', 'at'], 'noremap': 1, 'input': ['t']}]
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
$delete
normal ggdib
normal j.
call s:assert(getline(1), '<title></title>', 'textobj-auto:external textobject #1')
call s:assert(getline(2), '<body></body>', 'textobj-auto:external textobject #2')
%delete
call append(0, ['<title>fooo</title>', '<body>bar</body>'])
$delete
normal ggdab
normal j.
call s:assert(getline(1), '', 'textobj-auto:external textobject #3')
call s:assert(getline(2), '', 'textobj-auto:external textobject #4')
unlet g:sandwich#recipes
unlet g:textobj#sandwich#recipes
%delete
" synchro option
let g:sandwich#recipes = []
let g:textobj#sandwich#recipes = [{'buns': ['foo', 'baz'], 'synchro': 1, 'input': ['f']}]
call append(0, ['foo bar baz', ' foo baaaaar baz'])
$delete
normal ggdib
normal jl.
call s:assert(getline(1), 'foobaz', 'textobj-auto: synchro option #1')
call s:assert(getline(2), ' foobaz', 'textobj-auto: synchro option #2')
%delete
call append(0, ['foo bar baz', ' foo baaaaar baz'])
$delete
normal ggdab
normal jl.
call s:assert(getline(1), '', 'textobj-auto: synchro option #3')
call s:assert(getline(2), ' ', 'textobj-auto: synchro option #4')
%delete
catch
call s:quit_by_error()
endtry
qall!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
scriptencoding utf-8
let s:suite = themis#suite('keymappings')
function! s:suite.before() abort "{{{
runtime macros/sandwich/keymap/surround.vim
endfunction "}}}
function! s:suite.before_each() abort "{{{
%delete
set filetype=
call operator#sandwich#set_default()
unlet! g:sandwich#recipes
unlet! g:operator#sandwich#recipes
endfunction
"}}}
function! s:suite.after() abort "{{{
call s:suite.before_each()
mapclear
unlet g:sandwich_no_default_key_mappings
unlet g:operator_sandwich_no_default_key_mappings
unlet g:textobj_sandwich_no_default_key_mappings
runtime plugin/sandwich.vim
runtime plugin/operator/sandwich.vim
runtime plugin/textobj/sandwich.vim
endfunction
"}}}
function! s:suite.surround() abort "{{{
call setline('.', 'foo')
normal 0ysiw(
call g:assert.equals(getline('.'), '(foo)', 'failed at #1')
call setline('.', 'foo bar')
normal 0yss(
call g:assert.equals(getline('.'), '(foo bar)', 'failed at #2')
call setline('.', 'foo bar')
normal 04lyS(
call g:assert.equals(getline('.'), 'foo (bar)', 'failed at #3')
call setline('.', '[(foo)]')
normal 02lds(
call g:assert.equals(getline('.'), '[foo]', 'failed at #4')
call setline('.', '[(foo)]')
normal 02lds[
call g:assert.equals(getline('.'), '(foo)', 'failed at #5')
call setline('.', '[(foo)]')
normal 02ldss
call g:assert.equals(getline('.'), '[foo]', 'failed at #6')
call setline('.', '"''foo''"')
normal 02ldss
call g:assert.equals(getline('.'), '"foo"', 'failed at #7')
call setline('.', '[(foo)]')
normal 02lcs({
call g:assert.equals(getline('.'), '[{foo}]', 'failed at #8')
call setline('.', '[(foo)]')
normal 02lcs[{
call g:assert.equals(getline('.'), '{(foo)}', 'failed at #9')
call setline('.', '[(foo)]')
normal 02lcss{
call g:assert.equals(getline('.'), '[{foo}]', 'failed at #10')
call setline('.', '"''foo''"')
normal 02lcss`
call g:assert.equals(getline('.'), '"`foo`"', 'failed at #11')
call setline('.', 'foo')
normal 0viwS(
call g:assert.equals(getline('.'), '(foo)', 'failed at #12')
endfunction "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,72 @@
scriptencoding utf-8
let s:suite = themis#suite('magicchar-f: ')
function! s:suite.before() abort "{{{
nmap sd <Plug>(sandwich-delete)
xmap sd <Plug>(sandwich-delete)
endfunction
"}}}
function! s:suite.before_each() abort "{{{
%delete
set filetype=
unlet! g:sandwich#magicchar#f#patterns
unlet! b:sandwich_magicchar_f_patterns
unlet! g:sandwich#recipes g:operator#sandwich#recipes
unlet! b:sandwich_recipes b:operator_sandwich_recipes
call operator#sandwich#set_default()
endfunction
"}}}
function! s:suite.after() abort "{{{
call s:suite.before_each()
nunmap sd
xunmap sd
endfunction
"}}}
function! s:suite.default_pattern() abort "{{{
call setline('.', 'foo(bar)')
call cursor(1, 1)
normal sdf
call g:assert.equals(getline('.'), 'bar', 'failed at #1')
endfunction "}}}
function! s:suite.global_pattern() abort "{{{
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
call setline('.', 'foo.bar(baz)')
call cursor(1, 5)
normal sdf
call g:assert.equals(getline('.'), 'baz', 'failed at #1')
endfunction "}}}
function! s:suite.local_pattern() abort "{{{
let g:sandwich#magicchar#f#patterns = [
\ {
\ 'header' : '\<\h\k*\.\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
let b:sandwich_magicchar_f_patterns = [
\ {
\ 'header' : '\<\%(\h\k*\.\)*\h\k*',
\ 'bra' : '(',
\ 'ket' : ')',
\ 'footer' : '',
\ },
\ ]
call setline('.', 'foo.bar.baz(qux)')
call cursor(1, 9)
normal sdf
call g:assert.equals(getline('.'), 'qux', 'failed at #1')
endfunction "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,536 @@
scriptencoding utf-8
let s:suite = themis#suite('magicchar-t emmet-like behavior:')
let s:scope = themis#helper('scope')
let s:t = s:scope.funcs('autoload/sandwich/magicchar/t.vim')
" test seeds "{{{
let s:testseeds = {}
let s:testseeds.element = [
\ {
\ 'input': 'element',
\ 'token': ['element'],
\ 'items': [{'name': 'element', 'value': 'element'}],
\ },
\
\ {
\ 'input': '',
\ 'token': [],
\ 'items': [{'name': 'element', 'value': 'div'}],
\ },
\ ]
let s:testseeds.attributes = [
\ {
\ 'input': '#id1',
\ 'token': ['#', 'id1'],
\ 'items': [{'name': 'id', 'value': 'id1'}],
\ },
\
\ {
\ 'input': '#id2',
\ 'token': ['#', 'id2'],
\ 'items': [{'name': 'id', 'value': 'id2'}],
\ },
\
\ {
\ 'input': '#',
\ 'token': ['#'],
\ 'items': [{'name': 'id', 'value': ''}],
\ },
\
\ {
\ 'input': '.class1',
\ 'token': ['.', 'class1'],
\ 'items': [{'name': 'class', 'value': ['class1']}],
\ },
\
\ {
\ 'input': '.class2',
\ 'token': ['.', 'class2'],
\ 'items': [{'name': 'class', 'value': ['class2']}],
\ },
\
\ {
\ 'input': '.',
\ 'token': ['.'],
\ 'items': [{'name': 'class', 'value': ['']}],
\ },
\
\ {
\ 'input': '[attr=hello]',
\ 'token': ['[', 'attr', '=', 'hello', ']'],
\ 'items': [{'name': 'attr', 'value': 'hello'}],
\ },
\
\ {
\ 'input': '[attr=world]',
\ 'token': ['[', 'attr', '=', 'world', ']'],
\ 'items': [{'name': 'attr', 'value': 'world'}],
\ },
\
\ {
\ 'input': '[hello=world]',
\ 'token': ['[', 'hello', '=', 'world', ']'],
\ 'items': [{'name': 'hello', 'value': 'world'}],
\ },
\
\ {
\ 'input': '[]',
\ 'token': ['[', ']'],
\ 'items': [],
\ },
\
\ {
\ 'input': '[attr="including space"]',
\ 'token': ['[', 'attr', '=', '"including space"', ']'],
\ 'items': [{'name': 'attr', 'value': '"including space"'}],
\ },
\
\ {
\ 'input': '[ attr="including space"]',
\ 'token': ['[', ' ', 'attr', '=', '"including space"', ']'],
\ 'items': [{'name': 'attr', 'value': '"including space"'}],
\ },
\
\ {
\ 'input': '[attr ="including space"]',
\ 'token': ['[', 'attr', ' ', '=', '"including space"', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': '', 'value': '"including space"'},
\ ],
\ },
\
\ {
\ 'input': '[attr= "including space"]',
\ 'token': ['[', 'attr', '=', ' ', '"including space"', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': '"including space"', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': '[attr="including space" ]',
\ 'token': ['[', 'attr', '=', '"including space"', ' ', ']'],
\ 'items': [{'name': 'attr', 'value': '"including space"'}],
\ },
\
\ {
\ 'input': "[attr='including space']",
\ 'token': ['[', 'attr', '=', "'including space'", ']'],
\ 'items': [{'name': 'attr', 'value': "'including space'"}],
\ },
\
\ {
\ 'input': "[ attr='including space']",
\ 'token': ['[', ' ', 'attr', '=', "'including space'", ']'],
\ 'items': [{'name': 'attr', 'value': "'including space'"}],
\ },
\
\ {
\ 'input': "[attr ='including space']",
\ 'token': ['[', 'attr', ' ', '=', "'including space'", ']'],
\ 'items': [
\ {'name': 'attr', 'value': ""},
\ {'name': '', 'value': "'including space'"},
\ ],
\ },
\
\ {
\ 'input': "[attr= 'including space']",
\ 'token': ['[', 'attr', '=', ' ', "'including space'", ']'],
\ 'items': [
\ {'name': 'attr', 'value': ""},
\ {'name': "'including space'", 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[attr='including space' ]",
\ 'token': ['[', 'attr', '=', "'including space'", ' ', ']'],
\ 'items': [{'name': 'attr', 'value': "'including space'"}],
\ },
\
\ {
\ 'input': "[attr=withoutspace]",
\ 'token': ['[', 'attr', '=', 'withoutspace', ']'],
\ 'items': [{'name': 'attr', 'value': 'withoutspace'}],
\ },
\
\ {
\ 'input': "[ attr=withoutspace]",
\ 'token': ['[', ' ', 'attr', '=', 'withoutspace', ']'],
\ 'items': [{'name': 'attr', 'value': 'withoutspace'}],
\ },
\
\ {
\ 'input': "[attr =withoutspace]",
\ 'token': ['[', 'attr', ' ', '=', 'withoutspace', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': '', 'value': 'withoutspace'},
\ ],
\ },
\
\ {
\ 'input': "[attr= withoutspace]",
\ 'token': ['[', 'attr', '=', ' ', 'withoutspace', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': 'withoutspace', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[attr=withoutspace ]",
\ 'token': ['[', 'attr', '=', 'withoutspace', ' ', ']'],
\ 'items': [{'name': 'attr', 'value': 'withoutspace'}],
\ },
\
\ {
\ 'input': "[attr=]",
\ 'token': ['[', 'attr', '=', ']'],
\ 'items': [{'name': 'attr', 'value': ''}],
\ },
\
\ {
\ 'input': "[ attr=]",
\ 'token': ['[', ' ', 'attr', '=', ']'],
\ 'items': [{'name': 'attr', 'value': ''}],
\ },
\
\ {
\ 'input': "[attr =]",
\ 'token': ['[', 'attr', ' ', '=', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[attr= ]",
\ 'token': ['[', 'attr', '=', ' ', ']'],
\ 'items': [{'name': 'attr', 'value': ''}],
\ },
\
\ {
\ 'input': "[=value]",
\ 'token': ['[', '=', 'value', ']'],
\ 'items': [{'name': '', 'value': 'value'}],
\ },
\
\ {
\ 'input': "[ =value]",
\ 'token': ['[', ' ', '=', 'value', ']'],
\ 'items': [{'name': '', 'value': 'value'}],
\ },
\
\ {
\ 'input': "[= value]",
\ 'token': ['[', '=', ' ', 'value', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': 'value', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[=value ]",
\ 'token': ['[', '=', 'value', ' ', ']'],
\ 'items': [{'name': '', 'value': 'value'}],
\ },
\
\ {
\ 'input': "[==]",
\ 'token': ['[', '=', '=', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[ ==]",
\ 'token': ['[', ' ', '=', '=', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[= =]",
\ 'token': ['[', '=', ' ', '=', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[== ]",
\ 'token': ['[', '=', '=', ' ', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[attr==]",
\ 'token': ['[', 'attr', '=', '=', ']'],
\ 'items': [
\ {'name': 'attr', 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[=value=]",
\ 'token': ['[', '=', 'value', '=', ']'],
\ 'items': [
\ {'name': '', 'value': 'value'},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[==value]",
\ 'token': ['[', '=', '=', 'value', ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': 'value'},
\ ],
\ },
\
\ {
\ 'input': "['word1 word2'==]",
\ 'token': ['[', "'word1 word2'", '=', '=', ']'],
\ 'items': [
\ {'name': "'word1 word2'", 'value': ''},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[='word1 word2'=]",
\ 'token': ['[', '=', "'word1 word2'", '=', ']'],
\ 'items': [
\ {'name': '', 'value': "'word1 word2'"},
\ {'name': '', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[=='word1 word2']",
\ 'token': ['[', '=', '=', "'word1 word2'", ']'],
\ 'items': [
\ {'name': '', 'value': ''},
\ {'name': '', 'value': "'word1 word2'"},
\ ],
\ },
\
\ {
\ 'input': "[attr1=value1 attr2=value2]",
\ 'token': ['[', 'attr1', '=', 'value1', ' ', 'attr2', '=', 'value2', ']'],
\ 'items': [
\ {'name': 'attr1', 'value': 'value1'},
\ {'name': 'attr2', 'value': 'value2'},
\ ],
\ },
\
\ {
\ 'input': "[attr1=value1 attr2]",
\ 'token': ['[', 'attr1', '=', 'value1', ' ', 'attr2', ']'],
\ 'items': [
\ {'name': 'attr1', 'value': 'value1'},
\ {'name': 'attr2', 'value': ''},
\ ],
\ },
\
\ {
\ 'input': "[attr1 attr2=value2]",
\ 'token': ['[', 'attr1', ' ', 'attr2', '=', 'value2', ']'],
\ 'items': [
\ {'name': 'attr1', 'value': ''},
\ {'name': 'attr2', 'value': 'value2'},
\ ],
\ },
\
\ {
\ 'input': "[attr1 attr2]",
\ 'token': ['[', 'attr1', ' ', 'attr2', ']'],
\ 'items': [
\ {'name': 'attr1', 'value': ''},
\ {'name': 'attr2', 'value': ''},
\ ],
\ },
\ ]
function! s:testseeds.generate_parsed(itemlist) dict abort "{{{
let itemlist = deepcopy(a:itemlist)
call s:overwrite(itemlist, 'id')
call s:append(itemlist, 'class')
let custom_attr_list = map(filter(deepcopy(itemlist), 'has_key(v:val, "name") && v:val.name !~# ''\%(element\|id\|class\)'''), 'v:val.name')
call s:uniq(filter(custom_attr_list, 'v:val !=# ""'))
for attr in custom_attr_list
call s:overwrite(itemlist, attr)
endfor
return itemlist
endfunction
"}}}
function! s:overwrite(itemlist, name) abort "{{{
let i = 0
let n = len(a:itemlist)
let i_target = -1
while i < n
let item = a:itemlist[i]
if item.name ==# a:name
let i_target = i
break
endif
let i += 1
endwhile
if i_target > 0
let i = n - 1
let value = ''
let value_is_fixed = 0
while i > i_target
let item = a:itemlist[i]
if item.name ==# a:name
if !value_is_fixed
let value = item.value
let value_is_fixed = 1
endif
call remove(a:itemlist, i)
endif
let i -= 1
endwhile
if value_is_fixed
let a:itemlist[i_target]['value'] = value
endif
endif
endfunction
"}}}
function! s:append(itemlist, name) abort "{{{
let i = 0
let n = len(a:itemlist)
let i_target = -1
while i < n
let item = a:itemlist[i]
if item.name ==# a:name
let i_target = i
break
endif
let i += 1
endwhile
if i_target > 0
let i = n - 1
let value = []
while i > i_target
let item = a:itemlist[i]
if item.name ==# a:name
let value += item.value
call remove(a:itemlist, i)
endif
let i -= 1
endwhile
if value !=# []
let a:itemlist[i_target]['value'] += reverse(value)
endif
endif
endfunction
"}}}
function! s:uniq(list) abort "{{{
let i = len(a:list) - 1
while i > 0
let item = a:list[i]
if count(a:list, item) > 1
call remove(a:list, i)
endif
let i -= 1
endwhile
return a:list
endfunction
"}}}
"}}}
function! s:suite.tokenize() dict abort "{{{
" happy paths
" 1 seed
for element in deepcopy(s:testseeds.element)
for attribute1 in deepcopy(s:testseeds.attributes)
let input = join([element.input, attribute1.input], '')
let expect = element.token + attribute1.token
call g:assert.equals(s:t.tokenize(input), expect, 'input: ' . input)
endfor
endfor
" 2 seeds
for element in deepcopy(s:testseeds.element)
for attribute1 in deepcopy(s:testseeds.attributes)
for attribute2 in deepcopy(s:testseeds.attributes)
let input = join([element.input, attribute1.input, attribute2.input], '')
let expect = element.token + attribute1.token + attribute2.token
call g:assert.equals(s:t.tokenize(input), expect, 'input: ' . input)
endfor
endfor
endfor
" " 3 seeds
" for element in deepcopy(s:testseeds.element)
" for attribute1 in deepcopy(s:testseeds.attributes)
" for attribute2 in deepcopy(s:testseeds.attributes)
" for attribute3 in deepcopy(s:testseeds.attributes)
" let input = join([element.input, attribute1.input, attribute2.input, attribute3.input], '')
" let expect = element.token + attribute1.token + attribute2.token + attribute3.token
" call g:assert.equals(s:t.tokenize(input), expect, 'input: ' . input)
" endfor
" endfor
" endfor
" endfor
endfunction
"}}}
function! s:suite.parse() dict abort "{{{
" happy paths
" 1 seed
for element in deepcopy(s:testseeds.element)
for attribute1 in deepcopy(s:testseeds.attributes)
let input = element.token + attribute1.token
let expect = s:testseeds.generate_parsed(element.items + attribute1.items)
call g:assert.equals(s:t.parse(input), expect, 'input: ' . string(input))
endfor
endfor
" 2 seeds
for element in deepcopy(s:testseeds.element)
for attribute1 in deepcopy(s:testseeds.attributes)
for attribute2 in deepcopy(s:testseeds.attributes)
let input = element.token + attribute1.token + attribute2.token
let expect = s:testseeds.generate_parsed(element.items + attribute1.items + attribute2.items)
call g:assert.equals(s:t.parse(input), expect, 'input: ' . string(input))
endfor
endfor
endfor
" " 3 seeds
" for element in deepcopy(s:testseeds.element)
" for attribute1 in deepcopy(s:testseeds.attributes)
" for attribute2 in deepcopy(s:testseeds.attributes)
" for attribute3 in deepcopy(s:testseeds.attributes)
" let input = element.token + attribute1.token + attribute2.token + attribute3.token
" let expect = s:testseeds.generate_parsed(element.items + attribute1.items + attribute2.items + attribute3.items)
" call g:assert.equals(s:t.parse(input), expect, 'input: ' . string(input))
" endfor
" endfor
" endfor
" endfor
endfunction
"}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

View File

@ -0,0 +1,91 @@
scriptencoding utf-8
let s:suite = themis#suite('magicchar-t:')
let s:scope = themis#helper('scope')
let s:t = s:scope.funcs('autoload/sandwich/magicchar/t.vim')
function! s:suite.before() abort "{{{
nmap sa <Plug>(sandwich-add)
xmap sa <Plug>(sandwich-add)
omap sa <Plug>(sandwich-add)
nmap sd <Plug>(sandwich-delete)
xmap sd <Plug>(sandwich-delete)
nmap sr <Plug>(sandwich-replace)
xmap sr <Plug>(sandwich-replace)
endfunction
"}}}
function! s:suite.after() abort "{{{
nunmap sa
xunmap sa
ounmap sa
nunmap sd
xunmap sd
nunmap sr
xunmap sr
endfunction
"}}}
function! s:suite.add() abort "{{{
" #1
call setline(1, 'foo')
execute "normal 1Gsaiwtp\<CR>"
call g:assert.equals(getline(1), '<p>foo</p>', 'failed at #1')
" #2
call setline(1, 'foo')
execute "normal 1GsaiwTp\<CR>"
call g:assert.equals(getline(1), '<p>foo</p>', 'failed at #2')
endfunction "}}}
function! s:suite.delete() abort "{{{
" #1
call setline(1, '<p>foo</p>')
normal 1Gsdt
call g:assert.equals(getline(1), 'foo', 'failed at #1')
" #2
call setline(1, '<p>foo</p>')
normal 1GsdT
call g:assert.equals(getline(1), 'foo', 'failed at #2')
endfunction "}}}
function! s:suite.replace() abort "{{{
" #1
call setline(1, '<p>foo</p>')
execute "normal 1Gffsrttdiv\<CR>"
call g:assert.equals(getline(1), '<div>foo</div>', 'failed at #1')
" #2
call setline(1, '<p>foo</p>')
execute "normal 1GffsrTTdiv\<CR>"
call g:assert.equals(getline(1), '<div>foo</div>', 'failed at #2')
" #3
call setline(1, '<div title="foo">foo</div>')
execute "normal 1Gffsrttp\<CR>"
call g:assert.equals(getline(1), '<p title="foo">foo</p>', 'failed at #3')
" #4
call setline(1, '<div title="foo">foo</div>')
execute "normal 1GffsrTTp\<CR>"
call g:assert.equals(getline(1), '<p>foo</p>', 'failed at #4')
endfunction "}}}
function! s:suite.add_selection_exclusive() abort "{{{
set selection=exclusive
call s:suite.add()
set selection=inclusive
endfunction "}}}
function! s:suite.delete_selection_exclusive() abort "{{{
set selection=exclusive
call s:suite.delete()
set selection=inclusive
endfunction "}}}
function! s:suite.replace_selection_exclusive() abort "{{{
set selection=exclusive
call s:suite.replace()
set selection=inclusive
endfunction "}}}
" vim:set foldmethod=marker:
" vim:set commentstring="%s:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff