Enhance neovim's visual line mode with fully highlighted lines
neovim-plugin
1local M = {}
2local group_name = 'full_line_visual_mode'
3
4local a = vim.api
5M.autocmd_group = a.nvim_create_augroup(group_name, { clear = true })
6M.nsid = a.nvim_create_namespace(group_name)
7
8function M.is_autocmd_setup()
9 return not vim.tbl_isempty(a.nvim_get_autocmds { group = M.autocmd_group })
10end
11
12function M.setup_autocmd()
13 a.nvim_create_autocmd({ 'CursorMoved', 'ModeChanged' }, {
14 group = M.autocmd_group,
15 callback = vim.schedule_wrap(M.handle_autocmd),
16 })
17end
18
19function M.remove_autocmd()
20 a.nvim_clear_autocmds { group = M.autocmd_group }
21end
22
23function M.draw_lines_in_range(range_start, range_end)
24 for line = range_start, range_end do
25 a.nvim_buf_set_extmark(0, M.nsid, line - 1, 0, { line_hl_group = 'Visual' })
26 end
27end
28
29function M.clear_lines()
30 a.nvim_buf_clear_namespace(0, M.nsid, 0, -1)
31end
32
33function M.clear_lines_in_range(range_start, range_end)
34 a.nvim_buf_clear_namespace(0, M.nsid, range_start, range_end)
35end
36
37function M.cleanup()
38 M.remove_autocmd()
39 M.clear_lines()
40end
41
42-- we need to store the last positions of the start and end of the visual
43-- selection in order to partially update the selection. Fully clearing and
44-- redrawing on every update can cause flickering.
45--
46-- {start,end}_move_delta is just for convenience
47local selection_state = nil
48
49function M.update_visual_line_state()
50 local start, _end = M.get_selection_range()
51
52 if selection_state == nil then
53 selection_state = { old_start = start, old_end = _end }
54 else
55 selection_state = { old_start = selection_state.start, old_end = selection_state._end }
56 end
57
58 selection_state.start = start
59 selection_state._end = _end
60
61 selection_state.start_move_delta = selection_state.start - selection_state.old_start
62 selection_state.end_move_delta = selection_state._end - selection_state.old_end
63
64 return selection_state
65end
66
67function M.get_selection_range()
68 local start_line, end_line = vim.fn.line 'v', vim.fn.line '.'
69
70 -- ensure the start line is always less than or equal to the end line
71 if start_line > end_line then
72 start_line, end_line = end_line, start_line
73 end
74
75 return start_line, end_line
76end
77
78function M.handle_autocmd(opts)
79 if a.nvim_get_mode().mode ~= 'V' then
80 M.clear_lines()
81 return
82 end
83
84 local state = M.update_visual_line_state()
85
86 if opts.event == 'ModeChanged' then
87 M.clear_lines()
88 M.draw_lines_in_range(state.start, state._end)
89 return
90 end
91
92 -- nothing changed
93 if state.start_move_delta == 0 and state.end_move_delta == 0 then
94 return
95 end
96
97 -- add/remove selection lines from the start/end depending on which moved
98 -- if both the start and end have moved just clear and redraw everything
99 --
100 -- `s` = old start, `S` = new start, `e` = old end, `E` = new end
101 -- `_` = current vis line, `-` = remove vis line, `+` = add vis line
102 if state.start_move_delta ~= 0 and state.end_move_delta ~= 0 then
103 M.clear_lines()
104 M.draw_lines_in_range(state.start, state._end)
105 elseif state.start_move_delta < 0 then
106 -- ...S<+++s____E..
107 M.draw_lines_in_range(state.start, state.old_start - 1)
108 elseif state.start_move_delta > 0 then
109 -- ...s--->S____E..
110 M.clear_lines_in_range(state.old_start - 1, state.start - 1)
111 elseif state.end_move_delta > 0 then
112 -- ...S____e+++>E..
113 M.draw_lines_in_range(state.old_end + 1, state._end)
114 elseif state.end_move_delta < 0 then
115 -- ...S____E<---e..
116 M.clear_lines_in_range(state._end, state.old_end)
117 end
118end
119
120return M