From 9c49c1047079427ff0a2356cb37302934845108e Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 20 Mar 2023 08:12:33 +0100 Subject: feat(vim.gsplit): gain features of vim.split Problem: - vim.split has more features than vim.gsplit. - Cannot inspect the "separator" segments of vim.split or vim.gsplit. Solution: - Move common implementation from vim.split into vim.gsplit. - TODO: deprecate vim.split in favor of vim.totable(vim.gsplit())? - Introduce `keepsep` parameter. Related: 84f66909e4008a57da947f1640bfc24da5e41a72 --- runtime/lua/vim/shared.lua | 122 ++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 45 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 1c8defc93a..95ddb3c70d 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -59,6 +59,13 @@ end)() --- Splits a string at each instance of a separator. --- +--- Example: +---
lua
+---   for s in vim.gsplit(':aa::b:', ':', {plain=true}) do
+---     print(s)
+---   end
+---   
+--- ---@see |vim.split()| ---@see |luaref-patterns| ---@see https://www.lua.org/pil/20.2.html @@ -66,17 +73,40 @@ end)() --- ---@param s string String to split ---@param sep string Separator or pattern ----@param plain (boolean|nil) If `true` use `sep` literally (passed to string.find) ----@return fun():string (function) Iterator over the split components -function vim.gsplit(s, sep, plain) - vim.validate({ s = { s, 's' }, sep = { sep, 's' }, plain = { plain, 'b', true } }) +---@param opts (table|nil) Keyword arguments |kwargs|: +--- - keepsep: (boolean) Include segments matching `sep` instead of discarding them. +--- - plain: (boolean) Use `sep` literally (as in string.find). +--- - trimempty: (boolean) Discard empty segments at start and end of the sequence. +---@return fun():string|nil (function) Iterator over the split components +function vim.gsplit(s, sep, opts) + local plain + local trimempty = false + local keepsep = false + if type(opts) == 'boolean' then + plain = opts -- For backwards compatibility. + else + vim.validate({ s = { s, 's' }, sep = { sep, 's' }, opts = { opts, 't', true } }) + opts = opts or {} + plain, trimempty, keepsep = opts.plain, opts.trimempty, opts.keepsep + assert(not trimempty or not keepsep, 'keepsep+trimempty not supported') + end local start = 1 local done = false + local sepseg = nil -- Last matched `sep` segment. + local sepesc = plain and vim.pesc(sep) or sep + + -- For `trimempty`: + local empty_start = true -- Only empty segments seen so far. + local empty_segs = 0 -- Empty segments found between non-empty segments. + local nonemptyseg = nil local function _pass(i, j, ...) if i then assert(j + 1 > start, 'Infinite loop detected') + if keepsep then + sepseg = s:match(sepesc, start) + end local seg = s:sub(start, i - 1) start = j + 1 return seg, ... @@ -87,16 +117,48 @@ function vim.gsplit(s, sep, plain) end return function() - if done or (s == '' and sep == '') then - return - end - if sep == '' then + if trimempty and empty_segs > 0 then + -- trimempty: Pop the collected empty segments. + empty_segs = empty_segs - 1 + return '' + elseif trimempty and nonemptyseg then + local seg = nonemptyseg + nonemptyseg = nil + return seg + elseif keepsep and sepseg then + local seg = sepseg + sepseg = nil + return seg + elseif done or (s == '' and sep == '') then + return nil + elseif sep == '' then if start == #s then done = true end return _pass(start + 1, start) end - return _pass(s:find(sep, start, plain)) + + local seg = _pass(s:find(sep, start, plain)) + + -- Trim empty segments from start/end. + if trimempty and seg == '' then + while not done and seg == '' do + empty_segs = empty_segs + 1 + seg = _pass(s:find(sep, start, plain)) + end + if done and seg == '' then + return nil + elseif empty_start then + empty_start = false + empty_segs = 0 + return seg + end + nonemptyseg = seg ~= '' and seg or nil + seg = '' + empty_segs = empty_segs - 1 + end + + return seg end end @@ -108,51 +170,21 @@ end --- split("axaby", "ab?") --> {'','x','y'} --- split("x*yz*o", "*", {plain=true}) --> {'x','yz','o'} --- split("|x|y|z|", "|", {trimempty=true}) --> {'x', 'y', 'z'} +--- split("|x|y|z|", "|", {keepsep=true}) --> {'|', 'x', '|', 'y', '|', 'z', '|'} --- --- ---@see |vim.gsplit()| --- ---@param s string String to split ---@param sep string Separator or pattern ----@param kwargs (table|nil) Keyword arguments: ---- - plain: (boolean) If `true` use `sep` literally (passed to string.find) ---- - trimempty: (boolean) If `true` remove empty items from the front ---- and back of the list +---@param opts (table|nil) Keyword arguments |kwargs| accepted by |vim.gsplit()| ---@return string[] List of split components -function vim.split(s, sep, kwargs) - local plain - local trimempty = false - if type(kwargs) == 'boolean' then - -- Support old signature for backward compatibility - plain = kwargs - else - vim.validate({ kwargs = { kwargs, 't', true } }) - kwargs = kwargs or {} - plain = kwargs.plain - trimempty = kwargs.trimempty - end - +function vim.split(s, sep, opts) + -- TODO(justinmk): deprecate vim.split in favor of vim.totable(vim.gsplit()) local t = {} - local skip = trimempty - for c in vim.gsplit(s, sep, plain) do - if c ~= '' then - skip = false - end - - if not skip then - table.insert(t, c) - end + for c in vim.gsplit(s, sep, opts) do + table.insert(t, c) end - - if trimempty then - for i = #t, 1, -1 do - if t[i] ~= '' then - break - end - table.remove(t, i) - end - end - return t end -- cgit From 8a70adbde03ee9931dc4e1b6f31bd8635eb3633b Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 20 Mar 2023 13:36:06 +0100 Subject: fix(vim.version): prerelease compare Problem: semver specifies that digit sequences in a prerelease string should be compared as numbers, not lexically: https://semver.org/#spec-item-11 > Precedence for two pre-release versions with the same major, minor, > and patch version MUST be determined by comparing each dot separated > identifier from left to right until a difference is found as follows: > 1. Identifiers consisting of only digits are compared numerically. > 2. Identifiers with letters or hyphens are compared lexically in ASCII sort order. > 3. Numeric identifiers always have lower precedence than non-numeric identifiers. > 4. A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. Solution: cmp_prerel() treats all digit sequences in a prerelease string as numbers. This doesn't _exactly_ match the spec, which specifies that only dot-delimited digit sequences should be treated as numbers... --- runtime/lua/vim/shared.lua | 17 +++++++++-------- runtime/lua/vim/version.lua | 46 +++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 20 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 95ddb3c70d..9eb49cdfac 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -66,14 +66,15 @@ end)() --- end --- --- ----@see |vim.split()| ----@see |luaref-patterns| ----@see https://www.lua.org/pil/20.2.html ----@see http://lua-users.org/wiki/StringLibraryTutorial ---- ----@param s string String to split ----@param sep string Separator or pattern ----@param opts (table|nil) Keyword arguments |kwargs|: +--- @see |string.gmatch()| +--- @see |vim.split()| +--- @see |luaref-patterns| +--- @see https://www.lua.org/pil/20.2.html +--- @see http://lua-users.org/wiki/StringLibraryTutorial +--- +--- @param s string String to split +--- @param sep string Separator or pattern +--- @param opts (table|nil) Keyword arguments |kwargs|: --- - keepsep: (boolean) Include segments matching `sep` instead of discarding them. --- - plain: (boolean) Use `sep` literally (as in string.find). --- - trimempty: (boolean) Discard empty segments at start and end of the sequence. diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index 8d8b0d6da7..43001c195c 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -65,6 +65,33 @@ local M = {} local Version = {} Version.__index = Version +--- Compares prerelease strings: per semver, number parts must be must be treated as numbers: +--- "pre1.10" is greater than "pre1.2". https://semver.org/#spec-item-11 +local function cmp_prerel(prerel1, prerel2) + if not prerel1 or not prerel2 then + return prerel1 and -1 or (prerel2 and 1 or 0) + end + -- TODO(justinmk): not fully spec-compliant; this treats non-dot-delimited digit sequences as + -- numbers. Maybe better: "(.-)(%.%d*)". + local iter1 = prerel1:gmatch('([^0-9]*)(%d*)') + local iter2 = prerel2:gmatch('([^0-9]*)(%d*)') + while true do + local word1, n1 = iter1() + local word2, n2 = iter2() + if word1 == nil and word2 == nil then -- Done iterating. + return 0 + end + word1, n1, word2, n2 = + word1 or '', n1 and tonumber(n1) or 0, word2 or '', n2 and tonumber(n2) or 0 + if word1 ~= word2 then + return word1 < word2 and -1 or 1 + end + if n1 ~= n2 then + return n1 < n2 and -1 or 1 + end + end +end + function Version:__index(key) return type(key) == 'number' and ({ self.major, self.minor, self.patch })[key] or Version[key] end @@ -88,7 +115,7 @@ function Version:__eq(other) return false end end - return self.prerelease == other.prerelease + return 0 == cmp_prerel(self.prerelease, other.prerelease) end function Version:__tostring() @@ -111,13 +138,7 @@ function Version:__lt(other) return true end end - if self.prerelease and not other.prerelease then - return true - end - if other.prerelease and not self.prerelease then - return false - end - return (self.prerelease or '') < (other.prerelease or '') + return -1 == cmp_prerel(self.prerelease, other.prerelease) end ---@param other Version @@ -127,7 +148,7 @@ end --- @private --- ---- Creates a new Version object. Not public currently. +--- Creates a new Version object, or returns `nil` if `version` is invalid. --- --- @param version string|number[]|Version --- @param strict? boolean Reject "1.0", "0-x", "3.2a" or other non-conforming version strings @@ -173,6 +194,7 @@ function M._version(version, strict) -- Adapted from https://github.com/folke/la build = build ~= '' and build or nil, }, Version) end + return nil -- Invalid version string. end ---TODO: generalize this, move to func.lua @@ -341,7 +363,7 @@ function M.cmp(v1, v2) return -1 end ----Returns `true` if the given versions are equal. +---Returns `true` if the given versions are equal. See |vim.version.cmp()| for usage. ---@param v1 Version|number[] ---@param v2 Version|number[] ---@return boolean @@ -349,7 +371,7 @@ function M.eq(v1, v2) return M.cmp(v1, v2) == 0 end ----Returns `true` if `v1 < v2`. +---Returns `true` if `v1 < v2`. See |vim.version.cmp()| for usage. ---@param v1 Version|number[] ---@param v2 Version|number[] ---@return boolean @@ -357,7 +379,7 @@ function M.lt(v1, v2) return M.cmp(v1, v2) == -1 end ----Returns `true` if `v1 > v2`. +---Returns `true` if `v1 > v2`. See |vim.version.cmp()| for usage. ---@param v1 Version|number[] ---@param v2 Version|number[] ---@return boolean -- cgit From e51139f5c1d70bef1424f29e63eb527514e42865 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 22 Mar 2023 15:14:51 +0100 Subject: refactor(vim.gsplit): remove "keepsep" string.gmatch() is superior, use that instead. --- runtime/lua/vim/shared.lua | 25 ++++++++++--------------- runtime/lua/vim/version.lua | 2 ++ 2 files changed, 12 insertions(+), 15 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9eb49cdfac..884929e33a 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -66,6 +66,14 @@ end)() --- end --- --- +--- If you want to also inspect the separator itself (instead of discarding it), use +--- |string.gmatch()|. Example: +---
lua
+---   for word, num in ('foo111bar222'):gmatch('([^0-9]*)(%d*)') do
+---     print(('word: %s num: %s'):format(word, num))
+---   end
+---   
+--- --- @see |string.gmatch()| --- @see |vim.split()| --- @see |luaref-patterns| @@ -75,27 +83,22 @@ end)() --- @param s string String to split --- @param sep string Separator or pattern --- @param opts (table|nil) Keyword arguments |kwargs|: ---- - keepsep: (boolean) Include segments matching `sep` instead of discarding them. --- - plain: (boolean) Use `sep` literally (as in string.find). --- - trimempty: (boolean) Discard empty segments at start and end of the sequence. ---@return fun():string|nil (function) Iterator over the split components function vim.gsplit(s, sep, opts) local plain local trimempty = false - local keepsep = false if type(opts) == 'boolean' then plain = opts -- For backwards compatibility. else vim.validate({ s = { s, 's' }, sep = { sep, 's' }, opts = { opts, 't', true } }) opts = opts or {} - plain, trimempty, keepsep = opts.plain, opts.trimempty, opts.keepsep - assert(not trimempty or not keepsep, 'keepsep+trimempty not supported') + plain, trimempty = opts.plain, opts.trimempty end local start = 1 local done = false - local sepseg = nil -- Last matched `sep` segment. - local sepesc = plain and vim.pesc(sep) or sep -- For `trimempty`: local empty_start = true -- Only empty segments seen so far. @@ -105,9 +108,6 @@ function vim.gsplit(s, sep, opts) local function _pass(i, j, ...) if i then assert(j + 1 > start, 'Infinite loop detected') - if keepsep then - sepseg = s:match(sepesc, start) - end local seg = s:sub(start, i - 1) start = j + 1 return seg, ... @@ -126,10 +126,6 @@ function vim.gsplit(s, sep, opts) local seg = nonemptyseg nonemptyseg = nil return seg - elseif keepsep and sepseg then - local seg = sepseg - sepseg = nil - return seg elseif done or (s == '' and sep == '') then return nil elseif sep == '' then @@ -171,17 +167,16 @@ end --- split("axaby", "ab?") --> {'','x','y'} --- split("x*yz*o", "*", {plain=true}) --> {'x','yz','o'} --- split("|x|y|z|", "|", {trimempty=true}) --> {'x', 'y', 'z'} ---- split("|x|y|z|", "|", {keepsep=true}) --> {'|', 'x', '|', 'y', '|', 'z', '|'} --- --- ---@see |vim.gsplit()| +---@see |string.gmatch()| --- ---@param s string String to split ---@param sep string Separator or pattern ---@param opts (table|nil) Keyword arguments |kwargs| accepted by |vim.gsplit()| ---@return string[] List of split components function vim.split(s, sep, opts) - -- TODO(justinmk): deprecate vim.split in favor of vim.totable(vim.gsplit()) local t = {} for c in vim.gsplit(s, sep, opts) do table.insert(t, c) diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index 43001c195c..3aacf3d4e0 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -65,6 +65,8 @@ local M = {} local Version = {} Version.__index = Version +--- @private +--- --- Compares prerelease strings: per semver, number parts must be must be treated as numbers: --- "pre1.10" is greater than "pre1.2". https://semver.org/#spec-item-11 local function cmp_prerel(prerel1, prerel2) -- cgit