feat: `Mode:exit` (#30)

* fix(Mode): adapt to changes in `timeoutlen`

* feat(libmodal): do `self:exit()` inside callback

* docs: `Mode:exit`

* style: .editorconfig

* docs(libmodal): fix broken example

* feat(Mode): `self:switch`

* docs: `Mode:switch`

* feat(libmodal): `mode.switch`

Wraps `Mode:switch` for convenience

* docs: `libmodal.mode.switch`

* docs(examples): `*.switch`
pull/32/head
Iron-E 2 months ago committed by GitHub
parent 3966014dbc
commit 1ecc4add3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,63 @@
root = false
[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = tab
insert_final_newline = true
tab_width = 3
trim_trailing_whitespace = true
[doc/*.txt]
max_line_length = 80
[*.{yaml,yml}]
indent_style = space
[*.lua]
align_array_table = true
align_call_args = false
align_continuous_assign_statement = false
align_continuous_inline_comment = true
align_continuous_rect_table_field = false
align_function_params = false
align_if_branch = false
auto_collapse_lines = true
break_all_list_when_line_exceed = true
call_arg_parentheses = remove_table_only
continuation_indent = 1
detect_end_of_line = false
ignore_space_after_colon = false
ignore_spaces_inside_function_call = false
line_space_after_comment = max(2)
line_space_after_do_statement = max(2)
line_space_after_expression_statement = max(2)
line_space_after_for_statement = max(2)
line_space_after_function_statement = fixed(2)
line_space_after_if_statement = max(2)
line_space_after_local_or_assign_statement = max(2)
line_space_after_repeat_statement = max(2)
line_space_after_while_statement = max(2)
max_line_length = 120
never_indent_before_if_condition = false
never_indent_comment_on_if_branch = false
quote_style = single
remove_call_expression_list_finish_comma = false
space_after_comma = true
space_after_comma_in_for_statement = true
space_around_concat_operator = true
space_around_math_operator = true
space_around_table_append_operator = false
space_around_table_field_list = true
space_before_attribute = true
space_before_closure_open_parenthesis = false
space_before_function_call_open_parenthesis = false
space_before_function_call_single_arg = false
space_before_function_open_parenthesis = false
space_before_inline_comment = 1
space_before_open_square_bracket = false
space_inside_function_call_parentheses = false
space_inside_function_param_list_parentheses = false
space_inside_square_brackets = false
trailing_table_separator = smart

@ -85,7 +85,7 @@ The following is a reference for high-level functions meant to be used by mode
creators. For those who wish to see a low-level specification of |libmodal|,
see |libmodal-lua|.
Note: Examples for all topics covered here can be found in the "examples"
NOTE: Examples for all topics covered here can be found in the "examples"
folder at the root of the repository.
See: |api|, |lua-api|, https://github.com/Iron-E/nvim-tabmode,
@ -127,7 +127,9 @@ FUNCTIONS *libmodal-usage-function
To take input on a line-by-line basis, see |libmodal-prompt|.
Note: `libmodal.mode.enter()`/`libmodal#Enter()` may be called from inside
NOTE: mode transitions trigger |ModeChanged| events.
NOTE: `libmodal.mode.enter()`/`libmodal#Enter()` may be called from inside
itself. See |libmodal-examples| for an example.
Parameters: ~
@ -138,16 +140,14 @@ FUNCTIONS *libmodal-usage-function
{instruction} What to do when accepting user input.
- If {instruction} is a `dict`/`table`, then it is treated as a
map of user key-chord to Vim |command|s. Example: >
-- LUA
map of user key-chord to Vim |command|s. Example: >lua
local modeInstruction = {
zf = 'split',
zfo = 'vsplit',
-- You can also use lua functions
zfc = function() return 'tabnew' end
zfc = function() vim.api.nvim_command 'tabnew' end
}
" VIMSCRIPT
< >vim
let s:modeInstruction = {
'zf': 'split',
'zfo': 'vsplit',
@ -155,7 +155,7 @@ FUNCTIONS *libmodal-usage-function
}
<
Note: If no `?` key is defined, one will be created automatically.
NOTE: If no `?` key is defined, one will be created automatically.
- If {instruction} is a `function`, then it is called every time
that |getchar()| completes. The user input is received through
@ -172,7 +172,7 @@ FUNCTIONS *libmodal-usage-function
       lua require('libmodal').mode.enter('FOO', 's:foo')
<
Note: Some QoL features are available by default when
NOTE: Some QoL features are available by default when
specifying a `dict`/`table` value for {instruction} that
would otherwise have to be programmed manually if a
`function` is specified.
@ -188,15 +188,76 @@ FUNCTIONS *libmodal-usage-function
- 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|.
the user. In such cases, when exiting is desired the user should
either:
- set the `g:`{name}`ModeExit` variable to `true`, or
- use |libmodal.Mode:exit()|
See |libmodal-examples|.
See also: ~
|lua-eval| For type conversions between Vimscript to |Lua|.
|libmodal-examples| For examples of this function.
*libmodal.mode:switch()*
`libmodal.mode`.switch(...)
Convenience wrapper for |Mode:switch()|.
Parameters: ~
See |Mode:switch()|.
Example: ~
>lua
libmodal.mode.enter('Foo', {
f = libmodal.mode.switch('Bar', {
b = function()
vim.notify('Inside Bar mode')
end,
}),
})
<
*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|).
call `exit` only inside a `function` mapping as shown below.
Example: ~
>lua
libmodal.mode.enter('Foo', {
q = function(self)
self:exit()
end,
})
<
*libmodal.Mode:switch()*
`libmodal.Mode`:switch(...)
|libmodal.mode.enter()| a new mode, and when it is finished, |Mode:exit()|
the current mode.
Parameters: ~
See |libmodal.mode.enter()|.
Example: ~
>lua
libmodal.mode.enter('Foo', {
f = function(self)
self:switch('Bar', {
b = function()
vim.notify('Inside Bar mode')
end,
})
end,
})
<
*libmodal-layer* *libmodal.layer*
`libmodal.layer`.enter({keymap} [, {exit_char}]) *libmodal.layer.enter()*
@ -307,7 +368,7 @@ FUNCTIONS *libmodal-usage-function
{mode} and {lhs} are the same as in |vim.keymap.del()| except that a {mode}
table is not supported.
Note: this function cannot be called until after |libmodal.Layer:enter()|
NOTE: this function cannot be called until after |libmodal.Layer:enter()|
See also: ~
|libmodal-examples| For an example.
@ -364,7 +425,7 @@ FUNCTIONS *libmodal-usage-function
       lua require('libmodal').prompt.enter('FOO', 's:foo')
<
Note: If you want to create commands with arguments, you will
NOTE: If you want to create commands with arguments, you will
need to use a `function`.
{completions} An array-like `table` of commands that are offered by
@ -375,10 +436,10 @@ FUNCTIONS *libmodal-usage-function
- If unspecified, and {instruction} is not a `table`, then no
completions will be provided.
Note: If no `help` command is defined, one will be created
NOTE: If no `help` command is defined, one will be created
automatically.
Note: The user may set the `g:`{name}`ModeExit` variable to
NOTE: The user may set the `g:`{name}`ModeExit` variable to
`true` at any time to prematurely exit.
@ -416,7 +477,7 @@ Name Default Description
`LibmodalPrompt` `ModeMsg` Color for the mode text.
`LibmodalStar` `StatusLine` Color for the prompt text.
Note: `LibmodalStar`'s name — while not indicative of its use — is used for
NOTE: `LibmodalStar`'s name — while not indicative of its use — is used for
the sake of backwards compatability.
--------------------------------------------------------------------------------

@ -1,10 +1,25 @@
local libmodal = require 'libmodal'
local k = vim.keycode or function(s)
return vim.api.nvim_replace_termcodes(s, true, true, true)
end
local barModeKeymaps = {
p = function() vim.notify('Hello!') end,
}
-- register key commands and what they do
local fooModeKeymaps =
{
[''] = 'echom "You cant exit using escape."',
q = 'let g:fooModeExit = 1'
[k '<Esc>'] = 'echom "You cant exit using escape."',
q = 'let g:fooModeExit = 1', -- exits all instances of this mode
x = function(self)
self:exit() -- exits this instance of the mode
end,
y = function(self)
self:switch('Bar', barModeKeymaps) -- enters Bar and then exits Foo when it is done
end,
z = libmodal.mode.switch('Bar', barModeKeymaps), -- the same as above, but more convenience
}
-- tell the mode not to exit automatically

@ -9,10 +9,14 @@ end
-- register keymaps for splitting windows and then closing windows
local fooModeKeymaps =
{
h = 'norm h',
j = 'norm j',
k = 'norm k',
l = 'norm l',
zf = 'split',
zfo = 'vsplit',
zfc = 'q',
zff = split_twice
zff = split_twice,
zfo = 'vsplit',
}
-- enter the mode using the keymaps

@ -3,12 +3,13 @@ local ParseTable = require 'libmodal.collections.ParseTable'
local utils = require 'libmodal.utils' --- @type libmodal.utils
--- @class libmodal.Mode
--- @field private exit libmodal.utils.Vars
--- @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 libmodal.utils.Vars
--- @field private input_bytes? number[]
--- @field private instruction fun()|{[string]: fun()|string}
--- @field private local_exit boolean
--- @field private mappings libmodal.collections.ParseTable
--- @field private name string
--- @field private ns number the namespace where cursor highlights are drawn on
@ -22,17 +23,17 @@ local HELP_CHAR = '?'
local TIMEOUT =
{
CHAR = ' ',
LEN = vim.go.timeoutlen,
SEND = function(self) vim.api.nvim_feedkeys(self.CHAR, 'nt', false) end
}
TIMEOUT.CHAR_NUMBER = TIMEOUT.CHAR:byte()
--- execute the `instruction`.
--- @param instruction fun()|string a Lua function or Vimscript command.
--- @private
--- @param instruction fun(libmodal.Mode)|string a Lua function or Vimscript command.
--- @return nil
function Mode:execute_instruction(instruction)
if type(instruction) == 'function' then
instruction()
instruction(self)
else
vim.api.nvim_command(instruction)
end
@ -42,13 +43,14 @@ end
--- check the user's input against the `self.instruction` mappings to see if there is anything to execute.
--- if there is nothing to execute, the user's input is rendered on the screen (as does Vim by default).
--- @private
--- @return nil
function Mode:check_input_for_mapping()
-- stop any running timers
self.flush_input_timer:stop()
-- append the latest input to the locally stored input history.
self.input_bytes[#self.input_bytes + 1] = self.input:get()
self.input_bytes[#self.input_bytes + 1] = self.global_input:get()
-- get the command based on the users input.
local cmd = self.mappings:get(self.input_bytes)
@ -64,8 +66,9 @@ function Mode:check_input_for_mapping()
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.
local timeout = vim.api.nvim_get_option_value('timeoutlen', {})
self.flush_input_timer:start( -- start the timer
TIMEOUT.LEN, 0, vim.schedule_wrap(function()
timeout, 0, vim.schedule_wrap(function()
-- send input to interrupt a blocking `getchar`
TIMEOUT:SEND()
-- if there is a command, execute it.
@ -87,6 +90,7 @@ function Mode:check_input_for_mapping()
end
--- clears the virtual cursor from the screen
--- @private
function Mode:clear_virt_cursor()
vim.api.nvim_buf_clear_namespace(0, self.ns, 0, -1);
end
@ -100,6 +104,8 @@ function Mode:enter()
self.popups:push(utils.Popup.new())
end
self.local_exit = false
--- HACK: https://github.com/neovim/neovim/issues/20793
vim.api.nvim_command 'highlight Cursor blend=100'
vim.schedule(function() vim.opt.guicursor:append { 'a:Cursor/lCursor' } end)
@ -131,11 +137,25 @@ function Mode:enter()
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 the mode is not handling exit events automatically and the global exit var is true.
if self.supress_exit and globals.is_true(self.exit:get()) then
if self:exit_flagged() then
return false
end
@ -151,7 +171,7 @@ function Mode:get_user_input()
end
-- set the global input variable to the new input.
self.input:set(user_input)
self.global_input:set(user_input)
if not self.supress_exit and user_input == globals.ESC_NR then -- the user wants to exit.
return false -- as in, "I don't want to continue."
@ -173,7 +193,34 @@ function Mode:get_user_input()
return true
end
--- clears and then renders the virtual cursor
--- @private
function Mode:redraw_virtual_cursor()
self:clear_virt_cursor()
self:render_virt_cursor()
end
--- render the virtual cursor using extmarks
--- @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 }, {})
end
--- `enter` a `Mode` using the arguments given, and then flag the current mode to exit.
--- @param ... unknown arguments to `Mode.new`
--- @return nil
--- @see libmodal.Mode.enter which `...` shares the layout of
--- @see libmodal.Mode.exit
function Mode:switch(...)
local mode = Mode.new(...)
mode:enter()
self:exit()
end
--- uninitialize variables from after exiting the mode.
--- @private
--- @return nil
function Mode:tear_down()
if type(self.instruction) == 'table' then
@ -197,22 +244,11 @@ function Mode:tear_down()
utils.api.redraw()
end
--- clears and then renders the virtual cursor
function Mode:redraw_virtual_cursor()
self:clear_virt_cursor()
self:render_virt_cursor()
end
--- render the virtual cursor using extmarks
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 }, {})
end
--- create a new mode.
--- @private
--- @param name string the name of the mode.
--- @param instruction fun()|string|table a Lua function, keymap dictionary, Vimscript command.
--- @param instruction fun(libmodal.Mode)|string|table a Lua function, keymap dictionary, Vimscript command.
--- @param supress_exit? boolean
--- @return libmodal.Mode
function Mode.new(name, instruction, supress_exit)
name = vim.trim(name)
@ -220,9 +256,10 @@ function Mode.new(name, instruction, supress_exit)
-- inherit the metatable.
local self = setmetatable(
{
exit = utils.Vars.new('exit', name),
input = utils.Vars.new('input', name),
global_exit = utils.Vars.new('exit', name),
global_input = utils.Vars.new('input', name),
instruction = instruction,
local_exit = false,
name = name,
ns = vim.api.nvim_create_namespace('libmodal' .. name),
},

@ -1,71 +1,77 @@
--- @class libmodal
local libmodal = setmetatable(
{
layer =
{
--- enter a new layer.
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @param exit_char? string a character which can be used to exit the layer from normal mode.
--- @return fun()|nil exit a function to exit the layer, or `nil` if `exit_char` is passed
enter = function(keymap, exit_char)
local layer = require('libmodal.Layer').new(keymap)
layer:enter()
local libmodal = setmetatable({}, {
__index = function(tbl, key)
if key ~= 'Layer' then
return rawget(tbl, key)
else
if vim.deprecate then
vim.deprecate('`libmodal.Layer`', '`libmodal.layer`', '4.0.0', 'nvim-libmodal')
else
vim.notify_once(
'`libmodal.Layer` is deprecated in favor of `libmodal.layer`. It will work FOR NOW, but uncapitalize that `L` please :)',
vim.log.levels.WARN,
{title = 'nvim-libmodal'}
)
end
if exit_char then
layer:map('n', exit_char, function() layer:exit() end, {})
else
return function() layer:exit() end
end
end,
return rawget(tbl, 'layer')
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.Layer').new(keymap)
end,
},
libmodal.layer = {}
mode =
{
--- enter a mode.
--- @param name string the name of the mode.
--- @param instruction fun()|string|table a Lua function, keymap dictionary, Vimscript command.
enter = function(name, instruction, supress_exit)
require('libmodal.Mode').new(name, instruction, supress_exit):enter()
end
},
--- enter a new layer.
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @param exit_char? string a character which can be used to exit the layer from normal mode.
--- @return fun()|nil exit a function to exit the layer, or `nil` if `exit_char` is passed
function libmodal.layer.enter(keymap, exit_char)
local layer = require('libmodal.Layer').new(keymap)
layer:enter()
prompt =
{
--- enter a prompt.
--- @param name string the name of the prompt
--- @param instruction fun()|{[string]: fun()|string} what to do with user input
--- @param user_completions? string[] a list of possible inputs, provided by the user
enter = function(name, instruction, user_completions)
require('libmodal.Prompt').new(name, instruction, user_completions):enter()
end
}
},
{
__index = function(tbl, key)
if key ~= 'Layer' then
return rawget(tbl, key)
else
if vim.deprecate then
vim.deprecate('`libmodal.Layer`', '`libmodal.layer`', '4.0.0', 'nvim-libmodal')
else
vim.notify_once(
'`libmodal.Layer` is deprecated in favor of `libmodal.layer`. It will work FOR NOW, but uncapitalize that `L` please :)',
vim.log.levels.WARN,
{title = 'nvim-libmodal'}
)
end
if exit_char then
layer:map('n', exit_char, function() layer:exit() end, {})
else
return function() layer:exit() end
end
end
return rawget(tbl, 'layer')
end
end,
}
)
--- create a new layer.
--- @param keymap table the keymaps (e.g. `{n = {gg = {rhs = 'G', silent = true}}}`)
--- @return libmodal.Layer
function libmodal.layer.new(keymap)
return require('libmodal.Layer').new(keymap)
end
libmodal.mode = {}
--- enter a mode.
--- @param name string the name of the mode.
--- @param instruction fun()|string|table a Lua function, keymap dictionary, Vimscript command.
function libmodal.mode.enter(name, instruction, supress_exit)
local mode = require('libmodal.Mode').new(name, instruction, supress_exit)
mode:enter()
end
--- `enter` a mode using the arguments given, and do not return to the current mode.
--- @param ... unknown arguments to `libmodal.mode.enter`
--- @return fun(self: libmodal.Mode) switcher enters the mode
--- @see libmodal.mode.enter which this function takes the same arguments as
function libmodal.mode.switch(...)
local args = { ... }
return function(self)
self:switch(unpack(args))
end
end
libmodal.prompt = {}
--- enter a prompt.
--- @param name string the name of the prompt
--- @param instruction fun()|{[string]: fun()|string} what to do with user input
--- @param user_completions? string[] a list of possible inputs, provided by the user
function libmodal.prompt.enter(name, instruction, user_completions)
require('libmodal.Prompt').new(name, instruction, user_completions):enter()
end
return libmodal

Loading…
Cancel
Save