diff options
-rw-r--r-- | runtime/doc/lua.txt | 48 | ||||
-rw-r--r-- | runtime/doc/news.txt | 2 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 132 | ||||
-rw-r--r-- | runtime/lua/vim/version.lua | 48 | ||||
-rw-r--r-- | test/functional/lua/version_spec.lua | 13 | ||||
-rw-r--r-- | test/functional/lua/vim_spec.lua | 54 |
6 files changed, 185 insertions, 112 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 4f56593491..2baae3a123 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1649,19 +1649,37 @@ endswith({s}, {suffix}) *vim.endswith()* Return: ~ (boolean) `true` if `suffix` is a suffix of `s` -gsplit({s}, {sep}, {plain}) *vim.gsplit()* +gsplit({s}, {sep}, {opts}) *vim.gsplit()* Splits a string at each instance of a separator. + Example: >lua + + for s in vim.gsplit(':aa::b:', ':', {plain=true}) do + print(s) + 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 +< + Parameters: ~ - • {s} (string) String to split - • {sep} (string) Separator or pattern - • {plain} (boolean|nil) If `true` use `sep` literally (passed to - string.find) + • {s} string String to split + • {sep} string Separator or pattern + • {opts} (table|nil) Keyword arguments |kwargs|: + • plain: (boolean) Use `sep` literally (as in string.find). + • trimempty: (boolean) Discard empty segments at start and end + of the sequence. Return: ~ (function) Iterator over the split components See also: ~ + • |string.gmatch()| • |vim.split()| • |luaref-patterns| • https://www.lua.org/pil/20.2.html @@ -1729,7 +1747,7 @@ spairs({t}) *vim.spairs()* See also: ~ • Based on https://github.com/premake/premake-core/blob/master/src/base/table.lua -split({s}, {sep}, {kwargs}) *vim.split()* +split({s}, {sep}, {opts}) *vim.split()* Splits a string at each instance of a separator. Examples: >lua @@ -1741,19 +1759,17 @@ split({s}, {sep}, {kwargs}) *vim.split()* < Parameters: ~ - • {s} (string) String to split - • {sep} (string) Separator or pattern - • {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 + • {s} (string) String to split + • {sep} (string) Separator or pattern + • {opts} (table|nil) Keyword arguments |kwargs| accepted by + |vim.gsplit()| Return: ~ string[] List of split components See also: ~ • |vim.gsplit()| + • |string.gmatch()| startswith({s}, {prefix}) *vim.startswith()* Tests if `s` starts with `prefix`. @@ -2616,7 +2632,7 @@ cmp({v1}, {v2}) *vim.version.cmp()* (integer) -1 if `v1 < v2`, 0 if `v1 == v2`, 1 if `v1 > v2`. eq({v1}, {v2}) *vim.version.eq()* - Returns `true` if the given versions are equal. + Returns `true` if the given versions are equal. See |vim.version.cmp()| for usage. Parameters: ~ • {v1} Version|number[] @@ -2626,7 +2642,7 @@ eq({v1}, {v2}) *vim.version.eq()* (boolean) gt({v1}, {v2}) *vim.version.gt()* - Returns `true` if `v1 > v2` . + Returns `true` if `v1 > v2` . See |vim.version.cmp()| for usage. Parameters: ~ • {v1} Version|number[] @@ -2645,7 +2661,7 @@ last({versions}) *vim.version.last()* Version ?|ni lt({v1}, {v2}) *vim.version.lt()* - Returns `true` if `v1 < v2` . + Returns `true` if `v1 < v2` . See |vim.version.cmp()| for usage. Parameters: ~ • {v1} Version|number[] diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 57b3e00709..1499b9d742 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -132,6 +132,8 @@ The following new APIs or features were added. • |vim.fs.dir()| now has a `opts` argument with a depth field to allow recursively searching a directory tree. +• |vim.gsplit()| supports all features of |vim.split()|. + • |vim.secure.read()| reads a file and prompts the user if it should be trusted and, if so, returns the file's contents. diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 1c8defc93a..884929e33a 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -59,21 +59,52 @@ end)() --- Splits a string at each instance of a separator. --- ----@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 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 } }) +--- Example: +--- <pre>lua +--- for s in vim.gsplit(':aa::b:', ':', {plain=true}) do +--- print(s) +--- end +--- </pre> +--- +--- If you want to also inspect the separator itself (instead of discarding it), use +--- |string.gmatch()|. Example: +--- <pre>lua +--- for word, num in ('foo111bar222'):gmatch('([^0-9]*)(%d*)') do +--- print(('word: %s num: %s'):format(word, num)) +--- end +--- </pre> +--- +--- @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|: +--- - 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 + 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 = opts.plain, opts.trimempty + end local start = 1 local done = false + -- 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') @@ -87,16 +118,44 @@ 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 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 @@ -111,48 +170,17 @@ end --- </pre> --- ---@see |vim.gsplit()| +---@see |string.gmatch()| --- ---@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) 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 diff --git a/runtime/lua/vim/version.lua b/runtime/lua/vim/version.lua index 8d8b0d6da7..3aacf3d4e0 100644 --- a/runtime/lua/vim/version.lua +++ b/runtime/lua/vim/version.lua @@ -65,6 +65,35 @@ 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) + 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 +117,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 +140,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 +150,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 +196,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 +365,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 +373,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 +381,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 diff --git a/test/functional/lua/version_spec.lua b/test/functional/lua/version_spec.lua index 014fea5272..75b62e8318 100644 --- a/test/functional/lua/version_spec.lua +++ b/test/functional/lua/version_spec.lua @@ -101,6 +101,9 @@ describe('version', function() { v1 = 'v9.0.0', v2 = 'v0.9.0', want = 1, }, { v1 = 'v0.9.0', v2 = 'v0.0.0', want = 1, }, { v1 = 'v0.0.9', v2 = 'v0.0.0', want = 1, }, + { v1 = 'v0.0.9+aaa', v2 = 'v0.0.9+bbb', want = 0, }, + + -- prerelease 💩 https://semver.org/#spec-item-11 { v1 = 'v1.0.0-alpha', v2 = 'v1.0.0', want = -1, }, { v1 = '1.0.0', v2 = '1.0.0-alpha', want = 1, }, { v1 = '1.0.0-2', v2 = '1.0.0-1', want = 1, }, @@ -116,11 +119,11 @@ describe('version', function() { v1 = '1.0.0-alpha.beta', v2 = '1.0.0-alpha', want = 1, }, { v1 = '1.0.0-alpha', v2 = '1.0.0-alpha.beta', want = -1, }, { v1 = '1.0.0-alpha.beta', v2 = '1.0.0-beta', want = -1, }, - { v1 = '1.0.0-beta', v2 = '1.0.0-alpha.beta', want = 1, }, - { v1 = '1.0.0-beta', v2 = '1.0.0-beta.2', want = -1, }, - -- TODO - -- { v1 = '1.0.0-beta.2', v2 = '1.0.0-beta.11', want = -1, }, - { v1 = '1.0.0-beta.11', v2 = '1.0.0-rc.1', want = -1, }, + { v1 = '1.0.0-beta.2', v2 = '1.0.0-beta.11', want = -1, }, + { v1 = '1.0.0-beta.20', v2 = '1.0.0-beta.11', want = 1, }, + { v1 = '1.0.0-alpha.20', v2 = '1.0.0-beta.11', want = -1, }, + { v1 = '1.0.0-a.01.x.3', v2 = '1.0.0-a.1.x.003', want = 0, }, + { v1 = 'v0.9.0-dev-92+9', v2 = 'v0.9.0-dev-120+3', want = -1, }, } for _, tc in ipairs(testcases) do local msg = function(s) return ('v1 %s v2'):format(s == 0 and '==' or (s == 1 and '>' or '<')) end diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 0483ec46f0..a0428ed933 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -292,51 +292,51 @@ describe('lua stdlib', function() ]]} end) - it("vim.split", function() - local split = function(str, sep, kwargs) - return exec_lua('return vim.split(...)', str, sep, kwargs) - end - + it('vim.gsplit, vim.split', function() local tests = { - { "a,b", ",", false, false, { 'a', 'b' } }, - { ":aa::bb:", ":", false, false, { '', 'aa', '', 'bb', '' } }, - { ":aa::bb:", ":", false, true, { 'aa', '', 'bb' } }, - { "::ee::ff:", ":", false, false, { '', '', 'ee', '', 'ff', '' } }, - { "::ee::ff:", ":", false, true, { 'ee', '', 'ff' } }, - { "ab", ".", false, false, { '', '', '' } }, - { "a1b2c", "[0-9]", false, false, { 'a', 'b', 'c' } }, - { "xy", "", false, false, { 'x', 'y' } }, - { "here be dragons", " ", false, false, { "here", "be", "dragons"} }, - { "axaby", "ab?", false, false, { '', 'x', 'y' } }, - { "f v2v v3v w2w ", "([vw])2%1", false, false, { 'f ', ' v3v ', ' ' } }, - { "", "", false, false, {} }, - { "", "a", false, false, { '' } }, - { "x*yz*oo*l", "*", true, false, { 'x', 'yz', 'oo', 'l' } }, + { 'a,b', ',', false, false, { 'a', 'b' } }, + { ':aa::::bb:', ':', false, false, { '', 'aa', '', '', '', 'bb', '' } }, + { ':aa::::bb:', ':', false, true, { 'aa', '', '', '', 'bb' } }, + { ':aa::bb:', ':', false, true, { 'aa', '', 'bb' } }, + { '/a/b:/b/\n', '[:\n]', false, true, { '/a/b', '/b/' } }, + { '::ee::ff:', ':', false, false, { '', '', 'ee', '', 'ff', '' } }, + { '::ee::ff::', ':', false, true, { 'ee', '', 'ff' } }, + { 'ab', '.', false, false, { '', '', '' } }, + { 'a1b2c', '[0-9]', false, false, { 'a', 'b', 'c' } }, + { 'xy', '', false, false, { 'x', 'y' } }, + { 'here be dragons', ' ', false, false, { 'here', 'be', 'dragons'} }, + { 'axaby', 'ab?', false, false, { '', 'x', 'y' } }, + { 'f v2v v3v w2w ', '([vw])2%1', false, false, { 'f ', ' v3v ', ' ' } }, + { '', '', false, false, {} }, + { '', '', false, true, {} }, + { '\n', '[:\n]', false, true, {} }, + { '', 'a', false, false, { '' } }, + { 'x*yz*oo*l', '*', true, false, { 'x', 'yz', 'oo', 'l' } }, } for _, t in ipairs(tests) do - eq(t[5], split(t[1], t[2], {plain=t[3], trimempty=t[4]})) + eq(t[5], vim.split(t[1], t[2], {plain=t[3], trimempty=t[4]})) end -- Test old signature - eq({'x', 'yz', 'oo', 'l'}, split("x*yz*oo*l", "*", true)) + eq({'x', 'yz', 'oo', 'l'}, vim.split("x*yz*oo*l", "*", true)) local loops = { { "abc", ".-" }, } for _, t in ipairs(loops) do - matches("Infinite loop detected", pcall_err(split, t[1], t[2])) + matches("Infinite loop detected", pcall_err(vim.split, t[1], t[2])) end -- Validates args. - eq(true, pcall(split, 'string', 'string')) + eq(true, pcall(vim.split, 'string', 'string')) matches('s: expected string, got number', - pcall_err(split, 1, 'string')) + pcall_err(vim.split, 1, 'string')) matches('sep: expected string, got number', - pcall_err(split, 'string', 1)) - matches('kwargs: expected table, got number', - pcall_err(split, 'string', 'string', 1)) + pcall_err(vim.split, 'string', 1)) + matches('opts: expected table, got number', + pcall_err(vim.split, 'string', 'string', 1)) end) it('vim.trim', function() |