├── LICENSE.txt ├── README.md ├── autoload └── hitspop.vim ├── doc └── hitspop.txt ├── plugin └── hitspop.vim └── syntax └── hitspop.vim /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 obcat 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-hitspop 2 | 3 | Popup the number of search results. 4 | 5 | ![hitspop eyecatch][1] 6 | 7 | 8 | ## Installation 9 | 10 | Requires Vim 8.2.0896 or later. Neovim is not supported. 11 | 12 | If you use [vim-plug][2], then add the following line to your vimrc: 13 | 14 | ```vim 15 | Plug 'obcat/vim-hitspop' 16 | ``` 17 | 18 | You can use any other plugin manager. 19 | 20 | 21 | ## Usage 22 | 23 | The `hlsearch` option must be turned on for this plugin to work: 24 | 25 | ```vim 26 | set hlsearch 27 | ``` 28 | 29 | This is all you need to set up. If you run a search command like `/foo`, a popup 30 | will appear and show you the number of search results like `foo 3 of 7`. 31 | 32 | ### Tips 33 | 34 | When you stop highlighting, the popup will be closed automatically. 35 | 36 | Highlighting can be stopped with the `nohlsearch` command. To run this command 37 | quickly, you may want to set up the following map: 38 | 39 | 40 | ```vim 41 | nnoremap :nohlsearch 42 | ``` 43 | 44 | You can also use the nohlsearch feature of [is.vim][3] plugin to stop 45 | highlighting automatically. Please see the link for details. 46 | 47 | To be precise, popup will be closed when one of the following occurs after 48 | stopping highlighting: 49 | 50 | * The cursor was moved. 51 | * The time specified with `updatetime` option has elapsed. 52 | 53 | The default value of `updatetime` is `4000`, i.e. 4 seconds. If you want to 54 | close the popup as soon as possible after stopping highlighting, reduce the 55 | value of this option. I suggest around 100ms: 56 | 57 | ```vim 58 | set updatetime=100 59 | ``` 60 | 61 | Note that `updatetime` also controls the delay before Vim writes its swap file 62 | (see `:h updatetime`). 63 | 64 | 65 | ## Customization 66 | 67 | You can customize some features. 68 | 69 | 70 | ### Position 71 | 72 | By default, popup is displayed at top right corner of current window. If you 73 | want to display the popup, for example, at bottom left corner of current window, 74 | use this: 75 | 76 | ```vim 77 | let g:hitspop_line = 'winbot' 78 | let g:hitspop_column = 'winright' 79 | ``` 80 | 81 | ![popup at botright][4] 82 | 83 | You can also specify other positions. Please see help file for more information. 84 | 85 | 86 | ### Highlight 87 | 88 | The popup color can be changed setting the following highlight groups: 89 | 90 | * `hitspopNormal` (default: links to `Pmenu`) 91 | * `hitspopErrorMsg` (default: links to `Pmenu`) 92 | 93 | Example: 94 | 95 | ![errormsg highlighting][5] 96 | 97 | ```vim 98 | highlight link hitspopErrorMsg ErrorMsg 99 | ``` 100 | 101 | 📝 I use [iceberg.vim][6] for color scheme. 102 | 103 | 104 | ## License 105 | 106 | MIT License. 107 | 108 | 109 | 110 | [1]: https://user-images.githubusercontent.com/64692680/102915667-81b06800-44c5-11eb-8b53-e37eacc4e67b.gif 111 | [2]: https://github.com/junegunn/vim-plug 112 | [3]: https://github.com/haya14busa/is.vim 113 | [4]: https://user-images.githubusercontent.com/64692680/102915781-b3293380-44c5-11eb-9068-84fe2defe5fd.png 114 | [5]: https://user-images.githubusercontent.com/64692680/102916237-90e3e580-44c6-11eb-803d-6daa577bed98.png 115 | [6]: https://github.com/cocopon/iceberg.vim 116 | -------------------------------------------------------------------------------- /autoload/hitspop.vim: -------------------------------------------------------------------------------- 1 | " Maintainer: obcat 2 | " License: MIT License 3 | 4 | 5 | function! s:init() abort "{{{ 6 | let g:hitspop_line = get(g:, 'hitspop_line', 'wintop') 7 | let g:hitspop_line_mod = get(g:, 'hitspop_line_mod', 0) 8 | let g:hitspop_column = get(g:, 'hitspop_column', 'winright') 9 | let g:hitspop_column_mod = get(g:, 'hitspop_column_mod', 0) 10 | let g:hitspop_zindex = get(g:, 'hitspop_zindex', 50) 11 | let g:hitspop_minwidth = get(g:, 'hitspop_minwidth', 20) 12 | let g:hitspop_maxwidth = get(g:, 'hitspop_maxwidth', 30) 13 | let g:hitspop_timeout = get(g:, 'hitspop_timeout', 10) 14 | let s:HL_NORMAL = 'hitspopNormal' 15 | let s:HL_ERRORMSG = 'hitspopErrorMsg' 16 | exe 'hi default link' s:HL_NORMAL 'Pmenu' 17 | exe 'hi default link' s:HL_ERRORMSG 'Pmenu' 18 | 19 | let s:ERROR_MSGS = #{ 20 | \ invalid: 'Invalid', 21 | \ timeout: 'Timed out', 22 | \ notfound: 'No results', 23 | \ } 24 | let s:PADDING = [0, 1, 0, 1] 25 | let s:POPUP_STATIC_OPTIONS = #{ 26 | \ zindex: g:hitspop_zindex, 27 | \ padding: s:PADDING, 28 | \ highlight: s:HL_NORMAL, 29 | \ callback: {-> s:unlet_popup_id()}, 30 | \ } 31 | let s:UP = 1 32 | let s:DOWN = 0 33 | let s:prev_search_pattern = '' 34 | let s:prev_bufinfo = [] 35 | let s:timeout_counter = 0 36 | let s:notfound_flag = s:DOWN 37 | let s:invalid_flag = s:DOWN 38 | endfunction "}}} 39 | 40 | 41 | call s:init() 42 | 43 | " This function is called on CursorMoved, CursorMovedI, CursorHold, and WinEnter 44 | function! hitspop#main() abort "{{{ 45 | if !v:hlsearch || get(b:, 'hitspop_disable', 0) 46 | \ || get(b:, 'hitspop_blocked', 0) 47 | \ || get(g:, 'hitspop_disable', 0) 48 | \ || empty(@/) 49 | call s:delete_popup_if_exists() 50 | return 51 | endif 52 | 53 | let coord = s:get_coord() 54 | 55 | if !s:popup_exists() 56 | let s:popup_id = s:create_popup(coord) 57 | call setbufvar(winbufnr(s:popup_id), '&filetype', 'hitspop') 58 | else 59 | let opts = popup_getoptions(s:popup_id) 60 | if empty(opts) 61 | " It is assumed that the popup callback was not called due to some accident, so call it. 62 | call s:unlet_popup_id() 63 | return 64 | endif 65 | if [opts.line, opts.col] != [coord.line, coord.col] 66 | call s:move_popup(coord.line, coord.col) 67 | endif 68 | 69 | call s:update_content() 70 | endif 71 | endfunction "}}} 72 | 73 | 74 | " This function is called on WinLeave. 75 | " This matters when leaving tab page. 76 | function! hitspop#clean() abort "{{{ 77 | call s:delete_popup_if_exists() 78 | endfunction "}}} 79 | 80 | 81 | " This function is called on TerminalOpen. 82 | function! hitspop#define_autocmds_for_terminal_buffer(bufnr) abort "{{{ 83 | exe printf('augroup hitspop-autocmds-for-terminal-buffer-%d', a:bufnr) 84 | autocmd! 85 | " Don't show popup in terminal-job mode. 86 | exe printf('autocmd SafeState call s:delete_popup_if_exists_in_terminal_job_mode()', a:bufnr) 87 | augroup END 88 | endfunction "}}} 89 | 90 | 91 | function! s:create_popup(coord) abort "{{{ 92 | return popup_create( 93 | \ s:get_content(), 94 | \ deepcopy(s:POPUP_STATIC_OPTIONS)->extend(a:coord) 95 | \ ) 96 | endfunction "}}} 97 | 98 | function! s:delete_popup_if_exists() abort "{{{ 99 | if s:popup_exists() 100 | call popup_close(s:popup_id) 101 | endif 102 | endfunction "}}} 103 | 104 | function! s:delete_popup_if_exists_in_terminal_job_mode() abort "{{{ 105 | if mode() is# 't' 106 | call s:delete_popup_if_exists() 107 | endif 108 | endfunction "}}} 109 | 110 | function! s:move_popup(line, col) abort "{{{ 111 | call popup_move(s:popup_id, #{line: a:line, col: a:col}) 112 | endfunction "}}} 113 | 114 | 115 | function! s:update_content() abort "{{{ 116 | call popup_settext(s:popup_id, s:get_content()) 117 | endfunction "}}} 118 | 119 | 120 | function! s:popup_exists() abort "{{{ 121 | return exists('s:popup_id') 122 | endfunction "}}} 123 | 124 | 125 | function! s:unlet_popup_id() abort "{{{ 126 | unlet s:popup_id 127 | endfunction "}}} 128 | 129 | 130 | " Return search results 131 | " NOTE: This function try to return results without running searchcount() 132 | " because it can be slow even with a small timeout value. 133 | function! s:get_content() abort "{{{ 134 | let bufinfo = [bufnr(), b:changedtick] 135 | if @/ isnot# s:prev_search_pattern 136 | let s:invalid_flag = s:DOWN 137 | let s:timeout_counter = 0 138 | let s:notfound_flag = s:DOWN 139 | elseif bufinfo != s:prev_bufinfo 140 | let s:timeout_counter = 0 141 | let s:notfound_flag = s:DOWN 142 | endif 143 | let s:prev_search_pattern = @/ 144 | let s:prev_bufinfo = bufinfo 145 | 146 | let search_pattern = strtrans(@/) 147 | 148 | if s:invalid_flag is s:UP 149 | return s:format(search_pattern, s:ERROR_MSGS.invalid) 150 | endif 151 | if s:timeout_counter == 3 152 | return s:format(search_pattern, s:ERROR_MSGS.timeout) 153 | endif 154 | if s:notfound_flag is s:UP 155 | return s:format(search_pattern, s:ERROR_MSGS.notfound) 156 | endif 157 | 158 | try 159 | let result = searchcount(#{maxcount: 0, timeout: g:hitspop_timeout}) 160 | catch 161 | " Error: @/ is invalid search pattern (E54, E65, E944, ...) 162 | let s:invalid_flag = s:UP 163 | return s:format(search_pattern, s:ERROR_MSGS.invalid) 164 | endtry 165 | 166 | if result.incomplete == 1 167 | let s:timeout_counter += 1 168 | return s:format(search_pattern, s:ERROR_MSGS.timeout) 169 | endif 170 | let s:timeout_counter = 0 171 | 172 | if result.total == 0 173 | let s:notfound_flag = s:UP 174 | return s:format(search_pattern, s:ERROR_MSGS.notfound) 175 | endif 176 | 177 | return s:format( 178 | \ search_pattern, 179 | \ printf('%*d of %d', len(result.total), result.current, result.total) 180 | \ ) 181 | endfunction "}}} 182 | 183 | 184 | function! s:format(pattern, result) abort "{{{ 185 | let padding = s:PADDING[1] + s:PADDING[3] 186 | let result_width = strwidth(a:result) 187 | let separator = "\\" 188 | let separator_width = strwidth(separator) 189 | let patternfield_minwidth = g:hitspop_minwidth - (padding + separator_width + result_width) 190 | let patternfield_maxwidth = g:hitspop_maxwidth - (padding + separator_width + result_width) 191 | let truncation_symbol = '..' 192 | 193 | let content = printf('%-*S', 194 | \ patternfield_minwidth, 195 | \ patternfield_maxwidth < strwidth(a:pattern) 196 | \ ? s:truncate(a:pattern, truncation_symbol, patternfield_maxwidth) 197 | \ : a:pattern, 198 | \ ) 199 | let content .= separator 200 | let content .= a:result 201 | return content 202 | endfunction "}}} 203 | 204 | 205 | function! s:truncate(target, symbol, width) "{{{ 206 | return printf('%.*S%s', 207 | \ a:width - strwidth(a:symbol), 208 | \ a:target, 209 | \ a:symbol 210 | \ ) 211 | endfunction "}}} 212 | 213 | 214 | " Return dictionary used to specify popup position 215 | function! s:get_coord() abort "{{{ 216 | let [line, col] = win_screenpos(0) 217 | if !empty(menu_info('WinBar', 'a')) 218 | let line += 1 219 | endif 220 | if g:hitspop_line is# 'wintop' 221 | let pos = 'top' 222 | elseif g:hitspop_line is# 'winbot' 223 | let pos = 'bot' 224 | let line += winheight(0) - 1 225 | endif 226 | if g:hitspop_column is# 'winleft' 227 | let pos .= 'left' 228 | elseif g:hitspop_column is# 'winright' 229 | let pos .= 'right' 230 | let col += winwidth(0) - 1 231 | endif 232 | let line += g:hitspop_line_mod 233 | let col += g:hitspop_column_mod 234 | return #{pos: pos, line: line, col: col} 235 | endfunction "}}} 236 | 237 | 238 | " This is called from syntax/hitspop.vim 239 | function! hitspop#define_syntax() abort "{{{ 240 | let delimiter = '/' 241 | for msg in values(s:ERROR_MSGS) 242 | exe 'syn match' s:HL_ERRORMSG delimiter . escape(msg, delimiter) . '\s*$' . delimiter 243 | endfor 244 | endfunction "}}} 245 | 246 | 247 | " API function to get popup id 248 | function! hitspop#getpopupid() abort "{{{ 249 | return get(s:, 'popup_id', '') 250 | endfunction "}}} 251 | -------------------------------------------------------------------------------- /doc/hitspop.txt: -------------------------------------------------------------------------------- 1 | *hitspop.txt* Popup the number of search results 2 | 3 | __ ___ __ ____ ` 4 | / / / (_) /______/ __ \____ ____ ` 5 | / /_/ / / __/ ___/ /_/ / __ \/ __ \ ` 6 | / __ / / /_(__ ) ____/ /_/ / /_/ / ` 7 | /_/ /_/_/\__/____/_/ \____/ .___/ ` 8 | /_/ ` 9 | 10 | 11 | ============================================================================== 12 | CONTENTS 13 | 14 | INTRO .................................... |hitspop-intro| 15 | HIGHLIGHTS ............................... |hitspop-highlights| 16 | OPTIONS .................................. |hitspop-options| 17 | COMMANDS ................................. |hitspop-commands| 18 | ABOUT .................................... |hitspop-about| 19 | 20 | 21 | ============================================================================== 22 | INTRO *hitspop-intro* 23 | 24 | *HitsPop* is a Vim plugin for displaying the results of a search command in a 25 | tiny popup window. 26 | 27 | 28 | ============================================================================== 29 | HIGHLIGHTS *hitspop-highlights* 30 | 31 | hitspopNormal (default: links to |hl-Pmenu|) *hl-hitspopNormal* 32 | Highlight group for normal text in |HitsPop| popup. 33 | 34 | hitspopErrorMsg (default: links to |hl-Pmenu|) *hl-hitspopErrorMsg* 35 | Highlight group for error messages in |HitsPop| popup. 36 | 37 | 38 | ============================================================================== 39 | OPTIONS *hitspop-options* 40 | 41 | 'hlsearch' boolean (default: off) 42 | 'updatetime' number (default: 4000) 43 | These are Vim's |options|. The |hlsearch| option must be turned on for 44 | this plugin to work. The |updatetime| option determines smoothness of 45 | this plugin. I recommend setting this to 100. > 46 | > 47 | set hlsearch 48 | set updatetime=100 49 | < 50 | 51 | 52 | *g:hitspop_line* string (default: "wintop") 53 | *g:hitspop_line_mod* number (default: 0) 54 | *g:hitspop_column* string (default: "winright") 55 | *g:hitspop_column_mod* number (default: 0) 56 | Specify |HitsPop| popup position. The default position is top right 57 | corner of current window. Accepted values of the string options are: 58 | 59 | g:hitspop_line ~ 60 | "wintop" top line of current window 61 | "winbot" bottom line of current window 62 | g:hitspop_column ~ 63 | "winleft" leftmost column of current window 64 | "winright" rightmost column of current window 65 | > 66 | "winright" 67 | | 68 | -7 -6 -5 -4 -3 -2 -1| 0 69 | --+-------------------+---> "wintop" 70 | | | 1 71 | | | 2 72 | | | 3 73 | | window | 4 (default) 74 | | | 5 75 | | | 6 76 | | | 7 77 | +-------------------| 8 78 | | 9 79 | v 80 | < 81 | For example, to place popup 2 columns off to the left from bottom 82 | right corner of current window, use this: 83 | > 84 | let g:hitspop_line = 'winbot' 85 | let g:hitspop_line_mod = 0 86 | let g:hitspop_column = 'winright' 87 | let g:hitspop_column_mod = -2 88 | < 89 | 90 | 91 | *g:hitspop_minwidth* number (default: 20) 92 | *g:hitspop_maxwidth* number (default: 30) 93 | Minimum and maximum width of |HitsPop| popup. 94 | 95 | 96 | *g:hitspop_zindex* number (default: 50) 97 | Priority for |HitsPop| popup. Minumum value is 1, maximum value is 98 | 32000. If this value is less than the zindex of other popup, then 99 | |HitsPop| popup will go under it, and vice versa. 100 | 101 | *g:hitspop_timeout* number (default: 10) 102 | Timeout milliseconds for computing the number of search results. 0 or 103 | negative number means no timeout. 104 | 105 | *g:hitspop_disable* boolean (default: undefined) 106 | Set 1 to disable |HitsPop| for all buffers. This can be overriden 107 | with |b:hitspop_disable| on a per-buffer basis. 108 | 109 | 110 | *b:hitspop_disable* boolean (default: undefined) 111 | Set 1 to disable |HitsPop| for a specific buffer. This overrides 112 | |g:hitspop_disable|. For example, to disable |HitsPop| for |netrw| 113 | buffers, use this: 114 | > 115 | augroup my-hitspop 116 | autocmd! 117 | autocmd FileType netrw let b:hitspop_disable = 1 118 | augroup END 119 | < 120 | 121 | 122 | *b:hitspop_blocked* 123 | DEPRECATED: Use |b:hitspop_disable| instead. 124 | 125 | 126 | ============================================================================== 127 | COMMANDS *hitspop-commands* 128 | 129 | *:HitsPopEnable* 130 | DEPRECATED: Set |g:hitspop_disable| to 0 or remove it instead. 131 | 132 | *:HitsPopDisable* 133 | DEPRECATED: Set |g:hitspop_disable| to 1 instead. 134 | 135 | 136 | ============================================================================== 137 | ABOUT *hitspop-about* 138 | 139 | |HitsPop| is developed by obcat and licensed under the MIT License. Visit the 140 | project page for the latest version: 141 | 142 | https://github.com/obcat/vim-hitspop 143 | 144 | 145 | ============================================================================== 146 | vim:tw=78:ts=8:noet:ft=help:norl: 147 | -------------------------------------------------------------------------------- /plugin/hitspop.vim: -------------------------------------------------------------------------------- 1 | " Maintainer: obcat 2 | " License: MIT License 3 | 4 | 5 | if exists('g:loaded_hitspop') 6 | finish 7 | endif 8 | 9 | if !exists('*searchcount') 10 | finish 11 | endif 12 | 13 | augroup hitspop-autocmds 14 | autocmd! 15 | autocmd CursorMoved,CursorMovedI,CursorHold,WinEnter,VimResized * call hitspop#main() 16 | if exists('##WinScrolled') 17 | autocmd WinScrolled * call hitspop#main() 18 | endif 19 | autocmd WinLeave * call hitspop#clean() 20 | autocmd TerminalOpen * call hitspop#define_autocmds_for_terminal_buffer(expand('')) 21 | augroup END 22 | 23 | command! HitsPopEnable let g:hitspop_disable = 0 24 | command! HitsPopDisable let g:hitspop_disable = 1 25 | 26 | let g:loaded_hitspop = 1 27 | -------------------------------------------------------------------------------- /syntax/hitspop.vim: -------------------------------------------------------------------------------- 1 | " Maintainer: obcat 2 | " License: MIT License 3 | 4 | 5 | call hitspop#define_syntax() 6 | --------------------------------------------------------------------------------