From 7106e2ea97d31d8ac0b5a06f35a1aaaecb61aaaa Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 31 Oct 2023 15:23:54 -0500 Subject: [PATCH 01/27] feat: add a way to cache state between restarts of Neovim This is useful for features such as `org-clock-in-last` that has the ability to persist its "knowledge" of the last clocked in task. Org mode does this by saving to a cache file on system. This commit gives nvim-orgmode the same capability. --- lua/orgmode/state/state.lua | 100 ++++++++++++++++++++++++++++++++++++ lua/orgmode/utils/init.lua | 32 +++++++++++- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 lua/orgmode/state/state.lua diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua new file mode 100644 index 000000000..7987d3a0c --- /dev/null +++ b/lua/orgmode/state/state.lua @@ -0,0 +1,100 @@ +local utils = require('orgmode.utils') +local Promise = require('orgmode.utils.promise') + +local State = { data = {}, _ctx = { loaded = false, curr_loader = nil } } + +local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) +--- Returns the current State singleton +function State:new() + -- This is done so we can later iterate the 'data' + -- subtable cleanly and shove it into a cache + setmetatable(State, { + __index = function(tbl, key) + return tbl.data[key] + end, + __newindex = function(tbl, key, value) + tbl.data[key] = value + end, + }) + -- Start trying to load the state from cache as part of initializing the state + self:load() + return self +end + +---Save the current state to cache +---@return Promise +function State:save() + --- We want to ensure the state was loaded before saving. + return self:load():next(function(_) + utils.writefile(cache_path, vim.json.encode(State.data)):next(function(_) end, function(err_msg) + vim.schedule_wrap(function() + vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) + end) + end) + end, function(_) end) +end + +---Load the state cache into the current state +---@return Promise +function State:load() + --- If we currently have a loading operation already running, return that + --- promise. This avoids a race condition of sorts as without this there's + --- potential to have two State:load operations occuring and whichever + --- finishes last sets the state. Not desirable. + if self._ctx.curr_loader ~= nil then + return self._ctx.curr_loader + end + + --- If we've already loaded the state from cache we don't need to do so again + if self._ctx.loaded then + return Promise.new(function(resolve, _) + return resolve() + end) + end + + self._ctx.curr_loader = utils.readfile(cache_path, { raw = true }):next( + function(data) + local success, decoded = pcall(vim.json.decode, data, { + luanil = { object = true, array = true }, + }) + self._ctx.curr_loader = nil + if not success then + vim.schedule(function() + vim.notify('State cache load failure, error: ' .. decoded, vim.log.levels.WARN, { + title = 'Orgmode', + }) + -- Try to 'repair' the cache by saving the current state + self:save() + end) + end + -- Because the state cache repair happens potentially after the data has + -- been added to the cache, we need to ensure the decoded table is set to + -- empty if we got an error back on the json decode operation. + if type(decoded) ~= 'table' then + decoded = {} + end + + self._ctx.loaded = true + -- It is possible that while the state was loading from cache values + -- were saved into the state. We want to preference the newer values in + -- the state and still get whatever values may not have been set in the + -- interim of the load operation. + self.data = vim.tbl_deep_extend('force', decoded, self.data) + return self + end, ---@param err string + function(err) + -- If the file didn't exist then go ahead and save + -- our current cache and as a side effect create the file + if type(err) == 'string' and err:match([[^ENOENT.*]]) then + self:save() + else + -- If the file did exist, something is wrong. Kick this to the top + error(err) + end + end + ) + + return self._ctx.curr_loader +end + +return State:new() diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index 767eee525..a6e7f25b7 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -6,7 +6,8 @@ local debounce_timers = {} local query_cache = {} local tmp_window_augroup = vim.api.nvim_create_augroup('OrgTmpWindow', { clear = true }) -function utils.readfile(file) +function utils.readfile(file, opts) + opts = vim.tbl_deep_extend('keep', opts or {}, { raw = false }) return Promise.new(function(resolve, reject) uv.fs_open(file, 'r', 438, function(err1, fd) if err1 then @@ -24,6 +25,9 @@ function utils.readfile(file) if err4 then return reject(err4) end + if opts.raw then + return resolve(data) + end local lines = vim.split(data, '\n') table.remove(lines, #lines) return resolve(lines) @@ -34,6 +38,32 @@ function utils.readfile(file) end) end +function utils.writefile(file, data) + return Promise.new(function(resolve, reject) + uv.fs_open(file, 'w', 438, function(err1, fd) + if err1 then + return reject(err1) + end + uv.fs_fstat(fd, function(err2, stat) + if err2 then + return reject(err2) + end + uv.fs_write(fd, data, nil, function(err3, bytes) + if err3 then + return reject(err3) + end + uv.fs_close(fd, function(err4) + if err4 then + return reject(err4) + end + return resolve(bytes) + end) + end) + end) + end) + end) +end + function utils.open(target) if vim.fn.executable('xdg-open') == 1 then return vim.fn.system(string.format('xdg-open %s', target)) From afdd6a71551eb2fab7318473f7f4d581b7165897 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 10:26:10 -0500 Subject: [PATCH 02/27] refactor: use `or` pattern to set default No need to do `vim.tbl_deep_extend`. kristijanhusak Nov 2, 2023: > Doing this should be enough, since nil is considered a falsy value. Co-authored-by: Kristijan Husak --- lua/orgmode/utils/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/orgmode/utils/init.lua b/lua/orgmode/utils/init.lua index a6e7f25b7..91695d5a7 100644 --- a/lua/orgmode/utils/init.lua +++ b/lua/orgmode/utils/init.lua @@ -7,7 +7,7 @@ local query_cache = {} local tmp_window_augroup = vim.api.nvim_create_augroup('OrgTmpWindow', { clear = true }) function utils.readfile(file, opts) - opts = vim.tbl_deep_extend('keep', opts or {}, { raw = false }) + opts = opts or {} return Promise.new(function(resolve, reject) uv.fs_open(file, 'r', 438, function(err1, fd) if err1 then From 3e0c1bf11942c5a2fc76b844f2a2df32ce6bba8f Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 11:10:23 -0500 Subject: [PATCH 03/27] fix: get deep copy of `State:load` error Since we instantly attempt to repair the State file by saving it, the `decoded` variable can be set again to the correct value. As such, we need to take a copy of the value to ensure it doesn't get overriden by the next load operation. --- lua/orgmode/state/state.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 7987d3a0c..ee0b7b81a 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -59,8 +59,9 @@ function State:load() }) self._ctx.curr_loader = nil if not success then + local err_msg = vim.deepcopy(decoded) vim.schedule(function() - vim.notify('State cache load failure, error: ' .. decoded, vim.log.levels.WARN, { + vim.notify('State cache load failure, error: ' .. vim.inspect(err_msg), vim.log.levels.WARN, { title = 'Orgmode', }) -- Try to 'repair' the cache by saving the current state From a08cd5a65d92f67752e0ac265c272a5505f2ac81 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 11:12:12 -0500 Subject: [PATCH 04/27] refactor: use `:catch` & `:finally` in `State:load` Instead of using `Promise.next`, it's preferred to use the `catch` and `finally` functions provided by Orgmode's Promise library. See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1380279709 > Handle the errors through the `:catch` function instead of 2nd param to `:next`. > > ```lua > utils.writefile(cache_path, vim.json.encode(State.data)):catch(function(err_msg) > vim.schedule_wrap(function() > vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) > end) > end) > ``` --- lua/orgmode/state/state.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index ee0b7b81a..90d75e277 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -25,13 +25,13 @@ end ---@return Promise function State:save() --- We want to ensure the state was loaded before saving. - return self:load():next(function(_) - utils.writefile(cache_path, vim.json.encode(State.data)):next(function(_) end, function(err_msg) + return self:load():finally(function(_) + utils.writefile(cache_path, vim.json.encode(State.data)):catch(function(err_msg) vim.schedule_wrap(function() vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) end) end) - end, function(_) end) + end) end ---Load the state cache into the current state From 6161297c6d02fb394e1fea821570d523c152fd7b Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 11:14:17 -0500 Subject: [PATCH 05/27] refactor: use more idiomatic empty `Promise` creation in `State:load` See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1380276267 > You can just return `Promise.resolve()` here. --- lua/orgmode/state/state.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 90d75e277..8ffeeb2d9 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -47,9 +47,7 @@ function State:load() --- If we've already loaded the state from cache we don't need to do so again if self._ctx.loaded then - return Promise.new(function(resolve, _) - return resolve() - end) + return Promise.resolve() end self._ctx.curr_loader = utils.readfile(cache_path, { raw = true }):next( From 281925cc342f3490532664a134b47a229578b7c5 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 12:02:16 -0500 Subject: [PATCH 06/27] refactor: make the `State` module more ergonomic for initialization This change makes it easier to test. To use the state module a downstream caller must now do "require('orgmode.state.state')()". --- lua/orgmode/state/state.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 8ffeeb2d9..afefd53aa 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -5,7 +5,7 @@ local State = { data = {}, _ctx = { loaded = false, curr_loader = nil } } local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) --- Returns the current State singleton -function State:new() +function State.new() -- This is done so we can later iterate the 'data' -- subtable cleanly and shove it into a cache setmetatable(State, { @@ -16,6 +16,7 @@ function State:new() tbl.data[key] = value end, }) + local self = State -- Start trying to load the state from cache as part of initializing the state self:load() return self @@ -96,4 +97,4 @@ function State:load() return self._ctx.curr_loader end -return State:new() +return State.new From 4ebb84b823a5a77ea87b6f3682466f9e3c7f5f2f Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 12:07:37 -0500 Subject: [PATCH 07/27] test: add initial tests for the `State` module --- tests/plenary/state/state_spec.lua | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/plenary/state/state_spec.lua diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua new file mode 100644 index 000000000..72e5cdd3a --- /dev/null +++ b/tests/plenary/state/state_spec.lua @@ -0,0 +1,59 @@ +local utils = require('orgmode.utils') +local state_mod = require('orgmode.state.state') +local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) + +describe('State', function() + before_each(function() + -- Ensure the cache file is removed before each run + vim.fn.delete(cache_path, 'rf') + end) + it("should create a state file if it doesn't exist", function() + -- Ensure the file doesn't exist + local err, stat = pcall(vim.loop.fs_stat, cache_path) + if not err then + error('Cache file existed before it should! Ensure it is deleted before each test run!') + end + + -- This creates the cache file on new instances of `State` + local state = state_mod() + state:load():finally(function() + ---@diagnostic disable-next-line: redefined-local + local err, stat = pcall(vim.loop.fs_stat, cache_path) + if err then + if type(stat) == 'string' and stat:match([[^ENOENT.*]]) then + error('Cache file did not exist') + end + end + end) + end) + + it('should save the cache file as valid json', function() + local state = state_mod() + state:save():finally(function() + utils.readfile(cache_path, { raw = true }):next(function(cache_content) + local err, err_msg = vim.json.decode(cache_content, { + luanil = { object = true, array = true }, + }) + + if err then + error('Cache file did not contain valid json after saving! Error: ' .. vim.inspect(err_msg)) + end + end) + end) + end) + + it('should be able to save and load state data', function() + local state = state_mod() + + -- Set a variable into the state object + state.my_var = 'hello world' + state:save():finally(function() + -- "Wipe" the variable + state.my_var = nil + state:load():finally(function() + -- These should be the same after the wipe. We just loaded it back in from the state cache. + assert.are.equal(state.my_var, 'hello world') + end) + end) + end) +end) From c594f2bd7d5a51d8e6bf8d66b78c0939199d3d15 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 2 Nov 2023 12:35:53 -0500 Subject: [PATCH 08/27] test: validate self-healing capability of `State` module --- tests/plenary/state/state_spec.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 72e5cdd3a..3c3a6ec0c 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -56,4 +56,18 @@ describe('State', function() end) end) end) + + it('should be able to self-heal from an invalid state file', function() + local state = state_mod() + state.my_var = 'hello world' + state:save():finally(function() + vim.cmd.edit(cache_path) + vim.api.nvim_buf_set_lines(0, 0, -1, false, { '[ invalid json!' }) + vim.cmd.write() + local err, err_msg = state:load() + if err then + error('Unable to self-heal from an invalid state! Error: ' .. vim.inspect(err_msg)) + end + end) + end) end) From 0320f93310c4d59fcf62ee3bc7e248ab67ffea9b Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Mon, 6 Nov 2023 15:59:23 -0600 Subject: [PATCH 09/27] refactor: return new instance of state from state module See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1383961635 > Since this is a singleton we should return the instance of it here `return State.new()` --- lua/orgmode/state/state.lua | 2 +- tests/plenary/state/state_spec.lua | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index afefd53aa..6ce1d6e0a 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -97,4 +97,4 @@ function State:load() return self._ctx.curr_loader end -return State.new +return State.new() diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 3c3a6ec0c..7cff9025c 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -1,10 +1,11 @@ local utils = require('orgmode.utils') -local state_mod = require('orgmode.state.state') +local state = nil local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) describe('State', function() before_each(function() -- Ensure the cache file is removed before each run + state = require("orgmode.state.state") vim.fn.delete(cache_path, 'rf') end) it("should create a state file if it doesn't exist", function() @@ -15,7 +16,6 @@ describe('State', function() end -- This creates the cache file on new instances of `State` - local state = state_mod() state:load():finally(function() ---@diagnostic disable-next-line: redefined-local local err, stat = pcall(vim.loop.fs_stat, cache_path) @@ -28,7 +28,6 @@ describe('State', function() end) it('should save the cache file as valid json', function() - local state = state_mod() state:save():finally(function() utils.readfile(cache_path, { raw = true }):next(function(cache_content) local err, err_msg = vim.json.decode(cache_content, { @@ -43,7 +42,6 @@ describe('State', function() end) it('should be able to save and load state data', function() - local state = state_mod() -- Set a variable into the state object state.my_var = 'hello world' @@ -58,7 +56,6 @@ describe('State', function() end) it('should be able to self-heal from an invalid state file', function() - local state = state_mod() state.my_var = 'hello world' state:save():finally(function() vim.cmd.edit(cache_path) From c299d579235df2f070d1578106315f0b9614786e Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Mon, 6 Nov 2023 16:01:46 -0600 Subject: [PATCH 10/27] refactor: use `catch` for more consistency with other error handlers See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1383958731 > Move this also to a :catch so it is consistent with other error handlers in the file. --- lua/orgmode/state/state.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 6ce1d6e0a..20990f484 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -81,7 +81,7 @@ function State:load() -- interim of the load operation. self.data = vim.tbl_deep_extend('force', decoded, self.data) return self - end, ---@param err string + end):catch( function(err) -- If the file didn't exist then go ahead and save -- our current cache and as a side effect create the file @@ -91,8 +91,7 @@ function State:load() -- If the file did exist, something is wrong. Kick this to the top error(err) end - end - ) + end) return self._ctx.curr_loader end From 312d519b193a79428133f31cf1a46659e449be80 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Mon, 6 Nov 2023 16:03:12 -0600 Subject: [PATCH 11/27] refactor: early return to better raise file error in state load See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1383959850 Co-authored-by: Kristijan Husak --- lua/orgmode/state/state.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 20990f484..e638c271b 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -86,11 +86,10 @@ function State:load() -- If the file didn't exist then go ahead and save -- our current cache and as a side effect create the file if type(err) == 'string' and err:match([[^ENOENT.*]]) then - self:save() - else - -- If the file did exist, something is wrong. Kick this to the top - error(err) + return self:save() end + -- If the file did exist, something is wrong. Kick this to the top + error(err) end) return self._ctx.curr_loader From 2e01ff155cb1849674e9d74863960cfc28790f9f Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Mon, 6 Nov 2023 16:05:15 -0600 Subject: [PATCH 12/27] style: format state module --- lua/orgmode/state/state.lua | 9 +++++---- tests/plenary/state/state_spec.lua | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index e638c271b..57b4178cb 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -51,8 +51,9 @@ function State:load() return Promise.resolve() end - self._ctx.curr_loader = utils.readfile(cache_path, { raw = true }):next( - function(data) + self._ctx.curr_loader = utils + .readfile(cache_path, { raw = true }) + :next(function(data) local success, decoded = pcall(vim.json.decode, data, { luanil = { object = true, array = true }, }) @@ -81,8 +82,8 @@ function State:load() -- interim of the load operation. self.data = vim.tbl_deep_extend('force', decoded, self.data) return self - end):catch( - function(err) + end) + :catch(function(err) -- If the file didn't exist then go ahead and save -- our current cache and as a side effect create the file if type(err) == 'string' and err:match([[^ENOENT.*]]) then diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 7cff9025c..9a82315e7 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -5,7 +5,7 @@ local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json' describe('State', function() before_each(function() -- Ensure the cache file is removed before each run - state = require("orgmode.state.state") + state = require('orgmode.state.state') vim.fn.delete(cache_path, 'rf') end) it("should create a state file if it doesn't exist", function() @@ -42,7 +42,6 @@ describe('State', function() end) it('should be able to save and load state data', function() - -- Set a variable into the state object state.my_var = 'hello world' state:save():finally(function() From e073ca4b148715ccd4c6ca00ee8b36cba7e8c5d6 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 14:43:21 -0600 Subject: [PATCH 13/27] fix: ensure State:load properly manages cache healing --- lua/orgmode/state/state.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 57b4178cb..3ba1593c1 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -75,7 +75,6 @@ function State:load() decoded = {} end - self._ctx.loaded = true -- It is possible that while the state was loading from cache values -- were saved into the state. We want to preference the newer values in -- the state and still get whatever values may not have been set in the @@ -87,11 +86,16 @@ function State:load() -- If the file didn't exist then go ahead and save -- our current cache and as a side effect create the file if type(err) == 'string' and err:match([[^ENOENT.*]]) then - return self:save() + self:save() + return self end -- If the file did exist, something is wrong. Kick this to the top error(err) end) + :finally(function() + self._ctx.loaded = true + self._ctx.curr_loader = nil + end) return self._ctx.curr_loader end From 37b527445c776de876c9214d1157d67b69632a24 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 16:45:10 -0600 Subject: [PATCH 14/27] feat: track State:save context in State._ctx --- lua/orgmode/state/state.lua | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 3ba1593c1..8abbe507b 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -1,7 +1,7 @@ local utils = require('orgmode.utils') local Promise = require('orgmode.utils.promise') -local State = { data = {}, _ctx = { loaded = false, curr_loader = nil } } +local State = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil } } local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) --- Returns the current State singleton @@ -25,13 +25,19 @@ end ---Save the current state to cache ---@return Promise function State:save() + State._ctx.saved = false --- We want to ensure the state was loaded before saving. - return self:load():finally(function(_) - utils.writefile(cache_path, vim.json.encode(State.data)):catch(function(err_msg) - vim.schedule_wrap(function() - vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) + return self:load():finally(function() + utils + .writefile(cache_path, vim.json.encode(State.data)) + :next(function() + State._ctx.saved = true + end) + :catch(function(err_msg) + vim.schedule_wrap(function() + vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) + end) end) - end) end) end From 4130207cac1b1b6bdd3b626d013b330d64ec8c1f Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 16:46:02 -0600 Subject: [PATCH 15/27] test: ensure needed vim stdpaths are created on startup --- tests/minimal_init.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 4d3eda9fb..3b8726f0b 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -65,6 +65,16 @@ function M.setup(plugins) vim.env.XDG_STATE_HOME = M.root('xdg/state') vim.env.XDG_CACHE_HOME = M.root('xdg/cache') + local std_paths = { + 'cache', + 'data', + 'config', + } + + for _, std_path in pairs(std_paths) do + vim.fn.mkdir(vim.fn.stdpath(std_path), 'p') + end + -- NOTE: Cleanup the xdg cache on exit so new runs of the minimal init doesn't share any previous state, e.g. shada vim.api.nvim_create_autocmd('VimLeave', { callback = function() From 76beae344e8905dcacc22d8366365990964f026c Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 17:02:38 -0600 Subject: [PATCH 16/27] test: correctly handle async testing of the State module All tests should work as expected now. --- tests/plenary/state/state_spec.lua | 114 ++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 9a82315e7..115378999 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -6,64 +6,112 @@ describe('State', function() before_each(function() -- Ensure the cache file is removed before each run state = require('orgmode.state.state') + vim.wait(50, function() + return state._ctx.saved + end, 10) + state._ctx.saved = false + state._ctx.loaded = false vim.fn.delete(cache_path, 'rf') end) + + after_each(function() + state._ctx.saved = false + state._ctx.loaded = false + state = nil + -- vim.fn.delete(cache_path, 'rf') + end) + it("should create a state file if it doesn't exist", function() -- Ensure the file doesn't exist - local err, stat = pcall(vim.loop.fs_stat, cache_path) - if not err then + vim.fn.delete(cache_path, 'rf') + local stat = vim.loop.fs_stat(cache_path) + if stat then error('Cache file existed before it should! Ensure it is deleted before each test run!') end -- This creates the cache file on new instances of `State` - state:load():finally(function() - ---@diagnostic disable-next-line: redefined-local - local err, stat = pcall(vim.loop.fs_stat, cache_path) - if err then - if type(stat) == 'string' and stat:match([[^ENOENT.*]]) then - error('Cache file did not exist') - end - end - end) + state:load() + + -- wait until the state has been saved + vim.wait(50, function() + return state._ctx.saved + end, 10) + + local stat, err, _ = vim.loop.fs_stat(cache_path) + if not stat then + error(err) + end end) it('should save the cache file as valid json', function() - state:save():finally(function() - utils.readfile(cache_path, { raw = true }):next(function(cache_content) - local err, err_msg = vim.json.decode(cache_content, { - luanil = { object = true, array = true }, - }) - - if err then - error('Cache file did not contain valid json after saving! Error: ' .. vim.inspect(err_msg)) - end - end) + local data = nil + local read_f_err = nil + state:save():next(function() + utils + .readfile(cache_path, { raw = true }) + :next(function(state_data) + data = state_data + end) + :catch(function(err) + read_f_err = err + end) end) + + -- wait until the newly saved state file has been read + vim.wait(50, function() + return data ~= nil or read_f_err ~= nil + end, 10) + if read_f_err then + error(read_f_err) + end + + local success, decoded = pcall(vim.json.decode, data, { + luanil = { object = true, array = true }, + }) + if not success then + error('Cache file did not contain valid json after saving! Error: ' .. vim.inspect(decoded)) + end end) it('should be able to save and load state data', function() -- Set a variable into the state object state.my_var = 'hello world' - state:save():finally(function() - -- "Wipe" the variable - state.my_var = nil - state:load():finally(function() - -- These should be the same after the wipe. We just loaded it back in from the state cache. - assert.are.equal(state.my_var, 'hello world') - end) - end) + -- Save the state + state:save() + vim.wait(50, function() + return state._ctx.saved + end, 10) + -- Wipe the variable and "unload" the State + state.my_var = nil + state._ctx.loaded = false + + -- Ensure the state can be loaded from the file now by ignoring the previous load + state:load() + -- wait until the state has been loaded + vim.wait(50, function() + return state._ctx.loaded + end, 10) + -- These should be the same after the wipe. We just loaded it back in from the state cache. + assert.are.equal('hello world', state.my_var) end) it('should be able to self-heal from an invalid state file', function() + local err = nil state.my_var = 'hello world' state:save():finally(function() vim.cmd.edit(cache_path) vim.api.nvim_buf_set_lines(0, 0, -1, false, { '[ invalid json!' }) vim.cmd.write() - local err, err_msg = state:load() - if err then - error('Unable to self-heal from an invalid state! Error: ' .. vim.inspect(err_msg)) - end + state:load():catch(function(err) + err = err + end) end) + vim.wait(50, function() + return state._ctx.loaded + end, 10) + + if err then + error('Unable to self-heal from an invalid state! Error: ' .. vim.inspect(err_msg)) + end end) end) From d5c7f8f1dbbac567168ef8b80e5bfc6b0ad3849d Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 17:04:34 -0600 Subject: [PATCH 17/27] refactor: use echo_warning for notifications in the State module --- lua/orgmode/state/state.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 8abbe507b..57a543761 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -35,7 +35,7 @@ function State:save() end) :catch(function(err_msg) vim.schedule_wrap(function() - vim.notify('Failed to save current state! Error: ' .. err_msg, vim.log.levels.WARN, { title = 'Orgmode' }) + utils.echo_warning('Failed to save current state! Error: ' .. err_msg) end) end) end) @@ -67,9 +67,7 @@ function State:load() if not success then local err_msg = vim.deepcopy(decoded) vim.schedule(function() - vim.notify('State cache load failure, error: ' .. vim.inspect(err_msg), vim.log.levels.WARN, { - title = 'Orgmode', - }) + utils.echo_warning('State cache load failure, error: ' .. vim.inspect(err_msg)) -- Try to 'repair' the cache by saving the current state self:save() end) From 056191f3418d0974bec727e3b66e4a2f37f38c3f Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Tue, 7 Nov 2023 19:16:30 -0600 Subject: [PATCH 18/27] fix: make chaining off State:save correctly wait until file save --- lua/orgmode/state/state.lua | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 57a543761..9a5212f49 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -27,18 +27,17 @@ end function State:save() State._ctx.saved = false --- We want to ensure the state was loaded before saving. - return self:load():finally(function() - utils - .writefile(cache_path, vim.json.encode(State.data)) - :next(function() - State._ctx.saved = true - end) - :catch(function(err_msg) - vim.schedule_wrap(function() - utils.echo_warning('Failed to save current state! Error: ' .. err_msg) - end) + self:load() + return utils + .writefile(cache_path, vim.json.encode(State.data)) + :next(function() + State._ctx.saved = true + end) + :catch(function(err_msg) + vim.schedule_wrap(function() + utils.echo_warning('Failed to save current state! Error: ' .. err_msg) end) - end) + end) end ---Load the state cache into the current state From de1ad28e3e97347e89644016d2a86b97753ae307 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 15:54:23 -0600 Subject: [PATCH 19/27] feat: add synchronous wrappers for State:load & State:sync --- lua/orgmode/state/state.lua | 42 ++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 9a5212f49..58d2aa975 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -40,6 +40,16 @@ function State:save() end) end +---Synchronously save the state into cache +---@param timeout? number How long to wait for the save operation +function State:save_sync(timeout) + State._ctx.saved = false + self:save() + vim.wait(timeout or 500, function() + return State._ctx.saved + end, 20) +end + ---Load the state cache into the current state ---@return Promise function State:load() @@ -53,7 +63,7 @@ function State:load() --- If we've already loaded the state from cache we don't need to do so again if self._ctx.loaded then - return Promise.resolve() + return Promise.resolve(self) end self._ctx.curr_loader = utils @@ -103,4 +113,34 @@ function State:load() return self._ctx.curr_loader end +---Synchronously load the state from cache if it hasn't been loaded +---@param timeout? number How long to wait for the cache load before erroring +---@return State +function State:load_sync(timeout) + local state + local err + self + :load() + :next(function(loaded_state) + state = loaded_state + end) + :catch(function(reject) + err = reject + end) + + vim.wait(timeout or 500, function() + return state ~= nil or err ~= nil + end, 20) + + if err then + error(err) + end + + if err == nil and state == nil then + error('Did not load State in time') + end + + return state +end + return State.new() From d533595a338f21905624f8479da43ad8e967f11c Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 16:01:38 -0600 Subject: [PATCH 20/27] docs: update annotation for State.new --- lua/orgmode/state/state.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 58d2aa975..230a82f7a 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -4,7 +4,9 @@ local Promise = require('orgmode.utils.promise') local State = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil } } local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) ---- Returns the current State singleton + +---Returns the current State singleton +---@return State function State.new() -- This is done so we can later iterate the 'data' -- subtable cleanly and shove it into a cache From 389ba27b6b0947966242813a6cc00a9a6be30a48 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 16:33:35 -0600 Subject: [PATCH 21/27] fix: ensure save state is correctly tracked It's possible to have multiple save operations happening at once. We only want to declare the context as "saved" when all savers are finished. --- lua/orgmode/state/state.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 230a82f7a..a2ea80478 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -1,7 +1,7 @@ local utils = require('orgmode.utils') local Promise = require('orgmode.utils.promise') -local State = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil } } +local State = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil, savers = 0 } } local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) @@ -30,12 +30,17 @@ function State:save() State._ctx.saved = false --- We want to ensure the state was loaded before saving. self:load() + self._ctx.savers = self._ctx.savers + 1 return utils .writefile(cache_path, vim.json.encode(State.data)) :next(function() - State._ctx.saved = true + self._ctx.savers = self._ctx.savers - 1 + if self._ctx.savers == 0 then + State._ctx.saved = true + end end) :catch(function(err_msg) + self._ctx.savers = self._ctx.savers - 1 vim.schedule_wrap(function() utils.echo_warning('Failed to save current state! Error: ' .. err_msg) end) From 751b5dd70aa0cd8c791bf32e54d3b8f6b37332fb Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 16:34:35 -0600 Subject: [PATCH 22/27] feat: add State:wipe function to State module --- lua/orgmode/state/state.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index a2ea80478..401cac6be 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -150,4 +150,18 @@ function State:load_sync(timeout) return state end +---Reset the current state to empty +---@param overwrite? boolean Whether or not the cache should also be wiped +function State:wipe(overwrite) + overwrite = overwrite or false + + self.data = {} + self._ctx.curr_loader = nil + self._ctx.loaded = false + self._ctx.saved = false + if overwrite then + state:save_sync() + end +end + return State.new() From d74f13b1b3c0fe2bca8d4bd5c1b5b4466f45f3c6 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 16:37:34 -0600 Subject: [PATCH 23/27] test: update state_spec to use new State functions --- tests/plenary/state/state_spec.lua | 75 ++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 115378999..02cc8ca2e 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -9,8 +9,7 @@ describe('State', function() vim.wait(50, function() return state._ctx.saved end, 10) - state._ctx.saved = false - state._ctx.loaded = false + state:wipe() vim.fn.delete(cache_path, 'rf') end) @@ -77,41 +76,67 @@ describe('State', function() -- Set a variable into the state object state.my_var = 'hello world' -- Save the state - state:save() - vim.wait(50, function() - return state._ctx.saved - end, 10) + state:save_sync() -- Wipe the variable and "unload" the State state.my_var = nil state._ctx.loaded = false -- Ensure the state can be loaded from the file now by ignoring the previous load - state:load() - -- wait until the state has been loaded - vim.wait(50, function() - return state._ctx.loaded - end, 10) + state:load_sync() -- These should be the same after the wipe. We just loaded it back in from the state cache. assert.are.equal('hello world', state.my_var) end) it('should be able to self-heal from an invalid state file', function() - local err = nil - state.my_var = 'hello world' - state:save():finally(function() - vim.cmd.edit(cache_path) - vim.api.nvim_buf_set_lines(0, 0, -1, false, { '[ invalid json!' }) - vim.cmd.write() - state:load():catch(function(err) - err = err - end) + state:save_sync() + + -- Mangle the cache + vim.cmd.edit(cache_path) + vim.api.nvim_buf_set_lines(0, 0, -1, false, { '[ invalid json!' }) + vim.cmd.write() + + -- Ensure we reload the state from its cache file (this should also "heal" the cache) + state._ctx.loaded = false + state._ctx.saved = false + state:load_sync() + vim.wait(500, function() + return state._ctx.saved end) - vim.wait(50, function() - return state._ctx.loaded - end, 10) - if err then - error('Unable to self-heal from an invalid state! Error: ' .. vim.inspect(err_msg)) + -- Now attempt to read the file and check that it is, in fact, "healed" + local cache_data = nil + local read_f_err = nil + utils + .readfile(cache_path, { raw = true }) + :next(function(state_data) + cache_data = state_data + end) + :catch(function(reject) + read_f_err = reject + end) + :finally(function() + read_file = true + end) + + vim.wait(500, function() + return cache_data ~= nil or read_f_err ~= nil + end, 20) + + if read_f_err then + error('Unable to read the healed state cache! Error: ' .. vim.inspect(read_f_err)) + end + + local success, decoded = pcall(vim.json.decode, cache_data, { + luanil = { object = true, array = true }, + }) + + if not success then + error( + 'Unable to self-heal from an invalid state! Error: ' + .. vim.inspect(decoded) + .. '\n\t-> Got cache content as ' + .. vim.inspect(cache_data) + ) end end) end) From 4f5d9c220308388806fad635cb01bd9d2bcf2517 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Wed, 8 Nov 2023 17:00:27 -0600 Subject: [PATCH 24/27] test: wait for cache to be fully saved in self-healing test --- tests/plenary/state/state_spec.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 02cc8ca2e..e88746a97 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -102,6 +102,8 @@ describe('State', function() vim.wait(500, function() return state._ctx.saved end) + -- Wait a little longer to ensure the data is flushed into the cache + vim.wait(100) -- Now attempt to read the file and check that it is, in fact, "healed" local cache_data = nil From a0e8d3e7fbf7b335ff688fac888f3f276ef8b74e Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 9 Nov 2023 03:20:45 -0600 Subject: [PATCH 25/27] refactor: rename `State` -> `OrgState` with annotation See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1387688890 Co-authored-by: Kristijan Husak --- lua/orgmode/state/state.lua | 43 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lua/orgmode/state/state.lua b/lua/orgmode/state/state.lua index 401cac6be..d29ff022f 100644 --- a/lua/orgmode/state/state.lua +++ b/lua/orgmode/state/state.lua @@ -1,16 +1,17 @@ local utils = require('orgmode.utils') local Promise = require('orgmode.utils.promise') -local State = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil, savers = 0 } } +---@class OrgState +local OrgState = { data = {}, _ctx = { loaded = false, saved = false, curr_loader = nil, savers = 0 } } local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false }) ----Returns the current State singleton ----@return State -function State.new() +---Returns the current OrgState singleton +---@return OrgState +function OrgState.new() -- This is done so we can later iterate the 'data' -- subtable cleanly and shove it into a cache - setmetatable(State, { + setmetatable(OrgState, { __index = function(tbl, key) return tbl.data[key] end, @@ -18,7 +19,7 @@ function State.new() tbl.data[key] = value end, }) - local self = State + local self = OrgState -- Start trying to load the state from cache as part of initializing the state self:load() return self @@ -26,17 +27,17 @@ end ---Save the current state to cache ---@return Promise -function State:save() - State._ctx.saved = false +function OrgState:save() + OrgState._ctx.saved = false --- We want to ensure the state was loaded before saving. self:load() self._ctx.savers = self._ctx.savers + 1 return utils - .writefile(cache_path, vim.json.encode(State.data)) + .writefile(cache_path, vim.json.encode(OrgState.data)) :next(function() self._ctx.savers = self._ctx.savers - 1 if self._ctx.savers == 0 then - State._ctx.saved = true + OrgState._ctx.saved = true end end) :catch(function(err_msg) @@ -49,20 +50,20 @@ end ---Synchronously save the state into cache ---@param timeout? number How long to wait for the save operation -function State:save_sync(timeout) - State._ctx.saved = false +function OrgState:save_sync(timeout) + OrgState._ctx.saved = false self:save() vim.wait(timeout or 500, function() - return State._ctx.saved + return OrgState._ctx.saved end, 20) end ---Load the state cache into the current state ---@return Promise -function State:load() +function OrgState:load() --- If we currently have a loading operation already running, return that --- promise. This avoids a race condition of sorts as without this there's - --- potential to have two State:load operations occuring and whichever + --- potential to have two OrgState:load operations occuring and whichever --- finishes last sets the state. Not desirable. if self._ctx.curr_loader ~= nil then return self._ctx.curr_loader @@ -83,7 +84,7 @@ function State:load() if not success then local err_msg = vim.deepcopy(decoded) vim.schedule(function() - utils.echo_warning('State cache load failure, error: ' .. vim.inspect(err_msg)) + utils.echo_warning('OrgState cache load failure, error: ' .. vim.inspect(err_msg)) -- Try to 'repair' the cache by saving the current state self:save() end) @@ -122,8 +123,8 @@ end ---Synchronously load the state from cache if it hasn't been loaded ---@param timeout? number How long to wait for the cache load before erroring ----@return State -function State:load_sync(timeout) +---@return OrgState +function OrgState:load_sync(timeout) local state local err self @@ -144,7 +145,7 @@ function State:load_sync(timeout) end if err == nil and state == nil then - error('Did not load State in time') + error('Did not load OrgState in time') end return state @@ -152,7 +153,7 @@ end ---Reset the current state to empty ---@param overwrite? boolean Whether or not the cache should also be wiped -function State:wipe(overwrite) +function OrgState:wipe(overwrite) overwrite = overwrite or false self.data = {} @@ -164,4 +165,4 @@ function State:wipe(overwrite) end end -return State.new() +return OrgState.new() From 6407b0e330250600621d7788b8b6199a2ba3e965 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 9 Nov 2023 03:22:24 -0600 Subject: [PATCH 26/27] test: remove unnecessary logic in `State` testing See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1387686363 See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1387686363 See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1387691685 Co-authored-by: Kristijan Husak --- tests/plenary/state/state_spec.lua | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index e88746a97..606911563 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -6,23 +6,11 @@ describe('State', function() before_each(function() -- Ensure the cache file is removed before each run state = require('orgmode.state.state') - vim.wait(50, function() - return state._ctx.saved - end, 10) state:wipe() vim.fn.delete(cache_path, 'rf') end) - after_each(function() - state._ctx.saved = false - state._ctx.loaded = false - state = nil - -- vim.fn.delete(cache_path, 'rf') - end) - it("should create a state file if it doesn't exist", function() - -- Ensure the file doesn't exist - vim.fn.delete(cache_path, 'rf') local stat = vim.loop.fs_stat(cache_path) if stat then error('Cache file existed before it should! Ensure it is deleted before each test run!') From 54bbf8865f0396b607dd6023f1ddc3ffbed63157 Mon Sep 17 00:00:00 2001 From: Price Hiller Date: Thu, 9 Nov 2023 03:27:15 -0600 Subject: [PATCH 27/27] test: add annotation for `state` in state_spec See https://github.com/nvim-orgmode/orgmode/pull/624#discussion_r1387688890 Co-authored-by: Kristijan Husak --- tests/plenary/state/state_spec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/plenary/state/state_spec.lua b/tests/plenary/state/state_spec.lua index 606911563..dc16b859d 100644 --- a/tests/plenary/state/state_spec.lua +++ b/tests/plenary/state/state_spec.lua @@ -1,4 +1,5 @@ local utils = require('orgmode.utils') +---@type OrgState local state = nil local cache_path = vim.fs.normalize(vim.fn.stdpath('cache') .. '/org-cache.json', { expand_env = false })