diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index a390d74e09..2e5b25f89a 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2092,6 +2092,35 @@ vim.islist({t}) *vim.islist()* See also: ~ • |vim.isarray()| +vim.list.unique({t}, {key}) *vim.list.unique()* + Removes duplicate values from a list-like table in-place. + + Only the first occurrence of each value is kept. The operation is + performed in-place and the input table is modified. + + Accepts an optional `hash` argument that if provided is called for each + value in the list to compute a hash key for uniqueness comparison. This is + useful for deduplicating table values or complex objects. + + Example: >lua + + local t = {1, 2, 2, 3, 1} + vim.list.unique(t) + -- t is now {1, 2, 3} + + local t = { {id=1}, {id=2}, {id=1} } + vim.list.unique(t, function(x) return x.id end) + -- t is now { {id=1}, {id=2} } +< + + Parameters: ~ + • {t} (`any[]`) + • {key} (`fun(x: T): any??`) Optional hash function to determine + uniqueness of values + + Return: ~ + (`any[]`) The deduplicated list + vim.list_contains({t}, {value}) *vim.list_contains()* Checks if a list-like table (integer keys without gaps) contains `value`. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 0f02d5b586..e6437a354c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -227,6 +227,7 @@ LUA • |vim.version.intersect()| computes intersection of two version ranges. • |Iter:take()| and |Iter:skip()| now optionally accept predicates. • Built-in plugin manager |vim.pack| +• |vim.list.unique()| to deduplicate lists. OPTIONS diff --git a/runtime/doc/vimfn.txt b/runtime/doc/vimfn.txt index 80030d0466..9cd2464f4e 100644 --- a/runtime/doc/vimfn.txt +++ b/runtime/doc/vimfn.txt @@ -11674,6 +11674,8 @@ undotree([{buf}]) *undotree()* (`vim.fn.undotree.ret`) uniq({list} [, {func} [, {dict}]]) *uniq()* *E882* + Note: Prefer |vim.list.unique()| in Lua. + Remove second and succeeding copies of repeated adjacent {list} items in-place. Returns {list}. If you want a list to remain unmodified make a copy first: >vim diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index ff800d7d87..1674a52289 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -10628,6 +10628,8 @@ function vim.fn.undofile(name) end --- @return vim.fn.undotree.ret function vim.fn.undotree(buf) end +--- Note: Prefer |vim.list.unique()| in Lua. +--- --- Remove second and succeeding copies of repeated adjacent --- {list} items in-place. Returns {list}. If you want a list --- to remain unmodified make a copy first: >vim diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9476654e37..ccf7674253 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -348,6 +348,62 @@ function vim.list_contains(t, value) return false end +vim.list = {} + +--- Removes duplicate values from a list-like table in-place. +--- +--- Only the first occurrence of each value is kept. +--- The operation is performed in-place and the input table is modified. +--- +--- Accepts an optional `hash` argument that if provided is called for each +--- value in the list to compute a hash key for uniqueness comparison. +--- This is useful for deduplicating table values or complex objects. +--- +--- Example: +--- ```lua +--- +--- local t = {1, 2, 2, 3, 1} +--- vim.list.unique(t) +--- -- t is now {1, 2, 3} +--- +--- local t = { {id=1}, {id=2}, {id=1} } +--- vim.list.unique(t, function(x) return x.id end) +--- -- t is now { {id=1}, {id=2} } +--- ``` +--- +--- @generic T +--- @param t T[] +--- @param key? fun(x: T): any? Optional hash function to determine uniqueness of values +--- @return T[] : The deduplicated list +function vim.list.unique(t, key) + vim.validate('t', t, 'table') + local seen = {} --- @type table + + local finish = #t + key = key or function(a) + return a + end + + local j = 1 + for i = 1, finish do + local v = t[i] + local vh = key(v) + if not seen[vh] then + t[j] = v + if vh ~= nil then + seen[vh] = true + end + j = j + 1 + end + end + + for i = j, finish do + t[i] = nil + end + + return t +end + --- Checks if a table is empty. --- ---@see https://github.com/premake/premake-core/blob/master/src/base/table.lua diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index aba1f9aa9a..e32f27edef 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -12856,6 +12856,8 @@ M.funcs = { base = 1, tags = { 'E882' }, desc = [=[ + Note: Prefer |vim.list.unique()| in Lua. + Remove second and succeeding copies of repeated adjacent {list} items in-place. Returns {list}. If you want a list to remain unmodified make a copy first: >vim diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 9372f5c7b7..14e08cffba 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1260,6 +1260,28 @@ describe('lua stdlib', function() eq({ 2 }, exec_lua [[ return vim.list_extend({}, {2;a=1}, -1, 2) ]]) end) + it('vim.list.unique', function() + eq({ 1, 2, 3, 4, 5 }, vim.list.unique({ 1, 2, 2, 3, 4, 4, 5 })) + eq({ 1, 2, 3, 4, 5 }, vim.list.unique({ 1, 2, 3, 4, 4, 5, 1, 2, 3, 2, 1, 2, 3, 4, 5 })) + eq({ 1, 2, 3, 4, 5, field = 1 }, vim.list.unique({ 1, 2, 2, 3, 4, 4, 5, field = 1 })) + + -- Not properly defined, but test anyway + -- luajit evaluates #t as 7, whereas Lua 5.1 evaluates it as 12 + local r = vim.list.unique({ 1, 2, 2, 3, 4, 4, 5, nil, 6, 6, 7, 7 }) + if jit then + eq({ 1, 2, 3, 4, 5, nil, nil, nil, 6, 6, 7, 7 }, r) + else + eq({ 1, 2, 3, 4, 5, nil, 6, 7 }, r) + end + + eq( + { { 1 }, { 2 }, { 3 } }, + vim.list.unique({ { 1 }, { 1 }, { 2 }, { 2 }, { 3 }, { 3 } }, function(x) + return x[1] + end) + ) + end) + it('vim.tbl_add_reverse_lookup', function() eq( true,