From 773075b2bce8f2193343c0efa73f02b1f370dc67 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Mon, 30 Jun 2025 16:08:39 +0300 Subject: [PATCH] feat(vim.version): add `vim.version.intersect()` Problem: No way to compute intersection of two version ranges, which is useful when computing version range that fits inside several reference ranges. Solution: Add `vim.version.intersect()`. --- runtime/doc/lua.txt | 15 ++++ runtime/doc/news.txt | 1 + runtime/lua/vim/version.lua | 18 ++++ test/functional/lua/version_spec.lua | 125 +++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index f9767cbd68..99ead02c6e 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -4035,6 +4035,21 @@ vim.version.gt({v1}, {v2}) *vim.version.gt()* Return: ~ (`boolean`) +vim.version.intersect({r1}, {r2}) *vim.version.intersect()* + WARNING: This feature is experimental/unstable. + + Computes the common range shared by the given ranges. + + Parameters: ~ + • {r1} (`vim.VersionRange`) First range to intersect. See + |vim.VersionRange|. + • {r2} (`vim.VersionRange`) Second range to intersect. See + |vim.VersionRange|. + + Return: ~ + (`vim.VersionRange?`) Maximal range that is present inside both `r1` + and `r2`. `nil` if such range does not exist. See |vim.VersionRange|. + vim.version.last({versions}) *vim.version.last()* TODO: generalize this, move to func.lua diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 80e6af2db3..79ef80187c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -200,6 +200,7 @@ LUA • |vim.tbl_extend()| and |vim.tbl_deep_extend()| now accept a function behavior argument. • |vim.fs.root()| can define "equal priority" via nested lists. • |vim.version.range()| output can be converted to human-readable string with |tostring()|. +• |vim.version.intersect()| computes intersection of two version ranges. OPTIONS diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index dee99a22cc..7c49e5d38f 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -361,6 +361,24 @@ function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim end end +--- Computes the common range shared by the given ranges. +--- +--- @since 14 +--- @param r1 vim.VersionRange First range to intersect. +--- @param r2 vim.VersionRange Second range to intersect. +--- @return vim.VersionRange? Maximal range that is present inside both `r1` and `r2`. +--- `nil` if such range does not exist. +function M.intersect(r1, r2) + assert(getmetatable(r1) == range_mt) + assert(getmetatable(r2) == range_mt) + + local from = r1.from <= r2.from and r2.from or r1.from + local to = (r1.to == nil or (r2.to ~= nil and r2.to <= r1.to)) and r2.to or r1.to + if to == nil or from < to or (from == to and r1:has(from) and r2:has(from)) then + return setmetatable({ from = from, to = to }, VersionRange) + end +end + ---@param v string|vim.Version ---@return string local function create_err_msg(v) diff --git a/test/functional/lua/version_spec.lua b/test/functional/lua/version_spec.lua index 31bdbcc914..6b119bb013 100644 --- a/test/functional/lua/version_spec.lua +++ b/test/functional/lua/version_spec.lua @@ -145,6 +145,131 @@ describe('version', function() end) end) + describe('intersect', function() + local check = function(input, output) + local r1 = vim.version.range(input[1]) + local r2 = vim.version.range(input[2]) + if output == nil then + eq(vim.version.intersect(r1, r2), nil) + eq(vim.version.intersect(r2, r1), nil) + else + local ref = vim.version.range(output) + eq(vim.version.intersect(r1, r2), ref) + eq(vim.version.intersect(r2, r1), ref) + end + end + + it('returns biggest common range', function() + check({ '>=1.2.3', '>=2.0.0' }, '>=2.0.0') + check({ '>=1.2.3', '>=1.3.0' }, '>=1.3.0') + check({ '>=1.2.3', '>=1.2.4' }, '>=1.2.4') + check({ '>=1.2.3', '>=1.2.3' }, '>=1.2.3') + check({ '>=1.2.3', '>1.2.4' }, '>1.2.4') + check({ '>=1.2.3', '>1.2.3' }, '>1.2.3') + check({ '>=1.2.3', '>1.2.2' }, '>=1.2.3') + check({ '>1.2.3', '>1.2.4' }, '>1.2.4') + check({ '>1.2.3', '>1.2.3' }, '>1.2.3') + + check({ '>=1.2.3', '1.2.0 - 1.2.2' }, nil) + check({ '>=1.2.3', '1.2.0 - 1.2.2' }, nil) + check({ '>=1.2.3', '1.2.0 - 1.2.3' }, nil) + check({ '>=1.2.3', '1.2.0 - 1.2.4' }, '1.2.3 - 1.2.4') + check({ '>=1.2.3', '1.2.3 - 1.2.4' }, '1.2.3 - 1.2.4') + check({ '>=1.2.3', '1.2.4 - 1.3.0' }, '1.2.4 - 1.3.0') + check({ '>1.2.3', '1.2.0 - 1.2.2' }, nil) + check({ '>1.2.3', '1.2.0 - 1.2.2' }, nil) + check({ '>1.2.3', '1.2.0 - 1.2.3' }, nil) + check({ '>1.2.3', '1.2.0 - 1.2.4' }, '1.2.4-0 - 1.2.4') + check({ '>1.2.3', '1.2.3 - 1.2.4' }, '1.2.4-0 - 1.2.4') + check({ '>1.2.3', '1.2.4 - 1.3.0' }, '1.2.4 - 1.3.0') + + check({ '>=1.2.3', '=1.2.4' }, '=1.2.4') + check({ '>=1.2.3', '=1.2.3' }, '=1.2.3') + check({ '>=1.2.3', '=1.2.2' }, nil) + check({ '>1.2.3', '=1.2.4' }, '=1.2.4') + check({ '>1.2.3', '=1.2.3' }, nil) + check({ '>1.2.3', '=1.2.2' }, nil) + + check({ '>=1.2.3', '<=1.3.0' }, '1.2.3 - 1.3.1-0') + check({ '>=1.2.3', '<1.3.0' }, '1.2.3 - 1.3.0') + check({ '>=1.2.3', '<=1.2.3' }, '1.2.3 - 1.2.4-0') -- A better result would be '=1.2.3' + check({ '>=1.2.3', '<1.2.3' }, nil) + check({ '>=1.2.3', '<=1.2.2' }, nil) + check({ '>=1.2.3', '<1.2.2' }, nil) + check({ '>1.2.3', '<=1.3.0' }, '1.2.4-0 - 1.3.1-0') + check({ '>1.2.3', '<1.3.0' }, '1.2.4-0 - 1.3.0') + check({ '>1.2.3', '<=1.2.3' }, nil) + check({ '>1.2.3', '<1.2.3' }, nil) + check({ '>1.2.3', '<=1.2.2' }, nil) + check({ '>1.2.3', '<1.2.2' }, nil) + + check({ '1.2.3 - 1.3.0', '1.3.1 - 1.4.0' }, nil) + check({ '1.2.3 - 1.3.0', '1.3.0 - 1.4.0' }, nil) + check({ '1.2.3 - 1.3.0', '1.2.4 - 1.4.0' }, '1.2.4 - 1.3.0') + check({ '1.2.3 - 1.3.0', '1.2.3 - 1.4.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '1.2.2 - 1.4.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '1.2.4 - 1.3.0' }, '1.2.4 - 1.3.0') + check({ '1.2.3 - 1.3.0', '1.2.3 - 1.3.0' }, '1.2.3 - 1.3.0') + + check({ '1.2.3 - 1.3.0', '=1.4.0' }, nil) + check({ '1.2.3 - 1.3.0', '=1.3.0' }, nil) + check({ '1.2.3 - 1.3.0', '=1.2.4' }, '=1.2.4') + check({ '1.2.3 - 1.3.0', '=1.2.3' }, '=1.2.3') + check({ '1.2.3 - 1.3.0', '=1.2.2' }, nil) + + check({ '1.2.3 - 1.3.0', '<=1.4.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '<1.4.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '<=1.3.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '<1.3.0' }, '1.2.3 - 1.3.0') + check({ '1.2.3 - 1.3.0', '<=1.2.4' }, '1.2.3 - 1.2.5-0') + check({ '1.2.3 - 1.3.0', '<1.2.5' }, '1.2.3 - 1.2.5') + check({ '1.2.3 - 1.3.0', '<=1.2.3' }, '1.2.3 - 1.2.4-0') -- A better result would be '=1.2.3' + check({ '1.2.3 - 1.3.0', '<1.2.3' }, nil) + check({ '1.2.3 - 1.3.0', '<=1.2.2' }, nil) + check({ '1.2.3 - 1.3.0', '<1.2.2' }, nil) + + check({ '=1.2.3', '=1.2.4' }, nil) + check({ '=1.2.3', '=1.2.3' }, '=1.2.3') + + check({ '=1.2.3', '<1.2.3' }, nil) + check({ '<=1.2.2', '=1.2.3' }, nil) + + check({ '=1.2.3', '<=1.3.0' }, '=1.2.3') + check({ '=1.2.3', '<1.3.0' }, '=1.2.3') + check({ '=1.2.3', '<=1.2.3' }, '=1.2.3') + check({ '=1.2.3', '<1.2.3' }, nil) + check({ '=1.2.3', '<=1.2.2' }, nil) + check({ '=1.2.3', '<1.2.2' }, nil) + + check({ '<=1.2.3', '<=1.3.0' }, '<=1.2.3') + check({ '<=1.2.3', '<1.3.0' }, '<=1.2.3') + check({ '<=1.2.3', '<=1.2.3' }, '<=1.2.3') + check({ '<=1.2.3', '<1.2.3' }, '<1.2.3') + check({ '<=1.2.3', '<=1.2.2' }, '<=1.2.2') + check({ '<=1.2.3', '<1.2.2' }, '<1.2.2') + check({ '<1.2.3', '<=1.3.0' }, '<1.2.3') + check({ '<1.2.3', '<1.3.0' }, '<1.2.3') + check({ '<1.2.3', '<=1.2.3' }, '<1.2.3') + check({ '<1.2.3', '<1.2.3' }, '<1.2.3') + check({ '<1.2.3', '<=1.2.2' }, '<=1.2.2') + check({ '<1.2.3', '<1.2.2' }, '<1.2.2') + + -- Selective coverage of ranges with pre-releases + check({ '>=1.2.3-0', '>=1.2.3-1' }, '>=1.2.3-1') + check({ '>=1.2.3-alpha', '>=1.2.3-beta' }, '>=1.2.3-beta') + check({ '>=1.2.3-0', '>=1.2.3-alpha' }, '>=1.2.3-alpha') + check({ '>=1.2.3-0', '<1.2.3' }, '1.2.3-0 - 1.2.3') + check({ '>=1.2.3-0', '<1.2.3-1' }, '1.2.3-0 - 1.2.3-1') + check({ '>=1.2.3-alpha', '<1.2.3-beta' }, '1.2.3-alpha - 1.2.3-beta') + check({ '>=1.2.3-0', '1.2.2 - 1.2.3' }, '1.2.3-0 - 1.2.3') + check({ '>=1.2.3-0', '<=1.2.2' }, nil) + + check({ '<=1.2.3-0', '>=1.2.3' }, nil) + check({ '<=1.2.3-0', '=1.2.3' }, nil) + check({ '>=1.2.3-0', '<1.2.3-2' }, '1.2.3-0 - 1.2.3-2') + end) + end) + describe('cmp()', function() local testcases = { { v1 = 'v0.0.99', v2 = 'v9.0.0', want = -1 },