merge!: cleanup the plugin

This was my first plugin. Looking back on it, it was full of bad
design decisions (and I have elected to keep most of the user-facing
ones for backwards-compatability). This merge tries to uncrustify the
plugin by standardizing the documentation, removing much unecessary
code, reorganizing the internal structures, and removing references to
my own made-up terminology.
pull/14/head
Iron-E 2 years ago
parent 822614e7e8
commit 49a02ad692
No known key found for this signature in database
GPG Key ID: 19B71B7B7B021D22

@ -3,7 +3,7 @@
" argument specifying how many times to run (runs until exiting by default).
" PARAMS:
" * `a:1` => `modeName`
" * `a:2` => `modeCallback` OR `modeCombos`
" * `a:2` => `modeCallback` OR `modeKeymaps`
" * `a:3` => `supressExit`
function! libmodal#Enter(...) abort
call libmodal#_lua('mode', a:000)

File diff suppressed because it is too large Load Diff

@ -112,8 +112,8 @@ VARIABLES *libmodal-usage-variable
FUNCTIONS *libmodal-usage-functions*
*libmodal-mode* *libmodal#Enter()* *libmodal.mode.enter()*
`libmodal.mode`.enter({name}, {instruction} [, {supressExit}])
`libmodal`#Enter({name}, {instruction} [, {supressExit}])
`libmodal.mode`.enter({name}, {instruction} [, {supress_exit}])
`libmodal`#Enter({name}, {instruction} [, {supress_exit}])
Enter a new |vim-mode| using {instruction} to determine what actions will
be taken upon specific user inputs.
@ -182,8 +182,8 @@ FUNCTIONS *libmodal-usage-function
- If `g:libmodalTimeouts` is enabled, then user input will be
subjected to the |timeoutlen|.
{supressExit} Whether or not to automatically exit the mode upon an
<Esc> press.
{supress_exit} Whether or not to automatically exit the mode upon an
<Esc> press.
- If |v:false|/`false`, then <Esc> is automatically mapped to
exiting.
@ -198,7 +198,8 @@ FUNCTIONS *libmodal-usage-function
|libmodal-examples-mode| For examples of this function.
`libmodal.layer`.enter({keymap}) *libmodal-layer* *libmodal.layer.enter()*
*libmodal-layer* *libmodal.layer.enter()*
`libmodal.layer`.enter({keymap} [, {exit_char}])
While a |libmodal-mode| ignores behavior that has not been explicitly
defined, a |libmodal-layer| allows unrecognized |input| to be passed back
@ -221,19 +222,23 @@ FUNCTIONS *libmodal-usage-function
}
< Where {mode}, {lhs}, {rhs}, and {opts} are the same as in
|nvim_set_keymap()|
|vim.keymap.set()| (except you should not use multiple `<mode>` at
one time, despite |vim.keymap.set()| supporting it).
{exit_char} The character used to exit the layer.
Return: ~
- The `function` used to undo changes made by the layer.
- The `function` used to undo changes made by the layer, or `nil` if
{exit_char} is provided.
See also: ~
|libmodal-examples-layers| For an example.
|nvim_set_keymap()| For more information about {keymap}.
*libmodal-prompt* *libmodal#Prompt()* *libmodal.prompt.enter()*
`libmodal.prompt`.enter({name}, {instruction} [, {completions}, {supressExit}])
`libmodal`#Prompt({name}, {instruction} [, {completions}, {supressExit}])
*libmodal-prompt* *libmodal#Prompt()* *libmodal.prompt.enter()*
`libmodal.prompt`.enter({name}, {instruction} [, {completions}])
`libmodal`#Prompt({name}, {instruction} [, {completions}])
Besides accepting user input like keys in |Normal-mode|, |libmodal| is
also capable of prompting the user for |input| like |Cmdline-mode|. To
@ -296,15 +301,8 @@ FUNCTIONS *libmodal-usage-function
Note: If no `help` command is defined, one will be created
automatically.
{supressExit} Whether or not to automatically exit the mode upon an
<Esc> press.
- If |v:false|/`false`, then <Esc> is automatically mapped to
exiting.
- If |v:true|/`true`, then <Esc> is ignored unless specified by
the user. In such cases, the user should set the
`g:`{name}`ModeExit` variable to `true` when exiting is
desired. See |libmodal-examples-supress-exit|.
Note: The user may set the `g:`{name}`ModeExit` variable to
`true` at any time to prematurely exit.
See also: ~
@ -314,238 +312,9 @@ FUNCTIONS *libmodal-usage-function
================================================================================
3. Examples *libmodal-examples*
Below are examples written in |Lua| to help show how specific features of
|libmodal| may be implemented. In each example, the name of the mode is
defined as "FOO". Additionally, each category of example has one example for
both `function` and `table` {instruction}s.
The source code can be either copied from here or downloaded from the
repository's `examples/lua` folder. Assuming |libmodal| is installed, they can
all be tested using the |luafile| |command|.
See: |libmodal-usage|, |libmodal-use-case|, |lua-require-example|.
--------------------------------------------------------------------------------
MODES *libmodal-examples-modes*
Using a callback `function`: >
local libmodal = require('libmodal')
local fooModeInputHistory = {}
local function clearHistory(indexToCheck)
if #fooModeInputHistory >= indexToCheck then
fooModeInputHistory = {}
end
end
function fooMode()
fooModeInputHistory[#fooModeInputHistory + 1] = string.char(
vim.g.fooModeInput
)
local index = 1
if fooModeInputHistory[1] == 'z' then
if fooModeInputHistory[2] == 'f' then
if fooModeInputHistory[3] == 'o' then
vim.api.nvim_command "echom 'It works!'"
else index = 3 end
else index = 2 end
end
clearHistory(index)
end
libmodal.mode.enter('FOO', fooMode)
<
using a |key-mapping| `dict`: >
let s:barModeCombos = {
\ 'zf': 'split',
\ 'zfo': 'vsplit',
\ 'zfc': 'tabnew'
\}
call libmodal#Enter('BAR', s:barModeCombos)
<
Using a |key-mapping| `table`: >
local libmodal = require('libmodal')
local fooModeCombos = {
zf = 'split',
zfo = 'vsplit',
zfc = 'tabnew'
}
libmodal.mode.enter('FOO', fooModeCombos)
<
Exit Supression ~
*libmodal-examples-supress-exit*
Using a callback `function`: >
local libmodal = require('libmodal')
function fooMode()
local userInput = string.char(
vim.g.fooModeInput
)
if userInput == '' then
vim.api.nvim_command "echom 'You cant leave using <Esc>.'"
elseif userInput == 'q' then
vim.g.fooModeExit = true
end
end
vim.g.fooModeExit = false
libmodal.mode.enter('FOO', fooMode, true)
<
Using a |key-mapping| `dict`: >
let s:barModeCombos = {
\ '': 'echom "You cant exit using escape."',
\ 'q': 'let g:barModeExit = 1'
\}
let g:barModeExit = 0
call libmodal#Enter('BAR', s:barModeCombos, 1)
<
Using a |key-mapping| `table`: >
local libmodal = require('libmodal')
local fooModeCombos = {
[''] = 'echom "You cant exit using escape."',
q = 'let g:fooModeExit = 1'
}
vim.g.fooModeExit = false
libmodal.mode.enter('FOO', fooModeCombos, true)
<
Submodes ~
*libmodal-examples-submodes*
Using a callback `function`: >
local libmodal = require('libmodal')
local fooModeRecurse = 0
function fooMode()
local userInput = string.char(vim.g[
'foo'..tostring(fooModeRecurse)..'ModeInput'
])
if userInput == 'z' then
fooModeRecurse = fooModeRecurse + 1
enter()
fooModeRecurse = fooModeRecurse - 1
end
end
function enter()
libmodal.mode.enter('FOO' .. fooModeRecurse, fooMode)
end
enter()
<
Using a |key-mapping| `table`: >
let s:barModeRecurse = 0
let s:barModeCombos = {
\ 'z': 'BarModeEnter',
\}
function! s:BarMode()
let s:barModeRecurse += 1
call libmodal#Enter('BAR' . s:barModeRecurse, s:barModeCombos)
let s:barModeRecurse -= 1
endfunction
command! BarModeEnter call s:BarMode()
execute 'BarModeEnter'
<
Using a |key-mapping| `table`: >
local libmodal = require('libmodal')
local fooModeRecurse = 0
local fooModeCombos = {
z = 'lua fooMode()'
}
function fooMode()
fooModeRecurse = fooModeRecurse + 1
libmodal.mode.enter('FOO' .. fooModeRecurse, fooModeCombos)
fooModeRecurse = fooModeRecurse - 1
end
See the official examples at the link below:
fooMode()
<
--------------------------------------------------------------------------------
LAYERS *libmodal-examples-layers*
>
local libmodal = require('libmodal')
-- save the exit function
local exitFunc = libmodal.layer.enter({
n = { -- normal mode
gg = { -- remap `gg`
rhs = 'G', -- map it to `G`
noremap = true -- don't remap
},
G = { -- remap `G`
rhs = 'gg', -- map it to `gg`
noremap = true -- don't remap
}
}
})
-- exit the mode in five seconds
vim.loop.new_timer():start(5000, 0,
vim.schedule_wrap(exitFunc)
)
<
--------------------------------------------------------------------------------
PROMPTS *libmodal-examples-prompts*
Using a callback `function`: >
local libmodal = require('libmodal')
local commandList = {'new', 'close', 'last'}
function fooMode()
local userInput = vim.g.fooModeInput
if userInput == 'new' then
vim.api.nvim_command 'tabnew'
elseif userInput == 'close' then
vim.api.nvim_command 'tabclose'
elseif userInput == 'last' then
vim.api.nvim_command 'tablast'
end
end
libmodal.prompt.enter('FOO', fooMode, commandList)
<
Using a |command| `dict`: >
let s:commands = {
\ 'new': 'tabnew',
\ 'close': 'tabclose',
\ 'last': 'tablast'
\}
call libmodal#Prompt('TAB', s:commands)
<
Using a |command| `table`: >
local libmodal = require('libmodal')
local commands = {
new = 'tabnew',
close = 'tabclose',
last = 'tablast'
}
libmodal.prompt.enter('BAR', commands)
<
https://github.com/Iron-E/nvim-libmodal/tree/master/examples
================================================================================
4. Configuration *libmodal-configuration*

@ -1,9 +0,0 @@
" Register key combos for splitting windows and then closing windows
let s:barModeCombos = {
\ 'zf': 'split',
\ 'zfo': 'vsplit',
\ 'zfc': 'q'
\}
" Enter the mode using the key combos.
call libmodal#Enter('BAR', s:barModeCombos)

