1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103
|
" MIT (c) Wenxuan Zhang and Zach Nielsen
" Script-scoped constants
let s:STATE_CURSOR = 0b00001
let s:STATE_DIFF_RM = 0b00010
let s:STATE_DIFF_ADD = 0b00100
let s:STATE_DIFF_MOD = 0b01000
let s:STATE_WINDOW_RANGE = 0b10000
let s:last_pos = {}
let s:last_range = {}
let s:win_info = {}
let s:len_cache = {}
function! minimap#vim#MinimapToggle() abort
call s:toggle_window()
endfunction
function! minimap#vim#MinimapClose() abort
call s:close_window()
endfunction
function! minimap#vim#MinimapOpen() abort
call s:open_window()
endfunction
function! minimap#vim#MinimapRefresh() abort
call s:refresh_minimap(1)
endfunction
function! minimap#vim#MinimapUpdateHighlight() abort
call s:get_window_info()
call s:update_highlight()
endfunction
function! minimap#vim#MinimapRescan() abort
call remove(s:len_cache, expand('%'))
call s:get_window_info()
call s:refresh_minimap(1)
call s:update_highlight()
endfunction
function! s:buffer_enter_handler() abort
if &filetype ==# 'minimap'
call s:minimap_buffer_enter_handler()
elseif &buftype !=# 'terminal'
call s:source_buffer_enter_handler()
endif
endfunction
function! s:cursor_move_handler() abort
if &filetype ==# 'minimap'
call s:minimap_move()
else
call s:source_move()
endif
endfunction
function! s:win_enter_handler() abort
if &filetype ==# 'minimap'
call s:minimap_win_enter()
else
call s:source_win_enter()
endif
endfunction
function! s:tab_leave_handler() abort
" Nuke our state - we don't keep a per-tab cache so we need to rebuild on
" any tab change
let s:last_pos = {}
let s:last_range = {}
let s:win_info = {}
let s:len_cache = {}
endfunction
function! s:get_longest_line_cmd() abort
if has('mac')
return 'gwc'
endif
return 'wc'
endfunction
let s:bin_dir = expand('<sfile>:p:h:h:h').'/bin/'
if has('win32')
let s:minimap_gen = s:bin_dir.'minimap_generator.bat'
let s:default_shell = 'cmd.exe'
let s:default_shellflag = '/s /c'
else
let s:minimap_gen = s:bin_dir.'minimap_generator.sh'
let s:default_shell = 'sh'
let s:default_shellflag = '-c'
endif
let s:minimap_cache = {}
function! s:toggle_window() abort
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr != -1
call s:close_window()
return
endif
call s:open_window()
endfunction
function! s:close_window() abort
call s:clear_highlights()
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr == -1
return
endif
if winnr() == mmwinnr
if winbufnr(2) != -1
" Other windows are open, only close this one
close
exe 'wincmd p'
endif
else
exe mmwinnr . 'wincmd c'
endif
let s:win_info = {}
endfunction
function! s:quit_last() abort
let tabnum = tabpagenr()
if tabnum == tabpagenr('$') && tabnum == 1
doautocmd ExitPre,VimLeavePre,VimLeave
endif
let s:win_info = {}
execute 'quit'
endfunction
function! s:close_auto() abort
if winnr('$') == 3 && exists('g:coc_last_float_win') && win_id2win(g:coc_last_float_win) != 0
" This addresses an issue where the minimap will not close
" if CoC has a diagnostic window open - GH-74
elseif winnr('$') != 1
return
endif
if g:minimap_did_quit
silent! call s:quit_last()
else
" Since we just closed a buffer, we want to completely wipe out
" the minimap tied to that buffer.
bwipeout
endif
" In case the plugin accidentally highlights the main buffer.
call s:clear_highlights()
endfunction
function! s:open_window() abort
" If the minimap window is already open jump to it
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr != -1 || s:closed_on() || s:ignored() " Don't open if file/buftype is ignored/closed on
return
endif
let g:minimap_opening = 1 " Used to lock out autocmds, otherwise we get multiple fires that take a long time
" Preserve 'previous buffer' when opening the minimap
let prev_buffer = bufnr('#')
let curr_buffer = bufnr()
let openpos = g:minimap_left ? 'topleft vertical ' : 'botright vertical '
noautocmd execute 'silent! ' . openpos . g:minimap_width . 'split ' . '-MINIMAP-'
" Buffer-local options
setlocal filetype=minimap
setlocal noreadonly " in case the "view" mode is used
setlocal buftype=nofile
setlocal bufhidden=wipe
setlocal noswapfile
setlocal nobuflisted
setlocal nomodifiable
setlocal textwidth=0
" Window-local options
setlocal nolist
setlocal winfixwidth
setlocal nospell
setlocal nowrap
setlocal nonumber
setlocal nofoldenable
setlocal foldcolumn=0
setlocal foldmethod&
setlocal foldexpr&
setlocal nocursorline
silent! setlocal signcolumn=no
silent! setlocal norelativenumber
silent! setlocal sidescrolloff=0
let cpoptions_save = &cpoptions
set cpoptions&vim
augroup MinimapAutoCmds
autocmd!
autocmd QuitPre * let g:minimap_did_quit = 1
autocmd WinEnter <buffer> call s:handle_autocmd(0)
autocmd WinEnter * call s:handle_autocmd(1)
autocmd BufWritePost,VimResized * call s:handle_autocmd(2)
autocmd BufEnter,FileType * call s:handle_autocmd(3)
autocmd FocusGained,CursorMoved,CursorMovedI <buffer> call s:handle_autocmd(4)
if g:minimap_highlight_range
" Vim and Neovim (pre-November 2020) do not have a WinScrolled autocmd event.
if exists('##WinScrolled')
autocmd FocusGained,WinScrolled * call s:handle_autocmd(5)
else
autocmd FocusGained,CursorMoved,CursorMovedI * call s:handle_autocmd(5)
endif
endif
autocmd FocusGained,CursorMoved,CursorMovedI * call s:handle_autocmd(6)
if g:minimap_highlight_search != 0
autocmd CmdlineLeave * if expand('<afile>') == '/' || expand('<afile>') == '?' |
\ call s:minimap_update_color_search(getcmdline())
endif
autocmd VimEnter,DiffUpdated * call s:handle_autocmd(7)
autocmd TabLeave * call s:handle_autocmd(8)
augroup END
" https://github.com/neovim/neovim/issues/6211
noremap <buffer> <ScrollWheelUp> k
noremap <buffer> <2-ScrollWheelUp> k
noremap <buffer> <3-ScrollWheelUp> k
noremap <buffer> <4-ScrollWheelUp> k
noremap <buffer> <ScrollWheelDown> j
noremap <buffer> <2-ScrollWheelDown> j
noremap <buffer> <3-ScrollWheelDown> j
noremap <buffer> <4-ScrollWheelDown> j
let &cpoptions = cpoptions_save
execute 'wincmd p'
call s:refresh_minimap(1)
call s:get_window_info() " Must call after refresh_minimap so we can get the mm height after creation
call s:update_highlight()
" Restore buffer orders
execute(prev_buffer . 'buffer')
execute(curr_buffer . 'buffer')
let g:minimap_opening = 0
endfunction
function! s:handle_autocmd(cmd) abort
if g:minimap_opening == 0
if s:closed_on()
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr != -1
call s:close_window()
endif
elseif s:ignored()
return
elseif a:cmd == 0 " WinEnter <buffer>
" echom 'WinEnter <buffer>'
call s:close_auto()
elseif a:cmd == 1 " WinEnter *
" echom 'WinEnter *'
" If previously triggered minimap_did_quit, untrigger it
let g:minimap_did_quit = 0
call s:win_enter_handler()
elseif a:cmd == 2 " BufWritePost,VimResized *
" echom 'BufWritePost,VimResized *'
call s:refresh_minimap(1)
call s:update_highlight()
elseif a:cmd == 3 " BufEnter,FileType *
" echom 'BufEnter,FileType *'
call s:buffer_enter_handler()
elseif a:cmd == 4 " FocusGained,CursorMoved,CursorMovedI <buffer>
" echom 'FocusGained,CursorMoved,CursorMovedI <buffer>'
call s:minimap_move()
elseif a:cmd == 5 " FocusGained,WinScrolled * (neovim); else same autocmds as below
" echom 'FocusGained,WinScrolled * (neovim); else same autocmds as below'
call s:source_win_scroll()
elseif a:cmd == 6 " FocusGained,CursorMoved,CursorMovedI *
" echom 'FocusGained,CursorMoved,CursorMovedI *'
call s:source_move()
elseif a:cmd == 7 " VimEnter,DiffUpdated *
" echom 'VimEnter,DiffUpdated *'
call s:minimap_diffoff()
elseif a:cmd == 8 " TabLeave *
echom 'TabLeave *'
call s:tab_leave_handler()
endif
endif
endfunction
function! s:ignored() abort
return &filetype !=# 'minimap' &&
\ (
\ index(g:minimap_block_buftypes, &buftype) >= 0 ||
\ index(g:minimap_block_filetypes, &filetype) >= 0
\ )
endfunction
function! s:closed_on() abort
return &filetype !=# 'minimap' &&
\ (
\ index(g:minimap_close_buftypes, &buftype) >= 0 ||
\ index(g:minimap_close_filetypes, &filetype) >= 0
\ )
endfunction
function! s:refresh_minimap(force) abort
if &filetype ==# 'minimap'
execute 'wincmd p'
endif
let bufnr = bufnr('%')
let fname = fnamemodify(bufname('%'), ':p')
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr == -1
return
endif
call s:get_window_info()
if a:force || !has_key(s:minimap_cache, bufnr) ||
\ s:minimap_cache[bufnr].mtime != getftime(fname)
call s:generate_minimap(mmwinnr, bufnr, fname, &filetype)
endif
call s:render_minimap(mmwinnr, bufnr, fname, &filetype)
endfunction
function! s:generate_minimap(mmwinnr, bufnr, fname, ftype) abort
if !exists('s:win_info') || s:win_info == {}
call s:get_window_info()
endif
" No file will result in an empty win_info, bail out early
if s:win_info == {}
return
endif
let hscale = string(2.0 * g:minimap_width / s:win_info['working_width'])
let vscale = string(4.0 * winheight(s:win_info['mmwinid']) / line('$'))
" Users that have custom shells and shell flags may face problems.
let usershell = &shell
let userflag = &shellcmdflag
let &shell = s:default_shell
let &shellcmdflag = s:default_shellflag
let minimap_cmd = '"'.s:minimap_gen.'"'
if has('nvim')
let minimap_cmd = 'w !'.minimap_cmd.' '.hscale.' '.vscale.' '.g:minimap_width
" echom minimap_cmd
let minimap_output = execute(minimap_cmd) " Not work for vim 8.2 ?
else
let minimap_cmd = minimap_cmd.' '.hscale.' '.vscale.' '.g:minimap_width
" echom minimap_cmd
let minimap_output = system(minimap_cmd, join(getline(1, '$'), "\n"))
endif
" Recover the user's selected shell and flag.
let &shell = usershell
let &shellcmdflag = userflag
if v:shell_error
" print error message if file exists
if filereadable(expand('%'))
let msg = 'minimap: could not generate minimap for ' . a:fname
call s:print_warning_msg(msg)
if !empty(minimap_output)
call s:print_warning_msg(minimap_output)
endif
endif
return
endif
let cache = {}
let cache.mtime = getftime(a:fname)
let cache.content = minimap_output
let s:minimap_cache[a:bufnr] = cache
endfunction
function! s:print_warning_msg(msg) abort
echohl WarningMsg
echomsg a:msg
echohl None
endfunction
function! s:render_minimap(mmwinnr, bufnr, fname, ftype) abort
if !has_key(s:minimap_cache, a:bufnr)
return
endif
let curwinview = winsaveview()
execute a:mmwinnr . 'wincmd w'
setlocal modifiable
let cache = s:minimap_cache[a:bufnr]
silent 1,$delete _
silent put =cache.content
if has('nvim')
silent 1,3delete _
else
silent 1delete _
endif
if g:minimap_base_highlight !=# 'Normal'
silent! call matchdelete(g:minimap_base_matchid)
call matchadd(g:minimap_base_highlight, '.*', 10, g:minimap_base_matchid)
endif
setlocal nomodifiable
execute 'wincmd p'
call winrestview(curwinview)
endfunction
function! s:source_move() abort
if !exists('s:win_info["mmwinid"]') || win_getid() == s:win_info['mmwinid']
return
endif
let curr = line('.') - 1
let pos = s:buffer_to_map(curr, s:win_info['height'], s:win_info['mm_height'])
if s:last_pos != pos
let this_table = s:make_state_table_with_position(pos)
call s:render_highlight_table(this_table)
let s:last_pos = pos
endif
endfunction
" Only called if g:minimap_highlight_range is set.
function! s:source_win_scroll() abort
if !exists('s:win_info["mmwinid"]') || win_getid() == s:win_info['mmwinid']
return
endif
let range = s:get_highlight_range(s:win_info)
if s:last_range['pos1'] == range['pos1'] && s:last_range['pos2'] == range['pos2']
" Range is the same, no need to update anything
return
endif
let this_table = s:make_state_table_with_range(range)
call s:render_highlight_table(this_table)
let s:last_range = range
endfunction
" Pos is the new minimap line we are on
function! s:make_state_table_with_position(pos) abort
let this_table = {}
" Clear cursor state for last pos
let current_info = get(g:minimap_line_state_table, s:last_pos, {})
let current_state = get(current_info, 'state')
let this_table[s:last_pos] = {'state': and(current_state, invert(s:STATE_CURSOR)) }
" Set cursor state for new pos
let current_info = get(g:minimap_line_state_table, a:pos, {})
let current_state = get(current_info, 'state')
let this_table[a:pos] = {'state': or(current_state, s:STATE_CURSOR) }
return this_table
endfunction
" Build the update map - only includes lines that have changed
" Optional parameter is a table to integrate into the one we are building.
" Intended for use with states where both can exist without causing an issue
" (eg. CURSOR and RANGE states).
function! s:make_state_table_with_range(range,...) abort
let this_table = {}
" Only do items outside the last range
" (everything else is the same, so don't waste time updating it)
" Clear out the range state
for mm_line_num in range(s:last_range['pos1'], s:last_range['pos2'])
if mm_line_num < a:range['pos1'] || mm_line_num > a:range['pos2']
if a:0 >= 1 && has_key(a:1, mm_line_num)
let current_state = a:1[mm_line_num]['state']
else
let current_info = get(g:minimap_line_state_table, mm_line_num, {})
let current_state = get(current_info, 'state')
endif
let this_table[mm_line_num] = {'state': and(current_state, invert(s:STATE_WINDOW_RANGE)) }
endif
endfor
" Separate for loops, to account for the case when jumping around file
" would result in processing a bunch of lines that we never touched
" Add the range state
for mm_line_num in range(a:range['pos1'], a:range['pos2'])
if mm_line_num < s:last_range['pos1'] || mm_line_num > s:last_range['pos2']
if a:0 >= 1 && has_key(a:1, mm_line_num)
let current_state = a:1[mm_line_num]['state']
else
let current_info = get(g:minimap_line_state_table, mm_line_num, {})
let current_state = get(current_info, 'state')
endif
let this_table[mm_line_num] = {'state': or(current_state, s:STATE_WINDOW_RANGE) }
endif
endfor
" Merge items from the passed in table if it wasn't addressed in the loop above.
if a:0 >= 1
for mm_line_num in keys(a:1)
if !has_key(this_table, mm_line_num)
let this_table[mm_line_num] = { 'state': a:1[mm_line_num]['state'] }
endif
endfor
endif
return this_table
endfunction
function! s:get_highlight_range(win_info) abort
let startln = line('w0') - 1
let endln = line('w$') - 1
let pos1 = s:buffer_to_map(startln, a:win_info['height'], a:win_info['mm_height'])
let pos2 = s:buffer_to_map(endln, a:win_info['height'], a:win_info['mm_height'])
return { 'pos1': pos1, 'pos2': pos2 }
endfunction
" We expect to get 2 things from the background worker, separated by newlines:
" 1) The filename that was checked
" 2) The number of characters in the longest line (currently expecting ascii
" characters, multibyte characters haven't seen any testing)
"
" This event function collects the output we have gotten from the worker.
" Information may arrive all chunked up, so the collection step is required,
" see: https://neovim.io/doc/user/job_control.html
" After the program exits, we set the cached length. Following symlinks will
" result in multiple file names being checked, even though they are the same
" file. (aside: That may have been part of the reason the original 'get the
" length inline' strategy was so slow.)
" Once we have the cached length set, we do a hard refresh of the minimap,
" which will now find a cached length value and won't spawn another job for
" this file.
" All of this results in the minimap 'popping in', but the tradeoff is we are
" no longer blocking the file open to scan for the longest line.
let s:chunks = ['']
function! s:background_worker_event(job_id, data, event) dict abort
if a:event ==? 'stdout'
" echom 'received callback stdout: '.join(a:data)
let s:chunks[-1] .= a:data[0]
call extend(s:chunks, a:data[1:])
elseif a:event ==? 'stderr'
" echom 'received callback stderr: '.join(a:data)
elseif a:event ==? 'exit'
" Have the data, parse then call the set function
" echom 'received callback exit '.join(s:chunks)
" Parse each line into the number of lines + filename. wc -L gives
" <length of longest line> <filename>
for chunk in s:chunks
" echom 'doing ' . len(s:chunks) . '(' . chunk . ')'
if len(chunk) == 0
continue
endif
let splits = split(chunk, ' ')
let longest = splits[0]
let file_name = join(splits[1:], ' ')
" echom 'adding [' . file_name . '] val [' . str2nr(longest) .']'
let s:len_cache[file_name] = str2nr(longest)
endfor
let g:minimap_getting_window_info = 0
call s:refresh_minimap(1)
call s:update_highlight()
endif
endfunction
let s:callbacks = {
\ 'on_stdout': function('s:background_worker_event'),
\ 'on_stderr': function('s:background_worker_event'),
\ 'on_exit': function('s:background_worker_event')
\ }
function! s:get_window_info() abort
" This function moves to the minimap to get info, which will trigger
" autocmds set up to catch switching windows. Protect with a mutex so it
" only runs one time.
if g:minimap_getting_window_info == 0
let g:minimap_getting_window_info = 1
let mmwinnr = bufwinnr('-MINIMAP-')
if mmwinnr == -1
let g:minimap_getting_window_info = 0
return {}
endif
if winnr() == mmwinnr
let g:minimap_getting_window_info = 0
return {}
endif
let filename = expand('%')
" echom 'checking filename [' . filename .']'
if filename == ''
let g:minimap_getting_window_info = 0
return {}
endif
let curwinview = winsaveview()
let mmwinid = win_getid(mmwinnr)
let height = line('$')
let max_width = 0
" Get the max width of this buffer. Value is cached so this only runs
" on first open of any file. This means significant changes to the
" file may result in an inaccurate minimap, but the tradeoff is worth
" it. This cache only lasts for the life of this vim instance, so it
" will be updated with each new open.
if has_key(s:len_cache, filename)
let max_width = s:len_cache[filename]
elseif g:minimap_background_processing == 0
let line_num = 1
while line_num <= line('$')
call setpos('.', [0, line_num, 1])
" Move cursor to the last non-blank character on the line
normal! g_
let max_width = max([max_width, col('.')])
let line_num = line_num + 1
endwhile
let s:len_cache[filename] = max_width
else
" Make sure the file exists
if filereadable(filename)
" Spawn a job to get the longest line
let longest_line_cmd = s:get_longest_line_cmd()
let s:job_id = jobstart([longest_line_cmd, '-L', filename], s:callbacks)
else
" If not, don't spawn a job, it will freak out. We know
" there's nothing there, so let 0 ride.
endif
endif
" Let users override the max width. By default, this does nothing.
let working_width = min([max_width, g:minimap_window_width_override_for_scaling])
" Protect against divide by 0 or negative hscale - working_width must be > 0
let working_width = max([working_width, 1])
" Go to the minimap
call win_gotoid(mmwinid)
let mm_height = line('w$')
let mm_max_width = 0
if g:minimap_highlight_search
" Get max width of the minimap (characters, not window)
let line_num = 1
while line_num <= line('$')
call setpos('.', [0, line_num, 1])
" Move cursor to the last non-blank character on the line
normal! g_
let mm_max_width = max([mm_max_width, col('.')])
let line_num = line_num + 1
endwhile
" Scale to cursor positions, not bytes
let mm_max_width = (mm_max_width / 3) + 1
" The window can be smaller than the max width, so rail to the smaller
" value.
let mm_max_width = min([mm_max_width, g:minimap_width])
" echom 'max_width, mm_max_width: ' . join([max_width, mm_max_width])
endif
" Go back to previous window and reset the view
execute 'wincmd p'
call winrestview(curwinview)
if has_key(s:len_cache, filename)
let g:minimap_getting_window_info = 0
endif
let s:win_info = { 'mmwinid': mmwinid,
\ 'height': height, 'mm_height': mm_height,
\ 'working_width': working_width, 'mm_max_width': mm_max_width }
endif
endfunction
" botline is broken and this works. However, it's slow, so we call this function less.
" Remove this function when `getwininfo().botline` is fixed. <- still relevent?
" This function builds a new line state table from scratch, clearing out the
" old one.
function! s:update_highlight(...) abort
if s:win_info == {}
return
endif
" For unit tests. Very little overhead so not gating it
let g:minimap_run_update_highlight_count = g:minimap_run_update_highlight_count + 1
" Search does its own sub-line highlighting
if g:minimap_highlight_search
let his_idx = 2
if a:0 > 0 && a:1 ==? 'source_buffer_enter_handler'
let his_idx = 1
endif
call s:minimap_color_search(s:win_info, his_idx)
endif
" Clear all table highlights (search handles its own)
for key in keys(g:minimap_line_state_table)
silent! call matchdelete(g:minimap_line_state_table[key]['id'], s:win_info['mmwinid'])
endfor
" Build the global highlight state table
let g:minimap_line_state_table = {}
" Cursor
let curr = line('.') - 1
let pos = s:buffer_to_map(curr, s:win_info['height'], s:win_info['mm_height'])
let current_info = get(g:minimap_line_state_table, pos, {})
let current_state = get(current_info, 'state')
let g:minimap_line_state_table[pos] = { 'state': or(current_state, s:STATE_CURSOR) }
let s:last_pos = pos
" Range
if g:minimap_highlight_range
let pos_range = s:get_highlight_range(s:win_info)
for mm_line_number in range(pos_range['pos1'], pos_range['pos2'])
let current_info = get(g:minimap_line_state_table, mm_line_number, {})
let current_state = get(current_info, 'state')
let g:minimap_line_state_table[mm_line_number] = {'state': or(current_state, s:STATE_WINDOW_RANGE) }
endfor
let s:last_range = pos_range
endif
if g:minimap_git_colors
call s:minimap_color_git(s:win_info)
endif
" Render the state map
call s:render_highlight_table(g:minimap_line_state_table)
endfunction
function! s:render_highlight_table(table) abort
if s:win_info == {}
return
endif
let mmwinid = s:win_info['mmwinid']
" Loop over all entries of the passed in table
for [mm_line_number, info] in items(a:table)
" Need to clear out the previous highlight here - if a highlight already
" exists (ex. cursor -> range), we will lose the ID of the old match and
" won't be able to delete it
if exists("g:minimap_line_state_table[mm_line_number]['id']")
silent! call matchdelete(g:minimap_line_state_table[mm_line_number]['id'], mmwinid)
endif
" Request to remove from the table.
if info['state'] == 0
silent! call matchdelete(g:minimap_line_state_table[mm_line_number]['id'], mmwinid)
silent! unlet! g:minimap_line_state_table[mm_line_number]
continue
endif
"
" Bit mask to check states - Order matters for priority
"
" Cursor + Diff
if and(info['state'], or(s:STATE_CURSOR, s:STATE_DIFF_RM)) == or(s:STATE_CURSOR, s:STATE_DIFF_RM)
let line_color = g:minimap_cursor_diffremove_color
elseif and(info['state'], or(s:STATE_CURSOR, s:STATE_DIFF_ADD)) == or(s:STATE_CURSOR, s:STATE_DIFF_ADD)
let line_color = g:minimap_cursor_diffadd_color
elseif and(info['state'], or(s:STATE_CURSOR, s:STATE_DIFF_MOD)) == or(s:STATE_CURSOR, s:STATE_DIFF_MOD)
let line_color = g:minimap_cursor_diff_color
" Range + Diff
elseif and(info['state'], or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_RM)) == or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_RM)
let line_color = g:minimap_range_diffremove_color
elseif and(info['state'], or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_ADD)) == or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_ADD)
let line_color = g:minimap_range_diffadd_color
elseif and(info['state'], or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_MOD)) == or(s:STATE_WINDOW_RANGE, s:STATE_DIFF_MOD)
let line_color = g:minimap_range_diff_color
" Diff
elseif and(info['state'], s:STATE_DIFF_RM) == s:STATE_DIFF_RM
let line_color = g:minimap_diffremove_color
elseif and(info['state'], s:STATE_DIFF_ADD) == s:STATE_DIFF_ADD
let line_color = g:minimap_diffadd_color
elseif and(info['state'], s:STATE_DIFF_MOD) == s:STATE_DIFF_MOD
let line_color = g:minimap_diff_color
" Cursor
elseif and(info['state'], s:STATE_CURSOR) == s:STATE_CURSOR
let line_color = g:minimap_cursor_color
" Range
elseif and(info['state'], s:STATE_WINDOW_RANGE) == s:STATE_WINDOW_RANGE
let line_color = g:minimap_range_color
" Catcher
else
" Error, everything should be accounted for above
echom 'Error rendering highlights, missing state catcher: ' . info['state']
continue
endif
let id = matchaddpos(line_color, [str2nr(mm_line_number)], g:minimap_cursor_color_priority, -1, { 'window': mmwinid })
let g:minimap_line_state_table[mm_line_number] = { 'state': info['state'], 'id': id }
endfor
endfunction
" Translates a position in a buffer to its respective position in the map.
function! minimap#vim#BufferToMap(lnnum, buftotal, mmtotal) abort
return s:buffer_to_map(a:lnnum, a:buftotal, a:mmtotal)
endfunction
function! s:buffer_to_map(lnnum, buftotal, mmtotal) abort
return float2nr(1.0 * a:lnnum / a:buftotal * a:mmtotal) + 1
endfunction
" Clears the specified match id list
function! s:clear_id_list_colors(mmwinid, id_list) abort
for id in a:id_list
silent! call matchdelete(id, a:mmwinid) " require vim 8.1.1084+ or neovim 0.5.0+
endfor
endfunction
" Clears matches of current window only.
function! s:clear_highlights() abort
silent! call clearmatches(s:win_info['mmwinid'])
let g:minimap_search_id_list = []
endfunction
function! s:minimap_move() abort
if s:win_info == {}
call s:get_window_info()
if s:win_info == {}
echom 'Could not populate s:win_info, exiting'
endif
endif
let curr = line('.')
execute 'wincmd p'
" Position cursor at the top line of this mm block
let pos = float2nr(1.0 * (curr-1) / s:win_info['mm_height'] * s:win_info['height']) + 2
execute pos
if g:minimap_highlight_range
let range = s:get_highlight_range(s:win_info)
endif
execute 'wincmd p'
let this_table = s:make_state_table_with_position(curr)
if g:minimap_highlight_range
let this_table = s:make_state_table_with_range(range, this_table)
endif
call s:render_highlight_table(this_table)
let s:last_pos = curr
if g:minimap_highlight_range
let s:last_range = range
endif
endfunction
function! s:minimap_win_enter() abort
" do nothing
endfunction
function! s:source_win_enter() abort
call s:get_window_info()
call s:update_highlight()
endfunction
function! s:minimap_buffer_enter_handler() abort
if empty(s:last_pos) || s:last_pos <= 0
return
endif
" Move the cursor to where we were in the main buffer. Without this it
" jumps to the top of the minimap
call cursor(s:last_pos, 1)
endfunction
function! s:source_buffer_enter_handler() abort
silent! call clearmatches(s:win_info['mmwinid'])
call s:refresh_minimap(0)
call s:update_highlight('source_buffer_enter_handler')
endfunction
function! s:minimap_diffoff() abort
if s:win_info == {}
return
endif
let mmwinid = s:win_info['mmwinid']
silent! call win_execute(mmwinid, 'diffoff')
endfunction
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Git Stuff
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! minimap#vim#MinimapParseGitDiffLine(line, buffer_lines, mm_height) abort
return s:minimap_parse_git_diff_line(a:line, a:buffer_lines, a:mm_height)
endfunction
function! s:minimap_parse_git_diff_line(line, buffer_lines, mm_height) abort
let this_diff = {}
let blobs = split(a:line, ' ')
let del_info = blobs[1]
let add_info = blobs[2]
" Parse newfile info
let add_info = split(add_info, ',')
let add_start = str2nr(add_info[0])
let add_len = 1
if len(add_info) > 1
let add_len = abs(str2nr(add_info[1]))
endif
" Parse oldfile info
let del_info = split(del_info, ',')
let del_len = 1
if len(del_info) > 1
let del_len = abs(str2nr(del_info[1]))
endif
" Get diff type + end line
let this_diff['start'] = add_start
let this_diff['end'] = this_diff['start'] + add_len
if add_len != 0 && del_len != 0
let this_diff['color'] = g:minimap_diff_color
elseif add_len != 0
let this_diff['color'] = g:minimap_diffadd_color
elseif del_len != 0
let this_diff['color'] = g:minimap_diffremove_color
let this_diff['end'] = this_diff['start']
else
let this_diff['color'] = g:minimap_diff_color
let this_diff['end'] = this_diff['start']
endif
" Map locations to minimap
" echom 'buf: ' . join([this_diff['start'], this_diff['end']])
let this_diff['start'] = s:buffer_to_map(this_diff['start'] - 1, a:buffer_lines, a:mm_height)
let this_diff['end'] = s:buffer_to_map(this_diff['end'] - 1, a:buffer_lines, a:mm_height)
" echom 'mm : ' . join([this_diff['start'], this_diff['end']])
return this_diff
endfunction
function! s:minimap_color_git(win_info) abort
" Get git info
let git_call = 'git diff -U0 -- ' . expand('%')
let git_diff = substitute(system(git_call), '\n\+&', '', '') | silent echo strtrans(git_diff)
let lines = split(git_diff, '\n')
let diff_list = []
for line in lines
if line[0] ==? '@'
let this_diff = s:minimap_parse_git_diff_line(line,
\ a:win_info['height'], a:win_info['mm_height'])
" Add to list
let diff_list = add(diff_list, this_diff)
endif
endfor
" Color lines, creating a new id for each section
for a_diff in diff_list
let idx = a_diff['start']
while idx <= a_diff['end']
" Override the other diff states
let current_info = get(g:minimap_line_state_table, idx, {})
let current_state = and(get(current_info, 'state'), invert(or(s:STATE_DIFF_RM, or(s:STATE_DIFF_ADD, s:STATE_DIFF_MOD))))
let g:minimap_line_state_table[idx] = { 'state': or(current_state, s:get_diff_state_flag(a_diff['color'])) }
let idx = idx+1
endwhile
endfor
endfunction
function! s:get_diff_state_flag(state) abort
if a:state == g:minimap_diffremove_color
return s:STATE_DIFF_RM
elseif a:state == g:minimap_diffadd_color
return s:STATE_DIFF_ADD
elseif a:state == g:minimap_diff_color
return s:STATE_DIFF_MOD
endif
return 0xFFFF
endfunction
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Search Highlight Stuff
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! minimap#vim#ClearColorSearch() abort
if exists('g:minimap_search_id_list')
if s:win_info != {}
call s:clear_id_list_colors(s:win_info['mmwinid'], g:minimap_search_id_list)
let g:minimap_search_id_list = []
endif
endif
endfunction
function! minimap#vim#UpdateColorSearch(query) abort
call s:minimap_update_color_search(a:query)
endfunction
function! s:minimap_update_color_search(query) abort
if s:win_info != {} && win_getid() != s:win_info['mmwinid']
call s:minimap_color_search(s:win_info, a:query)
endif
endfunction
" Hook function for unit tests
function! minimap#vim#MinimapColorSearchGetSpans(win_info, query) abort
return s:minimap_color_search_get_spans(a:win_info, a:query)
endfunction
" Query argument is either the query string, or a number representing how far
" back into the search history we need to grab (it varies by context)
function! s:minimap_color_search_get_spans(win_info, query) abort
" Get the last search the user searched for
if type(a:query) != type(0)
let last_search = a:query
else
let tmp = split(execute('his /'), '\n')
let tmp = split(tmp[len(tmp)-a:query], '', 1)
" echom 'tmp: ' . join(tmp)
let last_search = join(tmp[2:-1])
endif
" echom 'last_search: ' . last_search
" Save the current view so we can return to it after searching
let curwinview = winsaveview()
" Start at top, save all match positions until we hit the bottom
call cursor(1, 1)
" The 'c' in this string lets the search match at the current cursor
" position. We need this for the first search to catch the very first
" character in the file, but we need to exclude it from all future
" searches, since the cursor will be moved to the start of each match.
let search_options_string = 'czW'
" Loop and get locations of all matches
let locations = []
let done = 0
while done == 0
let start_location = searchpos(last_search, search_options_string)
if start_location != [0, 0]
call searchpos(last_search, 'cezW')
let end_cursor = getpos('.')
let end_location = [end_cursor[1], end_cursor[2]]
if start_location[0] != end_location[0]
" Not equipped to handle matches that span more than one line
" yet, just skip it for now.
continue
endif
if end_location[1] < start_location[1]
" Error - end not farther than start. Abort.
echom 'Error looking for matches: end_location: [' . end_location[1] . '] is less than start_location: [' . start_location[1] . ']. Aborting'
break
endif
let match_len = end_location[1] - start_location[1]
let this_location = {}
let this_location['line'] = start_location[0]
let this_location['col'] = start_location[1]
let this_location['match_len'] = match_len
call add(locations, this_location)
let this_location = {}
let search_options_string = 'zW'
else
let done = 1
endif
endwhile
" Restore window view
call winrestview(curwinview)
" Convert all positions to mm
let mm_spans = []
for this_location in locations
let mm_line = s:buffer_to_map(this_location['line'] - 1, a:win_info['height'], a:win_info['mm_height'])
" Braille takes 3 bytes when using UTF-8. Column position is specified
" in number of bytes offset, so to calculate horizontal position to
" pass to the highlighting function, we need to multiply by 3
let mm_col = 3 * (s:buffer_to_map(this_location['col'] - 1, a:win_info['working_width'], a:win_info['mm_max_width']))
let mm_len = 3 * (s:buffer_to_map(this_location['match_len'] - 1, a:win_info['working_width'], a:win_info['mm_max_width']))
" If we don't land directly on an integer value of ([byte length]x + 1),
" the highlight will not show up. Make sure the values land in those
" bins. Above scaling gives 3 as a minimum. We take off any
" remainder, then bump it down to the leftmost column (which is
" offset by 1, hence the -2)
let mm_col = (mm_col - (mm_col % 3)) - 2
" echom 'buf: ' . join([this_location['line'], this_location['col'], this_location['match_len']])
" echom 'mm : ' . join([mm_line, mm_col, mm_len])
call add(mm_spans, [mm_line, mm_col, mm_len])
endfor
return mm_spans
endfunction
function! s:minimap_color_search(win_info, query) abort
if eval('v:hlsearch') == 0 || eval('&hlsearch') == 0
" Don't bother doing anything if any search highlighting is turned off
return
endif
let mm_spans = s:minimap_color_search_get_spans(a:win_info, a:query)
" Clear old colors before writing new ones
call s:clear_id_list_colors(a:win_info['mmwinid'], g:minimap_search_id_list)
let g:minimap_search_id_list = []
" Color lines, creating a new id for each group
for a_span in mm_spans
" span_list item: [line_number, column_number, length]
call add(g:minimap_search_id_list, s:get_next_search_matchid())
call s:set_span_color(g:minimap_search_color, [a_span],
\ g:minimap_search_color_priority, g:minimap_search_id_list[-1], a:win_info['mmwinid'])
endfor
endfunction
function! s:set_span_color(set_color, spans, priority, match_id, mmwinid) abort
call matchaddpos(a:set_color, a:spans, a:priority,
\ a:match_id, { 'window': a:mmwinid })
endfunction
function! s:get_next_search_matchid() abort
return g:minimap_search_matchid_safe_range + len(g:minimap_search_id_list)
endfunction
|