--- @type libmodal.globals local globals = require 'libmodal/src/globals' --- @type libmodal.collections.ParseTable local ParseTable = require 'libmodal/src/collections/ParseTable' --- @type libmodal.utils local utils = require 'libmodal/src/utils' --- @class libmodal.Mode --- @field private exit libmodal.utils.Vars --- @field private flush_input_timer unknown --- @field private help? libmodal.utils.Help --- @field private indicator libmodal.utils.Indicator --- @field private input libmodal.utils.Vars --- @field private instruction fun()|{[string]: fun()|string} --- @field private mappings libmodal.collections.ParseTable --- @field private name string --- @field private popups libmodal.collections.Stack --- @field private supress_exit boolean --- @field private timeouts_enabled boolean local Mode = utils.classes.new() local InputBytes = utils.classes.new( { clear = function(self) for i, _ in ipairs(self) do self[i] = nil end end }) local HELP = '?' local TIMEOUT = { CHAR = 'ΓΈ', LEN = vim.go.timeoutlen, SEND = function(self) vim.api.nvim_feedkeys(self.CHAR, 'nt', false) end } TIMEOUT.CHAR_NUMBER = string.byte(TIMEOUT.CHAR) --- execute the `instruction`. --- @param instruction fun()|string a Lua function or Vimscript command. function Mode.execute_instruction(instruction) if type(instruction) == globals.TYPE_FUNC then instruction() else vim.api.nvim_command(instruction) end end --- check the user's input against the `self.instruction` mappings to see if there is anything to execute. --- if there is nothing to execute, the user's input is rendered on the screen (as does Vim by default). function Mode:check_input_for_mapping() -- stop any running timers self.flush_input_timer:stop() -- append the latest input to the locally stored input history. 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) -- get the type of the command. local command_type = type(cmd) -- if there was no matching command if not cmd then if #self.input_bytes < 2 and self.input_bytes[1] == string.byte(HELP) then self.help:show() end self.input_bytes:clear() elseif command_type == globals.TYPE_TBL and globals.is_true(self.timeouts_enabled) then -- the command was a table, meaning that it MIGHT match. self.flush_input_timer:start( -- start the timer TIMEOUT.LEN, 0, vim.schedule_wrap(function() -- send input to interrupt a blocking `getchar` TIMEOUT:SEND() -- if there is a command, execute it. if cmd[ParseTable.CR] then self.execute_instruction(cmd[ParseTable.CR]) end -- clear input self.input_bytes:clear() self.popups:peek():refresh(self.input_bytes) end) ) else -- the command was an actual vim command. --- @diagnostic disable-next-line:param-type-mismatch already checked `cmd` != `table` self.execute_instruction(cmd) self.input_bytes:clear() end self.popups:peek():refresh(self.input_bytes) end --- enter this mode. function Mode:enter() -- intialize variables that are needed for each recurse of a function if type(self.instruction) == globals.TYPE_TBL then -- initialize the input history variable. self.popups:push(utils.Popup.new()) end self.previous_mode_name = vim.g.libmodalActiveModeName vim.g.libmodalActiveModeName = self.name --[[ MODE LOOP. ]] 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 -- try (using pcall) to use the mode. local ok, mode_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 end end self:tear_down() vim.api.nvim_exec_autocmds('ModeChanged', {pattern = self.name .. ':' .. previous_mode}) end --- get input from the user. --- @return boolean more_input function Mode:get_user_input() -- if the mode is not handling exit events automatically and the global exit var is true. if self.supress_exit and globals.is_true(self.exit:get()) then return false end -- echo the indicator. utils.api.hi_echo(self.indicator) -- capture input. local user_input = vim.fn.getchar() -- return if there was a timeout event. if user_input == TIMEOUT.CHAR_NUMBER then return true end -- set the global input variable to the new input. self.input:set(user_input) 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 == globals.TYPE_TBL then -- the instruction was provided as a was a set of mappings. self:check_input_for_mapping() elseif instruction_type == globals.TYPE_STR then -- the instruction is the name of a Vimscript function. vim.fn[self.instruction]() else -- the instruction is a function. self.instruction() end end return true end --- uninitialize variables from after exiting the mode. function Mode:tear_down() if type(self.instruction) == globals.TYPE_TBL then self.flush_input_timer:stop() self.input_bytes = nil self.popups:pop():close() end if self.previous_mode_name and #vim.trim(self.previous_mode_name) < 1 then vim.g.libmodalActiveModeName = nil else vim.g.libmodalActiveModeName = self.previous_mode_name end utils.api.redraw() end return { --- create a new mode. --- @param name string the name of the mode. --- @param instruction fun()|string|table a Lua function, keymap dictionary, Vimscript command. --- @return libmodal.Mode new = function(name, instruction, supress_exit) name = vim.trim(name) -- inherit the metatable. local self = setmetatable( { exit = utils.Vars.new('exit', name), indicator = utils.Indicator.new('LibmodalPrompt', '-- ' .. name .. ' --'), input = utils.Vars.new('input', name), instruction = instruction, name = name, }, Mode ) -- define the exit flag self.supress_exit = supress_exit or false -- if the user provided keymaps if type(instruction) == globals.TYPE_TBL then -- create a timer to perform actions with. self.flush_input_timer = vim.loop.new_timer() -- determine if a default `Help` should be created. if not self.instruction[HELP] then --- @diagnostic disable-next-line:param-type-mismatch we checked that `instruction` is a table above self.help = utils.Help.new(self.instruction, 'KEY MAP') end self.input_bytes = setmetatable({}, InputBytes) -- build the parse tree. --- @diagnostic disable-next-line:param-type-mismatch already checked `self.instruction` != `table` self.mappings = ParseTable.new(self.instruction) -- create a table for mode-specific data. self.popups = require('libmodal/src/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 end return self end }