diff --git a/examples/lua/keymaps.lua b/examples/lua/keymaps.lua index 803078a..895527a 100644 --- a/examples/lua/keymaps.lua +++ b/examples/lua/keymaps.lua @@ -21,6 +21,9 @@ local fooModeKeymaps = zfc = 'q', zff = split_twice, zfo = 'vsplit', + e = 'edit foo', + p = 'bp', + o = 'norm o', } -- enter the mode using the keymaps diff --git a/lua/libmodal/Mode.lua b/lua/libmodal/Mode.lua index 59219fc..bf97596 100644 --- a/lua/libmodal/Mode.lua +++ b/lua/libmodal/Mode.lua @@ -2,23 +2,47 @@ local globals = require 'libmodal.globals' local ParseTable = require 'libmodal.collections.ParseTable' local utils = require 'libmodal.utils' --- @type libmodal.utils +--- @alias CursorPosition {[1]: integer, [2]: integer} see `nvim_win_get_cursor` + --- @class libmodal.Mode +--- @field private cursor CursorPosition --- @field private flush_input_timer unknown --- @field private help? libmodal.utils.Help ---- @field private input libmodal.utils.Var[number] ---- @field private input_bytes? number[] local `input` history +--- @field private input libmodal.utils.Var[integer] +--- @field private input_bytes? integer[] local `input` history --- @field private instruction fun()|{[string]: fun()|string} --- @field private mappings libmodal.collections.ParseTable --- @field private modeline string[][] --- @field private name string ---- @field private ns number the namespace where cursor highlights are drawn on --- @field private popups libmodal.collections.Stack --- @field private supress_exit boolean ---- @field public count libmodal.utils.Var[number] +--- @field private virtual_cursor_autocmd integer +--- @field public count libmodal.utils.Var[integer] --- @field public exit libmodal.utils.Var[boolean] --- @field public timeouts? libmodal.utils.Var[boolean] local Mode = utils.classes.new() +--- Cursor events triggered by which modes +local CURSOR_EVENTS_BY_MODE = { + CursorMoved = { + n = true, + V = true, + v = true, + [utils.api.replace_termcodes ''] = true, + }, + + CursorMovedI = { + i = true, + R = true, + }, +} + +--- The namespaces used by modes +local NS = { + --- The virtual cursor namespace. Used to workaround neovim/neovim#20793 + CURSOR = vim.api.nvim_create_namespace('libmodal-mode-virtual_cursor'), +} + local HELP_CHAR = '?' local TIMEOUT = { @@ -45,7 +69,7 @@ function Mode:execute_instruction(instruction) end self.count:set(0) - self:redraw_virtual_cursor() + self:render_virtual_cursor(0, true) end --- 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() end --- clears the virtual cursor from the screen +--- @param bufnr integer to clear the cursor on --- @private -function Mode:clear_virt_cursor() - vim.api.nvim_buf_clear_namespace(0, self.ns, 0, -1); +function Mode:clear_virtual_cursor(bufnr) + vim.api.nvim_buf_clear_namespace(bufnr, NS.CURSOR, 0, -1); + end --- enter this mode. @@ -122,10 +148,22 @@ function Mode:enter() self.count:set(0) self.exit:set(false) - --- HACK: https://github.com/neovim/neovim/issues/20793 + --- HACK: neovim/neovim#20793 vim.api.nvim_command 'highlight Cursor blend=100' vim.schedule(function() vim.opt.guicursor:append { 'a:Cursor/lCursor' } end) - self:render_virt_cursor() + self.cursor = self:cursor_in(0) + self:render_virtual_cursor(0) + + do + local augroup = vim.api.nvim_create_augroup('libmodal-mode-' .. self.name, { clear = false }) + self.virtual_cursor_autocmd = vim.api.nvim_create_autocmd('BufLeave', { + callback = function(ev) + local bufnr = ev.buf + self:clear_virtual_cursor(bufnr) + end, + group = augroup, + }) + end self.previous_mode_name = vim.g.libmodalActiveModeName vim.g.libmodalActiveModeName = self.name @@ -194,19 +232,37 @@ function Mode:get_user_input() end end ---- clears and then renders the virtual cursor ---- @private -function Mode:redraw_virtual_cursor() - self:clear_virt_cursor() - self:render_virt_cursor() +--- @param winid integer +--- @return CursorPosition line_and_col +function Mode:cursor_in(winid) + local cursor = vim.api.nvim_win_get_cursor(winid) + cursor[1] = cursor[1] - 1 -- win_get_cursor returns +1 for our purpose + return cursor end --- render the virtual cursor using extmarks +--- @param winid integer +--- @param clear? boolean if true, clear other virtual cursors before rendering the new one --- @private -function Mode:render_virt_cursor() - local line_nr, col_nr = unpack(vim.api.nvim_win_get_cursor(0)) - line_nr = line_nr - 1 -- win_get_cursor returns +1 for our purpose - vim.highlight.range(0, self.ns, 'Cursor', { line_nr, col_nr }, { line_nr, col_nr + 1 }, {}) +function Mode:render_virtual_cursor(winid, clear) + local bufnr = vim.api.nvim_win_get_buf(winid) + if clear then + self:clear_virtual_cursor(bufnr) + end + + local cursor = self:cursor_in(winid) + vim.highlight.range(bufnr, NS.CURSOR, 'Cursor', cursor, cursor, { inclusive = true }) + + if not vim.deep_equal(self.cursor, cursor) then + local mode = vim.api.nvim_get_mode().mode + if CURSOR_EVENTS_BY_MODE.CursorMoved[mode] then + vim.api.nvim_exec_autocmds('CursorMoved', {}) + elseif CURSOR_EVENTS_BY_MODE.CursorMovedI[mode] then + vim.api.nvim_exec_autocmds('CursorMovedI', {}) + end + + self.cursor = cursor + end end --- show the mode indicator, if it is enabled @@ -234,6 +290,13 @@ end --- @private --- @return nil function Mode:tear_down() + --- HACK: neovim/neovim#20793 + self:clear_virtual_cursor(0) + vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end) + vim.api.nvim_command 'highlight Cursor blend=0' + vim.api.nvim_del_autocmd(self.virtual_cursor_autocmd) + self.cursor = nil + if type(self.instruction) == 'table' then self.flush_input_timer:stop() self.input_bytes = nil @@ -241,11 +304,6 @@ function Mode:tear_down() self.popups:pop():close() end - --- HACK: https://github.com/neovim/neovim/issues/20793 - self:clear_virt_cursor() - vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end) - vim.api.nvim_command 'highlight Cursor blend=0' - if self.previous_mode_name and #vim.trim(self.previous_mode_name) < 1 then vim.g.libmodalActiveModeName = nil else @@ -272,7 +330,6 @@ function Mode.new(name, instruction, supress_exit) input = utils.Var.new(name, 'input'), instruction = instruction, name = name, - ns = vim.api.nvim_create_namespace('libmodal' .. name), modeline = {{'-- ' .. name .. ' --', 'LibmodalPrompt'}}, }, Mode