commit cf9b36f3d97b6f9c66ffff008bc1b5a5dd14ca98
parent 7a051a4c389452b4955c22e3c29071433e01905a
Author: Lewis Russell <lewis6991@gmail.com>
Date: Fri, 25 Jul 2025 15:12:23 +0100
feat(lua): add vim.list.unique()
Problem:
No way to deduplicate values in a list in-place
Solution:
Add `vim.list.unique()`
Diffstat:
7 files changed, 114 insertions(+), 0 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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<any,boolean>
+
+ 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
@@ -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
@@ -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,