quick_scope.vim (12596B)
1 " Autoload interface functions ------------------------------------------------- 2 3 function! quick_scope#Toggle() abort 4 if g:qs_enable 5 let g:qs_enable = 0 6 call quick_scope#UnhighlightLine() 7 else 8 let g:qs_enable = 1 9 doautocmd CursorMoved 10 endif 11 endfunction 12 13 " The direction can be 0 (backward), 1 (forward) or 2 (both). Targets are the 14 " characters that can be highlighted. 15 function! quick_scope#HighlightLine(direction, targets) abort 16 if g:qs_enable && (!exists('b:qs_local_disable') || !b:qs_local_disable) && index(get(g:, 'qs_buftype_blacklist', []), &buftype) < 0 17 let line = getline(line('.')) 18 let len = strlen(line) 19 let pos = col('.') 20 21 if !empty(line) && len <= g:qs_max_chars 22 " Highlight after the cursor. 23 if a:direction != 0 24 let [patt_p, patt_s] = s:get_highlight_patterns(line, pos, len, a:targets) 25 call s:apply_highlight_patterns([patt_p, patt_s]) 26 endif 27 28 " Highlight before the cursor. 29 if a:direction != 1 30 let [patt_p, patt_s] = s:get_highlight_patterns(line, pos, -1, a:targets) 31 call s:apply_highlight_patterns([patt_p, patt_s]) 32 endif 33 endif 34 endif 35 endfunction 36 37 function! quick_scope#UnhighlightLine() abort 38 for m in filter(getmatches(), printf('v:val.group ==# "%s" || v:val.group ==# "%s"', g:qs_hi_group_primary, g:qs_hi_group_secondary)) 39 call matchdelete(m.id) 40 endfor 41 endfunction 42 43 " Set or reset flags and state for highlighting on key press. 44 function! quick_scope#Ready() abort 45 " Direction of highlight search. 0 is backward, 1 is forward 46 let s:direction = 0 47 48 " The corresponding character to f,F,t or T 49 let s:target = '' 50 51 " Position of where a dummy cursor should be placed. 52 let s:cursor = 0 53 54 " Characters with secondary highlights. Modified by get_highlight_patterns() 55 let s:chars_s = [] 56 57 call s:handle_extra_highlight(2) 58 59 " Intentionally return an empty string that will be concatenated with the 60 " return values from aim(), reload() and double_tap(). 61 return '' 62 endfunction 63 64 " Returns {character motion}{captured char} (to map to a character motion) to 65 " emulate one as closely as possible. 66 function! quick_scope#Aim(motion) abort 67 if (a:motion ==# 'f' || a:motion ==# 't') 68 let s:direction = 1 69 else 70 let s:direction = 0 71 endif 72 73 " Add a dummy cursor since calling getchar() places the actual cursor on 74 " the command line. 75 let s:cursor = matchadd(g:qs_hi_group_cursor, '\%#', g:qs_hi_priority + 1) 76 77 " Silence 'Type :quit<Enter> to exit Vim' message on <c-c> during a 78 " character search. 79 " 80 " This line also causes getchar() to cleanly cancel on a <c-c>. 81 let b:qs_prev_ctrl_c_map = maparg('<c-c>', 'n', 0, 1) 82 if empty(b:qs_prev_ctrl_c_map) 83 unlet b:qs_prev_ctrl_c_map 84 endif 85 execute 'nnoremap <silent> <c-c> <c-c>' 86 87 call quick_scope#HighlightLine(s:direction, g:qs_accepted_chars) 88 89 redraw 90 91 " Store and capture the target for the character motion. 92 let char = getchar() 93 let s:target = char ==# "\<S-lt>" ? '<' : nr2char(char) 94 95 return a:motion . s:target 96 endfunction 97 98 " Cleanup after a character motion is executed. 99 function! quick_scope#Reload() abort 100 " Remove dummy cursor 101 call matchdelete(s:cursor) 102 103 " Restore previous or default <c-c> functionality 104 if exists('b:qs_prev_ctrl_c_map') 105 call quick_scope#mapping#Restore(b:qs_prev_ctrl_c_map) 106 unlet b:qs_prev_ctrl_c_map 107 else 108 execute 'nunmap <c-c>' 109 endif 110 111 call quick_scope#UnhighlightLine() 112 113 " Intentionally return an empty string. 114 return '' 115 endfunction 116 117 " Trigger an extra highlight for a target character only if it originally had 118 " a secondary highlight. 119 function! quick_scope#DoubleTap() abort 120 if index(s:chars_s, s:target) != -1 121 " Warning: slight hack below. Although the cursor has already moved by 122 " this point, col('.') won't return the updated cursor position until the 123 " invoking mapping completes. So when highlight_line() is called here, the 124 " first occurrence of the target will be under the cursor, and the second 125 " occurrence will be where the first occurence should have been. 126 call quick_scope#HighlightLine(s:direction, [expand(s:target)]) 127 128 " Unhighlight only primary highlights (i.e., the character under the 129 " cursor). 130 for m in filter(getmatches(), printf('v:val.group ==# "%s"', g:qs_hi_group_primary)) 131 call matchdelete(m.id) 132 endfor 133 134 " Temporarily change the second occurrence highlight color to a primary 135 " highlight color. 136 call s:save_secondary_highlight() 137 execute 'highlight! link ' . g:qs_hi_group_secondary . ' ' . g:qs_hi_group_primary 138 139 " Set a temporary event to keep track of when to reset the extra 140 " highlight. 141 augroup quick_scope 142 autocmd CursorMoved * call s:handle_extra_highlight(1) 143 augroup END 144 145 call s:handle_extra_highlight(0) 146 endif 147 148 " Intentionally return an empty string. 149 return '' 150 endfunction 151 152 " Helpers ---------------------------------------------------------------------- 153 154 " Apply the highlights for each highlight group based on pattern strings. 155 " Arguments are expected to be lists of two items. 156 function! s:apply_highlight_patterns(patterns) abort 157 let [patt_p, patt_s] = a:patterns 158 if !empty(patt_p) 159 " Highlight columns corresponding to matched characters. 160 " 161 " Ignore the leading | in the primary highlights string. 162 call matchadd(g:qs_hi_group_primary, '\v%' . line('.') . 'l(' . patt_p[1:] . ')', g:qs_hi_priority) 163 endif 164 if !empty(patt_s) 165 call matchadd(g:qs_hi_group_secondary, '\v%' . line('.') . 'l(' . patt_s[1:] . ')', g:qs_hi_priority) 166 endif 167 endfunction 168 169 " Keep track of which characters have a secondary highlight (but no primary 170 " highlight) and store them in :chars_s. Used when g:qs_highlight_on_keys is 171 " active to decide whether to trigger an extra highlight. 172 function! s:save_chars_with_secondary_highlights(chars) abort 173 let [char_p, char_s] = a:chars 174 175 if !empty(char_p) 176 " Do nothing 177 elseif !empty(char_s) 178 call add(s:chars_s, char_s) 179 endif 180 endfunction 181 182 " Set or append to the pattern strings for the highlights. 183 function! s:add_to_highlight_patterns(patterns, highlights) abort 184 let [patt_p, patt_s] = a:patterns 185 let [hi_p, hi_s] = a:highlights 186 187 " If there is a primary highlight for the last word, add it to the primary 188 " highlight pattern. 189 if hi_p > 0 190 let patt_p = printf('%s|%%%sc', patt_p, hi_p) 191 elseif hi_s > 0 192 let patt_s = printf('%s|%%%sc', patt_s, hi_s) 193 endif 194 195 return [patt_p, patt_s] 196 endfunction 197 198 " Finds which characters to highlight and returns their column positions as a 199 " pattern string. 200 function! s:get_highlight_patterns(line, cursor, end, targets) abort 201 " Keeps track of the number of occurrences for each target 202 let occurrences = {} 203 204 " Patterns to match the characters that will be marked with primary and 205 " secondary highlight groups, respectively 206 let [patt_p, patt_s] = ['', ''] 207 208 " Indicates whether this is the first word under the cursor. We don't want 209 " to highlight any characters in it. 210 let is_first_word = 1 211 212 " We want to skip the first char as this is the char the cursor is at 213 let is_first_char = 1 214 215 " The position of a character in a word that will be given a highlight. A 216 " value of 0 indicates there is no character to highlight. 217 let [hi_p, hi_s] = [0, 0] 218 219 " The (next) characters that will be given a highlight. Used by 220 " save_chars_with_secondary_highlights() to see whether an extra highlight 221 " should be triggered if g:qs_highlight_on_keys is active. 222 let [char_p, char_s] = ['', ''] 223 224 " If 1, we're looping forwards from the cursor to the end of the line; 225 " otherwise, we're looping from the cursor to the beginning of the line. 226 let direction = a:cursor < a:end ? 1 : 0 227 228 " find the character index i and the byte index c 229 " of the current cursor position 230 let c = 1 231 let i = 0 232 let char = '' 233 while c != a:cursor 234 let char = matchstr(a:line, '.', byteidx(a:line, i)) 235 let c += len(char) 236 let i += 1 237 endwhile 238 239 " reposition cursor to end of the char's composing bytes 240 if !direction 241 let c += len(matchstr(a:line, '.', byteidx(a:line, i))) - 1 242 endif 243 244 " catch cases where multibyte chars may result in c not exactly equal to 245 " a:end 246 while (direction && c <= a:end || !direction && c >= a:end) 247 248 let char = matchstr(a:line, '.', byteidx(a:line, i)) 249 250 " Skips the first char as it is the char the cursor is at 251 if is_first_char 252 253 let is_first_char = 0 254 255 " Don't consider the character for highlighting, but mark the position 256 " as the start of a new word. 257 " use '\k' to check agains keyword characters (see :help 'iskeyword' and 258 " :help /\k) 259 elseif char !~# '\k' || empty(char) 260 if !is_first_word 261 let [patt_p, patt_s] = s:add_to_highlight_patterns([patt_p, patt_s], [hi_p, hi_s]) 262 endif 263 264 " We've reached a new word, so reset any highlights. 265 let [hi_p, hi_s] = [0, 0] 266 let [char_p, char_s] = ['', ''] 267 268 let is_first_word = 0 269 elseif index(a:targets, char) != -1 270 if has_key(occurrences, char) 271 let occurrences[char] += 1 272 else 273 let occurrences[char] = 1 274 endif 275 276 if !is_first_word 277 let char_occurrences = get(occurrences, char) 278 279 " If the search is forward, we want to be greedy; otherwise, we 280 " want to be reluctant. This prioritizes highlighting for 281 " characters at the beginning of a word. 282 " 283 " If this is the first occurrence of the letter in the word, 284 " mark it for a highlight. 285 " If we are looking backwards, c will point to the end of the 286 " end of composing bytes so we adjust accordingly 287 " eg. with a multibyte char of length 3, c will point to the 288 " 3rd byte. Minus (len(char) - 1) to adjust to 1st byte 289 if char_occurrences == 1 && ((direction == 1 && hi_p == 0) || direction == 0) 290 let hi_p = c - (1 - direction) * (len(char) - 1) 291 let char_p = char 292 elseif char_occurrences == 2 && ((direction == 1 && hi_s == 0) || direction == 0) 293 let hi_s = c - (1 - direction) * (len(char)- 1) 294 let char_s = char 295 endif 296 endif 297 endif 298 299 " update i to next character 300 " update c to next byteindex 301 if direction == 1 302 let i += 1 303 let c += strlen(char) 304 else 305 let i -= 1 306 let c -= strlen(char) 307 endif 308 endwhile 309 310 let [patt_p, patt_s] = s:add_to_highlight_patterns([patt_p, patt_s], [hi_p, hi_s]) 311 312 if exists('g:qs_highlight_on_keys') 313 call s:save_chars_with_secondary_highlights([char_p, char_s]) 314 endif 315 316 return [patt_p, patt_s] 317 endfunction 318 319 " Save the value of g:qs_hi_group_secondary to preserve customization before 320 " changing it as a result of a double_tap 321 function! s:save_secondary_highlight() abort 322 if &verbose 323 let s:saved_verbose = &verbose 324 set verbose=0 325 endif 326 327 redir => s:saved_secondary_highlight 328 execute 'silent highlight ' . g:qs_hi_group_secondary 329 redir END 330 331 if exists('s:saved_verbose') 332 execute 'set verbose=' . s:saved_verbose 333 endif 334 335 let s:saved_secondary_highlight = substitute(s:saved_secondary_highlight, '^.*xxx ', '', '') 336 endfunction 337 338 " Reset g:qs_hi_group_secondary to its saved value after it was changed as a result 339 " of a double_tap 340 function! s:reset_saved_secondary_highlight() abort 341 if s:saved_secondary_highlight =~# '^links to ' 342 let s:saved_secondary_hlgroup_only = substitute(s:saved_secondary_highlight, '^links to ', '', '') 343 execute 'highlight! link ' . g:qs_hi_group_secondary . ' ' . s:saved_secondary_hlgroup_only 344 else 345 execute 'highlight ' . g:qs_hi_group_secondary . ' ' . s:saved_secondary_highlight 346 endif 347 endfunction 348 349 " Highlight on key press ----------------------------------------------------- 350 " Manage state for keeping or removing the extra highlight after triggering a 351 " highlight on key press. 352 " 353 " State can be 0 (extra highlight has just been triggered), 1 (the cursor has 354 " moved while an extra highlight is active), or 2 (cancel an active extra 355 " highlight). 356 function! s:handle_extra_highlight(state) abort 357 if a:state == 0 358 let s:cursor_moved_count = 0 359 elseif a:state == 1 360 let s:cursor_moved_count = s:cursor_moved_count + 1 361 endif 362 363 " If the cursor has moved more than once since the extra highlight has been 364 " active (or the state is 2), reset the extra highlight. 365 if exists('s:cursor_moved_count') && (a:state == 2 || s:cursor_moved_count > 1) 366 call quick_scope#UnhighlightLine() 367 call s:reset_saved_secondary_highlight() 368 autocmd! quick_scope CursorMoved 369 endif 370 endfunction