You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
6.7 KiB
Lua
237 lines
6.7 KiB
Lua
local globals = require 'libmodal/src/globals'
|
|
local ParseTable = require 'libmodal/src/collections/ParseTable'
|
|
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|nil
|
|
--- @field private indicator libmodal.utils.Indicator
|
|
--- @field private input libmodal.utils.Vars
|
|
--- @field private instruction function|table<string, function|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 function|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()
|
|
-- The command was a table, meaning that it MIGHT match.
|
|
elseif command_type == globals.TYPE_TBL
|
|
and globals.is_true(self.timeouts_enabled)
|
|
then
|
|
-- start the timer
|
|
self.flush_input_timer: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.execute_instruction(cmd[ParseTable.CR])
|
|
end
|
|
-- clear input
|
|
self.input_bytes:clear()
|
|
self.popups:peek():refresh(self.input_bytes)
|
|
end)
|
|
)
|
|
|
|
-- The command was an actual vim command.
|
|
else
|
|
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 continue_mode = true
|
|
while continue_mode do
|
|
-- Try (using pcall) to use the mode.
|
|
local no_errors, mode_result = pcall(self.get_user_input, self)
|
|
|
|
-- If there were errors, handle them.
|
|
if not no_errors then
|
|
utils.show_error(mode_result)
|
|
continue_mode = false
|
|
else
|
|
continue_mode = mode_result
|
|
end
|
|
end
|
|
|
|
self:tear_down()
|
|
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.nvim_lecho(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.nvim_redraw()
|
|
end
|
|
|
|
return
|
|
{
|
|
--- Create a new mode.
|
|
--- @param name string the name of the mode.
|
|
--- @param instruction function|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.mode(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
|
|
self.help = utils.Help.new(self.instruction, 'KEY MAP')
|
|
end
|
|
|
|
self.input_bytes = setmetatable({}, InputBytes)
|
|
|
|
-- 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 = 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
|
|
}
|