@ -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 global_exit libmodal.utils.Vars
--- @field private global_input libmodal.utils.Vars
--- @field private help? libmodal.utils.Help
--- @field private input_bytes? number[]
--- @field private input libmodal.utils.Var[integer]
--- @field private input_bytes? integer[] local `input` history
--- @field private instruction fun()|{[string]: fun()|string}
--- @field private local_exit boolean
--- @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 show_name fun()
--- @field private supress_exit boolean
--- @field private timeouts_enabled boolean
--- @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 ' <C-v> ' ] = 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 =
{
@ -27,6 +51,12 @@ local TIMEOUT =
}
TIMEOUT.CHAR_NUMBER = TIMEOUT.CHAR : byte ( )
--- Byte for 0
local ZERO = string.byte ( 0 )
--- Byte for 9
local NINE = string.byte ( 9 )
--- execute the `instruction`.
--- @private
--- @param instruction fun(libmodal.Mode)|string a Lua function or Vimscript command.
@ -38,7 +68,8 @@ function Mode:execute_instruction(instruction)
vim.api . nvim_command ( instruction )
end
self : redraw_virtual_cursor ( )
self.count : set ( 0 )
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.
@ -50,7 +81,7 @@ function Mode:check_input_for_mapping()
self.flush_input_timer : stop ( )
-- append the latest input to the locally stored input history.
self.input_bytes [ # self.input_bytes + 1 ] = self. global_ input: get ( )
self.input_bytes [ # self.input_bytes + 1 ] = self. input: get ( )
-- get the command based on the users input.
local cmd = self.mappings : get ( self.input_bytes )
@ -65,7 +96,7 @@ function Mode:check_input_for_mapping()
end
self.input_bytes = { }
elseif command_type == ' table ' and globals.is_true ( self.timeouts _enabled ) then -- the command was a table, meaning that it MIGHT match.
elseif command_type == ' table ' and globals.is_true ( self.timeouts : get ( ) ) then -- the command was a table, meaning that it MIGHT match.
local timeout = vim.api . nvim_get_option_value ( ' timeoutlen ' , { } )
self.flush_input_timer : start ( -- start the timer
timeout , 0 , vim.schedule_wrap ( function ( )
@ -86,13 +117,23 @@ function Mode:check_input_for_mapping()
self.input_bytes = { }
end
self.popups : peek ( ) : refresh ( self.input_bytes )
local input_bytes = self.input_bytes or { }
local count = self.count : get ( )
if count > 0 then
local count_bytes = { tostring ( count ) : byte ( 1 , - 1 ) }
input_bytes = vim.list_extend ( count_bytes , input_bytes )
end
self.popups : peek ( ) : refresh ( input_bytes )
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.
@ -104,12 +145,25 @@ function Mode:enter()
self.popups : push ( utils.Popup . new ( ) )
end
self.local_exit = false
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
@ -118,49 +172,27 @@ function Mode:enter()
local previous_mode = self.previous_mode_name or vim.api . nvim_get_mode ( ) . mode
vim.api . nvim_exec_autocmds ( ' ModeChanged ' , { pattern = previous_mode .. ' : ' .. self.name } )
local continue_mode = true
while continue_mode do
repeat
-- try (using pcall) to use the mode.
local ok , mode_ result = pcall ( self.get_user_input , self )
local ok , result = pcall ( self.get_user_input , self )
-- if there were errors, handle them.
if not ok then
--- @diagnostic disable-next-line:param-type-mismatch if `not ok` then `mode_result` is a string
utils.notify_error ( ' Error during nvim-libmodal mode ' , mode_result )
continue_mode = false
else
continue_mode = mode_result
utils.notify_error ( ' Error during nvim-libmodal mode ' , result )
self.exit : set_local ( true )
end
end
until globals.is_true ( self.exit : get ( ) )
self : tear_down ( )
vim.api . nvim_exec_autocmds ( ' ModeChanged ' , { pattern = self.name .. ' : ' .. previous_mode } )
end
--- exit this instance of the mode.
--- WARN: does not interrupt the current mode to exit. It only flags that exit is desired for when control yields back
--- to the mode.
--- @return nil
function Mode : exit ( )
self.local_exit = true
end
--- @private
--- @return boolean `true` if the mode's exit was flagged
function Mode : exit_flagged ( )
return self.local_exit or globals.is_true ( self.global_exit : get ( ) )
end
--- get input from the user.
--- @private
--- @return boolean more_input
function Mode : get_user_input ( )
if self : exit_flagged ( ) then
return false
end
-- echo the indicator.
self .show_nam e( )
self : show_mode ( )
-- capture input.
local user_input = vim.fn . getchar ( )
@ -171,41 +203,76 @@ function Mode:get_user_input()
end
-- set the global input variable to the new input.
self.global_input : set ( user_input )
self.input : set ( user_input )
if ZERO <= user_input and user_input <= NINE then
local oldCount = self.count : get ( )
local newCount = tonumber ( oldCount .. string.char ( user_input ) )
self.count : set ( newCount )
end
if not self.supress_exit and user_input == globals.ESC_NR then -- the user wants to exit.
return false -- as in, "I don't want to continue."
else -- the user wants to continue.
--[[ The instruction type is determined every cycle, because the user may be assuming a more direct control
over the instruction and it may change over the course of execution . ] ]
local instruction_type = type ( self.instruction )
if instruction_type == ' table ' then -- the instruction was provided as a was a set of mappings.
self : check_input_for_mapping ( )
elseif instruction_type == ' string ' then -- the instruction is the name of a Vimscript function.
vim.fn [ self.instruction ] ( )
else -- the instruction is a function.
self.instruction ( )
if self.count : get ( ) < 1 then -- exit
return self.exit : set_local ( true )
end
self.count : set ( 0 ) -- reset count count
end
return true
--[[ The instruction type is determined every cycle, because the user may be assuming a more direct control
over the instruction and it may change over the course of execution . ] ]
local instruction_type = type ( self.instruction )
if instruction_type == ' table ' then -- the instruction was provided as a was a set of mappings.
self : check_input_for_mapping ( )
elseif instruction_type == ' string ' then -- the instruction is the name of a Vimscript function.
vim.fn [ self.instruction ] ( )
else -- the instruction is a function.
self.instruction ( )
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
function Mode : show_mode ( )
utils.api . redraw ( )
local showmode = vim.api . nvim_get_option_value ( ' showmode ' , { } )
if showmode then
vim.api . nvim_echo ( { { ' -- ' .. self.name .. ' -- ' , ' LibmodalPrompt ' } } , false , { } )
end
end
--- `enter` a `Mode` using the arguments given, and then flag the current mode to exit.
@ -216,13 +283,20 @@ end
function Mode : switch ( ... )
local mode = Mode.new ( ... )
mode : enter ( )
self : exi t( )
self .exit : s et_local ( true )
end
--- uninitialize variables from after exiting the mode.
--- @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
@ -230,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
@ -256,23 +325,16 @@ function Mode.new(name, instruction, supress_exit)
-- inherit the metatable.
local self = setmetatable (
{
global_exit = utils.Vars . new ( ' exit ' , name ) ,
global_input = utils.Vars . new ( ' input ' , name ) ,
count = utils.Var . new ( name , ' count ' ) ,
exit = utils.Var . new ( name , ' exit ' ) ,
input = utils.Var . new ( name , ' input ' ) ,
instruction = instruction ,
local_exit = false ,
name = name ,
ns = vim.api . nvim_create_namespace ( ' libmodal ' .. name ) ,
modeline = { { ' -- ' .. name .. ' -- ' , ' LibmodalPrompt ' } } ,
} ,
Mode
)
self.show_name = vim.o . showmode and
function ( )
utils.api . redraw ( )
vim.api . nvim_echo ( { { ' -- ' .. name .. ' -- ' , ' LibmodalPrompt ' } } , false , { } )
end or
utils.api . redraw
-- define the exit flag
self.supress_exit = supress_exit or false
@ -295,10 +357,7 @@ function Mode.new(name, instruction, supress_exit)
self.popups = require ( ' libmodal.collections.Stack ' ) . new ( )
-- create a variable for whether or not timeouts are enabled.
self.timeouts = utils.Vars . new ( ' timeouts ' , self.name )
-- read the correct timeout variable.
self.timeouts_enabled = self.timeouts : get ( ) or vim.g . libmodalTimeouts
self.timeouts = utils.Var . new ( self.name , ' timeouts ' , vim.g . libmodalTimeouts )
end
return self