mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 06:30:03 +08:00
366 lines
13 KiB
VimL
Vendored
366 lines
13 KiB
VimL
Vendored
"=============================================================================
|
|
" FILE: autoload/asterisk.vim
|
|
" AUTHOR: haya14busa
|
|
" License: MIT license {{{
|
|
" Permission is hereby granted, free of charge, to any person obtaining
|
|
" a copy of this software and associated documentation files (the
|
|
" "Software"), to deal in the Software without restriction, including
|
|
" without limitation the rights to use, copy, modify, merge, publish,
|
|
" distribute, sublicense, and/or sell copies of the Software, and to
|
|
" permit persons to whom the Software is furnished to do so, subject to
|
|
" the following conditions:
|
|
"
|
|
" The above copyright notice and this permission notice shall be included
|
|
" in all copies or substantial portions of the Software.
|
|
"
|
|
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
" OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
" IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
" CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
" TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
" SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
" }}}
|
|
"=============================================================================
|
|
scriptencoding utf-8
|
|
" Saving 'cpoptions' {{{
|
|
let s:save_cpo = &cpo
|
|
set cpo&vim
|
|
" }}}
|
|
|
|
let s:TRUE = !0
|
|
let s:FALSE = 0
|
|
let s:INT = { 'MAX': 2147483647 }
|
|
let s:DIRECTION = { 'forward': 1, 'backward': 0 } " see :h v:searchforward
|
|
|
|
let g:asterisk#keeppos = get(g:, 'asterisk#keeppos', s:FALSE)
|
|
|
|
" do_jump: do not move cursor if false
|
|
" is_whole: is_whole word. false if `g` flag given (e.g. * -> true, g* -> false)
|
|
let s:_config = {
|
|
\ 'direction' : s:DIRECTION.forward,
|
|
\ 'do_jump' : s:TRUE,
|
|
\ 'is_whole' : s:TRUE,
|
|
\ 'keeppos': s:FALSE
|
|
\ }
|
|
|
|
function! s:default_config() abort
|
|
return extend(deepcopy(s:_config), {'keeppos': g:asterisk#keeppos})
|
|
endfunction
|
|
|
|
" @return command: String
|
|
function! asterisk#do(mode, config) abort
|
|
let config = extend(s:default_config(), a:config)
|
|
let is_visual = s:is_visual(a:mode)
|
|
" Raw cword without \<\>
|
|
let cword = (is_visual ? s:get_selected_text() : s:escape_pattern(expand('<cword>')))
|
|
if cword is# ''
|
|
return s:generate_error_cmd(is_visual)
|
|
endif
|
|
" v:count handling
|
|
let should_plus_one_count = s:should_plus_one_count(cword, config, a:mode)
|
|
let maybe_count = (should_plus_one_count ? string(v:count1 + 1) : '')
|
|
let pre = (is_visual || should_plus_one_count ? "\<Esc>" . maybe_count : '')
|
|
" Including \<\> if necessary
|
|
let pattern = (is_visual ?
|
|
\ s:convert_2_word_pattern_4_visual(cword, config) : s:cword_pattern(cword, config))
|
|
let key = (config.direction is s:DIRECTION.forward ? '/' : '?')
|
|
" Get offset in current word
|
|
let offset = config.keeppos ? s:get_pos_in_cword(cword, a:mode) : 0
|
|
let pattern_offseted = pattern . (offset is 0 ? '' : key . 's+' . offset)
|
|
let search_cmd = pre . key . pattern_offseted
|
|
if config.do_jump
|
|
return search_cmd . "\<CR>"
|
|
elseif config.keeppos && offset isnot 0
|
|
"" Do not jump with keeppos feature
|
|
" NOTE: It doesn't move cursor, so we can assume it works with
|
|
" operator pending mode even if it returns command to execute.
|
|
let echo = s:generate_echo_cmd(pattern_offseted)
|
|
let restore = s:generate_restore_cmd()
|
|
"" *premove* & *aftermove* : not to cause flickr as mush as possible
|
|
" flick corner case: `#` with under cursor word at the top of window
|
|
" and the cursor is at the end of the word.
|
|
let premove =
|
|
\ (a:mode isnot# 'n' ? "\<Esc>" : '')
|
|
\ . 'm`'
|
|
\ . (config.direction is s:DIRECTION.forward ? '0' : '$')
|
|
" NOTE: Neovim doesn't stack pos to jumplist after "m`".
|
|
" https://github.com/haya14busa/vim-asterisk/issues/34
|
|
if has('nvim')
|
|
let aftermove = '``'
|
|
else
|
|
let aftermove = "\<C-o>"
|
|
endif
|
|
" NOTE: To avoid hit-enter prompt, it execute `restore` and `echo`
|
|
" command separately. I can also implement one function and call it
|
|
" once instead of separately, should I do this?
|
|
return printf("%s%s\<CR>%s:%s\<CR>:%s\<CR>", premove, search_cmd, aftermove, restore, echo)
|
|
else " Do not jump: Just handle search related
|
|
call s:set_search(pattern)
|
|
return s:generate_set_search_cmd(pattern, a:mode, config)
|
|
endif
|
|
endfunction
|
|
|
|
"" For keeppos feature
|
|
" NOTE: To avoid hit-enter prompt, this function name should be as short as
|
|
" possible. `r` is short for restore. Should I use more short function using
|
|
" basic global function instead of autoload one.
|
|
function! asterisk#r() abort
|
|
call winrestview(s:w)
|
|
call s:restore_event_ignore()
|
|
endfunction
|
|
|
|
function! s:set_view(view) abort
|
|
let s:w = a:view
|
|
endfunction
|
|
|
|
"" For keeppos feature
|
|
" NOTE: vim-asterisk moves cursor temporarily for keeppos feature with search
|
|
" commands. It should not trigger the event related to this cursor move, so
|
|
" set eventignore and restore it afterwards.
|
|
function! s:set_event_ignore() abort
|
|
let s:ei = &ei
|
|
let events = ['CursorMoved']
|
|
if exists('##CmdlineEnter')
|
|
let events += ['CmdlineEnter', 'CmdlineLeave']
|
|
endif
|
|
let &ei = join(events, ',')
|
|
endfunction
|
|
|
|
function! s:restore_event_ignore() abort
|
|
let &ei = s:ei
|
|
endfunction
|
|
|
|
" @return restore_command: String
|
|
function! s:generate_restore_cmd() abort
|
|
call s:set_view(winsaveview())
|
|
call s:set_event_ignore()
|
|
return 'call asterisk#r()'
|
|
endfunction
|
|
|
|
" @return \<cword\> if needed: String
|
|
function! s:cword_pattern(cword, config) abort
|
|
return printf((a:config.is_whole && a:cword =~# '\k' ? '\<%s\>' : '%s'), a:cword)
|
|
endfunction
|
|
|
|
" This function is based on https://github.com/thinca/vim-visualstar
|
|
" Author : thinca <thinca+vim@gmail.com>
|
|
" License : zlib License
|
|
" @return \<selected_pattern\>: String
|
|
function! s:convert_2_word_pattern_4_visual(pattern, config) abort
|
|
let text = a:pattern
|
|
let type = (a:config.direction is# s:DIRECTION.forward ? '/' : '?')
|
|
let [pre, post] = ['', '']
|
|
if a:config.is_whole
|
|
let [head_pos, tail_pos] = s:sort_pos([s:getcoord('.'), s:getcoord('v')])
|
|
let head = matchstr(text, '^.')
|
|
let is_head_multibyte = 1 < len(head)
|
|
let [l, col] = head_pos
|
|
let line = getline(l)
|
|
let before = line[: col - 2]
|
|
let outer = matchstr(before, '.$')
|
|
if text =~# '^\k' && ((!empty(outer) && len(outer) != len(head)) ||
|
|
\ (!is_head_multibyte && (col == 1 || before !~# '\k$')))
|
|
let pre = '\<'
|
|
endif
|
|
let tail = matchstr(text, '.$')
|
|
let is_tail_multibyte = 1 < len(tail)
|
|
let [l, col] = tail_pos
|
|
let col += s:is_exclusive() && head_pos[1] !=# tail_pos[1] ? - 1 : len(tail) - 1
|
|
let line = getline(l)
|
|
let after = line[col :]
|
|
let outer = matchstr(after, '^.')
|
|
if text =~# '\k$' && ((!empty(outer) && len(outer) != len(tail)) ||
|
|
\ (!is_tail_multibyte && (col == len(line) || after !~# '^\k')))
|
|
let post = '\>'
|
|
endif
|
|
endif
|
|
let text = substitute(escape(text, '\' . type), "\n", '\\n', 'g')
|
|
let text = substitute(text, "\r", '\\r', 'g')
|
|
return '\V' . pre . text . post
|
|
endfunction
|
|
|
|
"" Set pattern and history for search
|
|
" @return nothing
|
|
function! s:set_search(pattern) abort
|
|
let @/ = a:pattern
|
|
call histadd('/', @/)
|
|
endfunction
|
|
|
|
"" Generate command to turn on search related option like hlsearch to work
|
|
" with :h function-search-undo
|
|
" @return command: String
|
|
function! s:generate_set_search_cmd(pattern, mode, config) abort
|
|
" :h function-search-undo
|
|
" :h v:hlsearch
|
|
" :h v:searchforward
|
|
let hlsearch = 'let &hlsearch=&hlsearch'
|
|
let searchforward = printf('let v:searchforward = %d', a:config.direction)
|
|
let echo = s:generate_echo_cmd(a:pattern)
|
|
let esc = (a:mode isnot# 'n' ? "\<Esc>" : '')
|
|
return printf("%s:\<C-u>%s\<CR>:%s\<CR>:%s\<CR>", esc, hlsearch, searchforward, echo)
|
|
endfunction
|
|
|
|
" @return echo_command: String
|
|
function! s:generate_echo_cmd(message) abort
|
|
return printf('echo "%s"', escape(a:message, '\"'))
|
|
endfunction
|
|
|
|
"" Generate command to show error with empty pattern
|
|
" @return error_command: String
|
|
function! s:generate_error_cmd(is_visual) abort
|
|
" 'E348: No string under cursor'
|
|
let m = 'asterisk.vim: No selected string'
|
|
return (a:is_visual
|
|
\ ? printf("\<Esc>:echohl ErrorMsg | echom '%s' | echohl None\<CR>", m)
|
|
\ : '*')
|
|
endfunction
|
|
|
|
" @return boolean
|
|
function! s:should_plus_one_count(cword, config, mode) abort
|
|
" For backward, because count isn't needed with <expr> but it requires
|
|
" +1 for backward and for the case that cursor is not at the head of
|
|
" cword
|
|
if s:is_visual(a:mode)
|
|
return a:config.direction is# s:DIRECTION.backward ? s:TRUE : s:FALSE
|
|
else
|
|
return a:config.direction is# s:DIRECTION.backward
|
|
\ ? s:get_pos_char() =~# '\k' && ! s:is_head_of_cword(a:cword) && ! a:config.keeppos
|
|
\ : s:get_pos_char() !~# '\k'
|
|
endif
|
|
endfunction
|
|
|
|
" @return boolean
|
|
function! s:is_head_of_cword(cword) abort
|
|
return 0 == s:get_pos_in_cword(a:cword)
|
|
endfunction
|
|
|
|
" Assume the current mode is middle of visual mode.
|
|
" @return selected text
|
|
function! s:get_selected_text(...) abort
|
|
let mode = get(a:, 1, mode(1))
|
|
let end_col = s:curswant() is s:INT.MAX ? s:INT.MAX : s:get_col_in_visual('.')
|
|
let current_pos = [line('.'), end_col]
|
|
let other_end_pos = [line('v'), s:get_col_in_visual('v')]
|
|
let [begin, end] = s:sort_pos([current_pos, other_end_pos])
|
|
if s:is_exclusive() && begin[1] !=# end[1]
|
|
" Decrement column number for :set selection=exclusive
|
|
let end[1] -= 1
|
|
endif
|
|
if mode !=# 'V' && begin ==# end
|
|
let lines = [s:get_pos_char(begin)]
|
|
elseif mode ==# "\<C-v>"
|
|
let [min_c, max_c] = s:sort_num([begin[1], end[1]])
|
|
let lines = map(range(begin[0], end[0]), '
|
|
\ getline(v:val)[min_c - 1 : max_c - 1]
|
|
\ ')
|
|
elseif mode ==# 'V'
|
|
let lines = getline(begin[0], end[0])
|
|
else
|
|
if begin[0] ==# end[0]
|
|
let lines = [getline(begin[0])[begin[1]-1 : end[1]-1]]
|
|
else
|
|
let lines = [getline(begin[0])[begin[1]-1 :]]
|
|
\ + (end[0] - begin[0] < 2 ? [] : getline(begin[0]+1, end[0]-1))
|
|
\ + [getline(end[0])[: end[1]-1]]
|
|
endif
|
|
endif
|
|
return join(lines, "\n") . (mode ==# 'V' ? "\n" : '')
|
|
endfunction
|
|
|
|
" @return Number: return multibyte aware column number in Visual mode to
|
|
" select
|
|
function! s:get_col_in_visual(pos) abort
|
|
let [pos, other] = [a:pos, a:pos is# '.' ? 'v' : '.']
|
|
let c = col(pos)
|
|
let d = s:compare_pos(s:getcoord(pos), s:getcoord(other)) > 0
|
|
\ ? len(s:get_pos_char([line(pos), c - (s:is_exclusive() ? 1 : 0)])) - 1
|
|
\ : 0
|
|
return c + d
|
|
endfunction
|
|
|
|
function! s:get_multi_col(pos) abort
|
|
let c = col(a:pos)
|
|
return c + len(s:get_pos_char([line(a:pos), c])) - 1
|
|
endfunction
|
|
|
|
" Helper:
|
|
|
|
function! s:is_visual(mode) abort
|
|
return a:mode =~# "[vV\<C-v>]"
|
|
endfunction
|
|
|
|
" @return Boolean
|
|
function! s:is_exclusive() abort
|
|
return &selection is# 'exclusive'
|
|
endfunction
|
|
|
|
function! s:curswant() abort
|
|
return winsaveview().curswant
|
|
endfunction
|
|
|
|
" @return coordinate: [Number, Number]
|
|
function! s:getcoord(expr) abort
|
|
return getpos(a:expr)[1:2]
|
|
endfunction
|
|
|
|
"" Return character at given position with multibyte handling
|
|
" @arg [Number, Number] as coordinate or expression for position :h line()
|
|
" @return String
|
|
function! s:get_pos_char(...) abort
|
|
let pos = get(a:, 1, '.')
|
|
let [line, col] = type(pos) is# type('') ? s:getcoord(pos) : pos
|
|
return matchstr(getline(line), '.', col - 1)
|
|
endfunction
|
|
|
|
" @return int index of cursor in cword
|
|
function! s:get_pos_in_cword(cword, ...) abort
|
|
return (s:is_visual(get(a:, 1, mode(1))) || s:get_pos_char() !~# '\k') ? 0
|
|
\ : s:count_char(searchpos(a:cword, 'bcn')[1], s:get_multi_col('.'))
|
|
endfunction
|
|
|
|
" multibyte aware
|
|
function! s:count_char(from, to) abort
|
|
let chars = getline('.')[a:from-1:a:to-1]
|
|
return len(split(chars, '\zs')) - 1
|
|
endfunction
|
|
|
|
" 7.4.341
|
|
" http://ftp.vim.org/vim/patches/7.4/7.4.341
|
|
if v:version > 704 || v:version == 704 && has('patch341')
|
|
function! s:sort_num(xs) abort
|
|
return sort(a:xs, 'n')
|
|
endfunction
|
|
else
|
|
function! s:_sort_num_func(x, y) abort
|
|
return a:x - a:y
|
|
endfunction
|
|
function! s:sort_num(xs) abort
|
|
return sort(a:xs, 's:_sort_num_func')
|
|
endfunction
|
|
endif
|
|
|
|
function! s:sort_pos(pos_list) abort
|
|
" pos_list: [ [x1, y1], [x2, y2] ]
|
|
return sort(a:pos_list, 's:compare_pos')
|
|
endfunction
|
|
|
|
function! s:compare_pos(x, y) abort
|
|
return max([-1, min([1,(a:x[0] == a:y[0]) ? a:x[1] - a:y[1] : a:x[0] - a:y[0]])])
|
|
endfunction
|
|
|
|
" taken from :h Vital.Prelude.escape_pattern()
|
|
function! s:escape_pattern(str) abort
|
|
return escape(a:str, '~"\.^$[]*')
|
|
endfunction
|
|
|
|
" Restore 'cpoptions' {{{
|
|
let &cpo = s:save_cpo
|
|
unlet s:save_cpo
|
|
" }}}
|
|
" __END__ {{{
|
|
" vim: expandtab softtabstop=4 shiftwidth=4
|
|
" vim: foldmethod=marker
|
|
" }}}
|
|
|