From 4d04feb6629cb049cb2a13ba35f0c8d3c6b67ff4 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 14 Apr 2023 10:39:57 +0200 Subject: [PATCH] feat(lua): vim.tbl_contains supports general tables and predicates (#23040) * feat(lua): vim.tbl_contains supports general tables and predicates Problem: `vim.tbl_contains` only works for list-like tables (integer keys without gaps) and primitive values (in particular, not for nested tables). Solution: Rename `vim.tbl_contains` to `vim.list_contains` and add new `vim.tbl_contains` that works for general tables and optionally allows `value` to be a predicate function that is checked for every key. --- runtime/doc/lua.txt | 34 +++++++++++++++++++++--- runtime/doc/news.txt | 6 +++-- runtime/lua/editorconfig.lua | 2 +- runtime/lua/nvim/health.lua | 2 +- runtime/lua/provider/health.lua | 2 +- runtime/lua/vim/loader.lua | 4 +-- runtime/lua/vim/lsp.lua | 4 +-- runtime/lua/vim/lsp/_snippet.lua | 4 +-- runtime/lua/vim/lsp/codelens.lua | 2 +- runtime/lua/vim/lsp/util.lua | 2 +- runtime/lua/vim/shared.lua | 45 ++++++++++++++++++++++++++++++-- scripts/gen_help_html.lua | 8 +++--- scripts/lintcommit.lua | 2 +- test/functional/lua/vim_spec.lua | 16 ++++++++++++ 14 files changed, 110 insertions(+), 23 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 2424c73f8f..f39adf4f8d 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1698,6 +1698,19 @@ is_callable({f}) *vim.is_callable()* Return: ~ (boolean) `true` if `f` is callable, else `false` +list_contains({t}, {value}) *vim.list_contains()* + Checks if a list-like table (integer keys without gaps) contains `value`. + + Parameters: ~ + • {t} (table) Table to check (must be list-like, not validated) + • {value} any Value to compare + + Return: ~ + (boolean) `true` if `t` contains `value` + + See also: ~ + • |vim.tbl_contains()| for checking values in general tables + list_extend({dst}, {src}, {start}, {finish}) *vim.list_extend()* Extends a list-like table with the values of another list-like table. @@ -1797,16 +1810,31 @@ tbl_add_reverse_lookup({o}) *vim.tbl_add_reverse_lookup()* Return: ~ (table) o -tbl_contains({t}, {value}) *vim.tbl_contains()* - Checks if a list-like (vector) table contains `value`. +tbl_contains({t}, {value}, {opts}) *vim.tbl_contains()* + Checks if a table contains a given value, specified either directly or via + a predicate that is checked for each value. + + Example: >lua + + vim.tbl_contains({ 'a', { 'b', 'c' } }, function(v) + return vim.deep_equal(v, { 'b', 'c' }) + end, { predicate = true }) + -- true +< Parameters: ~ • {t} (table) Table to check - • {value} any Value to compare + • {value} any Value to compare or predicate function reference + • {opts} (table|nil) Keyword arguments |kwargs|: + • predicate: (boolean) `value` is a function reference to be + checked (default false) Return: ~ (boolean) `true` if `t` contains `value` + See also: ~ + • |vim.list_contains()| for checking values in list-like tables + tbl_count({t}) *vim.tbl_count()* Counts the number of non-nil values in table `t`. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index ba428679f0..aff1a4a24e 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -37,6 +37,10 @@ CHANGED FEATURES *news-changed* The following changes to existing APIs or features add new behavior. +• |vim.tbl_contains()| now works for general tables and allows specifying a + predicate function that is checked for each value. (Use |vim.list_contains()| + for checking list-like tables (integer keys without gaps) for literal values.) + • |vim.region()| can use a string accepted by |getpos()| as position. ============================================================================== @@ -44,8 +48,6 @@ REMOVED FEATURES *news-removed* The following deprecated functions or APIs were removed. -• ... - • Vimball support is removed. - :Vimuntar command removed. diff --git a/runtime/lua/editorconfig.lua b/runtime/lua/editorconfig.lua index 5b09126788..5188c13284 100644 --- a/runtime/lua/editorconfig.lua +++ b/runtime/lua/editorconfig.lua @@ -26,7 +26,7 @@ end function M.properties.charset(bufnr, val) assert( - vim.tbl_contains({ 'utf-8', 'utf-8-bom', 'latin1', 'utf-16be', 'utf-16le' }, val), + vim.list_contains({ 'utf-8', 'utf-8-bom', 'latin1', 'utf-16be', 'utf-16le' }, val), 'charset must be one of "utf-8", "utf-8-bom", "latin1", "utf-16be", or "utf-16le"' ) if val == 'utf-8' or val == 'utf-8-bom' then diff --git a/runtime/lua/nvim/health.lua b/runtime/lua/nvim/health.lua index b6d84404ec..6f544e9407 100644 --- a/runtime/lua/nvim/health.lua +++ b/runtime/lua/nvim/health.lua @@ -325,7 +325,7 @@ local function check_tmux() -- check for RGB capabilities local info = vim.fn.system({ 'tmux', 'display-message', '-p', '#{client_termfeatures}' }) info = vim.split(vim.trim(info), ',', { trimempty = true }) - if not vim.tbl_contains(info, 'RGB') then + if not vim.list_contains(info, 'RGB') then local has_rgb = false if #info == 0 then -- client_termfeatures may not be supported; fallback to checking show-messages diff --git a/runtime/lua/provider/health.lua b/runtime/lua/provider/health.lua index 8ebc8ccb17..a5fe14732c 100644 --- a/runtime/lua/provider/health.lua +++ b/runtime/lua/provider/health.lua @@ -449,7 +449,7 @@ local function python() local path_bin = vim.fs.normalize(path .. '/' .. pyname) if path_bin ~= vim.fs.normalize(python_exe) - and vim.tbl_contains(python_multiple, path_bin) + and vim.list_contains(python_multiple, path_bin) and executable(path_bin) then python_multiple[#python_multiple + 1] = path_bin diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua index 7f953adc21..66627fe4e7 100644 --- a/runtime/lua/vim/loader.lua +++ b/runtime/lua/vim/loader.lua @@ -449,7 +449,7 @@ function Loader.lsmod(path) if topname then Loader._indexed[path][topname] = { modpath = modpath, modname = topname } Loader._topmods[topname] = Loader._topmods[topname] or {} - if not vim.tbl_contains(Loader._topmods[topname], path) then + if not vim.list_contains(Loader._topmods[topname], path) then table.insert(Loader._topmods[topname], path) end end @@ -523,7 +523,7 @@ function M._inspect(opts) { ms(Loader._stats[stat].time / Loader._stats[stat].total) .. '\n', 'Bold' }, }) for k, v in pairs(Loader._stats[stat]) do - if not vim.tbl_contains({ 'time', 'total' }, k) then + if not vim.list_contains({ 'time', 'total' }, k) then chunks[#chunks + 1] = { '* ' .. k .. ':' .. string.rep(' ', 9 - #k) } chunks[#chunks + 1] = { tostring(v) .. '\n', 'Number' } end diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 3db3545786..5c78bd7580 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -171,7 +171,7 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids) if restrict_client_ids and #restrict_client_ids > 0 then local filtered_client_ids = {} for client_id in pairs(client_ids) do - if vim.tbl_contains(restrict_client_ids, client_id) then + if vim.list_contains(restrict_client_ids, client_id) then filtered_client_ids[client_id] = true end end @@ -2186,7 +2186,7 @@ function lsp.formatexpr(opts) opts = opts or {} local timeout_ms = opts.timeout_ms or 500 - if vim.tbl_contains({ 'i', 'R', 'ic', 'ix' }, vim.fn.mode()) then + if vim.list_contains({ 'i', 'R', 'ic', 'ix' }, vim.fn.mode()) then -- `formatexpr` is also called when exceeding `textwidth` in insert mode -- fall back to internal formatting return 1 diff --git a/runtime/lua/vim/lsp/_snippet.lua b/runtime/lua/vim/lsp/_snippet.lua index 797d8960d5..e7ada5415f 100644 --- a/runtime/lua/vim/lsp/_snippet.lua +++ b/runtime/lua/vim/lsp/_snippet.lua @@ -17,14 +17,14 @@ P.take_until = function(targets, specials) table.insert(raw, '\\') new_pos = new_pos + 1 c = string.sub(input, new_pos, new_pos) - if not vim.tbl_contains(targets, c) and not vim.tbl_contains(specials, c) then + if not vim.list_contains(targets, c) and not vim.list_contains(specials, c) then table.insert(esc, '\\') end table.insert(raw, c) table.insert(esc, c) new_pos = new_pos + 1 else - if vim.tbl_contains(targets, c) then + if vim.list_contains(targets, c) then break end table.insert(raw, c) diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 81cac6a511..005a0047fa 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -42,7 +42,7 @@ local function execute_lens(lens, bufnr, client_id) -- Need to use the client that returned the lens → must not use buf_request local command_provider = client.server_capabilities.executeCommandProvider local commands = type(command_provider) == 'table' and command_provider.commands or {} - if not vim.tbl_contains(commands, command.command) then + if not vim.list_contains(commands, command.command) then vim.notify( string.format( 'Language server does not support command `%s`. This command may require a client extension.', diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index dca258e4b9..31af2afb0b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1476,7 +1476,7 @@ end local function close_preview_window(winnr, bufnrs) vim.schedule(function() -- exit if we are in one of ignored buffers - if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then + if bufnrs and vim.list_contains(bufnrs, api.nvim_get_current_buf()) then return end diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9245adae3a..7ecb56eb92 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -252,12 +252,53 @@ function vim.tbl_filter(func, t) return rettab end ---- Checks if a list-like (vector) table contains `value`. +--- Checks if a table contains a given value, specified either directly or via +--- a predicate that is checked for each value. +--- +--- Example: +---
lua
+---  vim.tbl_contains({ 'a', { 'b', 'c' } }, function(v)
+---    return vim.deep_equal(v, { 'b', 'c' })
+---  end, { predicate = true })
+---  -- true
+--- 
+--- +---@see |vim.list_contains()| for checking values in list-like tables --- ---@param t table Table to check +---@param value any Value to compare or predicate function reference +---@param opts (table|nil) Keyword arguments |kwargs|: +--- - predicate: (boolean) `value` is a function reference to be checked (default false) +---@return boolean `true` if `t` contains `value` +function vim.tbl_contains(t, value, opts) + vim.validate({ t = { t, 't' }, opts = { opts, 't', true } }) + + local pred + if opts and opts.predicate then + vim.validate({ value = { value, 'c' } }) + pred = value + else + pred = function(v) + return v == value + end + end + + for _, v in pairs(t) do + if pred(v) then + return true + end + end + return false +end + +--- Checks if a list-like table (integer keys without gaps) contains `value`. +--- +---@see |vim.tbl_contains()| for checking values in general tables +--- +---@param t table Table to check (must be list-like, not validated) ---@param value any Value to compare ---@return boolean `true` if `t` contains `value` -function vim.tbl_contains(t, value) +function vim.list_contains(t, value) vim.validate({ t = { t, 't' } }) for _, v in ipairs(t) do diff --git a/scripts/gen_help_html.lua b/scripts/gen_help_html.lua index 367ce60765..e2ab70eca2 100644 --- a/scripts/gen_help_html.lua +++ b/scripts/gen_help_html.lua @@ -491,7 +491,7 @@ local function visit_node(root, level, lang_tree, headings, opt, stats) return '' -- Discard common "noise" lines. end -- XXX: Avoid newlines (too much whitespace) after block elements in old (preformatted) layout. - local div = opt.old and root:child(0) and vim.tbl_contains({'column_heading', 'h1', 'h2', 'h3'}, root:child(0):type()) + local div = opt.old and root:child(0) and vim.list_contains({'column_heading', 'h1', 'h2', 'h3'}, root:child(0):type()) return string.format('%s%s', div and trim(text) or text, div and '' or '\n') elseif node_name == 'line_li' then local sib = root:prev_sibling() @@ -522,7 +522,7 @@ local function visit_node(root, level, lang_tree, headings, opt, stats) s = fix_tab_after_conceal(s, node_text(root:next_sibling())) end return s - elseif vim.tbl_contains({'codespan', 'keycode'}, node_name) then + elseif vim.list_contains({'codespan', 'keycode'}, node_name) then if root:has_error() then return text end @@ -554,7 +554,7 @@ local function visit_node(root, level, lang_tree, headings, opt, stats) if root:has_error() then return text end - local in_heading = vim.tbl_contains({'h1', 'h2', 'h3'}, parent) + local in_heading = vim.list_contains({'h1', 'h2', 'h3'}, parent) local cssclass = (not in_heading and get_indent(node_text()) > 8) and 'help-tag-right' or 'help-tag' local tagname = node_text(root:child(1), false) if vim.tbl_count(stats.first_tags) < 2 then @@ -601,7 +601,7 @@ local function get_helpfiles(include) for f, type in vim.fs.dir(dir) do if (vim.endswith(f, '.txt') and type == 'file' - and (not include or vim.tbl_contains(include, f))) then + and (not include or vim.list_contains(include, f))) then local fullpath = vim.fn.fnamemodify(('%s/%s'):format(dir, f), ':p') table.insert(rv, fullpath) end diff --git a/scripts/lintcommit.lua b/scripts/lintcommit.lua index f9a4631b7e..7f3dffd22c 100644 --- a/scripts/lintcommit.lua +++ b/scripts/lintcommit.lua @@ -88,7 +88,7 @@ local function validate_commit(commit_message) -- Check if type is correct local type = vim.split(before_colon, "(", {plain = true})[1] local allowed_types = {'build', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'test', 'vim-patch'} - if not vim.tbl_contains(allowed_types, type) then + if not vim.list_contains(allowed_types, type) then return string.format( [[Invalid commit type "%s". Allowed types are: %s. diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 85e45788e8..6b69018bc0 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -461,6 +461,22 @@ describe('lua stdlib', function() pcall_err(exec_lua, [[return vim.pesc(2)]])) end) + it('vim.list_contains', function() + eq(true, exec_lua("return vim.list_contains({'a','b','c'}, 'c')")) + eq(false, exec_lua("return vim.list_contains({'a','b','c'}, 'd')")) + end) + + it('vim.tbl_contains', function() + eq(true, exec_lua("return vim.tbl_contains({'a','b','c'}, 'c')")) + eq(false, exec_lua("return vim.tbl_contains({'a','b','c'}, 'd')")) + eq(true, exec_lua("return vim.tbl_contains({[2]='a',foo='b',[5] = 'c'}, 'c')")) + eq(true, exec_lua([[ + return vim.tbl_contains({ 'a', { 'b', 'c' } }, function(v) + return vim.deep_equal(v, { 'b', 'c' }) + end, { predicate = true }) + ]])) + end) + it('vim.tbl_keys', function() eq({}, exec_lua("return vim.tbl_keys({})")) for _, v in pairs(exec_lua("return vim.tbl_keys({'a', 'b', 'c'})")) do