|
|
|
@ -16,79 +16,45 @@ local api = utils.api
|
|
|
|
|
*/
|
|
|
|
|
--]]
|
|
|
|
|
|
|
|
|
|
-- Public interface for this module.
|
|
|
|
|
local Mode = {}
|
|
|
|
|
|
|
|
|
|
-- Private class.
|
|
|
|
|
local _modeMetaTable = {}
|
|
|
|
|
Mode.ParseTable = require('libmodal/src/mode/ParseTable')
|
|
|
|
|
Mode.Popup = require('libmodal/src/Mode/Popup')
|
|
|
|
|
|
|
|
|
|
_modeMetaTable.ParseTable = require('libmodal/src/mode/ParseTable')
|
|
|
|
|
local _metaMode = {}
|
|
|
|
|
|
|
|
|
|
local _HELP = '?'
|
|
|
|
|
local _TIMEOUT = {
|
|
|
|
|
CHAR = 'ø',
|
|
|
|
|
NR = string.byte(_TIMEOUT.CHAR),
|
|
|
|
|
LEN = api.nvim_get_option('timeoutlen'),
|
|
|
|
|
SEND = function(__self)
|
|
|
|
|
['CHAR'] = 'ø',
|
|
|
|
|
['LEN'] = api.nvim_get_option('timeoutlen'),
|
|
|
|
|
['SEND'] = function(__self)
|
|
|
|
|
api.nvim_feedkeys(__self.CHAR, '', false)
|
|
|
|
|
end
|
|
|
|
|
}
|
|
|
|
|
_TIMEOUT.NR = string.byte(_TIMEOUT.CHAR)
|
|
|
|
|
|
|
|
|
|
--[[
|
|
|
|
|
/*
|
|
|
|
|
* META `_modeMetaTable`
|
|
|
|
|
* META `_metaMode`
|
|
|
|
|
*/
|
|
|
|
|
--]]
|
|
|
|
|
|
|
|
|
|
----------------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Reset libmodal's internal counter of user input to default.
|
|
|
|
|
]]
|
|
|
|
|
----------------------------------------
|
|
|
|
|
function _modeMetaTable:clearInputBytes()
|
|
|
|
|
self._inputBytes = {}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-----------------------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Update the floating window with the latest user input.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `modeName` => the name of the mode.
|
|
|
|
|
* Parse `self._mappings` and see if there is any command to execute.
|
|
|
|
|
]]
|
|
|
|
|
-----------------------------------------------
|
|
|
|
|
function _modeMetaTable:_updateFloatingWindow()
|
|
|
|
|
local inputChars = {}
|
|
|
|
|
for _, byte in ipairs(self._inputBytes) do
|
|
|
|
|
inputChars[#inputChars + 1] = string.char(byte)
|
|
|
|
|
end
|
|
|
|
|
api.nvim_buf_set_lines(
|
|
|
|
|
self._popupBuffer,
|
|
|
|
|
0, 1, true, {table.concat(inputChars)}
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--------------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Parse the `comboDict` and see if there is any command to execute.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `modeName` => the name of the mode that is currently active.
|
|
|
|
|
]]
|
|
|
|
|
--------------------------------------
|
|
|
|
|
function _modeMetaTable:_comboSelect()
|
|
|
|
|
function _metaMode:_checkInputForMapping()
|
|
|
|
|
-- Stop any running timers
|
|
|
|
|
if self._flushInputTimer then
|
|
|
|
|
self._flushInputTimer:stop()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Append the latest input to the locally stored input history.
|
|
|
|
|
self._inputBytes[#self._inputBytes + 1] = Vars.nvim_get(
|
|
|
|
|
self._inputBytes, self._modeName
|
|
|
|
|
)
|
|
|
|
|
self._input.bytes[#self._input.bytes + 1] = self._input:nvimGet(self._name)
|
|
|
|
|
|
|
|
|
|
-- Get the command based on the users input.
|
|
|
|
|
local cmd = self._keybindings:get(self._inputBytes)
|
|
|
|
|
local cmd = self._mappings:get(self._input.bytes)
|
|
|
|
|
|
|
|
|
|
-- Get the type of the command.
|
|
|
|
|
local commandType = type(cmd)
|
|
|
|
@ -96,13 +62,13 @@ function _modeMetaTable:_comboSelect()
|
|
|
|
|
|
|
|
|
|
-- if there was no matching command
|
|
|
|
|
if cmd == false then
|
|
|
|
|
if #self._inputBytes < 2 and self._inputBytes[1] == string.byte(_HELP) then
|
|
|
|
|
if #self._input.bytes < 2 and self._input.bytes[1] == string.byte(_HELP) then
|
|
|
|
|
self._help:show()
|
|
|
|
|
end
|
|
|
|
|
clearInputBytes = true
|
|
|
|
|
self._input:clear()
|
|
|
|
|
-- The command was a table, meaning that it MIGHT match.
|
|
|
|
|
elseif commandType == globals.TYPE_TBL
|
|
|
|
|
and globals.isTrue(self._timeoutsEnabled)
|
|
|
|
|
and globals.isTrue(self._timeouts.enabled)
|
|
|
|
|
then
|
|
|
|
|
-- Create a new timer
|
|
|
|
|
|
|
|
|
@ -112,120 +78,111 @@ function _modeMetaTable:_comboSelect()
|
|
|
|
|
-- Send input to interrupt a blocking `getchar`
|
|
|
|
|
_TIMEOUT:SEND()
|
|
|
|
|
-- if there is a command, execute it.
|
|
|
|
|
if cmd[self.ParseTable.CR] then
|
|
|
|
|
api.nvim_command(cmd[self.ParseTable.CR])
|
|
|
|
|
if cmd[Mode.ParseTable.CR] then
|
|
|
|
|
api.nvim_command(cmd[Mode.ParseTable.CR])
|
|
|
|
|
end
|
|
|
|
|
-- clear input
|
|
|
|
|
_clearInputBytes(modeName)
|
|
|
|
|
_updateFloatingWindow(modeName)
|
|
|
|
|
self._input:clear()
|
|
|
|
|
self._popup:refresh(self._input.bytes)
|
|
|
|
|
end)
|
|
|
|
|
)
|
|
|
|
|
-- The command was an actual vim command.
|
|
|
|
|
else
|
|
|
|
|
api.nvim_command(cmd)
|
|
|
|
|
clearInputBytes = true
|
|
|
|
|
self._input:clear()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if clearInputBytes then
|
|
|
|
|
self:_clearInputBytes()
|
|
|
|
|
end
|
|
|
|
|
self:_updateFloatingWindow()
|
|
|
|
|
self._popup:refresh(self._input.bytes)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
------------------------------------------------
|
|
|
|
|
--------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Set the initial values used for parsing user input as combos.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `modeName` => the name of the mode being initialized.
|
|
|
|
|
* `comboTable` => the table of combos being initialized.
|
|
|
|
|
* Enter `self`'s mode.
|
|
|
|
|
]]
|
|
|
|
|
------------------------------------------------
|
|
|
|
|
-- TODO
|
|
|
|
|
local function _initCombos(modeName, comboTable)
|
|
|
|
|
-- Placeholder for timeout value.
|
|
|
|
|
local timeoutsEnabled = nil
|
|
|
|
|
|
|
|
|
|
-- Read the correct timeout variable.
|
|
|
|
|
if api.nvim_exists('g', vars.timeouts:name(modeName)) then timeoutsEnabled =
|
|
|
|
|
vars.nvim_get(vars.timeouts, modeName)
|
|
|
|
|
else timeoutsEnabled =
|
|
|
|
|
vars.libmodalTimeouts
|
|
|
|
|
--------------------------
|
|
|
|
|
function _metaMode:enter()
|
|
|
|
|
if self._instruction == globals.TYPE_TBL then
|
|
|
|
|
-- Create a timer
|
|
|
|
|
self._flushInputTimer = vim.loop.new_timer()
|
|
|
|
|
|
|
|
|
|
-- Initialize the input history variable.
|
|
|
|
|
self._input = {
|
|
|
|
|
['bytes'] = {},
|
|
|
|
|
----------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Clear the self.bytes table.
|
|
|
|
|
]]
|
|
|
|
|
----------------------------
|
|
|
|
|
['clear'] = function(__self)
|
|
|
|
|
__self.bytes = {}
|
|
|
|
|
end
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- create a floating window
|
|
|
|
|
self._popup = Mode.Popup.new()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Assign the timeout variable according to `timeoutsEnabled`
|
|
|
|
|
self._timeoutsEnabled = timeoutsEnabled
|
|
|
|
|
|
|
|
|
|
-- create a floating window
|
|
|
|
|
local buf = api.nvim_create_buf(false, true)
|
|
|
|
|
vars.buffers.instances[modeName] = buf
|
|
|
|
|
self._popupWindow = api.nvim_call_function('libmodal#_winOpen', {buf})
|
|
|
|
|
--[[ MODE LOOP. ]]
|
|
|
|
|
local continueMode = true
|
|
|
|
|
while continueMode == true do
|
|
|
|
|
-- Try (using pcall) to use the mode.
|
|
|
|
|
local noErrors, modeResult = pcall(self._inputLoop, self)
|
|
|
|
|
|
|
|
|
|
-- Determine if a default `Help` should be created.
|
|
|
|
|
if not comboTable[_HELP] then
|
|
|
|
|
vars.help.instances[modeName] = utils.Help.new(comboTable, 'KEY MAP')
|
|
|
|
|
-- If there were errors, handle them.
|
|
|
|
|
if noErrors == false then
|
|
|
|
|
utils.showError(modeResult)
|
|
|
|
|
continueMode = false
|
|
|
|
|
else
|
|
|
|
|
continueMode = modeResult
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Build the parse tree.
|
|
|
|
|
vars.combos.instances[modeName] = mode.ParseTable.new(comboTable)
|
|
|
|
|
|
|
|
|
|
-- Create a timer
|
|
|
|
|
self._flushInputTimer = vim.loop.new_timer()
|
|
|
|
|
|
|
|
|
|
-- Initialize the input history variable.
|
|
|
|
|
_clearInputBytes(modeName)
|
|
|
|
|
self:_tearDown()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-----------------------------------------------------
|
|
|
|
|
----------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Remove variables used for a mode.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `modeName` => the name of the mode.
|
|
|
|
|
* `self._winState` => the window state prior to mode activation.
|
|
|
|
|
* Set the initial values used for parsing user input as combos.
|
|
|
|
|
]]
|
|
|
|
|
-----------------------------------------------------
|
|
|
|
|
function _modeMetaTable:_deconstruct()
|
|
|
|
|
if self._flushInputTimer:info()['repeat'] ~= 0 then
|
|
|
|
|
self._flushInputTimer:stop()
|
|
|
|
|
end
|
|
|
|
|
----------------------------------
|
|
|
|
|
function _metaMode:_initMappings()
|
|
|
|
|
-- Create a variable for whether or not timeouts are enabled.
|
|
|
|
|
self._timeouts = Vars.new('timeouts')
|
|
|
|
|
|
|
|
|
|
if self._popupWindow then
|
|
|
|
|
api.nvim_win_close(self._popupWindow, false)
|
|
|
|
|
-- Read the correct timeout variable.
|
|
|
|
|
if api.nvim_exists('g', self._timeouts:name(modeName)) then self._timeouts.enabled =
|
|
|
|
|
self._timeouts:nvimGet(self._name)
|
|
|
|
|
else self._timeouts.enabled =
|
|
|
|
|
Vars.libmodalTimeouts
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
self._winState:restore()
|
|
|
|
|
|
|
|
|
|
for k, _ in pairs(self) do
|
|
|
|
|
self[k] = nil
|
|
|
|
|
-- Determine if a default `Help` should be created.
|
|
|
|
|
if not self._instruction[_HELP] then
|
|
|
|
|
self._help = utils.Help.new(self._instruction, 'KEY MAP')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
api.nvim_command("mode | echo '' | call garbagecollect()")
|
|
|
|
|
-- Build the parse tree.
|
|
|
|
|
self._mappings = Mode.ParseTable.new(self._instruction)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
------------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Loop an initialized `mode`.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `handleExitEvents` => whether or not to automatically exit on `<Esc>` press.
|
|
|
|
|
* `indicator` => the indicator for the mode.
|
|
|
|
|
* `modeInstruction` => the instructions for the mode.
|
|
|
|
|
* `modeName` => the name of the `mode`.
|
|
|
|
|
]]
|
|
|
|
|
--[[ RETURNS:
|
|
|
|
|
* `boolean` => whether or not the mode should continue
|
|
|
|
|
]]
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
-- TODO
|
|
|
|
|
local function _modeLoop(handleExitEvents, indicator, modeInstruction, modeName)
|
|
|
|
|
------------------------------------
|
|
|
|
|
function _metaMode:_inputLoop()
|
|
|
|
|
-- If the mode is not handling exit events automatically and the global exit var is true.
|
|
|
|
|
if not handleExitEvents and globals.isTrue(
|
|
|
|
|
vars.nvim_get(vars.exit, modeName)
|
|
|
|
|
) then return false end
|
|
|
|
|
if self._exit.supress
|
|
|
|
|
and globals.isTrue(self._exit:nvimGet(self._name))
|
|
|
|
|
then
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Echo the indicator.
|
|
|
|
|
api.nvim_lecho(indicator)
|
|
|
|
|
api.nvim_lecho(self._indicator)
|
|
|
|
|
|
|
|
|
|
-- Capture input.
|
|
|
|
|
local userInput = api.nvim_input()
|
|
|
|
@ -236,72 +193,88 @@ local function _modeLoop(handleExitEvents, indicator, modeInstruction, modeName)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- Set the global input variable to the new input.
|
|
|
|
|
vars.nvim_set(vars.input, modeName, userInput)
|
|
|
|
|
self._input:nvimSet(self._name, userInput)
|
|
|
|
|
|
|
|
|
|
-- Make sure that the user doesn't want to exit.
|
|
|
|
|
if handleExitEvents and userInput == globals.ESC_NR then
|
|
|
|
|
return false
|
|
|
|
|
if not self._exit.supress
|
|
|
|
|
and userInput == globals.ESC_NR then return false
|
|
|
|
|
-- If the second argument was a dict, parse it.
|
|
|
|
|
elseif type(modeInstruction) == globals.TYPE_TBL then
|
|
|
|
|
_comboSelect(modeName)
|
|
|
|
|
-- If the second argument was a function, execute it.
|
|
|
|
|
else modeInstruction() end
|
|
|
|
|
elseif type(self._instruction) == globals.TYPE_TBL then
|
|
|
|
|
self:_checkInputForMapping()
|
|
|
|
|
else -- the second argument was a function; execute it.
|
|
|
|
|
self._instruction()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
---------------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Remove variables used for a mode.
|
|
|
|
|
]]
|
|
|
|
|
---------------------------------
|
|
|
|
|
function _metaMode:_tearDown()
|
|
|
|
|
if self._instruction == globals.TYPE_TBL then
|
|
|
|
|
self._flushInputTimer:stop()
|
|
|
|
|
self._flushInputTimer = nil
|
|
|
|
|
|
|
|
|
|
for k, _ in pairs(self._input) do
|
|
|
|
|
self._input[k] = nil
|
|
|
|
|
end
|
|
|
|
|
self._input = nil
|
|
|
|
|
|
|
|
|
|
self._popup:close()
|
|
|
|
|
self._popup = nil
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
self._winState:restore()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--[[
|
|
|
|
|
/*
|
|
|
|
|
* CLASS `Mode`
|
|
|
|
|
*/
|
|
|
|
|
--]]
|
|
|
|
|
|
|
|
|
|
------------------------
|
|
|
|
|
--[[ SUMMARY:
|
|
|
|
|
* Enter a mode.
|
|
|
|
|
]]
|
|
|
|
|
--[[ PARAMS:
|
|
|
|
|
* `args[1]` => the mode name.
|
|
|
|
|
* `args[2]` => the mode callback, or mode combo table.
|
|
|
|
|
* `args[3]` => optional exit supresion flag.
|
|
|
|
|
* `name` => the mode name.
|
|
|
|
|
* `instruction` => the mode callback, or mode combo table.
|
|
|
|
|
* `...` => optional exit supresion flag.
|
|
|
|
|
]]
|
|
|
|
|
------------------------
|
|
|
|
|
-- TODO
|
|
|
|
|
function _modeMetaTable:enter(...)
|
|
|
|
|
local args = {...}
|
|
|
|
|
|
|
|
|
|
--[[ SETUP. ]]
|
|
|
|
|
|
|
|
|
|
-- Create the indicator for the mode.
|
|
|
|
|
local indicator = utils.Indicator.mode(args[1])
|
|
|
|
|
|
|
|
|
|
-- Grab the state of the window.
|
|
|
|
|
self._winState = utils.WindowState.new()
|
|
|
|
|
|
|
|
|
|
-- Convert the name into one that can be used for variables.
|
|
|
|
|
local modeName = string.lower(args[1])
|
|
|
|
|
function Mode.new(name, instruction, ...)
|
|
|
|
|
-- Inherit the metatable.
|
|
|
|
|
self = {}
|
|
|
|
|
setmetatable(self, _metaMode)
|
|
|
|
|
self.__index = self
|
|
|
|
|
|
|
|
|
|
-- Define the exit flag
|
|
|
|
|
self._exit = Vars.new('exit')
|
|
|
|
|
self._exit.supress = (function(optionalValue)
|
|
|
|
|
if #optionalValue > 0 then
|
|
|
|
|
return globals.isTrue(optionalValue)
|
|
|
|
|
else
|
|
|
|
|
return false
|
|
|
|
|
end
|
|
|
|
|
end)(unpack({...}))
|
|
|
|
|
|
|
|
|
|
-- Determine whether or not this function should handle exiting automatically.
|
|
|
|
|
local handleExitEvents = true
|
|
|
|
|
if #args > 2 then
|
|
|
|
|
handleExitEvents = globals.isFalse(args[3])
|
|
|
|
|
end
|
|
|
|
|
-- Define other "session" variables.
|
|
|
|
|
self._indicator = utils.Indicator.mode(name)
|
|
|
|
|
self._instruction = instruction
|
|
|
|
|
self._name = name
|
|
|
|
|
self._winState = utils.WindowState.new()
|
|
|
|
|
|
|
|
|
|
-- Determine whether a callback was specified, or a combo table.
|
|
|
|
|
if type(args[2]) == globals.TYPE_TBL then
|
|
|
|
|
_initCombos(modeName, args[2])
|
|
|
|
|
if type(instruction) == globals.TYPE_TBL then
|
|
|
|
|
self:_initMappings()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--[[ MODE LOOP. ]]
|
|
|
|
|
local continueMode = true
|
|
|
|
|
while continueMode == true do
|
|
|
|
|
-- Try (using pcall) to use the mode.
|
|
|
|
|
local noErrors = true
|
|
|
|
|
noErrors, continueMode = pcall(_modeLoop,
|
|
|
|
|
handleExitEvents, indicator, args[2], modeName
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
-- If there were errors, handle them.
|
|
|
|
|
if noErrors == false then
|
|
|
|
|
utils.showError(continueMode)
|
|
|
|
|
continueMode = false
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
end
|
|
|
|
|
return self
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--[[
|
|
|
|
|