neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit f731766474901e5e345e0ca630315ef69122e556
parent 99873296beb4a868d7a2ac63d80bcaa206ff1d36
Author: Justin M. Keyes <justinkz@gmail.com>
Date:   Tue,  1 Jul 2025 04:19:42 -0700

Merge #34715 vim.version improvements


Diffstat:
Mruntime/doc/lua.txt | 88+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mruntime/doc/news.txt | 2++
Mruntime/lua/vim/version.lua | 108++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mtest/functional/lua/version_spec.lua | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 276 insertions(+), 70 deletions(-)

diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt @@ -3902,6 +3902,7 @@ versions (1.2.3-rc1) are not matched. > >1.2.3 greater than 1.2.3 <1.2.3 before 1.2.3 >=1.2.3 at least 1.2.3 + <=1.2.3 at most 1.2.3 ~1.2.3 is >=1.2.3 <1.3.0 "reasonably close to 1.2.3" ^1.2.3 is >=1.2.3 <2.0.0 "compatible with 1.2.3" ^0.2.3 is >=0.2.3 <0.3.0 (0.x.x is special) @@ -3916,7 +3917,7 @@ versions (1.2.3-rc1) are not matched. > * any version x same - 1.2.3 - 2.3.4 is >=1.2.3 <=2.3.4 + 1.2.3 - 2.3.4 is >=1.2.3 <2.3.4 Partial right: missing pieces treated as x (2.3 => 2.3.x). 1.2.3 - 2.3 is >=1.2.3 <2.4.0 @@ -3927,6 +3928,43 @@ versions (1.2.3-rc1) are not matched. > < +*vim.VersionRange* + + Fields: ~ + • {from} (`vim.Version`) + • {to}? (`vim.Version`) + • {has} (`fun(self: vim.VersionRange, version: string|vim.Version): boolean`) + See |VersionRange:has()|. + + +VersionRange:has({version}) *VersionRange:has()* + Check if a version is in the range (inclusive `from`, exclusive `to`). + + Example: >lua + local r = vim.version.range('1.0.0 - 2.0.0') + print(r:has('1.9.9')) -- true + print(r:has('2.0.0')) -- false + print(r:has(vim.version())) -- check against current Nvim version +< + + Or use cmp(), le(), lt(), ge(), gt(), and/or eq() to compare a version + against `.to` and `.from` directly: >lua + local r = vim.version.range('1.0.0 - 2.0.0') -- >=1.0, <2.0 + print(vim.version.ge({1,0,3}, r.from) and vim.version.lt({1,0,3}, r.to)) +< + + Attributes: ~ + Since: 0.9.0 + + Parameters: ~ + • {version} (`string|vim.Version`) + + Return: ~ + (`boolean`) + + See also: ~ + • https://github.com/npm/node-semver#ranges + vim.version.cmp({v1}, {v2}) *vim.version.cmp()* Parses and compares two version objects (the result of |vim.version.parse()|, or specified literally as a `{major, minor, patch}` @@ -3997,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 @@ -4057,29 +4110,8 @@ vim.version.parse({version}, {opts}) *vim.version.parse()* • https://semver.org/spec/v2.0.0.html vim.version.range({spec}) *vim.version.range()* - Parses a semver |version-range| "spec" and returns a range object: > - { - from: Version - to: Version - has(v: string|Version) - } -< - - `:has()` checks if a version is in the range (inclusive `from`, exclusive - `to`). - - Example: >lua - local r = vim.version.range('1.0.0 - 2.0.0') - print(r:has('1.9.9')) -- true - print(r:has('2.0.0')) -- false - print(r:has(vim.version())) -- check against current Nvim version -< - - Or use cmp(), le(), lt(), ge(), gt(), and/or eq() to compare a version - against `.to` and `.from` directly: >lua - local r = vim.version.range('1.0.0 - 2.0.0') -- >=1.0, <2.0 - print(vim.version.ge({1,0,3}, r.from) and vim.version.lt({1,0,3}, r.to)) -< + Parses a semver |version-range| "spec" and returns |vim.VersionRange| + object: Attributes: ~ Since: 0.9.0 @@ -4088,13 +4120,7 @@ vim.version.range({spec}) *vim.version.range()* • {spec} (`string`) Version range "spec" Return: ~ - (`table?`) A table with the following fields: - • {from} (`vim.Version`) - • {to}? (`vim.Version`) - • {has} (`fun(self: vim.VersionRange, version: string|vim.Version)`) - - See also: ~ - • https://github.com/npm/node-semver#ranges + (`vim.VersionRange?`) See |vim.VersionRange|. ============================================================================== diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt @@ -199,6 +199,8 @@ LUA • |vim.hl.range()| now allows multiple timed highlights. • |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 @@ -28,6 +28,7 @@ --- >1.2.3 greater than 1.2.3 --- <1.2.3 before 1.2.3 --- >=1.2.3 at least 1.2.3 +--- <=1.2.3 at most 1.2.3 --- ~1.2.3 is >=1.2.3 <1.3.0 "reasonably close to 1.2.3" --- ^1.2.3 is >=1.2.3 <2.0.0 "compatible with 1.2.3" --- ^0.2.3 is >=0.2.3 <0.3.0 (0.x.x is special) @@ -42,7 +43,7 @@ --- * any version --- x same --- ---- 1.2.3 - 2.3.4 is >=1.2.3 <=2.3.4 +--- 1.2.3 - 2.3.4 is >=1.2.3 <2.3.4 --- --- Partial right: missing pieces treated as x (2.3 => 2.3.x). --- 1.2.3 - 2.3 is >=1.2.3 <2.4.0 @@ -222,40 +223,11 @@ function M.last(versions) end ---@class vim.VersionRange ----@inlinedoc ---@field from vim.Version ---@field to? vim.Version local VersionRange = {} ----@nodoc ----@param version string|vim.Version -function VersionRange:has(version) - if type(version) == 'string' then - ---@diagnostic disable-next-line: cast-local-type - version = M.parse(version) - elseif getmetatable(version) ~= Version then - -- Need metatable to compare versions. - version = setmetatable(vim.deepcopy(version, true), Version) - end - if version then - if self.from == self.to then - return version == self.from - end - return version >= self.from and (self.to == nil or version < self.to) - end -end - ---- Parses a semver |version-range| "spec" and returns a range object: ---- ---- ``` ---- { ---- from: Version ---- to: Version ---- has(v: string|Version) ---- } ---- ``` ---- ---- `:has()` checks if a version is in the range (inclusive `from`, exclusive `to`). +--- Check if a version is in the range (inclusive `from`, exclusive `to`). --- --- Example: --- @@ -276,12 +248,42 @@ end --- --- @see # https://github.com/npm/node-semver#ranges --- @since 11 ---- +--- @param version string|vim.Version +--- @return boolean +function VersionRange:has(version) + if type(version) == 'string' then + ---@diagnostic disable-next-line: cast-local-type + version = M.parse(version) + elseif getmetatable(version) ~= Version then + -- Need metatable to compare versions. + version = setmetatable(vim.deepcopy(version, true), Version) + end + if not version then + return false + end + if self.from == self.to then + return version == self.from + end + return version >= self.from and (self.to == nil or version < self.to) +end + +local range_mt = { + __index = VersionRange, + __tostring = function(self) + if not self.to then + return '>=' .. tostring(self.from) + end + return ('%s - %s'):format(tostring(self.from), tostring(self.to)) + end, +} + +--- Parses a semver |version-range| "spec" and returns |vim.VersionRange| object: +--- @since 11 --- @param spec string Version range "spec" --- @return vim.VersionRange? function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim if spec == '*' or spec == '' then - return setmetatable({ from = M.parse('0.0.0') }, { __index = VersionRange }) + return setmetatable({ from = M.parse('0.0.0') }, range_mt) end ---@type number? @@ -295,7 +297,7 @@ function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim return setmetatable({ from = ra and ra.from, to = rb and (#parts == 3 and rb.from or rb.to), - }, { __index = VersionRange }) + }, range_mt) end ---@type string, string local mods, version = spec:lower():match('^([%^=<>~]*)(.*)$') @@ -314,9 +316,23 @@ function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim from = M._version({}) elseif mods == '<=' then from = M._version({}) - to.patch = to.patch + 1 + -- HACK: construct the smallest reasonable version bigger than `to` + -- to simulate `<=` while using exclusive right hand side + if to.prerelease then + to.prerelease = to.prerelease .. '.0' + else + to.patch = to.patch + 1 + to.prerelease = '0' + end elseif mods == '>' then - from.patch = from.patch + 1 + -- HACK: construct the smallest reasonable version bigger than `from` + -- to simulate `>` while using inclusive left hand side + if from.prerelease then + from.prerelease = from.prerelease .. '.0' + else + from.patch = from.patch + 1 + from.prerelease = '0' + end to = nil elseif mods == '>=' then to = nil @@ -341,7 +357,25 @@ function M.range(spec) -- Adapted from https://github.com/folke/lazy.nvim end end ---@diagnostic enable: need-check-nil - return setmetatable({ from = from, to = to }, { __index = VersionRange }) + return setmetatable({ from = from, to = to }, range_mt) + 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 diff --git a/test/functional/lua/version_spec.lua b/test/functional/lua/version_spec.lua @@ -58,10 +58,10 @@ describe('version', function() ['1.2.3'] = { from = { 1, 2, 3 }, to = { 1, 2, 3 } }, ['1.2'] = { from = { 1, 2, 0 }, to = { 1, 3, 0 } }, ['=1.2.3'] = { from = { 1, 2, 3 }, to = { 1, 2, 3 } }, - ['>1.2.3'] = { from = { 1, 2, 4 } }, + ['>1.2.3'] = { from = '1.2.4-0' }, ['>=1.2.3'] = { from = { 1, 2, 3 } }, ['<1.2.3'] = { from = { 0, 0, 0 }, to = { 1, 2, 3 } }, - ['<=1.2.3'] = { from = { 0, 0, 0 }, to = { 1, 2, 4 } }, + ['<=1.2.3'] = { from = { 0, 0, 0 }, to = '1.2.4-0' }, ['~1.2.3'] = { from = { 1, 2, 3 }, to = { 1, 3, 0 } }, ['^1.2.3'] = { from = { 1, 2, 3 }, to = { 2, 0, 0 } }, ['^0.2.3'] = { from = { 0, 2, 3 }, to = { 0, 3, 0 } }, @@ -89,6 +89,11 @@ describe('version', function() eq(output, range) end) + it('tostring() ' .. input, function() + eq(type(tostring(range)), 'string') + eq(vim.version.range(tostring(range)), range) + end) + it('[from] in range ' .. input, function() assert(range:has(output.from)) end) @@ -119,6 +124,20 @@ describe('version', function() assert(not vim.version.range('1.2.3-alpha'):has('1.2.3-beta')) assert(vim.version.range('>0.10'):has('0.12.0-dev')) assert(not vim.version.range('>=0.12'):has('0.12.0-dev')) + + assert(not vim.version.range('<=1.2.3'):has('1.2.4-alpha')) + assert(not vim.version.range('<=1.2.3-0'):has('1.2.3')) + assert(not vim.version.range('<=1.2.3-alpha'):has('1.2.3')) + assert(not vim.version.range('<=1.2.3-1'):has('1.2.4-0')) + assert(vim.version.range('<=1.2.3-0'):has('1.2.3-0')) + assert(vim.version.range('<=1.2.3-alpha'):has('1.2.3-alpha')) + + assert(vim.version.range('>1.2.3'):has('1.2.4-0')) + assert(vim.version.range('>1.2.3'):has('1.2.4-alpha')) + assert(vim.version.range('>1.2.3-0'):has('1.2.3-1')) + + local range_alpha = vim.version.range('1.2.3-alpha') + eq(vim.version.range(tostring(range_alpha)), range_alpha) end) it('returns nil with empty version', function() @@ -126,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 },