From 485b77bf1416786288385946fe2617a71dfd0046 Mon Sep 17 00:00:00 2001 From: Iron-E <36409591+Iron-E@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:04:16 +0000 Subject: [PATCH] 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 --- doc/libmodal.txt | 155 ++++++++++++++++++++++---- examples/lua/keymaps-supress-exit.lua | 5 +- examples/lua/keymaps.lua | 4 + examples/lua/vars.lua | 79 +++++++++++++ lua/libmodal/Mode.lua | 126 ++++++++++----------- lua/libmodal/Prompt.lua | 8 +- lua/libmodal/utils/Var.lua | 103 +++++++++++++++++ lua/libmodal/utils/Vars.lua | 56 ---------- lua/libmodal/utils/init.lua | 4 +- 9 files changed, 389 insertions(+), 151 deletions(-) create mode 100644 examples/lua/vars.lua create mode 100644 lua/libmodal/utils/Var.lua delete mode 100644 lua/libmodal/utils/Vars.lua diff --git a/doc/libmodal.txt b/doc/libmodal.txt index 57bbbf4..cb9e9df 100644 --- a/doc/libmodal.txt +++ b/doc/libmodal.txt @@ -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* diff --git a/examples/lua/keymaps-supress-exit.lua b/examples/lua/keymaps-supress-exit.lua index 5479459..c2725c5 100644 --- a/examples/lua/keymaps-supress-exit.lua +++ b/examples/lua/keymaps-supress-exit.lua @@ -13,8 +13,11 @@ local fooModeKeymaps = { [k ''] = '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 diff --git a/examples/lua/keymaps.lua b/examples/lua/keymaps.lua index 935d0e1..803078a 100644 --- a/examples/lua/keymaps.lua +++ b/examples/lua/keymaps.lua @@ -13,6 +13,10 @@ 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, diff --git a/examples/lua/vars.lua b/examples/lua/vars.lua new file mode 100644 index 0000000..841be6f --- /dev/null +++ b/examples/lua/vars.lua @@ -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) diff --git a/lua/libmodal/Mode.lua b/lua/libmodal/Mode.lua index 0cef9fb..38120f7 100644 --- a/lua/libmodal/Mode.lua +++ b/lua/libmodal/Mode.lua @@ -4,19 +4,19 @@ local utils = require 'libmodal.utils' --- @type libmodal.utils --- @class libmodal.Mode --- @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[number] +--- @field private input_bytes? number[] 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 public count libmodal.utils.Var[number] +--- @field public exit libmodal.utils.Var[boolean] +--- @field public timeouts? libmodal.utils.Var[boolean] local Mode = utils.classes.new() local HELP_CHAR = '?' @@ -27,6 +27,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,6 +44,7 @@ function Mode:execute_instruction(instruction) vim.api.nvim_command(instruction) end + self.count:set(0) self:redraw_virtual_cursor() end @@ -50,7 +57,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 +72,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() @@ -104,7 +111,8 @@ 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 vim.api.nvim_command 'highlight Cursor blend=100' @@ -118,49 +126,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,26 +157,29 @@ 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() - end + return self.exit:set_local(true) 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 @@ -208,6 +197,16 @@ function Mode:render_virt_cursor() vim.highlight.range(0, self.ns, 'Cursor', { line_nr, col_nr }, { line_nr, col_nr + 1 }, {}) 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. --- @param ... unknown arguments to `Mode.new` --- @return nil @@ -216,7 +215,7 @@ 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. @@ -256,23 +255,17 @@ 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 +288,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 diff --git a/lua/libmodal/Prompt.lua b/lua/libmodal/Prompt.lua index 830c93a..c154fbf 100644 --- a/lua/libmodal/Prompt.lua +++ b/lua/libmodal/Prompt.lua @@ -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 }, diff --git a/lua/libmodal/utils/Var.lua b/lua/libmodal/utils/Var.lua new file mode 100644 index 0000000..ffd1ca4 --- /dev/null +++ b/lua/libmodal/utils/Var.lua @@ -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 diff --git a/lua/libmodal/utils/Vars.lua b/lua/libmodal/utils/Vars.lua deleted file mode 100644 index 77fbf8a..0000000 --- a/lua/libmodal/utils/Vars.lua +++ /dev/null @@ -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 diff --git a/lua/libmodal/utils/init.lua b/lua/libmodal/utils/init.lua index 9083c96..8a21fc8 100644 --- a/lua/libmodal/utils/init.lua +++ b/lua/libmodal/utils/init.lua @@ -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`.