From 0c46ea7d38dcdadaf217c1d8fd9178744f9bcf1f Mon Sep 17 00:00:00 2001 From: Olivia Kinnear Date: Tue, 10 Feb 2026 11:43:47 -0600 Subject: [PATCH] feat(lua): add `Iter:unique()` (#37592) --- runtime/doc/lua.txt | 46 ++++++++++++++++++++++++++-- runtime/doc/news.txt | 3 +- runtime/lua/vim/_core/ex_cmd.lua | 7 +++-- runtime/lua/vim/_core/shared.lua | 5 ++- runtime/lua/vim/iter.lua | 51 +++++++++++++++++++++++++++++++ test/functional/lua/iter_spec.lua | 17 +++++++++++ 6 files changed, 122 insertions(+), 7 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index a71334eab7..18fca68a10 100644 --- 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 index 3b334963a8..083dcec7d5 100644 --- 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 index 6d08a48cba..a0bd3463ab 100644 --- 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 index 1d1b64cb99..1f01b70f2a 100644 --- 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 diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua index f3062564ce..758ef9b5dc 100644 --- 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 + + 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 index 1e4012521e..76495c4d5f 100644 --- 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