Skip to content

Commit 8366119

Browse files
authored
fix: virtual cursor edge cases (#32)
* perf(Mode): use dedicated virtual cursor namespace * fix(Mode): virtual cursor stays in inactive buffers when exiting * fix(Mode): `CursorMoved` not fired as expected
1 parent 3cb0a38 commit 8366119

File tree

2 files changed

+84
-24
lines changed

2 files changed

+84
-24
lines changed

examples/lua/keymaps.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ local fooModeKeymaps =
2121
zfc = 'q',
2222
zff = split_twice,
2323
zfo = 'vsplit',
24+
e = 'edit foo',
25+
p = 'bp',
26+
o = 'norm o',
2427
}
2528

2629
-- enter the mode using the keymaps

lua/libmodal/Mode.lua

Lines changed: 81 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,47 @@ local globals = require 'libmodal.globals'
22
local ParseTable = require 'libmodal.collections.ParseTable'
33
local utils = require 'libmodal.utils' --- @type libmodal.utils
44

5+
--- @alias CursorPosition {[1]: integer, [2]: integer} see `nvim_win_get_cursor`
6+
57
--- @class libmodal.Mode
8+
--- @field private cursor CursorPosition
69
--- @field private flush_input_timer unknown
710
--- @field private help? libmodal.utils.Help
8-
--- @field private input libmodal.utils.Var[number]
9-
--- @field private input_bytes? number[] local `input` history
11+
--- @field private input libmodal.utils.Var[integer]
12+
--- @field private input_bytes? integer[] local `input` history
1013
--- @field private instruction fun()|{[string]: fun()|string}
1114
--- @field private mappings libmodal.collections.ParseTable
1215
--- @field private modeline string[][]
1316
--- @field private name string
14-
--- @field private ns number the namespace where cursor highlights are drawn on
1517
--- @field private popups libmodal.collections.Stack
1618
--- @field private supress_exit boolean
17-
--- @field public count libmodal.utils.Var[number]
19+
--- @field private virtual_cursor_autocmd integer
20+
--- @field public count libmodal.utils.Var[integer]
1821
--- @field public exit libmodal.utils.Var[boolean]
1922
--- @field public timeouts? libmodal.utils.Var[boolean]
2023
local Mode = utils.classes.new()
2124

