1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 06:30:03 +08:00
SpaceVim/bundle/vim-asterisk/autoload/asterisk.vim
2022-06-10 16:31:14 +08:00

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
" }}}