-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathscrollview.lua
More file actions
3983 lines (3806 loc) · 148 KB
/
Copy pathscrollview.lua
File metadata and controls
3983 lines (3806 loc) · 148 KB
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
local api = vim.api
local fn = vim.fn
-- vim.tbl_islist was deprecated in Neovim v0.10. #131
local islist = vim.islist or vim.tbl_islist
local utils = require('scrollview.utils')
local binary_search = utils.binary_search
local concat = utils.concat
local copy = utils.copy
local echo = utils.echo
local extend = utils.extend
local mapify = utils.mapify
local preceding = utils.preceding
local remove_duplicates = utils.remove_duplicates
local round = utils.round
local sorted = utils.sorted
local subsequent = utils.subsequent
local t = utils.t
local tbl_get = utils.tbl_get
local to_bool = utils.to_bool
-- WARN: Sometimes 1-indexing is used (primarily for mutual Vim/Neovim API
-- calls) and sometimes 0-indexing (primarily for Neovim-specific API calls).
-- WARN: Don't move the cursor or change the current window. It can have
-- unwanted side effects (e.g., #18, #23, #43, window sizes changing to satisfy
-- winheight/winwidth, etc.).
-- WARN: Functionality that temporarily moves the cursor, or changes the 'wrap'
-- setting should use a window workspace to prevent unwanted side effects. More
-- details are in the documentation for with_win_workspace.
-- XXX: Some of the functionality is applicable to bars and signs, but is
-- described in comments as if it were only applicable to bars (since it was
-- written prior to sign support).
-- *************************************************
-- * Forward Declarations
-- *************************************************
-- Declared here since it's used by the earlier legend() function.
local get_sign_groups
-- Declared here since it's used by the earlier refresh_impl() function.
local is_sign_group_active
-- *************************************************
-- * Globals
-- *************************************************
-- Since there is no text displayed in the buffers, the same buffers are used
-- for multiple windows. This also prevents the buffer list from getting high
-- from usage of the plugin.
-- bar_bufnr has the bufnr of the buffer created for a position bar.
local bar_bufnr = -1
-- sign_bufnr has the bufnr of the buffer created for signs.
local sign_bufnr = -1
local popup_bufnr = -1
-- Keep count of pending async refreshes.
local pending_async_refresh_count = 0
-- Keep count of pending mousemove callbacks.
local pending_mousemove_callback_count = 0
-- Tracks whether the handle_mouse function is running.
local handling_mouse = false
-- A window variable is set on each scrollview window, as a way to check for
-- scrollview windows, in addition to matching the scrollview buffer number
-- saved in bar_bufnr. This was preferable versus maintaining a list of window
-- IDs.
local WIN_VAR = 'scrollview_key'
local WIN_VAL = 'scrollview_val'
-- For win workspaces, a window variable is used to store the base window ID.
local WIN_WORKSPACE_BASE_WINID_VAR = 'scrollview_win_workspace_base_winid'
-- Maps window IDs to a corresponding window workspace.
local win_workspace_lookup = {}
-- A type field is used to indicate the type of scrollview windows.
local BAR_TYPE = 0
local SIGN_TYPE = 1
-- A key for saving scrollbar properties using a window variable.
local PROPS_VAR = 'scrollview_props'
-- Maps sign groups to state (enabled or disabled).
local sign_group_state = {}
-- Maps sign groups to refresh callbacks.
local sign_group_callbacks = {}
-- Stores registered sign specifications.
-- WARN: This may seem array-like, but since items can be set to nil by
-- deregister_sign_spec(), it's dictionary-like (use pairs(), not ipairs()).
local sign_specs = {}
-- Keep track of how many sign specifications were registered. This is used for
-- ID assignment, and is not adjusted for deregistrations.
local sign_spec_counter = 0
-- Track whether there has been a <mousemove> occurrence. Hover highlights are
-- only used if this has been set to true. Without this, the bar would be
-- highlighted when being dragged even if the client doesn't support
-- <mousemove> (e.g., nvim-qt), and may retain the wrong highlight after
-- dragging completes if the mouse is still over the bar.
-- WARN: It's possible that Neovim is opened, with the mouse exactly where it
-- needs to be for a user to start dragging without first moving the mouse. In
-- that case, hover highlights should be used, but won't be. This scenario is
-- unlikely.
local mousemove_received = false
local CTRLS = t('<c-s>')
local CTRLV = t('<c-v>')
local MOUSEMOVE = t('<mousemove>')
local SIMPLE_MODE = 0 -- doesn't consider folds nor wrapped lines
local VIRTUAL_MODE = 1 -- considers folds, but not wrapped lines
local PROPER_MODE = 2 -- considers folds and wrapped lines
-- Memoization key prefixes.
local VIRTUAL_LINE_COUNT_KEY_PREFIX = 0
local PROPER_LINE_COUNT_KEY_PREFIX = 1
local TOPLINE_LOOKUP_KEY_PREFIX = 2
local GET_WINDOW_EDGES_KEY_PREFIX = 3
local ROW_LENGTH_LOOKUP_KEY_PREFIX = 4
local GET_NON_SCROLLVIEW_FLOATS_KEY_PREFIX = 5
-- Maps window ID and highlight group to a temporary highlight group with the
-- corresponding definition. This is reset on each refresh cycle.
local highlight_lookup = {}
-- Tracks the number of entries in the preceding table.
local highlight_lookup_size = 0
-- The indices for the array of border elements in the config returned by
-- nvim_win_get_config. The array specifies the eight characters that comprise
-- the border, in a clockwise order starting from the top-left corner.
local BORDER_TOP = 2
local BORDER_RIGHT = 4
local BORDER_BOTTOM = 6
local BORDER_LEFT = 8
-- Maps mouse buttons (e.g., 'left') to the Neovim key representation.
local MOUSE_LOOKUP = (function()
local valid_buttons = {
'left', 'middle', 'right', 'x1', 'x2',
'c-left', 'c-middle', 'c-right', 'c-x1', 'c-x2',
'm-left', 'm-middle', 'm-right', 'm-x1', 'm-x2',
}
local result = {}
for _, button in ipairs(valid_buttons) do
result[button] = t('<' .. button .. 'mouse>')
end
return result
end)()
-- Fake window IDs are used by read_input_stream for representing the command
-- line and tabline. These are negative so they can be distinguished from
-- valid IDs for actual windows.
local COMMAND_LINE_WINID = -1
local TABLINE_WINID = -2
-- *************************************************
-- * Memoization
-- *************************************************
local cache = {}
local memoize = false
local start_memoize = function()
memoize = true
end
local stop_memoize = function()
memoize = false
end
local reset_memoize = function()
cache = {}
end
-- *************************************************
-- * Key Sequence Callbacks
-- *************************************************
-- The max length of all key sequences that have been registered.
local max_key_sequence_length = 0
-- A buffer of recent key sequences, whose size does not exceed
-- max_key_sequence_length.
local active_key_sequence = ''
local last_raw_mode = nil
-- Maps a mode concatenated with a key sequence to a callback.
local key_sequence_callbacks = {}
-- WARN: The modes here do not exactly match mode(), where there is no 'o'
-- mode. Also, these modes do not exactly match the mapping modes (:h
-- map-overview), where 'v' would correspond to both visual and select modes,
-- and 'x' would be for visual mode only.
-- o: Operator-pending
-- n: Normal
-- v: Visual
-- s: Select
-- i: Insert
-- R: Replace
-- c: Command-line editing or Vim Ex mode
-- r: Prompt
-- !: Shell or external command
-- t: Terminal
local KNOWN_MODES = {'o', 'n', 'v', 's', 'i', 'R', 'c', 'r', '!', 't'}
vim.on_key(function(str)
-- Use pcall to avoid an error in some cases for nvim<0.8 (Neovim #17273).
pcall(function()
local raw_mode = fn.mode(1)
if raw_mode == last_raw_mode then
active_key_sequence = active_key_sequence .. str
else
-- Reset the active key sequence when the mode changes.
active_key_sequence = str
end
last_raw_mode = raw_mode
active_key_sequence = string.sub(
active_key_sequence, -max_key_sequence_length, -1)
local mode
if vim.startswith(raw_mode, 'no') then
mode = 'o'
else
mode = string.lower(string.sub(raw_mode, 1, 1))
if mode == CTRLV then
mode = 'v'
elseif mode == CTRLS then
mode = 's'
end
end
local known_mode = false
for _, x in ipairs(KNOWN_MODES) do
if mode == x then
known_mode = true
break
end
end
if not known_mode then
mode = nil
end
if mode ~= nil then
for start_idx = -1, -#active_key_sequence, -1 do
local subseq = string.sub(active_key_sequence, start_idx, -1)
local key = mode .. subseq
local callback = key_sequence_callbacks[key]
if callback ~= nil then
callback()
end
end
end
end)
end)
-- Register a sequence of keys, for which the callback will be executed when
-- those keys are pressed under the specified modes. There is a comment above
-- on the supported modes.
local register_key_sequence_callback = function(seq, modes, callback)
for idx = 1, #modes do
local mode = string.sub(modes, idx, idx)
local known_mode = false
for _, x in ipairs(KNOWN_MODES) do
if mode == x then
known_mode = true
break
end
end
if not known_mode then
error('Unknown mode: ' .. mode)
end
local key = mode .. seq
key_sequence_callbacks[key] = callback
max_key_sequence_length = math.max(max_key_sequence_length, #seq)
end
end
-- *************************************************
-- * Core
-- *************************************************
-- Return window height, subtracting 1 if there is a winbar.
local get_window_height = function(winid)
if winid == 0 then
winid = api.nvim_get_current_win()
end
local height = api.nvim_win_get_height(winid)
if to_bool(tbl_get(fn.getwininfo(winid)[1], 'winbar', 0)) then
height = height - 1
end
return height
end
-- Returns the position of window edges, with borders considered part of the
-- window.
local get_window_edges = function(winid)
local memoize_key = table.concat({GET_WINDOW_EDGES_KEY_PREFIX, winid}, ':')
if memoize and cache[memoize_key] then return cache[memoize_key] end
local top, left = unpack(fn.win_screenpos(winid))
local bottom = top + get_window_height(winid) - 1
local right = left + fn.winwidth(winid) - 1
-- Only edges have to be checked to determine if a border is present (i.e.,
-- corners don't have to be checked). Borders don't impact the top and left
-- positions calculated above; only the bottom and right positions.
local border = api.nvim_win_get_config(winid).border
if border ~= nil and islist(border) and #border == 8 then
if border[BORDER_TOP] ~= '' then
bottom = bottom + 1
end
if border[BORDER_RIGHT] ~= '' then
right = right + 1
end
if border[BORDER_BOTTOM] ~= '' then
bottom = bottom + 1
end
if border[BORDER_LEFT] ~= '' then
right = right + 1
end
end
local result = {top, bottom, left, right}
if memoize then cache[memoize_key] = result end
return result
end
-- Returns true for ordinary windows (not floating and not external), and false
-- otherwise.
local is_ordinary_window = function(winid)
local config = api.nvim_win_get_config(winid)
local not_external = not tbl_get(config, 'external', false)
local not_floating = tbl_get(config, 'relative', '') == ''
return not_external and not_floating
end
local is_scrollview_window = function(winid)
if is_ordinary_window(winid) then return false end
local has_attr = fn.getwinvar(winid, WIN_VAR, '') == WIN_VAL
if not has_attr then return false end
local bufnr = api.nvim_win_get_buf(winid)
return bufnr == bar_bufnr or bufnr == sign_bufnr
end
local get_scrollview_windows = function()
local result = {}
for _, winid in ipairs(api.nvim_tabpage_list_wins(0)) do
if is_scrollview_window(winid) then
table.insert(result, winid)
end
end
return result
end
-- Returns the non-workspace floating windows (including scrollview windows).
-- The optional 'hidden' parameter specifies whether hidden floating windows
-- are included (defaults to false).
local get_floating_windows = function(hidden)
if hidden == nil then
hidden = false
end
local result = {}
for _, winid in ipairs(api.nvim_tabpage_list_wins(0)) do
local config = api.nvim_win_get_config(winid)
local floating = tbl_get(config, 'relative', '') ~= ''
local workspace_win =
fn.getwinvar(winid, WIN_WORKSPACE_BASE_WINID_VAR, -1) ~= -1
local is_hidden = tbl_get(config, 'hide', false)
if not workspace_win
and floating
and (hidden or not is_hidden) then
table.insert(result, winid)
end
end
return result
end
-- Returns the non-scrollview floating windows. The optional 'hidden' parameter
-- specifies whether hidden floating windows are included (defaults to false).
local get_non_scrollview_floats = function(hidden)
if hidden == nil then
hidden = false
end
local memoize_key =
table.concat({GET_NON_SCROLLVIEW_FLOATS_KEY_PREFIX, hidden and 1 or 0}, ':')
if memoize and cache[memoize_key] then return cache[memoize_key] end
local result = {}
for _, winid in ipairs(get_floating_windows(hidden)) do
if not is_scrollview_window(winid) then
table.insert(result, winid)
end
end
if memoize then cache[memoize_key] = result end
return result
end
-- Return the candidate windows that overlap the region corresponding to the
-- specified edges.
local get_win_overlaps = function(top, bottom, left, right, candidates)
local result = {}
for _, winid in ipairs(candidates) do
local top2, bottom2, left2, right2 = unpack(get_window_edges(winid))
if top <= bottom2
and bottom >= top2
and left <= right2
and right >= left2 then
table.insert(result, winid)
end
end
return result
end
local is_mouse_over_scrollview_win = function(winid)
-- WARN: We use the positioning from the scrollview props. This is so that
-- clicking when hovering retains the hover highlight for scrollview windows
-- when their parent winnr > 1. Otherwise, it appeared getwininfo,
-- nvim_win_get_posiiton, and win_screenpos were not returning accurate info
-- (may relate to Neovim #24078). Perhaps it's because the windows were just
-- created and not yet in the necessary state. #100
local config = api.nvim_win_get_config(winid)
local mousepos = fn.getmousepos()
local props = api.nvim_win_get_var(winid, PROPS_VAR)
local parent_pos = fn.win_screenpos(props.parent_winid)
local row = props.row + parent_pos[1] - 1
local col = props.col + parent_pos[2] - 1
-- Adjust for floating window borders.
local parent_config = api.nvim_win_get_config(props.parent_winid)
local parent_is_float = tbl_get(parent_config, 'relative', '') ~= ''
if parent_is_float then
local border = parent_config.border
if border ~= nil and islist(border) and #border == 8 then
if border[BORDER_TOP] ~= '' then
row = row + 1
end
if border[BORDER_LEFT] ~= '' then
col = col + 1
end
end
end
-- Adjust for winbar. #117
if to_bool(tbl_get(fn.getwininfo(props.parent_winid)[1], 'winbar', 0)) then
row = row + 1
end
local result = mousepos.screenrow >= row
and mousepos.screenrow < row + props.height
and mousepos.screencol >= col
and mousepos.screencol < col + props.width
if result then
-- Return false if there are any floating windows with higher zindex.
local float_overlaps = get_win_overlaps(
mousepos.screenrow, mousepos.screenrow,
mousepos.screencol, mousepos.screencol,
get_floating_windows()
)
for _, overlap_winid in ipairs(float_overlaps) do
local overlap_config = api.nvim_win_get_config(overlap_winid)
if overlap_winid ~= winid and overlap_config.zindex > config.zindex then
result = false
break
end
end
end
return result
end
-- Set window option.
local set_window_option = function(winid, key, value)
-- Convert to Vim format (e.g., 1 instead of Lua true).
if value == true then
value = 1
elseif value == false then
value = 0
end
-- setwinvar(..., '&...', ...) is used in place of nvim_win_set_option
-- to avoid Neovim Issues #15529 and #15531, where the global window option
-- is set in addition to the window-local option, when using Neovim's API or
-- Lua interface.
fn.setwinvar(winid, '&' .. key, value)
end
-- Return the base window ID for the specified window. Assumes that windows
-- have been properly marked with WIN_WORKSPACE_BASE_WINID_VAR.
local get_base_winid = function(winid)
local base_winid = winid
pcall(function()
-- Loop until reaching a window with no base winid specified.
while true do
base_winid = api.nvim_win_get_var(
base_winid, WIN_WORKSPACE_BASE_WINID_VAR)
end
end)
return base_winid
end
-- Creates a temporary floating window that can be used for computations
-- ---corresponding to the specified window---that require temporary cursor
-- movements (e.g., counting virtual lines, where all lines in a closed fold
-- are counted as a single line). This can be used instead of working in the
-- actual window, to prevent unintended side-effects that arise from moving the
-- cursor in the actual window, even when autocmd's are disabled with
-- eventignore=all and the cursor is restored (e.g., Issue #18: window
-- flickering when resizing with the mouse, Issue #19: cursorbind/scrollbind
-- out-of-sync). This can also be used to prevent unintended side effects when
-- changing the 'wrap' setting temporarily while lines are wrapped (Issue #103).
local with_win_workspace = function(winid, fun)
local workspace_winid = win_workspace_lookup[winid]
if workspace_winid == nil then
-- If winid is already a window workspace, use that. Otherwise, create a
-- new workspace window.
if get_base_winid(winid) ~= winid then
workspace_winid = winid
else
-- Make the target window active, so that its folds are inherited by the
-- created floating window (this is necessary when there are multiple
-- windows that have the same buffer, each window having different
-- folds).
workspace_winid = api.nvim_win_call(winid, function()
local bufnr = api.nvim_win_get_buf(winid)
return api.nvim_open_win(bufnr, false, {
relative = 'editor',
focusable = false,
border = 'none',
width = math.max(1, api.nvim_win_get_width(winid)),
-- The floating window doesn't inherit a winbar. Use the
-- winbar-omitted height where applicable.
height = math.max(1, get_window_height(winid)),
row = 0,
col = 0,
})
end)
win_workspace_lookup[winid] = workspace_winid
-- Disable scrollbind and cursorbind on the workspace window so that diff
-- mode and other functionality that utilizes binding (e.g., :Gdiff,
-- :Gblame) can function properly.
set_window_option(workspace_winid, 'scrollbind', false)
set_window_option(workspace_winid, 'cursorbind', false)
api.nvim_win_set_var(workspace_winid, WIN_WORKSPACE_BASE_WINID_VAR, winid)
-- As a precautionary measure, make sure the floating window has no
-- winbar, which is assumed above.
if to_bool(fn.exists('+winbar')) then
set_window_option(workspace_winid, 'winbar', '')
end
-- Don't include the workspace window in a diff session. If included,
-- closing it could end the diff session (e.g., when there is one other
-- window in the session). Issue #57.
if api.nvim_win_get_option(0, 'foldmethod') == 'diff' then
-- First set foldmethod to manual so that folds are retained.
set_window_option(workspace_winid, 'foldmethod', 'manual')
set_window_option(workspace_winid, 'diff', false)
end
end
end
local success, result = pcall(function()
return api.nvim_win_call(workspace_winid, fun)
end)
if not success then error(result) end
return result
end
local reset_win_workspaces = function()
for _, workspace_winid in pairs(win_workspace_lookup) do
if api.nvim_win_is_valid(workspace_winid) then
api.nvim_win_close(workspace_winid, true)
end
end
win_workspace_lookup = {}
end
local is_visual_mode = function(mode)
return vim.tbl_contains({'v', 'V', t'<c-v>'}, mode)
end
local is_select_mode = function(mode)
return vim.tbl_contains({'s', 'S', t'<c-s>'}, mode)
end
local in_command_line_window = function()
if fn.win_gettype() == 'command' then return true end
if fn.mode() == 'c' then return true end
local bufnr = api.nvim_get_current_buf()
local buftype = api.nvim_buf_get_option(bufnr, 'buftype')
local bufname = fn.bufname(bufnr)
return buftype == 'nofile' and bufname == '[Command Line]'
end
-- Returns the window column where the buffer's text begins. This may be
-- negative due to horizontal scrolling. This may be greater than one due to
-- the sign column and 'number' column.
local buf_text_begins_col = function(winid)
-- Use a window workspace to avoid Issue #103.
return with_win_workspace(winid, function()
-- The calculation assumes lines don't wrap, so 'nowrap' is temporarily set.
local wrap = api.nvim_win_get_option(0, 'wrap')
set_window_option(0, 'wrap', false)
local result = fn.wincol() - fn.virtcol('.') + 1
set_window_option(0, 'wrap', wrap)
return result
end)
end
-- Returns the window column where the view of the buffer begins. This can be
-- greater than one due to the sign column and 'number' column.
local buf_view_begins_col = function(winid)
-- Use a window workspace to avoid Issue #103.
return with_win_workspace(winid, function()
-- The calculation assumes lines don't wrap, so 'nowrap' is temporarily set.
local wrap = api.nvim_win_get_option(0, 'wrap')
set_window_option(0, 'wrap', false)
local result = fn.wincol() - fn.virtcol('.') + fn.winsaveview().leftcol + 1
set_window_option(0, 'wrap', wrap)
return result
end)
end
local get_byte_count = function(winid)
return api.nvim_win_call(winid, function()
return fn.line2byte(fn.line('$') + 1) - 1
end)
end
-- Returns a boolean indicating whether a restricted state should be used.
local is_restricted = function(winid)
local bufnr = api.nvim_win_get_buf(winid)
local line_count = api.nvim_buf_line_count(bufnr)
local line_limit = vim.g.scrollview_line_limit
if line_limit ~= -1 and line_count > line_limit then
return true
end
local byte_count = get_byte_count(winid)
local byte_limit = vim.g.scrollview_byte_limit
if byte_limit ~= -1 and byte_count > byte_limit then
return true
end
return false
end
-- Returns the scrollview mode.
local scrollview_mode = function(winid)
if is_restricted(winid) then
return SIMPLE_MODE
end
local specified_mode = vim.g.scrollview_mode
if specified_mode == 'simple' then
return SIMPLE_MODE
elseif specified_mode == 'virtual' then
return VIRTUAL_MODE
elseif specified_mode == 'proper' then
return PROPER_MODE
elseif specified_mode == 'auto' then
if jit == nil then
-- Proper mode is slower. Only use it when luajit is available.
return VIRTUAL_MODE
end
local bufnr = api.nvim_win_get_buf(winid)
local line_count = api.nvim_buf_line_count(bufnr)
if not api.nvim_win_get_option(winid, 'wrap')
and not to_bool(fn.has('nvim-0.10')) then
-- Proper mode is not necessary when there is no wrapping and nvim<0.10
-- (on nvim>=0.10, diff filler and virtual text lines are also considered).
return VIRTUAL_MODE
end
local winheight = get_window_height(winid)
local threshold_multiple = 5
if line_count <= winheight * threshold_multiple then
return PROPER_MODE
end
end
-- Fallback for when mode is unknown and for auto mode's case where there are
-- relatively many lines.
return VIRTUAL_MODE
end
-- Return top line and bottom line in window. For folds, the top line
-- represents the start of the fold and the bottom line represents the end of
-- the fold.
local line_range = function(winid)
-- WARN: getwininfo(winid)[1].botline is not properly updated for some
-- movements (Neovim Issue #13510), so this is implemented as a workaround.
-- This was originally handled by using an asynchronous context, but this was
-- not possible for refreshing bars during mouse drags.
-- Using scrolloff=0 combined with H and L breaks diff mode. Scrolling is not
-- possible and/or the window scrolls when it shouldn't. Temporarily turning
-- off scrollbind and cursorbind accommodates, but the following is simpler.
return unpack(api.nvim_win_call(winid, function()
local topline = fn.line('w0')
local botline = fn.line('w$')
-- line('w$') returns 0 in silent Ex mode, but line('w0') is always greater
-- than or equal to 1.
botline = math.max(botline, topline)
return {topline, botline}
end))
end
-- Advance the current window cursor to the start of the next virtual span,
-- returning the range of lines jumped over, and a boolean indicating whether
-- that range was in a closed fold. A virtual span is a contiguous range of
-- lines that are either 1) not in a closed fold or 2) in a closed fold. If
-- there is no next virtual span, the cursor is returned to the first line.
local advance_virtual_span = function()
local start = fn.line('.')
local foldclosedend = fn.foldclosedend(start)
if foldclosedend ~= -1 then
-- The cursor started on a closed fold.
if foldclosedend == fn.line('$') then
vim.cmd('keepjumps normal! gg')
else
vim.cmd('keepjumps normal! j')
end
return start, foldclosedend, true
end
local lnum = start
while true do
vim.cmd('keepjumps normal! zj')
if lnum == fn.line('.') then
-- There are no more folds after the cursor. This is the last span.
vim.cmd('keepjumps normal! gg')
return start, fn.line('$'), false
end
lnum = fn.line('.')
local foldclosed = fn.foldclosed(lnum)
if foldclosed ~= -1 then
-- The cursor moved to a closed fold. The preceding line ends the prior
-- virtual span.
return start, lnum - 1, false
end
end
end
-- Returns a boolean indicating whether the count of folds (closed folds count
-- as a single fold) between the specified start and end lines exceeds 'n', in
-- the current window. The cursor may be moved.
local fold_count_exceeds = function(start, end_, n)
vim.cmd('keepjumps normal! ' .. start .. 'G')
if fn.foldclosed(start) ~= -1 then
n = n - 1
end
if n < 0 then
return true
end
-- Navigate down n folds.
if n > 0 then
vim.cmd('keepjumps normal! ' .. n .. 'zj')
end
local line1 = fn.line('.')
-- The fold count exceeds n if there is another fold to navigate to on a line
-- less than end_.
vim.cmd('keepjumps normal! zj')
local line2 = fn.line('.')
return line2 > line1 and line2 <= end_
end
-- Returns the count of virtual lines between the specified start and end lines
-- (both inclusive), in the current window. A closed fold counts as one virtual
-- line. The computation loops over virtual spans. The cursor may be moved.
local virtual_line_count_spanwise = function(start, end_)
start = math.max(1, start)
end_ = math.min(fn.line('$'), end_)
local count = 0
if end_ >= start then
vim.cmd('keepjumps normal! ' .. start .. 'G')
while true do
local range_start, range_end, fold = advance_virtual_span()
range_end = math.min(range_end, end_)
local delta = 1
if not fold then
delta = range_end - range_start + 1
end
count = count + delta
if range_end == end_ or fn.line('.') == 1 then
break
end
end
end
return count
end
-- Returns the count of virtual lines between the specified start and end lines
-- (both inclusive), in the current window. A closed fold counts as one virtual
-- line. The computation loops over lines. The cursor is not moved.
local virtual_line_count_linewise = function(start, end_)
local count = 0
local line = start
while line <= end_ do
count = count + 1
local foldclosedend = fn.foldclosedend(line)
if foldclosedend ~= -1 then
line = foldclosedend
end
line = line + 1
end
return count
end
-- Returns the count of virtual lines between the specified start and end lines
-- (both inclusive), in the specified window. A closed fold counts as one
-- virtual line. The computation loops over either lines or virtual spans, so
-- the cursor may be moved.
local virtual_line_count = function(winid, start, end_)
local last_line = api.nvim_buf_line_count(api.nvim_win_get_buf(winid))
if type(end_) == 'string' and end_ == '$' then
end_ = last_line
end
local base_winid = get_base_winid(winid)
local memoize_key =
table.concat({VIRTUAL_LINE_COUNT_KEY_PREFIX, base_winid, start, end_}, ':')
if memoize and cache[memoize_key] then return cache[memoize_key] end
local count = with_win_workspace(winid, function()
-- On an AMD Ryzen 7 2700X, linewise computation takes about 3e-7 seconds
-- per line (this is an overestimate, as it assumes all folds are open, but
-- the time is reduced when there are closed folds, as lines would be
-- skipped). Spanwise computation takes about 5e-5 seconds per fold (closed
-- folds count as a single fold). Therefore the linewise computation is
-- worthwhile when the number of folds is greater than (3e-7 / 5e-5) * L =
-- .006L, where L is the number of lines.
if fold_count_exceeds(start, end_, math.floor(last_line * .006)) then
return virtual_line_count_linewise(start, end_)
else
return virtual_line_count_spanwise(start, end_)
end
end)
if memoize then cache[memoize_key] = count end
return count
end
-- Returns the proper line count between the two lines. 'store' is an optional
-- dictionary that can be used to save/retrieve values for reuse.
local proper_line_count = function(winid, start, end_, store)
if store == nil then
store = {}
end
local last_line = api.nvim_buf_line_count(api.nvim_win_get_buf(winid))
if type(end_) == 'string' and end_ == '$' then
end_ = last_line
end
start = math.max(1, start)
local base_winid = get_base_winid(winid)
local memoize_key = table.concat(
{PROPER_LINE_COUNT_KEY_PREFIX, base_winid, start, end_}, ':')
if memoize and cache[memoize_key] then return cache[memoize_key] end
local count
-- The two approaches that follow, which use nvim_win_text_height and
-- virtcol, take about the same time to run. However, the nvim_win_text_height
-- approach also accounts for diff filler and virtual text lines, in addition
-- to folds and wrapped lines.
if api.nvim_win_text_height ~= nil then
count = api.nvim_win_text_height(
winid, {start_row = start - 1, end_row = end_ - 1}).all
else
api.nvim_win_call(winid, function()
if store.bufwidth == nil then
local winwidth = fn.winwidth(winid)
store.bufwidth = winwidth - buf_view_begins_col(winid) + 1
end
count = 0
local line = start
while line <= end_ do
local count_diff = 1
if api.nvim_win_get_option(winid, 'wrap') then
local virtcol = fn.virtcol({line, '$'})
count_diff = math.ceil((virtcol - 1) / store.bufwidth)
end
-- Avoid zero as a precaution (virtcol's result is one for empty
-- lines).
count_diff = math.max(1, count_diff)
count = count + count_diff
local foldclosedend = fn.foldclosedend(line)
if foldclosedend ~= -1 then
line = foldclosedend
end
line = line + 1
end
end)
end
if memoize then cache[memoize_key] = count end
return count
end
local calculate_scrollbar_height = function(winid)
local bufnr = api.nvim_win_get_buf(winid)
local winheight = get_window_height(winid)
local line_count = api.nvim_buf_line_count(bufnr)
local mode = scrollview_mode(winid)
local effective_line_count
if mode == SIMPLE_MODE then
effective_line_count = line_count
elseif mode == VIRTUAL_MODE then
effective_line_count = virtual_line_count(winid, 1, '$')
elseif mode == PROPER_MODE then
effective_line_count = proper_line_count(winid, 1, '$')
else
error('Unknown mode: ' .. mode)
end
if to_bool(vim.g.scrollview_include_end_region) then
effective_line_count = effective_line_count + winheight - 1
end
local height = winheight / effective_line_count
height = math.ceil(height * winheight)
height = math.max(1, height)
return height
end
-- Return the target number of items for a topline lookup table.
local get_target_topline_count = function(winid)
local target_topline_count = get_window_height(winid)
if to_bool(vim.g.scrollview_include_end_region) then
local scrollbar_height = calculate_scrollbar_height(winid)
target_topline_count = target_topline_count - scrollbar_height + 1
end
return target_topline_count
end
local sanitize_topline_lookup = function(
winid, topline_lookup, target_topline_count)
api.nvim_win_call(winid, function()
while #topline_lookup < target_topline_count do
table.insert(topline_lookup, fn.line('$'))
end
for idx, line in ipairs(topline_lookup) do
line = math.max(1, line)
line = math.min(fn.line('$'), line)
local foldclosed = fn.foldclosed(line)
if foldclosed ~= -1 then
line = foldclosed
end
topline_lookup[idx] = line
end
end)
end
-- Returns an array that maps window rows to the topline that corresponds to a
-- scrollbar at that row under virtual scrollview mode, in the current window.
-- The computation loops over virtual spans. The cursor may be moved.
local virtual_topline_lookup_spanwise = function()
local winid = api.nvim_get_current_win()
local target_topline_count = get_target_topline_count(winid)
local result = {} -- A list of line numbers
local total_vlines = virtual_line_count(winid, 1, '$')
if total_vlines > 1 and target_topline_count > 1 then
local line = 0
local virtual_line = 0
local prop = 0.0
local row = 1
local proportion = (row - 1) / (target_topline_count - 1)
vim.cmd('keepjumps normal! gg')
while #result < target_topline_count do
local range_start, range_end, fold = advance_virtual_span()
local line_delta = range_end - range_start + 1
local virtual_line_delta = 1
if not fold then
virtual_line_delta = line_delta
end
local prop_delta = virtual_line_delta / (total_vlines - 1)
while prop + prop_delta >= proportion and #result < target_topline_count do
local ratio = (proportion - prop) / prop_delta
local topline = line + 1
if fold then
-- If ratio >= 0.5, add all lines in the fold, otherwise don't add
-- the fold.
if ratio >= 0.5 then
topline = topline + line_delta
end
else
topline = topline + round(ratio * line_delta)
end
table.insert(result, topline)
row = row + 1
proportion = (row - 1) / (target_topline_count - 1)
end
-- A line number of 1 indicates that advance_virtual_span looped back to
-- the beginning of the document.
local looped = fn.line('.') == 1
if looped or #result >= target_topline_count then
break
end
line = line + line_delta
virtual_line = virtual_line + virtual_line_delta
prop = virtual_line / (total_vlines - 1)
end
end
while #result < target_topline_count do
table.insert(result, fn.line('$'))
end
sanitize_topline_lookup(winid, result, target_topline_count)
return result
end
-- Returns an array that maps window rows to the topline that corresponds to a
-- scrollbar at that row under virtual scrollview mode, in the current window.
local virtual_topline_lookup_linewise = function()
local winid = api.nvim_get_current_win()
local target_topline_count = get_target_topline_count(winid)
local last_line = fn.line('$')
local result = {} -- A list of line numbers
local total_vlines = virtual_line_count(winid, 1, '$')
if total_vlines > 1 and target_topline_count > 1 then
local count = 1 -- The count of virtual lines
local line = 1