25+
--- Cursor events triggered by which modes
26+
local CURSOR_EVENTS_BY_MODE = {
27+
CursorMoved = {
28+
n = true,
29+
V = true,
30+
v = true,
31+
[utils.api.replace_termcodes '<C-v>'] = true,
32+
},
33+
34+
CursorMovedI = {
35+
i = true,
36+
R = true,
37+
},
38+
}
39+
40+
--- The namespaces used by modes
41+
local NS = {
42+
--- The virtual cursor namespace. Used to workaround neovim/neovim#20793
43+
CURSOR = vim.api.nvim_create_namespace('libmodal-mode-virtual_cursor'),
44+
}
45+
2246
local HELP_CHAR = '?'
2347
local TIMEOUT =
2448
{
@@ -45,7 +69,7 @@ function Mode:execute_instruction(instruction)
4569
end
4670

4771
self.count:set(0)
48-
self:redraw_virtual_cursor()
72+
self:render_virtual_cursor(0, true)
4973
end
5074

5175
--- check the user's input against the `self.instruction` mappings to see if there is anything to execute.
@@ -105,9 +129,11 @@ function Mode:check_input_for_mapping()
105129
end
106130

107131
--- clears the virtual cursor from the screen
132+
--- @param bufnr integer to clear the cursor on
108133
--- @private
109-
function Mode:clear_virt_cursor()
110-
vim.api.nvim_buf_clear_namespace(0, self.ns, 0, -1);
134+
function Mode:clear_virtual_cursor(bufnr)
135+
vim.api.nvim_buf_clear_namespace(bufnr, NS.CURSOR, 0, -1);
136+
111137
end
112138

113139
--- enter this mode.
@@ -122,10 +148,22 @@ function Mode:enter()
122148
self.count:set(0)
123149
self.exit:set(false)
124150

125-
--- HACK: https://github.com/neovim/neovim/issues/20793
151+
--- HACK: neovim/neovim#20793
126152
vim.api.nvim_command 'highlight Cursor blend=100'
127153
vim.schedule(function() vim.opt.guicursor:append { 'a:Cursor/lCursor' } end)
128-
self:render_virt_cursor()
154+
self.cursor = self:cursor_in(0)
155+
self:render_virtual_cursor(0)
156+
157+
do
158+
local augroup = vim.api.nvim_create_augroup('libmodal-mode-' .. self.name, { clear = false })
159+
self.virtual_cursor_autocmd = vim.api.nvim_create_autocmd('BufLeave', {
160+
callback = function(ev)
161+
local bufnr = ev.buf
162+
self:clear_virtual_cursor(bufnr)
163+
end,
164+
group = augroup,
165+
})
166+
end
129167

130168
self.previous_mode_name = vim.g.libmodalActiveModeName
131169
vim.g.libmodalActiveModeName = self.name
@@ -194,19 +232,37 @@ function Mode:get_user_input()
194232
end
195233
end
196234

197-
--- clears and then renders the virtual cursor
198-
--- @private
199-
function Mode:redraw_virtual_cursor()
200-
self:clear_virt_cursor()
201-
self:render_virt_cursor()
235+
--- @param winid integer
236+
--- @return CursorPosition line_and_col
237+
function Mode:cursor_in(winid)
238+
local cursor = vim.api.nvim_win_get_cursor(winid)
239+
cursor[1] = cursor[1] - 1 -- win_get_cursor returns +1 for our purpose
240+
return cursor
202241
end
203242

204243
--- render the virtual cursor using extmarks
244+
--- @param winid integer
245+
--- @param clear? boolean if true, clear other virtual cursors before rendering the new one
205246
--- @private
206-
function Mode:render_virt_cursor()
207-
local line_nr, col_nr = unpack(vim.api.nvim_win_get_cursor(0))
208-
line_nr = line_nr - 1 -- win_get_cursor returns +1 for our purpose
209-
vim.highlight.range(0, self.ns, 'Cursor', { line_nr, col_nr }, { line_nr, col_nr + 1 }, {})
247+
function Mode:render_virtual_cursor(winid, clear)
248+
local bufnr = vim.api.nvim_win_get_buf(winid)
249+
if clear then
250+
self:clear_virtual_cursor(bufnr)
251+
end
252+
253+
local cursor = self:cursor_in(winid)
254+
vim.highlight.range(bufnr, NS.CURSOR, 'Cursor', cursor, cursor, { inclusive = true })
255+
256+
if not vim.deep_equal(self.cursor, cursor) then
257+
local mode = vim.api.nvim_get_mode().mode
258+
if CURSOR_EVENTS_BY_MODE.CursorMoved[mode] then
259+
vim.api.nvim_exec_autocmds('CursorMoved', {})
260+
elseif CURSOR_EVENTS_BY_MODE.CursorMovedI[mode] then
261+
vim.api.nvim_exec_autocmds('CursorMovedI', {})
262+
end
263+
264+
self.cursor = cursor
265+
end
210266
end
211267

212268
--- show the mode indicator, if it is enabled
@@ -234,18 +290,20 @@ end
234290
--- @private
235291
--- @return nil
236292
function Mode:tear_down()
293+
--- HACK: neovim/neovim#20793
294+
self:clear_virtual_cursor(0)
295+
vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end)
296+
vim.api.nvim_command 'highlight Cursor blend=0'
297+
vim.api.nvim_del_autocmd(self.virtual_cursor_autocmd)
298+
self.cursor = nil
299+
237300
if type(self.instruction) == 'table' then
238301
self.flush_input_timer:stop()
239302
self.input_bytes = nil
240303

241304
self.popups:pop():close()
242305
end
243306

244-
--- HACK: https://github.com/neovim/neovim/issues/20793
245-
self:clear_virt_cursor()
246-
vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end)
247-
vim.api.nvim_command 'highlight Cursor blend=0'
248-
249307
if self.previous_mode_name and #vim.trim(self.previous_mode_name) < 1 then
250308
vim.g.libmodalActiveModeName = nil
251309
else
@@ -272,7 +330,6 @@ function Mode.new(name, instruction, supress_exit)
272330
input = utils.Var.new(name, 'input'),
273331
instruction = instruction,
274332
name = name,
275-
ns = vim.api.nvim_create_namespace('libmodal' .. name),
276333
modeline = {{'-- ' .. name .. ' --', 'LibmodalPrompt'}},
277334
},
278335
Mode

0 commit comments

Comments
 (0)