diff options
-rw-r--r-- | runtime/doc/lua.txt | 511 | ||||
-rw-r--r-- | runtime/doc/news.txt | 3 | ||||
-rw-r--r-- | runtime/lua/vim/_watch.lua | 10 | ||||
-rw-r--r-- | runtime/lua/vim/iter.lua | 836 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 105 | ||||
-rwxr-xr-x | scripts/gen_vimdoc.py | 4 | ||||
-rw-r--r-- | scripts/genvimvim.lua | 17 | ||||
-rw-r--r-- | src/nvim/eval/userfunc.c | 2 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 33 | ||||
-rw-r--r-- | src/nvim/mouse.c | 7 | ||||
-rw-r--r-- | test/functional/lua/vim_spec.lua | 354 | ||||
-rw-r--r-- | test/functional/lua/watch_spec.lua | 24 | ||||
-rw-r--r-- | test/functional/ui/statuscolumn_spec.lua | 3 | ||||
-rw-r--r-- | test/old/testdir/test_shell.vim | 44 |
14 files changed, 1927 insertions, 26 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 6fdf3775f6..60067f1bc3 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1653,6 +1653,26 @@ endswith({s}, {suffix}) *vim.endswith()* Return: ~ (boolean) `true` if `suffix` is a suffix of `s` +filter({f}, {src}, {...}) *vim.filter()* + Filter a table or iterator. + + This is a convenience function that performs: >lua + + vim.iter(src):filter(f):totable() +< + + Parameters: ~ + • {f} function(...):bool Filter function. Accepts the current + iterator or table values as arguments and returns true if those + values should be kept in the final table + • {src} table|function Table or iterator function to filter + + Return: ~ + (table) + + See also: ~ + • |Iter:filter()| + gsplit({s}, {sep}, {opts}) *vim.gsplit()* Splits a string at each instance of a separator. @@ -1698,6 +1718,64 @@ is_callable({f}) *vim.is_callable()* Return: ~ (boolean) `true` if `f` is callable, else `false` +iter({src}, {...}) *vim.iter()* + Create an Iter |lua-iter| object from a table or iterator. + + The input value can be a table or a function iterator (see |luaref-in|). + + This function wraps the input value into an interface which allows + chaining multiple pipeline stages in an efficient manner. Each pipeline + stage receives as input the output values from the prior stage. The values + used in the first stage of the pipeline depend on the type passed to this + function: + + • List tables pass only the value of each element + • Non-list tables pass both the key and value of each element + • Function iterators pass all of the values returned by their respective + function + + Examples: >lua + + local it = vim.iter({ 1, 2, 3, 4, 5 }) + it:map(function(v) + return v * 3 + end) + it:rev() + it:skip(2) + it:totable() + -- { 9, 6, 3 } + + vim.iter(ipairs({ 1, 2, 3, 4, 5 })):map(function(i, v) + if i > 2 then return v end + end):totable() + -- { 3, 4, 5 } + + local it = vim.iter(vim.gsplit('1,2,3,4,5', ',')) + it:map(function(s) return tonumber(s) end) + for i, d in it:enumerate() do + print(string.format("Column %d is %d", i, d)) + end + -- Column 1 is 1 + -- Column 2 is 2 + -- Column 3 is 3 + -- Column 4 is 4 + -- Column 5 is 5 + + vim.iter({ a = 1, b = 2, c = 3, z = 26 }):any(function(k, v) + return k == 'z' + end) + -- true +< + + Parameters: ~ + • {src} table|function Table or iterator. + + Return: ~ + Iter |lua-iter| + + See also: ~ + • |lua-iter| + list_contains({t}, {value}) *vim.list_contains()* Checks if a list-like table (integer keys without gaps) contains `value`. @@ -1740,6 +1818,26 @@ list_slice({list}, {start}, {finish}) *vim.list_slice()* Return: ~ (list) Copy of table sliced from start to finish (inclusive) +map({f}, {src}, {...}) *vim.map()* + Map and filter a table or iterator. + + This is a convenience function that performs: >lua + + vim.iter(src):map(f):totable() +< + + Parameters: ~ + • {f} function(...):?any Map function. Accepts the current iterator + or table values as arguments and returns one or more new + values. Nil values are removed from the final table. + • {src} table|function Table or iterator function to filter + + Return: ~ + (table) + + See also: ~ + • |Iter:map()| + pesc({s}) *vim.pesc()* Escapes magic chars in |lua-patterns|. @@ -2001,6 +2099,20 @@ tbl_values({t}) *vim.tbl_values()* Return: ~ (list) List of values +totable({f}, {...}) *vim.totable()* + Collect an iterator into a table. + + This is a convenience function that performs: >lua + + vim.iter(f):totable() +< + + Parameters: ~ + • {f} (function) Iterator function + + Return: ~ + (table) + trim({s}) *vim.trim()* Trim whitespace (Lua pattern "%s") from both sides of a string. @@ -2817,4 +2929,403 @@ range({spec}) *vim.version.range()* See also: ~ • # https://github.com/npm/node-semver#ranges + +============================================================================== +Lua module: iter *lua-iter* + +Iter:all({self}, {pred}) *Iter:all()* + Return true if all of the items in the iterator match the given predicate. + + Parameters: ~ + • {pred} function(...):bool Predicate function. Takes all values + returned from the previous stage in the pipeline as arguments + and returns true if the predicate matches. + +Iter:any({self}, {pred}) *Iter:any()* + Return true if any of the items in the iterator match the given predicate. + + Parameters: ~ + • {pred} function(...):bool Predicate function. Takes all values + returned from the previous stage in the pipeline as arguments + and returns true if the predicate matches. + +Iter:each({self}, {f}) *Iter:each()* + Call a function once for each item in the pipeline. + + This is used for functions which have side effects. To modify the values + in the iterator, use |Iter:map()|. + + This function drains the iterator. + + Parameters: ~ + • {f} function(...) Function to execute for each item in the pipeline. + Takes all of the values returned by the previous stage in the + pipeline as arguments. + +Iter:enumerate({self}) *Iter:enumerate()* + Add an iterator stage that returns the current iterator count as well as + the iterator value. + + For list tables, prefer >lua + + vim.iter(ipairs(t)) +< + + over + + >lua + + vim.iter(t):enumerate() +< + + as the former is faster. + + Example: >lua + + local it = vim.iter(vim.gsplit('abc', '')):enumerate() + it:next() + -- 1 'a' + it:next() + -- 2 'b' + it:next() + -- 3 'c' +< + + Return: ~ + Iter + +Iter:filter({self}, {f}) *Iter:filter()* + Add a filter step to the iterator pipeline. + + Example: >lua + + local bufs = vim.iter(vim.api.nvim_list_bufs()):filter(vim.api.nvim_buf_is_loaded) +< + + Parameters: ~ + • {f} function(...):bool Takes all values returned from the previous + stage in the pipeline and returns false or nil if the current + iterator element should be removed. + + Return: ~ + Iter + +Iter:find({self}, {f}) *Iter:find()* + Find the first value in the iterator that satisfies the given predicate. + + Advances the iterator. Returns nil and drains the iterator if no value is + found. + + Examples: >lua + + local it = vim.iter({ 3, 6, 9, 12 }) + it:find(12) + -- 12 + + local it = vim.iter({ 3, 6, 9, 12 }) + it:find(20) + -- nil + + local it = vim.iter({ 3, 6, 9, 12 }) + it:find(function(v) return v % 4 == 0 end) + -- 12 +< + + Return: ~ + any + +Iter:fold({self}, {init}, {f}) *Iter:fold()* + Fold an iterator or table into a single value. + + Parameters: ~ + • {init} any Initial value of the accumulator. + • {f} function(acc:any, ...):A Accumulation function. + + Return: ~ + any + +Iter:last({self}) *Iter:last()* + Return the last item in the iterator. + + Drains the iterator. + + Example: >lua + + local it = vim.iter(vim.gsplit('abcdefg', '')) + it:last() + -- 'g' + + local it = vim.iter({ 3, 6, 9, 12, 15 }) + it:last() + -- 15 +< + + Return: ~ + any + +Iter:map({self}, {f}) *Iter:map()* + Add a map step to the iterator pipeline. + + If the map function returns nil, the value is filtered from the iterator. + + Example: >lua + + local it = vim.iter({ 1, 2, 3, 4 }):map(function(v) + if v % 2 == 0 then + return v * 3 + end + end) + it:totable() + -- { 6, 12 } +< + + Parameters: ~ + • {f} function(...):any Mapping function. Takes all values returned + from the previous stage in the pipeline as arguments and returns + one or more new values, which are used in the next pipeline + stage. Nil return values returned are filtered from the output. + + Return: ~ + Iter + +Iter:next({self}) *Iter:next()* + Return the next value from the iterator. + + Example: >lua + + local it = vim.iter(string.gmatch('1 2 3', 'd+')):map(tonumber) + it:next() + -- 1 + it:next() + -- 2 + it:next() + -- 3 +< + + Return: ~ + any + +Iter:nextback({self}) *Iter:nextback()* + Return the next value from the end of the iterator. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({1, 2, 3, 4}) + it:nextback() + -- 4 + it:nextback() + -- 3 +< + + Return: ~ + any + +Iter:nth({self}, {n}) *Iter:nth()* + Return the nth value in the iterator. + + This function advances the iterator. + + Example: >lua + + local it = vim.iter({ 3, 6, 9, 12 }) + it:nth(2) + -- 6 + it:nth(2) + -- 12 +< + + Parameters: ~ + • {n} (number) The index of the value to return. + + Return: ~ + any + +Iter:nthback({self}, {n}) *Iter:nthback()* + Return the nth value from the end of the iterator. + + This function advances the iterator. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({ 3, 6, 9, 12 }) + it:nthback(2) + -- 9 + it:nthback(2) + -- 3 +< + + Parameters: ~ + • {n} (number) The index of the value to return. + + Return: ~ + any + +Iter:peek({self}) *Iter:peek()* + Peek at the next value in the iterator without consuming it. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({ 3, 6, 9, 12 }) + it:peek() + -- 3 + it:peek() + -- 3 + it:next() + -- 3 +< + + Return: ~ + any + +Iter:peekback({self}) *Iter:peekback()* + Return the next value from the end of the iterator without consuming it. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({1, 2, 3, 4}) + it:peekback() + -- 4 + it:peekback() + -- 4 + it:nextback() + -- 4 +< + + Return: ~ + any + +Iter:rev({self}) *Iter:rev()* + Reverse an iterator. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({ 3, 6, 9, 12 }):rev() + it:totable() + -- { 12, 9, 6, 3 } +< + + Return: ~ + Iter + +Iter:rfind({self}, {f}) *Iter:rfind()* + Find the first value in the iterator that satisfies the given predicate, + starting from the end. + + Advances the iterator. Returns nil and drains the iterator if no value is + found. + + Only supported for iterators on list-like tables. + + Examples: >lua + + local it = vim.iter({ 1, 2, 3, 2, 1 }):enumerate() + it:rfind(1) + -- 5 1 + it:rfind(1) + -- 1 1 +< + + Return: ~ + any + + See also: ~ + • Iter.find + +Iter:skip({self}, {n}) *Iter:skip()* + Skip values in the iterator. + + Example: >lua + + local it = vim.iter({ 3, 6, 9, 12 }):skip(2) + it:next() + -- 9 +< + + Parameters: ~ + • {n} (number) Number of values to skip. + + Return: ~ + Iter + +Iter:skipback({self}, {n}) *Iter:skipback()* + Skip values in the iterator starting from the end. + + Only supported for iterators on list-like tables. + + Example: >lua + + local it = vim.iter({ 1, 2, 3, 4, 5 }):skipback(2) + it:next() + -- 1 + it:nextback() + -- 3 +< + + Parameters: ~ + • {n} (number) Number of values to skip. + + Return: ~ + Iter + +Iter:slice({self}, {first}, {last}) *Iter:slice()* + Slice an iterator, changing its start and end positions. + + This is equivalent to :skip(first - 1):skipback(len - last + 1) + + Only supported for iterators on list-like tables. + + Parameters: ~ + • {first} (number) + • {last} (number) + + Return: ~ + Iter + +Iter:totable({self}) *Iter:totable()* + Collect the iterator into a table. + + The resulting table depends on the initial source in the iterator + pipeline. List-like tables and function iterators will be collected into a + list-like table. If multiple values are returned from the final stage in + the iterator pipeline, each value will be included in a table. If a + map-like table was used as the initial source, then a map-like table is + returned. + + Examples: >lua + + vim.iter(string.gmatch('100 20 50', 'd+')):map(tonumber):totable() + -- { 100, 20, 50 } + + vim.iter({ 1, 2, 3 }):map(function(v) return v, 2 * v end):totable() + -- { { 1, 2 }, { 2, 4 }, { 3, 6 } } + + vim.iter({ a = 1, b = 2, c = 3 }):filter(function(k, v) return v % 2 ~= 0 end):totable() + -- { a = 1, c = 3 } +< + + Return: ~ + (table) + +new({src}, {...}) *new()* + Create a new Iter object from a table or iterator. + + Parameters: ~ + • {src} table|function Table or iterator to drain values from + + Return: ~ + Iter + +next() *next()* + TODO: Documentation + vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl: diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index dc541fcf37..bbf63a5ee0 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -35,7 +35,8 @@ ADDED FEATURES *news-added* The following new APIs or features were added. -• ... +• |vim.iter()| provides a generic iterator interface for tables and Lua +iterators |luaref-in|. ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/lua/vim/_watch.lua b/runtime/lua/vim/_watch.lua index dba1522ec8..dbffd726a2 100644 --- a/runtime/lua/vim/_watch.lua +++ b/runtime/lua/vim/_watch.lua @@ -88,6 +88,9 @@ local default_poll_interval_ms = 2000 --- be invoked recursively) --- - children (table|nil) --- A mapping of directory entry name to its recursive watches +-- - started (boolean|nil) +-- Whether or not the watcher has first been initialized. Used +-- to prevent a flood of Created events on startup. local function poll_internal(path, opts, callback, watches) path = vim.fs.normalize(path) local interval = opts and opts.interval or default_poll_interval_ms @@ -112,7 +115,9 @@ local function poll_internal(path, opts, callback, watches) end) ) assert(not start_err, start_err) - callback(path, M.FileChangeType.Created) + if watches.started then + callback(path, M.FileChangeType.Created) + end end watches.cancel = function() @@ -132,6 +137,7 @@ local function poll_internal(path, opts, callback, watches) if not watches.children[name] then watches.children[name] = { is_dir = ftype == 'directory', + started = watches.started, } poll_internal(filepath_join(path, name), opts, callback, watches.children[name]) end @@ -150,6 +156,8 @@ local function poll_internal(path, opts, callback, watches) watches.children = newchildren end + watches.started = true + return watches.cancel end diff --git a/runtime/lua/vim/iter.lua b/runtime/lua/vim/iter.lua new file mode 100644 index 0000000000..b73c03ba9a --- /dev/null +++ b/runtime/lua/vim/iter.lua @@ -0,0 +1,836 @@ +--- Iterator implementation. + +---@class Iter +local Iter = {} +Iter.__index = Iter +Iter.__call = function(self) + return self:next() +end + +--- Special case implementations for iterators on list tables. +---@class ListIter : Iter +---@field _table table Underlying table data (table iterators only) +---@field _head number Index to the front of a table iterator (table iterators only) +---@field _tail number Index to the end of a table iterator (table iterators only) +local ListIter = {} +ListIter.__index = setmetatable(ListIter, Iter) +ListIter.__call = function(self) + return self:next() +end + +--- Special case implementations for iterators on non-list tables. +---@class TableIter : Iter +local TableIter = {} +TableIter.__index = setmetatable(TableIter, Iter) +TableIter.__call = function(self) + return self:next() +end + +---@private +local function unpack(t) + if type(t) == 'table' then + return _G.unpack(t) + end + return t +end + +---@private +local function pack(...) + if select('#', ...) > 1 then + return { ... } + end + return ... +end + +--- Add a filter step to the iterator pipeline. +--- +--- Example: +--- <pre>lua +--- local bufs = vim.iter(vim.api.nvim_list_bufs()):filter(vim.api.nvim_buf_is_loaded) +--- </pre> +--- +---@param f function(...):bool Takes all values returned from the previous stage in the pipeline and +--- returns false or nil if the current iterator element should be +--- removed. +---@return Iter +function Iter.filter(self, f) + ---@private + local function fn(...) + local result = nil + if select(1, ...) ~= nil then + if not f(...) then + return true, nil + else + result = pack(...) + end + end + return false, result + end + + local next = self.next + self.next = function(this) + local cont, result + repeat + cont, result = fn(next(this)) + until not cont + return unpack(result) + end + return self +end + +---@private +function ListIter.filter(self, f) + local inc = self._head < self._tail and 1 or -1 + local n = self._head + for i = self._head, self._tail - inc, inc do + local v = self._table[i] + if f(unpack(v)) then + self._table[n] = v + n = n + inc + end + end + self._tail = n + return self +end + +--- Add a map step to the iterator pipeline. +--- +--- If the map function returns nil, the value is filtered from the iterator. +--- +--- Example: +--- <pre>lua +--- local it = vim.iter({ 1, 2, 3, 4 }):map(function(v) +--- if v % 2 == 0 then +--- return v * 3 +--- end +--- end) +--- it:totable() +--- -- { 6, 12 } +--- </pre> +--- +---@param f function(...):any Mapping function. Takes all values returned from the previous stage +--- in the pipeline as arguments and returns one or more new values, +--- which are used in the next pipeline stage. Nil return values returned +--- are filtered from the output. +---@return Iter +function Iter.map(self, f) + ---@private + local function fn(...) + local result = nil + if select(1, ...) ~= nil then + result = pack(f(...)) + if result == nil then + return true, nil + end + end + return false, result + end + + local next = self.next + self.next = function(this) + local cont, result + repeat + cont, result = fn(next(this)) + until not cont + return unpack(result) + end + return self +end + +---@private +function ListIter.map(self, f) + local inc = self._head < self._tail and 1 or -1 + local n = self._head + for i = self._head, self._tail - inc, inc do + local v = pack(f(unpack(self._table[i]))) + if v ~= nil then + self._table[n] = v + n = n + inc + end + end + self._tail = n + return self +end + +--- Call a function once for each item in the pipeline. +--- +--- This is used for functions which have side effects. To modify the values in the iterator, use +--- |Iter:map()|. +--- +--- This function drains the iterator. +--- +---@param f function(...) Function to execute for each item in the pipeline. Takes all of the +--- values returned by the previous stage in the pipeline as arguments. +function Iter.each(self, f) + ---@private + local function fn(...) + if select(1, ...) ~= nil then + f(...) + return true + end + end + while fn(self:next()) do + end +end + +---@private +function ListIter.each(self, f) + local inc = self._head < self._tail and 1 or -1 + for i = self._head, self._tail - inc, inc do + f(unpack(self._table[i])) + end + self._head = self._tail +end + +--- Collect the iterator into a table. +--- +--- The resulting table depends on the initial source in the iterator pipeline. List-like tables +--- and function iterators will be collected into a list-like table. If multiple values are returned +--- from the final stage in the iterator pipeline, each value will be included in a table. If a +--- map-like table was used as the initial source, then a map-like table is returned. +--- +--- Examples: +--- <pre>lua +--- vim.iter(string.gmatch('100 20 50', '%d+')):map(tonumber):totable() +--- -- { 100, 20, 50 } +--- +--- vim.iter({ 1, 2, 3 }):map(function(v) return v, 2 * v end):totable() +--- -- { { 1, 2 }, { 2, 4 }, { 3, 6 } } +--- +--- vim.iter({ a = 1, b = 2, c = 3 }):filter(function(k, v) return v % 2 ~= 0 end):totable() +--- -- { a = 1, c = 3 } +--- </pre> +--- +---@return table +function Iter.totable(self) + local t = {} + + while true do + local args = pack(self:next()) + if args == nil then + break + end + t[#t + 1] = args + end + return t +end + +---@private +function ListIter.totable(self) + if self._head == 1 and self._tail == #self._table + 1 and self.next == ListIter.next then + return self._table + end + + return Iter.totable(self) +end + +---@private +function TableIter.totable(self) + local t = {} + for k, v in self do + t[k] = v + end + return t +end + +--- Fold an iterator or table into a single value. +--- +---@generic A +--- +---@param init A Initial value of the accumulator. +---@param f function(acc:A, ...):A Accumulation function. +---@return A +function Iter.fold(self, init, f) + local acc = init + + --- Use a closure to handle var args returned from iterator + ---@private + local function fn(...) + if select(1, ...) ~= nil then + acc = f(acc, ...) + return true + end + end + + while fn(self:next()) do + end + return acc +end + +---@private +function ListIter.fold(self, init, f) + local acc = init + local inc = self._head < self._tail and 1 or -1 + for i = self._head, self._tail - inc, inc do + acc = f(acc, unpack(self._table[i])) + end + return acc +end + +--- Return the next value from the iterator. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter(string.gmatch('1 2 3', '%d+')):map(tonumber) +--- it:next() +--- -- 1 +--- it:next() +--- -- 2 +--- it:next() +--- -- 3 +--- +--- </pre> +--- +---@return any +function Iter.next(self) -- luacheck: no unused args + -- This function is provided by the source iterator in Iter.new. This definition exists only for + -- the docstring +end + +---@private +function ListIter.next(self) + if self._head ~= self._tail then + local v = self._table[self._head] + local inc = self._head < self._tail and 1 or -1 + self._head = self._head + inc + return unpack(v) + end +end + +--- Reverse an iterator. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }):rev() +--- it:totable() +--- -- { 12, 9, 6, 3 } +--- +--- </pre> +--- +---@return Iter +function Iter.rev(self) + error('rev() requires a list-like table') + return self +end + +---@private +function ListIter.rev(self) + local inc = self._head < self._tail and 1 or -1 + self._head, self._tail = self._tail - inc, self._head - inc + return self +end + +--- Peek at the next value in the iterator without consuming it. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:peek() +--- -- 3 +--- it:peek() +--- -- 3 +--- it:next() +--- -- 3 +--- +--- </pre> +--- +---@return any +function Iter.peek(self) -- luacheck: no unused args + error('peek() requires a list-like table') +end + +---@private +function ListIter.peek(self) + if self._head ~= self._tail then + return self._table[self._head] + end +end + +--- Find the first value in the iterator that satisfies the given predicate. +--- +--- Advances the iterator. Returns nil and drains the iterator if no value is found. +--- +--- Examples: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:find(12) +--- -- 12 +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:find(20) +--- -- nil +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:find(function(v) return v % 4 == 0 end) +--- -- 12 +--- +--- </pre> +--- +---@return any +function Iter.find(self, f) + if type(f) ~= 'function' then + local val = f + f = function(v) + return v == val + end + end + + local result = nil + + --- Use a closure to handle var args returned from iterator + ---@private + local function fn(...) + if select(1, ...) ~= nil then + if f(...) then + result = pack(...) + else + return true + end + end + end + + while fn(self:next()) do + end + return unpack(result) +end + +--- Find the first value in the iterator that satisfies the given predicate, starting from the end. +--- +--- Advances the iterator. Returns nil and drains the iterator if no value is found. +--- +--- Only supported for iterators on list-like tables. +--- +--- Examples: +--- <pre>lua +--- +--- local it = vim.iter({ 1, 2, 3, 2, 1 }):enumerate() +--- it:rfind(1) +--- -- 5 1 +--- it:rfind(1) +--- -- 1 1 +--- +--- </pre> +--- +---@see Iter.find +--- +---@return any +function Iter.rfind(self, f) -- luacheck: no unused args + error('rfind() requires a list-like table') +end + +---@private +function ListIter.rfind(self, f) -- luacheck: no unused args + if type(f) ~= 'function' then + local val = f + f = function(v) + return v == val + end + end + + local inc = self._head < self._tail and 1 or -1 + for i = self._tail - inc, self._head, -inc do + local v = self._table[i] + if f(unpack(v)) then + self._tail = i + return unpack(v) + end + end + self._head = self._tail +end + +--- Return the next value from the end of the iterator. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- local it = vim.iter({1, 2, 3, 4}) +--- it:nextback() +--- -- 4 +--- it:nextback() +--- -- 3 +--- </pre> +--- +---@return any +function Iter.nextback(self) -- luacheck: no unused args + error('nextback() requires a list-like table') +end + +function ListIter.nextback(self) + if self._head ~= self._tail then + local inc = self._head < self._tail and 1 or -1 + self._tail = self._tail - inc + return self._table[self._tail] + end +end + +--- Return the next value from the end of the iterator without consuming it. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- local it = vim.iter({1, 2, 3, 4}) +--- it:peekback() +--- -- 4 +--- it:peekback() +--- -- 4 +--- it:nextback() +--- -- 4 +--- </pre> +--- +---@return any +function Iter.peekback(self) -- luacheck: no unused args + error('peekback() requires a list-like table') +end + +function ListIter.peekback(self) + if self._head ~= self._tail then + local inc = self._head < self._tail and 1 or -1 + return self._table[self._tail - inc] + end +end + +--- Skip values in the iterator. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }):skip(2) +--- it:next() +--- -- 9 +--- +--- </pre> +--- +---@param n number Number of values to skip. +---@return Iter +function Iter.skip(self, n) + for _ = 1, n do + local _ = self:next() + end + return self +end + +---@private +function ListIter.skip(self, n) + local inc = self._head < self._tail and n or -n + self._head = self._head + inc + if (inc > 0 and self._head > self._tail) or (inc < 0 and self._head < self._tail) then + self._head = self._tail + end + return self +end + +--- Skip values in the iterator starting from the end. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- local it = vim.iter({ 1, 2, 3, 4, 5 }):skipback(2) +--- it:next() +--- -- 1 +--- it:nextback() +--- -- 3 +--- </pre> +--- +---@param n number Number of values to skip. +---@return Iter +function Iter.skipback(self, n) -- luacheck: no unused args + error('skipback() requires a list-like table') + return self +end + +---@private +function ListIter.skipback(self, n) + local inc = self._head < self._tail and n or -n + self._tail = self._tail - inc + if (inc > 0 and self._head > self._tail) or (inc < 0 and self._head < self._tail) then + self._head = self._tail + end + return self +end + +--- Return the nth value in the iterator. +--- +--- This function advances the iterator. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:nth(2) +--- -- 6 +--- it:nth(2) +--- -- 12 +--- +--- </pre> +--- +---@param n number The index of the value to return. +---@return any +function Iter.nth(self, n) + if n > 0 then + return self:skip(n - 1):next() + end +end + +--- Return the nth value from the end of the iterator. +--- +--- This function advances the iterator. +--- +--- Only supported for iterators on list-like tables. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter({ 3, 6, 9, 12 }) +--- it:nthback(2) +--- -- 9 +--- it:nthback(2) +--- -- 3 +--- +--- </pre> +--- +---@param n number The index of the value to return. +---@return any +function Iter.nthback(self, n) + if n > 0 then + return self:skipback(n - 1):nextback() + end +end + +--- Slice an iterator, changing its start and end positions. +--- +--- This is equivalent to :skip(first - 1):skipback(len - last + 1) +--- +--- Only supported for iterators on list-like tables. +--- +---@param first number +---@param last number +---@return Iter +function Iter.slice(self, first, last) -- luacheck: no unused args + return self:skip(math.max(0, first - 1)):skipback(math.max(0, self._tail - last - 1)) +end + +--- Return true if any of the items in the iterator match the given predicate. +--- +---@param pred function(...):bool Predicate function. Takes all values returned from the previous +--- stage in the pipeline as arguments and returns true if the +--- predicate matches. +function Iter.any(self, pred) + local any = false + + --- Use a closure to handle var args returned from iterator + ---@private + local function fn(...) + if select(1, ...) ~= nil then + if pred(...) then + any = true + else + return true + end + end + end + + while fn(self:next()) do + end + return any +end + +--- Return true if all of the items in the iterator match the given predicate. +--- +---@param pred function(...):bool Predicate function. Takes all values returned from the previous +--- stage in the pipeline as arguments and returns true if the +--- predicate matches. +function Iter.all(self, pred) + local all = true + + ---@private + local function fn(...) + if select(1, ...) ~= nil then + if not pred(...) then + all = false + else + return true + end + end + end + + while fn(self:next()) do + end + return all +end + +--- Return the last item in the iterator. +--- +--- Drains the iterator. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter(vim.gsplit('abcdefg', '')) +--- it:last() +--- -- 'g' +--- +--- local it = vim.iter({ 3, 6, 9, 12, 15 }) +--- it:last() +--- -- 15 +--- +--- </pre> +--- +---@return any +function Iter.last(self) + local last = self:next() + local cur = self:next() + while cur do + last = cur + cur = self:next() + end + return last +end + +---@private +function ListIter.last(self) + local inc = self._head < self._tail and 1 or -1 + local v = self._table[self._tail - inc] + self._head = self._tail + return v +end + +--- Add an iterator stage that returns the current iterator count as well as the iterator value. +--- +--- For list tables, prefer +--- <pre>lua +--- vim.iter(ipairs(t)) +--- </pre> +--- +--- over +--- +--- <pre>lua +--- vim.iter(t):enumerate() +--- </pre> +--- +--- as the former is faster. +--- +--- Example: +--- <pre>lua +--- +--- local it = vim.iter(vim.gsplit('abc', '')):enumerate() +--- it:next() +--- -- 1 'a' +--- it:next() +--- -- 2 'b' +--- it:next() +--- -- 3 'c' +--- +--- </pre> +--- +---@return Iter +function Iter.enumerate(self) + local i = 0 + return self:map(function(...) + i = i + 1 + return i, ... + end) +end + +---@private +function ListIter.enumerate(self) + local inc = self._head < self._tail and 1 or -1 + for i = self._head, self._tail - inc, inc do + local v = self._table[i] + self._table[i] = { i, v } + end + return self +end + +--- Create a new Iter object from a table or iterator. +--- +---@param src table|function Table or iterator to drain values from +---@return Iter +function Iter.new(src, ...) + local it = {} + if type(src) == 'table' then + local t = {} + + -- Check if source table can be treated like a list (indices are consecutive integers + -- starting from 1) + local count = 0 + for _ in pairs(src) do + count = count + 1 + local v = src[count] + if v == nil then + return TableIter.new(src) + end + t[count] = v + end + return ListIter.new(t) + end + + if type(src) == 'function' then + local s, var = ... + + --- Use a closure to handle var args returned from iterator + ---@private + local function fn(...) + if select(1, ...) ~= nil then + var = select(1, ...) + return ... + end + end + + function it.next() + return fn(src(s, var)) + end + + setmetatable(it, Iter) + else + error('src must be a table or function') + end + return it +end + +--- Create a new ListIter +--- +---@param t table List-like table. Caller guarantees that this table is a valid list. +---@return Iter +---@private +function ListIter.new(t) + local it = {} + it._table = t + it._head = 1 + it._tail = #t + 1 + setmetatable(it, ListIter) + return it +end + +--- Create a new TableIter +--- +---@param t table Table to iterate over. For list-like tables, use ListIter.new instead. +---@return Iter +---@private +function TableIter.new(t) + local it = {} + + local index = nil + function it.next() + local k, v = next(t, index) + if k ~= nil then + index = k + return k, v + end + end + + setmetatable(it, TableIter) + return it +end + +return Iter diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index f700f4a6b3..1a96ef9bc4 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -884,4 +884,109 @@ function vim.defaulttable(create) }) end +--- Create an Iter |lua-iter| object from a table or iterator. +--- +--- The input value can be a table or a function iterator (see |luaref-in|). +--- +--- This function wraps the input value into an interface which allows chaining +--- multiple pipeline stages in an efficient manner. Each pipeline stage +--- receives as input the output values from the prior stage. The values used in +--- the first stage of the pipeline depend on the type passed to this function: +--- +--- - List tables pass only the value of each element +--- - Non-list tables pass both the key and value of each element +--- - Function iterators pass all of the values returned by their respective +--- function +--- +--- Examples: +--- <pre>lua +--- local it = vim.iter({ 1, 2, 3, 4, 5 }) +--- it:map(function(v) +--- return v * 3 +--- end) +--- it:rev() +--- it:skip(2) +--- it:totable() +--- -- { 9, 6, 3 } +--- +--- vim.iter(ipairs({ 1, 2, 3, 4, 5 })):map(function(i, v) +--- if i > 2 then return v end +--- end):totable() +--- -- { 3, 4, 5 } +--- +--- local it = vim.iter(vim.gsplit('1,2,3,4,5', ',')) +--- it:map(function(s) return tonumber(s) end) +--- for i, d in it:enumerate() do +--- print(string.format("Column %d is %d", i, d)) +--- end +--- -- Column 1 is 1 +--- -- Column 2 is 2 +--- -- Column 3 is 3 +--- -- Column 4 is 4 +--- -- Column 5 is 5 +--- +--- vim.iter({ a = 1, b = 2, c = 3, z = 26 }):any(function(k, v) +--- return k == 'z' +--- end) +--- -- true +--- </pre> +--- +---@see |lua-iter| +--- +---@param src table|function Table or iterator. +---@return Iter @|lua-iter| +function vim.iter(src, ...) + local Iter = require('vim.iter') + return Iter.new(src, ...) +end + +--- Collect an iterator into a table. +--- +--- This is a convenience function that performs: +--- <pre>lua +--- vim.iter(f):totable() +--- </pre> +--- +---@param f function Iterator function +---@return table +function vim.totable(f, ...) + return vim.iter(f, ...):totable() +end + +--- Filter a table or iterator. +--- +--- This is a convenience function that performs: +--- <pre>lua +--- vim.iter(src):filter(f):totable() +--- </pre> +--- +---@see |Iter:filter()| +--- +---@param f function(...):bool Filter function. Accepts the current iterator or table values as +--- arguments and returns true if those values should be kept in the +--- final table +---@param src table|function Table or iterator function to filter +---@return table +function vim.filter(f, src, ...) + return vim.iter(src, ...):filter(f):totable() +end + +--- Map and filter a table or iterator. +--- +--- This is a convenience function that performs: +--- <pre>lua +--- vim.iter(src):map(f):totable() +--- </pre> +--- +---@see |Iter:map()| +--- +---@param f function(...):?any Map function. Accepts the current iterator or table values as +--- arguments and returns one or more new values. Nil values are removed +--- from the final table. +---@param src table|function Table or iterator function to filter +---@return table +function vim.map(f, src, ...) + return vim.iter(src, ...):map(f):totable() +end + return vim diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index 8860bdd5c6..52d03d9746 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -154,8 +154,10 @@ CONFIG = { 'fs.lua', 'secure.lua', 'version.lua', + 'iter.lua', ], 'files': [ + 'runtime/lua/vim/iter.lua', 'runtime/lua/vim/_editor.lua', 'runtime/lua/vim/shared.lua', 'runtime/lua/vim/loader.lua', @@ -185,6 +187,8 @@ CONFIG = { 'fn_helptag_fmt': lambda fstem, name: ( f'*vim.{name}()*' if fstem.lower() == '_editor' + else f'*{name}()*' + if fstem in ('iter.lua') else f'*{fstem}.{name}()*'), 'module_override': { # `shared` functions are exposed on the `vim` module. diff --git a/scripts/genvimvim.lua b/scripts/genvimvim.lua index 3e9e7077be..52bf46399d 100644 --- a/scripts/genvimvim.lua +++ b/scripts/genvimvim.lua @@ -37,6 +37,9 @@ local function cmd_kw(prev_cmd, cmd) while cmd:sub(shift, shift) == prev_cmd:sub(shift, shift) do shift = shift + 1 end + if cmd:sub(1, shift) == 'def' then + shift = shift + 1 + end if shift >= #cmd then return cmd else @@ -67,6 +70,20 @@ for _, cmd_desc in ipairs(ex_cmds.cmds) do if cmd:match('%w') and cmd ~= 'z' and not is_special_cased_cmd(cmd) then w(' ' .. cmd_kw(prev_cmd, cmd)) end + if cmd == 'delete' then + -- Add special abbreviations of :delete + w(' ' .. cmd_kw('d', 'dl')) + w(' ' .. cmd_kw('del', 'dell')) + w(' ' .. cmd_kw('dele', 'delel')) + w(' ' .. cmd_kw('delet', 'deletl')) + w(' ' .. cmd_kw('delete', 'deletel')) + w(' ' .. cmd_kw('d', 'dp')) + w(' ' .. cmd_kw('de', 'dep')) + w(' ' .. cmd_kw('del', 'delp')) + w(' ' .. cmd_kw('dele', 'delep')) + w(' ' .. cmd_kw('delet', 'deletp')) + w(' ' .. cmd_kw('delete', 'deletep')) + end prev_cmd = cmd end diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index e138c50b6a..63d5f94f11 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -3207,7 +3207,7 @@ static int ex_defer_inner(char *name, char **arg, const partial_T *const partial } /// Return true if currently inside a function call. -/// Give an error message and return FALSE when not. +/// Give an error message and return false when not. bool can_add_defer(void) { if (get_current_funccal() == NULL) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index a1b43113a6..b75ff45843 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -933,6 +933,17 @@ void free_prev_shellcmd(void) #endif +/// Check that "prevcmd" is not NULL. If it is NULL then give an error message +/// and return false. +static int prevcmd_is_set(void) +{ + if (prevcmd == NULL) { + emsg(_(e_noprev)); + return false; + } + return true; +} + /// Handle the ":!cmd" command. Also for ":r !cmd" and ":w !cmd" /// Bangs in the argument are replaced with the previously entered command. /// Remember the argument. @@ -974,8 +985,7 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out len += strlen(newcmd); } if (ins_prevcmd) { - if (prevcmd == NULL) { - emsg(_(e_noprev)); + if (!prevcmd_is_set()) { xfree(newcmd); return; } @@ -1012,10 +1022,20 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out } } while (trailarg != NULL); - xfree(prevcmd); - prevcmd = newcmd; + // Only set "prevcmd" if there is a command to run, otherwise keep te one + // we have. + if (strlen(newcmd) > 0) { + xfree(prevcmd); + prevcmd = newcmd; + } else { + free_newcmd = true; + } if (bangredo) { // put cmd in redo buffer for ! command + if (!prevcmd_is_set()) { + goto theend; + } + // If % or # appears in the command, it must have been escaped. // Reescape them, so that redoing them does not substitute them by the // buffername. @@ -1028,6 +1048,9 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out } // Add quotes around the command, for shells that need them. if (*p_shq != NUL) { + if (free_newcmd) { + xfree(newcmd); + } newcmd = xmalloc(strlen(prevcmd) + 2 * strlen(p_shq) + 1); STRCPY(newcmd, p_shq); STRCAT(newcmd, prevcmd); @@ -1050,6 +1073,8 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out do_filter(line1, line2, eap, newcmd, do_in, do_out); apply_autocmds(EVENT_SHELLFILTERPOST, NULL, NULL, false, curbuf); } + +theend: if (free_newcmd) { xfree(newcmd); } diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index 95fe4d70d3..28b40994a1 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -1097,7 +1097,8 @@ retnomove: return IN_UNKNOWN; } - on_status_line = (grid == DEFAULT_GRID_HANDLE && row + wp->w_winbar_height >= wp->w_height) + bool below_window = grid == DEFAULT_GRID_HANDLE && row + wp->w_winbar_height >= wp->w_height; + on_status_line = (below_window) ? row + wp->w_winbar_height - wp->w_height + 1 == 1 : false; @@ -1105,7 +1106,7 @@ retnomove: ? wp->w_winbar_height != 0 : false; - on_statuscol = !on_status_line && !on_winbar && col < win_col_off(wp) + on_statuscol = !below_window && !on_status_line && !on_winbar && col < win_col_off(wp) ? *wp->w_p_stc != NUL : false; @@ -1144,7 +1145,7 @@ retnomove: dragwin = NULL; // winpos and height may change in win_enter()! - if (grid == DEFAULT_GRID_HANDLE && row + wp->w_winbar_height >= wp->w_height) { + if (below_window) { // In (or below) status line status_line_offset = row + wp->w_winbar_height - wp->w_height + 1; dragwin = wp; diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 1ee1a13fd5..e5caf6f6f7 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -3029,6 +3029,360 @@ describe('lua stdlib', function() eq(false, if_nil(d, c)) eq(NIL, if_nil(a)) end) + + describe('vim.iter', function() + it('filter()', function() + local function odd(v) + return v % 2 ~= 0 + end + + local t = { 1, 2, 3, 4, 5 } + eq({ 1, 3, 5 }, vim.iter(t):filter(odd):totable()) + eq({ 2, 4 }, vim.iter(t):filter(function(v) return not odd(v) end):totable()) + eq({}, vim.iter(t):filter(function(v) if v > 5 then return v end end):totable()) + + do + local it = vim.iter(ipairs(t)) + it:filter(function(i, v) return i > 1 and v < 5 end) + it:map(function(_, v) return v * 2 end) + eq({ 4, 6, 8 }, it:totable()) + end + + local it = vim.iter(string.gmatch('the quick brown fox', '%w+')) + eq({'the', 'fox'}, it:filter(function(s) return #s <= 3 end):totable()) + end) + + it('map()', function() + local t = { 1, 2, 3, 4, 5 } + eq( + { 2, 4, 6, 8, 10 }, + vim + .iter(t) + :map(function(v) + return 2 * v + end) + :totable() + ) + + local it = vim.gsplit( + [[ + Line 1 + Line 2 + Line 3 + Line 4 + ]], + '\n' + ) + + eq( + { 'Lion 2', 'Lion 4' }, + vim + .iter(it) + :map(function(s) + local lnum = s:match('(%d+)') + if lnum and tonumber(lnum) % 2 == 0 then + return vim.trim(s:gsub('Line', 'Lion')) + end + end) + :totable() + ) + end) + + it('for loops', function() + local t = {1, 2, 3, 4, 5} + local acc = 0 + for v in vim.iter(t):map(function(v) return v * 3 end) do + acc = acc + v + end + eq(45, acc) + end) + + it('totable()', function() + do + local it = vim.iter({1, 2, 3}):map(function(v) return v, v*v end) + eq({{1, 1}, {2, 4}, {3, 9}}, it:totable()) + end + + do + local it = vim.iter(string.gmatch('1,4,lol,17,blah,2,9,3', '%d+')):map(tonumber) + eq({1, 4, 17, 2, 9, 3}, it:totable()) + end + end) + + it('next()', function() + local it = vim.iter({1, 2, 3}):map(function(v) return 2 * v end) + eq(2, it:next()) + eq(4, it:next()) + eq(6, it:next()) + eq(nil, it:next()) + end) + + it('rev()', function() + eq({3, 2, 1}, vim.iter({1, 2, 3}):rev():totable()) + + local it = vim.iter(string.gmatch("abc", "%w")) + matches('rev%(%) requires a list%-like table', pcall_err(it.rev, it)) + end) + + it('skip()', function() + do + local t = {4, 3, 2, 1} + eq(t, vim.iter(t):skip(0):totable()) + eq({3, 2, 1}, vim.iter(t):skip(1):totable()) + eq({2, 1}, vim.iter(t):skip(2):totable()) + eq({1}, vim.iter(t):skip(#t - 1):totable()) + eq({}, vim.iter(t):skip(#t):totable()) + eq({}, vim.iter(t):skip(#t + 1):totable()) + end + + do + local function skip(n) + return vim.iter(vim.gsplit('a|b|c|d', '|')):skip(n):totable() + end + eq({'a', 'b', 'c', 'd'}, skip(0)) + eq({'b', 'c', 'd'}, skip(1)) + eq({'c', 'd'}, skip(2)) + eq({'d'}, skip(3)) + eq({}, skip(4)) + eq({}, skip(5)) + end + end) + + it('skipback()', function() + do + local t = {4, 3, 2, 1} + eq(t, vim.iter(t):skipback(0):totable()) + eq({4, 3, 2}, vim.iter(t):skipback(1):totable()) + eq({4, 3}, vim.iter(t):skipback(2):totable()) + eq({4}, vim.iter(t):skipback(#t - 1):totable()) + eq({}, vim.iter(t):skipback(#t):totable()) + eq({}, vim.iter(t):skipback(#t + 1):totable()) + end + + local it = vim.iter(vim.gsplit('a|b|c|d', '|')) + matches('skipback%(%) requires a list%-like table', pcall_err(it.skipback, it, 0)) + end) + + it('slice()', function() + local t = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + eq({3, 4, 5, 6, 7}, vim.iter(t):slice(3, 7):totable()) + eq({}, vim.iter(t):slice(6, 5):totable()) + eq({}, vim.iter(t):slice(0, 0):totable()) + eq({1}, vim.iter(t):slice(1, 1):totable()) + eq({1, 2}, vim.iter(t):slice(1, 2):totable()) + eq({10}, vim.iter(t):slice(10, 10):totable()) + eq({8, 9, 10}, vim.iter(t):slice(8, 11):totable()) + end) + + it('nth()', function() + do + local t = {4, 3, 2, 1} + eq(nil, vim.iter(t):nth(0)) + eq(4, vim.iter(t):nth(1)) + eq(3, vim.iter(t):nth(2)) + eq(2, vim.iter(t):nth(3)) + eq(1, vim.iter(t):nth(4)) + eq(nil, vim.iter(t):nth(5)) + end + + do + local function nth(n) + return vim.iter(vim.gsplit('a|b|c|d', '|')):nth(n) + end + eq(nil, nth(0)) + eq('a', nth(1)) + eq('b', nth(2)) + eq('c', nth(3)) + eq('d', nth(4)) + eq(nil, nth(5)) + end + end) + + it('nthback()', function() + do + local t = {4, 3, 2, 1} + eq(nil, vim.iter(t):nthback(0)) + eq(1, vim.iter(t):nthback(1)) + eq(2, vim.iter(t):nthback(2)) + eq(3, vim.iter(t):nthback(3)) + eq(4, vim.iter(t):nthback(4)) + eq(nil, vim.iter(t):nthback(5)) + end + + local it = vim.iter(vim.gsplit('a|b|c|d', '|')) + matches('skipback%(%) requires a list%-like table', pcall_err(it.nthback, it, 1)) + end) + + it('any()', function() + local function odd(v) + return v % 2 ~= 0 + end + + do + local t = { 4, 8, 9, 10 } + eq(true, vim.iter(t):any(odd)) + end + + do + local t = { 4, 8, 10 } + eq(false, vim.iter(t):any(odd)) + end + + do + eq(true, vim.iter(vim.gsplit('a|b|c|d', '|')):any(function(s) return s == 'd' end)) + eq(false, vim.iter(vim.gsplit('a|b|c|d', '|')):any(function(s) return s == 'e' end)) + end + end) + + it('all()', function() + local function odd(v) + return v % 2 ~= 0 + end + + do + local t = { 3, 5, 7, 9 } + eq(true, vim.iter(t):all(odd)) + end + + do + local t = { 3, 5, 7, 10 } + eq(false, vim.iter(t):all(odd)) + end + + do + eq(true, vim.iter(vim.gsplit('a|a|a|a', '|')):all(function(s) return s == 'a' end)) + eq(false, vim.iter(vim.gsplit('a|a|a|b', '|')):all(function(s) return s == 'a' end)) + end + end) + + it('last()', function() + local s = 'abcdefghijklmnopqrstuvwxyz' + eq('z', vim.iter(vim.split(s, '')):last()) + eq('z', vim.iter(vim.gsplit(s, '')):last()) + end) + + it('enumerate()', function() + local it = vim.iter(vim.gsplit('abc', '')):enumerate() + eq({1, 'a'}, {it:next()}) + eq({2, 'b'}, {it:next()}) + eq({3, 'c'}, {it:next()}) + eq({}, {it:next()}) + end) + + it('peek()', function() + do + local it = vim.iter({ 3, 6, 9, 12 }) + eq(3, it:peek()) + eq(3, it:peek()) + eq(3, it:next()) + end + + do + local it = vim.iter(vim.gsplit('hi', '')) + matches('peek%(%) requires a list%-like table', pcall_err(it.peek, it)) + end + end) + + it('find()', function() + local t = {3, 6, 9, 12} + eq(12, vim.iter(t):find(12)) + eq(nil, vim.iter(t):find(15)) + eq(12, vim.iter(t):find(function(v) return v % 4 == 0 end)) + + do + local it = vim.iter(t) + local pred = function(v) return v % 3 == 0 end + eq(3, it:find(pred)) + eq(6, it:find(pred)) + eq(9, it:find(pred)) + eq(12, it:find(pred)) + eq(nil, it:find(pred)) + end + + do + local it = vim.iter(vim.gsplit('AbCdE', '')) + local pred = function(s) return s:match('[A-Z]') end + eq('A', it:find(pred)) + eq('C', it:find(pred)) + eq('E', it:find(pred)) + eq(nil, it:find(pred)) + end + end) + + it('rfind()', function() + local t = {1, 2, 3, 2, 1} + do + local it = vim.iter(t) + eq(1, it:rfind(1)) + eq(1, it:rfind(1)) + eq(nil, it:rfind(1)) + end + + do + local it = vim.iter(t):enumerate() + local pred = function(i) return i % 2 ~= 0 end + eq({5, 1}, {it:rfind(pred)}) + eq({3, 3}, {it:rfind(pred)}) + eq({1, 1}, {it:rfind(pred)}) + eq(nil, it:rfind(pred)) + end + + do + local it = vim.iter(vim.gsplit('AbCdE', '')) + matches('rfind%(%) requires a list%-like table', pcall_err(it.rfind, it, 'E')) + end + end) + + it('nextback()', function() + do + local it = vim.iter({ 1, 2, 3, 4 }) + eq(4, it:nextback()) + eq(3, it:nextback()) + eq(2, it:nextback()) + eq(1, it:nextback()) + eq(nil, it:nextback()) + eq(nil, it:nextback()) + end + + do + local it = vim.iter(vim.gsplit('hi', '')) + matches('nextback%(%) requires a list%-like table', pcall_err(it.nextback, it)) + end + end) + + it('peekback()', function() + do + local it = vim.iter({ 1, 2, 3, 4 }) + eq(4, it:peekback()) + eq(4, it:peekback()) + eq(4, it:peekback()) + end + + do + local it = vim.iter(vim.gsplit('hi', '')) + matches('peekback%(%) requires a list%-like table', pcall_err(it.peekback, it)) + end + end) + + it('fold()', function() + local t = {1, 2, 3, 4, 5} + eq(115, vim.iter(t):fold(100, function(acc, v) return acc + v end)) + eq({5, 4, 3, 2, 1}, vim.iter(t):fold({}, function(acc, v) + table.insert(acc, 1, v) + return acc + end)) + end) + + it('handles map-like tables', function() + local t = { a = 1, b = 2, c = 3 } + local it = vim.iter(t):map(function(k, v) + if v % 2 ~= 0 then + return k:upper(), v * 2 + end + end) + eq({ A = 2, C = 6 }, it:totable()) + end) + end) end) describe('lua: builtin modules', function() diff --git a/test/functional/lua/watch_spec.lua b/test/functional/lua/watch_spec.lua index bbcfd27cde..ad8678c17a 100644 --- a/test/functional/lua/watch_spec.lua +++ b/test/functional/lua/watch_spec.lua @@ -122,10 +122,6 @@ describe('vim._watch', function() table.insert(events, { path = path, change_type = change_type }) end) - -- polling generates Created events for the existing entries when it starts. - expected_events = expected_events + 1 - wait_for_events() - vim.wait(100) local watched_path = root_dir .. '/file' @@ -158,39 +154,35 @@ describe('vim._watch', function() root_dir ) - eq(5, #result) - eq({ - change_type = exec_lua([[return vim._watch.FileChangeType.Created]]), - path = root_dir, - }, result[1]) + eq(4, #result) eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Created]]), path = root_dir .. '/file', - }, result[2]) + }, result[1]) eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]), path = root_dir, - }, result[3]) + }, result[2]) -- The file delete and corresponding directory change events do not happen in any -- particular order, so allow either - if result[4].path == root_dir then + if result[3].path == root_dir then eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]), path = root_dir, - }, result[4]) + }, result[3]) eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]), path = root_dir .. '/file', - }, result[5]) + }, result[4]) else eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]), path = root_dir .. '/file', - }, result[4]) + }, result[3]) eq({ change_type = exec_lua([[return vim._watch.FileChangeType.Changed]]), path = root_dir, - }, result[5]) + }, result[4]) end end) end) diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index a2fe875e65..0eec693182 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -457,6 +457,9 @@ describe('statuscolumn', function() -- Check that statusline click doesn't register as statuscolumn click meths.input_mouse('right', 'press', '', 0, 12, 0) eq('', eval("g:testvar")) + -- Check that cmdline click doesn't register as statuscolumn click + meths.input_mouse('right', 'press', '', 0, 13, 0) + eq('', eval("g:testvar")) end) it('click labels do not leak memory', function() diff --git a/test/old/testdir/test_shell.vim b/test/old/testdir/test_shell.vim index 3d0056bfc1..7172d5ba33 100644 --- a/test/old/testdir/test_shell.vim +++ b/test/old/testdir/test_shell.vim @@ -206,4 +206,48 @@ func Test_set_shell() call delete('Xtestout') endfunc +func Test_shell_repeat() + CheckUnix + + let save_shell = &shell + + call writefile(['#!/bin/sh', 'echo "Cmd: [$*]" > Xlog'], 'Xtestshell', 'D') + call setfperm('Xtestshell', "r-x------") + set shell=./Xtestshell + defer delete('Xlog') + + call feedkeys(":!echo coconut\<CR>", 'xt') " Run command + call assert_equal(['Cmd: [-c echo coconut]'], readfile('Xlog')) + + call feedkeys(":!!\<CR>", 'xt') " Re-run previous + call assert_equal(['Cmd: [-c echo coconut]'], readfile('Xlog')) + + call writefile(['empty'], 'Xlog') + call feedkeys(":!\<CR>", 'xt') " :! + call assert_equal(['Cmd: [-c ]'], readfile('Xlog')) + + call feedkeys(":!!\<CR>", 'xt') " :! doesn't clear previous command + call assert_equal(['Cmd: [-c echo coconut]'], readfile('Xlog')) + + call feedkeys(":!echo banana\<CR>", 'xt') " Make sure setting previous command keeps working after a :! no-op + call assert_equal(['Cmd: [-c echo banana]'], readfile('Xlog')) + call feedkeys(":!!\<CR>", 'xt') + call assert_equal(['Cmd: [-c echo banana]'], readfile('Xlog')) + + let &shell = save_shell +endfunc + +func Test_shell_no_prevcmd() + " this doesn't do anything, just check it doesn't crash + let after =<< trim END + exe "normal !!\<CR>" + call writefile([v:errmsg, 'done'], 'Xtestdone') + qall! + END + if RunVim([], after, '--clean') + call assert_equal(['E34: No previous command', 'done'], readfile('Xtestdone')) + endif + call delete('Xtestdone') +endfunc + " vim: shiftwidth=2 sts=2 expandtab |