├── .gitignore ├── README.md ├── autoload └── indexed_search.vim ├── doc └── indexed-search.txt └── plugin └── indexed-search.vim /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IndexedSearch.vim 2 | 3 | Requires vim7.4 4 | 5 | Originally by [Yakov Lerner](http://www.vim.org/account/profile.php?user_id=2342) and put on GitHub by [Henrik Nyh](https://github.com/henrik) to have it there in a [Pathogen](http://www.vim.org/scripts/script.php?script_id=2332)-friendly format. Majorly rewritten by [Otto Modinos](https://github.com/otommod). 6 | 7 | [See the original plugin page at vim.org.](http://www.vim.org/scripts/script.php?script_id=1682) 8 | 9 | ``` 10 | This plugin redefines 6 search commands (/,?,n,N,*,#). At every 11 | search command, it automatically prints> 12 | "At match #N out of M matches". 13 | > 14 | -- the total number of matches (M) and the number(index) of current 15 | match (N). This helps to get oriented when searching forward and 16 | backward. 17 | 18 | There are no new commands and no new behavior to learn. 19 | Just watch the bottom line when you do /,?,n,N,*,#. 20 | ``` 21 | 22 | [See full help file.](https://github.com/henrik/vim-indexed-search/blob/master/doc/indexed-search.txt) 23 | 24 | ## Alternatives 25 | 26 | Is this plugin too slow for you? Do you want more (or less) features? Here're some other plugins that do (or can do) the same thing: 27 | 28 | * On Vim 8.1.1270+, you can use [`:set shortmess-=S`](https://stackoverflow.com/a/4671112/6962) 29 | * [google/vim-searchindex](https://github.com/google/vim-searchindex); very fast and unobtrusive 30 | * [osyo-manga/vim-anzu](https://github.com/osyo-manga/vim-anzu); tons of features 31 | * [romainl/vim-cool](https://github.com/romainl/vim-cool); initally just for disabling the highlighting of matches after a seach, now also show an index 32 | * [lacygoill/vim-search](https://github.com/lacygoill/vim-search); meant for [personal use](https://github.com/junegunn/vim-slash/issues/7) but can be used by everyone 33 | 34 | -------------------------------------------------------------------------------- /autoload/indexed_search.vim: -------------------------------------------------------------------------------- 1 | function! s:echohl(hl, msg) 2 | exec 'echohl' a:hl 3 | echo a:msg 4 | echohl None 5 | endfunction 6 | 7 | function! s:old_search(force) 8 | let winview = winsaveview() 9 | let line = winview["lnum"] 10 | let col = winview["col"] + 1 11 | let [index, total, is_on_match, first_match_lnum, last_match_lnum] = [0, 0, 0, 0, 0] 12 | 13 | call cursor(1, 1) 14 | let [matchline, matchcol] = searchpos(@/, 'Wc') 15 | let first_match_lnum = matchline 16 | while matchline && (total <= g:indexed_search_max_hits || a:force) 17 | let total += 1 18 | let last_match_lnum = matchline 19 | if matchline < line || (matchline == line && matchcol <= col) 20 | let index = total 21 | let is_on_match = matchline == line && matchcol == col 22 | endif 23 | let [matchline, matchcol] = searchpos(@/, 'W') 24 | endwhile 25 | let out_of_time = (!a:force && total > g:indexed_search_max_hits) 26 | \ + (!a:force && index > g:indexed_search_max_hits) 27 | 28 | call winrestview(winview) 29 | return [index, total, is_on_match, out_of_time, first_match_lnum, last_match_lnum] 30 | endfunction 31 | 32 | function! s:search(force) 33 | let [before, after, is_on_match, first_match_lnum, last_match_lnum] = [0, 0, 1, 0, 0] 34 | 35 | let now = reltime() 36 | let winview = winsaveview() 37 | let [save_ws, save_fen] = [&wrapscan, &foldenable] 38 | set nowrapscan nofoldenable 39 | 40 | " If we're at the last line and the file contains no EOL there, 41 | " `line2byte()` seems (to me) to give a wrong result. 42 | let eolbug = line('.') == line('$') && !&eol && (&bin || !&fixeol) 43 | 44 | " We need to find out whether the cursor is currently on a match or not 45 | " since that'll affect our numbering. Naturally, there's no easy way to 46 | " get such information. The hard way is to wiggle the cursor a bit and 47 | " try to search back and check if we ended up where we started. There are 48 | " two edge cases though. 49 | let curpos = getpos('.') 50 | if line2byte(line('$') + 1) <= 3 51 | " The buffer is empty or has only one character. 52 | " In this case, we can't wiggle the cursor, so we just search and 53 | " check for the 'E486 Pattern not found' error. 54 | set wrapscan 55 | try 56 | silent keepjumps normal! n 57 | catch /^Vim[^)]\+):E486\D/ 58 | let is_on_match = 0 59 | endtry 60 | set nowrapscan 61 | elseif line2byte('.') + col('.') - 1 <= 1 62 | " We're at the very start of the buffer. 63 | " We move the cursor forwards. 64 | silent! keepjumps goto 2 65 | silent! exec 'keepjumps normal!' (v:searchforward ? 'N' : 'n') 66 | else 67 | " In every other case, we move the cursor backwards. This works even 68 | " if we're at the very edge of the buffer which is nice because I 69 | " couldn't find any surefire way to check for that. 70 | silent! exec 'keepjumps goto' (line2byte('.') + col('.') - (eolbug ? 0 : 2)) 71 | silent! exec 'keepjumps normal!' (v:searchforward ? 'n' : 'N') 72 | endif 73 | if getpos('.') != curpos | let is_on_match = 0 | endif 74 | call winrestview(winview) 75 | 76 | " This is the algorithm itself; we first count all the matches before the 77 | " cursor and then all the ones after it. To count these, we first try 78 | " moving in tens; running '10n' is (mostly) the same as running 'n' 10 79 | " times but it's faster since it runs in C. If however there are only, 80 | " say, 9 matches, Vim will internally run 'n' 9 times before announcing 81 | " that the 10th found no match but with no way to see how many matched; 82 | " other than counting them one-by-one. While this wastes some searches as 83 | " a whole it ends up being far faster than doing it all one-by-one. 84 | try 85 | while before <= g:indexed_search_max_hits || a:force 86 | " if reltimefloat(reltime(now)) > 0.1 | break | endif 87 | try 88 | silent keepjumps normal! 10N 89 | let before += 10 90 | catch /^Vim[^)]\+):E38[45]\D/ 91 | try 92 | silent keepjumps normal! N 93 | let before += 1 94 | catch /^Vim[^)]\+):E38[45]\D/ 95 | let first_match_lnum = line('.') 96 | break 97 | endtry 98 | endtry 99 | endwhile 100 | call winrestview(winview) 101 | while before + after <= g:indexed_search_max_hits || a:force 102 | " if reltimefloat(reltime(now)) > 0.1 | break | endif 103 | try 104 | silent keepjumps normal! 10n 105 | let after += 10 106 | catch /^Vim[^)]\+):E38[45]\D/ 107 | try 108 | silent keepjumps normal! n 109 | let after += 1 110 | catch /^Vim[^)]\+):E38[45]\D/ 111 | let last_match_lnum = line('.') 112 | break 113 | endtry 114 | endtry 115 | endwhile 116 | finally 117 | let [&wrapscan, &foldenable] = [save_ws, save_fen] 118 | call winrestview(winview) 119 | endtry 120 | if !v:searchforward 121 | let [after, before] = [before, after] 122 | let [first_match_lnum, last_match_lnum] = [last_match_lnum, first_match_lnum] 123 | end 124 | 125 | let out_of_time = (!a:force && before > g:indexed_search_max_hits) 126 | \ + (!a:force && after + before > g:indexed_search_max_hits) 127 | 128 | let index = before + is_on_match 129 | let total = before + after + is_on_match 130 | return [index, total, is_on_match, out_of_time, first_match_lnum, last_match_lnum] 131 | endfunction 132 | 133 | function! s:index_message(index, total, is_on_match, out_of_time, first_match_lnum, last_match_lnum) 134 | let hl = 'Directory' 135 | let msg = '' 136 | 137 | let matches = a:total 138 | if a:out_of_time 139 | let matches = '> '. a:total 140 | if !a:is_on_match || a:out_of_time > 1 141 | return [hl, matches .' matches'] 142 | endif 143 | endif 144 | 145 | let line_info = "" 146 | if g:indexed_search_line_info 147 | let line_info = ' (FM:'. a:first_match_lnum .', LM:'. a:last_match_lnum .')' 148 | endif 149 | let shortmatch = matches . line_info . (g:indexed_search_shortmess ? '' : ' matches') 150 | 151 | if a:total == 0 152 | let hl = 'Error' 153 | let msg = 'No matches' 154 | 155 | elseif !a:is_on_match && a:index == 0 156 | let hl = 'WarningMsg' 157 | let msg = 'Before first match, of '. shortmatch 158 | if a:total == 1 | let msg = 'Before single match' | endif 159 | elseif !a:is_on_match && a:index == a:total 160 | let hl = 'WarningMsg' 161 | let msg = 'After last match of '. shortmatch 162 | if a:total == 1 | let msg = 'After single match' | endif 163 | elseif !a:is_on_match 164 | " hl remains default 165 | let msg = 'Between matches '. a:index .'-'. (a:index+1) .' of '. matches . line_info 166 | 167 | elseif !g:indexed_search_numbered_only && a:index == 1 && a:total == 1 168 | let hl = 'Search' 169 | let msg = 'Single match' 170 | elseif !g:indexed_search_numbered_only && a:index == 1 171 | let hl = 'Search' 172 | let msg = 'First of '. shortmatch 173 | elseif !g:indexed_search_numbered_only && a:index == a:total 174 | let hl = 'LineNr' 175 | let msg = 'Last of '. shortmatch 176 | else 177 | " hl remains default 178 | let msg = (g:indexed_search_shortmess ? '' : 'Match '). a:index .' of '. matches . line_info 179 | endif 180 | 181 | return [hl, msg.' /'.@/.'/'] 182 | endfunction 183 | 184 | 185 | function! indexed_search#show_index(force) 186 | if @/ == '' || (!a:force && line('$') >= g:indexed_search_max_lines) 187 | return 188 | endif 189 | 190 | let results = s:search(a:force) 191 | let [hl, msg] = call('s:index_message', results) 192 | call s:echohl(g:indexed_search_colors ? hl : 'None', msg) 193 | endfunction 194 | -------------------------------------------------------------------------------- /doc/indexed-search.txt: -------------------------------------------------------------------------------- 1 | *indexed-search.txt* Show match's count and index with search command 2 | 3 | Author: Yakov Lerner 4 | Otto Modinos 5 | 6 | INTRODUCTION *indexed-search* 7 | 8 | This plugin redefines 6 search commands (/,?,n,N,*,#). At every 9 | search command, it automatically prints > 10 | "At match #N out of M matches". 11 | < 12 | -- the total number of matches (M) and the number(index) of current match (N). 13 | This helps to get oriented when searching forward and backward. 14 | 15 | There's only one command and no new behavior to learn. Just watch the bottom 16 | line when you do /,?,n,N,*,#. 17 | 18 | Works on vim7. Won't cause slowdown on very large files (but then counters 19 | are not displayed). 20 | 21 | ============================================================================= 22 | COMMANDS *indexed-search-commands* 23 | 24 | *:ShowSearchIndex* 25 | :ShowSearchIndex Shows a message indicating your position relative to 26 | the search matches in the file. The message's content 27 | also depends on the value of the |g:indexed_search_shortmess| 28 | |g:indexed_search_line_info| and |g:indexed_search_colors| options. 29 | 30 | ============================================================================= 31 | OPTIONS *indexed-search-options* 32 | 33 | *g:indexed_search_mappings* 34 | g:indexed_search_mappings 35 | If 0, does not create mappings. |indexed-search-mappings| 36 | Default: 1 37 | 38 | *g:indexed_search_dont_move* 39 | g:indexed_search_dont_move 40 | If 1, the mappings for * and # stay on the word under the 41 | cursor. Basically, *N (or #N). 42 | Default: 0 43 | 44 | *g:indexed_search_center* 45 | g:indexed_search_center 46 | If 1, the mappings for n and N also center the cursor on the 47 | match. Basically, nzz (or Nzz). 48 | Default: 0 49 | 50 | *g:indexed_search_max_lines* 51 | g:indexed_search_max_lines 52 | If a file has more lines than this, the plugin doesn't display 53 | messages, for performance reasons. |indexed-search-performance| 54 | Default: 3000 55 | 56 | *g:indexed_search_max_hits* 57 | g:indexed_search_max_hits 58 | When there are more matches than this, the plugin doesn't try 59 | to count all of them. Instead it says, e.g. "> 1000 matches" 60 | Default: 1000 |indexed-search-performance| 61 | 62 | *g:indexed_search_colors* 63 | g:indexed_search_colors 64 | If 0, the messages are not displayed in color 65 | Default: 1 66 | *g:indexed_search_line_info* 67 | g:indexed_search_line_info 68 | If 1, the messages also mention the first and the last match 69 | line number in the following format: 70 | FM:, LM: 71 | Default: 0 72 | 73 | *g:indexed_search_shortmess* 74 | g:indexed_search_shortmess 75 | If 1, the messages are (a bit) shorter. 76 | Default: 0 77 | 78 | *g:indexed_search_numbered_only* 79 | g:indexed_search_numbered_only 80 | If 1, the messages only contain the search count. No 81 | "First of" and "Last of". 82 | Default: 0 83 | 84 | *g:indexed_search_n_always_searches_forward* 85 | g:indexed_search_n_always_searches_forward 86 | In vim, by default, the direction of n and N depends on 87 | whether / or ? was used for searching forward or backward 88 | respectively. 89 | If 1, n always search forward and N backward 90 | Default: 0 91 | 92 | ============================================================================= 93 | MAPPINGS *indexed-search-mappings* 94 | 95 | Indexed Search remaps the standard Vim keys /, ?, *, #, n and N, so that the 96 | appropriate message is displayed afterwards. If you don't want that, you can 97 | disable these mappings by letting |g:indexed_search_mappings| to 0. 98 | 99 | ============================================================================= 100 | PERFORMANCE *indexed-search-performance* 101 | 102 | Plugin bypasses the calculation of match index when it would take too much 103 | time (too many matches, too large file). You can use |g:indexed_search_max_lines| 104 | and |g:indexed_search_max_hits| to tune these performance limits. 105 | 106 | vim:tw=78:ts=8:ft=help:norl: 107 | -------------------------------------------------------------------------------- /plugin/indexed-search.vim: -------------------------------------------------------------------------------- 1 | " Author: Yakov Lerner 2 | " URL: http://www.vim.org/scripts/script.php?script_id=1682 3 | " Last change: 2018-03-21 4 | 5 | " This script redefines 6 search commands (/,?,n,N,*,#). At each search, it 6 | " shows at which match number you are, and the total number of matches, like 7 | " this: "At nth match out of N". This is printed at the bottom line at every 8 | " n,N,/,?,*,# search command, automatically. 9 | " 10 | " I am posting this plugin because I find it useful. 11 | 12 | " :ShowSearchIndex - Checking your match index 13 | " ----------------------------------------------------- 14 | " At any time, you can use :ShowSearchIndex to show at which match index you 15 | " are without moving the cursor. 16 | " 17 | " If cursor is exactly on the match, the message is: 18 | " At Nth match of M 19 | " If cursor is between matches, following messages are displayed: 20 | " Betwen matches 189-190 of 300 21 | " Before first match, of 300 22 | " After last match, of 300 23 | 24 | " To disable colors for messages, set g:indexed_search_colors to 0. 25 | " 26 | " Performance 27 | " ------------------------------------------------------ 28 | " Plugin bypasses match counting when it would take too much time, i.e. too 29 | " many matches or too large a file. You can change these limits with 30 | " g:indexed_search_max_lines and g:indexed_search_max_hits. 31 | 32 | 33 | if exists("g:loaded_indexed_search") || &cp || v:version < 700 34 | finish 35 | endif 36 | let g:loaded_indexed_search = 1 37 | 38 | let s:save_cpo = &cpo 39 | set cpo&vim 40 | 41 | 42 | " Performance tuning limits 43 | if !exists('g:indexed_search_max_lines') 44 | " Max filesize (in lines) up to where the plugin works 45 | let g:indexed_search_max_lines = 30000 46 | endif 47 | 48 | if !exists("g:indexed_search_max_hits") 49 | " Max number of matches up to where the plugin stops counting 50 | let g:indexed_search_max_hits = 1000 51 | endif 52 | 53 | " Appearance settings 54 | if !exists('g:indexed_search_colors') 55 | " Whether to use colors for messages 56 | let g:indexed_search_colors = 1 57 | endif 58 | 59 | if !exists('g:indexed_search_shortmess') 60 | " Make messages shorter 61 | let g:indexed_search_shortmess = 0 62 | endif 63 | 64 | if !exists('g:indexed_search_numbered_only') 65 | " Only show index number, no extra words 66 | let g:indexed_search_numbered_only = 0 67 | endif 68 | 69 | if !exists('g:indexed_search_line_info') 70 | let g:indexed_search_line_info = 0 71 | endif 72 | 73 | " Mappings 74 | if !exists('g:indexed_search_mappings') 75 | let g:indexed_search_mappings = 1 76 | endif 77 | 78 | if !exists('g:indexed_search_dont_move') 79 | let g:indexed_search_dont_move = 0 80 | endif 81 | 82 | if !exists('g:indexed_search_center') 83 | let g:indexed_search_center = 0 84 | endif 85 | 86 | if !exists('g:indexed_search_n_always_searches_forward') 87 | let g:indexed_search_n_always_searches_forward = 0 88 | endif 89 | 90 | 91 | command! -bang ShowSearchIndex :call indexed_search#show_index(0) 92 | 93 | 94 | function! s:should_unfold() 95 | return has('folding') && &fdo =~ 'search\|all' 96 | endfunction 97 | 98 | function! s:has_mapping(name) 99 | return !empty(maparg(a:name, mode())) 100 | endfunction 101 | 102 | function! s:restview() 103 | call winrestview(s:winview) 104 | endfunction 105 | 106 | function! s:star(seq) 107 | if g:indexed_search_dont_move 108 | let s:winview = winsaveview() 109 | return a:seq . "\(indexed-search-restview)" 110 | endif 111 | return a:seq 112 | endfunction 113 | 114 | function! s:n(seq) 115 | if g:indexed_search_n_always_searches_forward && !v:searchforward 116 | return ["\(indexed-search-n)", "\(indexed-search-N)"][a:seq ==# 'n'] 117 | endif 118 | return a:seq 119 | endfunction 120 | 121 | function! s:after() 122 | return (s:should_unfold() ? 'zv' : '') 123 | \ .(g:indexed_search_center ? 'zz' : '') 124 | \ .(s:has_mapping('(indexed-search-custom)') ? "\(indexed-search-custom)" : '') 125 | \ ."\(indexed-search-index)" 126 | endfunction 127 | 128 | 129 | if g:indexed_search_mappings 130 | noremap (indexed-search-index) 131 | nnoremap (indexed-search-index) :ShowSearchIndex 132 | xnoremap (indexed-search-index) :ShowSearchIndexgv 133 | 134 | noremap (indexed-search-n) n 135 | noremap (indexed-search-N) N 136 | 137 | noremap (indexed-search-restview) :call restview() 138 | xnoremap (indexed-search-restview) :call restview()gv 139 | 140 | map (indexed-search-after) after() 141 | imap (indexed-search-after) 142 | 143 | cmap "\" . (getcmdtype() =~ '[/?]' ? "\(indexed-search-after)" : '') 144 | " map gd 'gd' . "\(indexed-search-after)" 145 | " map gD 'gD' . "\(indexed-search-after)" 146 | map * star('*') . "\(indexed-search-after)" 147 | map # star('#') . "\(indexed-search-after)" 148 | map g* star('g*') . "\(indexed-search-after)" 149 | map g# star('g#') . "\(indexed-search-after)" 150 | map n n('n') . "\(indexed-search-after)" 151 | map N n('N') . "\(indexed-search-after)" 152 | endif 153 | 154 | 155 | let &cpo = s:save_cpo 156 | 157 | " Wishlist 158 | " - using high-precision timer of vim7, count number of millisec 159 | " to run the counters, and base auto-disabling on time it takes. 160 | " very complex regexes can be terribly slow even of files like 'man bash' 161 | " which is mere 5k lines long. Also when there are >10k matches in the file 162 | " set limit to 200 millisec 163 | " - implement CursorHold bg counting to which too_slow will resort 164 | " - even on large files, we can show "At last match", "After last match" 165 | " - define global vars for all highlights, with defaults 166 | --------------------------------------------------------------------------------