Compare commits

...

3 Commits

Author SHA1 Message Date
Iron-E 8366119447
fix: virtual cursor edge cases (#32)
* perf(Mode): use dedicated virtual cursor namespace

* fix(Mode): virtual cursor stays in inactive buffers when exiting

* fix(Mode): `CursorMoved` not fired as expected
2 months ago
Iron-E 3cb0a3817f
feat(Mode): show count in popup 2 months ago
Iron-E 485b77bf14
fix(mode): incompatible with vim.v.count (#26)
* fix(mode): incompatible with vim.v.count

* ref(Vars): track local copy of global value

* ref(Mode): use new `Vars`

* ref(Mode): handle exit during `enter`

* ref: swap `Var.new` param order

Mode name should come before var name

* docs(examples): Vars

* ref: rename `Vars` -> `Var`

* fix(Mode): check if `exit == 1` for vim compat

* fix(Prompt): use new `Var` impl

* ref: remove `count1`

It seems like `math.max` is enough for this purpose

* doc: use h3 headers
2 months ago

@ -108,12 +108,66 @@ VARIABLES *libmodal-usage-variable
|g:| For more information about global variables.
|vim.g| For info about accessing |g:| from lua.
MODE *libmodal.Mode-vars*
`Mode`.count *libmodal.Mode.count*
The |v:count| of the mode.
Type: ~
|libmodal-Var| of |lua-number|
Example: ~
>lua
libmodal.mode.enter('Foo', {
G = function(self)
local count = self.count:get()
vim.api.nvim_command('norm! ' .. tostring(count) .. 'G')
end,
})
<
`Mode`.exit *libmodal.Mode.exit*
If `true`, flags the mode to exit. It will read this value before reading
the user's next key.
Type: ~
|libmodal-Var| of `boolean`
Example: ~
>lua
libmodal.mode.enter('Foo', {
q = function(self)
vim.notify('Hello!')
self.exit:set_local(true)
end,
})
<
`Mode`.timeouts *libmodal.Mode.timeouts*
The |libmodal-timeouts| configuration for this mode.
Type: ~
|libmodal-Var| of `boolean`
Example: ~
>lua
libmodal.mode.enter('Foo', {
t = function(self)
local timeouts = self.timeouts:get()
self.timeouts:set_local(not timeouts) -- toggle timeouts
end,
})
<
--------------------------------------------------------------------------------
FUNCTIONS *libmodal-usage-functions*
*libmodal-mode* *libmodal#Enter()* *libmodal.mode.enter()*
`libmodal.mode`.enter({name}, {instruction} [, {supress_exit}])
`libmodal`#Enter({name}, {instruction} [, {supress_exit}])
MODE *libmodal-mode* *libmodal.mode*
`libmodal.mode`.enter({name}, {instruction} [, {supress_exit}]) *libmodal.mode.enter()*
`libmodal`#Enter({name}, {instruction} [, {supress_exit}]) *libmodal#Enter()*
Enter a new |vim-mode| using {instruction} to determine what actions will
be taken upon specific user inputs.
@ -147,7 +201,7 @@ FUNCTIONS *libmodal-usage-function
-- You can also use lua functions
zfc = function() vim.api.nvim_command 'tabnew' end
}
< >vim
<>vim
let s:modeInstruction = {
'zf': 'split',
'zfo': 'vsplit',
@ -161,6 +215,14 @@ FUNCTIONS *libmodal-usage-function
that |getchar()| completes. The user input is received through
`g:{name}ModeInput` (see above).
- |v:count| is provided through `g:{name}ModeCount`. For |v:count1|
do: >lua
local count1 = math.max(1, count) -- lua
<>vim
let count1 = max(1, count) " vimscript
<
*Error you cannot pass a funcref to Lua from Vimscript!
       - If you want to use a |funcref()| for {instruction}, it
       must be the name of the function as a `string`. >
@ -173,10 +235,12 @@ FUNCTIONS *libmodal-usage-function
<
NOTE: Some QoL features are available by default when
specifying a `dict`/`table` value for {instruction} that
specifying a `dict` / |lua-table| value for {instruction} that
would otherwise have to be programmed manually if a
`function` is specified.
- Bound |lua-function|s may accept a `self` parameter, which
allows access to |libmodal.Mode-vars|.
- A user's typed characters will show in the
lower right corner when {instruction} is a table.
- If `g:libmodalTimeouts` is enabled, then user input will be
@ -198,8 +262,7 @@ FUNCTIONS *libmodal-usage-function
|lua-eval| For type conversions between Vimscript to |Lua|.
|libmodal-examples| For examples of this function.
*libmodal.mode:switch()*
`libmodal.mode`.switch(...)
`libmodal.mode`.switch(...) *libmodal.mode:switch()*
Convenience wrapper for |Mode:switch()|.
@ -217,15 +280,14 @@ FUNCTIONS *libmodal-usage-function
})
<
*libmodal.Mode:exit()*
`libmodal.Mode`:exit()
`libmodal.Mode`:exit() *libmodal.Mode:exit()*
When the {instruction} parameter to |libmodal.mode.enter()| is a
|lua-table|, one can use |lua-function|s as mappings. When this is done, the
`self` parameter becomes available, and from this the `:exit()` function can
be called.
WARNING: this call will *not* interrupt |getchar()| (see |libmodal-mode|).
WARNING: this call will _not_ interrupt |getchar()| (see |libmodal-mode|).
call `exit` only inside a `function` mapping as shown below.
Example: ~
@ -237,8 +299,7 @@ FUNCTIONS *libmodal-usage-function
})
<
*libmodal.Mode:switch()*
`libmodal.Mode`:switch(...)
`libmodal.Mode`:switch(...) *libmodal.Mode:switch()*
|libmodal.mode.enter()| a new mode, and when it is finished, |Mode:exit()|
the current mode.
@ -258,8 +319,9 @@ FUNCTIONS *libmodal-usage-function
end,
})
<
*libmodal-layer* *libmodal.layer*
`libmodal.layer`.enter({keymap} [, {exit_char}]) *libmodal.layer.enter()*
LAYER *libmodal-layer* *libmodal.layer*
`libmodal.layer`.enter({keymap} [, {exit_char}]) *libmodal.layer.enter()*
While a |libmodal-mode| ignores behavior that has not been explicitly
defined, a |libmodal-layer| allows unrecognized |input| to be passed back
@ -296,7 +358,7 @@ FUNCTIONS *libmodal-usage-function
|libmodal-examples| For an example.
|vim.keymap.set()| For more information about `opts`.
`libmodal.layer`.new({keymap}) *libmodal.layer.new()*
`libmodal.layer`.new({keymap}) *libmodal.layer.new()*
See |libmodal.layer.enter()| for more information. This function only
differs from |libmodal.layer.enter()| in that instead of entering the layer
@ -354,7 +416,7 @@ FUNCTIONS *libmodal-usage-function
|libmodal.Layer:enter()| A shortcut to access this function.
|libmodal.Layer.exit()| How to create a |libmodal.Layer|
`libmodal.Layer`:map({mode}, {lhs}, {rhs}, {opts}) *libmodal.Layer:map()*
`libmodal.Layer`:map({mode}, {lhs}, {rhs}, {opts}) *libmodal.Layer:map()*
{mode}, {lhs}, {rhs}, and {opts} are the same as in |vim.keymap.set()|
except that a {mode} table is not supported.
@ -363,7 +425,7 @@ FUNCTIONS *libmodal-usage-function
|libmodal-examples| For an example.
|vim.keymap.set()| For information about the args.
`libmodal.Layer`:unmap({mode}, {lhs}) *libmodal.Layer:unmap()*
`libmodal.Layer`:unmap({mode}, {lhs}) *libmodal.Layer:unmap()*
{mode} and {lhs} are the same as in |vim.keymap.del()| except that a {mode}
table is not supported.
@ -374,9 +436,10 @@ FUNCTIONS *libmodal-usage-function
|libmodal-examples| For an example.
|vim.keymap.del()| For information about the args.
*libmodal-prompt* *libmodal#Prompt()* *libmodal.prompt.enter()*
`libmodal.prompt`.enter({name}, {instruction} [, {completions}])
`libmodal`#Prompt({name}, {instruction} [, {completions}])
PROMPT *libmodal-prompt* *libmodal.prompt*
`libmodal.prompt`.enter({name}, {instruction} [, {completions}]) *libmodal.prompt.enter()*
`libmodal`#Prompt({name}, {instruction} [, {completions}]) *libmodal#Prompt()*
Besides accepting user input like keys in |Normal-mode|, |libmodal| is
also capable of prompting the user for |input| like |Cmdline-mode|. To
@ -447,6 +510,58 @@ FUNCTIONS *libmodal-usage-function
|lua-eval| For type conversions between Vimscript to |Lua|.
|libmodal-examples| For examples of this function.
VAR *libmodal-Var* *libmodal.Var*
Some values mentioned above may be typed `libmodal-Var`. By default, `Var`s
mirror a specific |g:var|, but they may be given instance-local values as
well. In this case, the instance value is preferred to the global value.
`Var`:get() *libmodal.Var:get()*
Return: ~
|libmodal.Var:get_local()| if a local value exists, or
|libmodal.Var:get_global()|.
`Var`:get_global() *libmodal.Var:get_global()*
Return: ~
The global value.
See also: ~
|g:| For more information about global variables.
|vim.g| For info about accessing |g:| from lua.
`Var`:get_local() *libmodal.Var:get_local()*
Return: ~
The local value.
`Var`:set({value}) *libmodal.Var:set()*
|libmodal.Var:set_local()| if a local value exists, otherwise
|libmodal.Var:set_global()|.
Parameters: ~
{value} to set.
`Var`:set_global({value}) *libmodal.Var:set_global()*
Set a {value} locally.
Parameters: ~
{value} to set globally.
See also: ~
|g:| For more information about global variables.
|vim.g| For info about accessing |g:| from lua.
`Var`:set_local({value}) *libmodal.Var:set_local()*
Set a {value} globally.
Parameters: ~
{value} to set locally.
--------------------------------------------------------------------------------
EVENTS *libmodal-usage-events*

