asterisk.vim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. "=============================================================================
  2. " FILE: autoload/asterisk.vim
  3. " AUTHOR: haya14busa
  4. " License: MIT license {{{
  5. " Permission is hereby granted, free of charge, to any person obtaining
  6. " a copy of this software and associated documentation files (the
  7. " "Software"), to deal in the Software without restriction, including
  8. " without limitation the rights to use, copy, modify, merge, publish,
  9. " distribute, sublicense, and/or sell copies of the Software, and to
  10. " permit persons to whom the Software is furnished to do so, subject to
  11. " the following conditions:
  12. "
  13. " The above copyright notice and this permission notice shall be included
  14. " in all copies or substantial portions of the Software.
  15. "
  16. " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. " OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  18. " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  19. " IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  20. " CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  21. " TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  22. " SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  23. " }}}
  24. "=============================================================================
  25. scriptencoding utf-8
  26. " Saving 'cpoptions' {{{
  27. let s:save_cpo = &cpo
  28. set cpo&vim
  29. " }}}
  30. let s:TRUE = !0
  31. let s:FALSE = 0
  32. let s:INT = { 'MAX': 2147483647 }
  33. let s:DIRECTION = { 'forward': 1, 'backward': 0 } " see :h v:searchforward
  34. let g:asterisk#keeppos = get(g:, 'asterisk#keeppos', s:FALSE)
  35. " do_jump: do not move cursor if false
  36. " is_whole: is_whole word. false if `g` flag given (e.g. * -> true, g* -> false)
  37. let s:_config = {
  38. \ 'direction' : s:DIRECTION.forward,
  39. \ 'do_jump' : s:TRUE,
  40. \ 'is_whole' : s:TRUE,
  41. \ 'keeppos': s:FALSE
  42. \ }
  43. function! s:default_config() abort
  44. return extend(deepcopy(s:_config), {'keeppos': g:asterisk#keeppos})
  45. endfunction
  46. " @return command: String
  47. function! asterisk#do(mode, config) abort
  48. let config = extend(s:default_config(), a:config)
  49. let is_visual = s:is_visual(a:mode)
  50. " Raw cword without \<\>
  51. let cword = (is_visual ? s:get_selected_text() : s:escape_pattern(expand('<cword>')))
  52. if cword is# ''
  53. return s:generate_error_cmd(is_visual)
  54. endif
  55. " v:count handling
  56. let should_plus_one_count = s:should_plus_one_count(cword, config, a:mode)
  57. let maybe_count = (should_plus_one_count ? string(v:count1 + 1) : '')
  58. let pre = (is_visual || should_plus_one_count ? "\<Esc>" . maybe_count : '')
  59. " Including \<\> if necessary
  60. let pattern = (is_visual ?
  61. \ s:convert_2_word_pattern_4_visual(cword, config) : s:cword_pattern(cword, config))
  62. let key = (config.direction is s:DIRECTION.forward ? '/' : '?')
  63. " Get offset in current word
  64. let offset = config.keeppos ? s:get_pos_in_cword(cword, a:mode) : 0
  65. let pattern_offseted = pattern . (offset is 0 ? '' : key . 's+' . offset)
  66. let search_cmd = pre . key . pattern_offseted
  67. if config.do_jump
  68. return search_cmd . "\<CR>"
  69. elseif config.keeppos && offset isnot 0
  70. "" Do not jump with keeppos feature
  71. " NOTE: It doesn't move cursor, so we can assume it works with
  72. " operator pending mode even if it returns command to execute.
  73. let echo = s:generate_echo_cmd(pattern_offseted)
  74. let restore = s:generate_restore_cmd()
  75. "" *premove* & *aftermove* : not to cause flickr as mush as possible
  76. " flick corner case: `#` with under cursor word at the top of window
  77. " and the cursor is at the end of the word.
  78. let premove =
  79. \ (a:mode isnot# 'n' ? "\<Esc>" : '')
  80. \ . 'm`'
  81. \ . (config.direction is s:DIRECTION.forward ? '0' : '$')
  82. " NOTE: Neovim doesn't stack pos to jumplist after "m`".
  83. " https://github.com/haya14busa/vim-asterisk/issues/34
  84. if has('nvim')
  85. let aftermove = '``'
  86. else
  87. let aftermove = "\<C-o>"
  88. endif
  89. " NOTE: To avoid hit-enter prompt, it execute `restore` and `echo`
  90. " command separately. I can also implement one function and call it
  91. " once instead of separately, should I do this?
  92. return printf("%s%s\<CR>%s:%s\<CR>:%s\<CR>", premove, search_cmd, aftermove, restore, echo)
  93. else " Do not jump: Just handle search related
  94. call s:set_search(pattern)
  95. return s:generate_set_search_cmd(pattern, a:mode, config)
  96. endif
  97. endfunction
  98. "" For keeppos feature
  99. " NOTE: To avoid hit-enter prompt, this function name should be as short as
  100. " possible. `r` is short for restore. Should I use more short function using
  101. " basic global function instead of autoload one.
  102. function! asterisk#r() abort
  103. call winrestview(s:w)
  104. call s:restore_event_ignore()
  105. endfunction
  106. function! s:set_view(view) abort
  107. let s:w = a:view
  108. endfunction
  109. "" For keeppos feature
  110. " NOTE: vim-asterisk moves cursor temporarily for keeppos feature with search
  111. " commands. It should not trigger the event related to this cursor move, so
  112. " set eventignore and restore it afterwards.
  113. function! s:set_event_ignore() abort
  114. let s:ei = &ei
  115. let events = ['CursorMoved']
  116. if exists('##CmdlineEnter')
  117. let events += ['CmdlineEnter', 'CmdlineLeave']
  118. endif
  119. let &ei = join(events, ',')
  120. endfunction
  121. function! s:restore_event_ignore() abort
  122. let &ei = s:ei
  123. endfunction
  124. " @return restore_command: String
  125. function! s:generate_restore_cmd() abort
  126. call s:set_view(winsaveview())
  127. call s:set_event_ignore()
  128. return 'call asterisk#r()'
  129. endfunction
  130. " @return \<cword\> if needed: String
  131. function! s:cword_pattern(cword, config) abort
  132. return printf((a:config.is_whole && a:cword =~# '\k' ? '\<%s\>' : '%s'), a:cword)
  133. endfunction
  134. " This function is based on https://github.com/thinca/vim-visualstar
  135. " Author : thinca <thinca+vim@gmail.com>
  136. " License : zlib License
  137. " @return \<selected_pattern\>: String
  138. function! s:convert_2_word_pattern_4_visual(pattern, config) abort
  139. let text = a:pattern
  140. let type = (a:config.direction is# s:DIRECTION.forward ? '/' : '?')
  141. let [pre, post] = ['', '']
  142. if a:config.is_whole
  143. let [head_pos, tail_pos] = s:sort_pos([s:getcoord('.'), s:getcoord('v')])
  144. let head = matchstr(text, '^.')
  145. let is_head_multibyte = 1 < len(head)
  146. let [l, col] = head_pos
  147. let line = getline(l)
  148. let before = line[: col - 2]
  149. let outer = matchstr(before, '.$')
  150. if text =~# '^\k' && ((!empty(outer) && len(outer) != len(head)) ||
  151. \ (!is_head_multibyte && (col == 1 || before !~# '\k$')))
  152. let pre = '\<'
  153. endif
  154. let tail = matchstr(text, '.$')
  155. let is_tail_multibyte = 1 < len(tail)
  156. let [l, col] = tail_pos
  157. let col += s:is_exclusive() && head_pos[1] !=# tail_pos[1] ? - 1 : len(tail) - 1
  158. let line = getline(l)
  159. let after = line[col :]
  160. let outer = matchstr(after, '^.')
  161. if text =~# '\k$' && ((!empty(outer) && len(outer) != len(tail)) ||
  162. \ (!is_tail_multibyte && (col == len(line) || after !~# '^\k')))
  163. let post = '\>'
  164. endif
  165. endif
  166. let text = substitute(escape(text, '\' . type), "\n", '\\n', 'g')
  167. let text = substitute(text, "\r", '\\r', 'g')
  168. return '\V' . pre . text . post
  169. endfunction
  170. "" Set pattern and history for search
  171. " @return nothing
  172. function! s:set_search(pattern) abort
  173. let @/ = a:pattern
  174. call histadd('/', @/)
  175. endfunction
  176. "" Generate command to turn on search related option like hlsearch to work
  177. " with :h function-search-undo
  178. " @return command: String
  179. function! s:generate_set_search_cmd(pattern, mode, config) abort
  180. " :h function-search-undo
  181. " :h v:hlsearch
  182. " :h v:searchforward
  183. let hlsearch = 'let &hlsearch=&hlsearch'
  184. let searchforward = printf('let v:searchforward = %d', a:config.direction)
  185. let echo = s:generate_echo_cmd(a:pattern)
  186. let esc = (a:mode isnot# 'n' ? "\<Esc>" : '')
  187. return printf("%s:\<C-u>%s\<CR>:%s\<CR>:%s\<CR>", esc, hlsearch, searchforward, echo)
  188. endfunction
  189. " @return echo_command: String
  190. function! s:generate_echo_cmd(message) abort
  191. return printf('echo "%s"', escape(a:message, '\"'))
  192. endfunction
  193. "" Generate command to show error with empty pattern
  194. " @return error_command: String
  195. function! s:generate_error_cmd(is_visual) abort
  196. " 'E348: No string under cursor'
  197. let m = 'asterisk.vim: No selected string'
  198. return (a:is_visual
  199. \ ? printf("\<Esc>:echohl ErrorMsg | echom '%s' | echohl None\<CR>", m)
  200. \ : '*')
  201. endfunction
  202. " @return boolean
  203. function! s:should_plus_one_count(cword, config, mode) abort
  204. " For backward, because count isn't needed with <expr> but it requires
  205. " +1 for backward and for the case that cursor is not at the head of
  206. " cword
  207. if s:is_visual(a:mode)
  208. return a:config.direction is# s:DIRECTION.backward ? s:TRUE : s:FALSE
  209. else
  210. return a:config.direction is# s:DIRECTION.backward
  211. \ ? s:get_pos_char() =~# '\k' && ! s:is_head_of_cword(a:cword) && ! a:config.keeppos
  212. \ : s:get_pos_char() !~# '\k'
  213. endif
  214. endfunction
  215. " @return boolean
  216. function! s:is_head_of_cword(cword) abort
  217. return 0 == s:get_pos_in_cword(a:cword)
  218. endfunction
  219. " Assume the current mode is middle of visual mode.
  220. " @return selected text
  221. function! s:get_selected_text(...) abort
  222. let mode = get(a:, 1, mode(1))
  223. let end_col = s:curswant() is s:INT.MAX ? s:INT.MAX : s:get_col_in_visual('.')
  224. let current_pos = [line('.'), end_col]
  225. let other_end_pos = [line('v'), s:get_col_in_visual('v')]
  226. let [begin, end] = s:sort_pos([current_pos, other_end_pos])
  227. if s:is_exclusive() && begin[1] !=# end[1]
  228. " Decrement column number for :set selection=exclusive
  229. let end[1] -= 1
  230. endif
  231. if mode !=# 'V' && begin ==# end
  232. let lines = [s:get_pos_char(begin)]
  233. elseif mode ==# "\<C-v>"
  234. let [min_c, max_c] = s:sort_num([begin[1], end[1]])
  235. let lines = map(range(begin[0], end[0]), '
  236. \ getline(v:val)[min_c - 1 : max_c - 1]
  237. \ ')
  238. elseif mode ==# 'V'
  239. let lines = getline(begin[0], end[0])
  240. else
  241. if begin[0] ==# end[0]
  242. let lines = [getline(begin[0])[begin[1]-1 : end[1]-1]]
  243. else
  244. let lines = [getline(begin[0])[begin[1]-1 :]]
  245. \ + (end[0] - begin[0] < 2 ? [] : getline(begin[0]+1, end[0]-1))
  246. \ + [getline(end[0])[: end[1]-1]]
  247. endif
  248. endif
  249. return join(lines, "\n") . (mode ==# 'V' ? "\n" : '')
  250. endfunction
  251. " @return Number: return multibyte aware column number in Visual mode to
  252. " select
  253. function! s:get_col_in_visual(pos) abort
  254. let [pos, other] = [a:pos, a:pos is# '.' ? 'v' : '.']
  255. let c = col(pos)
  256. let d = s:compare_pos(s:getcoord(pos), s:getcoord(other)) > 0
  257. \ ? len(s:get_pos_char([line(pos), c - (s:is_exclusive() ? 1 : 0)])) - 1
  258. \ : 0
  259. return c + d
  260. endfunction
  261. function! s:get_multi_col(pos) abort
  262. let c = col(a:pos)
  263. return c + len(s:get_pos_char([line(a:pos), c])) - 1
  264. endfunction
  265. " Helper:
  266. function! s:is_visual(mode) abort
  267. return a:mode =~# "[vV\<C-v>]"
  268. endfunction
  269. " @return Boolean
  270. function! s:is_exclusive() abort
  271. return &selection is# 'exclusive'
  272. endfunction
  273. function! s:curswant() abort
  274. return winsaveview().curswant
  275. endfunction
  276. " @return coordinate: [Number, Number]
  277. function! s:getcoord(expr) abort
  278. return getpos(a:expr)[1:2]
  279. endfunction
  280. "" Return character at given position with multibyte handling
  281. " @arg [Number, Number] as coordinate or expression for position :h line()
  282. " @return String
  283. function! s:get_pos_char(...) abort
  284. let pos = get(a:, 1, '.')
  285. let [line, col] = type(pos) is# type('') ? s:getcoord(pos) : pos
  286. return matchstr(getline(line), '.', col - 1)
  287. endfunction
  288. " @return int index of cursor in cword
  289. function! s:get_pos_in_cword(cword, ...) abort
  290. return (s:is_visual(get(a:, 1, mode(1))) || s:get_pos_char() !~# '\k') ? 0
  291. \ : s:count_char(searchpos(a:cword, 'bcn')[1], s:get_multi_col('.'))
  292. endfunction
  293. " multibyte aware
  294. function! s:count_char(from, to) abort
  295. let chars = getline('.')[a:from-1:a:to-1]
  296. return len(split(chars, '\zs')) - 1
  297. endfunction
  298. " 7.4.341
  299. " http://ftp.vim.org/vim/patches/7.4/7.4.341
  300. if v:version > 704 || v:version == 704 && has('patch341')
  301. function! s:sort_num(xs) abort
  302. return sort(a:xs, 'n')
  303. endfunction
  304. else
  305. function! s:_sort_num_func(x, y) abort
  306. return a:x - a:y
  307. endfunction
  308. function! s:sort_num(xs) abort
  309. return sort(a:xs, 's:_sort_num_func')
  310. endfunction
  311. endif
  312. function! s:sort_pos(pos_list) abort
  313. " pos_list: [ [x1, y1], [x2, y2] ]
  314. return sort(a:pos_list, 's:compare_pos')
  315. endfunction
  316. function! s:compare_pos(x, y) abort
  317. return max([-1, min([1,(a:x[0] == a:y[0]) ? a:x[1] - a:y[1] : a:x[0] - a:y[0]])])
  318. endfunction
  319. " taken from :h Vital.Prelude.escape_pattern()
  320. function! s:escape_pattern(str) abort
  321. return escape(a:str, '~"\.^$[]*')
  322. endfunction
  323. " Restore 'cpoptions' {{{
  324. let &cpo = s:save_cpo
  325. unlet s:save_cpo
  326. " }}}
  327. " __END__ {{{
  328. " vim: expandtab softtabstop=4 shiftwidth=4
  329. " vim: foldmethod=marker
  330. " }}}