--[[ /* * IMPORTS */ --]] local classes = require('libmodal/src/classes') local globals = require('libmodal/src/globals') local ParseTable = require('libmodal/src/collections/ParseTable') local utils = require('libmodal/src/utils') local Vars = require('libmodal/src/Vars') local vim = vim local api = vim.api --[[ /* * MODULE */ --]] local Mode = {['TYPE'] = 'libmodal-mode'} local _HELP = '?' local _TIMEOUT = { ['CHAR'] = 'ΓΈ', ['LEN'] = api.nvim_get_option('timeoutlen'), ['SEND'] = function(__self) api.nvim_feedkeys(__self.CHAR, 'nt', false) end } _TIMEOUT.NR = string.byte(_TIMEOUT.CHAR) --[[ /* * META `_metaMode` */ --]] local _metaMode = classes.new(Mode.TYPE) local _metaInputBytes = classes.new(nil, { ['clear'] = function(__self) for i, _ in ipairs(__self) do __self[i] = nil end end }) classes = nil ----------------------------------------------------------- --[[ SUMMARY: * Execute some `selection` according to a set of determined logic. ]] --[[ REMARKS: * Only provides logic for when `self._instruction` is a table of commands. ]] --[[ PARAMS: * `selection` => The instruction that is desired to be executed. ]] ----------------------------------------------------------- function _metaMode._commandTableExecute(instruction) if type(instruction) == globals.TYPE_FUNC then instruction() else api.nvim_command(instruction) end end ----------------------------------------------- --[[ SUMMARY: * Parse `self.mappings` and see if there is any command to execute. ]] ----------------------------------------------- function _metaMode:_checkInputForMapping() -- Stop any running timers self._flushInputTimer:stop() -- Append the latest input to the locally stored input history. local inputBytes = self.inputBytes inputBytes[#inputBytes + 1] = self.input:nvimGet() -- Get the command based on the users input. local cmd = self.mappings:get(inputBytes) -- Get the type of the command. local commandType = type(cmd) -- if there was no matching command if not cmd then if #inputBytes < 2 and inputBytes[1] == string.byte(_HELP) then self._help:show() end inputBytes:clear() -- The command was a table, meaning that it MIGHT match. elseif commandType == globals.TYPE_TBL and globals.is_true(self._timeouts.enabled) then -- start the timer self._flushInputTimer:start( _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._commandTableExecute(cmd[ParseTable.CR]) end -- clear input inputBytes:clear() self._popups:peek():refresh(inputBytes) end) ) -- The command was an actual vim command. else self._commandTableExecute(cmd) inputBytes:clear() end self._popups:peek():refresh(inputBytes) end -------------------------- --[[ SUMMARY: * Enter `self`'s mode. ]] -------------------------- function _metaMode: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(require('libmodal/src/collections/Popup').new()) end --[[ MODE LOOP. ]] local continueMode = true while continueMode do -- Try (using pcall) to use the mode. local noErrors, modeResult = pcall(self._inputLoop, self) -- If there were errors, handle them. if not noErrors then utils.show_error(modeResult) continueMode = true else continueMode = modeResult end end self:_tearDown() end ---------------------------------- --[[ SUMMARY: * Set the initial values used for parsing user input as combos. ]] ---------------------------------- function _metaMode:_initMappings() -- Create a timer to perform actions with. self._flushInputTimer = vim.loop.new_timer() -- Determine if a default `Help` should be created. if not self._instruction[_HELP] then self._help = utils.Help.new(self._instruction, 'KEY MAP') end self.inputBytes = setmetatable({}, _metaInputBytes) -- Build the parse tree. 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 = Vars.new('timeouts', self._name) -- Read the correct timeout variable. if utils.api.nvim_exists('g', self._timeouts:name()) then self._timeouts.enabled = self._timeouts:nvimGet() else self._timeouts.enabled = Vars.libmodalTimeouts end end ------------------------------- --[[ SUMMARY: * Loop an initialized `mode`. ]] --[[ RETURNS: * `boolean` => whether or not the mode should continue ]] ------------------------------- function _metaMode:_inputLoop() -- If the mode is not handling exit events automatically and the global exit var is true. if self.exit.supress and globals.is_true(self.exit:nvimGet()) then return false end -- Echo the indicator. utils.api.nvim_lecho(self.indicator) -- Capture input. local userInput = utils.api.nvim_input() -- Return if there was a timeout event. if userInput == _TIMEOUT.NR then return true end -- Set the global input variable to the new input. self.input:nvimSet(userInput) if not self.exit.supress and userInput == 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 instructionType = type(self._instruction) if instructionType == globals.TYPE_TBL then -- The second argument was a dict. Parse it. self:_checkInputForMapping() elseif instructionType == globals.TYPE_STR and vim.fn then -- It is the name of a VimL function. This only works in Neovim 0.5+. vim.fn[self._instruction]() else -- the second argument was a function; execute it. self._instruction() end end return true end ------------------------------ --[[ SUMMARY: * Remove variables used for a mode. ]] ------------------------------ function _metaMode:_tearDown() if type(self._instruction) == globals.TYPE_TBL then self._flushInputTimer:stop() self.inputBytes = nil self._popups:pop():close() end self._winState:restore() utils.api.nvim_redraw() end --[[ /* * CLASS `Mode` */ --]] ----------------------------------------- --[[ SUMMARY: * Enter a mode. ]] --[[ PARAMS: * `name` => the mode name. * `instruction` => the mode callback, or mode combo table. * `...` => optional exit supresion flag. ]] ----------------------------------------- function Mode.new(name, instruction, ...) name = vim.trim(name) -- Inherit the metatable. local self = setmetatable( { ['exit'] = Vars.new('exit', name), ['indicator'] = require('libmodal/src/Indicator').mode(name), ['input'] = Vars.new('input', name), ['_instruction'] = instruction, ['_name'] = name, ['_winState'] = utils.WindowState.new(), }, _metaMode ) -- Define the exit flag self.exit.supress = (function(optionalValue) if optionalValue then return globals.is_true(optionalValue) else return false end end)(unpack({...})) -- Define other "session" variables. -- Determine whether a callback was specified, or a combo table. if type(instruction) == globals.TYPE_TBL then self:_initMappings() end return self end --[[ / * PUBLICIZE MODULE */ --]] return Mode