@ -13,8 +13,11 @@ local fooModeKeymaps =
{
[k '<Esc>'] = 'echom "You cant exit using escape."',
q = 'let g:fooModeExit = 1', -- exits all instances of this mode
w = function(self)
self.exit:set_global(true) -- exits all instances of the mode (with lua)
end,
x = function(self)
self:exit() -- exits this instance of the mode
self.exit:set_local(true) -- exits this instance of the mode
end,
y = function(self)
self:switch('Bar', barModeKeymaps) -- enters Bar and then exits Foo when it is done

@ -13,10 +13,17 @@ local fooModeKeymaps =
j = 'norm j',
k = 'norm k',
l = 'norm l',
G = function(self)
local count = self.count:get()
vim.api.nvim_command('norm! ' .. count .. 'G')
end,
zf = 'split',
zfc = 'q',
zff = split_twice,
zfo = 'vsplit',
e = 'edit foo',
p = 'bp',
o = 'norm o',
}
-- enter the mode using the keymaps

@ -0,0 +1,79 @@
--[[
This file demonstrates how `Var`s work in Modes and Prompts.
]]
--- WARN: do not import this in your code! it is not part of the public API.
local Var = require 'libmodal.utils.Var'
--- Check the value of the local var
--- @param var any
--- @param val unknown the value to check is equal to
local function assert_local_eq(var, val)
assert(var:get_local() == val, 'assertion: the global value equals ' .. vim.inspect(val))
end
--- Check the value of the global var
--- @param var any
--- @param val unknown the value to check is equal to
local function assert_global_eq(var, val)
assert(var:get_global() == val, 'assertion: the global value equals ' .. vim.inspect(val))
end
--- Check the value of the scoped var
--- @param var any
--- @param val unknown the value to check is equal to
--- @param scope 'global'|'local'
local function assert_eq(var, val, scope)
assert(var:get() == val, 'assertion: the value equals ' .. vim.inspect(val))
local fn = scope == 'local' and assert_local_eq or assert_global_eq
fn(var, val)
end
--- check the value of all vars
--- @param var any
--- @param val unknown the value to check is equal to
local function assert_all_eq(var, val)
assert_eq(var, val, 'local')
assert_global_eq(var, val)
end
local mode_name = 'Foo'
local var_name = 'Bar'
--- WARN: do not use this function in your code! It is not part of the public API.
local foo = Var.new(mode_name, var_name)
-- 1. baseline
assert_all_eq(foo, nil)
-- 2. without local value, `:get` and `:set` use globals
local global_value = true
foo:set(global_value)
assert_eq(foo, global_value, 'global')
assert_local_eq(foo, nil)
-- 3. set local value
foo:set_local(global_value)
assert_all_eq(foo, global_value)
-- 4. with local value, `:get` and `:set` use locals
local local_value = false
foo:set(local_value)
assert_eq(foo, local_value, 'local')
assert_global_eq(foo, global_value)
-- Finally, unset all so the test can be run again
foo:set_global(nil)
foo:set_local(nil)
assert_all_eq(foo, nil)

