From 7c661207cc4357553ed2b057b6c8f28400024361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Fu=C3=9Fenegger?= Date: Thu, 8 Jun 2023 12:11:24 +0200 Subject: [PATCH] feat(lua): add ringbuffer (#22894) https://en.wikipedia.org/wiki/Circular_buffer --- runtime/doc/lua.txt | 54 ++++++++++++++++++ runtime/doc/news.txt | 2 + runtime/lua/vim/shared.lua | 94 ++++++++++++++++++++++++++++++++ test/functional/lua/vim_spec.lua | 40 ++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 5e0a1edc11..babd9b801c 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1847,6 +1847,60 @@ pesc({s}) *vim.pesc()* See also: ~ • https://github.com/rxi/lume +ringbuf({size}) *vim.ringbuf()* + Create a ring buffer limited to a maximal number of items. Once the buffer + is full, adding a new entry overrides the oldest entry. +> + + local ringbuf = vim.ringbuf(4) + ringbuf:push("a") + ringbuf:push("b") + ringbuf:push("c") + ringbuf:push("d") + ringbuf:push("e") -- overrides "a" + print(ringbuf:pop()) -- returns "b" + print(ringbuf:pop()) -- returns "c" + + -- Can be used as iterator. Pops remaining items: + for val in ringbuf do + print(val) + end +< + + Returns a Ringbuf instance with the following methods: + + • |Ringbuf:push()| + • |Ringbuf:pop()| + • |Ringbuf:peek()| + • |Ringbuf:clear()| + + Parameters: ~ + • {size} (integer) + + Return: ~ + (table) + +Ringbuf:clear({self}) *Ringbuf:clear()* + Clear all items. + +Ringbuf:peek({self}) *Ringbuf:peek()* + Returns the first unread item without removing it + + Return: ~ + any?|ni + +Ringbuf:pop({self}) *Ringbuf:pop()* + Removes and returns the first unread item + + Return: ~ + any?|ni + +Ringbuf:push({self}, {item}) *Ringbuf:push()* + Adds an item, overriding the oldest item if the buffer is full. + + Parameters: ~ + • {item} any + spairs({t}) *vim.spairs()* Enumerate a table sorted by its keys. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 4afb3429f4..dbf5b131eb 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -59,6 +59,8 @@ The following new APIs or features were added. • |vim.iter()| provides a generic iterator interface for tables and Lua iterators |luaref-in|. +• Added |vim.ringbuf()| to create ring buffers. + • Added |vim.keycode()| for translating keycodes in a string. • Added |vim.treesitter.query.omnifunc()| for treesitter query files (set by diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 4f230c4412..f899157ab7 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -881,4 +881,98 @@ function vim.defaulttable(create) }) end +do + ---@class vim.Ringbuf + ---@field private _items table[] + ---@field private _idx_read integer + ---@field private _idx_write integer + ---@field private _size integer + local Ringbuf = {} + + --- Clear all items + function Ringbuf.clear(self) + self._items = {} + self._idx_read = 0 + self._idx_write = 0 + end + + --- Adds an item, overriding the oldest item if the buffer is full. + ---@generic T + ---@param item T + function Ringbuf.push(self, item) + self._items[self._idx_write] = item + self._idx_write = (self._idx_write + 1) % self._size + if self._idx_write == self._idx_read then + self._idx_read = (self._idx_read + 1) % self._size + end + end + + --- Removes and returns the first unread item + ---@generic T + ---@return T? + function Ringbuf.pop(self) + local idx_read = self._idx_read + if idx_read == self._idx_write then + return nil + end + local item = self._items[idx_read] + self._items[idx_read] = nil + self._idx_read = (idx_read + 1) % self._size + return item + end + + --- Returns the first unread item without removing it + ---@generic T + ---@return T? + function Ringbuf.peek(self) + if self._idx_read == self._idx_write then + return nil + end + return self._items[self._idx_read] + end + + --- Create a ring buffer limited to a maximal number of items. + --- Once the buffer is full, adding a new entry overrides the oldest entry. + --- + ---
+  ---   local ringbuf = vim.ringbuf(4)
+  ---   ringbuf:push("a")
+  ---   ringbuf:push("b")
+  ---   ringbuf:push("c")
+  ---   ringbuf:push("d")
+  ---   ringbuf:push("e")    -- overrides "a"
+  ---   print(ringbuf:pop()) -- returns "b"
+  ---   print(ringbuf:pop()) -- returns "c"
+  ---
+  ---   -- Can be used as iterator. Pops remaining items:
+  ---   for val in ringbuf do
+  ---     print(val)
+  ---   end
+  --- 
+ --- + --- Returns a Ringbuf instance with the following methods: + --- + --- - |Ringbuf:push()| + --- - |Ringbuf:pop()| + --- - |Ringbuf:peek()| + --- - |Ringbuf:clear()| + --- + ---@param size integer + ---@return vim.Ringbuf ringbuf (table) + function vim.ringbuf(size) + local ringbuf = { + _items = {}, + _size = size + 1, + _idx_read = 0, + _idx_write = 0, + } + return setmetatable(ringbuf, { + __index = Ringbuf, + __call = function(self) + return self:pop() + end, + }) + end +end + return vim diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 55c03e21b3..d5f550a5d1 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -3040,6 +3040,46 @@ describe('lua stdlib', function() eq(4, exec_lua [[ return vim.re.match("abcde", '[a-c]+') ]]) end) + + it("vim.ringbuf", function() + local results = exec_lua([[ + local ringbuf = vim.ringbuf(3) + ringbuf:push("a") -- idx: 0 + local peeka1 = ringbuf:peek() + local peeka2 = ringbuf:peek() + local popa = ringbuf:pop() + local popnil = ringbuf:pop() + ringbuf:push("a") -- idx: 1 + ringbuf:push("b") -- idx: 2 + + -- doesn't read last added item, but uses separate read index + local pop_after_add_b = ringbuf:pop() + + ringbuf:push("c") -- idx: 3 wraps around, overrides idx: 0 "a" + ringbuf:push("d") -- idx: 4 wraps around, overrides idx: 1 "a" + return { + peeka1 = peeka1, + peeka2 = peeka2, + pop1 = popa, + pop2 = popnil, + pop3 = ringbuf:pop(), + pop4 = ringbuf:pop(), + pop5 = ringbuf:pop(), + pop_after_add_b = pop_after_add_b, + } + ]]) + local expected = { + peeka1 = "a", + peeka2 = "a", + pop1 = "a", + pop2 = nil, + pop3 = "b", + pop4 = "c", + pop5 = "d", + pop_after_add_b = "a", + } + eq(expected, results) + end) end) describe('lua: builtin modules', function()