From 48251134ee59a3e2f0aeb89608fa820c21b25d4f Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Mon, 27 May 2024 08:08:23 -0500 Subject: perf: add fast path to vim.validate (#28977) For many small/simple functions (like those found in shared.lua), the runtime of vim.validate can far exceed the runtime of the function itself. Add an "overload" to vim.validate that uses a simple assertion pattern, rather than parsing a full "validation spec". --- runtime/lua/vim/shared.lua | 76 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index e9e4326057..2641d1feb0 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -214,7 +214,7 @@ end ---@param t table (table) Table ---@return T[] : List of keys function vim.tbl_keys(t) - vim.validate({ t = { t, 't' } }) + vim.validate('t', t, 'table') --- @cast t table local keys = {} @@ -231,7 +231,7 @@ end ---@param t table (table) Table ---@return T[] : List of values function vim.tbl_values(t) - vim.validate({ t = { t, 't' } }) + vim.validate('t', t, 'table') local values = {} for _, v in @@ -332,7 +332,7 @@ end ---@param value any Value to compare ---@return boolean `true` if `t` contains `value` function vim.list_contains(t, value) - vim.validate({ t = { t, 't' } }) + vim.validate('t', t, 'table') --- @cast t table for _, v in ipairs(t) do @@ -350,7 +350,7 @@ end ---@param t table Table to check ---@return boolean `true` if `t` is empty function vim.tbl_isempty(t) - vim.validate({ t = { t, 't' } }) + vim.validate('t', t, 'table') return next(t) == nil end @@ -580,7 +580,7 @@ end ---@return fun(table: table, index?: K):K, V # |for-in| iterator over sorted keys and their values ---@return T function vim.spairs(t) - assert(type(t) == 'table', ('expected table, got %s'):format(type(t))) + vim.validate('t', t, 'table') --- @cast t table -- collect the keys @@ -691,7 +691,7 @@ end ---@param t table Table ---@return integer : Number of non-nil values in table function vim.tbl_count(t) - vim.validate({ t = { t, 't' } }) + vim.validate('t', t, 'table') --- @cast t table local count = 0 @@ -723,7 +723,7 @@ end ---@param s string String to trim ---@return string String with whitespace removed from its beginning and end function vim.trim(s) - vim.validate({ s = { s, 's' } }) + vim.validate('s', s, 'string') return s:match('^%s*(.*%S)') or '' end @@ -733,7 +733,7 @@ end ---@param s string String to escape ---@return string %-escaped pattern string function vim.pesc(s) - vim.validate({ s = { s, 's' } }) + vim.validate('s', s, 'string') return (s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')) end @@ -743,7 +743,8 @@ end ---@param prefix string Prefix to match ---@return boolean `true` if `prefix` is a prefix of `s` function vim.startswith(s, prefix) - vim.validate({ s = { s, 's' }, prefix = { prefix, 's' } }) + vim.validate('s', s, 'string') + vim.validate('prefix', prefix, 'string') return s:sub(1, #prefix) == prefix end @@ -753,7 +754,8 @@ end ---@param suffix string Suffix to match ---@return boolean `true` if `suffix` is a suffix of `s` function vim.endswith(s, suffix) - vim.validate({ s = { s, 's' }, suffix = { suffix, 's' } }) + vim.validate('s', s, 'string') + vim.validate('suffix', suffix, 'string') return #suffix == 0 or s:sub(-#suffix) == suffix end @@ -877,8 +879,30 @@ do return true end - --- Validates a parameter specification (types and values). Specs are evaluated in alphanumeric - --- order, until the first failure. + --- Validate function arguments. + --- + --- This function has two valid forms: + --- + --- 1. vim.validate(name: str, value: any, type: string, optional?: bool) + --- 2. vim.validate(spec: table) + --- + --- Form 1 validates that argument {name} with value {value} has the type + --- {type}. {type} must be a value returned by |lua-type()|. If {optional} is + --- true, then {value} may be null. This form is significantly faster and + --- should be preferred for simple cases. + --- + --- Example: + --- + --- ```lua + --- function vim.startswith(s, prefix) + --- vim.validate('s', s, 'string') + --- vim.validate('prefix', prefix, 'string') + --- ... + --- end + --- ``` + --- + --- Form 2 validates a parameter specification (types and values). Specs are + --- evaluated in alphanumeric order, until the first failure. --- --- Usage example: --- @@ -930,8 +954,32 @@ do --- only if the argument is valid. Can optionally return an additional --- informative error message as the second returned value. --- - msg: (optional) error string if validation fails - function vim.validate(opt) - local ok, err_msg = is_valid(opt) + --- @overload fun(name: string, val: any, expected: string, optional?: boolean) + function vim.validate(opt, ...) + local ok = false + local err_msg ---@type string? + local narg = select('#', ...) + if narg == 0 then + ok, err_msg = is_valid(opt) + elseif narg >= 2 then + -- Overloaded signature for fast/simple cases + local name = opt --[[@as string]] + local v, expected, optional = ... ---@type string, string, boolean? + local actual = type(v) + + ok = (actual == expected) or (v == nil and optional == true) + if not ok then + err_msg = ('%s: expected %s, got %s%s'):format( + name, + expected, + actual, + v and (' (%s)'):format(v) or '' + ) + end + else + error('invalid arguments') + end + if not ok then error(err_msg, 2) end -- cgit From efa45832ea02e777ce3f5556ef3cd959c164ec24 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Tue, 28 May 2024 14:54:50 -0500 Subject: feat: add "jump" options to vim.diagnostic.config() (#29067) Problem: There is no easy way to configure the behavior of the default diagnostic "jump" mappings. For example, some users way want to show the floating window, and some may not (likewise, some way want to only move between warnings/errors, or disable the "wrap" parameter). Solution: Add a "jump" table to vim.diagnostic.config() that sets default values for vim.diagnostic.jump(). Alternatives: Users can override the default mappings to use the exact options to vim.diagnostic.jump() that they want, but this has a couple issues: - While the default mappings are not complicated, they are also not trivial, so overriding them requires users to understand implementation details (specifically things like setting "count" properly). - If plugins want to change the default mappings, or configure the behavior in any way (e.g. floating window display), it becomes even harder for users to tweak specific behavior. vim.diagnostic.config() already works quite well as the "entry point" for tuning knobs with diagnostic UI elements, so this fits in nicely and composes well with existing mental models and idioms. --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 2641d1feb0..0ec79e1dc7 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -379,7 +379,7 @@ local function tbl_extend(behavior, deep_extend, ...) for i = 1, select('#', ...) do local tbl = select(i, ...) - vim.validate({ ['after the second argument'] = { tbl, 't' } }) + vim.validate('after the second argument', tbl, 'table') --- @cast tbl table if tbl then for k, v in pairs(tbl) do -- cgit From 8cbb1f20e557461c8417583a7f69d53aaaef920b Mon Sep 17 00:00:00 2001 From: Ilia Choly Date: Tue, 4 Jun 2024 09:06:02 -0400 Subject: refactor(lua): use tuple syntax everywhere #29111 --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 0ec79e1dc7..9d54a80144 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -789,7 +789,7 @@ do } --- @nodoc - --- @class vim.validate.Spec {[1]: any, [2]: string|string[], [3]: boolean } + --- @class vim.validate.Spec [any, string|string[], boolean] --- @field [1] any Argument value --- @field [2] string|string[]|fun(v:any):boolean, string? Type name, or callable --- @field [3]? boolean -- cgit From 9afa1fd35510c5fe485f4a1dfdabf94e5f051a1c Mon Sep 17 00:00:00 2001 From: dundargoc Date: Mon, 3 Jun 2024 19:04:28 +0200 Subject: feat(lua): add `vim._with` It's a function to perform operations in their own sealed context, similar to pythons `with`. This helps ease operations where you need to perform an operation in a specific context, and then restore the context. Marked as private for now as it's not ready for public use. The current plan is to start using this internally so we can discover and fix any problems. Once this is ready to be exposed it will be renamed to `vim.with`. Usage: ```lua local ret = vim._with({context = val}, function() return "hello" end) ``` , where `context` is any combination of: - `buf` - `emsg_silent` - `hide` - `horizontal` - `keepalt` - `keepjumps` - `keepmarks` - `keeppatterns` - `lockmarks` - `noautocmd` - `options` - `sandbox` - `silent` - `unsilent` - `win` (except for `win` and `buf` which can't be used at the same time). This list will most likely be expanded in the future. Work on https://github.com/neovim/neovim/issues/19832. Co-authored-by: Lewis Russell --- runtime/lua/vim/shared.lua | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9d54a80144..7fd29d5f7b 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1139,4 +1139,82 @@ function vim._defer_require(root, mod) }) end +--- @nodoc +--- @class vim.context.mods +--- @field buf? integer +--- @field emsg_silent? boolean +--- @field hide? boolean +--- @field horizontal? boolean +--- @field keepalt? boolean +--- @field keepjumps? boolean +--- @field keepmarks? boolean +--- @field keeppatterns? boolean +--- @field lockmarks? boolean +--- @field noautocmd? boolean +--- @field options? table +--- @field sandbox? boolean +--- @field silent? boolean +--- @field unsilent? boolean +--- @field win? integer + +--- Executes function `f` with the given context specification. +--- +--- @param context vim.context.mods +function vim._with(context, f) + vim.validate('context', context, 'table') + vim.validate('f', f, 'function') + + vim.validate('context.buf', context.buf, 'number', true) + vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) + vim.validate('context.hide', context.hide, 'boolean', true) + vim.validate('context.horizontal', context.horizontal, 'boolean', true) + vim.validate('context.keepalt', context.keepalt, 'boolean', true) + vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) + vim.validate('context.keepmarks', context.keepmarks, 'boolean', true) + vim.validate('context.keeppatterns', context.keeppatterns, 'boolean', true) + vim.validate('context.lockmarks', context.lockmarks, 'boolean', true) + vim.validate('context.noautocmd', context.noautocmd, 'boolean', true) + vim.validate('context.options', context.options, 'table', true) + vim.validate('context.sandbox', context.sandbox, 'boolean', true) + vim.validate('context.silent', context.silent, 'boolean', true) + vim.validate('context.unsilent', context.unsilent, 'boolean', true) + vim.validate('context.win', context.win, 'number', true) + + -- Check buffer exists + if context.buf then + if not vim.api.nvim_buf_is_valid(context.buf) then + error('Invalid buffer id: ' .. context.buf) + end + end + + -- Check window exists + if context.win then + if not vim.api.nvim_win_is_valid(context.win) then + error('Invalid window id: ' .. context.win) + end + end + + -- Store original options + local previous_options ---@type table + if context.options then + previous_options = {} + for k, v in pairs(context.options) do + previous_options[k] = + vim.api.nvim_get_option_value(k, { win = context.win, buf = context.buf }) + vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + end + end + + local retval = { vim._with_c(context, f) } + + -- Restore original options + if previous_options then + for k, v in pairs(previous_options) do + vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + end + end + + return unpack(retval) +end + return vim -- cgit From f8795365deb88ab4e108858c563284b1082d06d4 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 21 Jun 2024 16:23:02 +0300 Subject: test(lua): cover `vim._with()` with tests Problem: `vim._with()` has many different use cases which are not covered with tests. Solution: cover with tests. Some (many) test cases are intentionally marked as "pending" because they cover cases which don't work as expected at the moment (and fixing them requires specific knowledge of C codebase). Use them as a reference for future fixes. Also some of "can be nested" tests currently might pass only because the tested context doesn't work. --- runtime/lua/vim/shared.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 7fd29d5f7b..79e614aaaa 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1144,7 +1144,6 @@ end --- @field buf? integer --- @field emsg_silent? boolean --- @field hide? boolean ---- @field horizontal? boolean --- @field keepalt? boolean --- @field keepjumps? boolean --- @field keepmarks? boolean @@ -1159,6 +1158,15 @@ end --- Executes function `f` with the given context specification. --- +--- Notes: +--- - Context `{ buf = buf }` has no guarantees about current window when +--- inside context. +--- - Context `{ buf = buf, win = win }` is yet not allowed, but this seems +--- to be an implementation detail. +--- - There should be no way to revert currently set `context.sandbox = true` +--- (like with nested `vim._with()` calls). Otherwise it kind of breaks the +--- whole purpose of sandbox execution. +--- --- @param context vim.context.mods function vim._with(context, f) vim.validate('context', context, 'table') @@ -1167,7 +1175,6 @@ function vim._with(context, f) vim.validate('context.buf', context.buf, 'number', true) vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) vim.validate('context.hide', context.hide, 'boolean', true) - vim.validate('context.horizontal', context.horizontal, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) vim.validate('context.keepmarks', context.keepmarks, 'boolean', true) @@ -1192,6 +1199,10 @@ function vim._with(context, f) if not vim.api.nvim_win_is_valid(context.win) then error('Invalid window id: ' .. context.win) end + -- TODO: Maybe allow it? + if context.buf and vim.api.nvim_win_get_buf(context.win) ~= context.buf then + error('Can not set both `buf` and `win` context.') + end end -- Store original options @@ -1214,7 +1225,7 @@ function vim._with(context, f) end end - return unpack(retval) + return unpack(retval, 1, table.maxn(retval)) end return vim -- cgit From 07cc559cdf11acb3031b6b7ba53e65285a6f4b3f Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 21 Jun 2024 16:23:05 +0300 Subject: feat(lua): update `vim._with` to allow more granular option contexts Problem: with a single `context.options` there is no way for user to force which scope (local, global, both) is being temporarily set and later restored. Solution: replace single `options` context with `bo`, `go`, `wo`, and `o`. Naming and implementation follows how options can be set directly with `vim.*` (like `vim.bo`, etc.). Options are set for possible target `win` or `buf` context. --- runtime/lua/vim/shared.lua | 99 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 79e614aaaa..0c20248d8e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1141,8 +1141,10 @@ end --- @nodoc --- @class vim.context.mods +--- @field bo? table --- @field buf? integer --- @field emsg_silent? boolean +--- @field go? table --- @field hide? boolean --- @field keepalt? boolean --- @field keepjumps? boolean @@ -1150,11 +1152,47 @@ end --- @field keeppatterns? boolean --- @field lockmarks? boolean --- @field noautocmd? boolean ---- @field options? table +--- @field o? table --- @field sandbox? boolean --- @field silent? boolean --- @field unsilent? boolean --- @field win? integer +--- @field wo? table + +--- @nodoc +--- @class vim.context.state +--- @field bo? table +--- @field go? table +--- @field wo? table + +local scope_map = { buf = 'bo', global = 'go', win = 'wo' } +local scope_order = { 'o', 'wo', 'bo', 'go' } +local state_restore_order = { 'bo', 'wo', 'go' } + +--- Gets data about current state, enough to properly restore specified options/env/etc. +--- @param context vim.context.mods +--- @return vim.context.state +local get_context_state = function(context) + local res = { bo = {}, go = {}, wo = {} } + + -- Use specific order from possibly most to least intrusive + for _, scope in ipairs(scope_order) do + for name, _ in pairs(context[scope] or {}) do + local sc = scope == 'o' and scope_map[vim.api.nvim_get_option_info2(name, {}).scope] or scope + + -- Do not override already set state and fall back to `vim.NIL` for + -- state `nil` values (which still needs restoring later) + res[sc][name] = res[sc][name] or vim[sc][name] or vim.NIL + + -- Always track global option value to properly restore later. + -- This matters for at least `o` and `wo` (which might set either/both + -- local and global option values). + res.go[name] = res.go[name] or vim.go[name] + end + end + + return res +end --- Executes function `f` with the given context specification. --- @@ -1166,14 +1204,24 @@ end --- - There should be no way to revert currently set `context.sandbox = true` --- (like with nested `vim._with()` calls). Otherwise it kind of breaks the --- whole purpose of sandbox execution. +--- - Saving and restoring option contexts (`bo`, `go`, `o`, `wo`) trigger +--- `OptionSet` events. This is an implementation issue because not doing it +--- seems to mean using either 'eventignore' option or extra nesting with +--- `{ noautocmd = true }` (which itself is a wrapper for 'eventignore'). +--- As `{ go = { eventignore = '...' } }` is a valid context which should be +--- properly set and restored, this is not a good approach. +--- Not triggering `OptionSet` seems to be a good idea, though. So probably +--- only moving context save and restore to lower level might resolve this. --- --- @param context vim.context.mods function vim._with(context, f) vim.validate('context', context, 'table') vim.validate('f', f, 'function') + vim.validate('context.bo', context.bo, 'table', true) vim.validate('context.buf', context.buf, 'number', true) vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) + vim.validate('context.go', context.go, 'table', true) vim.validate('context.hide', context.hide, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) vim.validate('context.keepjumps', context.keepjumps, 'boolean', true) @@ -1181,11 +1229,12 @@ function vim._with(context, f) vim.validate('context.keeppatterns', context.keeppatterns, 'boolean', true) vim.validate('context.lockmarks', context.lockmarks, 'boolean', true) vim.validate('context.noautocmd', context.noautocmd, 'boolean', true) - vim.validate('context.options', context.options, 'table', true) + vim.validate('context.o', context.o, 'table', true) vim.validate('context.sandbox', context.sandbox, 'boolean', true) vim.validate('context.silent', context.silent, 'boolean', true) vim.validate('context.unsilent', context.unsilent, 'boolean', true) vim.validate('context.win', context.win, 'number', true) + vim.validate('context.wo', context.wo, 'table', true) -- Check buffer exists if context.buf then @@ -1205,27 +1254,43 @@ function vim._with(context, f) end end - -- Store original options - local previous_options ---@type table - if context.options then - previous_options = {} - for k, v in pairs(context.options) do - previous_options[k] = - vim.api.nvim_get_option_value(k, { win = context.win, buf = context.buf }) - vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + -- Decorate so that save-set-restore options is done in correct window-buffer + local callback = function() + -- Cache current values to be changed by context + -- Abort early in case of bad context value + local ok, state = pcall(get_context_state, context) + if not ok then + error(state, 0) end - end - local retval = { vim._with_c(context, f) } + -- Apply some parts of the context in specific order + -- NOTE: triggers `OptionSet` event + for _, scope in ipairs(scope_order) do + for name, context_value in pairs(context[scope] or {}) do + vim[scope][name] = context_value + end + end + + -- Execute + local res = { pcall(f) } + + -- Restore relevant cached values in specific order, global scope last + -- NOTE: triggers `OptionSet` event + for _, scope in ipairs(state_restore_order) do + for name, cached_value in pairs(state[scope]) do + vim[scope][name] = cached_value + end + end - -- Restore original options - if previous_options then - for k, v in pairs(previous_options) do - vim.api.nvim_set_option_value(k, v, { win = context.win, buf = context.buf }) + -- Return + if not res[1] then + error(res[2], 0) end + table.remove(res, 1) + return unpack(res, 1, table.maxn(res)) end - return unpack(retval, 1, table.maxn(retval)) + return vim._with_c(context, callback) end return vim -- cgit From cd53db2157f0cd27877451a6b00d66e9bed74e73 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Sun, 23 Jun 2024 17:13:06 +0300 Subject: feat(lua): add `context.env` (environment variables) to `vim._with()` --- runtime/lua/vim/shared.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 0c20248d8e..6de1ce9d0c 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1144,6 +1144,7 @@ end --- @field bo? table --- @field buf? integer --- @field emsg_silent? boolean +--- @field env? table --- @field go? table --- @field hide? boolean --- @field keepalt? boolean @@ -1162,18 +1163,19 @@ end --- @nodoc --- @class vim.context.state --- @field bo? table +--- @field env? table --- @field go? table --- @field wo? table local scope_map = { buf = 'bo', global = 'go', win = 'wo' } -local scope_order = { 'o', 'wo', 'bo', 'go' } -local state_restore_order = { 'bo', 'wo', 'go' } +local scope_order = { 'o', 'wo', 'bo', 'go', 'env' } +local state_restore_order = { 'bo', 'wo', 'go', 'env' } --- Gets data about current state, enough to properly restore specified options/env/etc. --- @param context vim.context.mods --- @return vim.context.state local get_context_state = function(context) - local res = { bo = {}, go = {}, wo = {} } + local res = { bo = {}, env = {}, go = {}, wo = {} } -- Use specific order from possibly most to least intrusive for _, scope in ipairs(scope_order) do @@ -1187,7 +1189,9 @@ local get_context_state = function(context) -- Always track global option value to properly restore later. -- This matters for at least `o` and `wo` (which might set either/both -- local and global option values). - res.go[name] = res.go[name] or vim.go[name] + if sc ~= 'env' then + res.go[name] = res.go[name] or vim.go[name] + end end end @@ -1221,6 +1225,7 @@ function vim._with(context, f) vim.validate('context.bo', context.bo, 'table', true) vim.validate('context.buf', context.buf, 'number', true) vim.validate('context.emsg_silent', context.emsg_silent, 'boolean', true) + vim.validate('context.env', context.env, 'table', true) vim.validate('context.go', context.go, 'table', true) vim.validate('context.hide', context.hide, 'boolean', true) vim.validate('context.keepalt', context.keepalt, 'boolean', true) -- cgit From aa6b9c677d83d76d448c3bb0973bf8d14bfdf922 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Sat, 8 Jun 2024 21:40:18 +0200 Subject: refactor: use `vim._with` where possible This mostly means replacing `nvim_buf_call` and `nvim_win_call` with `vim._with`. --- runtime/lua/vim/shared.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 6de1ce9d0c..621de2b1c2 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -1218,6 +1218,8 @@ end --- only moving context save and restore to lower level might resolve this. --- --- @param context vim.context.mods +--- @param f function +--- @return any function vim._with(context, f) vim.validate('context', context, 'table') vim.validate('f', f, 'function') -- cgit From 7f33c1967b78ca8fda11fb0ad4c7f57d563e6ede Mon Sep 17 00:00:00 2001 From: Tyler Miller Date: Wed, 3 Jul 2024 15:36:00 -0700 Subject: fix(lua): use rawget() to get __call in vim.is_callable() (#29536) Lua 5.1 uses a "raw get" to retrieve `__call` from a metatable to determine if a table is callable. Mirror this behavior in `vim.is_callable()`. --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 621de2b1c2..99530bf72e 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -997,7 +997,7 @@ function vim.is_callable(f) if m == nil then return false end - return type(m.__call) == 'function' + return type(rawget(m, '__call')) == 'function' end --- Creates a table whose missing keys are provided by {createfn} (like Python's "defaultdict"). -- cgit From b6e350a6b4df40fcc99931c460668c36fadc9989 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 3 Sep 2024 17:14:25 +0100 Subject: fix(lua): allows tables with integer keys to be merged in tbl_deep_extend - The exclusion of lists was never justified in the commit history and is the wrong thing to do for a function that deals with tables. - Move the error checks out of the recursive path. Fixes #23654 --- runtime/lua/vim/shared.lua | 53 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 23 deletions(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 99530bf72e..2f10380bad 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -354,37 +354,22 @@ function vim.tbl_isempty(t) return next(t) == nil end ---- We only merge empty tables or tables that are not an array (indexed by integers) -local function can_merge(v) - return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.isarray(v)) -end - -local function tbl_extend(behavior, deep_extend, ...) - if behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force' then - error('invalid "behavior": ' .. tostring(behavior)) - end - - if select('#', ...) < 2 then - error( - 'wrong number of arguments (given ' - .. tostring(1 + select('#', ...)) - .. ', expected at least 3)' - ) - end - +--- Recursive worker for tbl_extend +--- @param behavior 'error'|'keep'|'force' +--- @param deep_extend boolean +--- @param ... table +local function tbl_extend_rec(behavior, deep_extend, ...) local ret = {} --- @type table if vim._empty_dict_mt ~= nil and getmetatable(select(1, ...)) == vim._empty_dict_mt then ret = vim.empty_dict() end for i = 1, select('#', ...) do - local tbl = select(i, ...) - vim.validate('after the second argument', tbl, 'table') - --- @cast tbl table + local tbl = select(i, ...) --[[@as table]] if tbl then for k, v in pairs(tbl) do - if deep_extend and can_merge(v) and can_merge(ret[k]) then - ret[k] = tbl_extend(behavior, true, ret[k], v) + if deep_extend and type(v) == 'table' and type(ret[k]) == 'table' then + ret[k] = tbl_extend_rec(behavior, true, ret[k], v) elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then error('key found in more than one map: ' .. k) @@ -395,9 +380,31 @@ local function tbl_extend(behavior, deep_extend, ...) end end end + return ret end +--- @param behavior 'error'|'keep'|'force' +--- @param deep_extend boolean +--- @param ... table +local function tbl_extend(behavior, deep_extend, ...) + if behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force' then + error('invalid "behavior": ' .. tostring(behavior)) + end + + local nargs = select('#', ...) + + if nargs < 2 then + error(('wrong number of arguments (given %d, expected at least 3)'):format(1 + nargs)) + end + + for i = 1, nargs do + vim.validate('after the second argument', select(i, ...), 'table') + end + + return tbl_extend_rec(behavior, deep_extend, ...) +end + --- Merges two or more tables. --- ---@see |extend()| -- cgit From 3a881132460430d23f2fdc87822c87d47f721cfc Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sun, 8 Sep 2024 18:23:46 +0200 Subject: fix(lua): revert vim.tbl_extend behavior change and document it Problem: vim.tbl_deep_extend had an undocumented feature where arrays (integer-indexed tables) were not merged but compared literally (used for merging default and user config, where one list should overwrite the other completely). Turns out this behavior was relied on in quite a number of plugins (even though it wasn't a robust solution even for that use case, since lists of tables (e.g., plugin specs) can be array-like as well). Solution: Revert the removal of this special feature. Check for list-like (contiguous integer indices) instead, as this is closer to the intent. Document this behavior. --- runtime/lua/vim/shared.lua | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/shared.lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 2f10380bad..4d06cdd77d 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -354,6 +354,12 @@ function vim.tbl_isempty(t) return next(t) == nil end +--- We only merge empty tables or tables that are not list-like (indexed by consecutive integers +--- starting from 1) +local function can_merge(v) + return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.islist(v)) +end + --- Recursive worker for tbl_extend --- @param behavior 'error'|'keep'|'force' --- @param deep_extend boolean @@ -368,7 +374,7 @@ local function tbl_extend_rec(behavior, deep_extend, ...) local tbl = select(i, ...) --[[@as table]] if tbl then for k, v in pairs(tbl) do - if deep_extend and type(v) == 'table' and type(ret[k]) == 'table' then + if deep_extend and can_merge(v) and can_merge(ret[k]) then ret[k] = tbl_extend_rec(behavior, true, ret[k], v) elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then @@ -421,6 +427,11 @@ end --- Merges recursively two or more tables. --- +--- Only values that are empty tables or tables that are not |lua-list|s (indexed by consecutive +--- integers starting from 1) are merged recursively. This is useful for merging nested tables +--- like default and user configurations where lists should be treated as literals (i.e., are +--- overwritten instead of merged). +--- ---@see |vim.tbl_extend()| --- ---@generic T1: table -- cgit