diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..82c68db --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/doc/libmodal.txt b/doc/libmodal.txt index 790d9c7..57bbbf4 100644 --- a/doc/libmodal.txt +++ b/doc/libmodal.txt @@ -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 is automatically mapped to exiting. - If |v:true|/`true`, then 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. -------------------------------------------------------------------------------- diff --git a/examples/lua/keymaps-supress-exit.lua b/examples/lua/keymaps-supress-exit.lua index 0661ab3..5479459 100644 --- a/examples/lua/keymaps-supress-exit.lua +++ b/examples/lua/keymaps-supress-exit.lua @@ -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 ''] = '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 diff --git a/examples/lua/keymaps.lua b/examples/lua/keymaps.lua index 6509a6f..935d0e1 100644 --- a/examples/lua/keymaps.lua +++ b/examples/lua/keymaps.lua @@ -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 diff --git a/lua/libmodal/Mode.lua b/lua/libmodal/Mode.lua index 18d563d..0cef9fb 100644 --- a/lua/libmodal/Mode.lua +++ b/lua/libmodal/Mode.lua @@ -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), }, diff --git a/lua/libmodal/init.lua b/lua/libmodal/init.lua index 1a92158..dc4cf7b 100644 --- a/lua/libmodal/init.lua +++ b/lua/libmodal/init.lua @@ -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