commit 0c46ea7d38dcdadaf217c1d8fd9178744f9bcf1f
parent 14c708634efec514463bb495d9648c78828ee198
Author: Olivia Kinnear <git@superatomic.dev>
Date: Tue, 10 Feb 2026 11:43:47 -0600
feat(lua): add `Iter:unique()` (#37592)
Diffstat:
6 files changed, 122 insertions(+), 7 deletions(-)
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt
@@ -1824,9 +1824,11 @@ vim.list.unique({t}, {key}) *vim.list.unique()*
Only the first occurrence of each value is kept. The operation is
performed in-place and the input table is modified.
- Accepts an optional `key` argument that if provided is called for each
+ Accepts an optional `key` argument, which 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.
+ useful for deduplicating table values or complex objects. If `key` returns
+ `nil` for a value, that value will be considered unique, even if multiple
+ values return `nil`.
Example: >lua
@@ -1847,6 +1849,9 @@ vim.list.unique({t}, {key}) *vim.list.unique()*
Return: ~
(`any[]`) The deduplicated list
+ See also: ~
+ • |Iter:unique()|
+
vim.list_contains({t}, {value}) *vim.list_contains()*
Checks if a list-like table (integer keys without gaps) contains `value`.
@@ -3366,6 +3371,43 @@ Iter:totable() *Iter:totable()*
Return: ~
(`table`)
+Iter:unique({key}) *Iter:unique()*
+ Removes duplicate values from an iterator pipeline.
+
+ Only the first occurrence of each value is kept.
+
+ Accepts an optional `key` argument, which if provided is called for each
+ value in the iterator to compute a hash key for uniqueness comparison.
+ This is useful for deduplicating table values or complex objects. If `key`
+ returns `nil` for a value, that value will be considered unique, even if
+ multiple values return `nil`.
+
+ If a function-based iterator returns multiple arguments, uniqueness is
+ checked based on the first return value. To change this behavior, specify
+ `key`.
+
+ Examples: >lua
+ vim.iter({ 1, 2, 2, 3, 2 }):unique():totable()
+ -- { 1, 2, 3 }
+
+ vim.iter({ {id=1}, {id=2}, {id=1} })
+ :unique(function(x)
+ return x.id
+ end)
+ :totable()
+ -- { {id=1}, {id=2} }
+<
+
+ Parameters: ~
+ • {key} (`fun(...):any?`) Optional hash function to determine
+ uniqueness of values.
+
+ Return: ~
+ (`Iter`)
+
+ See also: ~
+ • |vim.list.unique()|
+
==============================================================================
Lua module: vim.json *vim.json*
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -318,7 +318,8 @@ 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.
+• |vim.list.unique()| and |Iter:unique()| to deduplicate lists and iterators,
+ respectively.
• |vim.list.bisect()| for binary search.
• Experimental `vim.pos` and `vim.range` for Position/Range abstraction.
• |vim.json.encode()| has an `indent` option for pretty-formatting.
diff --git a/runtime/lua/vim/_core/ex_cmd.lua b/runtime/lua/vim/_core/ex_cmd.lua
@@ -10,13 +10,13 @@ end
--- @return string[]
local function get_client_names()
- local client_names = vim
+ return vim
.iter(lsp.get_clients())
:map(function(client)
return client.name
end)
+ :unique()
:totable()
- return vim.list.unique(client_names)
end
--- @return string[]
@@ -34,7 +34,8 @@ local function get_config_names()
vim.list_extend(config_names, vim.tbl_keys(lsp.config._configs))
return vim
- .iter(vim.list.unique(config_names))
+ .iter(config_names)
+ :unique()
--- @param name string
:filter(function(name)
return name ~= '*'
diff --git a/runtime/lua/vim/_core/shared.lua b/runtime/lua/vim/_core/shared.lua
@@ -365,9 +365,11 @@ end
--- Only the first occurrence of each value is kept.
--- The operation is performed in-place and the input table is modified.
---
---- Accepts an optional `key` argument that if provided is called for each
+--- Accepts an optional `key` argument, which 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.
+--- If `key` returns `nil` for a value, that value will be considered unique,
+--- even if multiple values return `nil`.
---
--- Example:
--- ```lua
@@ -385,6 +387,7 @@ end
--- @param t T[]
--- @param key? fun(x: T): any Optional hash function to determine uniqueness of values
--- @return T[] : The deduplicated list
+--- @see |Iter:unique()|
function vim.list.unique(t, key)
vim.validate('t', t, 'table')
local seen = {} --- @type table<any,boolean>
diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua
@@ -213,6 +213,57 @@ function ArrayIter:filter(f)
return self
end
+--- Removes duplicate values from an iterator pipeline.
+---
+--- Only the first occurrence of each value is kept.
+---
+--- Accepts an optional `key` argument, which if provided is called for each
+--- value in the iterator to compute a hash key for uniqueness comparison. This is
+--- useful for deduplicating table values or complex objects.
+--- If `key` returns `nil` for a value, that value will be considered unique,
+--- even if multiple values return `nil`.
+---
+--- If a function-based iterator returns multiple arguments, uniqueness is
+--- checked based on the first return value. To change this behavior, specify
+--- `key`.
+---
+--- Examples:
+---
+--- ```lua
+--- vim.iter({ 1, 2, 2, 3, 2 }):unique():totable()
+--- -- { 1, 2, 3 }
+---
+--- vim.iter({ {id=1}, {id=2}, {id=1} })
+--- :unique(function(x)
+--- return x.id
+--- end)
+--- :totable()
+--- -- { {id=1}, {id=2} }
+--- ```
+---
+---@param key? fun(...):any Optional hash function to determine uniqueness of values.
+---@return Iter
+---@see |vim.list.unique()|
+function Iter:unique(key)
+ local seen = {} --- @type table<any,boolean>
+
+ key = key or function(a)
+ return a
+ end
+
+ return self:filter(function(...)
+ local hash = key(...)
+ if hash == nil then
+ return true
+ elseif not seen[hash] then
+ seen[hash] = true
+ return true
+ else
+ return false
+ end
+ end)
+end
+
--- Flattens a |list-iterator|, un-nesting nested values up to the given {depth}.
--- Errors if it attempts to flatten a dict-like value.
---
diff --git a/test/functional/lua/iter_spec.lua b/test/functional/lua/iter_spec.lua
@@ -581,6 +581,23 @@ describe('vim.iter', function()
matches(flat_err, pcall_err(nested_non_lists.flatten, nested_non_lists, math.huge))
end)
+ it('unique()', function()
+ eq({ 1, 2, 3, 4, 5 }, vim.iter({ 1, 2, 2, 3, 4, 4, 5 }):unique():totable())
+ eq(
+ { 1, 2, 3, 4, 5 },
+ vim.iter({ 1, 2, 3, 4, 4, 5, 1, 2, 3, 2, 1, 2, 3, 4, 5 }):unique():totable()
+ )
+ eq(
+ { { 1 }, { 2 }, { 3 } },
+ vim
+ .iter({ { 1 }, { 1 }, { 2 }, { 2 }, { 3 }, { 3 } })
+ :unique(function(x)
+ return x[1]
+ end)
+ :totable()
+ )
+ end)
+
it('handles map-like tables', function()
local it = vim.iter({ a = 1, b = 2, c = 3 }):map(function(k, v)
if v % 2 ~= 0 then