@ -2,14 +2,14 @@
let s:barModeRecurse = 0
" Register 'z' as the map for recursing further (by calling the BarMode function again).
let s:barModeCombos = {
let s:barModeKeymaps = {
\ 'z': 'BarModeEnter',
\}
" define the BarMode() function which is called whenever the user presses 'z'
function! s:BarMode()
let s:barModeRecurse += 1
call libmodal#Enter('BAR' . s:barModeRecurse, s:barModeCombos)
call libmodal#Enter('BAR' . s:barModeRecurse, s:barModeKeymaps)
let s:barModeRecurse -= 1
endfunction

@ -1,5 +1,5 @@
" Register key commands and what they do.
let s:barModeCombos = {
let s:barModeKeymaps = {
\ '': 'echom "You cant exit using escape."',
\ 'q': 'let g:barModeExit = 1'
\}
@ -7,5 +7,5 @@ let s:barModeCombos = {
" Tell the mode not to exit automatically.
let g:barModeExit = 0
" Enter the mode using the key combos created before.
call libmodal#Enter('BAR', s:barModeCombos, 1)
" Enter the mode using the keymaps created before.
call libmodal#Enter('BAR', s:barModeKeymaps, 1)

@ -0,0 +1,9 @@
" Register keymaps for splitting windows and then closing windows
let s:barModeKeymaps = {
\ 'zf': 'split',
\ 'zfo': 'vsplit',
\ 'zfc': 'q'
\}
" Enter the mode using the keymaps.
call libmodal#Enter('BAR', s:barModeKeymaps)

@ -13,7 +13,4 @@ let s:layer = {
\}
" Capture the exit function
let s:exitFunc = luaeval("require('libmodal').layer.enter(_A)", s:layer)
" Call the exit function in 5 seconds.
call timer_start(5000, s:exitFunc)
let s:exitFunc = luaeval("require('libmodal').layer.enter(_A, '<Esc>')", s:layer)

@ -2,11 +2,11 @@
local libmodal = require 'libmodal'
-- Keep track of the user's input history manually.
local _inputHistory = {}
local input_history = {}
-- Clear the input history if it grows too long for our usage.
function _inputHistory:clear(indexToCheck)
if #self >= indexToCheck then
function input_history:clear(index_to_check)
if #self >= index_to_check then
for i, _ in ipairs(self) do
self[i] = nil
end
@ -14,18 +14,18 @@ function _inputHistory:clear(indexToCheck)
end
-- This is the function that will be called whenever the user presses a button.
local function fooMode()
local function foo_mode()
-- Append to the input history, the latest button press.
_inputHistory[#_inputHistory + 1] = string.char(
input_history[#input_history + 1] = string.char(
-- The input is a character number.
vim.g.fooModeInput
)
-- Custom logic to test for each character index to see if it matches the 'zfo' mapping.
local index = 1
if _inputHistory[1] == 'z' then
if _inputHistory[2] == 'f' then
if _inputHistory[3] == 'o' then
if input_history[1] == 'z' then
if input_history[2] == 'f' then
if input_history[3] == 'o' then
vim.api.nvim_command "echom 'It works!'"
else index = 3
end
@ -33,8 +33,8 @@ local function fooMode()
end
end
_inputHistory:clear(index)
input_history:clear(index)
end
-- Enter the mode to begin the demo.
libmodal.mode.enter('FOO', fooMode)
libmodal.mode.enter('FOO', foo_mode)

@ -2,21 +2,22 @@
local libmodal = require('libmodal')
-- Recurse counter.
local fooModeRecurse = 0
local foo_mode_recurse = 0
-- Register 'z' as the map for recursing further (by calling the FooMode function again).
local fooModeCombos = {
local foo_mode_keymaps =
{
z = 'lua FooMode()'
}
-- define the FooMode() function which is called whenever the user presses 'z'
function FooMode()
fooModeRecurse = fooModeRecurse + 1
libmodal.mode.enter('FOO' .. fooModeRecurse, fooModeCombos)
fooModeRecurse = fooModeRecurse - 1
foo_mode_recurse = foo_mode_recurse + 1
libmodal.mode.enter('FOO' .. foo_mode_recurse, foo_mode_keymaps)
foo_mode_recurse = foo_mode_recurse - 1
end
-- Define the character 'f' as the function we defined— but directly through lua, instead of vimL.
fooModeCombos['f'] = FooMode
foo_mode_keymaps['f'] = FooMode
-- Call FooMode() initially to begin the demo.
FooMode()

@ -2,7 +2,8 @@
local libmodal = require 'libmodal'
-- Register key commands and what they do.
local fooModeCombos = {
local fooModeKeymaps =
{
[''] = 'echom "You cant exit using escape."',
q = 'let g:fooModeExit = 1'
}
@ -10,5 +11,5 @@ local fooModeCombos = {
-- Tell the mode not to exit automatically.
vim.g.fooModeExit = false
-- Enter the mode using the key combos created before.
libmodal.mode.enter('FOO', fooModeCombos, true)
-- Enter the mode using the keymaps created before.
libmodal.mode.enter('FOO', fooModeKeymaps, true)

@ -2,18 +2,19 @@
local libmodal = require 'libmodal'
-- A function which will split the window both horizontally and vertically.
local function _split_twice()
local function split_twice()
vim.api.nvim_command 'split'
vim.api.nvim_command 'vsplit'
end
-- Register key combos for splitting windows and then closing windows
local fooModeCombos = {
-- Register keymaps for splitting windows and then closing windows
local fooModeKeymaps =
{
zf = 'split',
zfo = 'vsplit',
zfc = 'q',
zff = _split_twice
zff = split_twice
}
-- Enter the mode using the key combos.
libmodal.mode.enter('FOO', fooModeCombos)
-- Enter the mode using the keymaps.
libmodal.mode.enter('FOO', fooModeKeymaps)

@ -1,21 +1,21 @@
-- Imports
local libmodal = require('libmodal')
local libmodal = require 'libmodal'
-- create a new layer.
local exitFunc = libmodal.layer.enter({
n = { -- normal mode mappings
gg = { -- remap `gg`
rhs = 'G', -- map it to `G`
noremap = true, -- don't recursively map.
},
G = { -- remap `G`
rhs = 'gg', -- map it to `gg`
noremap = true -- don't recursively map.
libmodal.layer.enter(
{
n = { -- normal mode mappings
gg = { -- remap `gg`
rhs = 'G', -- map it to `G`
noremap = true, -- don't recursively map.
},
G = { -- remap `G`
rhs = 'gg', -- map it to `gg`
noremap = true -- don't recursively map.
}
}
}
})
},
'<Esc>'
)
-- The layer will deactivate in 5 seconds for this demo.
vim.loop.new_timer():start(5000, 0, vim.schedule_wrap(
function() exitFunc(); print('EXITED.') end
))
-- The layer will deactivate when you press <Esc>

@ -1,40 +1,28 @@
-- Imports
local libmodal = require('libmodal')
local libmodal = require 'libmodal'
-- create a new layer.
local layer = libmodal.Layer.new({
n = { -- normal mode mappings
gg = { -- remap `gg`
local layer = libmodal.layer.new(
{
n =
{ -- normal mode mappings
gg = -- remap `gg`
{
rhs = 'G', -- map it to `G`
noremap = true, -- don't recursively map.
-- other options such as `noremap` and `silent` can be set to `true` here
},
G = { -- remap `G`
G = -- remap `G`
{
rhs = 'gg', -- map it to `gg`
noremap = true -- don't recursively map.
}
-- other options such as `noremap` and `silent` can be set to `true` here
},
}
})
-- enter the `layer`.
layer:enter()
-- add a global function for exiting the mode.
function LibmodalLayerExampleExit()
layer:exit()
end
-- Add an additional mapping for `<Esc>` to exit the mode
layer:map('n', '<Esc>', function() layer:exit() end, {})
-- Add an additional mapping for `z`.
layer:map('n', 'z', 'gg', {noremap = true})
-- add an additional mapping for `q`.
layer:map(
'n', 'q', ':lua LibmodalLayerExampleExit()<CR>',
{noremap = true, silent = true}
)
layer:enter()
--[[ unmap `gg` and `G`. Notice they both return to their defaults,
rather than just not doing anything anymore. ]]
--[[ unmap `gg`. Notice that now both `gg` and `G` return the cursor to the top. ]]
layer:unmap('n', 'gg')
layer:unmap('n', 'G')
-- If you wish to only change the mappings of a layer temporarily, you should use another layer. `map` and `unmap` permanently add and remove from the layer's keymap.

@ -11,6 +11,7 @@ function FooMode()
vim.api.nvim_command 'tabnew'
elseif userInput == 'close' then
vim.api.nvim_command 'tabclose'
vim.g.fooModeExit = true
elseif userInput == 'last' then
vim.api.nvim_command 'tablast'
end

@ -2,11 +2,12 @@
local libmodal = require 'libmodal'
-- Define commands through a dictionary.
local commands = {
local commands =
{
new = 'tabnew',
close = 'tabclose',
last = 'tablast',
exit = libmodal.utils.api.mode_exit
exit = 'let g:fooModeExit = v:true',
}
-- Begin the prompt.

@ -2,27 +2,27 @@
local libmodal = require 'libmodal'
-- Recurse counter
local fooModeRecurse = 1
local foo_mode_recurse = 1
-- Function which is called whenever the user presses a button
function FooMode()
-- Append to the input history, the latest button press.
local userInput = string.char(vim.g[
-- The input is a character number.
'foo' .. tostring(fooModeRecurse) .. 'ModeInput'
'foo' .. tostring(foo_mode_recurse) .. 'ModeInput'
])
-- If the user pressed 'z', then increase the counter and recurse.
if userInput == 'z' then
fooModeRecurse = fooModeRecurse + 1
foo_mode_recurse = foo_mode_recurse + 1
Enter()
fooModeRecurse = fooModeRecurse - 1
foo_mode_recurse = foo_mode_recurse - 1
end
end
-- Function to wrap around entering the mode so it can be recursively called.
function Enter()
libmodal.mode.enter('FOO' .. fooModeRecurse, FooMode)
libmodal.mode.enter('FOO' .. foo_mode_recurse, FooMode)
end
-- Initially call the function to begin the demo.

@ -8,6 +8,7 @@ function! s:fooMode() abort
tabnew
elseif userInput == 'close'
tabclose
let g:fooModeExit = v:true
elseif userInput == 'last'
tablast
endif

@ -1,36 +1,63 @@
--[[
/*
* MODULE
*/
--]]
local libmodal = require('libmodal/src')
--[[
/*
* MIRRORS
*/
--]]
libmodal.layer = {enter = function(keymap)
local layer = libmodal.Layer.new(keymap)
layer:enter()
return function() layer:exit() end
end}
libmodal.mode = {enter = function(name, instruction, ...)
libmodal.Mode.new(name, instruction, ...):enter()
end}
libmodal.prompt = {enter = function(name, instruction, ...)
libmodal.Prompt.new(name, instruction, ...):enter()
end}
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return libmodal
-- TODO: remove the __index here after a period of time to let people remove `libmodal.Layer` from their configurations
return setmetatable(
{
layer =
{
--- Enter a new layer.
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @param exit_char nil|string a character which can be used to exit the layer from normal mode.
--- @return function|nil exit a function to exit the layer, or `nil` if `exit_char` is passed
enter = function(keymap, exit_char)
local layer = require('libmodal/src/Layer').new(keymap)
layer:enter()
if exit_char then
layer:map('n', exit_char, function() layer:exit() end, {})
else
return function() layer:exit() end
end
end,
--- Create a new layer.
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @return libmodal.Layer
new = function(keymap)
return require('libmodal/src/Layer').new(keymap)
end,
},
mode =
{
--- Enter a mode.
--- @param name string the name of the mode.
--- @param instruction function|string|table a Lua function, keymap dictionary, Vimscript command.
enter = function(name, instruction, supress_exit)
require('libmodal/src/Mode').new(name, instruction, supress_exit):enter()
end
},
prompt =
{
--- Enter a prompt.
--- @param name string the name of the prompt
--- @param instruction function|table<string, function|string> what to do with user input
--- @param user_completions table<string>|nil a list of possible inputs, provided by the user
enter = function(name, instruction, user_completions)
require('libmodal/src/Prompt').new(name, instruction, user_completions):enter()
end
}
},
{
__index = function(tbl, key)
if key ~= 'Layer' then
return rawget(tbl, key)
else
require('libmodal/src/utils/api').nvim_show_err(
require('libmodal/src/globals').DEFAULT_ERROR_TITLE,
'`libmodal.Layer` is deprecated in favor of `libmodal.layer`. It will work FOR NOW, but uncapitalize that `L` please :)'
)
return require 'libmodal/src/Layer'
end
end,
}
)

@ -1,37 +0,0 @@
--[[
/*
* MODULE
*/
--]]
local HighlightSegment = {}
--[[
/*
* CLASS `HighlightSegment`
*/
--]]
--------------------------------
--[[ SUMMARY:
* Create a new `Indicator.HighlightSegment`.
]]
--[[ PARAMS:
* `hlgroup` => The `highlight-group` to be used for this `Indicator.HighlightSegment`.
* `str` => The text for this `Indicator.HighlightSegment`.
]]
--------------------------------
function HighlightSegment.new(hlgroup, str)
return {
hl = hlgroup,
str = str
}
end
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return HighlightSegment

@ -1,71 +0,0 @@
--[[
/*
* MODULE
*/
--]]
local Indicator = {
HighlightSegment = require('libmodal/src/Indicator/HighlightSegment')
}
-- highlight group names
local _HL_GROUP_MODE = 'LibmodalPrompt'
local _HL_GROUP_PROMPT = 'LibmodalStar'
-- predefined segments
local _SEGMENT_MODE_BEGIN = Indicator.HighlightSegment.new(_HL_GROUP_MODE, '-- ')
local _SEGMENT_MODE_END = Indicator.HighlightSegment.new(_HL_GROUP_MODE, ' --')
local _PROMPT_TEMPLATE = {'* ', ' > '}
--[[
/*
* META `Indicator`
*/
--]]
--[[
/*
* CLASS `Indicator`
*/
--]]
---------------------------------
--[[ SUMMARY:
* Create a new `Indicator` for a mode.
]]
--[[ PARAMS:
* `modeName` => the name of the mode that this `Indicator` is for.
]]
---------------------------------
function Indicator.mode(modeName)
return {
[1] = _SEGMENT_MODE_BEGIN,
[2] = Indicator.HighlightSegment.new(
_HL_GROUP_MODE, tostring(modeName)
),
[3] = _SEGMENT_MODE_END,
}
end
-----------------------------------
--[[ SUMMARY:
* Create a new `Indicator` for a prompt.
]]
--[[ PARAMS:
* `modeName` => the name of the mode that this `Indicator` is for.
]]
-----------------------------------
function Indicator.prompt(modeName)
return Indicator.HighlightSegment.new(
_HL_GROUP_PROMPT,
table.concat(_PROMPT_TEMPLATE, modeName)
)
end
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return Indicator

@ -1,253 +1,135 @@
--[[
/*
* IMPORTS
*/
--]]
local api = vim.api
local libmodal_api = require('libmodal/src/utils/api')
--[[
/*
* MODULE
*/
--]]
local Layer = {TYPE = 'libmodal-layer'}
local _BUFFER_CURRENT = 0
local _ERR_NO_MAP = 'E5555: API call: E31: No such mapping'
local _RESTORED = nil
local function convertKeymap(keymapEntry)
local lhs = keymapEntry.lhs
keymapEntry.lhs = nil
return {lhs, keymapEntry}
--- Remove and return the right-hand side of a `keymap`.
--- @param keymap table the keymap to unpack
--- @return string lhs, table options
local function unpack_keymap_lhs(keymap)
local lhs = keymap.lhs
keymap.lhs = nil
return lhs, keymap
end
local function deconvertKeymap(convertedKeymap)
local rhs = convertedKeymap.rhs
convertedKeymap.rhs = nil
--- Remove and return the right-hand side of a `keymap`.
--- @param keymap table the keymap to unpack
--- @return function|string rhs, table options
local function unpack_keymap_rhs(keymap)
local rhs = keymap.rhs
keymap.rhs = nil
return {rhs, convertedKeymap}
return rhs, keymap
end
--[[
/*
* META `Layer`
*/
--]]
local _metaLayer = require('libmodal/src/classes').new(Layer.TYPE)
---------------------------
--[[ SUMMARY:
* Enter the `Layer`.
* Only activates for the current buffer.
]]
---------------------------
function _metaLayer:enter()
if self._priorKeymap then
--- @class libmodal.Layer
--- @field private existing_keymap table the keymaps to restore when exiting the mode; generated automatically
--- @field private layer_keymap table the keymaps to apply when entering the mode; provided by user
local Layer = require('libmodal/src/utils/classes').new()
--- Apply the `Layer`'s keymaps buffer.
function Layer:enter()
if self.existing_keymap then
error('This layer has already been entered. `:exit()` before entering again.')
end
-- add local aliases.
local layerKeymap = self._keymap
local priorKeymap = {}
--[[ iterate over the new mappings to both:
1. Populate `priorKeymap`
2. Map the `layerKeymap` to the buffer. ]]
for mode, newMappings in pairs(layerKeymap) do
-- if `mode` key has not yet been made for `priorKeymap`.
if not priorKeymap[mode] then
priorKeymap[mode] = {}
self.existing_keymap = {}
--[[ iterate over the new keymaps to both:
1. Populate a list of keymaps which will be overwritten to `existing_keymap`
2. Apply the layer's keymappings. ]]
for mode, new_keymaps in pairs(self.layer_keymap) do
-- if `mode` key has not yet been made for `existing_keymap`.
if not self.existing_keymap[mode] then
self.existing_keymap[mode] = {}
end
-- store the previously mapped keys
for _, bufMap in ipairs(api.nvim_buf_get_keymap(_BUFFER_CURRENT, mode)) do
-- if the new mappings would overwrite this one
if newMappings[bufMap.lhs] then
for _, existing_keymap in ipairs(vim.api.nvim_get_keymap(mode)) do
-- if the new keymaps would overwrite this one
if new_keymaps[existing_keymap.lhs] then
-- remove values so that it is in line with `nvim_set_keymap`.
local lhs, keymap = unpack(convertKeymap(bufMap))
priorKeymap[mode][lhs] = keymap
local lhs, keymap = unpack_keymap_lhs(existing_keymap)
self.existing_keymap[mode][lhs] = keymap
end
end
-- add the new mappings
for lhs, newMapping in pairs(newMappings) do
local rhs, options = unpack(deconvertKeymap(newMapping))
api.nvim_buf_set_keymap(_BUFFER_CURRENT, mode, lhs, rhs, options)
-- add the new keymaps
for lhs, new_keymap in pairs(new_keymaps) do
local rhs, options = unpack_keymap_rhs(new_keymap)
vim.keymap.set(mode, lhs, rhs, options)
end
end
self._priorKeymap = priorKeymap
end
--------------------------------------------------------
--[[ SUMMARY:
* Add a mapping to the mode.
]]
--[[ PARAMS:
* `mode` => the mode that this mapping for.
* `lhs` => the left hand side of the mapping.
* `rhs` => the right hand side of the mapping.
* `options` => options for the mapping.
]]
--[[ SEE ALSO:
* `nvim_buf_set_keymap()`
]]
--------------------------------------------------------
function _metaLayer:_mapToBuffer(mode, lhs, rhs, options)
local priorKeymap = self._priorKeymap
if not priorKeymap then error(
"You can't map to a buffer without activating the layer first."
) end
if not priorKeymap[mode][lhs] then -- the mapping's state has not been saved.
for _, bufMap in
ipairs(api.nvim_buf_get_keymap(_BUFFER_CURRENT, mode))
do -- check if it exists in the buffer
if bufMap.lhs == lhs then -- add it to the undo list
priorKeymap[mode][lhs] = unpack(convertKeymap(bufMap))
break
--- Add a keymap to the mode.
--- @param mode string the mode that this keymap for.
--- @param lhs string the left hand side of the keymap.
--- @param rhs function|string the right hand side of the keymap.
--- @param options table options for the keymap.
--- @see `vim.keymap.set`
function Layer:map(mode, lhs, rhs, options)
if self.existing_keymap then -- the layer has been activated
if not self.existing_keymap[mode][lhs] then -- the keymap's state has not been saved.
for _, existing_keymap in ipairs(vim.api.nvim_get_keymap(mode)) do -- check if it has a keymap
if existing_keymap.lhs == lhs then -- add it to the undo list
existing_keymap.lhs = nil
self.existing_keymap[mode][lhs] = existing_keymap
break
end
end
end
-- map the `lhs` to `rhs` in `mode` with `options` for the current buffer.
vim.keymap.set(mode, lhs, rhs, options)
end
-- map the `lhs` to `rhs` in `mode` with `options` for the current buffer.
api.nvim_buf_set_keymap(_BUFFER_CURRENT, mode, lhs, rhs, options)
-- add the new mapping to the layer's keymap
options.rhs = rhs
self.layer_keymap[mode][lhs] = options
end
------------------------------------------------
--[[ SUMMARY:
* Add a mapping to the mode.
]]
--[[ PARAMS:
* `mode` => the mode that this mapping for.
* `lhs` => the left hand side of the mapping.
* `rhs` => the right hand side of the mapping.
* `options` => options for the mapping.
]]
--[[ SEE ALSO:
* `nvim_buf_set_keymap()`
]]
------------------------------------------------
function _metaLayer:map(mode, lhs, rhs, options)
if self._priorKeymap then -- the layer has been activated.
self:_mapToBuffer(mode, lhs, rhs, options)
--- Restore one keymapping to its original state.
--- @param mode string the mode of the keymap.
--- @param lhs string the keys which invoke the keymap.
--- @see `vim.api.nvim_del_keymap`
function Layer:unmap(mode, lhs)
if not self.existing_keymap then
error("Don't call this function before activating the layer; just remove from the keymap passed to `Layer.new` instead.")
end
-- add the new mapping to the keymap
self._keymap[mode][lhs] = vim.tbl_extend('force',
options, {rhs = rhs}
)
end
----------------------------------------------
--[[ SUMMARY:
* Undo a mapping after `enter()`.
]]
--[[ PARAMS:
* `mode` => the mode to map (e.g. `n`, `i`).
* `lhs` => the mapping to undo.
]]
----------------------------------------------
function _metaLayer:_unmapFromBuffer(mode, lhs)
local priorKeymap = self._priorKeymap
local priorMapping = self._priorKeymap[mode][lhs]
if not priorKeymap then error(
"You can't undo a map from a buffer without activating the layer first."
) end
if priorMapping then -- there is an older mapping to go back to.
-- undo the mapping
local rhs, deconvertedKeymap = unpack(deconvertKeymap(priorMapping))
api.nvim_buf_set_keymap(_BUFFER_CURRENT, mode, lhs, rhs, deconvertedKeymap)
-- set the prior mapping as restored.
priorKeymap[mode][lhs] = _RESTORED
if self.existing_keymap[mode][lhs] then -- there is an older keymap to go back to, so undo this layer_keymap
local rhs, options = unpack_keymap_rhs(self.existing_keymap[mode][lhs])
vim.keymap.set(mode, lhs, rhs, options)
else
-- just delete the buffer mapping.
local noErrors, err = pcall(api.nvim_buf_del_keymap, _BUFFER_CURRENT, mode, lhs)
-- just make the keymap go back to default
local no_errors, err = pcall(vim.api.nvim_del_keymap, mode, lhs)
if not noErrors and err ~= _ERR_NO_MAP then
if not no_errors and err ~= 'E31: No such mapping' then
print(err)
end
end
end
------------------------------------
--[[ SUMMARY:
* Remove a mapping from the mode.
]]
--[[ PARAMS:
* `mode` => the mode that this mapping for.
* `lhs` => the left hand side of the mapping.
]]
--[[ SEE ALSO:
* `nvim_buf_del_keymap()`
]]
------------------------------------
function _metaLayer:unmap(mode, lhs)
-- unmap for the buffer too, if the layer is activated.
if self._priorKeymap then
self:_unmapFromBuffer(mode, lhs)
end
-- remove the mapping from the internal keymap
self._keymap[mode][lhs] = _RESTORED
-- remove this keymap from the list of ones to restore
self.existing_keymap[mode][lhs] = nil
end
--------------------------
--[[ SUMMARY:
* Exit the layer.
]]
--------------------------
function _metaLayer:exit()
if not self._priorKeymap then
--- Exit the layer, restoring all previous keymaps.
function Layer:exit()
if not self.existing_keymap then
error('This layer has not been entered yet.')
end
for mode, mappings in pairs(self._keymap) do
for lhs, _ in pairs(mappings) do
self:_unmapFromBuffer(mode, lhs)
for mode, keymaps in pairs(self.layer_keymap) do
for lhs, _ in pairs(keymaps) do
self:unmap(mode, lhs)
end
end
self._priorKeymap = _RESTORED
self.existing_keymap = nil
end
--[[
/*
* CLASS `Layer`
*/
--]]
-----------------------------------------------------
--[[ SUMMARY:
* Create a new `Layer` for the buffer-local keymap.
]]
--[[ PARAMS:
* `mappings` => the list of user mappings to replace.
]]
--[[ RETURNS:
* A new `Layer`.
]]
-----------------------------------------------------
function Layer.new(keymap)
return setmetatable(
{_keymap = keymap},
_metaLayer
)
end
--[[
/*
* PUBLICIZE `Layer`
*/
--]]
return Layer
return
{
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @return libmodal.Layer
new = function(keymap)
return setmetatable({layer_keymap = keymap}, Layer)
end
}

@ -1,32 +1,23 @@
--[[/* 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')
--[[/* MODULE */]]
local Mode = {TYPE = 'libmodal-mode'}
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)
--[[
/*
* META `_metaMode`
*/
--]]
local _metaMode = classes.new(Mode.TYPE)
local _metaInputBytes = classes.new(nil, {
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
@ -34,158 +25,108 @@ local _metaInputBytes = classes.new(nil, {
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 vim.api.nvim_command(instruction) 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
-----------------------------------------------
--[[ SUMMARY:
* Parse `self.mappings` and see if there is any command to execute.
]]
-----------------------------------------------
function _metaMode:_checkInputForMapping()
--- 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._flushInputTimer:stop()
self.flush_input_timer:stop()
-- Append the latest input to the locally stored input history.
local inputBytes = self.inputBytes
inputBytes[#inputBytes + 1] = self.input:nvimGet()
self.input_bytes[#self.input_bytes + 1] = self.input:get()
-- Get the command based on the users input.
local cmd = self.mappings:get(inputBytes)
local cmd = self.mappings:get(self.input_bytes)
-- Get the type of the command.
local commandType = type(cmd)
local command_type = 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()
if #self.input_bytes < 2 and self.input_bytes[1] == string.byte(HELP) then
self.help:show()
end
inputBytes:clear()
self.input_bytes:clear()
-- The command was a table, meaning that it MIGHT match.
elseif commandType == globals.TYPE_TBL
and globals.is_true(self._timeouts.enabled)
elseif command_type == globals.TYPE_TBL
and globals.is_true(self.timeouts_enabled)
then
-- start the timer
self._flushInputTimer:start(
_TIMEOUT.LEN, 0, vim.schedule_wrap(function()
self.flush_input_timer:start(
TIMEOUT.LEN, 0, vim.schedule_wrap(function()
-- Send input to interrupt a blocking `getchar`
_TIMEOUT:SEND()
TIMEOUT:SEND()
-- if there is a command, execute it.
if cmd[ParseTable.CR] then
self._commandTableExecute(cmd[ParseTable.CR])
self.execute_instruction(cmd[ParseTable.CR])
end
-- clear input
inputBytes:clear()
self._popups:peek():refresh(inputBytes)
self.input_bytes:clear()
self.popups:peek():refresh(self.input_bytes)
end)
)
-- The command was an actual vim command.
else
self._commandTableExecute(cmd)
inputBytes:clear()
self.execute_instruction(cmd)
self.input_bytes:clear()
end
self._popups:peek():refresh(inputBytes)
self.popups:peek():refresh(self.input_bytes)
end
--------------------------
--[[ SUMMARY:
* Enter `self`'s mode.
]]
--------------------------
function _metaMode:enter()
--- 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
if type(self.instruction) == globals.TYPE_TBL then
-- Initialize the input history variable.
self._popups:push(require('libmodal/src/collections/Popup').new())
self.popups:push(utils.Popup.new())
end
self._previousModeName = vim.g.libmodalActiveModeName
vim.g.libmodalActiveModeName = self._name
self.previous_mode_name = vim.g.libmodalActiveModeName
vim.g.libmodalActiveModeName = self.name
--[[ MODE LOOP. ]]
local continueMode = true
while continueMode do
local continue_mode = true
while continue_mode do
-- Try (using pcall) to use the mode.
local noErrors, modeResult = pcall(self._inputLoop, self)
local no_errors, mode_result = pcall(self.get_user_input, self)
-- If there were errors, handle them.
if not noErrors then
utils.show_error(modeResult)
continueMode = true
if not no_errors then
utils.show_error(mode_result)
continue_mode = false
else
continueMode = modeResult
continue_mode = mode_result
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 vim.g[self._timeouts:name()] ~= nil
then self._timeouts.enabled =
self._timeouts:nvimGet()
else self._timeouts.enabled =
Vars.libmodalTimeouts
end
self:tear_down()
end
-------------------------------
--[[ SUMMARY:
* Loop an initialized `mode`.
]]
--[[ RETURNS:
* `boolean` => whether or not the mode should continue
]]
-------------------------------
function _metaMode:_inputLoop()
--- 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.exit.supress
and globals.is_true(self.exit:nvimGet())
then
if self.supress_exit and globals.is_true(self.exit:get()) then
return false
end
@ -193,114 +134,103 @@ function _metaMode:_inputLoop()
utils.api.nvim_lecho(self.indicator)
-- Capture input.
local userInput = utils.api.nvim_input()
local user_input = vim.fn.getchar()
-- Return if there was a timeout event.
if userInput == _TIMEOUT.CHAR_NUMBER then
if user_input == TIMEOUT.CHAR_NUMBER then
return true
end
-- Set the global input variable to the new input.
self.input:nvimSet(userInput)
self.input:set(user_input)
if not self.exit.supress and userInput == globals.ESC_NR then -- The user wants to exit.
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 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 then -- It is the name of a VimL function.
vim.fn[self._instruction]()
else -- the second argument was a function; execute it.
self._instruction()
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
------------------------------
--[[ 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()
--- 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._previousModeName and #vim.trim(self._previousModeName) < 1 then
if self.previous_mode_name and #vim.trim(self.previous_mode_name) < 1 then
vim.g.libmodalActiveModeName = nil
else
vim.g.libmodalActiveModeName = self._previousModeName
vim.g.libmodalActiveModeName = self.previous_mode_name
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({...}))
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 other "session" variables.
-- Define the exit flag
self.supress_exit = supress_exit or false
-- Determine whether a callback was specified, or a combo table.
if type(instruction) == globals.TYPE_TBL then
self:_initMappings()
end
-- 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()
return self
end
-- 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)
--[[
/
* PUBLICIZE MODULE
*/
--]]
-- Create a table for mode-specific data.
self.popups = require('libmodal/src/collections/Stack').new()
return Mode
-- 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
}

@ -1,188 +1,153 @@
--[[/* IMPORTS */]]
local globals = require('libmodal/src/globals')
local utils = require('libmodal/src/utils')
local Vars = require('libmodal/src/Vars')
--[[/* MODULE */]]
local Prompt = {TYPE = 'libmodal-prompt'}
local _HELP = 'help'
local _REPLACEMENTS = {
local globals = require 'libmodal/src/globals'
local utils = require 'libmodal/src/utils'
--- @class libmodal.Prompt
--- @field private completions table<string>|nil
--- @field private exit libmodal.utils.Vars
--- @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 name string
local Prompt = utils.classes.new()
local HELP = 'help'
local REPLACEMENTS =
{
'(', ')', '[', ']', '{', '}',
'=', '+', '<', '>', '^',
',', '/', ':', '?', '@', '!', '$', '*', '.', '%', '&', '\\',
}
for i, replacement in ipairs(_REPLACEMENTS) do
_REPLACEMENTS[i], _ = vim.pesc(replacement)
for i, replacement in ipairs(REPLACEMENTS) do
REPLACEMENTS[i], _ = vim.pesc(replacement)
end
--[[
/*
* META `Prompt`
*/
--]]
local _metaPrompt = require('libmodal/src/classes').new(Prompt.TYPE)
---------------------------------------------------
--[[ SUMMARY:
* Execute the specified instruction based on user input.
]]
--[[ PARAMS:
* `userInput` => the input from the user, used to determine how to execute the `self._instruction`.
]]
---------------------------------------------------
function _metaPrompt:_executeInstruction(userInput)
-- get the instruction for the mode.
local instruction = self._instruction
if type(instruction) == globals.TYPE_TBL then -- The instruction is a command table.
if instruction[userInput] then -- There is a defined command for the input.
local to_execute = instruction[userInput]
--- Execute the instruction specified by the `user_input`.
--- @param user_input string
function Prompt:execute_instruction(user_input)
if type(self.instruction) == globals.TYPE_TBL then -- The self.instruction is a command table.
if self.instruction[user_input] then -- There is a defined command for the input.
local to_execute = self.instruction[user_input]
if type(to_execute) == globals.TYPE_FUNC then
to_execute()
else
vim.api.nvim_command(instruction[userInput])
vim.api.nvim_command(to_execute)
end
elseif userInput == _HELP then -- The user did not define a 'help' command, so use the default.
self._help:show()
elseif user_input == HELP then -- The user did not define a 'help' command, so use the default.
self.help:show()
else -- show an error.
utils.api.nvim_show_err(globals.DEFAULT_ERROR_TITLE, 'Unknown command')
end
elseif type(instruction) == globals.TYPE_STR then -- The instruction is a function.
vim.fn[instruction]()
else -- attempt to call the instruction.
instruction()
elseif type(self.instruction) == globals.TYPE_STR then -- The self.instruction is a function.
vim.fn[self.instruction]()
else -- attempt to call the self.instruction.
self.instruction()
end
end
---------------------------------
--[[ SUMMARY:
* Loop to get user input with `input()`.
]]
---------------------------------
function _metaPrompt:_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
--- Get more input from the user.
--- @return boolean more_input
function Prompt:get_user_input()
-- clear previous `echo`s.
utils.api.nvim_redraw()
-- determine what to do with the input
local function userInputCallback(userInput)
if userInput and string.len(userInput) > 0 then -- The user actually entered something.
self.input:nvimSet(userInput)
self:_executeInstruction(userInput)
else -- indicate we want to leave the prompt
return false
end
local continue_prompt -- will set to true `true` if looping this prompt again
--- 1. Set `g:<mode_name>ModeInput` to `user_input`
--- 2. Execute any commands indicated by `user_input`
--- 3. Read `g:<mode_name>ModeExit` to see if we should `continue_prompt`
--- @param user_input string
local function user_input_callback(user_input)
if user_input and string.len(user_input) > 0 then -- the user actually entered something.
self.input:set(user_input)
self:execute_instruction(user_input)
return true
local should_exit = self.exit:get()
if should_exit ~= nil then
continue_prompt = not should_exit
end
else -- the user entered nothing.
continue_prompt = false
end
end
-- echo the highlighting
vim.api.nvim_command('echohl ' .. self.indicator.hl)
-- set the user input variable
if self._completions then
if self.completions then
vim.api.nvim_command('echo "' .. self.indicator.str .. '"')
return vim.ui.select(self._completions, {}, userInputCallback)
vim.ui.select(self.completions, {}, user_input_callback)
else
return vim.ui.input({prompt = self.indicator.str}, userInputCallback)
vim.ui.input({prompt = self.indicator.str}, user_input_callback)
end
return continue_prompt == nil and true or continue_prompt
end
----------------------------
--[[ SUMMARY:
* Enter a prompt 'mode'.
]]
----------------------------
function _metaPrompt:enter()
--- Enter the prompt.
function Prompt:enter()
-- enter the mode using a loop.
local continueMode = true
while continueMode do
local noErrors, promptResult = pcall(self._inputLoop, self)
local continue_mode = true
while continue_mode do
local no_errors, prompt_result = pcall(self.get_user_input, self)
-- if there were errors.
if not noErrors then
utils.show_error(promptResult)
continueMode = true
if not no_errors then
utils.show_error(prompt_result)
continue_mode = false
else
continueMode = promptResult
continue_mode = prompt_result
end
end
end
--[[
/*
* CLASS `Prompt`
*/
--]]
-------------------------------------------
--[[ SUMMARY:
* Enter a prompt.
]]
--[[ PARAMS:
* `name` => the prompt name.
* `instruction` => the prompt callback, or mode command table.
* `...` => a completions table.
]]
-------------------------------------------
function Prompt.new(name, instruction, ...)
name = vim.trim(name)
local self = setmetatable(
{
exit = Vars.new('exit', name),
indicator = require('libmodal/src/Indicator').prompt(name),
input = require('libmodal/src/Vars').new('input', name),
_instruction = instruction,
_name = name
},
_metaPrompt
)
-- get the arguments
local userCompletions, supressExit = unpack({...})
self.exit.supress = supressExit or false
-- get the completion list.
if type(instruction) == globals.TYPE_TBL then -- unload the keys of the mode command table.
-- Create one if the user specified a command table.
local completions = {}
local containedHelp = false
for command, _ in pairs(instruction) do
completions[#completions + 1] = command
if command == _HELP then containedHelp = true
return
{
--- Enter a prompt.
--- @param name string the name of the prompt
--- @param instruction function|table<string, function|string> what to do with user input
--- @param user_completions table<string>|nil a list of possible inputs, provided by the user
--- @return libmodal.Prompt
new = function(name, instruction, user_completions)
name = vim.trim(name)
local self = setmetatable(
{
exit = utils.Vars.new('exit', name),
indicator = utils.Indicator.prompt(name),
input = utils.Vars.new('input', name),
instruction = instruction,
name = name
},
Prompt
)
-- get the completion list.
if type(instruction) == globals.TYPE_TBL then -- unload the keys of the mode command table.
-- Create one if the user specified a command table.
local completions = {}
local contained_help = false
for command, _ in pairs(instruction) do
completions[#completions + 1] = command
if command == HELP then
contained_help = true
end
end
end
if not containedHelp then -- assign it.
completions[#completions + 1] = _HELP
self._help = utils.Help.new(instruction, 'COMMAND')
if not contained_help then -- assign it.
completions[#completions + 1] = HELP
self.help = utils.Help.new(instruction, 'COMMAND')
end
self.completions = completions
elseif user_completions then
-- Use the table that the user gave.
self.completions = user_completions
end
self._completions = completions
elseif userCompletions then
-- Use the table that the user gave.
self._completions = userCompletions
return self
end
return self
end
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return Prompt
}

@ -1,111 +0,0 @@
--[[/* IMPORTS */]]
local api = vim.api
--[[
/*
* MODULE
*/
--]]
local Vars = {
libmodalTimeouts = vim.g.libmodalTimeouts,
TYPE = 'libmodal-vars'
}
--[[
/*
* META `_metaVars`
*/
--]]
local _metaVars = require('libmodal/src/classes').new(Vars.TYPE)
---------------------------------
--[[ SUMMARY:
* Get the name of `modeName`s global setting.
]]
--[[ PARAMS:
* `modeName` => the name of the mode.
]]
---------------------------------
function _metaVars:name()
return self._modeName .. self._varName
end
------------------------------------
--[[ SUMMARY:
* Retrieve a variable value.
]]
--[[ PARAMS:
* `modeName` => the mode name this value is being retrieved for.
]]
------------------------------------
function _metaVars:nvimGet()
return vim.g[self:name()]
end
-----------------------------------------
--[[ SUMMARY:
* Set a variable value.
]]
--[[ PARAMS:
* `modeName` => the mode name this value is being retrieved for.
* `val` => the value to set `self`'s Vimscript var to.
]]
-----------------------------------------
function _metaVars:nvimSet(val)
vim.g[self:name()] = val
end
--[[
/*
* CLASS `VARS`
*/
--]]
--------------------------
--[[ SUMMARY:
* Create a new entry in `Vars`
]]
--[[ PARAMS:
* `keyName` => the name of the key used to refer to this variable in `Vars`.
]]
--------------------------
function Vars.new(keyName, modeName)
local self = setmetatable({}, _metaVars)
local function noSpaces(strWithSpaces, firstLetterModifier)
local splitStr = vim.split(
string.gsub(strWithSpaces, vim.pesc('_'), vim.pesc(' ')),
' '
)
local function camelCase(str, func)
return func(string.sub(str, 0, 1) or '')
.. string.lower(string.sub(str, 2) or '')
end
splitStr[1] = camelCase(splitStr[1], firstLetterModifier)
for i = 2, #splitStr do splitStr[i] =
camelCase(splitStr[i], string.upper)
end
return table.concat(splitStr)
end
self._modeName = noSpaces(modeName, string.lower)
self._varName = 'Mode' .. noSpaces(keyName, string.upper)
return self
end
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return Vars

@ -1,37 +0,0 @@
return {
-------------------------
--[[ SUMMARY:
* Define a class-metatable.
]]
--[[
* `name` => the name of the class.
* `base` => the base class to use (`{}` by default).
]]
-------------------------
new = function(name, ...)
-- set self to `base`, or `{}` if nil.
local self = unpack({...}) or {}
-- set `__index`.
if not self.__index then
self.__index = self
end
-- set `__type`.
self.__type = name
return self
end,
------------------
--[[ SUMMARY:
* Get the type of some value `v`, if it has one.
]]
--[[ PARAMS:
* `v` => the value to get the type of.
]]
------------------
type = function(v)
return v.__type or type(v)
end
}

@ -1,56 +1,14 @@
--[[
/*
* IMPORTS
*/
--]]
local globals = require('libmodal/src/globals')
--[[
/*
* MODULE
*/
--]]
local _REGEX_ALL = '.'
local ParseTable = {
CR = 13, -- The number corresponding to <CR> in vim.
TYPE = 'libmodal-parse-table',
--------------------------------------
--[[ SUMMARY:
* Split some `str` over a `regex`.
]]
--[[ PARAMS:
* `str` => the string to split.
* `regex` => the regex to split `str` with.
]]
--[[ RETURNS:
* The split `str`.
]]
--------------------------------------
stringSplit = function(str, regex)
local split = {}
for char in string.gmatch(str, regex) do
split[#split + 1] = char
end
return split
end
}
--- The number corresponding to <CR> in vim.
local CR = 13
local globals = require 'libmodal/src/globals'
--- @class libmodal.collections.ParseTable
local ParseTable = require('libmodal/src/utils/classes').new()
----------------------------------
--[[ SUMMARY:
* Reverse the elements of some table.
]]
--[[ PARAMS:
* `tbl` => the table to reverse.
]]
--[[ RETURNS:
* The reversed `tbl`.
]]
----------------------------------
local function _table_reverse(tbl)
--- Reverse the order of elements in some `tbl`
--- @param tbl table the table to reverse
--- @return table tbl_reversed
local function table_reverse(tbl)
local reversed = {}
while #reversed < #tbl do
-- look, no variables!
@ -59,196 +17,124 @@ local function _table_reverse(tbl)
return reversed
end
------------------------------
--[[ SUMMARY:
* Parse a `key`.
]]
--[[ PARAMS:
* `key` => the key to parse.
]]
--[[ RETURNS:
* The parsed `key`.
]]
------------------------------
function ParseTable.parse(key)
return ParseTable.stringSplit(key, _REGEX_ALL)
--- @param str string
--- @return table<string> chars of `str`
local function chars(str)
return vim.split(str, '')
end
-----------------------------------------
--[[ SUMMARY
* Get `splitKey` from some `parseTable`.
]]
--[[ PARAMS:
* `parseTable` => the table to fetch `splitKey` from.
* `splitKey` => the key split into groups.
]]
-----------------------------------------
local function _get(parseTable, splitKey)
--[[ Get the next character in the combo string. ]]
--- Retrieve the mapping of `lhs_reversed_bytes`
--- @param parse_table libmodal.collections.ParseTable the table to fetch `lhs_reversed_bytes` from.
--- @param lhs_reversed_bytes table<string> the characters of the left-hand side of the mapping reversed passed to `string.byte`
--- @return false|function|string|table match a string/func when fully matched; a table when partially matched; false when no match.
local function get(parse_table, lhs_reversed_bytes)
--[[ Get the next character in the keymap string. ]]
local k = ''
if #splitKey > 0 then -- There is more input to parse
k = table.remove(splitKey) -- the table should already be `char2nr()`'d
else -- the user input has run out, but there is more in the `parseTable`.
return parseTable
if #lhs_reversed_bytes > 0 then -- There is more input to parse
k = table.remove(lhs_reversed_bytes) -- the table should already be `string.byte`'d
else -- the user input has run out, but there is more in the `parse_table`.
return parse_table
end
--[[ Parse the `k`. ]]
-- Make sure the dicitonary has a key for that value.
if parseTable[k] then
local val = parseTable[k]
local valType = type(val)
if parse_table[k] then
local val = parse_table[k]
local val_type = type(val)
if valType == globals.TYPE_TBL then
if val[ParseTable.CR] and #splitKey < 1 then
if val_type == globals.TYPE_TBL then
if val[CR] and #lhs_reversed_bytes < 1 then
return val
else
return _get(val, splitKey)
return get(val, lhs_reversed_bytes)
end
elseif valType == globals.TYPE_STR or valType == globals.TYPE_FUNC and #splitKey < 1 then
elseif val_type == globals.TYPE_STR or val_type == globals.TYPE_FUNC and #lhs_reversed_bytes < 1 then
return val
end
end
return nil
end
------------------------------------------------
--[[ SUMMARY:
* Update the values of some `dict` using a `splitKey`.
]]
--[[ PARAMS:
* `parseTable` => the parseTable to update.
* `splitKey` => the key split into groups.
]]
------------------------------------------------
local function _put(parseTable, splitKey, value)
--- Insert a `value` into `parse_table` at the position indicated by `lhs_reversed_bytes`
--- @param lhs_reversed_bytes table<string> the characters of the left-hand side of the mapping reversed passed to `string.byte`
--- @param value function|string the right-hand-side of the mapping
local function put(parse_table, lhs_reversed_bytes, value)
--[[ Get the next character in the table. ]]
local k = string.byte(table.remove(splitKey))
if #splitKey > 0 then -- there are still characters left in the key.
if not parseTable[k] then parseTable[k] = {}
-- If there is a previous command mapping in place
else local valueType = type(parseTable[k])
if valueType == globals.TYPE_STR or valueType == globals.TYPE_FUNC then
-- Swap the mapping to a `CR`
parseTable[k] = {[ParseTable.CR] = parseTable[k]}
local byte = string.byte(table.remove(lhs_reversed_bytes))
if #lhs_reversed_bytes > 0 then -- there are still characters left in the key.
if not parse_table[byte] then -- this is a new mapping
parse_table[byte] = {}
else -- if there is a previous command mapping in place
local value_type = type(parse_table[byte])
if value_type == globals.TYPE_STR or value_type == globals.TYPE_FUNC then -- if this is not a tree of inputs already
-- Make the mapping require hitting enter before executing
parse_table[byte] = {[CR] = parse_table[byte]}
end
end
-- run _update() again
_put(parseTable[k], splitKey, value)
-- If parseTable[k] is a pre-existing table, don't clobber the table— clobber the `CR` value.
elseif type(parseTable[k]) == globals.TYPE_TBL then
parseTable[k][ParseTable.CR] = value
else parseTable[k] = value -- parseTable[k] is not a table, go ahead and clobber the value.
-- run put() again
put(parse_table[byte], lhs_reversed_bytes, value)
-- If parse_Table[k] is a pre-existing table, don't clobber the table— clobber the `CR` value.
elseif type(parse_table[byte]) == globals.TYPE_TBL then
parse_table[byte][CR] = value
else
parse_table[byte] = value -- parse_table[k] is not a table, go ahead and clobber the value.
end
end
--[[
/*
* META `ParseTable`
*/
--]]
local _metaParseTable = require('libmodal/src/classes').new(ParseTable.TYPE)
-------------------------------------
--[[ SUMMARY:
* Get a value from this `ParseTable`.
]]
--[[ PARAMS:
* `key` => the PARSED key to get.
]]
--[[ RETURNS:
* `function` => when `key` is a full match.
* `table` => when the `key` partially mathes.
* `false` => when `key` is not ANYWHERE.
]]
-------------------------------------
function _metaParseTable:get(keyDict)
return _get(self, _table_reverse(keyDict))
--- Retrieve the mapping of `lhs_reversed_bytes`
--- @param key_dict table a list of characters (most recent input first)
--- @return false|function|string|table match a string/func when fully matched; a table when partially matched; false when no match.
function ParseTable:get(key_dict)
return get(self, table_reverse(key_dict))
end
--------------------------------------
--[[ SUMMARY:
* Get a value from this `ParseTable`.
]]
--[[ PARAMS:
* `key` => the key to get.
]]
--[[ RETURNS:
* `function` => when `key` is a full match.
* `table` => when the `key` partially mathes.
* `false` => when `key` is not ANYWHERE.
]]
--------------------------------------
function _metaParseTable:parseGet(key)
local parsedTable = ParseTable.parse(string.reverse(key))
--- Parse `key` and retrieve its value
--- @param key string the left-hand-side of the mapping to retrieve
--- @return false|function|string|table match a string/func when fully found; a table when partially found; false when not found.
function ParseTable:parse_get(key)
local parsed_table = chars(string.reverse(key))
-- convert all of the strings to bytes.
for i, v in ipairs(parsedTable) do
parsedTable[i] = string.byte(v)
for i, v in ipairs(parsed_table) do
parsed_table[i] = string.byte(v)
end
return _get(self, parsedTable)
return get(self, parsed_table)
end
---------------------------------------------
--[[ SUMMARY:
* Put `value` into the parse tree as `key`.
]]
--[[ PARAMS:
* `key` => the key that `value` is reffered to by.
* `value` => the value to store as `key`.
]]
---------------------------------------------
function _metaParseTable:parsePut(key, value)
_put(self, ParseTable.parse(string.reverse(key)), value)
--- Parse `key` and assign it to `value`.
--- @param key string the left-hand-side of the mapping
--- @param value function|string the right-hand-side of the mapping
function ParseTable:parse_put(key, value)
put(self, chars(string.reverse(key)), value)
end
--------------------------------------------------
--[[ SUMMARY:
* Create the union of `self` and `tableToUnite`
]]
--[[ PARAMS:
* `tableToUnite` => the table to unite with `self.`
]]
--------------------------------------------------
function _metaParseTable:parsePutAll(tableToUnite)
for k, v in pairs(tableToUnite) do
self:parsePut(k, v)
--- `:parse_put` all `{key, value}` pairs in `keys_and_values`.
--- @param keys_and_values table<string, function|string>
function ParseTable:parse_put_all(keys_and_values)
for k, v in pairs(keys_and_values) do
self:parse_put(k, v)
end
end
--[[
/*
* CLASS `ParseTable`
*/
--]]
----------------------------------
--[[ SUMMARY:
* Create a new parse table from a user-defined table.
]]
--[[ PARAMS:
* `userTable` => the table of combos defined by the user.
]]
----------------------------------
function ParseTable.new(userTable)
local self = setmetatable({}, _metaParseTable)
-- Parse the passed in table.
self:parsePutAll(userTable)
-- Return the new `ParseTable`.
return self
end
return
{
CR = CR,
--- Create a new `libmodal.collections.ParseTable` from a user-provided table.
--- @param user_table table keymaps (e.g. `{zfo = 'tabnew'}`)
--- @return libmodal.collections.ParseTable
new = function(user_table)
local self = setmetatable({}, ParseTable)
--[[
/*
* PUBLICIZE MODULE
*/
--]]
-- Parse the passed in table.
self:parse_put_all(user_table)
return ParseTable
-- Return the new `ParseTable`.
return self
end,
}

@ -1,141 +0,0 @@
--[[/* MODULE */]]
local Popup = require('libmodal/src/classes').new(
'libmodal-popup',
{config = {
anchor = 'SW',
col = vim.go.columns - 1,
focusable = false,
height = 1,
relative = 'editor',
row = vim.go.lines - vim.go.cmdheight - 1,
style = 'minimal',
width = 1
}}
)
----------------------------
--[[ SUMMARY:
* Check if `window` is non-`nil` and is valid.
]]
--[[ PARAMS:
* `window` => the window number.
]]
--[[ RETURNS:
* `true` => `window` is non-`nil` and is valid
* `false` => otherwise
]]
----------------------------
local function valid(window)
return window and vim.api.nvim_win_is_valid(window)
end
--[[
/*
* META `Popup`
*/
--]]
local _metaPopup = require('libmodal/src/classes').new(Popup.TYPE)
-------------------------------------
--[[ SUMMARY:
* Close `self.window`
* The `self` is inert after calling this.
]]
--[[ PARAMS:
* `keep_buffer` => whether or not to keep `self.buffer`.
]]
-------------------------------------
function _metaPopup:close(keepBuffer)
if valid(self.window) then
vim.api.nvim_win_close(self.window, false)
end
self.window = nil
if not keepBuffer then
self.buffer = nil
self._inputChars = nil
end
end
--------------------------
--[[ SUMMARY:
* Open the popup.
* If the popup was already open, close it and re-open it.
]]
--------------------------
function _metaPopup:open(config)
if not config then config = Popup.config end
if valid(self.window) then
config = vim.tbl_extend('keep', config, vim.api.nvim_win_get_config(self.window))
self:close(true)
end
self.window = vim.api.nvim_open_win(self.buffer, false, config)
end
---------------------------------------
--[[ SUMMARY:
* Update `buffer` with the latest user `inputBytes`.
]]
--[[ PARAMS:
* `inputBytes` => the charaters to fill the popup with.
]]
---------------------------------------
function _metaPopup:refresh(inputBytes)
local inputBytesLen = #inputBytes
-- The user simply typed one more character onto the last one.
if inputBytesLen == #self._inputChars + 1 then
self._inputChars[inputBytesLen] = string.char(inputBytes[inputBytesLen])
elseif inputBytesLen == 1 then -- the user's typing was reset by a parser.
self._inputChars = {string.char(inputBytes[1])}
else -- other tries to optimize this procedure fell through, so do it the hard way.
local chars = {}
for i, byte in ipairs(inputBytes) do
chars[i] = string.char(byte)
end
self._inputChars = chars
end
vim.api.nvim_buf_set_lines(self.buffer, 0, 1, true, {
table.concat(self._inputChars)
})
if not valid(self.window) or vim.api.nvim_win_get_tabpage(self.window) ~= vim.api.nvim_get_current_tabpage() then
self:open()
end
vim.api.nvim_win_set_width(self.window, #self._inputChars)
end
--[[/* CLASS `Popup` */]]
--------------------
--[[ SUMMARY:
* Create a new popup window.
]]
--[[ RETURNS:
* A new popup window.
]]
--------------------
function Popup.new(config)
local self = setmetatable(
{
buffer = vim.api.nvim_create_buf(false, true),
_inputChars = {},
},
_metaPopup
)
self:open(config)
return self
end
--[[/* PUBLICIZE `Popup` */]]
return Popup

@ -1,103 +1,27 @@
--[[
/*
* MODULE `Stack`
*/
--]]
--- @class libmodal.collections.Stack
local Stack = require('libmodal/src/utils/classes').new()
local Stack = {TYPE = 'libmodal-stack'}
--[[
/*
* META `Stack`
*/
--]]
local _metaStack = require('libmodal/src/classes').new(Stack.TYPE)
_metaStack._len = 0
--------------------------------
--[[ SUMMARY:
* Get the foremost value in `self`.
]]
--[[
* The foremost value in `self`.
]]
--------------------------------
function _metaStack:peek()
return self._top
--- @return unknown top the foremost value of the stack
function Stack:peek()
return self[#self]
end
-------------------------
--[[ SUMMARY:
* Remove the foremost value in `self` and return it.
]]
--[[ RETURNS:
* The foremost value in `self`.
]]
-------------------------
function _metaStack:pop()
local previousLen = self._len
if previousLen < 1 then return nil
end
-- Store the previous top of the stack.
local previousTop = self._top
-- Remove the previous top of the stack.
self[previousLen] = nil
-- Get the new length of the stack
local newLen = previousLen - 1
-- Update the values of the stack.
if newLen < 1 then -- the stack is empty
self._len = nil
self._top = nil
else -- there is still something in the stack
self._len = newLen
self._top = self[newLen]
end
-- Return the previous top of the stack.
return previousTop
--- Remove the foremost value from the stack and return it.
--- @return unknown top the foremost value of the stack
function Stack:pop()
return table.remove(self)
end
-------------------------------
--[[ SUMMARY:
* Push some `value` onto `self`.
]]
--[[ PARAMS:
* `value` => the value to append to `self`.
]]
-------------------------------
function _metaStack:push(value)
-- create placeholder so new values are not put into the table until operations have succeeded.
local newLen = self._len + 1
--- Push some `value` on to the stack.
--- @param value unknown the value to push onto the stack.
function Stack:push(value)
-- Push to the stack
self[newLen] = value
-- update stack values
self._len = newLen
self._top = value
self[#self + 1] = value
end
--[[
/*
* CLASS `Stack`
*/
--]]
function Stack.new()
return setmetatable({}, _metaStack)
end
--[[
/*
* PUBLICIZE `Stack`
*/
--]]
return Stack
return
{
new = function()
return setmetatable({}, Stack)
end,
}

@ -1,5 +1,5 @@
return {
ParseTable = require('libmodal/src/collections/ParseTable'),
Popup = require('libmodal/src/collections/Popup'),
Stack = require('libmodal/src/collections/Stack')
return
{
ParseTable = require 'libmodal/src/collections/ParseTable',
Stack = require 'libmodal/src/collections/Stack'
}

@ -1,24 +1,42 @@
local _VIM_FALSE = 0
local _VIM_TRUE = 1
local VIM_FALSE = 0
local VIM_TRUE = 1
return {
DEFAULT_ERROR_TITLE = 'vim-libmodal error',
--- The default error title for `nvim-libmodal` errors.
DEFAULT_ERROR_TITLE = 'nvim-libmodal error',
--- The key-code for the escape character.
ESC_NR = 27,
TYPE_FUNC = 'function',
TYPE_NUM = 'number',
TYPE_STR = 'string',
TYPE_TBL = 'table',
--- The string which is returned by `type(function() end)` (or any function)
TYPE_FUNC = type(function() end),
VIM_FALSE = _VIM_FALSE,
VIM_TRUE = _VIM_TRUE,
--- The string which is returned by `type(0)` (or any number)
TYPE_NUM = type(0),
--- The string which is returned by `type ''` (or any string)
TYPE_STR = type '',
--- The string which is returned by `type {}` (or any table)
TYPE_TBL = type {},
--- The value of Vimscript's `v:false`
VIM_FALSE = VIM_FALSE,
--- The value of Vimscript's `v:true`
VIM_TRUE = VIM_TRUE,
--- Assert some value is either `false` or `v:false`.
--- @param val boolean|number
--- @return boolean
is_false = function(val)
return val == false or val == _VIM_FALSE
return val == false or val == VIM_FALSE
end,
--- Assert some value is either `true` or `v:true`.
--- @param val boolean|number
--- @return boolean
is_true = function(val)
return val == true or val == _VIM_TRUE
return val == true or val == VIM_TRUE
end
}

@ -1,10 +1,9 @@
return {
classes = require('libmodal/src/classes'),
collections = require('libmodal/src/collections'),
globals = require('libmodal/src/globals'),
Indicator = require('libmodal/src/Indicator'),
Layer = require('libmodal/src/Layer'),
Mode = require('libmodal/src/Mode'),
Prompt = require('libmodal/src/Prompt'),
utils = require('libmodal/src/utils')
return
{
collections = require 'libmodal/src/collections',
globals = require 'libmodal/src/globals',
Layer = require 'libmodal/src/Layer',
Mode = require 'libmodal/src/Mode',
Prompt = require 'libmodal/src/Prompt',
utils = require 'libmodal/src/utils',
}

@ -1,85 +1,74 @@
--[[/* IMPORTS */]]
local globals = require 'libmodal/src/globals'
local globals = require('libmodal/src/globals')
--- @class libmodal.utils.Help
local Help = require('libmodal/src/utils/classes').new()
--[[/* Utilities */]]
--- Align `tbl` according to the `longestKeyLen`.
--- Align `tbl` according to the `longest_key_len`.
--- @param tbl table what to align.
--- @param longestKeyLen number how long the longest key is.
--- @param longest_key_len number how long the longest key is.
--- @return table aligned
local function tabAlign(tbl, longestKeyLen)
local toPrint = {}
local function align_columns(tbl, longest_key_len)
local to_print = {}
for key, value in pairs(tbl) do
toPrint[#toPrint + 1] = key
to_print[#to_print + 1] = key
local len = string.len(key)
local byte = string.byte(key)
-- account for ASCII chars that take up more space.
if byte <= 32 or byte == 127 then len = len + 1 end
if byte <= 32 or byte == 127 then
len = len + 1
end
for _ = len, longestKeyLen do
toPrint[#toPrint + 1] = ' '
for _ = len, longest_key_len do
to_print[#to_print + 1] = ' '
end
toPrint[#toPrint + 1] = table.concat(
{'', '\n'},
(type(value) == globals.TYPE_STR) and value or '<lua function>'
)
to_print[#to_print + 1] = '' .. (type(value) == globals.TYPE_STR and value or vim.inspect(value)) .. '\n'
end
return toPrint
return to_print
end
--[[/* MODULE */]]
local Help = {TYPE = 'libmodal-help'}
--[[/* META `Help` */]]
local _metaHelp = require('libmodal/src/classes').new(Help.TYPE)
--- Show the contents of this `Help`.
function _metaHelp:show()
for _, helpText in ipairs(self) do
print(helpText)
function Help:show()
for _, help_text in ipairs(self) do
print(help_text)
end
vim.fn.getchar()
end
--[[/* CLASS `Help` */]]
--- Create a default help table with `commandsOrMaps` and vim expressions.
--- @param commandsOrMaps table commands or mappings to vim expressions.
--- @return table Help
function Help.new(commandsOrMaps, title)
-- find the longest key in the table.
local longestKeyLen = 0
for key, _ in pairs(commandsOrMaps) do
local keyLen = string.len(key)
if keyLen > longestKeyLen then
longestKeyLen = keyLen
return
{
--- Create a default help table with `commands_or_maps` and vim expressions.
--- @param commands_or_maps table<string, function|string> commands or mappings to vim expressions.
--- @param title string
--- @return libmodal.utils.Help
new = function(commands_or_maps, title)
-- find the longest key in the table, or at least the length of the title
local longest_key_maps = string.len(title)
for key, _ in pairs(commands_or_maps) do
local key_len = string.len(key)
if key_len > longest_key_maps then
longest_key_maps = key_len
end
end
end
-- adjust the longest key length if the table header is longer.
if longestKeyLen < string.len(title) then
longestKeyLen = string.len(title)
-- define the separator for the help table.
local help_separator = {}
for i = 1, string.len(title) do
help_separator[i] = '-'
end
help_separator = table.concat(help_separator)
-- Create a new `Help`.
return setmetatable(
{
[1] = ' ',
[2] = table.concat(align_columns({[title] = 'VIM EXPRESSION'}, longest_key_maps)),
[3] = table.concat(align_columns({[help_separator] = '--------------'}, longest_key_maps)),
[4] = table.concat(align_columns(commands_or_maps, longest_key_maps)),
},
Help
)
end
-- define the separator for the help table.
local helpSeparator = {}
for i = 1, string.len(title) do helpSeparator[i] = '-' end
helpSeparator = table.concat(helpSeparator)
-- Create a new `Help`.
return setmetatable(
{
[1] = ' ',
[2] = table.concat(tabAlign({[title] = 'VIM EXPRESSION'}, longestKeyLen)),
[3] = table.concat(tabAlign({[helpSeparator] = '--------------'}, longestKeyLen)),
[4] = table.concat(tabAlign(commandsOrMaps, longestKeyLen)),
},
_metaHelp
)
end
return Help
}

@ -0,0 +1,28 @@
local MODE_HIGHLIGHT = 'LibmodalPrompt'
local PROMPT_HIGHLIGHT = 'LibmodalStar'
--- @class libmodal.utils.Indicator
--- @field public hl string the highlight group to use when printing `str`
--- @field public str string the text to write
local Indicator = {}
--- @param highlight_group string the highlight group to use when printing `str`
--- @param str string what to print
--- @return libmodal.utils.Indicator
function Indicator.new(highlight_group, str)
return
{
hl = highlight_group,
str = str,
}
end
function Indicator.mode(mode_name)
return Indicator.new(MODE_HIGHLIGHT, '-- ' .. mode_name .. ' --')
end
function Indicator.prompt(prompt_name)
return Indicator.new(PROMPT_HIGHLIGHT, '* ' .. prompt_name .. ' > ')
end
return Indicator

@ -0,0 +1,85 @@
--- @class libmodal.utils.Popup
--- @field private buffer number the number of the window which this popup is rendered on.
--- @field private input_chars table<string> the characters input by the user.
--- @field private window number the number of the window which this popup is rendered on.
local Popup = require('libmodal/src/utils/classes').new()
--- @param window number
--- @return boolean `true` if the window is non-`nil` and `nvim_win_is_valid`
local function valid(window)
return window and vim.api.nvim_win_is_valid(window)
end
--- Close `self.window`
--- The `self` is inert after calling this.
--- @param keep_buffer boolean `self.buffer` is passed to `nvim_buf_delete` unless `keep_buffer` is `false`
function Popup:close(keep_buffer)
if valid(self.window) then
vim.api.nvim_win_close(self.window, false)
end
self.window = nil
if not keep_buffer then
vim.api.nvim_buf_delete(self.buffer, {force = true})
self.buffer = nil
self.input_chars = nil
end
end
--- Attempt to open this popup. If the popup was already open, close it and re-open it.
function Popup:open(config)
if not config then
config =
{
anchor = 'SW',
col = vim.go.columns - 1,
focusable = false,
height = 1,
relative = 'editor',
row = vim.go.lines - vim.go.cmdheight - 1,
style = 'minimal',
width = 1
}
end
if valid(self.window) then
self:close(true)
end
self.window = vim.api.nvim_open_win(self.buffer, false, config)
end
--- Display `input_bytes` in `self.buffer`
--- @param input_bytes table<number> a list of character codes to display
function Popup:refresh(input_bytes)
-- The user simply typed one more character onto the last one.
if #input_bytes == #self.input_chars + 1 then
self.input_chars[#input_bytes] = string.char(input_bytes[#input_bytes])
elseif #input_bytes == 1 then -- the user's typing was reset by a parser.
self.input_chars = {string.char(input_bytes[1])}
else -- other tries to optimize this procedure fell through, so do it the hard way.
self.input_chars = {}
for i, byte in ipairs(input_bytes) do
self.input_chars[i] = string.char(byte)
end
end
vim.api.nvim_buf_set_lines(self.buffer, 0, 1, true, {table.concat(self.input_chars)})
-- Close and reopen the window if it was not already open.
if not valid(self.window) or vim.api.nvim_win_get_tabpage(self.window) ~= vim.api.nvim_get_current_tabpage() then
self:open()
end
vim.api.nvim_win_set_width(self.window, #self.input_chars)
end
return
{
new = function(config)
local self = setmetatable({buffer = vim.api.nvim_create_buf(false, true), input_chars = {}}, Popup)
self:open(config)
return self
end
}

@ -0,0 +1,51 @@
--- @class libmodal.utils.Vars
--- @field private mode_name string the highlight group to use when printing `str`
--- @field private var_name string the highlight group to use when printing `str`
local Vars = require('libmodal/src/utils/classes').new()
--- @return string name the global Neovim setting
function Vars:name()
return self.mode_name .. self.var_name
end
--- @return unknown `vim.g[self:name()])`
function Vars:get()
return vim.g[self:name()]
end
--- @param val unknown set `vim.g[self:name()])` equal to this value
function Vars:set(val)
vim.g[self:name()] = val
end
return
{
--- Create a new set of variables
--- @param var_name string the name of the key used to refer to this variable in `Vars`.
--- @param mode_name string the name of the mode
new = function(var_name, mode_name)
local self = setmetatable({}, Vars)
local function no_spaces(str_with_spaces, first_letter_modifier)
local split_str = vim.split(string.gsub(str_with_spaces, vim.pesc '_', vim.pesc ' '), ' ')
local function camel_case(str, func)
return func(string.sub(str, 0, 1) or '') .. string.lower(string.sub(str, 2) or '')
end
split_str[1] = camel_case(split_str[1], first_letter_modifier)
for i = 2, #split_str do split_str[i] =
camel_case(split_str[i], string.upper)
end
return table.concat(split_str)
end
self.mode_name = no_spaces(mode_name, string.lower)
self.var_name = 'Mode' .. no_spaces(var_name, string.upper)
return self
end
}

@ -1,62 +0,0 @@
--[[/* IMPORTS */]]
local api = require('libmodal/src/utils/api')
--[[
/*
* MODULE
*/
--]]
local WindowState = {TYPE = 'libmodal-window-state'}
--[[
/*
* META `WindowState`
*/
--]]
local _metaWindowState = require('libmodal/src/classes').new(WindowState.TYPE)
-----------------------------------
--[[ SUMMARY
* Restore the state of `self`.
]]
-----------------------------------
function _metaWindowState:restore()
vim.go.winheight = self.height
vim.go.winwidth = self.width
api.nvim_redraw()
end
--[[
/*
* CLASS `WindowState`
*/
--]]
--------------------------
--[[ SUMMARY:
* Create a table representing the size of the current window.
]]
--[[ RETURNS:
* The new `WindowState`.
]]
--------------------------
function WindowState.new()
return setmetatable(
{
height = vim.go.winheight,
width = vim.go.winwidth,
},
_metaWindowState
)
end
--[[
/*
* PUBLICIZE MODULE
*/
--]]
return WindowState

@ -1,7 +1,5 @@
--[[/* IMPORTS */]]
local globals = require('libmodal/src/globals')
local HighlightSegment = require('libmodal/src/Indicator/HighlightSegment')
local globals = require 'libmodal/src/globals'
local Indicator = require 'libmodal/src/utils/Indicator'
--[[/* MODULE */]]
@ -25,63 +23,32 @@ function api.nvim_bell()
vim.api.nvim_command('normal '..string.char(27)) -- escape char
end
--- Gets one character of user input, as a number.
function api.nvim_input()
return vim.fn.getchar()
end
--------------------------
--[[ SUMMARY:
* Run `mode` to refresh the screen.
* The function was not named `nvim_mode` because that would be really confusing given the name of this plugin.
]]
--------------------------
--- Run the `mode` command to refresh the screen.
function api.nvim_redraw()
vim.api.nvim_command 'mode'
end
---------------------------------
--[[ SUMMARY:
* Echo a table of {`hlgroup`, `str`} tables.
* Meant to be read as "nvim list echo".
]]
--[[ PARAMS:
* `hlTables` => the tables to echo with highlights.
]]
---------------------------------
local lecho_template = {
[1] = "echohl ",
[2] = nil,
[3] = " | echon '",
[4] = nil,
[5] = "'"
}
function api.nvim_lecho(hlTables)
--- Echo a list of `Indicator`s with their associated highlighting.
--- @param indicators libmodal.utils.Indicator|table<libmodal.utils.Indicator> the indicators to echo
function api.nvim_lecho(indicators)
if indicators.hl then -- wrap the single indicator in a table to form a list of indicators
indicators = {indicators}
end
api.nvim_redraw()
for _, hlTable in ipairs(hlTables) do
-- `:echohl` the hlgroup and then `:echon` the string
lecho_template[2] = tostring(hlTable.hl)
lecho_template[4] = tostring(hlTable.str)
vim.api.nvim_command(table.concat(lecho_template))
for _, indicator in ipairs(indicators) do
vim.api.nvim_command('echohl ' .. indicator.hl .. " | echon '" .. indicator.str .. "'")
end
vim.api.nvim_command 'echohl None'
end
--------------------------------------
--[[ SUMMARY:
* Show a `title` error.
]]
--[[ PARAMS:
* `title` => the title of the error.
* `msg` => the message of the error.
]]
--------------------------------------
--- Show an error.
--- @param title string a succint category of error
--- @param msg string a descriptive reason for the error
function api.nvim_show_err(title, msg)
api.nvim_lecho({
HighlightSegment.new('Title', tostring(title)..'\n'),
HighlightSegment.new('Error', tostring(msg)),
})
api.nvim_lecho {Indicator.new('Title', tostring(title)..'\n'), Indicator.new('Error', tostring(msg))}
vim.fn.getchar()
end

@ -0,0 +1,16 @@
return
{
--- Define a metatable.
--- @param template table the default value
new = function(template)
-- set self to `template`, or `{}` if nil.
local self = template or {}
-- set `__index`.
if not self.__index then
self.__index = self
end
return self
end,
}

@ -1,11 +1,14 @@
--[[/* MODULE */]]
local utils = {}
utils.api = require('libmodal/src/utils/api')
utils.Help = require('libmodal/src/utils/Help')
utils.WindowState = require('libmodal/src/utils/WindowState')
--[[/* FUNCTIONS */]]
local utils =
{
api = require 'libmodal/src/utils/api',
classes = require 'libmodal/src/utils/classes',
Indicator = require 'libmodal/src/utils/Indicator',
Help = require 'libmodal/src/utils/Help',
Popup = require 'libmodal/src/utils/Popup',
Vars = require 'libmodal/src/utils/Vars',
}
--- Show an error from `pcall()`.
--- @param pcall_err string the error generated by `pcall()`.
@ -18,6 +21,4 @@ function utils.show_error(pcall_err)
)
end
--[[/* PUBLICIZE MODULE */]]
return utils

@ -5,8 +5,5 @@ vim.g.libmodalTimeouts = vim.g.libmodalTimeouts or vim.go.timeout
-- The default highlight groups (for colors) are specified below.
-- Change these default colors by defining or linking the corresponding highlight group.
vim.cmd
[[
highlight default link LibmodalPrompt ModeMsg
highlight default link LibmodalStar StatusLine
]]
vim.api.nvim_set_hl(0, 'LibmodalPrompt', {default = true, link = 'ModeMsg'})
vim.api.nvim_set_hl(0, 'LibmodalStar', {default = true, link = 'StatusLine'})

Loading…
Cancel
Save