@ -2,23 +2,47 @@ local globals = require 'libmodal.globals'
local ParseTable = require 'libmodal.collections.ParseTable'
local utils = require 'libmodal.utils' --- @type libmodal.utils
--- @alias CursorPosition {[1]: integer, [2]: integer} see `nvim_win_get_cursor`
--- @class libmodal.Mode
--- @field private cursor CursorPosition
--- @field private flush_input_timer unknown
--- @field private global_exit libmodal.utils.Vars
--- @field private global_input libmodal.utils.Vars
--- @field private help? libmodal.utils.Help
--- @field private input_bytes? number[]
--- @field private input libmodal.utils.Var[integer]
--- @field private input_bytes? integer[] local `input` history
--- @field private instruction fun()|{[string]: fun()|string}
--- @field private local_exit boolean
--- @field private mappings libmodal.collections.ParseTable
--- @field private modeline string[][]
--- @field private name string
--- @field private ns number the namespace where cursor highlights are drawn on
--- @field private popups libmodal.collections.Stack
--- @field private show_name fun()
--- @field private supress_exit boolean
--- @field private timeouts_enabled boolean
--- @field private virtual_cursor_autocmd integer
--- @field public count libmodal.utils.Var[integer]
--- @field public exit libmodal.utils.Var[boolean]
--- @field public timeouts? libmodal.utils.Var[boolean]
local Mode = utils.classes.new()
--- Cursor events triggered by which modes
local CURSOR_EVENTS_BY_MODE = {
CursorMoved = {
n = true,
V = true,
v = true,
[utils.api.replace_termcodes '<C-v>'] = true,
},
CursorMovedI = {
i = true,
R = true,
},
}
--- The namespaces used by modes
local NS = {
--- The virtual cursor namespace. Used to workaround neovim/neovim#20793
CURSOR = vim.api.nvim_create_namespace('libmodal-mode-virtual_cursor'),
}
local HELP_CHAR = '?'
local TIMEOUT =
{
@ -27,6 +51,12 @@ local TIMEOUT =
}
TIMEOUT.CHAR_NUMBER = TIMEOUT.CHAR:byte()
--- Byte for 0
local ZERO = string.byte(0)
--- Byte for 9
local NINE = string.byte(9)
--- execute the `instruction`.
--- @private
--- @param instruction fun(libmodal.Mode)|string a Lua function or Vimscript command.
@ -38,7 +68,8 @@ function Mode:execute_instruction(instruction)
vim.api.nvim_command(instruction)
end
self:redraw_virtual_cursor()
self.count:set(0)
self:render_virtual_cursor(0, true)
end
--- check the user's input against the `self.instruction` mappings to see if there is anything to execute.
@ -50,7 +81,7 @@ function Mode:check_input_for_mapping()
self.flush_input_timer:stop()
-- append the latest input to the locally stored input history.
self.input_bytes[#self.input_bytes + 1] = self.global_input:get()
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)
@ -65,7 +96,7 @@ function Mode:check_input_for_mapping()
end
self.input_bytes = {}
elseif command_type == 'table' and globals.is_true(self.timeouts_enabled) then -- the command was a table, meaning that it MIGHT match.
elseif command_type == 'table' and globals.is_true(self.timeouts:get()) then -- the command was a table, meaning that it MIGHT match.
local timeout = vim.api.nvim_get_option_value('timeoutlen', {})
self.flush_input_timer:start( -- start the timer
timeout, 0, vim.schedule_wrap(function()
@ -86,13 +117,23 @@ function Mode:check_input_for_mapping()
self.input_bytes = {}
end
self.popups:peek():refresh(self.input_bytes)
local input_bytes = self.input_bytes or {}
local count = self.count:get()
if count > 0 then
local count_bytes = { tostring(count):byte(1, -1) }
input_bytes = vim.list_extend(count_bytes, input_bytes)
end
self.popups:peek():refresh(input_bytes)
end
--- clears the virtual cursor from the screen
--- @param bufnr integer to clear the cursor on
--- @private
function Mode:clear_virt_cursor()
vim.api.nvim_buf_clear_namespace(0, self.ns, 0, -1);
function Mode:clear_virtual_cursor(bufnr)
vim.api.nvim_buf_clear_namespace(bufnr, NS.CURSOR, 0, -1);
end
--- enter this mode.
@ -104,12 +145,25 @@ function Mode:enter()
self.popups:push(utils.Popup.new())
end
self.local_exit = false
self.count:set(0)
self.exit:set(false)
--- HACK: https://github.com/neovim/neovim/issues/20793
--- HACK: neovim/neovim#20793
vim.api.nvim_command 'highlight Cursor blend=100'
vim.schedule(function() vim.opt.guicursor:append { 'a:Cursor/lCursor' } end)
self:render_virt_cursor()
self.cursor = self:cursor_in(0)
self:render_virtual_cursor(0)
do
local augroup = vim.api.nvim_create_augroup('libmodal-mode-' .. self.name, { clear = false })
self.virtual_cursor_autocmd = vim.api.nvim_create_autocmd('BufLeave', {
callback = function(ev)
local bufnr = ev.buf
self:clear_virtual_cursor(bufnr)
end,
group = augroup,
})
end
self.previous_mode_name = vim.g.libmodalActiveModeName
vim.g.libmodalActiveModeName = self.name
@ -118,49 +172,27 @@ function Mode:enter()
local previous_mode = self.previous_mode_name or vim.api.nvim_get_mode().mode
vim.api.nvim_exec_autocmds('ModeChanged', {pattern = previous_mode .. ':' .. self.name})
local continue_mode = true
while continue_mode do
repeat
-- try (using pcall) to use the mode.
local ok, mode_result = pcall(self.get_user_input, self)
local ok, result = pcall(self.get_user_input, self)
-- if there were errors, handle them.
if not ok then
--- @diagnostic disable-next-line:param-type-mismatch if `not ok` then `mode_result` is a string
utils.notify_error('Error during nvim-libmodal mode', mode_result)
continue_mode = false
else
continue_mode = mode_result
utils.notify_error('Error during nvim-libmodal mode', result)
self.exit:set_local(true)
end
end
until globals.is_true(self.exit:get())
self:tear_down()
vim.api.nvim_exec_autocmds('ModeChanged', {pattern = self.name .. ':' .. previous_mode})
end
--- exit this instance of the mode.
--- WARN: does not interrupt the current mode to exit. It only flags that exit is desired for when control yields back
--- to the mode.
--- @return nil
function Mode:exit()
self.local_exit = true
end
--- @private
--- @return boolean `true` if the mode's exit was flagged
function Mode:exit_flagged()
return self.local_exit or globals.is_true(self.global_exit:get())
end
--- get input from the user.
--- @private
--- @return boolean more_input
function Mode:get_user_input()
if self:exit_flagged() then
return false
end
-- echo the indicator.
self.show_name()
self:show_mode()
-- capture input.
local user_input = vim.fn.getchar()
@ -171,41 +203,76 @@ function Mode:get_user_input()
end
-- set the global input variable to the new input.
self.global_input:set(user_input)
self.input:set(user_input)
if ZERO <= user_input and user_input <= NINE then
local oldCount = self.count:get()
local newCount = tonumber(oldCount .. string.char(user_input))
self.count:set(newCount)
end
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 == 'table' then -- the instruction was provided as a was a set of mappings.
self:check_input_for_mapping()
elseif instruction_type == 'string' then -- the instruction is the name of a Vimscript function.
vim.fn[self.instruction]()
else -- the instruction is a function.
self.instruction()
if self.count:get() < 1 then -- exit
return self.exit:set_local(true)
end
self.count:set(0) -- reset count count
end
return true
--[[ 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 == 'table' then -- the instruction was provided as a was a set of mappings.
self:check_input_for_mapping()
elseif instruction_type == 'string' then -- the instruction is the name of a Vimscript function.
vim.fn[self.instruction]()
else -- the instruction is a function.
self.instruction()
end
end
--- clears and then renders the virtual cursor
--- @private
function Mode:redraw_virtual_cursor()
self:clear_virt_cursor()
self:render_virt_cursor()
--- @param winid integer
--- @return CursorPosition line_and_col
function Mode:cursor_in(winid)
local cursor = vim.api.nvim_win_get_cursor(winid)
cursor[1] = cursor[1] - 1 -- win_get_cursor returns +1 for our purpose
return cursor
end
--- render the virtual cursor using extmarks
--- @param winid integer
--- @param clear? boolean if true, clear other virtual cursors before rendering the new one
--- @private
function Mode:render_virt_cursor()
local line_nr, col_nr = unpack(vim.api.nvim_win_get_cursor(0))
line_nr = line_nr - 1 -- win_get_cursor returns +1 for our purpose
vim.highlight.range(0, self.ns, 'Cursor', { line_nr, col_nr }, { line_nr, col_nr + 1 }, {})
function Mode:render_virtual_cursor(winid, clear)
local bufnr = vim.api.nvim_win_get_buf(winid)
if clear then
self:clear_virtual_cursor(bufnr)
end
local cursor = self:cursor_in(winid)
vim.highlight.range(bufnr, NS.CURSOR, 'Cursor', cursor, cursor, { inclusive = true })
if not vim.deep_equal(self.cursor, cursor) then
local mode = vim.api.nvim_get_mode().mode
if CURSOR_EVENTS_BY_MODE.CursorMoved[mode] then
vim.api.nvim_exec_autocmds('CursorMoved', {})
elseif CURSOR_EVENTS_BY_MODE.CursorMovedI[mode] then
vim.api.nvim_exec_autocmds('CursorMovedI', {})
end
self.cursor = cursor
end
end
--- show the mode indicator, if it is enabled
function Mode:show_mode()
utils.api.redraw()
local showmode = vim.api.nvim_get_option_value('showmode', {})
if showmode then
vim.api.nvim_echo({{'-- ' .. self.name .. ' --', 'LibmodalPrompt'}}, false, {})
end
end
--- `enter` a `Mode` using the arguments given, and then flag the current mode to exit.
@ -216,13 +283,20 @@ end
function Mode:switch(...)
local mode = Mode.new(...)
mode:enter()
self:exit()
self.exit:set_local(true)
end
--- uninitialize variables from after exiting the mode.
--- @private
--- @return nil
function Mode:tear_down()
--- HACK: neovim/neovim#20793
self:clear_virtual_cursor(0)
vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end)
vim.api.nvim_command 'highlight Cursor blend=0'
vim.api.nvim_del_autocmd(self.virtual_cursor_autocmd)
self.cursor = nil
if type(self.instruction) == 'table' then
self.flush_input_timer:stop()
self.input_bytes = nil
@ -230,11 +304,6 @@ function Mode:tear_down()
self.popups:pop():close()
end
--- HACK: https://github.com/neovim/neovim/issues/20793
self:clear_virt_cursor()
vim.schedule(function() vim.opt.guicursor:remove { 'a:Cursor/lCursor' } end)
vim.api.nvim_command 'highlight Cursor blend=0'
if self.previous_mode_name and #vim.trim(self.previous_mode_name) < 1 then
vim.g.libmodalActiveModeName = nil
else
@ -256,23 +325,16 @@ function Mode.new(name, instruction, supress_exit)
-- inherit the metatable.
local self = setmetatable(
{
global_exit = utils.Vars.new('exit', name),
global_input = utils.Vars.new('input', name),
count = utils.Var.new(name, 'count'),
exit = utils.Var.new(name, 'exit'),
input = utils.Var.new(name, 'input'),
instruction = instruction,
local_exit = false,
name = name,
ns = vim.api.nvim_create_namespace('libmodal' .. name),
modeline = {{'-- ' .. name .. ' --', 'LibmodalPrompt'}},
},
Mode
)
self.show_name = vim.o.showmode and
function()
utils.api.redraw()
vim.api.nvim_echo({{'-- ' .. name .. ' --', 'LibmodalPrompt'}}, false, {})
end or
utils.api.redraw
-- define the exit flag
self.supress_exit = supress_exit or false
@ -295,10 +357,7 @@ function Mode.new(name, instruction, supress_exit)
self.popups = require('libmodal.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
self.timeouts = utils.Var.new(self.name, 'timeouts', vim.g.libmodalTimeouts)
end
return self

@ -3,9 +3,9 @@ local utils = require 'libmodal.utils' --- @type libmodal.utils
--- @class libmodal.Prompt
--- @field private completions? string[]
--- @field private indicator {hl: string, text: string}
--- @field private exit libmodal.utils.Vars
--- @field private exit libmodal.utils.Var
--- @field private help? libmodal.utils.Help
--- @field private input libmodal.utils.Vars
--- @field private input libmodal.utils.Var
--- @field private instruction fun()|{[string]: fun()|string}
--- @field private name string
local Prompt = utils.classes.new()
@ -113,9 +113,9 @@ function Prompt.new(name, instruction, user_completions)
local self = setmetatable(
{
exit = utils.Vars.new('exit', name),
exit = utils.Var.new(name, 'exit'),
indicator = {hl = 'LibmodalStar', text = '* ' .. name .. ' > '},
input = utils.Vars.new('input', name),
input = utils.Var.new(name, 'input'),
instruction = instruction,
name = name
},

@ -0,0 +1,103 @@
--- @param str_with_spaces string
--- @param first_letter_modifier fun(s: string): string
local function no_spaces(str_with_spaces, first_letter_modifier)
local split_str = vim.split(str_with_spaces:gsub(vim.pesc '_', vim.pesc ' '), ' ')
--- @param str string
--- @param func fun(s: string): string
local function camel_case(str, func)
return func(str:sub(0, 1) or '') .. (str:sub(2) or ''):lower()
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
--- @class libmodal.utils.Var
--- @field private mode_name string the highlight group to use when printing `str`
--- @field private value? unknown the local value of the variable
--- @field private var_name string the highlight group to use when printing `str`
local Var = require('libmodal.utils.classes').new()
--- create a new set of variables
--- @param mode_name string the name of the mode
--- @param var_name string the name of the key used to refer to this variable in `Var`.
--- @param default_global? unknown the default global value
--- @return libmodal.utils.Var
function Var.new(mode_name, var_name, default_global)
local self = setmetatable({}, Var)
self.mode_name = no_spaces(mode_name, string.lower)
self.var_name = 'Mode' .. no_spaces(var_name, string.upper)
self.value = nil
if default_global ~= nil and self:get_global() == nil then
self:set_global(default_global)
end
return self
end
--- @generic T
--- @return T value the local value if it exists, or the global value
function Var:get()
local local_value = self:get_local()
if local_value == nil then
return self:get_global()
end
return local_value
end
--- @generic T
--- @return T global_value the global value
function Var:get_local()
return self.value
end
--- @generic T
--- @return T global_value the global value
function Var:get_global()
return vim.g[self:name()]
end
--- @return string name the global Neovim setting
function Var:name()
return self.mode_name .. self.var_name
end
--- NOTE: the local value is only set if not `nil`, for backwards compatibility purposes.
--- local values did not always exist, and since `get` prefers local values, it may
--- too-eagerly shadow the global variable.
--- @param val unknown set local value if it exists, or the global value
--- @return nil
function Var:set(val)
if self:get_local() == nil then
self:set_global(val)
else
self:set_local(val)
end
end
--- @param val unknown set the local value equal to this
--- @return nil
function Var:set_local(val)
self.value = val
end
--- @param val unknown set the global value equal to this
--- @return nil
function Var:set_global(val)
if val == nil then
vim.api.nvim_del_var(self:name()) -- because `nvim_set_var('foo', nil)` actually sets 'foo' to `vim.NIL`
else
vim.api.nvim_set_var(self:name(), val)
end
end
return Var

@ -1,56 +0,0 @@
--- @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.utils.classes').new()
--- @return unknown `vim.g[self:name()])`
function Vars:get()
return vim.g[self:name()]
end
--- @return string name the global Neovim setting
function Vars:name()
return self.mode_name .. self.var_name
end
--- 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
--- @return libmodal.utils.Vars
function Vars.new(var_name, mode_name)
local self = setmetatable({}, Vars)
--- @param str_with_spaces string
--- @param first_letter_modifier fun(s: string): string
local function no_spaces(str_with_spaces, first_letter_modifier)
local split_str = vim.split(str_with_spaces:gsub(vim.pesc '_', vim.pesc ' '), ' ')
--- @param str string
--- @param func fun(s: string): string
local function camel_case(str, func)
return func(str:sub(0, 1) or '') .. (str:sub(2) or ''):lower()
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
--- @generic T
--- @param val T set `g:{self:name()})` equal to this value
--- @return nil
function Vars:set(val)
vim.api.nvim_set_var(self:name(), val)
end
return Vars

@ -3,14 +3,14 @@
--- @field classes libmodal.utils.classes
--- @field Help libmodal.utils.Help
--- @field Popup libmodal.utils.Popup
--- @field Vars libmodal.utils.Vars
--- @field Var libmodal.utils.Var
local utils =
{
api = require 'libmodal.utils.api',
classes = require 'libmodal.utils.classes',
Help = require 'libmodal.utils.Help',
Popup = require 'libmodal.utils.Popup',
Vars = require 'libmodal.utils.Vars',
Var = require 'libmodal.utils.Var',
}
--- `vim.notify` with a `msg` some `error` which has a `vim.v.throwpoint` and `vim.v.exception`.

Loading…
Cancel
Save